Summary
In V2, malformed streamed tool JSON can fail the assistant step while leaving the started tool unresolved. The next user prompt then repairs the stale tool as interrupted, so the transcript shows both the original provider-invalid-output error and a delayed Tool execution interrupted error for the same call.
Environment
- opencode version:
0.0.0-next-15329 (the long-lived Session was originally created under next-15293)
- OS: macOS 25.5.0 (arm64)
- Terminal: Zed 1.10.0+stable.318.111c4082fd38215c31fa87803ece7695f898a94e
- Shell:
/bin/zsh
- Install/channel:
next
- Active plugins: global
share-with-team.ts and @warp-dot-dev/opencode-warp; project-specific plugin status was not relevant to the failure
Reproduction
- Run a V2 session through the native OpenAI Responses route.
- Let the provider start a tool call and stream arguments that end as invalid JSON before the call can be parsed.
- Observe the step fail with
provider.invalid-output.
- Send another user prompt to resume the Session.
The provider-dependent malformed output is intermittent, but the captured durable sequence was:
14:34:12 session.tool.input.started tool=patch
14:34:22 session.tool.input.ended bytes=2522, invalid JSON ending mid-string
14:34:22 session.step.failed provider.invalid-output
14:34:22 session.execution.failed provider.invalid-output
14:35:03 session.input.admitted text=continue
14:35:03 session.execution.started
14:35:03 session.tool.failed aborted: Tool execution interrupted: patch
14:35:03 session.input.promoted
Expected Behavior
The malformed JSON should remain the truthful provider failure. Any tool created by session.tool.input.started should be settled as failed in that same terminal step, before session.step.failed / session.execution.failed completes.
Resuming the Session should not discover and publish a second delayed interruption for the already-failed attempt.
Actual Behavior
The parser correctly rejected the 2,522-byte argument string:
Invalid JSON input for openai-responses tool call patch
The raw argument string ended mid-string, so this error was valid and the patch tool was never invoked. However, the projected tool remained in a streaming state after the step and execution failed. On the next prompt, failInterruptedTools found it and emitted:
Tool execution interrupted: patch
The TUI consequently rendered a Patch failed block plus the provider-invalid-output error, making one malformed provider call look like two independent failures.
Additional Context
This is adjacent to #27871 and #33687, but the captured occurrence is on the V2 event model and has a narrower invariant: every session.tool.input.started record must receive a terminal tool event before the owning step reaches terminal failure.
Relevant current code:
packages/llm/src/protocols/shared.ts: parseToolInput correctly classifies malformed JSON.
packages/core/src/session/runner/llm.ts: terminal stream handling calls failUnsettledTools, while the next drain also runs failInterruptedTools over projected streaming/running tools.
packages/core/src/session/runner/publish-llm-event.ts: tool input start creates an unsettled tool; input end closes the raw fragment but does not settle the tool.
A regression test should emit tool-input-start, partial input, and an InvalidProviderOutput failure, then assert that the same attempt durably emits session.tool.failed before session.step.failed, and that a subsequent resume emits no repair failure for that call.
Summary
In V2, malformed streamed tool JSON can fail the assistant step while leaving the started tool unresolved. The next user prompt then repairs the stale tool as interrupted, so the transcript shows both the original provider-invalid-output error and a delayed
Tool execution interruptederror for the same call.Environment
0.0.0-next-15329(the long-lived Session was originally created undernext-15293)/bin/zshnextshare-with-team.tsand@warp-dot-dev/opencode-warp; project-specific plugin status was not relevant to the failureReproduction
provider.invalid-output.The provider-dependent malformed output is intermittent, but the captured durable sequence was:
Expected Behavior
The malformed JSON should remain the truthful provider failure. Any tool created by
session.tool.input.startedshould be settled as failed in that same terminal step, beforesession.step.failed/session.execution.failedcompletes.Resuming the Session should not discover and publish a second delayed interruption for the already-failed attempt.
Actual Behavior
The parser correctly rejected the 2,522-byte argument string:
The raw argument string ended mid-string, so this error was valid and the patch tool was never invoked. However, the projected tool remained in a streaming state after the step and execution failed. On the next prompt,
failInterruptedToolsfound it and emitted:The TUI consequently rendered a
Patch failedblock plus the provider-invalid-output error, making one malformed provider call look like two independent failures.Additional Context
This is adjacent to #27871 and #33687, but the captured occurrence is on the V2 event model and has a narrower invariant: every
session.tool.input.startedrecord must receive a terminal tool event before the owning step reaches terminal failure.Relevant current code:
packages/llm/src/protocols/shared.ts:parseToolInputcorrectly classifies malformed JSON.packages/core/src/session/runner/llm.ts: terminal stream handling callsfailUnsettledTools, while the next drain also runsfailInterruptedToolsover projected streaming/running tools.packages/core/src/session/runner/publish-llm-event.ts: tool input start creates an unsettled tool; input end closes the raw fragment but does not settle the tool.A regression test should emit
tool-input-start, partial input, and anInvalidProviderOutputfailure, then assert that the same attempt durably emitssession.tool.failedbeforesession.step.failed, and that a subsequent resume emits no repair failure for that call.