{"record":{"id":"b5268a89a796cc6f","repo":"Yeachan-Heo/oh-my-codex","slug":"checkpoint-evidence-is-missing-or-foreign","errorCode":null,"errorMessage":"checkpoint evidence is missing or foreign","messagePattern":"checkpoint evidence is missing or foreign","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/hooks/session.ts","lineNumber":1627,"sourceCode":"    const lockParkPath = checkpoint.lockParkPath ?? expectedPark;\n    const evidenceIdentity = checkpoint.evidenceIdentity ?? checkpoint.identity;\n    const claimPath = join(context.lockPath, `owner.${ownerToken}.${recoveryToken}.recovery`);\n    const lock = await lstatRecoveryPath(context.lockPath);\n    const parkedLock = await lstatRecoveryPath(lockParkPath);\n    if (lock && parkedLock) throw new Error('checkpoint lock paths are both present');\n    if (parkedLock) {\n      if (!checkpoint.lockIdentity || !sameRecoveryIdentity(parkedLock, checkpoint.lockIdentity, 'directory')) throw new Error('checkpoint parked lock identity mismatch');\n      const quarantinePath = `${context.lockPath}.quarantine.${ownerToken}.${recoveryToken}`;\n      const completed = await completeRecoveryCheckpoint(checkpointPath, checkpointBytes, { dev: checkpointStat.dev, ino: checkpointStat.ino });\n      if (!completed.completed) throw new Error(completed.reason);\n      return { status: 'dead', lockPath: context.lockPath, evidenceSource: 'owner.json', safeToRecover: true, action: 'quarantined', recovered: true, reason: 'Dead session pointer lock recovery checkpoint resumed.', quarantinePath };\n    }\n    if (!lock || lock.isSymbolicLink() || !lock.isDirectory()) throw new Error('checkpoint lock is missing or foreign');\n    const source = await lstatRecoveryPath(checkpoint.sourcePath);\n    const claim = await lstatRecoveryPath(claimPath);\n    const evidencePath = source && sameRecoveryIdentity(source, evidenceIdentity, 'file') ? checkpoint.sourcePath\n      : claim && sameRecoveryIdentity(claim, evidenceIdentity, 'file') ? claimPath : undefined;\n    if (!evidencePath) throw new Error('checkpoint evidence is missing or foreign');\n    if (claim && !sameRecoveryIdentity(claim, evidenceIdentity, 'file')) throw new Error('checkpoint claim is foreign');\n    // v1/v2 did not persist the directory identity. It is safe to derive only\n    // while the original directory still contains the exact recorded evidence.\n    const lockIdentity = checkpoint.lockIdentity ?? { dev: lock.dev, ino: lock.ino };\n    if (!sameRecoveryIdentity(lock, lockIdentity, 'directory')) throw new Error('checkpoint lock identity mismatch');\n    const ownerBytes = await transactionDependencies.fs.readFile(evidencePath, 'utf8');\n    const owner = await inspectLockOwnerFile(evidencePath);\n    if (owner.status !== 'dead' || owner.owner?.token !== ownerToken || checkpoint.evidenceBytes !== undefined && checkpoint.evidenceBytes !== ownerBytes) throw new Error('checkpoint owner evidence changed');\n    const quarantinePath = `${context.lockPath}.quarantine.${ownerToken}.${recoveryToken}`;\n    const quarantine = await lstatRecoveryPath(quarantinePath);\n    if (quarantine) {\n      if (!sameRecoveryIdentity(quarantine, evidenceIdentity, 'file')) throw new Error('checkpoint quarantine is foreign');\n    } else {\n      await transactionDependencies.fs.link(evidencePath, quarantinePath);\n      const linked = await lstatRecoveryPath(quarantinePath);\n      if (!linked || !sameRecoveryIdentity(linked, evidenceIdentity, 'file')) throw new Error('checkpoint quarantine link mismatch');\n    }\n    // Revalidate the bytes, dead owner and identities immediately before moving","sourceCodeStart":1609,"sourceCodeEnd":1645,"githubUrl":"https://github.com/Yeachan-Heo/oh-my-codex/blob/3ad79a8a6fe6e95fdbb8c00e40716fffe4011ce2/src/hooks/session.ts#L1609-L1645","documentation":"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.","triggerScenarios":"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.","commonSituations":"Atomic-save tooling replacing owner.json; two recovery processes racing; leftover checkpoints from old lock incarnations after the lock was recreated.","solutions":["Inspect checkpoint.sourcePath and claimPath: confirm whether files exist and whether inodes changed","If the evidence is truly gone, remove the stale checkpoint and let a fresh recovery run re-derive state","Serialize recoveries (single runner / advisory lock) so evidence isn't consumed twice","Avoid tools that rewrite-in-place or rename-replace files inside the lock directory"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"const [src, claim] = await Promise.all([lstat(checkpoint.sourcePath), lstat(claimPath)].map(p => p.catch(() => null)));\nconst ok = identityMatches(src, evidenceIdentity) || identityMatches(claim, evidenceIdentity);\nif (!ok) await fs.rm(checkpointPath, { force: true });","typeGuard":null,"tryCatchPattern":"catch (e) { if (/checkpoint evidence is missing or foreign/.test(String(e))) { await clearCheckpointAndRetry(); } else throw e; }","preventionTips":["Never rewrite-in-place files used as recovery evidence","Run one recovery at a time","Discard checkpoints older than the current lock incarnation"],"tags":["lock-recovery","checkpoint","file-identity","race-condition"],"backgroundTag":"stale-checkpoint-recovery","analyzedSha":"3ad79a8a6fe6e95fdbb8c00e40716fffe4011ce2","analyzedAt":"2026-08-27T22:18:39.783Z","schemaVersion":2},"datasetVersion":"2026-08-28T00:17:15.603Z"}