thedotmack/claude-mem · warning

chroma-mcp subprocess closed unexpectedly, applying…

Error message

chroma-mcp subprocess closed unexpectedly, applying reconnect backoff

What it means

ChromaMcpManager spawns a chroma-mcp MCP subprocess to talk to Chroma. When the transport's onclose fires for a transport that was NOT intentionally closed (and the connection generation is still current), the manager logs this warning, tears down client/transport state, and applies a reconnect backoff before re-spawning.

Solutions

  1. Check chroma-mcp server logs/exit code to find why the subprocess died (missing uv, Python version, port/env issues).
  2. Verify uv and the chroma-mcp package are installed and resolvable in the environment the plugin runs in.
  3. Wait for the automatic reconnect backoff to re-spawn the subprocess; no action is needed for transient kills.
  4. Check system logs for OOM-killer events and reduce Chroma collection size/memory pressure.
  5. Ensure no external process manager is killing child processes of the plugin.

Example fix

// before: no visibility into why the child died
// after: capture exit code and stderr for diagnosis
child.on('exit', (code, signal) => {
  logger.warn('CHROMA_MCP', `chroma-mcp exited code=${code} signal=${signal}`);
});
Defensive patterns

Strategy: retry

Validate before calling

// before relying on chroma-mcp, check prerequisites
const { stdout } = await Bun.$`uv --version`.nothrow();
if (stdout.trim() === '') console.error('uv missing; chroma-mcp subprocess will fail');

Try / catch

// transport onclose is event-based; guard reconnection
manager.on('disconnected', () => {
  // backoff already applied; optionally alert after N rapid reconnects
  if (manager.reconnectCount > 5) alertOps('chroma-mcp crash-looping');
});

Prevention

When it happens

Trigger: The chroma-mcp child process crashes or exits (OOM, unhandled exception, bad UV/python env), the stdio pipe breaks, or the process is killed externally while ensureConnected considers the connection live.

Common situations: chroma-mcp fails to start due to missing uv/Python, the subprocess is OOM-killed under load, the user kills the pid manually, or an OS-level resource limit terminates the child mid-session.

Understand the failure class

Background: ECONNREFUSED and "connection refused" / "could not connect to server" errors: what they mean and how to fix them — this error's family across 44 libraries.

Related errors


AI-assisted analysis of thedotmack/claude-mem@d8bc9755e7 (2026-09-17). Data as JSON: /api/errors/34e538e42cf8eb0a. Report an issue: GitHub.

Appendix: source

Thrown at src/services/sync/ChromaMcpManager.ts:342

    const currentTransport = this.transport;
    // Captured HERE, while the child is alive and attached — not in the
    // onclose handler below, which by definition runs after it has died.
    const transportChild = (this.transport as unknown as { _process?: ChildProcess })._process;
    const currentTracked = transportChild ? trackChild(transportChild) : null;
    const currentTrackedPid = currentTracked?.pid;
    this.transport.onclose = () => {
      if (this.transport !== currentTransport) {
        logger.debug('CHROMA_MCP', 'Ignoring stale onclose from previous transport');
        return;
      }
      if (
        this.connectionGeneration !== connectionGeneration ||
        this.intentionallyClosingTransports.has(currentTransport as unknown as object)
      ) {
        logger.debug('CHROMA_MCP', 'Ignoring onclose from intentionally closed transport');
        return;
      }
      logger.warn('CHROMA_MCP', 'chroma-mcp subprocess closed unexpectedly, applying reconnect backoff');
      this.connected = false;
      getSupervisor().unregisterProcess(CHROMA_SUPERVISOR_ID);
      this.client = null;
      this.transport = null;
      this.lastConnectionFailureTimestamp = Date.now();

      // Direct child (uvx) emitted close, but on Linux the grandchildren
      // (uv/python/chroma-mcp) often outlive their parent because MCP SDK
      // does not use process groups. Sweep the descendant tree using the
      // captured PID — best-effort; pgrep returns nothing if everything
      // already exited (#2313).
      this.scheduleUnexpectedCloseCleanup(currentTracked);
    };
  }

  private scheduleUnexpectedCloseCleanup(tracked: TrackedChild | null): void {
    let cleanup: Promise<void>;
    cleanup = this.cleanupUnexpectedCloseSubprocess(tracked).finally(() => {

View on GitHub (pinned to d8bc9755e7)