paperclipai/paperclip · error
OpenCode session already has an active turn
Error message
OpenCode session already has an active turn
What it means
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.
Source
Thrown at packages/paperclip-runner/src/drivers/opencode/opencode-server-driver.ts:545
return this.#events;
}
startEventPump(): void {
void this.#recoverPendingRuntimeRequests()
.catch((error) =>
this.#emit("harness.diagnostic", {
code: "opencode_runtime_request_recovery_failed",
message: redact(String(error), this.#runtime.sensitiveValues),
}),
)
.finally(() => this.#pumpEvents());
}
async startTurn(input: {
message: NativeUserMessage;
}): Promise<{ turnId: string }> {
if (this.#activeTurnId !== null)
throw new Error("OpenCode session already has an active turn");
const turnId = `turn-${randomBytes(12).toString("hex")}`;
this.#activeTurnId = turnId;
this.#emit("turn.submitted", {
envelopeSchema: this.#taskEnvelope.schema,
text: input.message.text,
});
this.#emit("turn.accepted", { turnId }, { turnId });
this.#emit("turn.started", { status: "inProgress" }, { turnId });
const [providerID, ...modelParts] = this.#model.split("/");
const modelID = modelParts.join("/");
// A resumed OpenCode provider session already retains the original system
// instructions and task envelope in its conversation. Repeating both on
// every Paperclip continuation can overflow smaller context windows and
// OpenCode then completes with `finish: unknown` and zero tokens. The
// native model envelope still carries the authoritative wake delta,
// interaction responses, completion contract, and current issue context.
const prompt = this.#sendFullContext
? JSON.stringify({View on GitHub (pinned to 01ad858492)
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.
Example fix
// before
const a = await session.startTurn({ message });
const b = await session.startTurn({ message }); // throws: turn a still active
// after
const a = await session.startTurn({ message });
for await (const evt of session.events()) {
if (evt.type === 'turn.completed' || evt.type === 'turn.aborted') break;
}
const b = await session.startTurn({ message }); Defensive patterns
Strategy: try-catch
Validate before calling
const snap = await session.snapshot(); if (snap.activeTurnId) throw new SkipStartError(snap.activeTurnId);
Type guard
function canStartTurn(s) { return s != null && typeof s.snapshot === 'function'; } Try / catch
try {
await session.startTurn({ message });
} catch (e) {
if (e.message === 'OpenCode session already has an active turn') {
await session.interrupt({});
await session.startTurn({ message });
} else throw e;
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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`.
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
- opencode_run_attach_busy
- stale OpenCode turn
- OpenCode request ${input.requestId} is already settling
- OpenCode evals require exact version 1.18.17; received ${ver
- [opencode-local] Remote model availability probe for "${mode
AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10).
Data as JSON: /api/errors/6657b5b88fd4fcd0.
Report an issue: GitHub.