Skip to content

core: malformed tool input remains unresolved until the next prompt #36421

Description

@kitlangton

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

  1. Run a V2 session through the native OpenAI Responses route.
  2. Let the provider start a tool call and stream arguments that end as invalid JSON before the call can be parsed.
  3. Observe the step fail with provider.invalid-output.
  4. 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.

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions