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 {