From 3555f815afe6667f409692033f8da97861726c7b Mon Sep 17 00:00:00 2001 From: weichen Date: Thu, 15 Oct 2026 07:51:41 +0000 Subject: [PATCH] Keep late background progress out of a newer conversation turn Why: Completion or progress from an earlier task could appear in the process of an unrelated newer answer. Evidence: The new ownership regression fails before the guard and passes afterward; the frontend suite, TypeScript and unchanged export gate pass. Impact: Project child progress only into its originating live turn while still reconciling background results. Refs: feature/workbench-flow-completion chronological display requirement --- src/store.ts | 9 ++++++--- test/work-state.test.ts | 15 +++++++++++++++ 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/src/store.ts b/src/store.ts index 7c0a752..2ed0bc9 100644 --- a/src/store.ts +++ b/src/store.ts @@ -730,6 +730,9 @@ export const store = { if (e.runId && liveEventTypes.has(e.type)) observeDetail(runObservations, e.runId, e.seq) const now = eventTime(e) const project = (mutate: (l: LiveState) => void) => touchLive(e.runId ?? '', mutate, now) + const belongsToLive = (agentRun: string) => !!state.live && (e.runId === state.live.runId + || Object.prototype.hasOwnProperty.call(state.live.agentNotes, agentRun) + || state.runs.some(run => run.id === agentRun && run.parentRunId === state.live?.runId)) switch (e.type) { case 'message.delta': { const p = e.payload as { index: number; text: string } @@ -791,17 +794,17 @@ export const store = { } case 'agent.spawn': { const p = e.payload as { agentRun: string; name: string; task: string } - if (state.live) set({ live: { ...state.live, agentNotes: { ...state.live.agentNotes, [p.agentRun]: `已派生 ${p.name}` } } }) + if (state.live && belongsToLive(p.agentRun)) set({ live: { ...state.live, agentNotes: { ...state.live.agentNotes, [p.agentRun]: `已派生 ${p.name}` } } }) break } case 'agent.progress': { const p = e.payload as { agentRun: string; note?: string } - if (state.live && p.note) set({ live: { ...state.live, agentNotes: { ...state.live.agentNotes, [p.agentRun]: p.note } } }) + if (state.live && p.note && belongsToLive(p.agentRun)) set({ live: { ...state.live, agentNotes: { ...state.live.agentNotes, [p.agentRun]: p.note } } }) break } case 'agent.result': { const p = e.payload as { agentRun: string; summary?: string } - if (state.live) set({ live: { ...state.live, agentNotes: { ...state.live.agentNotes, [p.agentRun]: p.summary ?? '任务已结束' } } }) + if (state.live && belongsToLive(p.agentRun)) set({ live: { ...state.live, agentNotes: { ...state.live.agentNotes, [p.agentRun]: p.summary ?? '任务已结束' } } }) store.reconcile() break } diff --git a/test/work-state.test.ts b/test/work-state.test.ts index bc0ec09..c001b98 100644 --- a/test/work-state.test.ts +++ b/test/work-state.test.ts @@ -64,6 +64,21 @@ it('preserves rejected referenced input without retrying it or blocking later qu }) describe('work projection ordering and context', () => { + it('keeps an earlier background task out of a newer foreground process', async () => { + const current: Run = { id: 'new-topic', conversationId: 'a', projectId: 'p', kind: 'conversation', status: 'running', createdAt: '' } + const child: Run = { ...current, id: 'old-child', kind: 'subagent', parentRunId: 'old-topic' } + vi.mocked(api.conversationDetail).mockResolvedValue(detail('a', [current, child])) + await store.openConversation('a') + store.applyEvent(event(1, 'message.delta', { index: 0, text: '正在回答新的问题' }, current.id)) + store.applyEvent(event(2, 'agent.progress', { agentRun: child.id, note: '旧材料仍在整理' }, child.id)) + expect(store.getState().live?.agentNotes[child.id]).toBeUndefined() + store.applyEvent(event(3, 'agent.spawn', { agentRun: 'new-child', name: '新任务', task: '核对新问题' }, current.id)) + store.applyEvent(event(4, 'agent.progress', { agentRun: 'new-child', note: '核对当前问题' }, 'new-child')) + expect(store.getState().live?.agentNotes['new-child']).toBe('核对当前问题') + store.applyEvent(event(5, 'agent.result', { agentRun: child.id, summary: '旧材料已整理' }, child.id)) + expect(store.getState().live?.runId).toBe(current.id) + expect(store.getState().live?.agentNotes[child.id]).toBeUndefined() + }) it('settles foreground completion and queued input when background events overtake its detail response', async () => { vi.useFakeTimers() const run: Run = { id: 'front', conversationId: 'a', projectId: 'p', kind: 'conversation', status: 'running', createdAt: '' }