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

Autopilot handoff session_id does not match the selected ses

Error message

Autopilot handoff session_id does not match the selected session.

What it means

Thrown by assertBoundHandoffIdentity() during `autopilot advance` when the handoff JSON contains a session_id string that differs from the currently selected OMX session. This is a safety check that prevents a handoff produced in one session from being applied to a different session.

Source

Thrown at src/cli/autopilot.ts:57

  const valueFlags = new Set(['--task', '--session', '--to', '--handoff-json']);
  const words: string[] = [];
  for (let i = 0; i < args.length; i += 1) {
    if (valueFlags.has(args[i])) { i += 1; continue; }
    if (!args[i].startsWith('--')) words.push(args[i]);
  }
  return words.join(' ').trim();
}

async function jsonInput(raw: string): Promise<Record<string, unknown>> {
  const text = raw.trim().startsWith('{') ? raw : await readFile(raw, 'utf-8');
  const parsed = JSON.parse(text) as unknown;
  if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) throw new Error('--handoff-json must resolve to a JSON object.');
  return parsed as Record<string, unknown>;
}

function assertBoundHandoffIdentity(handoff: Record<string, unknown>, cwd: string, sessionId?: string): void {
  if (typeof handoff.session_id === 'string' && handoff.session_id !== sessionId) {
    throw new Error('Autopilot handoff session_id does not match the selected session.');
  }
  if (typeof handoff.workingDirectory === 'string' && handoff.workingDirectory !== cwd) {
    throw new Error('Autopilot handoff workingDirectory does not match the selected workspace.');
  }
  handoff.session_id = sessionId;
  handoff.workingDirectory = cwd;
}

async function readAutopilot(cwd: string, sessionId?: string) {
  return sessionId
    ? readModeStateForExplicitSession('autopilot', sessionId, cwd)
    : readModeState('autopilot', cwd);
}

/**
 * A terminalization that carried skipped gates must never read as clean success. The durable
 * marker is the machine token `complete-with-skipped-gates`; this renders the human string and
 * names each skipped gate with its missing evidence so the report is actionable.

View on GitHub (pinned to 3ad79a8a6f)

Solutions

  1. Regenerate the handoff from the current session (the session that will consume it) so session_id matches
  2. Remove or null out the session_id field in the handoff JSON — the check only applies to string values, and assertBoundHandoffIdentity will stamp the correct current sessionId
  3. Verify the selected session with the session status command before advancing and switch to the session the handoff was created for

Example fix

// before
{"session_id":"sess-old","task":"..."}
// after
{"task":"..."}  // session_id gets stamped from the current session
Defensive patterns

Strategy: validation

Validate before calling

const h = JSON.parse(fs.readFileSync(handoffPath,'utf-8'));
if (typeof h.session_id === 'string' && h.session_id !== currentSessionId) {
  delete h.session_id; // let CLI stamp the current session
  fs.writeFileSync(handoffPath, JSON.stringify(h));
}

Try / catch

try { await advance(handoff); } catch (e) { if (e.message.includes('session_id does not match')) regenerateHandoffFromCurrentSession(); else throw e; }

Prevention

When it happens

Trigger: Calling autopilot advance with a handoff payload whose session_id field (a string) does not equal the sessionId of the current session (as resolved from cwd/session selection). Undefined or non-string session_id values pass the check and are overwritten with the current sessionId.

Common situations: Replaying a handoff JSON captured from an earlier run after the session was recreated, running the advance command in a fresh shell where a different session is selected, or hand-editing a handoff file and leaving a stale session_id.

Related errors


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