{"record":{"id":"6272daa37f5b2ed6","repo":"Yeachan-Heo/oh-my-codex","slug":"autopilot-session-id-must-match-the-selected-writa","errorCode":null,"errorMessage":"autopilot.session_id must match the selected writable session scope","messagePattern":"autopilot\\.session_id must match the selected writable session scope","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/modes/base.ts","lineNumber":346,"sourceCode":"    baseStateDir,\n  });\n  const current = mode === 'ralph' && scope.sessionId\n    ? await readModeStateForActiveDecision(mode, scope.sessionId, projectRoot)\n    : explicitSessionId\n      ? await readModeStateForExplicitSession(mode, explicitSessionId, projectRoot)\n      : await readModeState(mode, projectRoot);\n  if (!current) throw new Error(`Mode ${mode} not found`);\n  await mkdir(scope.stateDir, { recursive: true });\n\n  if (mode === 'ralph') {\n    assertRalphUpdateMatchesSession(current, scope.sessionId);\n  }\n\n  const updatedBase = { ...current, ...updates };\n  if (mode === 'autopilot') {\n    const submittedSessionId = typeof updates.session_id === 'string' ? updates.session_id.trim() : '';\n    if (submittedSessionId && scope.sessionId && submittedSessionId !== scope.sessionId) {\n      throw new Error('autopilot.session_id must match the selected writable session scope');\n    }\n    const canonicalWorkspace = projectRoot ?? process.cwd();\n    const submittedWorkingDirectory = typeof updates.workingDirectory === 'string' ? updates.workingDirectory.trim() : '';\n    if (submittedWorkingDirectory && submittedWorkingDirectory !== canonicalWorkspace) {\n      throw new Error('autopilot.workingDirectory must match the selected writable workspace');\n    }\n    if (scope.sessionId) updatedBase.session_id = scope.sessionId;\n    updatedBase.workingDirectory = canonicalWorkspace;\n    // Shared invariant, not a local copy: see src/state/handoff-carrier.ts for why a supplied\n    // malformed carrier must be rejected before any merge normalizes it away.\n    const suppliedHandoffs = updates.handoff_artifacts;\n    assertValidHandoffCarriersIn(updates as Record<string, unknown>, 'supplied');\n    // Also the PERSISTED state: a stored `state.handoff_artifacts` array survives this shallow merge\n    // and the gate would read it, so validating only the incoming payload left it fail-open.\n    assertValidHandoffCarriersIn(current as Record<string, unknown>, 'stored');\n    const currentHandoffs = requirePersistedHandoffCarrier(current.handoff_artifacts, 'handoff_artifacts carrier');\n    const nextHandoffs = requirePersistedHandoffCarrier(suppliedHandoffs, 'supplied handoff_artifacts carrier');\n    if (Object.keys(currentHandoffs).length > 0 || Object.keys(nextHandoffs).length > 0) {","sourceCodeStart":328,"sourceCodeEnd":364,"githubUrl":"https://github.com/Yeachan-Heo/oh-my-codex/blob/3ad79a8a6fe6e95fdbb8c00e40716fffe4011ce2/src/modes/base.ts#L328-L364","documentation":"Autopilot mode state updates include a session_id, and the library enforces that any explicitly submitted session_id matches the writable session scope already selected for the operation. This prevents state updates from silently targeting a different session than the one the caller is scoped to.","triggerScenarios":"Calling updateModeState (or updateAutopilotPipelineState) with mode 'autopilot' and an updates object whose non-empty session_id string differs from scope.sessionId after trimming.","commonSituations":"Stale session id cached from a previous session, copy-pasting a session id from another environment, or reusing a payload built before the writable scope was resolved.","solutions":["Omit session_id from updates — the scope's session id is applied automatically (scope.sessionId is assigned right after the check)","If you must pass it, pass exactly the scope.sessionId you used to create the writable scope","Re-resolve the current writable session scope before building the updates payload"],"exampleFix":"// before\nawait updateModeState('autopilot', { session_id: cachedSessionId });\n// after\nawait updateModeState('autopilot', { /* session_id omitted; derived from scope */ });","handlingStrategy":"validation","validationCode":"const sid = (updates.session_id ?? '').trim();\nif (sid && sid !== scope.sessionId) throw new Error('refusing: session_id mismatch');\nawait updateModeState('autopilot', updates);","typeGuard":null,"tryCatchPattern":"try { await updateModeState('autopilot', updates); } catch (e) { if (e instanceof Error && e.message.includes('autopilot.session_id')) { /* drop session_id and retry */ } throw e; }","preventionTips":["Never populate session_id manually; let the scope assign it","Log scope.sessionId alongside every autopilot update for traceability"],"tags":["autopilot","session","state-management"],"backgroundTag":"session-scope-mismatch","analyzedSha":"3ad79a8a6fe6e95fdbb8c00e40716fffe4011ce2","analyzedAt":"2026-08-27T22:18:39.783Z","schemaVersion":2},"datasetVersion":"2026-08-28T00:17:15.603Z"}