{"record":{"id":"f1b81d1df0a4515a","repo":"moeru-ai/airi","slug":"tool-call-callid-is-ambiguous-select-an-invocation","errorCode":null,"errorMessage":"Tool call \"${callId}\" is ambiguous. Select an invocation.","messagePattern":"Tool call \"(.+?)\" is ambiguous\\. Select an invocation\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/stage-ui/src/stores/tool-call-rerun.ts","lineNumber":54,"sourceCode":"  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\n  if (generationTranscript && target) {\n    generationTranscript = {\n      ...generationTranscript,\n      rounds: generationTranscript.rounds.map((round, index) => index < target.roundIndex","sourceCodeStart":36,"sourceCodeEnd":72,"githubUrl":"https://github.com/moeru-ai/airi/blob/438a067dde47aa0bdb46c2323d1fe293dc805218/packages/stage-ui/src/stores/tool-call-rerun.ts#L36-L72","documentation":"findInvocation resolves a tool call ID to a single concrete invocation. When a tool call ID appears more than once in the message's candidates (e.g. parallel or repeated invocations of the same tool), it cannot pick one, so it throws to force the caller to disambiguate via an invocationId. This is an internal invariant guard protecting rerun correctness.","triggerScenarios":"Calling findInvocation (via target/invocation helpers in tool-call-rerun.ts) with a callId that has no invocationId while the assistant message contains 2+ tool call candidates with that same call ID.","commonSituations":"Re-running a tool call from a message where the model issued the same tool call multiple times (parallel tool use, or duplicated call IDs after message merging/editing), and the rerun payload omitted the invocationId discriminator.","solutions":["Pass an explicit invocationId in the rerun payload so findInvocation can select the exact occurrence.","Inspect the target assistant message's tool call candidates and pick the intended invocation before rerunning.","If duplication is unexpected, check upstream message construction for duplicated tool call entries."],"exampleFix":"// before\nawait rerunToolCall({ toolCallId: 'call_1', toolName: 'search' })\n// after\nawait rerunToolCall({ toolCallId: 'call_1', toolName: 'search', invocationId: 'inv_2' })","handlingStrategy":"validation","validationCode":"// Count invocations for the callId before rerunning\nconst matches = message.toolCalls.filter(c => c.id === toolCallId)\nif (matches.length > 1 && !invocationId)\n  throw new Error(`Tool call \"${toolCallId}\" is ambiguous; pass an invocationId.`)","typeGuard":null,"tryCatchPattern":"try {\n  await rerunToolCall(payload)\n} catch (e) {\n  if (e.message.includes('is ambiguous')) {\n    // surface a picker UI listing invocations for this callId\n  }\n}","preventionTips":["Always include invocationId when a message may contain duplicate tool call IDs.","Validate candidate counts in UI before enabling the rerun action.","Avoid generating duplicate tool call IDs when constructing messages."],"tags":["tool-call","ambiguity","rerun"],"backgroundTag":"invalid-argument-value","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"}