thedotmack/claude-mem · error

Spawn-lock holder's worker port did not open within the cold

Error message

Spawn-lock holder's worker port did not open within the cold-boot wait (~15s)

What it means

Same cold-boot port wait, but this invocation lost the spawn gate (spawnLockHeld false): another gated launcher won and spawned, and this one waited on the winner's worker. The port still never opened within ~15s, so this hook also returns false. The spawn gate means exactly one spawner exists; losers just poll the shared port.

Source

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

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

    // Cold boot (#2795): on the first session after a reboot the SessionStart
    // `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;
}

View on GitHub (pinned to e2d1df569a)

Solutions

  1. Triage identically to the winner-side failure: run the worker manually to surface the pre-bind error.
  2. Check the worker port for conflicts and clear stale pid/lock files under ~/.claude-mem.
  3. The next hook event re-enters the full spawn path — verify it succeeds there before debugging further.
  4. Serialize cold boot by pre-warming the daemon at login.
Defensive patterns

Strategy: retry

Validate before calling

// loser path: don't spawn, just wait for the winner's port
const alive = await waitForWorkerPort({ attempts: 6, backoffMs: 500 });
if (!alive) return false; // next hook event re-enters the full spawn path

Prevention

When it happens

Trigger: Multiple hooks (start + context) race a cold boot; the spawn-gate winner's worker fails to bind (crash, slow init, port conflict) while all losers block on the same port-open wait and time out together.

Common situations: Several Claude sessions opened simultaneously after reboot; winner's spawn failing for environment reasons (missing env, corrupted DB) so every waiter fails.

Related errors


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