From dc1a32a52b70fde443a4d7c003847aa018c8cfb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=86=B0=E6=9C=94?= <565183519@qq.com> Date: Wed, 12 Aug 2026 07:18:46 +0800 Subject: [PATCH] fix(hololake): avoid no-op agent workspace writes --- .../internal-release-packaging/SKILL.md | 2 ++ .../scripts/local-native-candidate.test.mjs | 2 +- .../src/components/AiWorkspace.test.tsx | 24 +++++++++++++++++++ .../components/aiWorkspaceConversations.ts | 24 ++++++++++++++++--- .../src/lib/aiWorkspaceSessionStore.test.ts | 24 +++++++++++++++++++ .../src/lib/aiWorkspaceSessionStore.ts | 1 - 6 files changed, 72 insertions(+), 5 deletions(-) diff --git a/product-source/hololake-platform/docs/skills/internal-release-packaging/SKILL.md b/product-source/hololake-platform/docs/skills/internal-release-packaging/SKILL.md index 62ca4d1..f20b6e4 100644 --- a/product-source/hololake-platform/docs/skills/internal-release-packaging/SKILL.md +++ b/product-source/hololake-platform/docs/skills/internal-release-packaging/SKILL.md @@ -54,6 +54,8 @@ pnpm package:internal:windows 内测 Mac 包是 ad-hoc 签名、未公证;Windows 包在没有商业证书时未做 Authenticode 签名。交付时必须明确说明,不能描述成正式签名发行版。 +`tauri.local-candidate.conf.json` 的独立 bundle identifier 只保证候选应用可以与已安装版本并存,不隔离运行数据。当前架构仍从共享的 `com.tolaria.app` 命名空间读取设置、挂载知识库和 Agent 会话。需要无扰动验收时,必须用临时 `XDG_CONFIG_HOME` 启动候选,并在临时配置副本上验证;不能把“没有点击保存”当成“没有写盘”。 + ## Windows 构建恢复顺序 1. `pnpm install --frozen-lockfile` diff --git a/product-source/hololake-platform/scripts/local-native-candidate.test.mjs b/product-source/hololake-platform/scripts/local-native-candidate.test.mjs index 2772cb8..5b26324 100644 --- a/product-source/hololake-platform/scripts/local-native-candidate.test.mjs +++ b/product-source/hololake-platform/scripts/local-native-candidate.test.mjs @@ -4,7 +4,7 @@ import test from 'node:test' const readJson = async path => JSON.parse(await readFile(path, 'utf8')) -test('local native candidate has an isolated macOS identity', async () => { +test('local native candidate has an isolated macOS package identity', async () => { const [base, candidate, packageJson] = await Promise.all([ readJson('src-tauri/tauri.conf.json'), readJson('src-tauri/tauri.local-candidate.conf.json'), diff --git a/product-source/hololake-platform/src/components/AiWorkspace.test.tsx b/product-source/hololake-platform/src/components/AiWorkspace.test.tsx index b4d2ae2..a3293eb 100644 --- a/product-source/hololake-platform/src/components/AiWorkspace.test.tsx +++ b/product-source/hololake-platform/src/components/AiWorkspace.test.tsx @@ -360,6 +360,30 @@ describe('AiWorkspace', () => { expect(screen.queryByRole('button', { name: 'Current work' })).toBeNull() }) + it('does not rewrite unchanged conversation settings when the workspace opens', async () => { + const onConversationSettingsChange = vi.fn() + + render( + , + ) + + await waitFor(() => { + expect(screen.getByRole('button', { name: 'Current work' })).toBeTruthy() + }) + expect(onConversationSettingsChange).not.toHaveBeenCalled() + }) + it('recovers stored sessions that no longer have conversation metadata', async () => { setAiWorkspaceSessionMessages('orphaned-chat', [{ userMessage: 'Recovered prompt', diff --git a/product-source/hololake-platform/src/components/aiWorkspaceConversations.ts b/product-source/hololake-platform/src/components/aiWorkspaceConversations.ts index eaad66b..215e398 100644 --- a/product-source/hololake-platform/src/components/aiWorkspaceConversations.ts +++ b/product-source/hololake-platform/src/components/aiWorkspaceConversations.ts @@ -197,6 +197,20 @@ function conversationsToSettings(conversations: AiConversation[]): AiWorkspaceCo })) } +function conversationSettingsEqual( + current: AiWorkspaceConversationSetting[], + stored: AiWorkspaceConversationSetting[] | null | undefined, +): boolean { + if (!stored || current.length !== stored.length) return false + return current.every((conversation, index) => { + const persisted = stored[index] + return conversation.archived === (persisted.archived === true) + && conversation.id === persisted.id + && conversation.target_id === (persisted.target_id?.trim() || null) + && conversation.title === persisted.title + }) +} + export function activeConversationForState( conversations: AiConversation[], activeId: ConversationId, @@ -370,10 +384,12 @@ function initialActiveId(conversations: AiConversation[], requestedId: Conversat function useConversationSettingsPersistence({ conversations, onSettingsChange, + settings, settingsReady, }: { conversations: AiConversation[] onSettingsChange?: (conversations: AiWorkspaceConversationSetting[]) => void + settings?: AiWorkspaceConversationSetting[] | null settingsReady: boolean }) { const onSettingsChangeRef = useRef(onSettingsChange) @@ -384,8 +400,10 @@ function useConversationSettingsPersistence({ useEffect(() => { if (!settingsReady) return - onSettingsChangeRef.current?.(conversationsToSettings(conversations)) - }, [conversations, settingsReady]) + const nextSettings = conversationsToSettings(conversations) + if (conversationSettingsEqual(nextSettings, settings)) return + onSettingsChangeRef.current?.(nextSettings) + }, [conversations, settings, settingsReady]) } function useTitleConversationFromAnswer(setConversations: SetConversations) { @@ -490,7 +508,7 @@ export function useConversations({ const titleConversationFromAnswer = useTitleConversationFromAnswer(setConversations) const updateDefaultConversationTargets = useUpdateDefaultConversationTargets(setConversations) - useConversationSettingsPersistence({ conversations, onSettingsChange, settingsReady }) + useConversationSettingsPersistence({ conversations, onSettingsChange, settings, settingsReady }) return { activeId, addConversation, archiveConversation, closeConversation, conversations, deleteConversation, forkConversation, diff --git a/product-source/hololake-platform/src/lib/aiWorkspaceSessionStore.test.ts b/product-source/hololake-platform/src/lib/aiWorkspaceSessionStore.test.ts index e91238f..4d74c1b 100644 --- a/product-source/hololake-platform/src/lib/aiWorkspaceSessionStore.test.ts +++ b/product-source/hololake-platform/src/lib/aiWorkspaceSessionStore.test.ts @@ -178,4 +178,28 @@ describe('aiWorkspaceSessionStore', () => { expect(store.aiWorkspaceSessionSnapshot('native-chat').messages).toHaveLength(1) }) }) + + it('does not rewrite native history when hydration only reads it', async () => { + isTauriState.value = true + invokeMock.mockImplementation(async (command: string) => { + if (command === 'get_ai_workspace_sessions') { + return { + 'native-chat': { + messages: [{ userMessage: 'Native', actions: [], response: 'History', id: 'native-message' }], + status: 'done', + }, + } + } + return undefined + }) + + const store = await import('./aiWorkspaceSessionStore') + + await vi.waitFor(() => { + expect(store.aiWorkspaceSessionSnapshot('native-chat').messages).toHaveLength(1) + }) + await new Promise(resolve => setTimeout(resolve, 300)) + + expect(invokeMock).not.toHaveBeenCalledWith('save_ai_workspace_sessions', expect.anything()) + }) }) diff --git a/product-source/hololake-platform/src/lib/aiWorkspaceSessionStore.ts b/product-source/hololake-platform/src/lib/aiWorkspaceSessionStore.ts index dc94fe3..e199d38 100644 --- a/product-source/hololake-platform/src/lib/aiWorkspaceSessionStore.ts +++ b/product-source/hololake-platform/src/lib/aiWorkspaceSessionStore.ts @@ -160,7 +160,6 @@ async function syncFromNativeStorage(): Promise { const mergedSessions = mergeStoredSessions(sessionStore.getSnapshot(), nativeSessions) sessionStore.replaceSnapshot(mergedSessions) sessionStore.writeStoredSnapshot(mergedSessions) - if (Object.keys(mergedSessions).length > 0) scheduleNativeSessionsWrite(mergedSessions) } function ensureSessionStoreSync(): void {