Skip to content

fix: tool_use without tool_result causes API error on session resume #213

@randomm

Description

@randomm

Problem

When a session is interrupted mid-tool (e.g., abort, crash, or user interrupt) before the tool finishes executing, the tool part is left in `"pending"` state. On next session load, `processor.ts` silently ignores these pending tool parts when processing `tool-result` and `tool-error` events — because it only checks for `"running"` state, not `"pending"`. The result is a dangling `tool_use` block sent to the Anthropic API with no corresponding `tool_result`, which causes:

```
Error: tool_use block requires a corresponding tool_result block
```

Root Cause

File: `packages/opencode/src/session/processor.ts`

Line 182 (tool-result handler):
```ts
if (match.state.status === "running") {
```

Line 206 (tool-error handler):
```ts
if (match.state.status === "running") {
```

Both checks exclude `"pending"` tools — tools interrupted before execution began are never transitioned to an error state, so they remain as orphaned `tool_use` blocks in the persisted message.

Existing Partial Workaround

`message-v2.ts` lines 660–670 converts `pending`/`running` tool parts to synthetic error results during serialization for the API. This prevents the immediate crash but does not fix the root state — the tool remains `pending` in the store, so the workaround fires on every subsequent resume too.

Fix

In `processor.ts`, extend both conditions to also handle `"pending"` tools:

Line 182 — change:
```ts
if (match.state.status === "running") {
```
to:
```ts
if (match.state.status === "running" || match.state.status === "pending") {
```

When the matched tool is `"pending"` (i.e., `startTime` was never set), synthesize a safe `startTime`:
```ts
const startTime = match.state.time.start ?? Date.now()
```

Apply the same change at line 206 for the `tool-error` handler.

Acceptance Criteria

  • `processor.ts` line 182: condition includes `|| match.state.status === "pending"`
  • `processor.ts` line 206: condition includes `|| match.state.status === "pending"`
  • `startTime` defaults to `Date.now()` when `match.state.time.start` is `undefined` (pending case)
  • Existing tests pass: `bun test` in `packages/opencode`
  • Type check passes: `bun run typecheck` in `packages/opencode`
  • New test: session with a `pending` tool on resume produces a `tool_result` error block (not a dangling `tool_use`)
  • No `@ts-ignore`, `as any`, or suppression comments

Context

Quality Gates

  • TDD: write failing test first
  • Coverage: new code covered
  • Linting passes
  • `bun run typecheck && bun test` green locally before push

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions