diff --git a/conversation-repair.mjs b/conversation-repair.mjs index 0f62f1f..a827496 100644 --- a/conversation-repair.mjs +++ b/conversation-repair.mjs @@ -4,6 +4,7 @@ */ import { extractConversationMessageText } from './conversation-memory.mjs'; +import { mergeConversationSnapshot } from './chat-finish-sync.mjs'; export function parseStoredConversationRow(row) { if (!row) return null; @@ -215,6 +216,15 @@ export function repairConversationFromDbRows(gooseMessages, dbRows) { return Array.isArray(gooseMessages) ? gooseMessages : []; } if (dbBuilt.length === 0) return Array.isArray(gooseMessages) ? gooseMessages : []; + const goose = Array.isArray(gooseMessages) ? gooseMessages : []; + const dbIds = new Set(dbBuilt.map((message) => message?.id).filter(Boolean)); + const hasSharedAnchor = goose.some((message) => message?.id && dbIds.has(message.id)); + if (hasSharedAnchor) { + // A recovered Goose session can contain only the last DB-backed user turn + // plus replies created after rotation. Prefer DB content for shared history, + // while retaining every Goose-only reply and follow-up around that anchor. + return mergeConversationSnapshot(goose, dbBuilt); + } return dbBuilt; } diff --git a/conversation-repair.test.mjs b/conversation-repair.test.mjs index a4b1070..5aecb08 100644 --- a/conversation-repair.test.mjs +++ b/conversation-repair.test.mjs @@ -69,6 +69,70 @@ test('shouldRepairConversationFromDb when goose has empty placeholders', () => { assert.match(repaired[1].content[0].text, /中考/); }); +test('repairConversationFromDbRows retains the fresh Goose tail after session recovery', () => { + const goose = [ + gooseMsg('recovery-user', 'user', '【Memind 任务编排】继续修改页面'), + gooseMsg('fresh-assistant', 'assistant', '第一轮修改已完成'), + gooseMsg('followup-user', 'user', '再加一个页脚'), + gooseMsg('followup-assistant', 'assistant', '页脚也已完成'), + ]; + const dbRows = [ + { + message_key: 'old-user', + role: 'user', + text: '写一首诗并做成页面', + created_at: 1, + sequence_no: 0, + }, + { + message_key: 'old-assistant', + role: 'assistant', + text: '初版页面已完成', + created_at: 2, + sequence_no: 1, + }, + { + message_key: 'old-followup-user', + role: 'user', + text: '把标题改成金色', + created_at: 3, + sequence_no: 2, + }, + { + message_key: 'old-followup-assistant', + role: 'assistant', + text: '标题已改成金色', + created_at: 4, + sequence_no: 3, + }, + { + message_key: 'recovery-user', + role: 'user', + text: '继续修改页面', + created_at: 5, + sequence_no: 4, + }, + ]; + + const repaired = repairConversationFromDbRows(goose, dbRows); + + assert.deepEqual( + repaired.map((message) => message.id), + [ + 'old-user', + 'old-assistant', + 'old-followup-user', + 'old-followup-assistant', + 'recovery-user', + 'fresh-assistant', + 'followup-user', + 'followup-assistant', + ], + ); + assert.equal(repaired[4].content[0].text, '继续修改页面'); + assert.equal(repaired[7].content[0].text, '页脚也已完成'); +}); + test('shouldRepairConversationFromDb returns false when goose is complete', () => { const goose = [ gooseMsg('u1', 'user', 'hello'), diff --git a/docs/regression-guards/mindspace-publish-and-chat-finish.md b/docs/regression-guards/mindspace-publish-and-chat-finish.md index 1dde04c..82066c3 100644 --- a/docs/regression-guards/mindspace-publish-and-chat-finish.md +++ b/docs/regression-guards/mindspace-publish-and-chat-finish.md @@ -211,6 +211,19 @@ runtime 也必须和发布 manifest 的 git commit 对齐。 - 源码静态检查:`npm run verify:chat-finish-sync` - 文档:本文件 +### 恢复会话快照:禁止用旧 DB 历史覆盖新回复 + +视觉上下文不兼容等故障会把 Agent Run 旋转到新 Goose session。新 session +可能只包含最后一条数据库已同步的用户消息,以及旋转后新生成的助手回复和后续追问; +此时数据库旧历史条数通常更多。 + +- `conversation-repair.mjs` 发现 Goose 与 DB 存在共同消息 ID 时,必须以该 ID + 为锚点合并:共享历史采用 DB 内容,同时保留 Goose 独有的新回复、后续追问和修改结果 +- 只有双方没有共同消息 ID,且 Goose 确实是空占位/残缺记录时,才允许用 DB 历史整体修复 +- Portal 保存 session snapshot 前不得把“当前同步计数”与“旧 messages_json”组合成伪最新快照 + +守卫:`conversation-repair.test.mjs` 中的 session recovery fresh-tail 用例。 + --- ## 3. 页面需求澄清:禁止误报“未生成 public HTML”