thedotmack/claude-mem · error

Worker lazy-spawned but did not become ready before hook rea

Error message

Worker lazy-spawned but did not become ready before hook readiness timeout

What it means

The worker's port opened (it is alive) but waitForWorkerReadiness timed out before the ready flag flipped — readiness additionally requires the health endpoint to report the worker fully initialized. The hook returns false and skips its API call, degrading that hook event.

Source

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

    // `start` hook is booting the daemon in parallel, and a cold macOS+Chroma
    // worker needs ~7s to bind. The old 3-attempt/250ms budget (~0.75s) expired
    // long before that, so the context (and session-init) hooks raced boot and
    // soft-failed to empty — dropping memory injection and the user_prompts row
    // (the upstream trigger for #2794). Wait up to ~15.5s (≈ POST_SPAWN_WAIT) so
    // whichever worker wins the port is seen before we give up.
    const alive = await waitForWorkerPort({ attempts: 6, backoffMs: 500 });
    if (!alive) {
      logger.warn('SYSTEM', spawnLockHeld
        ? 'Worker port did not open after lazy-spawn within the cold-boot wait (~15s)'
        : 'Spawn-lock holder\'s worker port did not open within the cold-boot wait (~15s)');
      return false;
    }
  } finally {
    if (spawnLockHeld) releaseSpawnLock();
  }
  const ready = await waitForWorkerReadiness();
  if (!ready) {
    logger.warn('SYSTEM', 'Worker lazy-spawned but did not become ready before hook readiness timeout');
    return false;
  }
  // Amplifier guard: even if the worker that won the port is still stale,
  // never recycle a second time in the same hook invocation.
  if (expectedPluginVersion !== null) {
    await warnIfVersionStillMismatched(expectedPluginVersion);
  }
  return true;
}

let aliveCache: boolean | null = null;

export async function ensureWorkerAliveOnce(): Promise<boolean> {
  if (aliveCache !== null) return aliveCache;
  aliveCache = await ensureWorkerRunning();
  return aliveCache;
}

View on GitHub (pinned to e2d1df569a)

Solutions

  1. Wait and retry — the next hook event usually finds the worker ready; send the next prompt.
  2. Raise the readiness budget via CLAUDE_MEM_HEALTH_TIMEOUT_MS or the corresponding settings key (bounded; see readTimeoutEnv).
  3. Inspect worker logs for the long-running init step and fix that (e.g., pre-download embedding models).
  4. Pre-warm the worker at login so sessions never race initialization.

Example fix

# before
export CLAUDE_MEM_HEALTH_TIMEOUT_MS=2000   # too short for cold Chroma boot

# after
export CLAUDE_MEM_HEALTH_TIMEOUT_MS=20000  # within the 500..300000 bounds
Defensive patterns

Strategy: retry

Validate before calling

const ready = await waitForWorkerReadiness();
if (!ready) {
  // port is open but init incomplete — skip this event, retry next
  return false;
}

Type guard

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

Prevention

When it happens

Trigger: Worker bound its port but is still initializing (Chroma connect, embeddings setup, DB migrations, session store warm-up) past the readiness budget.

Common situations: First-run Chroma embedding model download; a large migration after upgrade; slow or busy disks stretching init past the timeout.

Understand the failure class

Related errors


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