thedotmack/claude-mem · warning
Port still bound entering restart fallback — verifying…
Error message
Port still bound entering restart fallback — verifying current port owner instead of spawning
What it means
During restart, if the port is still bound when the fallback would spawn a CLI worker, the service refuses to spawn a competitor that could never bind and instead verifies whoever currently owns the port. This avoids duplicate workers and reports the actual port owner's health payload.
Solutions
- Identify the current port owner (lsof -i :PORT) and check its /health payload
- If it is a stale zombie worker, kill it manually and let restart proceed
- If it is a healthy successor, no action needed — restart is effectively done
- Investigate why the old worker did not exit (check for hung SDK queries)
Example fix
// before kill -9 $(lsof -t -i:37777) # only if /health confirms it is a stale worker // after curl -s localhost:37777/health # verify owner first; if healthy, skip restart entirely
Defensive patterns
Strategy: validation
Validate before calling
const inUse = await isPortInUse(port);
const healthy = inUse ? await fetch(`http://localhost:${port}/health`).then(r => r.ok).catch(() => false) : false;
if (inUse && !healthy) console.error('unknown process holds port', port); Try / catch
// Check port ownership before attempting any spawn
if (await isPortInUse(port)) {
const owner = await verifyPortOwner(port); // throws/report if not a claude-mem worker
} Prevention
- Always check who owns the port before spawning
- Ensure shutdown handlers exit promptly (no hung SDK queries)
- Use lsof/netstat to identify stale workers after abnormal exits
When it happens
Trigger: After the old worker's shutdown, the port did not free within the wait window — either the old process ignored SIGTERM/lives on, or an unobserved successor already bound the port — so portWaitSkipped/handoffSawLiveWorker leads to verification instead of spawnDaemon.
Common situations: Zombie worker process that never exited (hung request, blocked event loop); a successor spawned moments earlier that the handoff poller missed; another application grabbed the port; Windows TIME_WAIT/lock delays.
Related errors
- Cloud sync: worker has not picked up the sync settings yet…
- Self-replacing worker handoff did not verify in time —…
- Cannot lazy-spawn worker: worker-service.cjs not found in…
- Claude CLI preflight failed
- [claude-mem] Worker GET
AI-assisted analysis of thedotmack/claude-mem@d8bc9755e7 (2026-09-17).
Data as JSON: /api/errors/34baa6a25414b177.
Report an issue: GitHub.
Appendix: source
Thrown at src/services/worker-service.ts:1270
const restartScript = resolveWorkerScriptPath() ?? __filename;
let spawnedScript = 'none (port still bound — nothing spawned)';
let spawnLockHeld = false;
if (restartFreed) {
// Owner-or-dead guarded (Phase 5): delete only the old worker's PID
// file (oldPid) or a dead pid's leftover. If a successor we failed to
// observe already wrote its own file, it must survive this cleanup.
removePidFileIfOwner(oldPid);
// Spawn gate (src/shared/worker-spawn-gate.ts): if another launcher
// (a hook or the MCP server) is already mid-spawn, skip our own spawn
// and just verify its worker below.
spawnLockHeld = acquireSpawnLock();
} else {
// The port never freed: either the old worker refuses to die (the
// verification below fails and reports its health payload) or a
// successor we failed to observe in time already owns the port (the
// verification below passes). Spawning a competitor here is never
// useful — it could not bind the port anyway.
logger.warn('SYSTEM', 'Port still bound entering restart fallback — verifying current port owner instead of spawning', { port, portWaitSkipped: handoffSawLiveWorker });
}
try {
if (spawnLockHeld) {
const restartPid = spawnDaemon(restartScript, port);
if (restartPid === undefined) {
console.error('Failed to spawn worker daemon during restart.');
// Manual release: process.exit() does not unwind to finally.
releaseSpawnLock();
process.exit(1);
}
spawnedScript = restartScript;
logger.info('SYSTEM', 'Worker restart spawned (CLI fallback)', { pid: restartPid, script: restartScript });
// Hold the lock until the spawned worker owns the port (the spawn
// isn't "done" until then — same rule as the other gated
// launchers); the longer verification below runs unlocked.
await waitForHealth(port, getPlatformTimeout(15000));
} else if (restartFreed) {
spawnedScript = 'none (another launcher holds the spawn lock)';View on GitHub (pinned to d8bc9755e7)