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 <dread9ko@gmail.com>
This commit is contained in:
Brennan Benson
2026-11-12 18:38:23 -08:00
co-authored by Taras Kornichuk
parent be1fc2a1b3
commit 6fe6a86dec
2 changed files with 58 additions and 2 deletions
@@ -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)
@@ -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()
}
)