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

  1. Identify the current port owner (lsof -i :PORT) and check its /health payload
  2. If it is a stale zombie worker, kill it manually and let restart proceed
  3. If it is a healthy successor, no action needed — restart is effectively done
  4. 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

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


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)