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
- 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
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
- Never populate session_id manually; let the scope assign it
- Log scope.sessionId alongside every autopilot update for traceability
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
- Missing value for ${flag}.
- --handoff-json must resolve to a JSON object.
- Autopilot handoff session_id does not match the selected ses
- Autopilot handoff workingDirectory does not match the select
- Missing --task.
AI-assisted analysis of Yeachan-Heo/oh-my-codex@3ad79a8a6f (2026-08-27).
Data as JSON: /api/errors/6272daa37f5b2ed6.
Report an issue: GitHub.