{"record":{"id":"168015259281a27b","repo":"paperclipai/paperclip","slug":"stale-opencode-turn","errorCode":null,"errorMessage":"stale OpenCode turn","messagePattern":"stale OpenCode turn","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/paperclip-runner/src/drivers/opencode/opencode-server-driver.ts","lineNumber":604,"sourceCode":"          tools: { question: true },\n          ...(this.#sendFullContext\n            ? { system: this.#systemInstructions }\n            : {}),\n          parts: [{ type: \"text\", text: prompt }],\n        }),\n      },\n    );\n    this.#sendFullContext = false;\n    return { turnId };\n  }\n\n  async interrupt(input: { turnId?: string; reason?: string }): Promise<void> {\n    if (\n      input.turnId &&\n      this.#activeTurnId &&\n      input.turnId !== this.#activeTurnId\n    )\n      throw new Error(\"stale OpenCode turn\");\n    await api(\n      this.#fetch,\n      this.#runtime,\n      `/session/${encodeURIComponent(this.#providerSessionId)}/abort`,\n      { method: \"POST\" },\n    );\n  }\n\n  pendingRuntimeRequests(): HarnessRuntimeRequest[] {\n    return [...this.#pendingRuntimeRequests.values()].map(({ request }) =>\n      structuredClone(request),\n    );\n  }\n\n  async resolveRuntimeRequest(input: {\n    requestId: string;\n    turnId: string;\n    resolution: HarnessRuntimeRequestResolution;","sourceCodeStart":586,"sourceCodeEnd":622,"githubUrl":"https://github.com/paperclipai/paperclip/blob/01ad8584922b5d85292b1723cae71fa0d9b07a19/packages/paperclip-runner/src/drivers/opencode/opencode-server-driver.ts#L586-L622","documentation":"`interrupt` aborts the OpenCode session via `/session/{id}/abort`. If the caller passes a `turnId` that does not match the currently active turn id tracked by the driver, the driver treats the request as targeting a turn that no longer exists and throws instead of aborting, protecting the live turn from being cancelled by stale bookkeeping.","triggerScenarios":"Calling `session.interrupt({ turnId })` with a turnId from an earlier, already-finished turn while a newer turn is active (`input.turnId !== this.#activeTurnId`). Happens when cached turn ids are reused after reconnect, or when a persisted snapshot's `activeTurnId` is out of date relative to the live session.","commonSituations":"A retry worker holds a stale turn id after the original turn already completed; two processes restored the same session from `snapshot()` and hold different views of the active turn; UI shows an old run whose turn id was superseded and the user clicks 'Stop'.","solutions":["Fetch the current active turn id via `await session.snapshot()` (`activeTurnId` field) and pass that to `interrupt`.","If you intend to abort regardless of turn bookkeeping, call `interrupt({})` without a `turnId` — the guard is skipped.","If the turn already ended, no interrupt is needed; drop the stale turn id and skip the abort call.","After restoring a session from a persisted snapshot, resynchronize turn ids from live events before issuing turn-scoped calls."],"exampleFix":"// before\nawait session.interrupt({ turnId: staleTurnId }); // throws 'stale OpenCode turn'\n\n// after\nconst snap = await session.snapshot();\nif (snap.activeTurnId) await session.interrupt({ turnId: snap.activeTurnId });\nelse await session.interrupt({});","handlingStrategy":"validation","validationCode":"const snap = await session.snapshot();\nconst isValid = !input.turnId || input.turnId === snap.activeTurnId;\nif (!isValid) return; // nothing to interrupt","typeGuard":"function isCurrentTurn(snap, turnId) { return turnId === undefined || turnId === snap.activeTurnId; }","tryCatchPattern":"try {\n  await session.interrupt({ turnId });\n} catch (e) {\n  if (e.message === 'stale OpenCode turn') {\n    const snap = await session.snapshot();\n    if (snap.activeTurnId) await session.interrupt({ turnId: snap.activeTurnId });\n  } else throw e;\n}","preventionTips":["Read turn ids from `snapshot()` or live events, never from cache.","Skip interrupts for turns already known to be terminal.","After restoring sessions, resynchronize turn state before scoped calls.","Omit turnId when you intend an unconditional abort."],"tags":["session-state","stale-turn","opencode","interrupt"],"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-14T00:17:10.932Z"}