session: tool aborted by a background-service restart leaves unpaired tool_calls, producing 400 on resume
Summary
When a tool call is aborted because the command it runs stops/restarts the OpenCode background service, the session persists an assistant message containing tool_calls with no matching tool result. After the restart, the resume path dispatches the next step before that result is paired, so a strict OpenAI-compatible provider rejects the whole history with HTTP 400 invalid_request_error. Retries resend the same malformed history.
Environment
- opencode version: 2.0.21
- OS: macOS (Darwin 27.0.0, arm64)
- Terminal: OpenChamber desktop app (terminal environment variables not set)
- Shell: /bin/zsh
- Install/channel: latest (release build, not local/dev)
- Active plugins:
opencode.browser(built-in), localopenchamber-agent-tool
Reproduction
- In a session, let the agent run a shell tool whose command stops/restarts the managed background service, e.g.
opencode service unset env <VAR>(any command that restarts or kills the service). - The restart interrupts the in-flight tool. It is persisted with status
error,{"type":"aborted","message":"Tool execution interrupted: shell"}, and a synthetic "The server restarted while you were working. Continue from where you left off..." message is inserted. - On resume, a new step is dispatched to the provider while the aborted tool's result is not yet paired with its
tool_call_id. - The request therefore contains an assistant message with
tool_callsbut no correspondingtoolmessage for that call. - The request fails with 400; retries keep resending the same malformed history.
Expected Behavior
Every assistant tool_calls message sent to a provider must be immediately followed by a tool message for each tool_call_id. An aborted/interrupted call should get a synthetic error/placeholder result, or the dangling call should be dropped. Resuming after a service restart must never emit an unpaired tool_calls message.
Actual Behavior
The provider returns:
An assistant message with 'tool_calls' must be followed by tool messages responding to each 'tool_call_id'. (insufficient tool messages following tool_calls message)In the application this surfaces as AI.Error.InvalidRequest and the session drain fails; the user sees "opencode failed to send message".
Additional Context
Reconstructed from the local session DB (opencode 2.0.21) — one session, all times UTC, IDs/prompts removed:
seq A: assistant message issues a shell tool call. The command restarts the service; the tool is persisted aserror/{"type":"aborted"}.seq B(+~5s): synthetic "The server restarted while you were working..." message inserted.seq C: assistant message issues the next tool call; the tool starts at timeT.- A provider request is created at
T + ~0.3sand fails — i.e. while that tool was still running and before its result was ever written. - A later retry succeeds; the session recovers on its own (or after the user sends a new message).
The same pattern repeated three times in this one session, each time immediately after a service restart (each triggered by a tool command that restarted the service). Sessions without an interruption did not reproduce it. Only strict OpenAI-compatible endpoints trip on it; providers that tolerate orphaned calls keep working.
Workaround: avoid running commands that restart/stop the managed background service inside a running turn.
Related issues: #44852, #37364, #51213, #49911 (withdrawn), #22808 (auto-closed).
Source: anomalyco/opencode