{"record":{"id":"dd6f53826ad2889c","repo":"paperclipai/paperclip","slug":"capability-live-attempt-active-turn","errorCode":"capability_live_attempt_active_turn","errorMessage":"capability_live_attempt_active_turn","messagePattern":"capability_live_attempt_active_turn","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/paperclip-runner/src/live/live-session.ts","lineNumber":1444,"sourceCode":"    await this.#persist();\n    return \"committed\";\n  }\n\n  async completeAttempt(\n    status: Exclude<CapabilityLiveAttemptStatus, \"running\" | \"terminated\">,\n    failureCode: string | null = null,\n  ): Promise<CapabilityLiveSessionSnapshot> {\n    const attempt = this.#attempts.find((candidate) => candidate.attemptId === this.#currentAttemptId);\n    if (attempt === undefined || attempt.status !== \"running\") {\n      throw new Error(\"capability_live_attempt_not_running\");\n    }\n    if (\n      status === \"succeeded\" &&\n      (this.#activeTurnId !== null ||\n        this.#turnWaiter !== null ||\n        this.#pendingTurnAdmission !== null)\n    ) {\n      throw new Error(\"capability_live_attempt_active_turn\");\n    }\n    attempt.status = status;\n    attempt.finishedAt = this.#now().toISOString();\n    attempt.failureCode = status === \"failed\"\n      ? requireNonEmpty(failureCode ?? \"attempt_failed\", \"attempt_failure_code\")\n      : null;\n    await this.#persist();\n    return this.snapshot();\n  }\n\n  async sendMessage(\n    message: string,\n    /** Launch-only diagnostics may opt out; qualification campaigns must not. */\n    options: { allowMissingUsage?: boolean } = {},\n  ): Promise<CapabilityLiveTurnResult> {\n    const value = message.trim();\n    if (value.length === 0) throw new Error(\"Capability live messages cannot be empty\");\n    if (this.#status === \"suspended\" || this.#transport === null) {","sourceCodeStart":1426,"sourceCodeEnd":1462,"githubUrl":"https://github.com/paperclipai/paperclip/blob/01ad8584922b5d85292b1723cae71fa0d9b07a19/packages/paperclip-runner/src/live/live-session.ts#L1426-L1462","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nsession.finishAttempt('succeeded'); // may throw while a turn is running\n// after\nawait session.waitForIdleTurn(); // resolves when #activeTurnId/#turnWaiter/#pendingTurnAdmission are null\nsession.finishAttempt('succeeded');","handlingStrategy":"try-catch","validationCode":"const idle = session.activeTurnId === null && session.turnWaiter === null && session.pendingTurnAdmission === null;\nif (!idle) throw new Error('attempt still has an active turn');","typeGuard":"function isIdle(session: { status: string; hasActiveTurn?: boolean }): boolean {\n  return !session.hasActiveTurn && session.status !== 'closed' && session.status !== 'failed';\n}","tryCatchPattern":"try {\n  session.finishAttempt('succeeded');\n} catch (e) {\n  if ((e as Error).message === 'capability_live_attempt_active_turn') {\n    await session.waitForIdleTurn();\n    session.finishAttempt('succeeded');\n  } else throw e;\n}","preventionTips":["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"],"tags":["concurrency","state-machine","lifecycle"],"backgroundTag":"invalid-state-transition","analyzedSha":"01ad8584922b5d85292b1723cae71fa0d9b07a19","analyzedAt":"2026-09-10T03:14:50.855Z","contentChangedAt":"2026-09-10T03:14:50.855Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}