{"record":{"id":"34baa6a25414b177","repo":"thedotmack/claude-mem","slug":"port-still-bound-entering-restart-fallback-verif","errorCode":null,"errorMessage":"Port still bound entering restart fallback — verifying current port owner instead of spawning","messagePattern":"Port still bound entering restart fallback — verifying current port owner instead of spawning","errorType":"console","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"src/services/worker-service.ts","lineNumber":1270,"sourceCode":"      const restartScript = resolveWorkerScriptPath() ?? __filename;\n      let spawnedScript = 'none (port still bound — nothing spawned)';\n      let spawnLockHeld = false;\n      if (restartFreed) {\n        // Owner-or-dead guarded (Phase 5): delete only the old worker's PID\n        // file (oldPid) or a dead pid's leftover. If a successor we failed to\n        // observe already wrote its own file, it must survive this cleanup.\n        removePidFileIfOwner(oldPid);\n        // Spawn gate (src/shared/worker-spawn-gate.ts): if another launcher\n        // (a hook or the MCP server) is already mid-spawn, skip our own spawn\n        // and just verify its worker below.\n        spawnLockHeld = acquireSpawnLock();\n      } else {\n        // The port never freed: either the old worker refuses to die (the\n        // verification below fails and reports its health payload) or a\n        // successor we failed to observe in time already owns the port (the\n        // verification below passes). Spawning a competitor here is never\n        // useful — it could not bind the port anyway.\n        logger.warn('SYSTEM', 'Port still bound entering restart fallback — verifying current port owner instead of spawning', { port, portWaitSkipped: handoffSawLiveWorker });\n      }\n      try {\n        if (spawnLockHeld) {\n          const restartPid = spawnDaemon(restartScript, port);\n          if (restartPid === undefined) {\n            console.error('Failed to spawn worker daemon during restart.');\n            // Manual release: process.exit() does not unwind to finally.\n            releaseSpawnLock();\n            process.exit(1);\n          }\n          spawnedScript = restartScript;\n          logger.info('SYSTEM', 'Worker restart spawned (CLI fallback)', { pid: restartPid, script: restartScript });\n          // Hold the lock until the spawned worker owns the port (the spawn\n          // isn't \"done\" until then — same rule as the other gated\n          // launchers); the longer verification below runs unlocked.\n          await waitForHealth(port, getPlatformTimeout(15000));\n        } else if (restartFreed) {\n          spawnedScript = 'none (another launcher holds the spawn lock)';","sourceCodeStart":1252,"sourceCodeEnd":1288,"githubUrl":"https://github.com/thedotmack/claude-mem/blob/d8bc9755e74915e5c3b999181e10a67c889bce2a/src/services/worker-service.ts#L1252-L1288","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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)"],"exampleFix":"// before\nkill -9 $(lsof -t -i:37777)   # only if /health confirms it is a stale worker\n// after\ncurl -s localhost:37777/health  # verify owner first; if healthy, skip restart entirely","handlingStrategy":"validation","validationCode":"const inUse = await isPortInUse(port);\nconst healthy = inUse ? await fetch(`http://localhost:${port}/health`).then(r => r.ok).catch(() => false) : false;\nif (inUse && !healthy) console.error('unknown process holds port', port);","typeGuard":null,"tryCatchPattern":"// Check port ownership before attempting any spawn\nif (await isPortInUse(port)) {\n  const owner = await verifyPortOwner(port); // throws/report if not a claude-mem worker\n}","preventionTips":["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"],"tags":["worker","port","restart","zombie-process"],"backgroundTag":"address-already-in-use","analyzedSha":"d8bc9755e74915e5c3b999181e10a67c889bce2a","analyzedAt":"2026-09-17T16:40:26.182Z","contentChangedAt":"2026-09-17T16:40:26.182Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}