paperclipai/paperclip · error

OpenCode tool call is not bound to an active turn

Error message

OpenCode tool call is not bound to an active turn

What it means

`dispatchTool` routes each OpenCode MCP tool call into the Paperclip event stream tagged with the currently active turn. If `#activeTurnId` is null — no turn is running — the tool call cannot be attributed to a turn and is rejected. This happens when OpenCode emits tool calls outside an active prompt window (e.g. late replays after the turn ended).

Source

Thrown at packages/paperclip-runner/src/drivers/opencode/opencode-server-driver.ts:875

          ? harnessRuntimeRequestOutcome(request, { reason: "session_closed" })
          : harnessRuntimeInputExpiredOutcome(request, "provider_process_lost"),
        { turnId: request.turnId, itemId: request.itemId },
      );
    }
    this.#pendingRuntimeRequests.clear();
    this.#events.close();
    await this.#runtime.close();
  }

  async dispatchTool(call: {
    tool: string;
    callId: string;
    arguments: unknown;
  }): Promise<unknown> {
    const tool = canonicalOpenCodeMcpToolName(call.tool);
    const turnId = this.#activeTurnId;
    if (turnId === null)
      throw new Error("OpenCode tool call is not bound to an active turn");
    this.#emit(
      "item.started",
      {
        kind: "dynamicToolCall",
        item: {
          type: "tool_call",
          id: call.callId,
          name: tool,
          arguments: call.arguments,
        },
      },
      { turnId, itemId: call.callId },
    );
    if (tool === PRP_COMPLETION_TOOL_NAME || tool === PRP_BLOCK_TOOL_NAME) {
      const validation = validatePrpStructuredRunResult(call.arguments);
      if (!validation.ok) throw new Error("Invalid semantic result");
      if (
        (tool === PRP_BLOCK_TOOL_NAME &&

View on GitHub (pinned to 01ad858492)

Solutions

  1. Only call `dispatchTool` between a successful `startTurn` and the turn's terminal event; check `await session.snapshot()` (`activeTurnId !== null`) first.
  2. Filter the OpenCode event stream by source sequence / turn so stale tool calls are dropped instead of dispatched.
  3. If this occurs during reconnect replay, skip the event rather than dispatch — the turn's results were already finalized.
  4. Start a turn (`startTurn`) before issuing tool calls if your integration drives tools proactively.

Example fix

// before
await session.dispatchTool({ tool, callId, arguments }); // throws when no active turn

// after
const snap = await session.snapshot();
if (snap.activeTurnId) {
  await session.dispatchTool({ tool, callId, arguments });
}
Defensive patterns

Strategy: validation

Validate before calling

const snap = await session.snapshot();
if (!snap.activeTurnId) return; // drop out-of-turn tool call

Type guard

function hasActiveTurn(snap) { return typeof snap.activeTurnId === 'string' && snap.activeTurnId.length > 0; }

Try / catch

try {
  await session.dispatchTool({ tool, callId, arguments });
} catch (e) {
  if (e.message === 'OpenCode tool call is not bound to an active turn') {
    logger.warn({ callId }, 'dropped stale tool call outside active turn');
  } else throw e;
}

Prevention

When it happens

Trigger: Calling `session.dispatchTool({ tool, callId, arguments })` before any `startTurn`, after the active turn completed/aborted (turn id cleared), or after a resume where `attachRun` was called but no new turn started while OpenCode still replays buffered tool calls.

Common situations: Replaying SSE history after reconnect: old tool-call events arrive when the driver considers the turn done; a client invokes driver-level tool dispatch directly outside the prompt loop; race where `turn.completed` clears state just before a final tool call lands.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10). Data as JSON: /api/errors/08494b15821fe4f7. Report an issue: GitHub.