Yeachan-Heo/oh-my-codex · error · Error

autopilot.session_id must match the selected writable sessio

Error message

autopilot.session_id must match the selected writable session scope

What it means

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.

Source

Thrown at src/modes/base.ts:346

    baseStateDir,
  });
  const current = mode === 'ralph' && scope.sessionId
    ? await readModeStateForActiveDecision(mode, scope.sessionId, projectRoot)
    : explicitSessionId
      ? await readModeStateForExplicitSession(mode, explicitSessionId, projectRoot)
      : await readModeState(mode, projectRoot);
  if (!current) throw new Error(`Mode ${mode} not found`);
  await mkdir(scope.stateDir, { recursive: true });

  if (mode === 'ralph') {
    assertRalphUpdateMatchesSession(current, scope.sessionId);
  }

  const updatedBase = { ...current, ...updates };
  if (mode === 'autopilot') {
    const submittedSessionId = typeof updates.session_id === 'string' ? updates.session_id.trim() : '';
    if (submittedSessionId && scope.sessionId && submittedSessionId !== scope.sessionId) {
      throw new Error('autopilot.session_id must match the selected writable session scope');
    }
    const canonicalWorkspace = projectRoot ?? process.cwd();
    const submittedWorkingDirectory = typeof updates.workingDirectory === 'string' ? updates.workingDirectory.trim() : '';
    if (submittedWorkingDirectory && submittedWorkingDirectory !== canonicalWorkspace) {
      throw new Error('autopilot.workingDirectory must match the selected writable workspace');
    }
    if (scope.sessionId) updatedBase.session_id = scope.sessionId;
    updatedBase.workingDirectory = canonicalWorkspace;
    // Shared invariant, not a local copy: see src/state/handoff-carrier.ts for why a supplied
    // malformed carrier must be rejected before any merge normalizes it away.
    const suppliedHandoffs = updates.handoff_artifacts;
    assertValidHandoffCarriersIn(updates as Record<string, unknown>, 'supplied');
    // Also the PERSISTED state: a stored `state.handoff_artifacts` array survives this shallow merge
    // and the gate would read it, so validating only the incoming payload left it fail-open.
    assertValidHandoffCarriersIn(current as Record<string, unknown>, 'stored');
    const currentHandoffs = requirePersistedHandoffCarrier(current.handoff_artifacts, 'handoff_artifacts carrier');
    const nextHandoffs = requirePersistedHandoffCarrier(suppliedHandoffs, 'supplied handoff_artifacts carrier');
    if (Object.keys(currentHandoffs).length > 0 || Object.keys(nextHandoffs).length > 0) {

View on GitHub (pinned to 3ad79a8a6f)

Solutions

  1. Omit session_id from updates — the scope's session id is applied automatically (scope.sessionId is assigned right after the check)
  2. If you must pass it, pass exactly the scope.sessionId you used to create the writable scope
  3. Re-resolve the current writable session scope before building the updates payload

Example fix

// before
await updateModeState('autopilot', { session_id: cachedSessionId });
// after
await updateModeState('autopilot', { /* session_id omitted; derived from scope */ });
Defensive patterns

Strategy: validation

Validate before calling

const sid = (updates.session_id ?? '').trim();
if (sid && sid !== scope.sessionId) throw new Error('refusing: session_id mismatch');
await updateModeState('autopilot', updates);

Try / catch

try { await updateModeState('autopilot', updates); } catch (e) { if (e instanceof Error && e.message.includes('autopilot.session_id')) { /* drop session_id and retry */ } throw e; }

Prevention

When it happens

Trigger: Calling updateModeState (or updateAutopilotPipelineState) with mode 'autopilot' and an updates object whose non-empty session_id string differs from scope.sessionId after trimming.

Common situations: 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.

Related errors


AI-assisted analysis of Yeachan-Heo/oh-my-codex@3ad79a8a6f (2026-08-27). Data as JSON: /api/errors/6272daa37f5b2ed6. Report an issue: GitHub.