affaan-m/ECC · warning · CapsuleError
capsule.busy
capsule.busy
Error message
capsule append lock is already held
What it means
withAppendLock creates the append lock file with fs.openSync(lockPath, 'wx'), which fails with EEXIST when the file already exists. The library maps that EEXIST to capsule.busy: another operation currently holds the exclusive append lock for this capsule. It never waits or infers stale ownership by design — cooperative locking only.
Solutions
- Retry the append after the current holder finishes (serialize operations with a queue or mutex in your own code)
- Use a separate capsule per concurrent process/runner instead of one shared directory
- Check whether a prior operation crashed while holding the lock and, after confirming the owner is truly gone, remove the lock file deliberately (owner-verified, never blindly)
- Add retry-with-backoff around capsule.append() for capsule.busy
Example fix
// before: blind concurrent append
await capsuleA.append(record);
// after: serialize per directory
const locks = new Map();
async function withDirLock(dir, fn) {
const prev = locks.get(dir) ?? Promise.resolve();
const run = prev.then(fn);
locks.set(dir, run.catch(() => {}));
return run;
}
await withDirLock(dir, () => capsuleA.append(record)); Defensive patterns
Strategy: retry
Validate before calling
const lockPath = require('path').join(dir, 'append.lock');
if (fs.existsSync(lockPath)) {
// another append is in progress — schedule a retry instead of appending now
} Try / catch
async function appendWithRetry(capsule, record, attempts = 5) {
for (let i = 0; i < attempts; i++) {
try { return capsule.append(record); }
catch (e) {
if (e.code === 'capsule.busy') {
await new Promise(r => setTimeout(r, 100 * 2 ** i));
continue;
}
throw e;
}
}
throw new Error('capsule still busy after retries');
} Prevention
- Serialize all appends to a capsule through a single queue/mutex
- Give each concurrent process its own capsule
- Add exponential-backoff retry around capsule.append for capsule.busy
- Investigate lingering locks only after confirming the owning process is dead
When it happens
Trigger: Calling capsule.append() (or anything routed through withAppendLock) while another append operation holds the lock in the same process or another live process; forgetting to release/await a prior operation; concurrent CI workers sharing one capsule directory.
Common situations: Two build/eval jobs writing to the same capsule simultaneously; a stuck earlier run whose lock was never released; a CLI invocation racing an in-process background append; a shared NFS/workspace directory between agents.
Related errors
- capsule.lock_lost
- Refusing to release a changed Claude settings lock
- Another ECC process is updating Claude settings
- Another Nasiko lifecycle operation is already in progress…
- Another Nasiko lifecycle operation won lock acquisition
AI-assisted analysis of affaan-m/ECC@8321021c54 (2026-09-16).
Data as JSON: /api/errors/bce094e13a858e77.
Report an issue: GitHub.
Appendix: source
Thrown at scripts/lib/eval-harness/capsule.js:112
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) {
if (error.code === 'ENOENT') throw new CapsuleError('capsule.lock_lost', 'append lock disappeared before release');
}
throw inspectionDenied;
}
}
/** Exclusive cooperative append lock. Never waits or infers stale ownership. */
function withAppendLock(dir, operation) {
const lockPath = path.join(dir, APPEND_LOCK_FILE);
let fd;
try {
fd = fs.openSync(lockPath, 'wx', 0o600);
} catch (error) {
if (error.code === 'EEXIST') throw new CapsuleError('capsule.busy', 'capsule append lock is already held');
throw error;
}
let identity;
try {
identity = fs.fstatSync(fd);
return operation();
} finally {
releaseOwnedLock(lockPath, fd, identity);
}
}
class Capsule {
/**
* @param {string} dir capsule root (created if missing)
* @param {object} meta { run_id, capsule_id, harness_version, task_family }
*/
constructor(dir, meta, options = {}) {
this.dir = path.resolve(dir);View on GitHub (pinned to 8321021c54)