paperclipai/paperclip · error
capability_live_attempt_active_turn
capability_live_attempt_active_turn
Error message
capability_live_attempt_active_turn
What it means
CapabilityLiveSession.finishAttempt (or equivalent attempt-terminating path) enforces that a live session attempt cannot be marked 'succeeded' while a turn is still in flight. The guard checks #activeTurnId, #turnWaiter, and #pendingTurnAdmission; if any is set, the success transition is illegal because the session still has work that has not settled.
Source
Thrown at packages/paperclip-runner/src/live/live-session.ts:1444
await this.#persist();
return "committed";
}
async completeAttempt(
status: Exclude<CapabilityLiveAttemptStatus, "running" | "terminated">,
failureCode: string | null = null,
): Promise<CapabilityLiveSessionSnapshot> {
const attempt = this.#attempts.find((candidate) => candidate.attemptId === this.#currentAttemptId);
if (attempt === undefined || attempt.status !== "running") {
throw new Error("capability_live_attempt_not_running");
}
if (
status === "succeeded" &&
(this.#activeTurnId !== null ||
this.#turnWaiter !== null ||
this.#pendingTurnAdmission !== null)
) {
throw new Error("capability_live_attempt_active_turn");
}
attempt.status = status;
attempt.finishedAt = this.#now().toISOString();
attempt.failureCode = status === "failed"
? requireNonEmpty(failureCode ?? "attempt_failed", "attempt_failure_code")
: null;
await this.#persist();
return this.snapshot();
}
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) {View on GitHub (pinned to 01ad858492)
Solutions
- Wait for the active turn (or turn waiter / pending admission) to settle before marking the attempt succeeded
- Interrupt or cancel the active turn first (session interrupt path), then finish the attempt
- If the attempt actually failed, finish with status='failed' which is allowed while a turn is active
- Audit the ordering in supervisor/teardown code so attempt completion always happens after turn drain
Example fix
// before
session.finishAttempt('succeeded'); // may throw while a turn is running
// after
await session.waitForIdleTurn(); // resolves when #activeTurnId/#turnWaiter/#pendingTurnAdmission are null
session.finishAttempt('succeeded'); Defensive patterns
Strategy: try-catch
Validate before calling
const idle = session.activeTurnId === null && session.turnWaiter === null && session.pendingTurnAdmission === null;
if (!idle) throw new Error('attempt still has an active turn'); Type guard
function isIdle(session: { status: string; hasActiveTurn?: boolean }): boolean {
return !session.hasActiveTurn && session.status !== 'closed' && session.status !== 'failed';
} Try / catch
try {
session.finishAttempt('succeeded');
} catch (e) {
if ((e as Error).message === 'capability_live_attempt_active_turn') {
await session.waitForIdleTurn();
session.finishAttempt('succeeded');
} else throw e;
} Prevention
- Always await in-flight sendMessage/invokeTool promises before finishing an attempt
- Expose and check an isIdle/busy helper on the session in supervisor code
- Treat attempt success as the last step of a teardown sequence, after turn drain
When it happens
Trigger: Calling the attempt-finish API with status='succeeded' while a sendMessage/invokeTool turn is running, a turn waiter is awaiting completion, or a turn admission is still pending. Marking success from a concurrent teardown path without first interrupting/draining the active turn.
Common situations: Race between a session supervisor deciding the attempt succeeded and a still-open Codex turn; cleanup code that closes the attempt before resolving the active turn; tests that forget to await sendMessage before finishing the attempt.
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
- ACPX provider ownership admission is closed
- capability_live_attempt_not_running
- Capability live session already has an active turn
- Capability live turn admission was abandoned during teardown
- provider turn ended with status ${turn.status}
AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10).
Data as JSON: /api/errors/dd6f53826ad2889c.
Report an issue: GitHub.