paperclipai/paperclip · error · Error

Failed to start worktree port reservation lock probe at ${lo

Error message

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

What it means

Thrown by startRegistryLockHeartbeat when the heartbeat worker started (flag 1 set) but the probe port it reports is <= 0. The worker binds a TCP probe port that other processes use to verify the lock owner is truly alive; a worker that cannot bind reports no port, and the lock cannot be advertised, so acquisition aborts and cleans up.

Source

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

      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();
}

function acquireRegistryLock(lockPath: string, deadline: number): RegistryLockLease | null {
  try {
    fs.mkdirSync(lockPath);
    const token = `${process.pid}-${randomUUID()}`;

View on GitHub (pinned to a7e689b3c3)

Solutions

  1. Retry the operation once; ephemeral port bind failures are frequently transient.
  2. Verify the environment allows binding 127.0.0.1 on an ephemeral port (check seccomp/firewall/apparmor profiles, docker network policies).
  3. Raise the file-descriptor limit (ulimit -n) if the process is FD-exhausted.
  4. Reduce simultaneous worktree operations that each start lock workers.
Defensive patterns

Strategy: retry

Validate before calling

// Preflight: can this process bind an ephemeral loopback port right now?
import net from "node:net";
function canBindLoopback(): Promise<boolean> {
  return new Promise((resolve) => {
    const s = net.createServer();
    s.once("error", () => resolve(false));
    s.listen(0, "127.0.0.1", () => s.close(() => resolve(true)));
  });
}

Try / catch

try {
  return withWorktreePortRegistryLockSync(home, run);
} catch (error) {
  if (error instanceof Error && error.message.includes("lock probe")) {
    return withWorktreePortRegistryLockSync(home, run); // transient bind failure: retry once
  }
  throw error;
}

Prevention

When it happens

Trigger: The worker's server.listen on an ephemeral port fails (EADDRNOTAVAIL, EACCES on restricted networks) or the listen callback never fires within the 2s readiness wait, leaving control[2] at 0; withWorktreePortRegistryLockSync then throws during lock acquisition.

Common situations: Containers/network namespaces with loopback binding restricted or no IPv4 stack ready; firewall or seccomp policies blocking socket creation; FD exhaustion (EMFILE) so listen fails; momentarily no free ports under heavy churn.

Related errors


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