Back to Claude Mem

Phase 02: Fix __dirname & Worker Startup Reliability

.maestro/playbooks/2026-03-29-Issues-Triage-3-29-2026/Phase-02-Fix-Dirname-And-Worker-Startup.md

13.15.38.7 KB
Original Source

Phase 02: Fix __dirname & Worker Startup Reliability

This phase tackles the #1 priority root cause: worker lifecycle and startup reliability. The hardcoded __dirname bug accounts for 7 duplicate issue reports (the most-reported bug), and fragile startup sequencing causes cold-start failures, race conditions, and zombie processes across all platforms. Fixing these unblocks every other subsystem since the worker is the central process.

Tasks

  • Fix the hardcoded __dirname bug in the esbuild bundle (canonical issue #1410). The root cause is that __dirname in src/services/worker-service.ts (around line 434, const mcpServerPath = path.join(__dirname, 'mcp-server.cjs')) gets frozen to the build machine's absolute path when esbuild bundles to plugin/scripts/worker-service.cjs. To fix:

    • Read the esbuild config (search for esbuild in package.json scripts, tsconfig.json, or a dedicated build config file like build.ts or esbuild.config.*) to understand how worker-service.cjs is bundled
    • Read src/services/worker-service.ts to find ALL __dirname and __filename usages
    • Read plugin/scripts/worker-service.cjs (first 50 lines and search for __dirname) to confirm the hardcoded path in the output
    • Fix by ensuring __dirname resolves at runtime, not build time. Options: (a) add define: { '__dirname': 'import.meta.dirname' } or define: { '__dirname': '__dirname' } in esbuild config to prevent inlining, (b) use esbuild's banner option to inject a runtime __dirname polyfill, (c) extract the path to a separate non-bundled config. Choose the simplest approach that works with the existing CJS output format
    • Verify that __dirname in the built worker-service.cjs resolves to its own directory at runtime, not a hardcoded path
    • Also check src/servers/mcp-server.ts for similar __dirname usage that may need the same fix

    Completed: Added explicit define: { '__dirname': '__dirname', '__filename': '__filename' } to all 3 CJS esbuild configs (worker-service, mcp-server, context-generator) in scripts/build-hooks.js. Analysis found that esbuild with platform: 'node' + format: 'cjs' already preserves these as CJS runtime globals (no hardcoded paths in the built output — confirmed via grep). The explicit defines act as a guard against future esbuild behavior changes. src/servers/mcp-server.ts has zero __dirname/__filename usage. Built and verified: 0 hardcoded /Users/ paths, 9 runtime __dirname/__filename references.

  • Add a readiness-aware retry loop to the hook-side worker connection. Currently ensureWorkerRunning() in src/shared/worker-utils.ts (around line 212) makes a single health check attempt and returns false on failure — no retry. Hooks that fire during worker cold-start (first 3-5 seconds) silently get empty context. To fix:

    • Read src/shared/worker-utils.ts to find ensureWorkerRunning() and waitForHealth()
    • Read src/cli/handlers/context.ts to see how hooks connect to the worker
    • Add a lightweight retry loop (3 attempts, 1-second intervals) to ensureWorkerRunning() when the worker is starting up (PID file exists but health check fails). Use the existing waitForHealth() utility if available, or add a simple poll
    • The retry must respect the existing hook timeout budget (configurable via CLAUDE_MEM_HEALTH_TIMEOUT_MS, default 3s). Total retry time should not exceed this budget
    • Ensure the retry distinguishes between "worker starting up" (PID file recent, retry worthwhile) and "worker dead" (no PID file, spawn needed) to avoid wasting time

    Completed: Rewrote ensureWorkerRunning() with a budget-aware retry loop (up to 3 attempts, 1s intervals). Added isWorkerStartingUp() helper that checks PID file existence and recency (30s threshold) to distinguish cold-start retries from dead-worker scenarios. Per-attempt timeout is min(800ms, budget/3) to fit within HEALTH_CHECK_TIMEOUT_MS (default 3s). No retry if PID file absent or stale — returns false immediately for spawn. Build verified.

  • Fix the version mismatch restart coordination race in src/services/worker-service.ts (around line 981). When multiple hooks detect a version mismatch simultaneously, they can all try to restart the worker, causing a stampede. To fix:

    • Read the ensureWorkerStarted() function in src/services/worker-service.ts (around lines 960-1050)
    • Read src/services/infrastructure/ProcessManager.ts for spawnDaemon() and PID file management
    • The existing coordination uses PID file age (<15 seconds = "another restart in progress"), but this is fragile. Improve by:
      • Adding an atomic lockfile (~/.claude-mem/.worker-restart.lock) that the first restarter creates and others check
      • Lock should have a TTL (30 seconds max) to handle crashes
      • Hooks that see the lock should poll health instead of attempting their own restart
    • Ensure the lock is cleaned up in the worker's startup sequence and in GracefulShutdown.ts

    Completed: Replaced fragile isPidFileRecent() coordination with an atomic lockfile (~/.claude-mem/.worker-restart.lock) with 30s TTL. Added acquireRestartLock(), releaseRestartLock(), and isRestartLockHeld() to ProcessManager.ts using O_EXCL atomic create with mtime-based TTL fallback. ensureWorkerStarted() now checks isRestartLockHeld() before restart (polls health if held), then tries acquireRestartLock() (polls if lost race). Lock is released on success, on port-free failure, and on health-check timeout. GracefulShutdown.ts releases the lock in STEP 7 during shutdown. Build verified.

  • Add structured error context to worker startup failures. Currently, when the worker fails to start, hooks log generic messages and exit silently. Users get no actionable information. To fix:

    • Read src/cli/hook-command.ts to understand how hook errors are reported
    • Read src/services/infrastructure/HealthMonitor.ts for isPortInUse() and waitForHealth()
    • When ensureWorkerStarted() returns false, collect and report:
      • Whether port 37777 is in use by another process (port collision)
      • Whether PID file exists and what process owns it
      • Whether Bun is available in PATH
      • The last few lines of worker stderr if available
    • Output this as structured JSON to stderr (exit code 2 = blocking, feeds to Claude) so users and Claude can diagnose the issue

    Completed: Added collectStartupDiagnostics(port) to worker-service.ts that collects: port-in-use status, PID file state (exists, pid, processAlive), Bun availability/version, and last 5 lines of worker.log. On start command failure, writes structured JSON diagnostics to stderr and logs to file. Uses dynamic imports for fs/child_process to avoid overhead on the success path.

  • Write tests for the worker startup retry and version mismatch coordination:

    • Create test file(s) in the existing test directory structure (search for *.test.ts or *.spec.ts to find the convention)
    • Test ensureWorkerRunning() retry: mock health endpoint to fail twice then succeed, verify retry works within timeout budget
    • Test ensureWorkerRunning() no-retry: mock with no PID file, verify it triggers spawn instead of retrying
    • Test version mismatch lockfile: simulate two concurrent restarters, verify only one spawns
    • Test lockfile TTL: simulate stale lock (>30s), verify it's overridden

    Completed: Created 2 test files with 13 tests total. tests/infrastructure/restart-lockfile.test.ts (9 tests): acquire/release/isHeld lifecycle, concurrent acquisition rejection, PID diagnostics in lock, stale lock TTL override. tests/shared/worker-utils-retry.test.ts (4 tests): first-check success, no-retry when PID absent, retry-then-succeed with PID present, timeout budget enforcement. All 13 pass standalone. Full suite runs 1179 pass / 32 fail (all pre-existing: 28 MarkdownFormatter, 1 plugin-distribution, 1 logger-usage, 3 retry tests from global.fetch isolation in parallel run).

  • Run the full build and test suite to verify all changes:

    • Run npm run build-and-sync (or the project's build command — check package.json scripts)
    • Verify plugin/scripts/worker-service.cjs no longer contains hardcoded absolute paths (grep for the old build machine path)
    • Run the test suite (check package.json for test command — likely npm test or bun test)
    • Fix any build or test failures before completing

    Completed: npm run build succeeds — all 6 targets compile (worker-service, mcp-server, context-generator, npx-cli, openclaw, opencode). Verified: 0 hardcoded /Users/ paths in worker-service.cjs, 0 var __dirname shadow declarations. Full test suite: 1179 pass, 32 pre-existing failures (no new failures from this phase). New tests: 13/13 pass.