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

  1. Check what now exists at context.lockPath (ls -la) and remove/resolve any foreign symlink or file
  2. Delete the stale checkpoint file so recovery restarts from scratch instead of resuming
  3. Ensure no concurrent cleanup job deletes lock directories while recovery runs
  4. 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

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


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