{"record":{"id":"b24f58cd35331809","repo":"paperclipai/paperclip","slug":"capability-live-turn-start-completed-after-admissi","errorCode":null,"errorMessage":"Capability live turn start completed after admission teardown","messagePattern":"Capability live turn start completed after admission teardown","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/paperclip-runner/src/live/live-session.ts","lineNumber":1577,"sourceCode":"        runtimeWorkspaceRoots: [this.#config.workingDirectory],\n        input: [userInput(value)],\n      });\n    } catch (error) {\n      if (this.#pendingTurnAdmission !== admission) {\n        await terminal.catch(() => undefined);\n        throw error;\n      }\n      if (admission.cancellation !== null) {\n        return this.#completeInterruptedAdmission(admission, userEntryId);\n      }\n      this.#rejectTurn(error);\n      this.#releaseTurnAdmission(admission);\n      await terminal.catch(() => undefined);\n      throw error;\n    }\n    if (this.#pendingTurnAdmission !== admission) {\n      await terminal.catch(() => undefined);\n      throw new Error(\"Capability live turn start completed after admission teardown\");\n    }\n    const turnId = text(record(response.turn).id);\n    if (turnId.length === 0) {\n      this.#rejectTurn(new Error(\"Codex turn response omitted turn.id\"));\n      this.#releaseTurnAdmission(admission);\n      throw new Error(\"Codex turn response omitted turn.id\");\n    }\n    if (this.#activeTurnId !== null && this.#activeTurnId !== turnId) {\n      this.#rejectTurn(new Error(\"Codex turn identity changed during start\"));\n      this.#releaseTurnAdmission(admission);\n      throw new Error(\"Codex turn identity changed during start\");\n    }\n    if (this.#turnWaiter !== null) this.#activeTurnId = turnId;\n    this.#bindAdmissionUser(userEntryId, turnId);\n    const draftId = (this.#turnWaiter as TurnWaiter | null)?.draftId ?? null;\n    const draft =\n      draftId === null ? undefined : this.#transcript.find((entry) => entry.id === draftId);\n    if (draft !== undefined && draft.turnId === null) draft.turnId = turnId;","sourceCodeStart":1559,"sourceCodeEnd":1595,"githubUrl":"https://github.com/paperclipai/paperclip/blob/01ad8584922b5d85292b1723cae71fa0d9b07a19/packages/paperclip-runner/src/live/live-session.ts#L1559-L1595","documentation":"After the Codex turn.start RPC resolves, sendMessage re-checks that its admission is still the session's current pending admission. If teardown released the admission while the start request was in flight, the turn result arrives too late to be bound, so the method cancels the terminal promise, releases resources, and throws.","triggerScenarios":"Session close/fail/interrupt completes between admission and the return of the turn.start RPC response; a slow turn.start response overlapping a teardown triggered elsewhere.","commonSituations":"Slow agent process responding to turn.start while a user or supervisor tears the session down; flaky transport causing long start latency and a concurrent timeout-triggered close.","solutions":["Avoid closing/failing the session while a turn.start request is outstanding","Treat the thrown error as 'turn not started' and abandon the old session; retry on a fresh session","Await sendMessage before invoking any teardown path; use lifecycle coordination (e.g. a busy flag)","Investigate why turn.start was slow if this happens without explicit teardown"],"exampleFix":"// before\nconst p = session.sendMessage(msg);\nsession.fail(new Error('timeout')); // races turn.start\nawait p; // throws 'completed after admission teardown'\n// after\nawait session.sendMessage(msg); // then decide about teardown\nsession.fail(new Error('timeout'));","handlingStrategy":"try-catch","validationCode":"if (session.status === 'closed' || session.status === 'failed') throw new Error('session teardown in progress; abort send');","typeGuard":"null","tryCatchPattern":"try {\n  await session.sendMessage(msg);\n} catch (e) {\n  if ((e as Error).message === 'Capability live turn start completed after admission teardown') {\n    session = await createLiveSession(opts); // retry on a fresh session\n  } else throw e;\n}","preventionTips":["Hold a teardown lock that sendMessage acquires so close/fail cannot interleave with turn.start","Add timeouts on turn.start so slow starts do not invite concurrent teardowns","Log admission lifecycle to spot racing teardown paths"],"tags":["concurrency","race-condition","rpc"],"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-14T00:17:10.932Z"}