{"record":{"id":"6657b5b88fd4fcd0","repo":"paperclipai/paperclip","slug":"opencode-session-already-has-an-active-turn","errorCode":null,"errorMessage":"OpenCode session already has an active turn","messagePattern":"OpenCode session already has an active turn","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/paperclip-runner/src/drivers/opencode/opencode-server-driver.ts","lineNumber":545,"sourceCode":"    return this.#events;\n  }\n\n  startEventPump(): void {\n    void this.#recoverPendingRuntimeRequests()\n      .catch((error) =>\n        this.#emit(\"harness.diagnostic\", {\n          code: \"opencode_runtime_request_recovery_failed\",\n          message: redact(String(error), this.#runtime.sensitiveValues),\n        }),\n      )\n      .finally(() => this.#pumpEvents());\n  }\n\n  async startTurn(input: {\n    message: NativeUserMessage;\n  }): Promise<{ turnId: string }> {\n    if (this.#activeTurnId !== null)\n      throw new Error(\"OpenCode session already has an active turn\");\n    const turnId = `turn-${randomBytes(12).toString(\"hex\")}`;\n    this.#activeTurnId = turnId;\n    this.#emit(\"turn.submitted\", {\n      envelopeSchema: this.#taskEnvelope.schema,\n      text: input.message.text,\n    });\n    this.#emit(\"turn.accepted\", { turnId }, { turnId });\n    this.#emit(\"turn.started\", { status: \"inProgress\" }, { turnId });\n    const [providerID, ...modelParts] = this.#model.split(\"/\");\n    const modelID = modelParts.join(\"/\");\n    // A resumed OpenCode provider session already retains the original system\n    // instructions and task envelope in its conversation. Repeating both on\n    // every Paperclip continuation can overflow smaller context windows and\n    // OpenCode then completes with `finish: unknown` and zero tokens. The\n    // native model envelope still carries the authoritative wake delta,\n    // interaction responses, completion contract, and current issue context.\n    const prompt = this.#sendFullContext\n      ? JSON.stringify({","sourceCodeStart":527,"sourceCodeEnd":563,"githubUrl":"https://github.com/paperclipai/paperclip/blob/01ad8584922b5d85292b1723cae71fa0d9b07a19/packages/paperclip-runner/src/drivers/opencode/opencode-server-driver.ts#L527-L563","documentation":"This driver enforces a single-turn invariant per OpenCode harness session: `#activeTurnId` must be null before a new turn can be submitted. `startTurn` refuses to run when a prior turn is still considered active, preventing two concurrent prompt_async submissions against the same provider session. It is a guard against interleaving agent work, which OpenCode sessions do not support.","triggerScenarios":"Calling `session.startTurn({ message })` while `#activeTurnId !== null` — i.e. a previous `startTurn` completed (turn accepted) but its turn has not yet reached a terminal state (turn completed/aborted) that clears `#activeTurnId`. Typical: awaiting the turn without draining `events()` and calling again, or retrying `startTurn` after a timeout while the old turn still runs.","commonSituations":"A supervisor loop that re-submits a prompt on a heartbeat timeout without first aborting the in-flight turn; a retry after a network blip on `prompt_async` where the caller assumes the turn failed; parallel workers sharing one persisted session via `snapshot()`/restore and both calling `startTurn`.","solutions":["Wait for the current turn to finish: consume the session's `events()` iterable until a terminal turn event (`turn.completed`/`turn.aborted`) before calling `startTurn` again.","If the current turn is genuinely stuck, call `await session.interrupt({})` first (no turnId) to abort the OpenCode session, let the turn state clear, then start the new turn.","Check `pendingRuntimeRequests()` — an unanswered permission/question request can hold the turn open; resolve it via `resolveRuntimeRequest` before starting a new turn.","Ensure each OpenCode session object is used by exactly one run/worker; create a new session via the driver factory instead of reusing a busy one."],"exampleFix":"// before\nconst a = await session.startTurn({ message });\nconst b = await session.startTurn({ message }); // throws: turn a still active\n\n// after\nconst a = await session.startTurn({ message });\nfor await (const evt of session.events()) {\n  if (evt.type === 'turn.completed' || evt.type === 'turn.aborted') break;\n}\nconst b = await session.startTurn({ message });","handlingStrategy":"try-catch","validationCode":"const snap = await session.snapshot();\nif (snap.activeTurnId) throw new SkipStartError(snap.activeTurnId);","typeGuard":"function canStartTurn(s) { return s != null && typeof s.snapshot === 'function'; }","tryCatchPattern":"try {\n  await session.startTurn({ message });\n} catch (e) {\n  if (e.message === 'OpenCode session already has an active turn') {\n    await session.interrupt({});\n    await session.startTurn({ message });\n  } else throw e;\n}","preventionTips":["Always drive one turn to a terminal event before starting the next.","Never share a session instance across concurrent workers.","On retry timers, abort the prior turn before re-submitting.","Resolve pending runtime requests promptly so turns can finish."],"tags":["session-state","concurrency","opencode","turn-management"],"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"}