paperclipai/paperclip · error
Capability live session already has a turn waiter
Error message
Capability live session already has a turn waiter
What it means
Concurrency guard in CapabilityLiveSession.#armTurnWaiter: the session allows only one outstanding turn waiter at a time; arming a second waiter while this.#turnWaiter is still set throws this sentinel. It fires when a new turn is started before the previous turn's waiter was consumed/rejected — i.e. overlapping turn requests on one live session, which would silently lose one turn's terminal result.
Solutions
- Await the previous turn's completion (or its rejection) before starting another turn on the same live session.
- Serialize turn starts behind a queue/lock so #armTurnWaiter cannot run while a waiter is armed.
- Check for a path that starts a turn without clearing the waiter on the terminal event.
Defensive patterns
Strategy: validation
When it happens
Trigger: Thrown at packages/paperclip-runner/src/live/live-session.ts:1997 when the library encounters an invalid state.
Common situations: See trigger scenarios.
AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-09-18).
Data as JSON: /api/errors/bea35009d2f9176e.
Report an issue: GitHub.
Appendix: source
Thrown at packages/paperclip-runner/src/live/live-session.ts:1997
}
if (persistenceError !== null) {
waiter?.reject(
persistenceError instanceof Error
? persistenceError
: new Error(String(persistenceError)),
);
throw persistenceError;
}
this.#emit({
turnId: null,
kind: "error",
message: redactCodexDiagnostic(error.message),
});
waiter?.reject(error);
}
#armTurnWaiter(): Promise<Omit<CapabilityLiveTurnResult, "snapshot">> {
if (this.#turnWaiter !== null) throw new Error("Capability live session already has a turn waiter");
const terminal = new Promise<Omit<CapabilityLiveTurnResult, "snapshot">>((resolve, reject) => {
const timer = setTimeout(() => {
const waiter = this.#turnWaiter;
this.#turnWaiter = null;
if (waiter !== null) {
this.#emit({
turnId: this.#activeTurnId,
kind: "error",
message: `Capability Codex turn timed out after ${this.#config.turnTimeoutMs}ms`,
});
}
reject(new Error(`Capability Codex turn timed out after ${this.#config.turnTimeoutMs}ms`));
}, this.#config.turnTimeoutMs);
this.#turnWaiter = { resolve, reject, timer, assistantText: "", draftId: null };
});
// A caller may await this promise only after another `await` of its own
// (see `reconcileActiveTurn`). The timer above can reject before that
// point, so attach a no-op handler here, at creation, on every callView on GitHub (pinned to 3f1d897a7c)