Repository navigation
Description
Summary
A server restart that interrupts a running shell tool call breaks the next request in that session. Anthropic rejects it with 400 tool_use.input: Input should be an object. One restart did this to all 3 sessions that had a tool call running at the time. This may be a reliable way to trigger #53865.
Environment
- opencode version: 2.0.30 (server) after the restart. Unknown before it.
- OS: Linux 7.0.0-1014-aws x86_64
- Terminal: none. Sessions run headless through the server API.
- Shell: /bin/bash
- Install/channel: latest
- Active plugins:
compound-engineering(git plugin); local pluginsgoal,session-id.js,skill-commands.tsin.opencode/plugins/ - Provider/model: built-in
anthropicprovider, direct (no relay),claude-opus-5-5, varianthigh
Reproduction
- Start background subagent sessions on the Anthropic provider that run long
shellcalls. - Restart the opencode server while a
shellcall is running. - The session gets the "server restarted" message. The interrupted call is stored with
status: "error",error.type: "aborted"("Tool execution interrupted: shell") andexecuted: false. - The session sends its next request.
Expected Behavior
The interrupted call is replayed as a tool_use with object input and an error tool_result, and the session continues.
Actual Behavior
The request fails in about 0.3 s:
{"type":"error","error":{"type":"invalid_request_error","message":"messages.142.content.1.tool_use.input: Input should be an object"}}
opencode saves an empty assistant message (content: [], finish: "error", error.type: "provider.invalid-request") and ends the turn with outcome: "failed". A later turn in the same session succeeded, but the parent agent had already received state="error" and treated the subagent as crashed.
All 3 sessions, times in UTC:
| Session | Interrupted call's message | Restart | Failed request | Error path |
|---|---|---|---|---|
| A | [reasoning, shell] |
22:16:00 | 22:16:05 | messages.142.content.1 |
| B | [reasoning, reasoning, shell] |
22:16:00 | 22:18:01 | messages.147.content.2 |
| C | [reasoning, shell] |
22:16:00 | 22:18:34 | messages.164.content.0 |
In A and B, the error's content index is the interrupted shell call's position in its message. In C it is one lower, perhaps because a reasoning block was dropped on replay.
Additional Context
- Stored inputs are clean.
opencode session exportshows every tool input as an object, including the 3 interrupted calls. So, as in anthropic: intermittent 400 "tool_use.input: Input should be an object" — tool input sent as raw string right after tool result #53865, the bad input probably exists only in the outgoing request. I did not capture that request. - This is a different case from fix(core): reject malformed tool arguments and settle unfinished calls #53685. That fix (2.0.25) closes out calls whose arguments never finished. These calls had complete arguments and had started running (
time.ranis set) when the restart aborted them. - Each session had one interrupted call. No other tool call in the 3 sessions was interrupted.
- Not reproduced on purpose. All 3 cases come from one unplanned restart.
- Related: anthropic: intermittent 400 "tool_use.input: Input should be an object" — tool input sent as raw string right after tool result #53865, closed as not planned. On 2026-10-14 a user reported it still happens on 2.0.29 in background subagent sessions.
Activity
opencode-agent commentedon Oct 15, 2026
Thanks for the detailed report. I tried to reproduce this on 2.0.30 and couldn't.
I pointed the built-in
anthropicprovider (claude-opus-5-5) at a local mock of the Anthropic Messages API. The mock saves every request body and returns the same 400 if anytool_use.inputis not an object. The model was set up to reply with reasoning followed by a longshellcall (sleep 120). While that call was running, I killed the server withkill -9and restarted it. I tried three setups:opencode servesession continued with a new prompt[reasoning, shell]and[reasoning, reasoning, shell]shellcall running in a background subagent sessionEach time the interrupted call was stored as you described (
status: "error",aborted,executed: false). The next request sent it as atool_usewith the original object input, followed by an errortool_result. Anthropic would accept that, and the sessions and the parent's subagent notification completed normally.The main differences from your setup are long histories (100+ messages), your plugins and
variant: high. Things that would help:tool_useblock at the failing index: is itsinputa string, empty or partial JSON, and which tool call does it belong to? In session C the index is one lower, so it may not be the interrupted call.compound-engineeringplugin and the local plugins (goal,session-id.js,skill-commands.ts) disabled, since a plugin hook could change the request or message history.opencode session export <id>), redacted as needed.Related: #53865 (same 400, closed as not planned, with a later report on 2.0.29) and #53685 (fix for tool calls whose arguments never finished).
Parker-Fawcett commentedon Oct 16, 2026
Reporters on #55886 observed Anthropic rejecting
tool_use.inputafter a server restart interrupts a runningshellcall, although stored inputs remain clean. I reproduced the lowering side from a persisted interrupted assistant message and found one concrete trigger: if an assistant message is stored/replayed witherror.type === "aborted", astreamingorrunningnon-hosted tool call can be emitted without a matching result while its JSON input may not have been parsed yet. The provider-history lowering boundary currently returnstool.state.inputdirectly for non-streaming states and only parses streaming input.I'm preparing a focused
corepatch that changes onlypackages/core/src/session/runner/to-llm-message.ts:Schema.decodeUnknownOptionand keep only object values, otherwise use{}.assistant.error.type === "aborted", synthesize the existing{ type: "aborted", message: "Tool execution interrupted" }error result for any unfinishedstreaming/runningnon-hosted call, matching the interrupted-terminal shape.The change adds deterministic core tests in
session-runner-message.test.tscovering aborted streaming (invalid JSON), aborted running (object input), and a non-abort error control. No API or schema change is intended. Rootbun run checkis failing in my environment becauseoxlintcannot load its JS plugin, so validation is package-level only so far.Before implementing the final PR, is this lowering-boundary approach acceptable, or is the repo's restart expectation that these calls remain represented differently?
fix(core): keep interrupted tool calls replayable for Anthropic
rekram1-node commentedon Oct 16, 2026
Closing as can't reproduce. We tried on the latest code with a real server, a mock Anthropic API, and
kill -9and graceful restarts while ashellcall was running. We covered plainserve, the managed service's automatic resume, and background subagents. Every time, the interrupted call was stored as you described, and the next request sent it as atool_usewith an object input followed by an errortool_result. We also couldn't find any code path that turns that stored state into a non-object input.This most likely came from a build older than the recent fixes for interrupted tool calls. Your local plugins (
goal,session-id.js,skill-commands.ts) are another possible cause. Plugincontextandhttp.requesthooks can rewrite the message history or the outgoing request body after opencode builds it. That would fit the bad input appearing only in the request while the stored inputs stay clean. We checkedcompound-engineering, and it doesn't register any hooks that could do this.If it happens again on the latest version, please try it with the local plugins disabled. Then reopen and include the redacted outgoing request body of the failing request, in particular the
tool_useblock at the index named in the error.fix(core): coerce unsettled streaming tool input through JSON decoding
opencode-agent commentedon Oct 18, 2026
I'm closing this for now because there hasn't been a reply to the questions above. On 2.0.30 I couldn't reproduce the 400
tool_use.input: Input should be an objectafter a restart that interrupts ashellcall. I tried a plain server session, the managed service resuming by itself (with one and with two reasoning blocks before the call), and a background subagent. In each case the interrupted call was sent with its original object input and an errortool_result.If it happens again, please comment here or open a new issue with:
tool_useblock at the failing indexcompound-engineeringand the local plugins disabledopencode session export <id>)Related: #53865.