paperclipai/paperclip · error

Capability live session already has an active turn

Error message

Capability live session already has an active turn

What it means

CapabilityLiveSession enforces a single-turn model: only one of #activeTurnId, #turnWaiter, or #pendingTurnAdmission may be outstanding. sendMessage throws when a turn is already active, waiting, or mid-admission, because concurrent user turns on one live session are unsupported.

Source

Thrown at packages/paperclip-runner/src/live/live-session.ts:1473

  async sendMessage(
    message: string,
    /** Launch-only diagnostics may opt out; qualification campaigns must not. */
    options: { allowMissingUsage?: boolean } = {},
  ): Promise<CapabilityLiveTurnResult> {
    const value = message.trim();
    if (value.length === 0) throw new Error("Capability live messages cannot be empty");
    if (this.#status === "suspended" || this.#transport === null) {
      await this.#connect(true);
    }
    if (this.#transport === null || this.#status === "closed" || this.#status === "failed") {
      throw new Error("Capability live session is not connected");
    }
    if (
      this.#activeTurnId !== null ||
      this.#turnWaiter !== null ||
      this.#pendingTurnAdmission !== null
    ) {
      throw new Error("Capability live session already has an active turn");
    }
    let settleAdmission!: () => void;
    const admission: PendingTurnAdmission = {
      transport: this.#transport,
      cancellation: null,
      settled: new Promise<void>((resolveSettled) => {
        settleAdmission = resolveSettled;
      }),
      settle: () => settleAdmission(),
    };
    this.#pendingTurnAdmission = admission;
    this.#status = "running";
    this.#clearIdleTimer();
    this.#turnEventCount = 0;
    this.#turnUsageEventCount = 0;
    // Publish the user entry before the first admission await. The UI must not
    // lose the submitted message merely because bounded preflight is slow.
    const userEntryId = this.#appendTranscript("user", value, null);

View on GitHub (pinned to 01ad858492)

Solutions

  1. Serialize sends: await the previous sendMessage promise before issuing the next
  2. Maintain a send mutex/queue around the session so only one turn is ever in flight
  3. Interrupt the current turn first if the new message must preempt it, then send
  4. Use separate sessions if genuinely concurrent turns are needed

Example fix

// before
session.sendMessage('a');
session.sendMessage('b'); // throws: turn 'a' still active
// after
await session.sendMessage('a');
await session.sendMessage('b');
Defensive patterns

Strategy: try-catch

Validate before calling

if (session.activeTurnId !== null || session.turnWaiter !== null || session.pendingTurnAdmission !== null) {
  throw new Error('turn already in flight');
}

Type guard

null

Try / catch

try {
  await session.sendMessage(msg);
} catch (e) {
  if ((e as Error).message === 'Capability live session already has an active turn') {
    await enqueueSend(msg); // retry after the current turn drains
  } else throw e;
}

Prevention

When it happens

Trigger: Calling sendMessage while a previous sendMessage turn has not completed; calling sendMessage while invokeTool's devtools turn is running; racing two sendMessage calls from different code paths without awaiting the first.

Common situations: UI double-submitting a chat message; a queue/dispatcher firing a new prompt before the prior turn resolved; automated scripts issuing overlapping prompts to the same session.

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


AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10). Data as JSON: /api/errors/872890c02c923868. Report an issue: GitHub.