affaan-m/ECC · error · CapsuleError
capsule.lock_lost
capsule.lock_lost
Error message
append lock disappeared before release
What it means
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.
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.
Example fix
// before: parallel jobs sharing one capsule dir, cleanup deletes .append-lock mid-run
parallel([appendCapsule(sharedDir), cleanupStaleLocks(sharedDir)]);
// after: exclusive ownership — no external cleanup while locked
await withAppendLock(sharedDir, async () => { await appendCapsule(sharedDir); }); // cleanup runs only between operations Defensive patterns
Strategy: retry
Validate before calling
// before appending, ensure no competing cleanup can touch the lock:
// confirm the lock exists and nothing else owns the directory
if (!fs.existsSync(path.join(dir, '.append-lock')) && lockHeldElsewhere(dir)) {
throw new Error('capsule dir is in use or being cleaned; retry later');
} Try / catch
try {
await withAppendLock(dir, op);
} catch (error) {
if (error instanceof CapsuleError && error.code === 'capsule.lock_lost') {
// another process removed the lock: results are untrustworthy —
// ensure exclusivity, discard partial state, then retry the operation
await cleanupAndRetry(dir, op);
} else throw error;
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.
Related errors
- Another Nasiko lifecycle operation won lock acquisition
- Another Nasiko lifecycle operation won stale-lock recovery
- capsule.busy
- Legacy sync path changed before removal
- Nasiko lifecycle lock changed during stale-owner recovery
AI-assisted analysis of affaan-m/ECC@8321021c54 (2026-09-16).
Data as JSON: /api/errors/fb69cb4f03be4fdf.
Report an issue: GitHub.
Appendix: source
Thrown at scripts/lib/eval-harness/capsule.js:80
const fields = ['schema', 'run_id', 'capsule_id', 'harness_version', 'task_family'];
for (const [index, entry] of entries.entries()) {
if (fields.some(field => entry[field] !== meta[field])) {
return { ok: false, code: 'capsule.metadata_mismatch', reason: `metadata identity differs from journal entry ${index}`, failed_at: index };
}
}
return null;
}
function releaseOwnedLock(lockPath, fd, identity) {
let inspectionDenied;
try {
// Keep the original descriptor open while checking ownership so its inode
// cannot be reused. Preserve a replacement detected before release; this
// check is not atomic against noncooperating filesystem mutation.
if (identity) {
let current;
try { current = fs.lstatSync(lockPath); } catch (error) {
if (error.code === 'ENOENT') throw new CapsuleError('capsule.lock_lost', 'append lock disappeared before release');
if (error.code !== 'EPERM') throw error;
inspectionDenied = error;
}
if (!inspectionDenied) {
if (!current.isFile() || current.dev !== identity.dev || current.ino !== identity.ino) {
throw new CapsuleError('capsule.lock_lost', 'append lock ownership changed before release');
}
fs.unlinkSync(lockPath);
}
}
} finally {
fs.closeSync(fd);
}
if (inspectionDenied) {
// Windows may deny stat while a removed file awaits its last handle close.
// Only confirmed absence changes the error. Never unlink after closing:
// the pathname could now belong to another owner, even with a reused inode.
try { fs.lstatSync(lockPath); } catch (error) {View on GitHub (pinned to 8321021c54)