{"record":{"id":"e951faa5ec23c281","repo":"thedotmack/claude-mem","slug":"port-did-not-free-up-after-shutdown","errorCode":null,"errorMessage":"Port did not free up after shutdown","messagePattern":"Port did not free up after shutdown","errorType":"console","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"src/services/worker-service.ts","lineNumber":1101,"sourceCode":"    case 'start': {\n      const result = await ensureWorkerStarted(port);\n      if (result === 'dead') {\n        exitWithStatus('error', 'Failed to start worker');\n      } else {\n        exitWithStatus('ready', result === 'warming' ? 'Worker started; still warming up' : undefined);\n      }\n      break;\n    }\n\n    case 'stop': {\n      // Capture the dying worker's pid BEFORE shutdown so the PID-file\n      // cleanup below can prove it deletes THAT worker's file (or a dead\n      // pid's leftover) — never a live successor's (Phase 5).\n      const stoppedPid = await getCurrentWorkerPid(port, 2000);\n      await httpShutdown(port);\n      const freed = await waitForPortFree(port, getPlatformTimeout(15000));\n      if (!freed) {\n        logger.warn('SYSTEM', 'Port did not free up after shutdown', { port });\n      }\n      removePidFileIfOwner(stoppedPid);\n      logger.info('SYSTEM', 'Worker stopped successfully');\n      process.exit(0);\n      break;\n    }\n\n    case 'restart': {\n      logger.info('SYSTEM', 'Restarting worker');\n      // Capture the old worker's pid BEFORE shutdown so we can later prove\n      // the worker answering health checks is a NEW process, not the corpse.\n      const oldPid = await getCurrentWorkerPid(port, 2000);\n      // Track whether the worker accepted the shutdown POST: an accepted\n      // reason=restart shutdown means the dying worker spawns its OWN\n      // successor the moment its port frees (worker-shutdown.ts handoff).\n      // That handoff is the PRIMARY restart path; this CLI defers to it.\n      const shutdownAccepted = await httpShutdown(port, 'restart');\n","sourceCodeStart":1083,"sourceCodeEnd":1119,"githubUrl":"https://github.com/thedotmack/claude-mem/blob/e2d1df569a8f04075d40e92461128ece7cf04c82/src/services/worker-service.ts#L1083-L1119","documentation":"During `claude-mem stop`, the CLI sends the HTTP shutdown request and then waits (about 15s, platform-adjusted) for the port to become free. If the listener socket is still bound after that window, this warning fires; the CLI still removes the PID file if it owns it and exits 0.","triggerScenarios":"The worker process ignores or delays shutdown (stuck in a long graceful task); a child process inherited the listening socket; lingering keep-alive HTTP connections keep it bound; another unrelated process grabbed the same port immediately after release.","commonSituations":"Stop issued while the observer is mid-summarization; older worker version with a slower shutdown path; container/WSL environments where socket teardown lags.","solutions":["Check who holds the port: `lsof -i :<port>` (macOS/Linux) or `netstat -ano | findstr :<port>` (Windows)","Kill the surviving worker process (its pid is in the claude-mem PID file) if it refuses to die","Retry `claude-mem stop` — the second attempt usually succeeds once in-flight work drains"],"exampleFix":null,"handlingStrategy":"retry","validationCode":"async function assertPortFree(port: number): Promise<void> {\n  const inUse = await isPortInUse(port);\n  if (inUse) throw new Error(`port ${port} still bound after stop — inspect the owner before restarting`);\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["After stop, verify the port is free before issuing start/restart","Avoid issuing concurrent stop/restart commands to the same worker","If stop repeatedly fails, kill the pid from the PID file rather than spawning around a stuck listener"],"tags":["shutdown","port","worker","cli"],"backgroundTag":"port-in-use","analyzedSha":"e2d1df569a8f04075d40e92461128ece7cf04c82","analyzedAt":"2026-08-20T23:58:13.836Z","schemaVersion":2},"datasetVersion":"2026-08-29T07:17:48.351Z"}