thedotmack/claude-mem · warning

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

Error message

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

What it means

On `claude-mem restart`, the dying worker spawns its own successor and the CLI verifies the handoff (new pid, matching version) within ~30s platform-adjusted time. If verification never observes a healthy successor, this warning fires and the CLI falls back to spawning a replacement itself through the spawn gate, so exactly one restart path wins.

Source

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

      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 e2d1df569a)

Solutions

  1. Check worker logs for why the successor died (dependency preflight, port bind, DB migration failures)
  2. Run `claude-mem start` (or another `restart`) and watch for 'Worker started successfully'
  3. If it recurs, capture `lastObserved` from the warning — it states what verification actually saw (no pid, wrong version, no health)
Defensive patterns

Strategy: fallback

Validate before calling

async function verifyWorkerAfterRestart(port: number, expectedVersion: string): Promise<boolean> {
  try {
    const res = await fetch(`http://127.0.0.1:${port}/health`);
    const body = await res.json();
    return res.ok && body.version === expectedVersion;
  } catch { return false; }
}

Prevention

When it happens

Trigger: The successor process spawned by the dying worker crashed at startup; the machine is too slow for the successor to become healthy inside the verification window; the old worker predates the self-replacement handoff feature so no successor was ever spawned.

Common situations: Restarting right after a claude-mem upgrade where the new version's worker fails an init step; restarting during heavy load; port contention from another service delaying successor startup.

Related errors


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