thedotmack/claude-mem · warning

Worker is healthy but not ready; skipping hook API call

Error message

Worker is healthy but not ready; skipping hook API call

What it means

A worker is already alive on the port with a matching version, but waitForWorkerReadiness timed out before the worker flipped its ready flag (readiness is stronger than port-open/health-alive). The hook skips its API call and returns false, so that hook event degrades (e.g., no memory injected).

Source

Thrown at src/shared/worker-utils.ts:482

  // (warnIfVersionStillMismatched). Stays null when no worker was alive
  // (plain cold-start lazy-spawn — no recycle happened, nothing to amplify)
  // or when the resolved version is unreadable ('unknown').
  let expectedPluginVersion: string | null = null;

  if (await isWorkerPortAlive()) {
    // A worker is already alive. If it is a DIFFERENT version than the one
    // this resolution would spawn (e.g. the user upgraded but the previous
    // worker is still squatting the port), recycle it so the resolved
    // version takes over — otherwise the stale worker keeps serving
    // indefinitely.
    const { matches, pluginVersion, workerVersion } = await checkVersionMatch(getWorkerPort(), resolvedScript?.version ?? null);
    if (pluginVersion !== 'unknown') {
      expectedPluginVersion = pluginVersion;
    }
    if (matches) {
      const ready = await waitForWorkerReadiness();
      if (!ready) {
        logger.warn('SYSTEM', 'Worker is healthy but not ready; skipping hook API call');
        return false;
      }
      if (expectedPluginVersion !== null) {
        await warnIfVersionStillMismatched(expectedPluginVersion);
      }
      return true;
    }

    logger.info('SYSTEM', 'Worker version mismatch — killing stale worker', {
      pluginVersion,
      workerVersion,
    });
    // The stale worker must never run its own replacement. The previous
    // design (POST /api/admin/restart, then the dying worker spawns its
    // successor) executed the OLD install's handoff code: a ≤13.11.0 worker
    // resolves the successor script from its own install dir, respawns its
    // own version, and re-binds the port before this hook's lazy-spawn — so
    // the mismatch recurs on every hook forever (#3378: 2,424 recycles in

View on GitHub (pinned to e2d1df569a)

Solutions

  1. Retry by invoking another hook event / sending the next prompt — the worker usually finishes booting on its own.
  2. Raise the health/readiness budget via CLAUDE_MEM_HEALTH_TIMEOUT_MS (bounded; see readTimeoutEnv).
  3. Pre-warm the daemon at login so the first session does not race boot.
  4. Check worker logs for the init step that is slow (Chroma first-run, migrations) and address it.
Defensive patterns

Strategy: retry

Validate before calling

const ready = await waitForWorkerReadiness();
if (!ready) {
  // worker still initializing — skip API call this event, retry next event
  return false;
}

Type guard

const isWorkerReady = async (): Promise<boolean> => {
  try { return (await fetchWorkerHealth()).ready === true; }
  catch { return false; }
};

Prevention

When it happens

Trigger: Cold boot where the port binds quickly but internal init (Chroma, DB migrations, session store) exceeds the readiness budget; waitForWorkerReadiness exhausts its attempts right before the ready flag sets.

Common situations: First session after a reboot (cold macOS+Chroma start needs ~7s); slow disks; CPU contention from a busy machine slowing worker init.

Related errors


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