thedotmack/claude-mem · warning

Self-replacing worker handoff did not verify in time —…

Error message

Self-replacing worker handoff did not verify in time — falling back to CLI spawn

What it means

This warning is emitted by the worker restart flow when a self-replacing handoff (the dying worker launching a successor that takes over the port) fails to be verified within the allotted polling window. The service logs the last observed handoff state and abandons verification, falling back to spawning a worker via the CLI. It is a fallback path, not a fatal failure.

Solutions

  1. Check the worker logs for the successor's boot errors around the restart time
  2. Increase the handoff verification timeout if workers on this machine boot slowly
  3. Verify nothing else holds the port (lsof/netstat) so the successor can bind
  4. Ensure the CLI spawn fallback succeeded (it will start a replacement worker anyway)

Example fix

// before: verify with default window
const handoff = await waitForHandoff(oldPid, port);
// after: allow more time on slow platforms
const handoff = await waitForHandoff(oldPid, port, getPlatformTimeout(HOOK_TIMEOUTS.HANDOFF_VERIFY) * 2);
Defensive patterns

Strategy: fallback

Validate before calling

// Before relying on the handoff, probe the port owner
const owner = await fetch(`http://localhost:${port}/health`).then(r => r.ok).catch(() => false);
if (!owner) console.warn('handoff target not yet healthy; fallback spawn expected');

Try / catch

// Treat handoff as best-effort; always verify a worker exists afterwards
const status = await ensureWorkerStarted(port);
if (status === 'dead') await spawnFallback();

Prevention

When it happens

Trigger: The dying worker's restart handoff was initiated, but waitForHandoff verification never observed a healthy successor (handoff.lastObserved stale, lastPollSawHealth false) before the verification timeout elapsed, so the code proceeds to the CLI spawn fallback instead of process.exit(0).

Common situations: Slow machine where the new worker takes longer than the verification window to bind the port and serve /health; the successor crashed on boot; port blocked by an unrelated process; platform timers shortened on Windows.

Understand the failure class

Background: Request timed out: what client-side request timeouts mean across libraries (Request timed out, TIMED_OUT, APITimeoutError) — this error's family across 39 libraries.

Related errors


AI-assisted analysis of thedotmack/claude-mem@d8bc9755e7 (2026-09-17). Data as JSON: /api/errors/cf0b0afa3f9b2276. Report an issue: GitHub.

Appendix: source

Thrown at src/services/worker-service.ts:1230

      let handoffDetail = '';
      let handoffSawLiveWorker = false;
      if (oldPid !== null && shutdownAccepted) {
        // PRIMARY: the dying worker self-replaces. Do NOT waitForPortFree and
        // do NOT spawn — the successor re-binds the port within ~200ms of it
        // freeing, so a port-free wait here loses the race against the very
        // handoff this CLI just triggered (and a CLI spawn would be a second
        // restart initiator — the disease this flow cures). Just verify the
        // successor.
        const handoff = await verifyRestartedWorker(port, oldPid, packageVersion, getPlatformTimeout(30000));
        if (handoff.ok) {
          console.log(`Worker restart verified (pid: ${handoff.pid}, version: ${handoff.version})`);
          logger.info('SYSTEM', 'Worker restart verified', { pid: handoff.pid, version: handoff.version });
          process.exit(0);
        }
        handoffDetail = `; handoff attempt: ${handoff.lastObserved}`;
        handoffSawLiveWorker = handoff.lastPollSawHealth;
        logger.warn('SYSTEM', 'Self-replacing worker handoff did not verify in time — falling back to CLI spawn', {
          oldPid,
          lastObserved: handoff.lastObserved,
        });
      }

      // FALLBACK — reached when no worker was running, the shutdown POST was
      // not accepted (e.g. the old worker predates the self-replacement
      // handoff), or the handoff never produced a verified successor (its
      // spawn failed). Only here may the CLI spawn, and only through the
      // spawn gate so it can never race a hook's lazy-spawn.
      //
      // When the handoff verification's most recent poll already saw a live
      // health responder, a worker (just not a verifiable successor) holds
      // the port — waiting for it to free would burn the full timeout for
      // nothing, so skip straight to verifying the current owner.
      const restartFreed = handoffSawLiveWorker
        ? false
        : await waitForPortFree(port, getPlatformTimeout(15000));

View on GitHub (pinned to d8bc9755e7)