You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Duplicate function_call_output for reused tool-call IDs makes session unrecoverable #54588
Session permanently stuck: every drain fails with AI.Error.InvalidRequest: Duplicate function_call_output for call_id 'read:1'. Each function_call must have exactly one matching function_call_output.
Root cause (verified in session export): the model
(moonshotai/kimi-k3 via nim) reused tool-call IDs across turns —
msg idx 616 read:1=docs/.md, idx 617 read:1=
src/.java, and read:0 twice (idx 617, 619).
OpenCode forwards both outputs for one call ID, the AI SDK rejects
the request in SessionStep.attempt during drain.
The duplicate lives in persisted history, so session.revert +
retry does NOT fix it (verified 4x in opencode.log across 5 hours).
Session outcome=failed, unrecoverable in place. OpenCode should
namespace/dedupe tool-call IDs per turn, or make revert drop the
duplicated messages.
Thanks for the detailed report. I reproduced this with a simulated model on 2.0.22, and it still happens on the latest dev (38454c6).
Steps:
The model calls read with call ID read:0 on a.md.
In the next turn it reuses read:0 to read b.md. The tool itself works fine.
On the following prompt, OpenCode sends the whole history with the reused ID unchanged: two assistant tool calls and two tool results, all with ID read:0.
A strict provider like NIM rejects that request, and because the duplicate is stored in the session history, every later request fails the same way, which matches what you saw. I couldn't trigger the provider's own Duplicate function_call_output error here without credentials, but the request OpenCode builds has exactly the duplicate it complains about. The script below reproduces it.
Until this is fixed, the only workaround I know of is to start a new session.
Reproduction script (drive.ts)
// Repro for #54588: model reuses tool-call ID "read:0" across turns;// the next request then carries two tool results for the same call ID.import{Effect,Stream}from"effect"import{Llm,OpenCodeDriver}from"opencode-drive"constrequests: unknown[]=[]exportdefaultOpenCodeDriver.use({
...(process.env.OC_DEV ? {opencode: {dev: process.env.OC_DEV}} : {}),project: {git: true,files: {"a.md": "file A\n","b.md": "file B\n"}},tui: {recording: true},},({ ui, llm })=>Effect.gen(function*(){yield*llm.serve((request,index)=>{requests.push(request)switch(index){case0: // turn 1: read a.md with id read:0returnStream.make(Llm.toolCall({index: 0,id: "read:0",name: "read",input: {path: "a.md"}}),Llm.finish("tool-calls"),)case1:
returnStream.make(Llm.text("Read A."))case2: // turn 2: the model REUSES read:0 for a different filereturnStream.make(Llm.toolCall({index: 0,id: "read:0",name: "read",input: {path: "b.md"}}),Llm.finish("tool-calls"),)default:
returnStream.make(Llm.text(`Done ${index}.`))}})yield*ui.submit("Read a.md")yield*ui.waitFor("Read A.")yield*ui.submit("Read b.md")yield*ui.waitFor("Done 3.")yield*ui.submit("Continue")yield*ui.waitFor("Done 4.")yield*ui.screenshot("final")constlast=JSON.stringify(requests[requests.length-1])constoccurrences=(last.match(/read:0/g)??[]).lengthyield*Effect.log(`LAST MESSAGES: ${JSON.stringify((requests[requests.length-1]asany).body.messages.slice(1))}`)yield*Effect.log(`occurrences of "read:0" in last request: ${occurrences}`)}),)
Run it with opencode-drive: opencode-drive run ./drive.ts. It drives opencode from your PATH; set OC_DEV to the path of an OpenCode checkout to run that from source instead.
Until ids are made unique at ingest, a session that's already stuck can be rescued by hand: export it, find the later assistant message that reuses read:0/read:1, and rename those ids in that tool call and in its matching tool result (e.g. read:1 → read:1-t617). Then re-import it. Each function_call then has exactly one function_call_output, and the provider accepts the history again. The durable fix is for the client to own uniqueness, e.g. rewrite incoming call ids to ${messageId}:${callId} when it stores them, because some providers emit ids that only stay unique within a single turn.
Activity
opencode-agent commentedon Oct 11, 2026
Thanks for the detailed report. I reproduced this with a simulated model on 2.0.22, and it still happens on the latest
dev(38454c6).Steps:
readwith call IDread:0ona.md.read:0to readb.md. The tool itself works fine.read:0.A strict provider like NIM rejects that request, and because the duplicate is stored in the session history, every later request fails the same way, which matches what you saw. I couldn't trigger the provider's own
Duplicate function_call_outputerror here without credentials, but the request OpenCode builds has exactly the duplicate it complains about. The script below reproduces it.Related issues:
tool_0IDs, causing collisions elsewhereUntil this is fixed, the only workaround I know of is to start a new session.
Reproduction script (drive.ts)
Run it with opencode-drive:
opencode-drive run ./drive.ts. It drivesopencodefrom yourPATH; setOC_DEVto the path of an OpenCode checkout to run that from source instead.MounirMeziani commentedon Oct 19, 2026
Until ids are made unique at ingest, a session that's already stuck can be rescued by hand: export it, find the later assistant message that reuses
read:0/read:1, and rename those ids in that tool call and in its matching tool result (e.g.read:1→read:1-t617). Then re-import it. Eachfunction_callthen has exactly onefunction_call_output, and the provider accepts the history again. The durable fix is for the client to own uniqueness, e.g. rewrite incoming call ids to${messageId}:${callId}when it stores them, because some providers emit ids that only stay unique within a single turn.JustCax commentedon Oct 19, 2026
@MounirMeziani i Thanks for sharing this workaround! It sounds like a promising solution.