thedotmack/claude-mem · warning · ChromaUnavailableError

Local Chroma mutations are unavailable after shutdown begins

Error message

Local Chroma mutations are unavailable after shutdown begins

What it means

ChromaUnavailableError thrown by ChromaMcpManager.callTool when a mutation tool (add/update/delete on local Chroma) is invoked after shutdown has begun. Once shutdown starts, acceptingLocalMutations is flipped to false, so local vector-store writes are no longer accepted — reads and non-mutation tools still go through unqueued. The library throws instead of queueing so writes are not silently lost or executed against a dying subprocess.

Solutions

  1. Wait for all mutation calls to complete before calling shutdown/dispose on ChromaMcpManager (await pending sync/backfill promises first).
  2. Check the shutdown flag before calling: expose or track acceptingLocalMutations state and skip/defer mutations when it is false.
  3. Persist the pending mutations and replay them in a later backfill run after the manager is restarted.
  4. Route mutations through the queue (enqueueMutation) during normal operation so they are either executed or deferred before shutdown, not rejected.
  5. Catch ChromaUnavailableError and treat it as a deferral signal rather than a data loss error.

Example fix

// before
await Promise.all([chromaManager.callTool('chroma_delete_documents', { ids })]);
await chromaManager.shutdown();
// after
await chromaManager.shutdown();
// mutations during shutdown are rejected; run them on a fresh manager
const freshManager = await ChromaMcpManager.create(config);
await freshManager.callTool('chroma_delete_documents', { ids });
Defensive patterns

Strategy: try-catch

Validate before calling

// Track manager lifecycle; only call mutations while running
if (!manager.isAcceptingMutations?.()) {
  console.warn('chroma mutations rejected: shutdown in progress');
} else {
  await manager.callTool('chroma_add_documents', args);
}

Type guard

function isMutationAllowed(manager: { callTool(t: string, a: Record<string, unknown>): Promise<unknown> }, toolName: string): boolean {
  return !['chroma_add_documents','chroma_update_documents','chroma_delete_documents'].includes(toolName) || !managerIsShuttingDown;
}

Try / catch

try {
  await manager.callTool(toolName, args);
} catch (e) {
  if (e instanceof ChromaUnavailableError && e.message.includes('after shutdown begins')) {
    pendingMutations.push({ toolName, args }); // defer to next session
  } else { throw e; }
}

Prevention

When it happens

Trigger: Calling callTool() with a mutation tool name (per ChromaMcpManager.isMutationTool, e.g. chroma_add_documents / chroma_update_documents / chroma_delete_documents) while serializeMutations is enabled and shutdown of the manager has already started (acceptingLocalMutations === false).

Common situations: App shutdown/teardown racing with a sync pipeline that still flushes document mutations; worker stop hooks firing while an in-flight indexing job enqueues writes; tests tearing down the manager before background backfill finishes.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


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

Appendix: source

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

      recordUvxVectorSearchUnavailable(unavailableMessage);
      throw new ChromaUnavailableError(unavailableMessage, error instanceof Error ? error : undefined);
    } finally {
      if (timeoutId) {
        clearTimeout(timeoutId);
      }
      if (this.activePrewarmChild === child) {
        this.activePrewarmChild = null;
        this.activePrewarmTracked = null;
      }
    }
  }

  async callTool(toolName: string, toolArguments: Record<string, unknown>): Promise<unknown> {
    if (!this.serializeMutations || !ChromaMcpManager.isMutationTool(toolName)) {
      return this.callToolUnqueued(toolName, toolArguments);
    }
    if (!this.acceptingLocalMutations) {
      throw new ChromaUnavailableError('Local Chroma mutations are unavailable after shutdown begins');
    }

    return this.enqueueMutation(() => this.callToolUnqueued(toolName, toolArguments), toolName);
  }

  private async callToolUnqueued(toolName: string, toolArguments: Record<string, unknown>): Promise<unknown> {
    const callGeneration = this.connectionGeneration;
    await this.ensureConnected();

    logger.debug('CHROMA_MCP', `Calling tool: ${toolName}`, {
      arguments: JSON.stringify(toolArguments).slice(0, 200)
    });

    let result;
    try {
      result = await this.client!.callTool({
        name: toolName,
        arguments: toolArguments

View on GitHub (pinned to d8bc9755e7)