thedotmack/claude-mem · error

Worker port did not open after lazy-spawn within the cold-bo

Error message

Worker port did not open after lazy-spawn within the cold-boot wait (~15s)

What it means

This invocation spawned the worker (spawnLockHeld true), then waitForWorkerPort — 6 attempts with 500ms backoff (~15.5s, matching POST_SPAWN_WAIT) — never saw the port open, so the hook gives up. The long budget exists because cold macOS+Chroma boots need ~7s (#2795); exceeding ~15s means the worker is stuck or crashing before it can bind.

Source

Thrown at src/shared/worker-utils.ts:595

          });
        }
        return false;
      }
    } else {
      logger.info('SYSTEM', 'Another launcher holds the spawn lock — skipping lazy-spawn and waiting for its worker');
    }

    // Cold boot (#2795): on the first session after a reboot the SessionStart
    // `start` hook is booting the daemon in parallel, and a cold macOS+Chroma
    // worker needs ~7s to bind. The old 3-attempt/250ms budget (~0.75s) expired
    // long before that, so the context (and session-init) hooks raced boot and
    // soft-failed to empty — dropping memory injection and the user_prompts row
    // (the upstream trigger for #2794). Wait up to ~15.5s (≈ POST_SPAWN_WAIT) so
    // whichever worker wins the port is seen before we give up.
    const alive = await waitForWorkerPort({ attempts: 6, backoffMs: 500 });
    if (!alive) {
      logger.warn('SYSTEM', spawnLockHeld
        ? 'Worker port did not open after lazy-spawn within the cold-boot wait (~15s)'
        : 'Spawn-lock holder\'s worker port did not open within the cold-boot wait (~15s)');
      return false;
    }
  } finally {
    if (spawnLockHeld) releaseSpawnLock();
  }
  const ready = await waitForWorkerReadiness();
  if (!ready) {
    logger.warn('SYSTEM', 'Worker lazy-spawned but did not become ready before hook readiness timeout');
    return false;
  }
  // Amplifier guard: even if the worker that won the port is still stale,
  // never recycle a second time in the same hook invocation.
  if (expectedPluginVersion !== null) {
    await warnIfVersionStillMismatched(expectedPluginVersion);
  }
  return true;
}

View on GitHub (pinned to e2d1df569a)

Solutions

  1. Run the worker manually (bun plugin/scripts/worker-service.cjs) and read the pre-bind crash output.
  2. Check the port is free: lsof -i :$CLAUDE_MEM_WORKER_PORT; kill whatever squats it.
  3. If logs show Chroma contention, clear stale Chroma lock files under ~/.claude-mem/chroma and retry.
  4. Pre-start the daemon at login so the first session does not cold-boot inside a hook.
Defensive patterns

Strategy: retry

Validate before calling

const alive = await waitForWorkerPort({ attempts: 6, backoffMs: 500 });
if (!alive) {
  // diagnose pre-bind crash: run worker manually, check port
  return false;
}

Type guard

const isPortOpen = async (port: number): Promise<boolean> => {
  try { await fetch(`http://127.0.0.1:${port}/health`); return true; }
  catch { return false; }
};

Prevention

When it happens

Trigger: The spawned worker crashes before listen (bad config, DB lock, missing env), a cold start drags past 15s, or another process squats the configured worker port so bind fails.

Common situations: First boot after reboot on slow disks; first-run Chroma initialization; port conflict from an orphaned or foreign process on CLAUDE_MEM_WORKER_PORT.

Related errors


AI-assisted analysis of thedotmack/claude-mem@e2d1df569a (2026-08-20). Data as JSON: /api/errors/7228998580479f27. Report an issue: GitHub.