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
- Wait for all mutation calls to complete before calling shutdown/dispose on ChromaMcpManager (await pending sync/backfill promises first).
- Check the shutdown flag before calling: expose or track acceptingLocalMutations state and skip/defer mutations when it is false.
- Persist the pending mutations and replay them in a later backfill run after the manager is restarted.
- Route mutations through the queue (enqueueMutation) during normal operation so they are either executed or deferred before shutdown, not rejected.
- 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
- Await all sync/backfill work before calling shutdown on the manager
- Queue mutations through enqueueMutation during normal operation so they are deferred, not rejected
- Check manager lifecycle state before issuing write calls
- Treat ChromaUnavailableError as deferral, persist and replay mutations later
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
- chroma-mcp call cancelled during shutdown
- chroma-mcp connection cancelled during shutdown
- chroma-mcp tool " " returned error
- failed to kill in-flight chroma-mcp prewarm tree…
- failed to kill prior chroma-mcp tree (best-effort)
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: toolArgumentsView on GitHub (pinned to d8bc9755e7)