Skip to content

core: tool stuck in pending state when hitting SQLITE full errors #52796

Description

@alohaninja

Description

If writing a local tool's result to the DB fails (I hit SQLiteError: database or disk is full), the step still ends normally, and the next request goes out with a tool_use that has no tool_result. Anthropic rejects it:

messages.16: `tool_use` ids were found without `tool_result` blocks immediately after: toolu_01Rd…

The call stays running in the DB until the session is interrupted.

Cause, in packages/core/src/session/runner/publish-llm-event.ts: toolExecution and failTool set tool.settled = true before bus.publish. When the publish fails, the in-memory flag stays set, so failUnsettledTools skips the call in step.ts and never records a Tool.Failed. toolExecution has the same problem with its "no content" defect, which also runs after the flag is set.

How I hit it: a parent session started three subagent calls. One child failed with SQLITE_FULL partway through its run. When the other two finished, the parent sent its next request and got the 400 above.

Reproduced on v2 at 80caae8 with two runner tests, one per path. In each, the follow-up request contains a tool-call with no result:

  • toolExecution: the Tool.Success projection dies for one call.
  • failTool: a tool returns Tool.Error, and the first Tool.Failed projection dies. The step's failUnsettledTools should retry it, but skips it.

I have a small fix with both tests: clear the flag again if the publish fails, and check for content before setting it. The check-and-mark stays atomic, per the publisher's concurrency note. Happy to open a PR.

Plugins

superpowers, openchamber agent-tool, a couple of local plugins (none of them affect this path; the repro is a core unit test)

OpenCode version

2.0.19 (still reproduces on v2 @ 80caae8)

Steps to reproduce

  1. Run a session where a local tool call's session.tool.success write fails. Easiest way is a nearly full disk (SQLITE_FULL), or in tests, bus.project(SessionEvent.Tool.Success, () => Effect.die(...)).
  2. Have the provider end the step with tool-calls.
  3. The continuation request contains the tool_use with no tool_result, and Anthropic returns a 400.

Test case:

scenario("never replays a local tool call whose result failed to persist", function* (s) {
  yield* s.bus.project(SessionEvent.Tool.Success, (event) =>
    event.data.id === "call-store-orphan" ? Effect.die("SQLiteError: database or disk is full") : Effect.void,
  )
  yield* s.llm.push(
    TestLLM.toolCalls(LLMEvent.toolCall({ id: "call-store-orphan", name: "storefail", input: {} })),
    TestLLM.stop(),
  )
  yield* s.runPrompt("Storage fails after the tool ran").pipe(Effect.exit)
  // fails on v2: requests[1] has tool-call "call-store-orphan" with no tool-result
})

Screenshot and/or share link

N/A

Operating System

macOS (Darwin 25.3.0 arm64)

Terminal

OpenChamber (desktop)

Activity

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

Metadata

Metadata

Assignees

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