Skip to content

server: restart that interrupts a tool call fails the next Anthropic request with "tool_use.input: Input should be an object" #55886

Closed as not planned

Description

@wallace-maschine

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 plugins goal, session-id.js, skill-commands.ts in .opencode/plugins/
  • Provider/model: built-in anthropic provider, direct (no relay), claude-opus-5-5, variant high

Reproduction

  1. Start background subagent sessions on the Anthropic provider that run long shell calls.
  2. Restart the opencode server while a shell call is running.
  3. The session gets the "server restarted" message. The interrupted call is stored with status: "error", error.type: "aborted" ("Tool execution interrupted: shell") and executed: false.
  4. 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

Activity

  1. opencode-agent commented on Oct 15, 2026

    @opencode-agent
    Contributor

    Thanks for the detailed report. I tried to reproduce this on 2.0.30 and couldn't.

    I pointed the built-in anthropic provider (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 any tool_use.input is not an object. The model was set up to reply with reasoning followed by a long shell call (sleep 120). While that call was running, I killed the server with kill -9 and restarted it. I tried three setups:

    • a plain opencode serve session continued with a new prompt
    • the managed background service, which resumed the session by itself with the "server restarted" message, using both [reasoning, shell] and [reasoning, reasoning, shell]
    • the same, with the shell call running in a background subagent session

    Each time the interrupted call was stored as you described (status: "error", aborted, executed: false). The next request sent it as a tool_use with the original object input, followed by an error tool_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:

    • The outgoing request body of a failing request (redacted), in particular the tool_use block at the failing index: is its input a 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.
    • Whether it still happens with the compound-engineering plugin and the local plugins (goal, session-id.js, skill-commands.ts) disabled, since a plugin hook could change the request or message history.
    • The exported message at the failing index, and the message just before it (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).

  2. Parker-Fawcett commented on Oct 16, 2026

    @Parker-Fawcett

    Reporters on #55886 observed Anthropic rejecting tool_use.input after a server restart interrupts a running shell call, 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 with error.type === "aborted", a streaming or running non-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 returns tool.state.input directly for non-streaming states and only parses streaming input.

    I'm preparing a focused core patch that changes only packages/core/src/session/runner/to-llm-message.ts:

    • parse streaming tool input through Schema.decodeUnknownOption and keep only object values, otherwise use {}.
    • when assistant.error.type === "aborted", synthesize the existing { type: "aborted", message: "Tool execution interrupted" } error result for any unfinished streaming/running non-hosted call, matching the interrupted-terminal shape.
    • keep completed/error behavior and metadata unchanged.

    The change adds deterministic core tests in session-runner-message.test.ts covering aborted streaming (invalid JSON), aborted running (object input), and a non-abort error control. No API or schema change is intended. Root bun run check is failing in my environment because oxlint cannot 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?

  3. added a commit that references this issue on Oct 16, 2026
    43ab82e
  4. rekram1-node commented on Oct 16, 2026

    @rekram1-node
    Collaborator

    Closing as can't reproduce. We tried on the latest code with a real server, a mock Anthropic API, and kill -9 and graceful restarts while a shell call was running. We covered plain serve, 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 a tool_use with an object input followed by an error tool_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. Plugin context and http.request hooks 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 checked compound-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_use block at the index named in the error.

  5. added a commit that references this issue on Oct 16, 2026
    d038b64
  6. opencode-agent commented on Oct 18, 2026

    @opencode-agent
    Contributor

    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 object after a restart that interrupts a shell call. 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 error tool_result.

    If it happens again, please comment here or open a new issue with:

    • the outgoing request body of a failing request (redacted), especially the tool_use block at the failing index
    • whether it still happens with compound-engineering and the local plugins disabled
    • the exported messages around the failing index (opencode session export <id>)

    Related: #53865.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions