{"record":{"id":"bce094e13a858e77","repo":"affaan-m/ECC","slug":"capsule-busy","errorCode":"capsule.busy","errorMessage":"capsule append lock is already held","messagePattern":"capsule append lock is already held","errorType":"error_code","errorClass":"CapsuleError","httpStatus":null,"severity":"warning","filePath":"scripts/lib/eval-harness/capsule.js","lineNumber":112,"sourceCode":"  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) {\n      if (error.code === 'ENOENT') throw new CapsuleError('capsule.lock_lost', 'append lock disappeared before release');\n    }\n    throw inspectionDenied;\n  }\n}\n\n/** Exclusive cooperative append lock. Never waits or infers stale ownership. */\nfunction withAppendLock(dir, operation) {\n  const lockPath = path.join(dir, APPEND_LOCK_FILE);\n  let fd;\n  try {\n    fd = fs.openSync(lockPath, 'wx', 0o600);\n  } catch (error) {\n    if (error.code === 'EEXIST') throw new CapsuleError('capsule.busy', 'capsule append lock is already held');\n    throw error;\n  }\n  let identity;\n  try {\n    identity = fs.fstatSync(fd);\n    return operation();\n  } finally {\n    releaseOwnedLock(lockPath, fd, identity);\n  }\n}\n\nclass Capsule {\n  /**\n   * @param {string} dir capsule root (created if missing)\n   * @param {object} meta { run_id, capsule_id, harness_version, task_family }\n   */\n  constructor(dir, meta, options = {}) {\n    this.dir = path.resolve(dir);","sourceCodeStart":94,"sourceCodeEnd":130,"githubUrl":"https://github.com/affaan-m/ECC/blob/8321021c54d670126ce3b2969d5deb880b4b0c2a/scripts/lib/eval-harness/capsule.js#L94-L130","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before: blind concurrent append\nawait capsuleA.append(record);\n// after: serialize per directory\nconst locks = new Map();\nasync function withDirLock(dir, fn) {\n  const prev = locks.get(dir) ?? Promise.resolve();\n  const run = prev.then(fn);\n  locks.set(dir, run.catch(() => {}));\n  return run;\n}\nawait withDirLock(dir, () => capsuleA.append(record));","handlingStrategy":"retry","validationCode":"const lockPath = require('path').join(dir, 'append.lock');\nif (fs.existsSync(lockPath)) {\n  // another append is in progress — schedule a retry instead of appending now\n}","typeGuard":null,"tryCatchPattern":"async function appendWithRetry(capsule, record, attempts = 5) {\n  for (let i = 0; i < attempts; i++) {\n    try { return capsule.append(record); }\n    catch (e) {\n      if (e.code === 'capsule.busy') {\n        await new Promise(r => setTimeout(r, 100 * 2 ** i));\n        continue;\n      }\n      throw e;\n    }\n  }\n  throw new Error('capsule still busy after retries');\n}","preventionTips":["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"],"tags":["filesystem","concurrency","locking"],"backgroundTag":"address-already-in-use","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"}