paperclipai/paperclip · error · Error

Failed to start worktree port reservation lock heartbeat at

Error message

Failed to start worktree port reservation lock heartbeat at ${lockPath}

What it means

Thrown by startRegistryLockHeartbeat in the worktree port registry when the heartbeat Worker thread fails to signal readiness within 2 seconds (control flag never reaches 1). The heartbeat worker refreshes the lock's owner files and answers liveness probes; without it the lock cannot be held safely, so acquisition aborts and the freshly created lock directory is removed.

Source

Thrown at packages/shared/src/worktree-port-registry.ts:280

  const worker = new Worker(REGISTRY_LOCK_HEARTBEAT_SOURCE, {
    eval: true,
    execArgv: [],
    workerData: {
      control: control.buffer,
      heartbeatMs: WORKTREE_PORT_REGISTRY_LOCK_HEARTBEAT_MS,
      lockPath,
      ownerFiles: [
        WORKTREE_PORT_REGISTRY_LOCK_OWNER_FILE,
        WORKTREE_PORT_REGISTRY_LOCK_OWNER_BACKUP_FILE,
      ],
      probeTimeoutMs: WORKTREE_PORT_REGISTRY_LOCK_PROBE_TIMEOUT_MS,
      token,
    },
  });
  Atomics.wait(control, 0, 0, 2_000);
  if (Atomics.load(control, 0) !== 1) {
    void worker.terminate();
    throw new Error(`Failed to start worktree port reservation lock heartbeat at ${lockPath}`);
  }
  const probePort = Atomics.load(control, 2);
  if (probePort <= 0) {
    void worker.terminate();
    throw new Error(`Failed to start worktree port reservation lock probe at ${lockPath}`);
  }
  return { token, worker, control, probePort };
}

function stopRegistryLockHeartbeat(lease: RegistryLockLease): void {
  Atomics.store(lease.control, 1, 1);
  Atomics.notify(lease.control, 1);
  lease.worker.postMessage("stop");
  if (Atomics.load(lease.control, 0) === 1) {
    Atomics.wait(lease.control, 0, 1, 2_000);
  }
  void lease.worker.terminate();
}

View on GitHub (pinned to a7e689b3c3)

Solutions

  1. Retry the worktree/port-registry operation once; transient thread-start starvation is the most common cause.
  2. Check thread/resource limits in the environment (ulimit -u, cgroup pids limit, available memory) and raise them.
  3. Verify the Node.js version supports worker_threads and that no NODE_OPTIONS/execArgv flags interfere with eval workers.
  4. Reduce concurrent worktree operations hammering the registry at the same moment.
Defensive patterns

Strategy: retry

Validate before calling

// Before heavy fan-out, confirm worker threads can start in this environment:
import { isMainThread, Worker } from "node:worker_threads";
function workersAvailable(): Promise<boolean> {
  return new Promise((resolve) => {
    try {
      const w = new Worker("Atomics.store(new Int32Array(workerData.b),0,1)", { eval: true, workerData: { b: new SharedArrayBuffer(4) } });
      w.on("error", () => resolve(false));
      w.on("exit", (c) => resolve(c === 0));
    } catch {
      resolve(false);
    }
  });
}

Try / catch

try {
  return withWorktreePortRegistryLockSync(home, run);
} catch (error) {
  if (error instanceof Error && error.message.includes("heartbeat")) {
    return withWorktreePortRegistryLockSync(home, run); // one retry for thread-start starvation
  }
  throw error;
}

Prevention

When it happens

Trigger: withWorktreePortRegistryLockSync spins up the eval-mode Worker; the worker throws during startup (bad embedded source, Node flags incompatibility), or the process/system is so loaded the thread cannot start and touch the SharedArrayBuffer within the 2s Atomics.wait budget.

Common situations: Heavily loaded CI machines or containers with CPU/thread limits (cgroup pids.max, low ulimit) preventing thread spawn; restrictive sandboxed runtimes where worker_threads spawning is disabled; Node versions or execArgv (e.g. --experimental flags, inspector) that break eval workers; transient fork/resource exhaustion (EMFILE, ENOMEM).

Related errors


AI-assisted analysis of paperclipai/paperclip@a7e689b3c3 (2026-08-21). Data as JSON: /api/errors/579191e554e4f747. Report an issue: GitHub.