{"record":{"id":"08494b15821fe4f7","repo":"paperclipai/paperclip","slug":"opencode-tool-call-is-not-bound-to-an-active-turn","errorCode":null,"errorMessage":"OpenCode tool call is not bound to an active turn","messagePattern":"OpenCode tool call is not bound to an active turn","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/paperclip-runner/src/drivers/opencode/opencode-server-driver.ts","lineNumber":875,"sourceCode":"          ? harnessRuntimeRequestOutcome(request, { reason: \"session_closed\" })\n          : harnessRuntimeInputExpiredOutcome(request, \"provider_process_lost\"),\n        { turnId: request.turnId, itemId: request.itemId },\n      );\n    }\n    this.#pendingRuntimeRequests.clear();\n    this.#events.close();\n    await this.#runtime.close();\n  }\n\n  async dispatchTool(call: {\n    tool: string;\n    callId: string;\n    arguments: unknown;\n  }): Promise<unknown> {\n    const tool = canonicalOpenCodeMcpToolName(call.tool);\n    const turnId = this.#activeTurnId;\n    if (turnId === null)\n      throw new Error(\"OpenCode tool call is not bound to an active turn\");\n    this.#emit(\n      \"item.started\",\n      {\n        kind: \"dynamicToolCall\",\n        item: {\n          type: \"tool_call\",\n          id: call.callId,\n          name: tool,\n          arguments: call.arguments,\n        },\n      },\n      { turnId, itemId: call.callId },\n    );\n    if (tool === PRP_COMPLETION_TOOL_NAME || tool === PRP_BLOCK_TOOL_NAME) {\n      const validation = validatePrpStructuredRunResult(call.arguments);\n      if (!validation.ok) throw new Error(\"Invalid semantic result\");\n      if (\n        (tool === PRP_BLOCK_TOOL_NAME &&","sourceCodeStart":857,"sourceCodeEnd":893,"githubUrl":"https://github.com/paperclipai/paperclip/blob/01ad8584922b5d85292b1723cae71fa0d9b07a19/packages/paperclip-runner/src/drivers/opencode/opencode-server-driver.ts#L857-L893","documentation":"`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).","triggerScenarios":"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.","commonSituations":"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.","solutions":["Only call `dispatchTool` between a successful `startTurn` and the turn's terminal event; check `await session.snapshot()` (`activeTurnId !== null`) first.","Filter the OpenCode event stream by source sequence / turn so stale tool calls are dropped instead of dispatched.","If this occurs during reconnect replay, skip the event rather than dispatch — the turn's results were already finalized.","Start a turn (`startTurn`) before issuing tool calls if your integration drives tools proactively."],"exampleFix":"// before\nawait session.dispatchTool({ tool, callId, arguments }); // throws when no active turn\n\n// after\nconst snap = await session.snapshot();\nif (snap.activeTurnId) {\n  await session.dispatchTool({ tool, callId, arguments });\n}","handlingStrategy":"validation","validationCode":"const snap = await session.snapshot();\nif (!snap.activeTurnId) return; // drop out-of-turn tool call","typeGuard":"function hasActiveTurn(snap) { return typeof snap.activeTurnId === 'string' && snap.activeTurnId.length > 0; }","tryCatchPattern":"try {\n  await session.dispatchTool({ tool, callId, arguments });\n} catch (e) {\n  if (e.message === 'OpenCode tool call is not bound to an active turn') {\n    logger.warn({ callId }, 'dropped stale tool call outside active turn');\n  } else throw e;\n}","preventionTips":["Dispatch tools only between startTurn and the turn's terminal event.","Filter replayed SSE events by source sequence on reconnect.","Check `snapshot().activeTurnId` before dispatch.","Start a turn before proactively invoking tools."],"tags":["tool-call","turn-binding","opencode","event-replay"],"backgroundTag":"invalid-state-transition","analyzedSha":"01ad8584922b5d85292b1723cae71fa0d9b07a19","analyzedAt":"2026-09-10T03:14:50.855Z","contentChangedAt":"2026-09-10T03:14:50.855Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}