thedotmack/claude-mem · warning

Spawn-lock holder's worker not ready within window

Error message

Spawn-lock holder's worker not ready within window

What it means

This launcher lost the spawn gate (another hook, the MCP server, or the CLI already holds it), skipped its own spawn, and waited on the lock holder's worker. That worker did not pass /readiness within the window, but some sign of life (health endpoint, PID file, process) remains, so the path returns 'warming' with this warning rather than declaring failure.

Source

Thrown at src/services/worker-spawner.ts:172

      }
    } else {
      logger.info('SYSTEM', 'Another launcher holds the spawn lock — skipping duplicate spawn and waiting for its worker');
    }

    const ready = await waitForReadiness(port, getPlatformTimeout(HOOK_TIMEOUTS.READINESS_WAIT));
    if (!ready) {
      const workerStillHealthy = await waitForHealth(port, 1000);
      const workerPidStillAlive = cleanStalePidFile() === 'alive';
      const spawnedProcessStillAlive = spawnedPid !== undefined && spawnedPid > 0 && isPidAlive(spawnedPid);
      if (!workerStillHealthy && !workerPidStillAlive && !spawnedProcessStillAlive) {
        logger.error('SYSTEM', spawnLockHeld
          ? 'Worker exited before readiness endpoint became available'
          : 'Spawn-lock holder never produced a live worker before readiness timed out');
        return 'dead';
      }
      logger.warn('SYSTEM', spawnLockHeld
        ? 'Worker spawned but readiness endpoint not responding within window'
        : 'Spawn-lock holder\'s worker not ready within window');
      return 'warming';
    }
    clearWorkerSpawnAttempted();
    // touchPidFile is existsSync-guarded and merely refreshes the live worker's
    // pid-file mtime — correct for lock losers too, since the worker IS up.
    touchPidFile();
    logger.info('SYSTEM', spawnLockHeld
      ? 'Worker started successfully'
      : 'Worker is up (started by another launcher)');
    return 'ready';
  } finally {
    if (spawnLockHeld) releaseSpawnLock();
  }
}

View on GitHub (pinned to e2d1df569a)

Solutions

  1. Nothing to fix in the loser path — poll /readiness and the worker typically turns ready shortly
  2. If it never turns ready, debug the winner's spawn: check worker logs for startup failures
  3. Avoid racing launchers by starting the worker once at session/boot time before hooks fire
Defensive patterns

Strategy: retry

Validate before calling

async function waitForHolderWorker(port: number, timeoutMs = 60000): Promise<boolean> {
  const deadline = Date.now() + timeoutMs;
  while (Date.now() < deadline) {
    try { if (await fetch(`http://127.0.0.1:${port}/readiness`).then(r => r.ok)) return true; } catch {}
    await new Promise(r => setTimeout(r, 1000));
  }
  return false;
}

Prevention

When it happens

Trigger: Two hooks fire simultaneously on a machine with no worker: one wins the spawn lock, the other waits; the winner's worker is still initializing when the loser's readiness wait expires.

Common situations: Parallel tool invocations at session start; MCP server and a hook both lazy-spawning; slow init making every waiter's window expire.

Related errors


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