fix: scope observation to current turn

Require assistant natural language after the latest user message so a completed prompt cannot reuse historical replies when queuing the observation snapshot.
This commit is contained in:
brother7 committed 2026-08-24 12:55:01 +08:00
1 parent 7a15b1b49c
commit 16f5c57f17
3 files changed
+65 -7

No files matched your search

@@ -120,13 +120,14 @@
noise remains. noise remains.
- `pnpm run typecheck`: passed, including after the pinned-pnpm dependency rebuild. - `pnpm run typecheck`: passed, including after the pinned-pnpm dependency rebuild.
- `pnpm exec eslint . --quiet`: passed. - `pnpm exec eslint . --quiet`: passed.
- Full unit suite after planner fixes: 174 files passed; 1495 tests passed, - Full unit suite after planner fixes: 174 files passed; 1496 tests passed,
2 skipped (1497 total). 2 skipped (1498 total).
- Pressure suite: 1 file and 1 test passed. - Pressure suite: 1 file and 1 test passed.
- Focused post-review suite: 3 files and 33 tests passed for migration, - Focused post-review suite: 3 files and 34 tests passed for migration,
session-observation sync, and Conversation store settlement behavior. It covers session-observation sync, and Conversation store settlement behavior. It covers
valid-v2 no-legacy-read, unknown/modified Agent backup, live ordinary-prompt valid-v2 no-legacy-read, unknown/modified Agent backup, live ordinary-prompt
settlement, no hydrate/follow-up replay, and assistant-natural-language gating. settlement, no hydrate/follow-up replay, and current-turn assistant natural-
language gating that cannot reuse an older assistant reply.
- Electron E2E: 3 tests passed for first Pi conversation latency/submit ordering, - Electron E2E: 3 tests passed for first Pi conversation latency/submit ordering,
Pi product UI, and bundled Skill project configuration. Pi product UI, and bundled Skill project configuration.
- `pnpm run build:vite`: passed (Renderer, Electron Main, Preload, and utility). - `pnpm run build:vite`: passed (Renderer, Electron Main, Preload, and utility).
+21 -3
View File
@@ -363,8 +363,22 @@ export function queueAgentSessionSync(
/** Queue the final product Conversation snapshot after a completed prompt turn. */ /** Queue the final product Conversation snapshot after a completed prompt turn. */
export function queueCodingConversationSessionSync(snapshot: ConversationSnapshot): void { export function queueCodingConversationSessionSync(snapshot: ConversationSnapshot): void {
const messages: RawMessage[] = snapshot.nodes.flatMap((node) => { const messageNodes = snapshot.nodes.filter((node) => node.kind === 'message');
if (node.kind !== 'message') return []; let lastUserIndex = -1;
for (let index = messageNodes.length - 1; index >= 0; index -= 1) {
if (messageNodes[index].role === 'user') {
lastUserIndex = index;
break;
}
}
if (lastUserIndex < 0) return;
const currentTurnAssistantIds = new Set(messageNodes
.slice(lastUserIndex + 1)
.filter((node) => node.role === 'assistant')
.map((node) => node.id));
if (currentTurnAssistantIds.size === 0) return;
const messages: RawMessage[] = messageNodes.flatMap((node) => {
return [{ return [{
id: node.id, id: node.id,
role: node.role, role: node.role,
@@ -384,7 +398,11 @@ export function queueCodingConversationSessionSync(snapshot: ConversationSnapsho
messages, messages,
updatedAt, updatedAt,
); );
if (!data?.messages.some((message) => message.role === 'assistant')) return; if (!data?.messages.some((message) => (
message.role === 'assistant'
&& typeof message.id === 'string'
&& currentTurnAssistantIds.has(message.id)
))) return;
queueAgentSessionData(data); queueAgentSessionData(data);
} }
+39
View File
@@ -220,6 +220,45 @@ describe('local Agent session sync', () => {
expect(pushSessionDataMock).not.toHaveBeenCalled(); expect(pushSessionDataMock).not.toHaveBeenCalled();
}); });
it('does not reuse an older assistant reply when the current turn has no reply', async () => {
const base = createProductSnapshot('conversation-1');
queueCodingConversationSessionSync({
...base,
nodes: [
{
kind: 'message',
id: 'old-user',
role: 'user',
status: 'complete',
blocks: [{ kind: 'text', id: 'old-question', text: '旧问题', status: 'complete' }],
},
{
kind: 'message',
id: 'old-assistant',
role: 'assistant',
status: 'complete',
blocks: [{ kind: 'text', id: 'old-answer', text: '旧回答', status: 'complete' }],
},
{
kind: 'message',
id: 'current-user',
role: 'user',
status: 'complete',
blocks: [{ kind: 'text', id: 'current-question', text: '新问题', status: 'complete' }],
},
],
run: {
status: 'idle',
runId: 'run-2',
settledAt: Date.parse('2026-08-24T08:01:00.000Z'),
terminalReason: 'completed',
},
});
await flushPendingAgentSessionSync();
expect(pushSessionDataMock).not.toHaveBeenCalled();
});
it('never uploads a pending snapshot under a different authenticated account', async () => { it('never uploads a pending snapshot under a different authenticated account', async () => {
queueAgentSessionSync('prj_1', 'ses_1', [ queueAgentSessionSync('prj_1', 'ses_1', [
{ id: 'user-1', role: 'user', content: '账号 A 的问题' }, { id: 'user-1', role: 'user', content: '账号 A 的问题' },