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

  1. Retry the append after the current holder finishes (serialize operations with a queue or mutex in your own code)
  2. Use a separate capsule per concurrent process/runner instead of one shared directory
  3. 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)
  4. 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

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


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)