fix: prevent chroma-mcp process leaks on worker restart

Critical bugfix: ChromaSync now properly cleans up chroma-mcp subprocesses
when the worker is restarted, preventing memory exhaustion from orphaned
processes accumulating over time.

Changes:
- Store reference to StdioClientTransport subprocess
- Explicitly close transport in close() method to kill subprocess
- Add error handling to ensure cleanup even on failures
- Reset all state in finally block

Problem:
Each worker restart spawned a new chroma-mcp process but never killed the
old one. After multiple restarts, orphaned processes accumulated (16+ seen
in production), consuming 900MB+ RAM and eventually causing OOM kills that
silently failed backfills.

Impact:
- Eliminates process accumulation
- Prevents memory exhaustion from leaked subprocesses
- Fixes silent backfill failures caused by OOM kills
- Ensures graceful cleanup on worker shutdown

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Alex Newman
2025-12-13 20:13:05 -05:00
parent 2681a2d251
commit 6d4a4819de
2 changed files with 52 additions and 27 deletions
File diff suppressed because one or more lines are too long
+31 -6
View File
@@ -73,6 +73,7 @@ interface StoredUserPrompt {
export class ChromaSync {
private client: Client | null = null;
private transport: StdioClientTransport | null = null;
private connected: boolean = false;
private project: string;
private collectionName: string;
@@ -101,7 +102,7 @@ export class ChromaSync {
// See: https://github.com/thedotmack/claude-mem/issues/170 (Python 3.14 incompatibility)
const settings = SettingsDefaultsManager.loadFromFile(USER_SETTINGS_PATH);
const pythonVersion = settings.CLAUDE_MEM_PYTHON_VERSION;
const transport = new StdioClientTransport({
this.transport = new StdioClientTransport({
command: 'uvx',
args: [
'--python', pythonVersion,
@@ -119,7 +120,7 @@ export class ChromaSync {
capabilities: {}
});
await this.client.connect(transport);
await this.client.connect(this.transport);
this.connected = true;
logger.info('CHROMA_SYNC', 'Connected to Chroma MCP server', { project: this.project });
@@ -815,14 +816,38 @@ export class ChromaSync {
}
/**
* Close the Chroma client connection
* Close the Chroma client connection and cleanup subprocess
*/
async close(): Promise<void> {
if (this.client && this.connected) {
await this.client.close();
if (!this.connected && !this.client && !this.transport) {
return;
}
try {
// Close client first
if (this.client) {
try {
await this.client.close();
} catch (error) {
logger.warn('CHROMA_SYNC', 'Error closing Chroma client', { project: this.project }, error as Error);
}
}
// Explicitly close transport to kill subprocess
if (this.transport) {
try {
await this.transport.close();
} catch (error) {
logger.warn('CHROMA_SYNC', 'Error closing transport', { project: this.project }, error as Error);
}
}
logger.info('CHROMA_SYNC', 'Chroma client and subprocess closed', { project: this.project });
} finally {
// Always reset state, even if errors occurred
this.connected = false;
this.client = null;
logger.info('CHROMA_SYNC', 'Chroma client closed', { project: this.project });
this.transport = null;
}
}
}