Yeachan-Heo/oh-my-codex · error · Error
checkpoint lock is missing or foreign
Error message
checkpoint lock is missing or foreign
What it means
During dead-lock recovery, the recovery checkpoint recorded that the canonical lock path was a directory, but on resume lstat shows the lock is missing, a symlink, or not a directory. The library throws to abort recovery because the on-disk state no longer matches the checkpoint's assumptions, making the recovery unsafe.
Source
Thrown at src/hooks/session.ts:1622
const recoveryToken = match?.[2];
const expectedSource = ownerToken && (checkpoint.sourcePath === join(context.lockPath, 'owner.json') || checkpoint.sourcePath === join(context.lockPath, `owner.${ownerToken}.tmp`));
const expectedPark = `${context.lockPath}.parked-lock-${recoveryToken}`;
const isLegacyV1 = checkpoint.version === 1 && checkpoint.lockIdentity === undefined && checkpoint.lockParkPath === undefined;
if (!match || !expectedSource || !checkpoint.identity || !['evidence-pending', 'evidence-quarantined', 'directory-pending'].includes(checkpoint.phase) || ![1, 2, 3].includes(checkpoint.version) || !isLegacyV1 && checkpoint.lockParkPath !== expectedPark || !sameRecoveryIdentity(checkpointStat, { dev: checkpointStat.dev, ino: checkpointStat.ino }, 'file')) throw new Error('invalid checkpoint');
const lockParkPath = checkpoint.lockParkPath ?? expectedPark;
const evidenceIdentity = checkpoint.evidenceIdentity ?? checkpoint.identity;
const claimPath = join(context.lockPath, `owner.${ownerToken}.${recoveryToken}.recovery`);
const lock = await lstatRecoveryPath(context.lockPath);
const parkedLock = await lstatRecoveryPath(lockParkPath);
if (lock && parkedLock) throw new Error('checkpoint lock paths are both present');
if (parkedLock) {
if (!checkpoint.lockIdentity || !sameRecoveryIdentity(parkedLock, checkpoint.lockIdentity, 'directory')) throw new Error('checkpoint parked lock identity mismatch');
const quarantinePath = `${context.lockPath}.quarantine.${ownerToken}.${recoveryToken}`;
const completed = await completeRecoveryCheckpoint(checkpointPath, checkpointBytes, { dev: checkpointStat.dev, ino: checkpointStat.ino });
if (!completed.completed) throw new Error(completed.reason);
return { status: 'dead', lockPath: context.lockPath, evidenceSource: 'owner.json', safeToRecover: true, action: 'quarantined', recovered: true, reason: 'Dead session pointer lock recovery checkpoint resumed.', quarantinePath };
}
if (!lock || lock.isSymbolicLink() || !lock.isDirectory()) throw new Error('checkpoint lock is missing or foreign');
const source = await lstatRecoveryPath(checkpoint.sourcePath);
const claim = await lstatRecoveryPath(claimPath);
const evidencePath = source && sameRecoveryIdentity(source, evidenceIdentity, 'file') ? checkpoint.sourcePath
: claim && sameRecoveryIdentity(claim, evidenceIdentity, 'file') ? claimPath : undefined;
if (!evidencePath) throw new Error('checkpoint evidence is missing or foreign');
if (claim && !sameRecoveryIdentity(claim, evidenceIdentity, 'file')) throw new Error('checkpoint claim is foreign');
// v1/v2 did not persist the directory identity. It is safe to derive only
// while the original directory still contains the exact recorded evidence.
const lockIdentity = checkpoint.lockIdentity ?? { dev: lock.dev, ino: lock.ino };
if (!sameRecoveryIdentity(lock, lockIdentity, 'directory')) throw new Error('checkpoint lock identity mismatch');
const ownerBytes = await transactionDependencies.fs.readFile(evidencePath, 'utf8');
const owner = await inspectLockOwnerFile(evidencePath);
if (owner.status !== 'dead' || owner.owner?.token !== ownerToken || checkpoint.evidenceBytes !== undefined && checkpoint.evidenceBytes !== ownerBytes) throw new Error('checkpoint owner evidence changed');
const quarantinePath = `${context.lockPath}.quarantine.${ownerToken}.${recoveryToken}`;
const quarantine = await lstatRecoveryPath(quarantinePath);
if (quarantine) {
if (!sameRecoveryIdentity(quarantine, evidenceIdentity, 'file')) throw new Error('checkpoint quarantine is foreign');
} else {View on GitHub (pinned to 3ad79a8a6f)
Solutions
- Check what now exists at context.lockPath (ls -la) and remove/resolve any foreign symlink or file
- Delete the stale checkpoint file so recovery restarts from scratch instead of resuming
- Ensure no concurrent cleanup job deletes lock directories while recovery runs
- Verify the lock root is a stable local directory, not symlinked or on a volatile mount
Example fix
// before
const result = await recoverDeadLock(context); // throws 'checkpoint lock is missing or foreign'
// after
import { lstat } from 'node:fs/promises';
const st = await lstat(context.lockPath).catch(() => null);
if (!st?.isDirectory()) {
await fs.rm(checkpointPath, { force: true }); // drop stale checkpoint, retry recovery
}
const result = await recoverDeadLock(context); Defensive patterns
Strategy: validation
Validate before calling
const st = await lstat(context.lockPath).catch(() => null);
if (!st?.isDirectory()) {
await fs.rm(checkpointPath, { force: true }); // stale checkpoint
} Type guard
const isRecoverableLockDir = (st: Stats | null): st is Stats => !!st && st.isDirectory() && !st.isSymbolicLink();
Try / catch
catch (e) { if (/checkpoint lock is missing or foreign/.test(String(e))) { /* drop checkpoint, retry recovery */ } else throw e; } Prevention
- Serialize recovery runs per lock path
- Keep external cleaners away from lock directories
- Check lock directory type before resuming checkpoints
When it happens
Trigger: Resuming a recovery checkpoint (owner.json evidence recovery) when the lock directory at context.lockPath was deleted, replaced by a symlink, or turned into a regular file between checkpoint creation and resume.
Common situations: Another process or human removed/recreated the lock directory while a checkpoint was parked; external cleanup scripts (git clean, tmp watchers) racing the recovery; NFS or symlinked lock roots.
Related errors
- checkpoint evidence is missing or foreign
- [native-assets] lock readback mismatch
- checkpoint claim is foreign
- invalid auth slot path
- ${label} is not a file: ${path}
AI-assisted analysis of Yeachan-Heo/oh-my-codex@3ad79a8a6f (2026-08-27).
Data as JSON: /api/errors/1211de4bc0e10ea1.
Report an issue: GitHub.