{"record":{"id":"21aaf1ad33749a8b","repo":"moeru-ai/airi","slug":"tool-invocation-invocationid-is-missing-or-ambiguous","errorCode":null,"errorMessage":"Tool invocation \"${invocationId}\" is missing or ambiguous.","messagePattern":"Tool invocation \"(.+?)\" is missing or ambiguous\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/stage-ui/src/stores/tool-call-rerun.ts","lineNumber":50,"sourceCode":"}\n\ninterface ExecuteToolCallRerunOptions<TToolset extends string = string> {\n  messages: ChatHistoryItem[]\n  payload: ToolCallRerunPayload<TToolset>\n  resolveTools: () => Promise<Tool[]>\n}\n\ntype ToolCallResultInput = Omit<ChatSlicesToolCallResult, 'type'>\ntype ToolExecuteOptions = NonNullable<Parameters<Tool['execute']>[1]>\n\nfunction findInvocation(message: ChatAssistantMessage, callId: string, invocationId?: string) {\n  const candidates = message.generationTranscript?.rounds.flatMap((round, roundIndex) =>\n    round.toolInvocations.filter(call => call.callId === callId).map(call => ({ call, roundIndex })),\n  )\n  if (invocationId !== undefined) {\n    const matches = candidates?.filter(candidate => candidate.call.id === invocationId)\n    if (!candidates || matches?.length !== 1)\n      throw new Error(`Tool invocation \"${invocationId}\" is missing or ambiguous.`)\n    return { ...matches[0], occurrence: candidates.indexOf(matches[0]) }\n  }\n  if (candidates && candidates.length > 1)\n    throw new Error(`Tool call \"${callId}\" is ambiguous. Select an invocation.`)\n  return candidates?.[0] ? { ...candidates[0], occurrence: 0 } : undefined\n}\n\n/**\n * Replaces one execution and its display results, preserving their order.\n * Native continuation for this round and later rounds becomes stale after the edit.\n * Repeated provider call ids require an AIRI invocation id.\n */\nexport function replaceToolCallResult(message: ChatAssistantMessage, result: ToolCallResultInput, invocationId?: string): ChatAssistantMessage {\n  const target = findInvocation(message, result.id, invocationId)\n  const occurrence = target?.occurrence ?? 0\n  const toolResult = { id: result.id, isError: result.isError, result: result.result }\n  const resultSlice: ChatSlicesToolCallResult = { type: 'tool-call-result', ...toolResult }\n  let generationTranscript = message.generationTranscript","sourceCodeStart":32,"sourceCodeEnd":68,"githubUrl":"https://github.com/moeru-ai/airi/blob/438a067dde47aa0bdb46c2323d1fe293dc805218/packages/stage-ui/src/stores/tool-call-rerun.ts#L32-L68","documentation":"findInvocation locates a tool invocation within a message's generationTranscript rounds by callId, optionally narrowed by invocationId. When invocationId is given, exactly one matching candidate must exist; zero or multiple matches make the reference ambiguous or nonexistent, so it throws rather than guessing which invocation to rerun.","triggerScenarios":"Calling findInvocation (via target/invocation selectors) with an invocationId that no longer exists in the transcript, matches multiple stored invocations, or when the message has no generationTranscript/rounds at all.","commonSituations":"Rerunning a tool call after the session was edited/regenerated so ids changed; passing an invocation id copied from another message; transcripts containing duplicate invocation ids after a buggy import; referencing a call from a cleared history.","solutions":["Refresh the invocation id from the current message transcript before rerunning (re-read message.generationTranscript).","If the callId legitimately appears multiple times, omit invocationId to get the ambiguity error and then select a specific invocation id.","Verify the invocationId belongs to the same message whose transcript you pass in.","Fix data producing duplicate invocation ids if the transcript was imported/generated incorrectly."],"exampleFix":"// before\nfindInvocation(message, { callId, invocationId: 'stale-id' })\n// after\nconst invocation = findInvocation(freshMessage, { callId, invocationId: currentInvocationId })\nif (!invocation) return // call no longer present","handlingStrategy":"try-catch","validationCode":"const transcript = message.generationTranscript?.rounds.flatMap(r => r.toolInvocations.filter(c => c.callId === callId)) ?? []\nif (transcript.length === 0) return null","typeGuard":null,"tryCatchPattern":"try {\n  const found = findInvocation(message, { callId, invocationId })\n} catch (e) {\n  if (e.message.includes('missing or ambiguous')) {\n    refreshTranscript(message.id)\n    return\n  }\n  throw e\n}","preventionTips":["Always read invocation ids from the latest transcript state, never cache across regenerations.","Scope callId lookups to the exact message the id came from.","Ensure transcript writes keep invocation ids unique within a message."],"tags":["tool-call","transcript","record-not-found","ambiguity"],"backgroundTag":"record-not-found","analyzedSha":"438a067dde47aa0bdb46c2323d1fe293dc805218","analyzedAt":"2026-09-17T01:14:42.644Z","contentChangedAt":"2026-09-17T01:14:42.644Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}