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

checkpoint evidence is missing or foreign

Error message

checkpoint evidence is missing or foreign

What it means

The recovery checkpoint pinned evidence by dev/ino identity, but neither the recorded source file nor the claim file at the expected paths matches that identity (both missing or both replaced by different inodes). The library refuses to continue because the evidence backing the recovery decision is gone.

Source

Thrown at src/hooks/session.ts:1627

    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 {
      await transactionDependencies.fs.link(evidencePath, quarantinePath);
      const linked = await lstatRecoveryPath(quarantinePath);
      if (!linked || !sameRecoveryIdentity(linked, evidenceIdentity, 'file')) throw new Error('checkpoint quarantine link mismatch');
    }
    // Revalidate the bytes, dead owner and identities immediately before moving

View on GitHub (pinned to 3ad79a8a6f)

Solutions

  1. Inspect checkpoint.sourcePath and claimPath: confirm whether files exist and whether inodes changed
  2. If the evidence is truly gone, remove the stale checkpoint and let a fresh recovery run re-derive state
  3. Serialize recoveries (single runner / advisory lock) so evidence isn't consumed twice
  4. Avoid tools that rewrite-in-place or rename-replace files inside the lock directory
Defensive patterns

Strategy: validation

Validate before calling

const [src, claim] = await Promise.all([lstat(checkpoint.sourcePath), lstat(claimPath)].map(p => p.catch(() => null)));
const ok = identityMatches(src, evidenceIdentity) || identityMatches(claim, evidenceIdentity);
if (!ok) await fs.rm(checkpointPath, { force: true });

Try / catch

catch (e) { if (/checkpoint evidence is missing or foreign/.test(String(e))) { await clearCheckpointAndRetry(); } else throw e; }

Prevention

When it happens

Trigger: Resuming recovery after owner.json/source file was deleted or rewritten (new inode) at checkpoint.sourcePath and claimPath; editors that write via rename; concurrent recovery runs consuming the evidence.

Common situations: Atomic-save tooling replacing owner.json; two recovery processes racing; leftover checkpoints from old lock incarnations after the lock was recreated.

Related errors


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