{"record":{"id":"2bcd9d05aba13fa0","repo":"paperclipai/paperclip","slug":"stop-the-active-turn-before-reconnecting-the-codex","errorCode":null,"errorMessage":"stop the active turn before reconnecting the Codex transport","messagePattern":"stop the active turn before reconnecting the Codex transport","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/paperclip-runner/src/live/live-session.ts","lineNumber":2167,"sourceCode":"    });\n    await this.#persist();\n    return this.sendMessage(JSON.stringify({\n      schema: \"paperclip.semantic-interaction-result.v1\",\n      interactionId: input.interactionId,\n      kind: interaction.kind,\n      outcome: input.outcome,\n      result: input.result,\n      instruction: \"Continue using this typed interaction result in the same mock issue session.\",\n    }));\n  }\n\n  async reconnect(): Promise<void> {\n    if (\n      this.#activeTurnId !== null ||\n      this.#turnWaiter !== null ||\n      this.#pendingTurnAdmission !== null\n    ) {\n      throw new Error(\"stop the active turn before reconnecting the Codex transport\");\n    }\n    await this.#disconnect(\"reconnect\");\n    await this.#connect(true);\n  }\n\n  async suspend(reason = \"session suspended\"): Promise<void> {\n    if (this.#status === \"closed\" || this.#status === \"suspended\") return;\n    this.#clearIdleTimer();\n    const admission = this.#pendingTurnAdmission;\n    if (admission !== null) {\n      admission.cancellation = { reason, resumeLifecycle: false };\n      this.#status = \"suspending\";\n      await this.#persist();\n      // Closing first actively aborts an attach/start RPC whose response was\n      // lost. Waiting before close would let lifecycle shutdown deadlock on\n      // the very transport operation that teardown must terminate.\n      await this.#disconnect(reason);\n      await this.#boundAdmissionSettlementAfterClose(admission, reason);","sourceCodeStart":2149,"sourceCodeEnd":2185,"githubUrl":"https://github.com/paperclipai/paperclip/blob/01ad8584922b5d85292b1723cae71fa0d9b07a19/packages/paperclip-runner/src/live/live-session.ts#L2149-L2185","documentation":"This error is thrown by LiveSession.reconnect() in the Codex live-session transport. reconnect() tears down and re-establishes the transport, which is only safe when no turn is in flight: an active turn (#activeTurnId), a pending turn waiter, or a turn still awaiting admission would be orphaned by the disconnect. The library throws this error to prevent silently aborting an in-progress turn during reconnection.","triggerScenarios":"Calling session.reconnect() while a turn is executing, a caller is awaiting turn completion (#turnWaiter set), or a turn is queued awaiting admission (#pendingTurnAdmission set).","commonSituations":"App-level watchdog logic that reconnects on flaky network timers without first checking turn state; racing a reconnect against a submitTurn; reusing a session object after a turn timed out client-side while the session still considers it active.","solutions":["Wait for the active turn to complete (or call the stop/cancel-turn API) before invoking reconnect()","Check session turn state (active turn id / waiter) before scheduling a reconnect","Serialize reconnect attempts behind a mutex or queue so they cannot overlap turn submission","If the turn is wedged, stop/abort the turn and wait for its terminal event, then reconnect"],"exampleFix":"// before\nawait session.reconnect();\n// after\nif (session.hasActiveTurn) {\n  await session.stopTurn(); // or await session.waitForTurnCompletion()\n}\nawait session.reconnect();","handlingStrategy":"try-catch","validationCode":"function canReconnect(session) {\n  return !session.hasActiveTurn && !session.isWaitingForTurn && !session.hasPendingTurnAdmission;\n}","typeGuard":"function isIdle(session: LiveSession): boolean {\n  return session.activeTurnId === null;\n}","tryCatchPattern":"try {\n  await session.reconnect();\n} catch (err) {\n  if (err.message.includes(\"stop the active turn before reconnecting\")) {\n    await session.stopTurn();\n    await session.reconnect();\n  } else throw err;\n}","preventionTips":["Gate reconnects on session idle state, never on timers alone","Use a turn-completion event/promise before any transport lifecycle call","Serialize all transport lifecycle operations (connect/reconnect/disconnect) behind one queue","Add client-side turn timeouts that explicitly stop the turn before recovery logic runs"],"tags":["lifecycle","codex","session","reconnect"],"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"}