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

  1. Await the previous turn's completion (or its rejection) before starting another turn on the same live session.
  2. Serialize turn starts behind a queue/lock so #armTurnWaiter cannot run while a waiter is armed.
  3. 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 call

View on GitHub (pinned to 3f1d897a7c)