Refactor logging and improve service initialization

- Updated logging in `search-server.ts` to use `silentDebug` for non-critical messages, reducing console noise.
- Added a health check endpoint in `worker-service.ts` to improve service monitoring.
- Modified the worker service startup process to start the HTTP server immediately and perform slow initialization in the background.
- Refactored database initialization in `Database.ts` to use the correct `Database` class.
- Implemented a port availability check in `context-hook.ts` to ensure the worker service is ready before making requests.
This commit is contained in:
Alex Newman
2025-12-07 18:13:07 -05:00
parent cb1d939750
commit 83b4806718
9 changed files with 212 additions and 172 deletions
+2 -2
View File
@@ -7,13 +7,13 @@
"hooks": [
{
"type": "command",
"command": "node ${CLAUDE_PLUGIN_ROOT}/scripts/context-hook.js",
"command": "node \"${CLAUDE_PLUGIN_ROOT}/../scripts/smart-install.js\" && node ${CLAUDE_PLUGIN_ROOT}/scripts/context-hook.js",
"timeout": 5
},
{
"type": "command",
"command": "node ${CLAUDE_PLUGIN_ROOT}/scripts/user-message-hook.js",
"timeout": 10
"timeout": 5
}
]
}
+1 -1
View File
@@ -1,2 +1,2 @@
#!/usr/bin/env node
import x from"path";import{stdin as i}from"process";import{execSync as S}from"child_process";import l from"path";import{homedir as g}from"os";import{existsSync as _,readFileSync as R}from"fs";import{join as t,dirname as u,basename as y}from"path";import{homedir as c}from"os";import{fileURLToPath as d}from"url";function f(){return typeof __dirname<"u"?__dirname:u(d(import.meta.url))}var k=f(),r=process.env.CLAUDE_MEM_DATA_DIR||t(c(),".claude-mem"),s=process.env.CLAUDE_CONFIG_DIR||t(c(),".claude"),C=t(r,"archives"),I=t(r,"logs"),v=t(r,"trash"),O=t(r,"backups"),U=t(r,"settings.json"),j=t(r,"claude-mem.db"),M=t(r,"vector-db"),b=t(s,"settings.json"),H=t(s,"commands"),L=t(s,"CLAUDE.md");function a(){try{let e=l.join(g(),".claude-mem","settings.json");if(_(e)){let o=JSON.parse(R(e,"utf-8")),n=parseInt(o.env?.CLAUDE_MEM_WORKER_PORT,10);if(!isNaN(n))return n}}catch{}return parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10)}async function p(e){let o=e?.cwd??process.cwd(),n=o?x.basename(o):"unknown-project",m=`http://127.0.0.1:${a()}/api/context/inject?project=${encodeURIComponent(n)}`;return S(`curl -s "${m}"`,{encoding:"utf-8",timeout:5e3})}var D=process.argv.includes("--colors");if(i.isTTY||D)p(void 0).then(e=>{console.log(e),process.exit(0)});else{let e="";i.on("data",o=>e+=o),i.on("end",async()=>{let o=e.trim()?JSON.parse(e):void 0,n=await p(o);console.log(JSON.stringify({hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:n}})),process.exit(0)})}
import D from"path";import{stdin as c}from"process";import{execSync as l}from"child_process";import w from"path";import{homedir as h}from"os";import{existsSync as R,readFileSync as x}from"fs";import{join as e,dirname as f,basename as T}from"path";import{homedir as p}from"os";import{fileURLToPath as g}from"url";function _(){return typeof __dirname<"u"?__dirname:f(g(import.meta.url))}var v=_(),n=process.env.CLAUDE_MEM_DATA_DIR||e(p(),".claude-mem"),i=process.env.CLAUDE_CONFIG_DIR||e(p(),".claude"),C=e(n,"archives"),b=e(n,"logs"),O=e(n,"trash"),U=e(n,"backups"),j=e(n,"settings.json"),M=e(n,"claude-mem.db"),W=e(n,"vector-db"),H=e(i,"settings.json"),L=e(i,"commands"),N=e(i,"CLAUDE.md");function m(){try{let t=w.join(h(),".claude-mem","settings.json");if(R(t)){let r=JSON.parse(x(t,"utf-8")),o=parseInt(r.env?.CLAUDE_MEM_WORKER_PORT,10);if(!isNaN(o))return o}}catch{}return parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10)}async function S(t,r=1e4){let o=Date.now(),s=100;for(;Date.now()-o<r;)try{return l(`curl -s -f -m 1 "http://localhost:${t}/api/health" > /dev/null 2>&1`,{timeout:1e3}),!0}catch{await new Promise(a=>setTimeout(a,s))}return!1}async function u(t){let r=t?.cwd??process.cwd(),o=r?D.basename(r):"unknown-project",s=m();if(!await S(s))throw new Error(`Worker service not available on port ${s} after 10s. Try: npm run worker:restart`);let d=`http://localhost:${s}/api/context/inject?project=${encodeURIComponent(o)}`;return l(`curl -s "${d}"`,{encoding:"utf-8",timeout:5e3}).trim()}var E=process.argv.includes("--colors");if(c.isTTY||E)u(void 0).then(t=>{console.log(t),process.exit(0)});else{let t="";c.on("data",r=>t+=r),c.on("end",async()=>{let r=t.trim()?JSON.parse(t):void 0,o=await u(r);console.log(JSON.stringify({hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:o}})),process.exit(0)})}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -387,7 +387,7 @@ async function main() {
} catch (error) {
// Worker might already be running or PM2 not available - that's okay
// The ensureWorkerRunning() function will handle auto-start when needed
log(' Worker will start automatically when needed', colors.dim);
log('️ Worker startup error', colors.dim);
}
// Success - dependencies installed (if needed)
+28 -3
View File
@@ -20,14 +20,39 @@ export interface SessionStartInput {
[key: string]: any;
}
async function waitForPort(port: number, maxWaitMs: number = 10000): Promise<boolean> {
const startTime = Date.now();
const pollInterval = 100;
while (Date.now() - startTime < maxWaitMs) {
try {
execSync(`curl -s -f -m 1 "http://localhost:${port}/api/health" > /dev/null 2>&1`, {
timeout: 1000,
});
return true;
} catch {
await new Promise((resolve) => setTimeout(resolve, pollInterval));
}
}
return false;
}
async function contextHook(input?: SessionStartInput): Promise<string> {
const cwd = input?.cwd ?? process.cwd();
const project = cwd ? path.basename(cwd) : "unknown-project";
const port = getWorkerPort();
const url = `http://127.0.0.1:${port}/api/context/inject?project=${encodeURIComponent(project)}`;
// Wait for worker to be available
const isAvailable = await waitForPort(port);
if (!isAvailable) {
throw new Error(
`Worker service not available on port ${port} after 10s. Try: npm run worker:restart`
);
}
const url = `http://localhost:${port}/api/context/inject?project=${encodeURIComponent(project)}`;
const result = execSync(`curl -s "${url}"`, { encoding: "utf-8", timeout: 5000 });
const runtime = process.title.includes('bun') ? 'bun' : 'node';
return result + "\n\n [Executed with " + runtime + " runtime]";
return result.trim();
}
// Entry Point - handle stdin/stdout
+76 -76
View File
@@ -30,7 +30,7 @@ try {
search = new SessionSearch();
store = new SessionStore();
} catch (error: any) {
console.error('[search-server] Failed to initialize search:', error.message);
silentDebug('[search-server] Failed to initialize search:', error.message);
process.exit(1);
}
@@ -77,8 +77,8 @@ async function queryChroma(
try {
parsed = JSON.parse(resultText);
} catch (error) {
console.error('[search-server] Failed to parse Chroma response as JSON:', error);
console.error('[search-server] Raw Chroma response:', resultText);
silentDebug('[search-server] Failed to parse Chroma response as JSON:', error);
silentDebug('[search-server] Raw Chroma response:', resultText);
return { ids: [], distances: [], metadatas: [] };
}
@@ -475,7 +475,7 @@ const tools = [
// PATH 1: FILTER-ONLY (no query text) - Skip Chroma/FTS5, use direct SQLite filtering
// This path enables date filtering which Chroma cannot do (requires direct SQLite access)
if (!query) {
console.error(`[search-server] Filter-only query (no query text), using direct SQLite filtering (enables date filters)`);
silentDebug(`[search-server] Filter-only query (no query text), using direct SQLite filtering (enables date filters)`);
const obsOptions = { ...options, type: obs_type, concepts, files };
if (searchObservations) {
observations = search.searchObservations(undefined, obsOptions);
@@ -491,7 +491,7 @@ const tools = [
else if (chromaClient) {
let chromaSucceeded = false;
try {
console.error(`[search-server] Using ChromaDB semantic search (type filter: ${type || 'all'})`);
silentDebug(`[search-server] Using ChromaDB semantic search (type filter: ${type || 'all'})`);
// Build Chroma where filter for doc_type
let whereFilter: Record<string, any> | undefined;
@@ -506,7 +506,7 @@ const tools = [
// Step 1: Chroma semantic search with optional type filter
const chromaResults = await queryChroma(query, 100, whereFilter);
chromaSucceeded = true; // Chroma didn't throw error
console.error(`[search-server] ChromaDB returned ${chromaResults.ids.length} semantic matches`);
silentDebug(`[search-server] ChromaDB returned ${chromaResults.ids.length} semantic matches`);
if (chromaResults.ids.length > 0) {
// Step 2: Filter by recency (90 days)
@@ -517,7 +517,7 @@ const tools = [
isRecent: meta && meta.created_at_epoch > ninetyDaysAgo
})).filter(item => item.isRecent);
console.error(`[search-server] ${recentMetadata.length} results within 90-day window`);
silentDebug(`[search-server] ${recentMetadata.length} results within 90-day window`);
// Step 3: Categorize IDs by document type
const obsIds: number[] = [];
@@ -535,7 +535,7 @@ const tools = [
}
}
console.error(`[search-server] Categorized: ${obsIds.length} obs, ${sessionIds.length} sessions, ${promptIds.length} prompts`);
silentDebug(`[search-server] Categorized: ${obsIds.length} obs, ${sessionIds.length} sessions, ${promptIds.length} prompts`);
// Step 4: Hydrate from SQLite with additional filters
if (obsIds.length > 0) {
@@ -550,14 +550,14 @@ const tools = [
prompts = store.getUserPromptsByIds(promptIds, { orderBy: 'date_desc', limit: options.limit });
}
console.error(`[search-server] Hydrated ${observations.length} obs, ${sessions.length} sessions, ${prompts.length} prompts from SQLite`);
silentDebug(`[search-server] Hydrated ${observations.length} obs, ${sessions.length} sessions, ${prompts.length} prompts from SQLite`);
} else {
// Chroma returned 0 results - this is the correct answer, don't fall back to FTS5
console.error(`[search-server] ChromaDB found no matches (this is final - NOT falling back to FTS5)`);
silentDebug(`[search-server] ChromaDB found no matches (this is final - NOT falling back to FTS5)`);
}
} catch (chromaError: any) {
console.error('[search-server] ChromaDB failed - returning empty results (FTS5 fallback removed):', chromaError.message);
console.error('[search-server] Install UVX/Python to enable vector search: https://docs.astral.sh/uv/getting-started/installation/');
silentDebug('[search-server] ChromaDB failed - returning empty results (FTS5 fallback removed):', chromaError.message);
silentDebug('[search-server] Install UVX/Python to enable vector search: https://docs.astral.sh/uv/getting-started/installation/');
// Return empty results - no fallback
observations = [];
sessions = [];
@@ -566,8 +566,8 @@ const tools = [
}
// ChromaDB not initialized - return empty results (no fallback)
else {
console.error(`[search-server] ChromaDB not initialized - returning empty results (FTS5 fallback removed)`);
console.error(`[search-server] Install UVX/Python to enable vector search: https://docs.astral.sh/uv/getting-started/installation/`);
silentDebug(`[search-server] ChromaDB not initialized - returning empty results (FTS5 fallback removed)`);
silentDebug(`[search-server] Install UVX/Python to enable vector search: https://docs.astral.sh/uv/getting-started/installation/`);
observations = [];
sessions = [];
prompts = [];
@@ -700,9 +700,9 @@ const tools = [
if (chromaClient) {
try {
console.error('[search-server] Using hybrid semantic search for timeline query');
silentDebug('[search-server] Using hybrid semantic search for timeline query');
const chromaResults = await queryChroma(query, 100);
console.error(`[search-server] Chroma returned ${chromaResults.ids.length} semantic matches`);
silentDebug(`[search-server] Chroma returned ${chromaResults.ids.length} semantic matches`);
if (chromaResults.ids.length > 0) {
const ninetyDaysAgo = Date.now() - (90 * 24 * 60 * 60 * 1000);
@@ -716,7 +716,7 @@ const tools = [
}
}
} catch (chromaError: any) {
console.error('[search-server] Chroma query failed - no results (FTS5 fallback removed):', chromaError.message);
silentDebug('[search-server] Chroma query failed - no results (FTS5 fallback removed):', chromaError.message);
}
}
@@ -733,7 +733,7 @@ const tools = [
const topResult = results[0];
anchorId = topResult.id;
anchorEpoch = topResult.created_at_epoch;
console.error(`[search-server] Query mode: Using observation #${topResult.id} as timeline anchor`);
silentDebug(`[search-server] Query mode: Using observation #${topResult.id} as timeline anchor`);
timeline = store.getTimelineAroundObservation(topResult.id, topResult.created_at_epoch, depth_before, depth_after, project);
}
// MODE 2: Anchor-based timeline
@@ -1019,7 +1019,7 @@ const tools = [
try {
if (query) {
// Semantic search filtered to decision type
console.error('[search-server] Using Chroma semantic search with type=decision filter');
silentDebug('[search-server] Using Chroma semantic search with type=decision filter');
const chromaResults = await queryChroma(query, Math.min((filters.limit || 20) * 2, 100), { type: 'decision' });
const obsIds = chromaResults.ids;
@@ -1030,7 +1030,7 @@ const tools = [
}
} else {
// No query: get all decisions, rank by "decision" keyword
console.error('[search-server] Using metadata-first + semantic ranking for decisions');
silentDebug('[search-server] Using metadata-first + semantic ranking for decisions');
const metadataResults = search.findByType('decision', filters);
if (metadataResults.length > 0) {
@@ -1051,7 +1051,7 @@ const tools = [
}
}
} catch (chromaError: any) {
console.error('[search-server] Chroma search failed, using SQLite fallback:', chromaError.message);
silentDebug('[search-server] Chroma search failed, using SQLite fallback:', chromaError.message);
}
}
@@ -1116,7 +1116,7 @@ const tools = [
// Search for change-type observations and change-related concepts
if (chromaClient) {
try {
console.error('[search-server] Using hybrid search for change-related observations');
silentDebug('[search-server] Using hybrid search for change-related observations');
// Get all observations with type="change" or concepts containing change
const typeResults = search.findByType('change', filters);
@@ -1144,7 +1144,7 @@ const tools = [
}
}
} catch (chromaError: any) {
console.error('[search-server] Chroma ranking failed, using SQLite order:', chromaError.message);
silentDebug('[search-server] Chroma ranking failed, using SQLite order:', chromaError.message);
}
}
@@ -1223,7 +1223,7 @@ const tools = [
// Search for how-it-works concept observations
if (chromaClient) {
try {
console.error('[search-server] Using metadata-first + semantic ranking for how-it-works');
silentDebug('[search-server] Using metadata-first + semantic ranking for how-it-works');
const metadataResults = search.findByConcept('how-it-works', filters);
if (metadataResults.length > 0) {
@@ -1243,7 +1243,7 @@ const tools = [
}
}
} catch (chromaError: any) {
console.error('[search-server] Chroma ranking failed, using SQLite order:', chromaError.message);
silentDebug('[search-server] Chroma ranking failed, using SQLite order:', chromaError.message);
}
}
@@ -1304,11 +1304,11 @@ const tools = [
// Vector-first search via ChromaDB
if (chromaClient) {
try {
console.error('[search-server] Using hybrid semantic search (Chroma + SQLite)');
silentDebug('[search-server] Using hybrid semantic search (Chroma + SQLite)');
// Step 1: Chroma semantic search (top 100)
const chromaResults = await queryChroma(query, 100);
console.error(`[search-server] Chroma returned ${chromaResults.ids.length} semantic matches`);
silentDebug(`[search-server] Chroma returned ${chromaResults.ids.length} semantic matches`);
if (chromaResults.ids.length > 0) {
// Step 2: Filter by recency (90 days)
@@ -1318,17 +1318,17 @@ const tools = [
return meta && meta.created_at_epoch > ninetyDaysAgo;
});
console.error(`[search-server] ${recentIds.length} results within 90-day window`);
silentDebug(`[search-server] ${recentIds.length} results within 90-day window`);
// Step 3: Hydrate from SQLite in temporal order
if (recentIds.length > 0) {
const limit = options.limit || 20;
results = store.getObservationsByIds(recentIds, { orderBy: 'date_desc', limit });
console.error(`[search-server] Hydrated ${results.length} observations from SQLite`);
silentDebug(`[search-server] Hydrated ${results.length} observations from SQLite`);
}
}
} catch (chromaError: any) {
console.error('[search-server] Chroma query failed - no results (FTS5 fallback removed):', chromaError.message);
silentDebug('[search-server] Chroma query failed - no results (FTS5 fallback removed):', chromaError.message);
}
}
@@ -1391,11 +1391,11 @@ const tools = [
// Vector-first search via ChromaDB
if (chromaClient) {
try {
console.error('[search-server] Using hybrid semantic search for sessions');
silentDebug('[search-server] Using hybrid semantic search for sessions');
// Step 1: Chroma semantic search (top 100)
const chromaResults = await queryChroma(query, 100, { doc_type: 'session_summary' });
console.error(`[search-server] Chroma returned ${chromaResults.ids.length} semantic matches`);
silentDebug(`[search-server] Chroma returned ${chromaResults.ids.length} semantic matches`);
if (chromaResults.ids.length > 0) {
// Step 2: Filter by recency (90 days)
@@ -1405,17 +1405,17 @@ const tools = [
return meta && meta.created_at_epoch > ninetyDaysAgo;
});
console.error(`[search-server] ${recentIds.length} results within 90-day window`);
silentDebug(`[search-server] ${recentIds.length} results within 90-day window`);
// Step 3: Hydrate from SQLite in temporal order
if (recentIds.length > 0) {
const limit = options.limit || 20;
results = store.getSessionSummariesByIds(recentIds, { orderBy: 'date_desc', limit });
console.error(`[search-server] Hydrated ${results.length} sessions from SQLite`);
silentDebug(`[search-server] Hydrated ${results.length} sessions from SQLite`);
}
}
} catch (chromaError: any) {
console.error('[search-server] Chroma query failed - no results (FTS5 fallback removed):', chromaError.message);
silentDebug('[search-server] Chroma query failed - no results (FTS5 fallback removed):', chromaError.message);
}
}
@@ -1478,11 +1478,11 @@ const tools = [
// Metadata-first, semantic-enhanced search
if (chromaClient) {
try {
console.error('[search-server] Using metadata-first + semantic ranking for concept search');
silentDebug('[search-server] Using metadata-first + semantic ranking for concept search');
// Step 1: SQLite metadata filter (get all IDs with this concept)
const metadataResults = search.findByConcept(concept, filters);
console.error(`[search-server] Found ${metadataResults.length} observations with concept "${concept}"`);
silentDebug(`[search-server] Found ${metadataResults.length} observations with concept "${concept}"`);
if (metadataResults.length > 0) {
// Step 2: Chroma semantic ranking (rank by relevance to concept)
@@ -1497,7 +1497,7 @@ const tools = [
}
}
console.error(`[search-server] Chroma ranked ${rankedIds.length} results by semantic relevance`);
silentDebug(`[search-server] Chroma ranked ${rankedIds.length} results by semantic relevance`);
// Step 3: Hydrate in semantic rank order
if (rankedIds.length > 0) {
@@ -1507,14 +1507,14 @@ const tools = [
}
}
} catch (chromaError: any) {
console.error('[search-server] Chroma ranking failed, using SQLite order:', chromaError.message);
silentDebug('[search-server] Chroma ranking failed, using SQLite order:', chromaError.message);
// Fall through to SQLite fallback
}
}
// Fall back to SQLite-only if Chroma unavailable or failed
if (results.length === 0) {
console.error('[search-server] Using SQLite-only concept search');
silentDebug('[search-server] Using SQLite-only concept search');
results = search.findByConcept(concept, filters);
}
@@ -1578,11 +1578,11 @@ const tools = [
// Metadata-first, semantic-enhanced search for observations
if (chromaClient) {
try {
console.error('[search-server] Using metadata-first + semantic ranking for file search');
silentDebug('[search-server] Using metadata-first + semantic ranking for file search');
// Step 1: SQLite metadata filter (get all results with this file)
const metadataResults = search.findByFile(filePath, filters);
console.error(`[search-server] Found ${metadataResults.observations.length} observations, ${metadataResults.sessions.length} sessions for file "${filePath}"`);
silentDebug(`[search-server] Found ${metadataResults.observations.length} observations, ${metadataResults.sessions.length} sessions for file "${filePath}"`);
// Sessions: Keep as-is (already summarized, no semantic ranking needed)
sessions = metadataResults.sessions;
@@ -1601,7 +1601,7 @@ const tools = [
}
}
console.error(`[search-server] Chroma ranked ${rankedIds.length} observations by semantic relevance`);
silentDebug(`[search-server] Chroma ranked ${rankedIds.length} observations by semantic relevance`);
// Step 3: Hydrate in semantic rank order
if (rankedIds.length > 0) {
@@ -1611,14 +1611,14 @@ const tools = [
}
}
} catch (chromaError: any) {
console.error('[search-server] Chroma ranking failed, using SQLite order:', chromaError.message);
silentDebug('[search-server] Chroma ranking failed, using SQLite order:', chromaError.message);
// Fall through to SQLite fallback
}
}
// Fall back to SQLite-only if Chroma unavailable or failed
if (observations.length === 0 && sessions.length === 0) {
console.error('[search-server] Using SQLite-only file search');
silentDebug('[search-server] Using SQLite-only file search');
const results = search.findByFile(filePath, filters);
observations = results.observations;
sessions = results.sessions;
@@ -1707,11 +1707,11 @@ const tools = [
// Metadata-first, semantic-enhanced search
if (chromaClient) {
try {
console.error('[search-server] Using metadata-first + semantic ranking for type search');
silentDebug('[search-server] Using metadata-first + semantic ranking for type search');
// Step 1: SQLite metadata filter (get all IDs with this type)
const metadataResults = search.findByType(type, filters);
console.error(`[search-server] Found ${metadataResults.length} observations with type "${typeStr}"`);
silentDebug(`[search-server] Found ${metadataResults.length} observations with type "${typeStr}"`);
if (metadataResults.length > 0) {
// Step 2: Chroma semantic ranking (rank by relevance to type)
@@ -1726,7 +1726,7 @@ const tools = [
}
}
console.error(`[search-server] Chroma ranked ${rankedIds.length} results by semantic relevance`);
silentDebug(`[search-server] Chroma ranked ${rankedIds.length} results by semantic relevance`);
// Step 3: Hydrate in semantic rank order
if (rankedIds.length > 0) {
@@ -1736,14 +1736,14 @@ const tools = [
}
}
} catch (chromaError: any) {
console.error('[search-server] Chroma ranking failed, using SQLite order:', chromaError.message);
silentDebug('[search-server] Chroma ranking failed, using SQLite order:', chromaError.message);
// Fall through to SQLite fallback
}
}
// Fall back to SQLite-only if Chroma unavailable or failed
if (results.length === 0) {
console.error('[search-server] Using SQLite-only type search');
silentDebug('[search-server] Using SQLite-only type search');
results = search.findByType(type, filters);
}
@@ -1944,11 +1944,11 @@ const tools = [
// Vector-first search via ChromaDB
if (chromaClient) {
try {
console.error('[search-server] Using hybrid semantic search for user prompts');
silentDebug('[search-server] Using hybrid semantic search for user prompts');
// Step 1: Chroma semantic search (top 100)
const chromaResults = await queryChroma(query, 100, { doc_type: 'user_prompt' });
console.error(`[search-server] Chroma returned ${chromaResults.ids.length} semantic matches`);
silentDebug(`[search-server] Chroma returned ${chromaResults.ids.length} semantic matches`);
if (chromaResults.ids.length > 0) {
// Step 2: Filter by recency (90 days)
@@ -1958,17 +1958,17 @@ const tools = [
return meta && meta.created_at_epoch > ninetyDaysAgo;
});
console.error(`[search-server] ${recentIds.length} results within 90-day window`);
silentDebug(`[search-server] ${recentIds.length} results within 90-day window`);
// Step 3: Hydrate from SQLite in temporal order
if (recentIds.length > 0) {
const limit = options.limit || 20;
results = store.getUserPromptsByIds(recentIds, { orderBy: 'date_desc', limit });
console.error(`[search-server] Hydrated ${results.length} user prompts from SQLite`);
silentDebug(`[search-server] Hydrated ${results.length} user prompts from SQLite`);
}
}
} catch (chromaError: any) {
console.error('[search-server] Chroma query failed - no results (FTS5 fallback removed):', chromaError.message);
silentDebug('[search-server] Chroma query failed - no results (FTS5 fallback removed):', chromaError.message);
}
}
@@ -2307,9 +2307,9 @@ const tools = [
// Use hybrid search if available
if (chromaClient) {
try {
console.error('[search-server] Using hybrid semantic search for timeline query');
silentDebug('[search-server] Using hybrid semantic search for timeline query');
const chromaResults = await queryChroma(query, 100);
console.error(`[search-server] Chroma returned ${chromaResults.ids.length} semantic matches`);
silentDebug(`[search-server] Chroma returned ${chromaResults.ids.length} semantic matches`);
if (chromaResults.ids.length > 0) {
// Filter by recency (90 days)
@@ -2319,15 +2319,15 @@ const tools = [
return meta && meta.created_at_epoch > ninetyDaysAgo;
});
console.error(`[search-server] ${recentIds.length} results within 90-day window`);
silentDebug(`[search-server] ${recentIds.length} results within 90-day window`);
if (recentIds.length > 0) {
results = store.getObservationsByIds(recentIds, { orderBy: 'date_desc', limit: mode === 'auto' ? 1 : limit });
console.error(`[search-server] Hydrated ${results.length} observations from SQLite`);
silentDebug(`[search-server] Hydrated ${results.length} observations from SQLite`);
}
}
} catch (chromaError: any) {
console.error('[search-server] Chroma query failed - no results (FTS5 fallback removed):', chromaError.message);
silentDebug('[search-server] Chroma query failed - no results (FTS5 fallback removed):', chromaError.message);
}
}
@@ -2378,7 +2378,7 @@ const tools = [
} else {
// Auto mode: Use top result as timeline anchor
const topResult = results[0];
console.error(`[search-server] Auto mode: Using observation #${topResult.id} as timeline anchor`);
silentDebug(`[search-server] Auto mode: Using observation #${topResult.id} as timeline anchor`);
// Get timeline around this observation
const timeline = store.getTimelineAroundObservation(
@@ -2637,15 +2637,15 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
// Cleanup function to properly terminate all child processes
async function cleanup() {
console.error('[search-server] Shutting down...');
silentDebug('[search-server] Shutting down...');
// Close Chroma client (terminates uvx/python processes)
if (chromaClient) {
try {
await chromaClient.close();
console.error('[search-server] Chroma client closed');
silentDebug('[search-server] Chroma client closed');
} catch (error: any) {
console.error('[search-server] Error closing Chroma client:', error.message);
silentDebug('[search-server] Error closing Chroma client:', error.message);
}
}
@@ -2653,22 +2653,22 @@ async function cleanup() {
if (search) {
try {
search.close();
console.error('[search-server] SessionSearch closed');
silentDebug('[search-server] SessionSearch closed');
} catch (error: any) {
console.error('[search-server] Error closing SessionSearch:', error.message);
silentDebug('[search-server] Error closing SessionSearch:', error.message);
}
}
if (store) {
try {
store.close();
console.error('[search-server] SessionStore closed');
silentDebug('[search-server] SessionStore closed');
} catch (error: any) {
console.error('[search-server] Error closing SessionStore:', error.message);
silentDebug('[search-server] Error closing SessionStore:', error.message);
}
}
console.error('[search-server] Shutdown complete');
silentDebug('[search-server] Shutdown complete');
process.exit(0);
}
@@ -2681,12 +2681,12 @@ async function main() {
// Start the MCP server FIRST (critical - must start before blocking operations)
const transport = new StdioServerTransport();
await server.connect(transport);
console.error('[search-server] Claude-mem search server started');
silentDebug('[search-server] Claude-mem search server started');
// Initialize Chroma client in background (non-blocking)
setTimeout(async () => {
try {
console.error('[search-server] Initializing Chroma client...');
silentDebug('[search-server] Initializing Chroma client...');
const chromaTransport = new StdioClientTransport({
command: 'uvx',
args: ['chroma-mcp', '--client-type', 'persistent', '--data-dir', VECTOR_DB_DIR],
@@ -2702,17 +2702,17 @@ async function main() {
await client.connect(chromaTransport);
chromaClient = client;
console.error('[search-server] Chroma client connected successfully');
silentDebug('[search-server] Chroma client connected successfully');
} catch (error: any) {
console.error('[search-server] Failed to initialize Chroma client:', error.message);
console.error('[search-server] Vector search unavailable - text queries will return empty results (FTS5 fallback removed)');
console.error('[search-server] Install UVX/Python to enable vector search: https://docs.astral.sh/uv/getting-started/installation/');
silentDebug('[search-server] Failed to initialize Chroma client:', error.message);
silentDebug('[search-server] Vector search unavailable - text queries will return empty results (FTS5 fallback removed)');
silentDebug('[search-server] Install UVX/Python to enable vector search: https://docs.astral.sh/uv/getting-started/installation/');
chromaClient = null;
}
}, 0);
}
main().catch((error) => {
console.error('[search-server] Fatal error:', error);
silentDebug('[search-server] Fatal error:', error);
process.exit(1);
});
+2 -2
View File
@@ -44,7 +44,7 @@ export class DatabaseManager {
// Ensure the data directory exists
ensureDir(DATA_DIR);
this.db = new BunDatabase(DB_PATH, { create: true, readwrite: true });
this.db = new Database(DB_PATH, { create: true, readwrite: true });
// Apply optimized SQLite settings
this.db.run('PRAGMA journal_mode = WAL');
@@ -168,4 +168,4 @@ export async function initializeDatabase(): Promise<Database> {
return await manager.initialize();
}
export { BunDatabase as Database };
export { Database };
+24 -9
View File
@@ -96,6 +96,11 @@ export class WorkerService {
* Setup HTTP routes (delegate to route classes)
*/
private setupRoutes(): void {
// Health check endpoint
this.app.get('/api/health', (_req, res) => {
res.status(200).json({ status: 'ok' });
});
this.viewerRoutes.setupRoutes(this.app);
this.sessionRoutes.setupRoutes(this.app);
this.dataRoutes.setupRoutes(this.app);
@@ -139,6 +144,25 @@ export class WorkerService {
* Start the worker service
*/
async start(): Promise<void> {
// Start HTTP server FIRST - make port available immediately
const port = getWorkerPort();
this.server = await new Promise<http.Server>((resolve, reject) => {
const srv = this.app.listen(port, () => resolve(srv));
srv.on('error', reject);
});
logger.info('SYSTEM', 'Worker started', { port, pid: process.pid });
// Do slow initialization in background (non-blocking)
this.initializeBackground().catch((error) => {
logger.error('SYSTEM', 'Background initialization failed', {}, error as Error);
});
}
/**
* Background initialization - runs after HTTP server is listening
*/
private async initializeBackground(): Promise<void> {
// Cleanup orphaned processes from previous sessions
await this.cleanupOrphanedProcesses();
@@ -155,15 +179,6 @@ export class WorkerService {
await this.mcpClient.connect(transport);
logger.success('WORKER', 'Connected to MCP search server');
// Start HTTP server
const port = getWorkerPort();
this.server = await new Promise<http.Server>((resolve, reject) => {
const srv = this.app.listen(port, () => resolve(srv));
srv.on('error', reject);
});
logger.info('SYSTEM', 'Worker started', { port, pid: process.pid });
}
/**