feat: Switch to persistent Chroma HTTP server
Replace MCP subprocess approach with persistent Chroma HTTP server for improved performance and reliability. This re-enables Chroma on Windows by eliminating the subprocess spawning that caused console popups. Changes: - NEW: ChromaServerManager.ts - Manages local Chroma server lifecycle via `npx chroma run` - REFACTOR: ChromaSync.ts - Uses chromadb npm package's ChromaClient instead of MCP subprocess (removes Windows disabling) - UPDATE: worker-service.ts - Starts Chroma server on initialization - UPDATE: GracefulShutdown.ts - Stops Chroma server on shutdown - UPDATE: SettingsDefaultsManager.ts - New Chroma configuration options - UPDATE: build-hooks.js - Mark optional chromadb deps as external Benefits: - Eliminates subprocess spawn latency on first query - Single server process instead of per-operation subprocesses - No Python/uvx dependency for local mode - Re-enables Chroma vector search on Windows - Future-ready for cloud-hosted Chroma (claude-mem pro) - Cross-platform: Linux, macOS, Windows Configuration: CLAUDE_MEM_CHROMA_MODE=local|remote CLAUDE_MEM_CHROMA_HOST=127.0.0.1 CLAUDE_MEM_CHROMA_PORT=8000 CLAUDE_MEM_CHROMA_SSL=false Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -87,6 +87,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@anthropic-ai/claude-agent-sdk": "^0.1.76",
|
"@anthropic-ai/claude-agent-sdk": "^0.1.76",
|
||||||
"@modelcontextprotocol/sdk": "^1.25.1",
|
"@modelcontextprotocol/sdk": "^1.25.1",
|
||||||
|
"chromadb": "^1.9.2",
|
||||||
"ansi-to-html": "^0.7.2",
|
"ansi-to-html": "^0.7.2",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"glob": "^11.0.3",
|
"glob": "^11.0.3",
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+289
-293
File diff suppressed because one or more lines are too long
@@ -92,7 +92,12 @@ async function buildHooks() {
|
|||||||
outfile: `${hooksDir}/${WORKER_SERVICE.name}.cjs`,
|
outfile: `${hooksDir}/${WORKER_SERVICE.name}.cjs`,
|
||||||
minify: true,
|
minify: true,
|
||||||
logLevel: 'error', // Suppress warnings (import.meta warning is benign)
|
logLevel: 'error', // Suppress warnings (import.meta warning is benign)
|
||||||
external: ['bun:sqlite'],
|
external: [
|
||||||
|
'bun:sqlite',
|
||||||
|
// Optional chromadb embedding providers (not needed for HTTP client mode)
|
||||||
|
'cohere-ai',
|
||||||
|
'ollama'
|
||||||
|
],
|
||||||
define: {
|
define: {
|
||||||
'__DEFAULT_PACKAGE_VERSION__': `"${version}"`
|
'__DEFAULT_PACKAGE_VERSION__': `"${version}"`
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -29,6 +29,13 @@ export interface CloseableDatabase {
|
|||||||
close(): Promise<void>;
|
close(): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stoppable service interface for Chroma server
|
||||||
|
*/
|
||||||
|
export interface StoppableServer {
|
||||||
|
stop(): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configuration for graceful shutdown
|
* Configuration for graceful shutdown
|
||||||
*/
|
*/
|
||||||
@@ -37,6 +44,7 @@ export interface GracefulShutdownConfig {
|
|||||||
sessionManager: ShutdownableService;
|
sessionManager: ShutdownableService;
|
||||||
mcpClient?: CloseableClient;
|
mcpClient?: CloseableClient;
|
||||||
dbManager?: CloseableDatabase;
|
dbManager?: CloseableDatabase;
|
||||||
|
chromaServer?: StoppableServer;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -76,7 +84,14 @@ export async function performGracefulShutdown(config: GracefulShutdownConfig): P
|
|||||||
await config.dbManager.close();
|
await config.dbManager.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
// STEP 6: Force kill any remaining child processes (Windows zombie port fix)
|
// STEP 6: Stop Chroma server (local mode only)
|
||||||
|
if (config.chromaServer) {
|
||||||
|
logger.info('SHUTDOWN', 'Stopping Chroma server...');
|
||||||
|
await config.chromaServer.stop();
|
||||||
|
logger.info('SHUTDOWN', 'Chroma server stopped');
|
||||||
|
}
|
||||||
|
|
||||||
|
// STEP 7: Force kill any remaining child processes (Windows zombie port fix)
|
||||||
if (childPids.length > 0) {
|
if (childPids.length > 0) {
|
||||||
logger.info('SYSTEM', 'Force killing remaining children');
|
logger.info('SYSTEM', 'Force killing remaining children');
|
||||||
for (const pid of childPids) {
|
for (const pid of childPids) {
|
||||||
|
|||||||
@@ -0,0 +1,251 @@
|
|||||||
|
/**
|
||||||
|
* ChromaServerManager - Singleton managing local Chroma HTTP server lifecycle
|
||||||
|
*
|
||||||
|
* Starts a persistent Chroma server via `npx chroma run` at worker startup
|
||||||
|
* and manages its lifecycle. In 'remote' mode, skips server start and connects
|
||||||
|
* to an existing server (future cloud support).
|
||||||
|
*
|
||||||
|
* Cross-platform: Linux, macOS, Windows
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { spawn, ChildProcess } from 'child_process';
|
||||||
|
import path from 'path';
|
||||||
|
import os from 'os';
|
||||||
|
import { logger } from '../../utils/logger.js';
|
||||||
|
|
||||||
|
export interface ChromaServerConfig {
|
||||||
|
dataDir: string;
|
||||||
|
host: string;
|
||||||
|
port: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ChromaServerManager {
|
||||||
|
private static instance: ChromaServerManager | null = null;
|
||||||
|
private serverProcess: ChildProcess | null = null;
|
||||||
|
private config: ChromaServerConfig;
|
||||||
|
private starting: boolean = false;
|
||||||
|
private ready: boolean = false;
|
||||||
|
|
||||||
|
private constructor(config: ChromaServerConfig) {
|
||||||
|
this.config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get or create the singleton instance
|
||||||
|
*/
|
||||||
|
static getInstance(config?: ChromaServerConfig): ChromaServerManager {
|
||||||
|
if (!ChromaServerManager.instance) {
|
||||||
|
const defaultConfig: ChromaServerConfig = {
|
||||||
|
dataDir: path.join(os.homedir(), '.claude-mem', 'vector-db'),
|
||||||
|
host: '127.0.0.1',
|
||||||
|
port: 8000
|
||||||
|
};
|
||||||
|
ChromaServerManager.instance = new ChromaServerManager(config || defaultConfig);
|
||||||
|
}
|
||||||
|
return ChromaServerManager.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start the Chroma HTTP server
|
||||||
|
* Spawns `npx chroma run` as a background process
|
||||||
|
*/
|
||||||
|
async start(): Promise<void> {
|
||||||
|
if (this.ready || this.starting) {
|
||||||
|
logger.debug('CHROMA_SERVER', 'Server already started or starting', {
|
||||||
|
ready: this.ready,
|
||||||
|
starting: this.starting
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.starting = true;
|
||||||
|
|
||||||
|
// Cross-platform: use npx.cmd on Windows
|
||||||
|
const isWindows = process.platform === 'win32';
|
||||||
|
const command = isWindows ? 'npx.cmd' : 'npx';
|
||||||
|
|
||||||
|
const args = [
|
||||||
|
'chroma', 'run',
|
||||||
|
'--path', this.config.dataDir,
|
||||||
|
'--host', this.config.host,
|
||||||
|
'--port', String(this.config.port)
|
||||||
|
];
|
||||||
|
|
||||||
|
logger.info('CHROMA_SERVER', 'Starting Chroma server', {
|
||||||
|
command,
|
||||||
|
args: args.join(' '),
|
||||||
|
dataDir: this.config.dataDir
|
||||||
|
});
|
||||||
|
|
||||||
|
this.serverProcess = spawn(command, args, {
|
||||||
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
|
detached: !isWindows, // Don't detach on Windows (no process groups)
|
||||||
|
windowsHide: true // Hide console window on Windows
|
||||||
|
});
|
||||||
|
|
||||||
|
// Log server output for debugging
|
||||||
|
this.serverProcess.stdout?.on('data', (data) => {
|
||||||
|
const msg = data.toString().trim();
|
||||||
|
if (msg) {
|
||||||
|
logger.debug('CHROMA_SERVER', msg);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.serverProcess.stderr?.on('data', (data) => {
|
||||||
|
const msg = data.toString().trim();
|
||||||
|
if (msg) {
|
||||||
|
// Filter out noisy startup messages
|
||||||
|
if (!msg.includes('Chroma') || msg.includes('error') || msg.includes('Error')) {
|
||||||
|
logger.debug('CHROMA_SERVER', msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.serverProcess.on('error', (err) => {
|
||||||
|
logger.error('CHROMA_SERVER', 'Server process error', {}, err);
|
||||||
|
this.ready = false;
|
||||||
|
this.starting = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.serverProcess.on('exit', (code, signal) => {
|
||||||
|
logger.info('CHROMA_SERVER', 'Server process exited', { code, signal });
|
||||||
|
this.ready = false;
|
||||||
|
this.starting = false;
|
||||||
|
this.serverProcess = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wait for the server to become ready
|
||||||
|
* Polls the heartbeat endpoint until success or timeout
|
||||||
|
*/
|
||||||
|
async waitForReady(timeoutMs: number = 60000): Promise<boolean> {
|
||||||
|
const startTime = Date.now();
|
||||||
|
const checkInterval = 500;
|
||||||
|
|
||||||
|
logger.info('CHROMA_SERVER', 'Waiting for server to be ready', {
|
||||||
|
host: this.config.host,
|
||||||
|
port: this.config.port,
|
||||||
|
timeoutMs
|
||||||
|
});
|
||||||
|
|
||||||
|
while (Date.now() - startTime < timeoutMs) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`http://${this.config.host}:${this.config.port}/api/v1/heartbeat`
|
||||||
|
);
|
||||||
|
if (response.ok) {
|
||||||
|
this.ready = true;
|
||||||
|
this.starting = false;
|
||||||
|
logger.info('CHROMA_SERVER', 'Server ready', {
|
||||||
|
host: this.config.host,
|
||||||
|
port: this.config.port,
|
||||||
|
startupTimeMs: Date.now() - startTime
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Server not ready yet, continue polling
|
||||||
|
}
|
||||||
|
await new Promise(resolve => setTimeout(resolve, checkInterval));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.starting = false;
|
||||||
|
logger.error('CHROMA_SERVER', 'Server failed to start within timeout', {
|
||||||
|
timeoutMs,
|
||||||
|
elapsedMs: Date.now() - startTime
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the server is running and ready
|
||||||
|
*/
|
||||||
|
isRunning(): boolean {
|
||||||
|
return this.ready && this.serverProcess !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the server URL for client connections
|
||||||
|
*/
|
||||||
|
getUrl(): string {
|
||||||
|
return `http://${this.config.host}:${this.config.port}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the server configuration
|
||||||
|
*/
|
||||||
|
getConfig(): ChromaServerConfig {
|
||||||
|
return { ...this.config };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop the Chroma server
|
||||||
|
* Gracefully terminates the server process
|
||||||
|
*/
|
||||||
|
async stop(): Promise<void> {
|
||||||
|
if (!this.serverProcess) {
|
||||||
|
logger.debug('CHROMA_SERVER', 'No server process to stop');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info('CHROMA_SERVER', 'Stopping server', { pid: this.serverProcess.pid });
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const proc = this.serverProcess!;
|
||||||
|
const pid = proc.pid;
|
||||||
|
|
||||||
|
const cleanup = () => {
|
||||||
|
this.serverProcess = null;
|
||||||
|
this.ready = false;
|
||||||
|
this.starting = false;
|
||||||
|
logger.info('CHROMA_SERVER', 'Server stopped', { pid });
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Set up exit handler
|
||||||
|
proc.once('exit', cleanup);
|
||||||
|
|
||||||
|
// Cross-platform graceful shutdown
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
// Windows: just send SIGTERM
|
||||||
|
proc.kill('SIGTERM');
|
||||||
|
} else {
|
||||||
|
// Unix: kill the process group to ensure all children are killed
|
||||||
|
if (pid !== undefined) {
|
||||||
|
try {
|
||||||
|
process.kill(-pid, 'SIGTERM');
|
||||||
|
} catch (err) {
|
||||||
|
// Process group kill failed, try direct kill
|
||||||
|
proc.kill('SIGTERM');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
proc.kill('SIGTERM');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Force kill after timeout if still running
|
||||||
|
setTimeout(() => {
|
||||||
|
if (this.serverProcess) {
|
||||||
|
logger.warn('CHROMA_SERVER', 'Force killing server after timeout', { pid });
|
||||||
|
try {
|
||||||
|
proc.kill('SIGKILL');
|
||||||
|
} catch {
|
||||||
|
// Already dead
|
||||||
|
}
|
||||||
|
cleanup();
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset the singleton instance (for testing)
|
||||||
|
*/
|
||||||
|
static reset(): void {
|
||||||
|
if (ChromaServerManager.instance) {
|
||||||
|
// Don't await - just trigger stop
|
||||||
|
ChromaServerManager.instance.stop().catch(() => {});
|
||||||
|
}
|
||||||
|
ChromaServerManager.instance = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
+126
-251
@@ -1,27 +1,27 @@
|
|||||||
/**
|
/**
|
||||||
* ChromaSync Service
|
* ChromaSync Service
|
||||||
*
|
*
|
||||||
* Automatically syncs observations and session summaries to ChromaDB via MCP.
|
* Automatically syncs observations and session summaries to ChromaDB via HTTP.
|
||||||
* This service provides real-time semantic search capabilities by maintaining
|
* This service provides real-time semantic search capabilities by maintaining
|
||||||
* a vector database synchronized with SQLite.
|
* a vector database synchronized with SQLite.
|
||||||
*
|
*
|
||||||
|
* Uses the chromadb npm package's built-in ChromaClient for HTTP connections.
|
||||||
|
* Supports both local server (managed by ChromaServerManager) and remote/cloud
|
||||||
|
* servers for future claude-mem pro features.
|
||||||
|
*
|
||||||
* Design: Fail-fast with no fallbacks - if Chroma is unavailable, syncing fails.
|
* Design: Fail-fast with no fallbacks - if Chroma is unavailable, syncing fails.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
import { ChromaClient, Collection } from 'chromadb';
|
||||||
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
||||||
import { ParsedObservation, ParsedSummary } from '../../sdk/parser.js';
|
import { ParsedObservation, ParsedSummary } from '../../sdk/parser.js';
|
||||||
import { SessionStore } from '../sqlite/SessionStore.js';
|
import { SessionStore } from '../sqlite/SessionStore.js';
|
||||||
import { logger } from '../../utils/logger.js';
|
import { logger } from '../../utils/logger.js';
|
||||||
import { SettingsDefaultsManager } from '../../shared/SettingsDefaultsManager.js';
|
import { SettingsDefaultsManager } from '../../shared/SettingsDefaultsManager.js';
|
||||||
import { USER_SETTINGS_PATH } from '../../shared/paths.js';
|
import { USER_SETTINGS_PATH } from '../../shared/paths.js';
|
||||||
|
import { ChromaServerManager } from './ChromaServerManager.js';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import os from 'os';
|
import os from 'os';
|
||||||
|
|
||||||
// Version injected at build time by esbuild define
|
|
||||||
declare const __DEFAULT_PACKAGE_VERSION__: string;
|
|
||||||
const packageVersion = typeof __DEFAULT_PACKAGE_VERSION__ !== 'undefined' ? __DEFAULT_PACKAGE_VERSION__ : '0.0.0-dev';
|
|
||||||
|
|
||||||
interface ChromaDocument {
|
interface ChromaDocument {
|
||||||
id: string;
|
id: string;
|
||||||
document: string;
|
document: string;
|
||||||
@@ -75,94 +75,66 @@ interface StoredUserPrompt {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class ChromaSync {
|
export class ChromaSync {
|
||||||
private client: Client | null = null;
|
private chromaClient: ChromaClient | null = null;
|
||||||
private transport: StdioClientTransport | null = null;
|
private collection: Collection | null = null;
|
||||||
private connected: boolean = false;
|
|
||||||
private project: string;
|
private project: string;
|
||||||
private collectionName: string;
|
private collectionName: string;
|
||||||
private readonly VECTOR_DB_DIR: string;
|
private readonly VECTOR_DB_DIR: string;
|
||||||
private readonly BATCH_SIZE = 100;
|
private readonly BATCH_SIZE = 100;
|
||||||
|
|
||||||
// Windows: Chroma disabled due to MCP SDK spawning console popups
|
|
||||||
// See: https://github.com/anthropics/claude-mem/issues/675
|
|
||||||
// Will be re-enabled when we migrate to persistent HTTP server
|
|
||||||
private readonly disabled: boolean;
|
|
||||||
|
|
||||||
constructor(project: string) {
|
constructor(project: string) {
|
||||||
this.project = project;
|
this.project = project;
|
||||||
this.collectionName = `cm__${project}`;
|
this.collectionName = `cm__${project}`;
|
||||||
this.VECTOR_DB_DIR = path.join(os.homedir(), '.claude-mem', 'vector-db');
|
this.VECTOR_DB_DIR = path.join(os.homedir(), '.claude-mem', 'vector-db');
|
||||||
|
|
||||||
// Disable on Windows to prevent console popups from MCP subprocess spawning
|
|
||||||
// The MCP SDK's StdioClientTransport spawns Python processes that create visible windows
|
|
||||||
this.disabled = process.platform === 'win32';
|
|
||||||
if (this.disabled) {
|
|
||||||
logger.warn('CHROMA_SYNC', 'Vector search disabled on Windows (prevents console popups)', {
|
|
||||||
project: this.project,
|
|
||||||
reason: 'MCP SDK subprocess spawning causes visible console windows'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if Chroma is disabled (Windows)
|
* Ensure HTTP client is connected to Chroma server
|
||||||
*/
|
* In local mode, verifies ChromaServerManager has started the server
|
||||||
isDisabled(): boolean {
|
* In remote mode, connects directly to configured host
|
||||||
return this.disabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ensure MCP client is connected to Chroma server
|
|
||||||
* Throws error if connection fails
|
* Throws error if connection fails
|
||||||
*/
|
*/
|
||||||
private async ensureConnection(): Promise<void> {
|
private async ensureConnection(): Promise<void> {
|
||||||
if (this.connected && this.client) {
|
if (this.chromaClient) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info('CHROMA_SYNC', 'Connecting to Chroma MCP server...', { project: this.project });
|
logger.info('CHROMA_SYNC', 'Connecting to Chroma HTTP server...', { project: this.project });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Use Python 3.13 by default to avoid onnxruntime compatibility issues with Python 3.14+
|
|
||||||
// See: https://github.com/thedotmack/claude-mem/issues/170 (Python 3.14 incompatibility)
|
|
||||||
const settings = SettingsDefaultsManager.loadFromFile(USER_SETTINGS_PATH);
|
const settings = SettingsDefaultsManager.loadFromFile(USER_SETTINGS_PATH);
|
||||||
const pythonVersion = settings.CLAUDE_MEM_PYTHON_VERSION;
|
const mode = settings.CLAUDE_MEM_CHROMA_MODE || 'local';
|
||||||
const isWindows = process.platform === 'win32';
|
const host = settings.CLAUDE_MEM_CHROMA_HOST || '127.0.0.1';
|
||||||
|
const port = parseInt(settings.CLAUDE_MEM_CHROMA_PORT || '8000', 10);
|
||||||
|
const ssl = settings.CLAUDE_MEM_CHROMA_SSL === 'true';
|
||||||
|
|
||||||
const transportOptions: any = {
|
// In local mode, verify server is running
|
||||||
command: 'uvx',
|
if (mode === 'local') {
|
||||||
args: [
|
const serverManager = ChromaServerManager.getInstance();
|
||||||
'--python', pythonVersion,
|
if (!serverManager.isRunning()) {
|
||||||
'chroma-mcp',
|
throw new Error('Chroma server not running. Ensure worker started correctly.');
|
||||||
'--client-type', 'persistent',
|
}
|
||||||
'--data-dir', this.VECTOR_DB_DIR
|
|
||||||
],
|
|
||||||
stderr: 'ignore'
|
|
||||||
};
|
|
||||||
|
|
||||||
// CRITICAL: On Windows, try to hide console window to prevent PowerShell popups
|
|
||||||
// Note: windowsHide may not be supported by MCP SDK's StdioClientTransport
|
|
||||||
if (isWindows) {
|
|
||||||
transportOptions.windowsHide = true;
|
|
||||||
logger.debug('CHROMA_SYNC', 'Windows detected, attempting to hide console window', { project: this.project });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.transport = new StdioClientTransport(transportOptions);
|
// Create HTTP client
|
||||||
|
const protocol = ssl ? 'https' : 'http';
|
||||||
|
const chromaPath = `${protocol}://${host}:${port}`;
|
||||||
|
|
||||||
// Empty capabilities object: this client only calls Chroma tools, doesn't expose any
|
this.chromaClient = new ChromaClient({ path: chromaPath });
|
||||||
this.client = new Client({
|
|
||||||
name: 'claude-mem-chroma-sync',
|
// Verify connection with heartbeat
|
||||||
version: packageVersion
|
await this.chromaClient.heartbeat();
|
||||||
}, {
|
|
||||||
capabilities: {}
|
logger.info('CHROMA_SYNC', 'Connected to Chroma HTTP server', {
|
||||||
|
project: this.project,
|
||||||
|
host,
|
||||||
|
port,
|
||||||
|
ssl,
|
||||||
|
mode
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.client.connect(this.transport);
|
|
||||||
this.connected = true;
|
|
||||||
|
|
||||||
logger.info('CHROMA_SYNC', 'Connected to Chroma MCP server', { project: this.project });
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('CHROMA_SYNC', 'Failed to connect to Chroma MCP server', { project: this.project }, error as Error);
|
logger.error('CHROMA_SYNC', 'Failed to connect to Chroma HTTP server', { project: this.project }, error as Error);
|
||||||
|
this.chromaClient = null;
|
||||||
throw new Error(`Chroma connection failed: ${error instanceof Error ? error.message : String(error)}`);
|
throw new Error(`Chroma connection failed: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -174,7 +146,11 @@ export class ChromaSync {
|
|||||||
private async ensureCollection(): Promise<void> {
|
private async ensureCollection(): Promise<void> {
|
||||||
await this.ensureConnection();
|
await this.ensureConnection();
|
||||||
|
|
||||||
if (!this.client) {
|
if (this.collection) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.chromaClient) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'Chroma client not initialized. Call ensureConnection() before using client methods.' +
|
'Chroma client not initialized. Call ensureConnection() before using client methods.' +
|
||||||
` Project: ${this.project}`
|
` Project: ${this.project}`
|
||||||
@@ -182,50 +158,15 @@ export class ChromaSync {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Try to get collection info (will fail if doesn't exist)
|
// getOrCreateCollection handles both cases
|
||||||
await this.client.callTool({
|
this.collection = await this.chromaClient.getOrCreateCollection({
|
||||||
name: 'chroma_get_collection_info',
|
name: this.collectionName
|
||||||
arguments: {
|
|
||||||
collection_name: this.collectionName
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.debug('CHROMA_SYNC', 'Collection exists', { collection: this.collectionName });
|
logger.debug('CHROMA_SYNC', 'Collection ready', { collection: this.collectionName });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Check if this is a connection error - don't try to create collection
|
logger.error('CHROMA_SYNC', 'Failed to get/create collection', { collection: this.collectionName }, error as Error);
|
||||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
throw new Error(`Collection setup failed: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
const isConnectionError =
|
|
||||||
errorMessage.includes('Not connected') ||
|
|
||||||
errorMessage.includes('Connection closed') ||
|
|
||||||
errorMessage.includes('MCP error -32000');
|
|
||||||
|
|
||||||
if (isConnectionError) {
|
|
||||||
// Reset connection state so next call attempts reconnect
|
|
||||||
this.connected = false;
|
|
||||||
this.client = null;
|
|
||||||
logger.error('CHROMA_SYNC', 'Connection lost during collection check',
|
|
||||||
{ collection: this.collectionName }, error as Error);
|
|
||||||
throw new Error(`Chroma connection lost: ${errorMessage}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only attempt creation if it's genuinely a "collection not found" error
|
|
||||||
logger.error('CHROMA_SYNC', 'Collection check failed, attempting to create', { collection: this.collectionName }, error as Error);
|
|
||||||
logger.info('CHROMA_SYNC', 'Creating collection', { collection: this.collectionName });
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.client.callTool({
|
|
||||||
name: 'chroma_create_collection',
|
|
||||||
arguments: {
|
|
||||||
collection_name: this.collectionName,
|
|
||||||
embedding_function_name: 'default'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
logger.info('CHROMA_SYNC', 'Collection created', { collection: this.collectionName });
|
|
||||||
} catch (createError) {
|
|
||||||
logger.error('CHROMA_SYNC', 'Failed to create collection', { collection: this.collectionName }, createError as Error);
|
|
||||||
throw new Error(`Collection creation failed: ${createError instanceof Error ? createError.message : String(createError)}`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -375,22 +316,18 @@ export class ChromaSync {
|
|||||||
|
|
||||||
await this.ensureCollection();
|
await this.ensureCollection();
|
||||||
|
|
||||||
if (!this.client) {
|
if (!this.collection) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'Chroma client not initialized. Call ensureConnection() before using client methods.' +
|
'Chroma collection not initialized. Call ensureCollection() before using collection methods.' +
|
||||||
` Project: ${this.project}`
|
` Project: ${this.project}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.client.callTool({
|
await this.collection.add({
|
||||||
name: 'chroma_add_documents',
|
ids: documents.map(d => d.id),
|
||||||
arguments: {
|
documents: documents.map(d => d.document),
|
||||||
collection_name: this.collectionName,
|
metadatas: documents.map(d => d.metadata)
|
||||||
documents: documents.map(d => d.document),
|
|
||||||
ids: documents.map(d => d.id),
|
|
||||||
metadatas: documents.map(d => d.metadata)
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.debug('CHROMA_SYNC', 'Documents added', {
|
logger.debug('CHROMA_SYNC', 'Documents added', {
|
||||||
@@ -409,7 +346,6 @@ export class ChromaSync {
|
|||||||
/**
|
/**
|
||||||
* Sync a single observation to Chroma
|
* Sync a single observation to Chroma
|
||||||
* Blocks until sync completes, throws on error
|
* Blocks until sync completes, throws on error
|
||||||
* No-op on Windows (Chroma disabled to prevent console popups)
|
|
||||||
*/
|
*/
|
||||||
async syncObservation(
|
async syncObservation(
|
||||||
observationId: number,
|
observationId: number,
|
||||||
@@ -420,8 +356,6 @@ export class ChromaSync {
|
|||||||
createdAtEpoch: number,
|
createdAtEpoch: number,
|
||||||
discoveryTokens: number = 0
|
discoveryTokens: number = 0
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (this.disabled) return;
|
|
||||||
|
|
||||||
// Convert ParsedObservation to StoredObservation format
|
// Convert ParsedObservation to StoredObservation format
|
||||||
const stored: StoredObservation = {
|
const stored: StoredObservation = {
|
||||||
id: observationId,
|
id: observationId,
|
||||||
@@ -456,7 +390,6 @@ export class ChromaSync {
|
|||||||
/**
|
/**
|
||||||
* Sync a single summary to Chroma
|
* Sync a single summary to Chroma
|
||||||
* Blocks until sync completes, throws on error
|
* Blocks until sync completes, throws on error
|
||||||
* No-op on Windows (Chroma disabled to prevent console popups)
|
|
||||||
*/
|
*/
|
||||||
async syncSummary(
|
async syncSummary(
|
||||||
summaryId: number,
|
summaryId: number,
|
||||||
@@ -467,8 +400,6 @@ export class ChromaSync {
|
|||||||
createdAtEpoch: number,
|
createdAtEpoch: number,
|
||||||
discoveryTokens: number = 0
|
discoveryTokens: number = 0
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (this.disabled) return;
|
|
||||||
|
|
||||||
// Convert ParsedSummary to StoredSummary format
|
// Convert ParsedSummary to StoredSummary format
|
||||||
const stored: StoredSummary = {
|
const stored: StoredSummary = {
|
||||||
id: summaryId,
|
id: summaryId,
|
||||||
@@ -519,7 +450,6 @@ export class ChromaSync {
|
|||||||
/**
|
/**
|
||||||
* Sync a single user prompt to Chroma
|
* Sync a single user prompt to Chroma
|
||||||
* Blocks until sync completes, throws on error
|
* Blocks until sync completes, throws on error
|
||||||
* No-op on Windows (Chroma disabled to prevent console popups)
|
|
||||||
*/
|
*/
|
||||||
async syncUserPrompt(
|
async syncUserPrompt(
|
||||||
promptId: number,
|
promptId: number,
|
||||||
@@ -529,8 +459,6 @@ export class ChromaSync {
|
|||||||
promptNumber: number,
|
promptNumber: number,
|
||||||
createdAtEpoch: number
|
createdAtEpoch: number
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (this.disabled) return;
|
|
||||||
|
|
||||||
// Create StoredUserPrompt format
|
// Create StoredUserPrompt format
|
||||||
const stored: StoredUserPrompt = {
|
const stored: StoredUserPrompt = {
|
||||||
id: promptId,
|
id: promptId,
|
||||||
@@ -562,11 +490,11 @@ export class ChromaSync {
|
|||||||
summaries: Set<number>;
|
summaries: Set<number>;
|
||||||
prompts: Set<number>;
|
prompts: Set<number>;
|
||||||
}> {
|
}> {
|
||||||
await this.ensureConnection();
|
await this.ensureCollection();
|
||||||
|
|
||||||
if (!this.client) {
|
if (!this.collection) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'Chroma client not initialized. Call ensureConnection() before using client methods.' +
|
'Chroma collection not initialized. Call ensureCollection() before using collection methods.' +
|
||||||
` Project: ${this.project}`
|
` Project: ${this.project}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -582,24 +510,14 @@ export class ChromaSync {
|
|||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
try {
|
try {
|
||||||
const result = await this.client.callTool({
|
const result = await this.collection.get({
|
||||||
name: 'chroma_get_documents',
|
limit,
|
||||||
arguments: {
|
offset,
|
||||||
collection_name: this.collectionName,
|
where: { project: this.project },
|
||||||
limit,
|
include: ['metadatas']
|
||||||
offset,
|
|
||||||
where: { project: this.project }, // Filter by project
|
|
||||||
include: ['metadatas']
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = result.content[0];
|
const metadatas = result.metadatas || [];
|
||||||
if (data.type !== 'text') {
|
|
||||||
throw new Error('Unexpected response type from chroma_get_documents');
|
|
||||||
}
|
|
||||||
|
|
||||||
const parsed = JSON.parse(data.text);
|
|
||||||
const metadatas = parsed.metadatas || [];
|
|
||||||
|
|
||||||
if (metadatas.length === 0) {
|
if (metadatas.length === 0) {
|
||||||
break; // No more documents
|
break; // No more documents
|
||||||
@@ -607,13 +525,14 @@ export class ChromaSync {
|
|||||||
|
|
||||||
// Extract SQLite IDs from metadata
|
// Extract SQLite IDs from metadata
|
||||||
for (const meta of metadatas) {
|
for (const meta of metadatas) {
|
||||||
if (meta.sqlite_id) {
|
if (meta && meta.sqlite_id) {
|
||||||
|
const sqliteId = meta.sqlite_id as number;
|
||||||
if (meta.doc_type === 'observation') {
|
if (meta.doc_type === 'observation') {
|
||||||
observationIds.add(meta.sqlite_id);
|
observationIds.add(sqliteId);
|
||||||
} else if (meta.doc_type === 'session_summary') {
|
} else if (meta.doc_type === 'session_summary') {
|
||||||
summaryIds.add(meta.sqlite_id);
|
summaryIds.add(sqliteId);
|
||||||
} else if (meta.doc_type === 'user_prompt') {
|
} else if (meta.doc_type === 'user_prompt') {
|
||||||
promptIds.add(meta.sqlite_id);
|
promptIds.add(sqliteId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -645,11 +564,8 @@ export class ChromaSync {
|
|||||||
* Backfill: Sync all observations missing from Chroma
|
* Backfill: Sync all observations missing from Chroma
|
||||||
* Reads from SQLite and syncs in batches
|
* Reads from SQLite and syncs in batches
|
||||||
* Throws error if backfill fails
|
* Throws error if backfill fails
|
||||||
* No-op on Windows (Chroma disabled to prevent console popups)
|
|
||||||
*/
|
*/
|
||||||
async ensureBackfilled(): Promise<void> {
|
async ensureBackfilled(): Promise<void> {
|
||||||
if (this.disabled) return;
|
|
||||||
|
|
||||||
logger.info('CHROMA_SYNC', 'Starting smart backfill', { project: this.project });
|
logger.info('CHROMA_SYNC', 'Starting smart backfill', { project: this.project });
|
||||||
|
|
||||||
await this.ensureCollection();
|
await this.ensureCollection();
|
||||||
@@ -816,132 +732,91 @@ export class ChromaSync {
|
|||||||
/**
|
/**
|
||||||
* Query Chroma collection for semantic search
|
* Query Chroma collection for semantic search
|
||||||
* Used by SearchManager for vector-based search
|
* Used by SearchManager for vector-based search
|
||||||
* Returns empty results on Windows (Chroma disabled to prevent console popups)
|
|
||||||
*/
|
*/
|
||||||
async queryChroma(
|
async queryChroma(
|
||||||
query: string,
|
query: string,
|
||||||
limit: number,
|
limit: number,
|
||||||
whereFilter?: Record<string, any>
|
whereFilter?: Record<string, any>
|
||||||
): Promise<{ ids: number[]; distances: number[]; metadatas: any[] }> {
|
): Promise<{ ids: number[]; distances: number[]; metadatas: any[] }> {
|
||||||
if (this.disabled) {
|
await this.ensureCollection();
|
||||||
return { ids: [], distances: [], metadatas: [] };
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.ensureConnection();
|
if (!this.collection) {
|
||||||
|
|
||||||
if (!this.client) {
|
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'Chroma client not initialized. Call ensureConnection() before using client methods.' +
|
'Chroma collection not initialized. Call ensureCollection() before using collection methods.' +
|
||||||
` Project: ${this.project}`
|
` Project: ${this.project}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const whereStringified = whereFilter ? JSON.stringify(whereFilter) : undefined;
|
|
||||||
|
|
||||||
const arguments_obj = {
|
|
||||||
collection_name: this.collectionName,
|
|
||||||
query_texts: [query],
|
|
||||||
n_results: limit,
|
|
||||||
include: ['documents', 'metadatas', 'distances'],
|
|
||||||
where: whereStringified
|
|
||||||
};
|
|
||||||
|
|
||||||
let result;
|
|
||||||
try {
|
try {
|
||||||
result = await this.client.callTool({
|
const results = await this.collection.query({
|
||||||
name: 'chroma_query_documents',
|
queryTexts: [query],
|
||||||
arguments: arguments_obj
|
nResults: limit,
|
||||||
|
where: whereFilter,
|
||||||
|
include: ['documents', 'metadatas', 'distances']
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Extract unique SQLite IDs from document IDs
|
||||||
|
const ids: number[] = [];
|
||||||
|
const docIds = results.ids?.[0] || [];
|
||||||
|
for (const docId of docIds) {
|
||||||
|
// Extract sqlite_id from document ID (supports three formats):
|
||||||
|
// - obs_{id}_narrative, obs_{id}_fact_0, etc (observations)
|
||||||
|
// - summary_{id}_request, summary_{id}_learned, etc (session summaries)
|
||||||
|
// - prompt_{id} (user prompts)
|
||||||
|
const obsMatch = docId.match(/obs_(\d+)_/);
|
||||||
|
const summaryMatch = docId.match(/summary_(\d+)_/);
|
||||||
|
const promptMatch = docId.match(/prompt_(\d+)/);
|
||||||
|
|
||||||
|
let sqliteId: number | null = null;
|
||||||
|
if (obsMatch) {
|
||||||
|
sqliteId = parseInt(obsMatch[1], 10);
|
||||||
|
} else if (summaryMatch) {
|
||||||
|
sqliteId = parseInt(summaryMatch[1], 10);
|
||||||
|
} else if (promptMatch) {
|
||||||
|
sqliteId = parseInt(promptMatch[1], 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sqliteId !== null && !ids.includes(sqliteId)) {
|
||||||
|
ids.push(sqliteId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
ids,
|
||||||
|
distances: results.distances?.[0] || [],
|
||||||
|
metadatas: results.metadatas?.[0] || []
|
||||||
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
|
||||||
|
// Check for connection errors
|
||||||
const isConnectionError =
|
const isConnectionError =
|
||||||
errorMessage.includes('Not connected') ||
|
errorMessage.includes('ECONNREFUSED') ||
|
||||||
errorMessage.includes('Connection closed') ||
|
errorMessage.includes('ENOTFOUND') ||
|
||||||
errorMessage.includes('MCP error -32000');
|
errorMessage.includes('fetch failed');
|
||||||
|
|
||||||
if (isConnectionError) {
|
if (isConnectionError) {
|
||||||
// Reset connection state so next call attempts reconnect
|
// Reset connection state so next call attempts reconnect
|
||||||
this.connected = false;
|
this.chromaClient = null;
|
||||||
this.client = null;
|
this.collection = null;
|
||||||
logger.error('CHROMA_SYNC', 'Connection lost during query',
|
logger.error('CHROMA_SYNC', 'Connection lost during query',
|
||||||
{ project: this.project, query }, error as Error);
|
{ project: this.project, query }, error as Error);
|
||||||
throw new Error(`Chroma query failed - connection lost: ${errorMessage}`);
|
throw new Error(`Chroma query failed - connection lost: ${errorMessage}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.error('CHROMA_SYNC', 'Query failed', { project: this.project, query }, error as Error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
const resultText = result.content[0]?.text || (() => {
|
|
||||||
logger.error('CHROMA', 'Missing text in MCP chroma_query_documents result', {
|
|
||||||
project: this.project,
|
|
||||||
query_text: query
|
|
||||||
});
|
|
||||||
return '';
|
|
||||||
})();
|
|
||||||
|
|
||||||
// Parse JSON response
|
|
||||||
let parsed: any;
|
|
||||||
try {
|
|
||||||
parsed = JSON.parse(resultText);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('CHROMA_SYNC', 'Failed to parse Chroma response', { project: this.project }, error as Error);
|
|
||||||
return { ids: [], distances: [], metadatas: [] };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract unique IDs from document IDs
|
|
||||||
const ids: number[] = [];
|
|
||||||
const docIds = parsed.ids?.[0] || [];
|
|
||||||
for (const docId of docIds) {
|
|
||||||
// Extract sqlite_id from document ID (supports three formats):
|
|
||||||
// - obs_{id}_narrative, obs_{id}_fact_0, etc (observations)
|
|
||||||
// - summary_{id}_request, summary_{id}_learned, etc (session summaries)
|
|
||||||
// - prompt_{id} (user prompts)
|
|
||||||
const obsMatch = docId.match(/obs_(\d+)_/);
|
|
||||||
const summaryMatch = docId.match(/summary_(\d+)_/);
|
|
||||||
const promptMatch = docId.match(/prompt_(\d+)/);
|
|
||||||
|
|
||||||
let sqliteId: number | null = null;
|
|
||||||
if (obsMatch) {
|
|
||||||
sqliteId = parseInt(obsMatch[1], 10);
|
|
||||||
} else if (summaryMatch) {
|
|
||||||
sqliteId = parseInt(summaryMatch[1], 10);
|
|
||||||
} else if (promptMatch) {
|
|
||||||
sqliteId = parseInt(promptMatch[1], 10);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sqliteId !== null && !ids.includes(sqliteId)) {
|
|
||||||
ids.push(sqliteId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const distances = parsed.distances?.[0] || [];
|
|
||||||
const metadatas = parsed.metadatas?.[0] || [];
|
|
||||||
|
|
||||||
return { ids, distances, metadatas };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Close the Chroma client connection and cleanup subprocess
|
* Close the Chroma client connection
|
||||||
|
* Server lifecycle is managed by ChromaServerManager, not here
|
||||||
*/
|
*/
|
||||||
async close(): Promise<void> {
|
async close(): Promise<void> {
|
||||||
if (!this.connected && !this.client && !this.transport) {
|
// Just clear references - server lifecycle managed by ChromaServerManager
|
||||||
return;
|
this.chromaClient = null;
|
||||||
}
|
this.collection = null;
|
||||||
|
logger.info('CHROMA_SYNC', 'Chroma client closed', { project: this.project });
|
||||||
// Close client first
|
|
||||||
if (this.client) {
|
|
||||||
await this.client.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Explicitly close transport to kill subprocess
|
|
||||||
if (this.transport) {
|
|
||||||
await this.transport.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info('CHROMA_SYNC', 'Chroma client and subprocess closed', { project: this.project });
|
|
||||||
|
|
||||||
// Always reset state
|
|
||||||
this.connected = false;
|
|
||||||
this.client = null;
|
|
||||||
this.transport = null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|||||||
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
||||||
import { getWorkerPort, getWorkerHost } from '../shared/worker-utils.js';
|
import { getWorkerPort, getWorkerHost } from '../shared/worker-utils.js';
|
||||||
import { logger } from '../utils/logger.js';
|
import { logger } from '../utils/logger.js';
|
||||||
|
import { ChromaServerManager } from './sync/ChromaServerManager.js';
|
||||||
|
|
||||||
// Version injected at build time by esbuild define
|
// Version injected at build time by esbuild define
|
||||||
declare const __DEFAULT_PACKAGE_VERSION__: string;
|
declare const __DEFAULT_PACKAGE_VERSION__: string;
|
||||||
@@ -120,6 +121,9 @@ export class WorkerService {
|
|||||||
// Route handlers
|
// Route handlers
|
||||||
private searchRoutes: SearchRoutes | null = null;
|
private searchRoutes: SearchRoutes | null = null;
|
||||||
|
|
||||||
|
// Chroma server (local mode)
|
||||||
|
private chromaServer: ChromaServerManager | null = null;
|
||||||
|
|
||||||
// Initialization tracking
|
// Initialization tracking
|
||||||
private initializationComplete: Promise<void>;
|
private initializationComplete: Promise<void>;
|
||||||
private resolveInitialization!: () => void;
|
private resolveInitialization!: () => void;
|
||||||
@@ -256,8 +260,33 @@ export class WorkerService {
|
|||||||
const { ModeManager } = await import('./domain/ModeManager.js');
|
const { ModeManager } = await import('./domain/ModeManager.js');
|
||||||
const { SettingsDefaultsManager } = await import('../shared/SettingsDefaultsManager.js');
|
const { SettingsDefaultsManager } = await import('../shared/SettingsDefaultsManager.js');
|
||||||
const { USER_SETTINGS_PATH } = await import('../shared/paths.js');
|
const { USER_SETTINGS_PATH } = await import('../shared/paths.js');
|
||||||
|
const os = await import('os');
|
||||||
|
|
||||||
const settings = SettingsDefaultsManager.loadFromFile(USER_SETTINGS_PATH);
|
const settings = SettingsDefaultsManager.loadFromFile(USER_SETTINGS_PATH);
|
||||||
|
|
||||||
|
// Start Chroma server if in local mode
|
||||||
|
const chromaMode = settings.CLAUDE_MEM_CHROMA_MODE || 'local';
|
||||||
|
if (chromaMode === 'local') {
|
||||||
|
logger.info('SYSTEM', 'Starting local Chroma server...');
|
||||||
|
this.chromaServer = ChromaServerManager.getInstance({
|
||||||
|
dataDir: path.join(os.homedir(), '.claude-mem', 'vector-db'),
|
||||||
|
host: settings.CLAUDE_MEM_CHROMA_HOST || '127.0.0.1',
|
||||||
|
port: parseInt(settings.CLAUDE_MEM_CHROMA_PORT || '8000', 10)
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.chromaServer.start();
|
||||||
|
const ready = await this.chromaServer.waitForReady(60000);
|
||||||
|
|
||||||
|
if (ready) {
|
||||||
|
logger.success('SYSTEM', 'Chroma server ready');
|
||||||
|
} else {
|
||||||
|
logger.warn('SYSTEM', 'Chroma server failed to start - vector search disabled');
|
||||||
|
this.chromaServer = null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.info('SYSTEM', 'Chroma remote mode - skipping local server');
|
||||||
|
}
|
||||||
|
|
||||||
const modeId = settings.CLAUDE_MEM_MODE;
|
const modeId = settings.CLAUDE_MEM_MODE;
|
||||||
ModeManager.getInstance().loadMode(modeId);
|
ModeManager.getInstance().loadMode(modeId);
|
||||||
logger.info('SYSTEM', `Mode loaded: ${modeId}`);
|
logger.info('SYSTEM', `Mode loaded: ${modeId}`);
|
||||||
@@ -430,7 +459,8 @@ export class WorkerService {
|
|||||||
server: this.server.getHttpServer(),
|
server: this.server.getHttpServer(),
|
||||||
sessionManager: this.sessionManager,
|
sessionManager: this.sessionManager,
|
||||||
mcpClient: this.mcpClient,
|
mcpClient: this.mcpClient,
|
||||||
dbManager: this.dbManager
|
dbManager: this.dbManager,
|
||||||
|
chromaServer: this.chromaServer || undefined
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -50,6 +50,15 @@ export interface SettingsDefaults {
|
|||||||
// Feature Toggles
|
// Feature Toggles
|
||||||
CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY: string;
|
CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY: string;
|
||||||
CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE: string;
|
CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE: string;
|
||||||
|
// Chroma Vector Database Configuration
|
||||||
|
CLAUDE_MEM_CHROMA_MODE: string; // 'local' | 'remote'
|
||||||
|
CLAUDE_MEM_CHROMA_HOST: string;
|
||||||
|
CLAUDE_MEM_CHROMA_PORT: string;
|
||||||
|
CLAUDE_MEM_CHROMA_SSL: string;
|
||||||
|
// Future cloud support
|
||||||
|
CLAUDE_MEM_CHROMA_API_KEY: string;
|
||||||
|
CLAUDE_MEM_CHROMA_TENANT: string;
|
||||||
|
CLAUDE_MEM_CHROMA_DATABASE: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SettingsDefaultsManager {
|
export class SettingsDefaultsManager {
|
||||||
@@ -94,6 +103,15 @@ export class SettingsDefaultsManager {
|
|||||||
// Feature Toggles
|
// Feature Toggles
|
||||||
CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY: 'true',
|
CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY: 'true',
|
||||||
CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE: 'false',
|
CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE: 'false',
|
||||||
|
// Chroma Vector Database Configuration
|
||||||
|
CLAUDE_MEM_CHROMA_MODE: 'local', // 'local' starts npx chroma run, 'remote' connects to existing server
|
||||||
|
CLAUDE_MEM_CHROMA_HOST: '127.0.0.1',
|
||||||
|
CLAUDE_MEM_CHROMA_PORT: '8000',
|
||||||
|
CLAUDE_MEM_CHROMA_SSL: 'false',
|
||||||
|
// Future cloud support (claude-mem pro)
|
||||||
|
CLAUDE_MEM_CHROMA_API_KEY: '',
|
||||||
|
CLAUDE_MEM_CHROMA_TENANT: 'default_tenant',
|
||||||
|
CLAUDE_MEM_CHROMA_DATABASE: 'default_database',
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user