{"record":{"id":"fb69cb4f03be4fdf","repo":"affaan-m/ECC","slug":"capsule-lock-lost","errorCode":"capsule.lock_lost","errorMessage":"append lock disappeared before release","messagePattern":"append lock disappeared before release","errorType":"error_code","errorClass":"CapsuleError","httpStatus":null,"severity":"error","filePath":"scripts/lib/eval-harness/capsule.js","lineNumber":80,"sourceCode":"  const fields = ['schema', 'run_id', 'capsule_id', 'harness_version', 'task_family'];\n  for (const [index, entry] of entries.entries()) {\n    if (fields.some(field => entry[field] !== meta[field])) {\n      return { ok: false, code: 'capsule.metadata_mismatch', reason: `metadata identity differs from journal entry ${index}`, failed_at: index };\n    }\n  }\n  return null;\n}\n\nfunction releaseOwnedLock(lockPath, fd, identity) {\n  let inspectionDenied;\n  try {\n    // Keep the original descriptor open while checking ownership so its inode\n    // cannot be reused. Preserve a replacement detected before release; this\n    // check is not atomic against noncooperating filesystem mutation.\n    if (identity) {\n      let current;\n      try { current = fs.lstatSync(lockPath); } catch (error) {\n        if (error.code === 'ENOENT') throw new CapsuleError('capsule.lock_lost', 'append lock disappeared before release');\n        if (error.code !== 'EPERM') throw error;\n        inspectionDenied = error;\n      }\n      if (!inspectionDenied) {\n        if (!current.isFile() || current.dev !== identity.dev || current.ino !== identity.ino) {\n          throw new CapsuleError('capsule.lock_lost', 'append lock ownership changed before release');\n        }\n        fs.unlinkSync(lockPath);\n      }\n    }\n  } finally {\n    fs.closeSync(fd);\n  }\n  if (inspectionDenied) {\n    // Windows may deny stat while a removed file awaits its last handle close.\n    // Only confirmed absence changes the error. Never unlink after closing:\n    // the pathname could now belong to another owner, even with a reused inode.\n    try { fs.lstatSync(lockPath); } catch (error) {","sourceCodeStart":62,"sourceCodeEnd":98,"githubUrl":"https://github.com/affaan-m/ECC/blob/8321021c54d670126ce3b2969d5deb880b4b0c2a/scripts/lib/eval-harness/capsule.js#L62-L98","documentation":"releaseOwnedLock in scripts/lib/eval-harness/capsule.js verifies, via lstat, that the append lock file still exists and still belongs to this owner (matching dev/ino captured at lock time) before unlinking it. It throws CapsuleError('capsule.lock_lost', 'append lock disappeared before release') when the lock pathname no longer exists (ENOENT) at release time, meaning some other process removed it while we held the descriptor. The harness deliberately never infers stale ownership, so a vanished lock aborts instead of unlinking a path that might now belong to someone else.","triggerScenarios":"Calling withAppendLock(dir, op) where, during the operation, another process deletes dir/.append-lock (or the whole directory is removed/recreated), so at release time fs.lstatSync(lockPath) fails with ENOENT. Also occurs when a cleanup script, 'rm -rf', or an external stale-lock sweeper removes the lock file mid-operation.","commonSituations":"A teammate or parallel CI job running its own lock cleanup ('delete stale *.lock files') concurrently, a temp-directory cleaner wiping the capsule dir mid-run, two machines sharing the working directory over a network filesystem with divergent views, or the capsule directory being deleted by a later pipeline stage while an earlier append is still finishing.","solutions":["Find and stop whatever deleted the lock file: audit concurrent cleanups, stale-lock sweepers, or 'rm -rf' of the capsule directory overlapping the append operation, and serialize them against withAppendLock.","Re-run the failed operation: the lock_lost error means mutual exclusion is no longer guaranteed, so discard the partial result and retry once no competing process is active.","Do not run multiple agents/CI jobs against the same capsule directory simultaneously; give each run its own directory or use a cross-process coordination step.","On network/shared filesystems, run the harness on a local filesystem — NFS/SMB can lose or desynchronize lock files and dev/ino identity."],"exampleFix":"// before: parallel jobs sharing one capsule dir, cleanup deletes .append-lock mid-run\nparallel([appendCapsule(sharedDir), cleanupStaleLocks(sharedDir)]);\n\n// after: exclusive ownership — no external cleanup while locked\nawait withAppendLock(sharedDir, async () => { await appendCapsule(sharedDir); }); // cleanup runs only between operations","handlingStrategy":"retry","validationCode":"// before appending, ensure no competing cleanup can touch the lock:\n// confirm the lock exists and nothing else owns the directory\nif (!fs.existsSync(path.join(dir, '.append-lock')) && lockHeldElsewhere(dir)) {\n  throw new Error('capsule dir is in use or being cleaned; retry later');\n}","typeGuard":null,"tryCatchPattern":"try {\n  await withAppendLock(dir, op);\n} catch (error) {\n  if (error instanceof CapsuleError && error.code === 'capsule.lock_lost') {\n    // another process removed the lock: results are untrustworthy —\n    // ensure exclusivity, discard partial state, then retry the operation\n    await cleanupAndRetry(dir, op);\n  } else throw error;\n}","preventionTips":["Never run external lock/stale-file cleanup concurrently with withAppendLock on the same directory.","Give each concurrent agent or CI job its own capsule directory.","Run on a local filesystem, not NFS/SMB, to keep lock identity consistent.","Treat capsule.lock_lost as a failed run: discard partial appends and re-run."],"tags":["filesystem","locking","concurrency","race-condition"],"backgroundTag":"internal-invariant-violation","analyzedSha":"8321021c54d670126ce3b2969d5deb880b4b0c2a","analyzedAt":"2026-09-16T10:08:13.343Z","contentChangedAt":"2026-09-16T10:08:13.343Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}