From 6fe6a86dec7ca92a6d928c01062bf59bf8154cb8 Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Thu, 12 Nov 2026 18:38:23 -0800 Subject: [PATCH] fix(chat): retire running tools when provider identities become input aliases Refresh raw and canonical tool entries when an echo or dispatch acceptance aliases a provider item. Exclude aliased raw items when rebuilding the derived index so stale tools cannot return after reopening a journal. Co-authored-by: Taras Kornichuk --- .../input-order/journal-running-tools.ts | 12 ++++- .../workers/reports/worker-quiet-chat.test.ts | 48 +++++++++++++++++++ 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/src/main/native-chat/agent-session-journal/input-order/journal-running-tools.ts b/src/main/native-chat/agent-session-journal/input-order/journal-running-tools.ts index 3ec6148b71d..4991ca6b850 100644 --- a/src/main/native-chat/agent-session-journal/input-order/journal-running-tools.ts +++ b/src/main/native-chat/agent-session-journal/input-order/journal-running-tools.ts @@ -35,9 +35,14 @@ export class JournalRunningTools { ? [row.itemId] : row.kind === 'lifecycle-batch' ? row.mutations.map((mutation) => mutation.itemId) - : [] + : row.kind === 'dispatch' && row.providerItemId + ? [row.providerItemId] + : [] for (const raw of ids) { const id = state.aliases.get(raw) ?? raw + if (raw !== id) { + this.refresh(state, raw) + } this.refresh(state, id, state.items.get(id)) } } @@ -47,7 +52,10 @@ export class JournalRunningTools { const body = item?.body const fence = journalItemExecutionFence(state, id) const key = - body?.kind === 'tool-call' && body.state === 'running' && scope?.kind === 'turn' + !state.aliases.has(id) && + body?.kind === 'tool-call' && + body.state === 'running' && + scope?.kind === 'turn' ? JSON.stringify([scope.turnItemId, fence]) : undefined const previous = this.owners.get(id) diff --git a/src/main/runtime/orchestration/workers/reports/worker-quiet-chat.test.ts b/src/main/runtime/orchestration/workers/reports/worker-quiet-chat.test.ts index 18d96ee7f12..ab9031e25e5 100644 --- a/src/main/runtime/orchestration/workers/reports/worker-quiet-chat.test.ts +++ b/src/main/runtime/orchestration/workers/reports/worker-quiet-chat.test.ts @@ -7,6 +7,9 @@ import { agentJournalItemKey } from '../../../../../shared/agent-session/journal import type { JournalItemRow } from '../../../../native-chat/agent-session-journal/durable-rows/journal-row-schema' import { createStructuredAgentSessionConversationReads } from '../../../../native-chat/agent-session-wire/host/structured-agent-session-conversation-reads' +import type { AgentJournalMessageItem } from '../../../../../shared/agent-session/journal/agent-session-journal-types' +import { agentSessionSendBodyFingerprint } from '../../../../../shared/agent-session/requests/structured-agent-session-send-mutation' + const fixture = runtimeFixture() const { chat } = fixture const journals = createFixture() @@ -112,3 +115,48 @@ it.each([ expect(journal.lastProviderActivityAt(2)).toBeUndefined() } ) + +it.each(['echo', 'acceptance'] as const)( + 'retires a running tool whose provider id becomes an input alias through %s', + async (alias) => { + const journal = await journals.open() + const turn = { provider: 'orca', clientMessageId: 'turn' } as const + const turnItemId = agentJournalItemKey(turn) + const scope = { kind: 'turn', turnItemId } as const + await journal.appendItem( + turn, + { kind: 'turn', turnId: 'turn', state: 'running', startedAt: 1 }, + { fence: 2, turnScope: scope } + ) + const identity = journals.item(0) + await journal.appendItem( + identity, + { kind: 'tool-call', name: 'Bash', input: {}, state: 'running' }, + { fence: 2, turnScope: scope, observedAt: 0 } + ) + expect(journal.runningTool(turnItemId, 2)).toEqual({ name: 'Bash', startedAt: 0 }) + const body: AgentJournalMessageItem = { + kind: 'message', + role: 'user', + blocks: [{ type: 'text', text: 'continue' }] + } + await journal.appendSubmission({ + clientMessageId: 'send-1', + payloadFingerprint: agentSessionSendBodyFingerprint('session-1', body), + body, + fence: 2 + }) + await (alias === 'echo' + ? journal.appendItem(identity, body, { fence: 2, turnScope: scope }) + : journal.resolveDispatch({ + clientMessageId: 'send-1', + state: 'accepted', + providerIdentity: identity, + fence: 2 + })) + expect(journal.runningTool(turnItemId, 2)).toBeUndefined() + await journals.journals.closeAll() + const restored = await journals.open() + expect(restored.runningTool(turnItemId, 2)).toBeUndefined() + } +)