{"record":{"id":"4cfb47c1450792ac","repo":"paperclipai/paperclip","slug":"codex-turn-response-omitted-turn-id","errorCode":null,"errorMessage":"Codex turn response omitted turn.id","messagePattern":"Codex turn response omitted turn\\.id","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/paperclip-runner/src/drivers/codex/codex-harness-session.ts","lineNumber":262,"sourceCode":"      throw error;\n    } finally {\n      this.turnStartPending = false;\n      // Release a terminal notification that arrived and parked itself\n      // while this turn/start was in flight. This runs before turn.accepted\n      // below, in the same synchronous continuation, so a released waiter\n      // never observes the terminal turn ahead of turn.accepted.\n      releaseTurnStartSettled();\n    }\n    this.assertProtocolIntegrity();\n    const turn = record(response.turn);\n    const turnId = text(turn.id);\n    if (turnId.length === 0) {\n      // A start notification is only optimistic until the response validates.\n      // Clear it before released semantic/terminal waiters can observe an\n      // active turn for a request that was never accepted.\n      this.activeTurnId = null;\n      this.turnStarted = false;\n      throw new Error(\"Codex turn response omitted turn.id\");\n    }\n    if (this.activeTurnId !== null && this.activeTurnId !== turnId) {\n      this.failProtocol(\n        \"turn_start_mismatch\",\n        \"turn/start response disagreed with turn/started\",\n      );\n      throw new Error(\"Codex turn identity changed during start\");\n    }\n    this.activeTurnId ??= turnId;\n    if (dispositionOnlyRecovery) {\n      this.dispositionOnlyRecoveryTurnId = turnId;\n    }\n    this.emit(\"turn.accepted\", { turnId }, { turnId });\n    if (this.interruptQueued) {\n      this.interruptQueued = false;\n      await this.#sendInterrupt(turnId, \"queued_before_start\");\n    }\n    return {","sourceCodeStart":244,"sourceCodeEnd":280,"githubUrl":"https://github.com/paperclipai/paperclip/blob/01ad8584922b5d85292b1723cae71fa0d9b07a19/packages/paperclip-runner/src/drivers/codex/codex-harness-session.ts#L244-L280","documentation":"After sending the turn/start JSON-RPC request, startTurn reads response.turn.id; if it is missing or empty, no turn was actually accepted by the Codex app-server, so the method rolls back the optimistic turn-started state and throws this error. The rollback ensures semantic/terminal waiters released by the settled turn/start cannot observe an active turn for a request the provider never accepted.","triggerScenarios":"The turn/start RPC resolves successfully but its response payload has no turn object or turn.id is an empty/non-string value — e.g. an app-server version returning a different response shape, a proxy stripping fields, or the provider rejecting the turn without a JSON-RPC error.","commonSituations":"Upgrading or downgrading the Codex app-server so turn/start's response schema changed; running against a mock or misconfigured transport; provider-side rejection surfaced as a success-shaped empty response instead of a protocol error.","solutions":["Check the Codex app-server version and pin one whose turn/start response includes turn.id; update paperclip-runner if a schema change requires a fix.","Log the raw turn/start response to confirm the shape; if a proxy/middleware is in the path, verify it is not dropping the turn field.","Retry startTurn after verifying session state was rolled back (activeTurnId is null); if it persists, recreate the session."],"exampleFix":"// before (naive caller)\nconst { turnId } = await session.startTurn({ message });\n// after\ntry {\n  const { turnId } = await session.startTurn({ message });\n} catch (e) {\n  if (e.message === \"Codex turn response omitted turn.id\") {\n    await recreateSessionAndResubmit();\n  } else throw e;\n}","handlingStrategy":"try-catch","validationCode":null,"typeGuard":"function turnResponseHasId(response: unknown): response is { turn: { id: string } } {\n  const r = response as { turn?: { id?: unknown } };\n  return typeof r?.turn?.id === \"string\" && r.turn.id.length > 0;\n}","tryCatchPattern":"try {\n  const { turnId } = await session.startTurn({ message });\n} catch (e) {\n  if (e.message === \"Codex turn response omitted turn.id\") {\n    // optimistic state was rolled back by the library; safe to resubmit\n    // after verifying the app-server version / response shape\n    await resubmitTurnAfterShapeCheck();\n  } else throw e;\n}","preventionTips":["Pin and smoke-test the Codex app-server version against expected turn/start response shape before upgrading.","Log raw turn/start responses in staging to catch shape drift early.","Ensure no proxy/middleware between runner and app-server mutates RPC payloads."],"tags":["protocol","codex","unexpected-response","rpc"],"backgroundTag":"unexpected-response-shape","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"}