Refactor worker port handling and improve logging

- Replaced hardcoded migration port with dynamic port retrieval using `getWorkerPort()` in worker-cli.ts.
- Updated context generator to clarify error handling comments.
- Introduced timeout constants in ProcessManager for better maintainability.
- Configured SQLite settings using constants for mmap size and cache size in DatabaseManager.
- Added timeout constants for Git and NPM commands in BranchManager.
- Enhanced error logging in FormattingService and SearchManager to provide more context on failures.
- Removed deprecated silentDebug function and replaced its usage with logger.debug.
- Updated tests to use dynamic worker port retrieval instead of hardcoded values.
This commit is contained in:
Alex Newman
2025-12-11 14:49:47 -05:00
parent 83b0f9551b
commit ded9671a82
26 changed files with 283 additions and 235 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -5
View File
@@ -1,11 +1,8 @@
import { ProcessManager } from '../services/process/ProcessManager.js';
// During migration, use port 38888 to run alongside the PM2-managed worker on 37777
// Once migration is complete (Phase 3+), this will switch to using settings
const MIGRATION_PORT = 38888;
import { getWorkerPort } from '../shared/worker-utils.js';
const command = process.argv[2];
const port = MIGRATION_PORT;
const port = getWorkerPort();
async function main() {
switch (command) {
+1 -1
View File
@@ -335,7 +335,7 @@ export async function generateContext(input?: ContextInput, useColors: boolean =
priorAssistantMessage = messages.assistantMessage;
}
} catch (error) {
// Ignore
// Expected: Transcript file may not exist or be readable
}
}
+12 -7
View File
@@ -9,6 +9,13 @@ const PID_FILE = join(DATA_DIR, 'worker.pid');
const LOG_DIR = join(DATA_DIR, 'logs');
const MARKETPLACE_ROOT = join(homedir(), '.claude', 'plugins', 'marketplaces', 'thedotmack');
// Timeout constants
const PROCESS_STOP_TIMEOUT_MS = 5000;
const HEALTH_CHECK_TIMEOUT_MS = 10000;
const HEALTH_CHECK_INTERVAL_MS = 200;
const HEALTH_CHECK_FETCH_TIMEOUT_MS = 1000;
const PROCESS_EXIT_CHECK_INTERVAL_MS = 100;
interface PidInfo {
pid: number;
port: number;
@@ -124,7 +131,7 @@ export class ProcessManager {
}
}
static async stop(timeout: number = 5000): Promise<boolean> {
static async stop(timeout: number = PROCESS_STOP_TIMEOUT_MS): Promise<boolean> {
const info = this.getPidInfo();
if (!info) return true;
@@ -202,9 +209,8 @@ export class ProcessManager {
}
}
private static async waitForHealth(pid: number, port: number, timeoutMs: number = 10000): Promise<{ success: boolean; pid?: number; error?: string }> {
private static async waitForHealth(pid: number, port: number, timeoutMs: number = HEALTH_CHECK_TIMEOUT_MS): Promise<{ success: boolean; pid?: number; error?: string }> {
const startTime = Date.now();
const checkInterval = 200;
while (Date.now() - startTime < timeoutMs) {
// Check if process is still alive
@@ -215,7 +221,7 @@ export class ProcessManager {
// Try health check
try {
const response = await fetch(`http://127.0.0.1:${port}/health`, {
signal: AbortSignal.timeout(1000)
signal: AbortSignal.timeout(HEALTH_CHECK_FETCH_TIMEOUT_MS)
});
if (response.ok) {
return { success: true, pid };
@@ -224,7 +230,7 @@ export class ProcessManager {
// Not ready yet
}
await new Promise(resolve => setTimeout(resolve, checkInterval));
await new Promise(resolve => setTimeout(resolve, HEALTH_CHECK_INTERVAL_MS));
}
return { success: false, error: 'Health check timed out' };
@@ -232,13 +238,12 @@ export class ProcessManager {
private static async waitForExit(pid: number, timeout: number): Promise<void> {
const startTime = Date.now();
const checkInterval = 100;
while (Date.now() - startTime < timeout) {
if (!this.isProcessAlive(pid)) {
return;
}
await new Promise(resolve => setTimeout(resolve, checkInterval));
await new Promise(resolve => setTimeout(resolve, PROCESS_EXIT_CHECK_INTERVAL_MS));
}
throw new Error('Process did not exit within timeout');
+6 -2
View File
@@ -1,6 +1,10 @@
import { Database } from 'bun:sqlite';
import { DATA_DIR, DB_PATH, ensureDir } from '../../shared/paths.js';
// SQLite configuration constants
const SQLITE_MMAP_SIZE_BYTES = 256 * 1024 * 1024; // 256MB
const SQLITE_CACHE_SIZE_PAGES = 10_000;
export interface Migration {
version: number;
up: (db: Database) => void;
@@ -51,8 +55,8 @@ export class DatabaseManager {
this.db.run('PRAGMA synchronous = NORMAL');
this.db.run('PRAGMA foreign_keys = ON');
this.db.run('PRAGMA temp_store = memory');
this.db.run('PRAGMA mmap_size = 268435456'); // 256MB
this.db.run('PRAGMA cache_size = 10000');
this.db.run(`PRAGMA mmap_size = ${SQLITE_MMAP_SIZE_BYTES}`);
this.db.run(`PRAGMA cache_size = ${SQLITE_CACHE_SIZE_PAGES}`);
// Initialize schema_versions table
this.initializeSchemaVersions();
+9 -4
View File
@@ -13,6 +13,11 @@ import { logger } from '../../utils/logger.js';
const INSTALLED_PLUGIN_PATH = join(homedir(), '.claude', 'plugins', 'marketplaces', 'thedotmack');
// Timeout constants
const GIT_COMMAND_TIMEOUT_MS = 30_000;
const NPM_INSTALL_TIMEOUT_MS = 120_000;
const DEFAULT_SHELL_TIMEOUT_MS = 60_000;
export interface BranchInfo {
branch: string | null;
isBeta: boolean;
@@ -36,7 +41,7 @@ function execGit(command: string): string {
return execSync(`git ${command}`, {
cwd: INSTALLED_PLUGIN_PATH,
encoding: 'utf-8',
timeout: 30000,
timeout: GIT_COMMAND_TIMEOUT_MS,
windowsHide: true
}).trim();
}
@@ -44,7 +49,7 @@ function execGit(command: string): string {
/**
* Execute shell command in installed plugin directory
*/
function execShell(command: string, timeoutMs: number = 60000): string {
function execShell(command: string, timeoutMs: number = DEFAULT_SHELL_TIMEOUT_MS): string {
return execSync(command, {
cwd: INSTALLED_PLUGIN_PATH,
encoding: 'utf-8',
@@ -165,7 +170,7 @@ export async function switchBranch(targetBranch: string): Promise<SwitchResult>
}
logger.debug('BRANCH', 'Running npm install');
execShell('npm install', 120000); // 2 minute timeout for npm
execShell('npm install', NPM_INSTALL_TIMEOUT_MS);
logger.success('BRANCH', 'Branch switch complete', {
branch: targetBranch
@@ -223,7 +228,7 @@ export async function pullUpdates(): Promise<SwitchResult> {
if (existsSync(installMarker)) {
unlinkSync(installMarker);
}
execShell('npm install', 120000);
execShell('npm install', NPM_INSTALL_TIMEOUT_MS);
logger.success('BRANCH', 'Updates pulled', { branch: info.branch });
+19 -6
View File
@@ -4,6 +4,7 @@
*/
import { ObservationSearchResult, SessionSummarySearchResult, UserPromptSearchResult } from '../sqlite/types.js';
import { logger } from '../../utils/logger.js';
export type FormatType = 'index' | 'full';
@@ -102,7 +103,9 @@ Other tips:
if (facts.length > 0) {
metadata.push(`Facts: ${facts.join('; ')}`);
}
} catch {}
} catch (e) {
logger.warn('FORMAT', 'Invalid JSON in facts field', { obsId: obs.id });
}
}
if (obs.concepts) {
@@ -111,7 +114,9 @@ Other tips:
if (concepts.length > 0) {
metadata.push(`Concepts: ${concepts.join(', ')}`);
}
} catch {}
} catch (e) {
logger.warn('FORMAT', 'Invalid JSON in concepts field', { obsId: obs.id });
}
}
if (obs.files_read || obs.files_modified) {
@@ -119,12 +124,16 @@ Other tips:
if (obs.files_read) {
try {
files.push(...JSON.parse(obs.files_read));
} catch {}
} catch (e) {
logger.warn('FORMAT', 'Invalid JSON in files_read field', { obsId: obs.id });
}
}
if (obs.files_modified) {
try {
files.push(...JSON.parse(obs.files_modified));
} catch {}
} catch (e) {
logger.warn('FORMAT', 'Invalid JSON in files_modified field', { obsId: obs.id });
}
}
if (files.length > 0) {
metadata.push(`Files: ${[...new Set(files)].join(', ')}`);
@@ -190,12 +199,16 @@ Other tips:
if (session.files_read) {
try {
files.push(...JSON.parse(session.files_read));
} catch {}
} catch (e) {
logger.warn('FORMAT', 'Invalid JSON in session files_read field', { sessionId: session.sdk_session_id });
}
}
if (session.files_edited) {
try {
files.push(...JSON.parse(session.files_edited));
} catch {}
} catch (e) {
logger.warn('FORMAT', 'Invalid JSON in session files_edited field', { sessionId: session.sdk_session_id });
}
}
if (files.length > 0) {
metadata.push(`Files: ${[...new Set(files)].join(', ')}`);
+59 -59
View File
@@ -13,7 +13,7 @@ import { ChromaSync } from '../sync/ChromaSync.js';
import { FormattingService } from './FormattingService.js';
import { TimelineService, TimelineItem } from './TimelineService.js';
import { ObservationSearchResult, SessionSummarySearchResult, UserPromptSearchResult } from '../sqlite/types.js';
import { happy_path_error__with_fallback } from '../../utils/silent-debug.js';
import { logger } from '../../utils/logger.js';
const COLLECTION_NAME = 'cm__claude-mem';
@@ -97,7 +97,7 @@ export class SearchManager {
// 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) {
happy_path_error__with_fallback(`[mcp-server] Filter-only query (no query text), using direct SQLite filtering (enables date filters)`);
logger.debug('SEARCH', 'Filter-only query (no query text), using direct SQLite filtering', { enablesDateFilters: true });
const obsOptions = { ...options, type: obs_type, concepts, files };
if (searchObservations) {
observations = this.sessionSearch.searchObservations(undefined, obsOptions);
@@ -113,7 +113,7 @@ export class SearchManager {
else if (this.chromaSync) {
let chromaSucceeded = false;
try {
happy_path_error__with_fallback(`[mcp-server] Using ChromaDB semantic search (type filter: ${type || 'all'})`);
logger.debug('SEARCH', 'Using ChromaDB semantic search', { typeFilter: type || 'all' });
// Build Chroma where filter for doc_type
let whereFilter: Record<string, any> | undefined;
@@ -128,7 +128,7 @@ export class SearchManager {
// Step 1: Chroma semantic search with optional type filter
const chromaResults = await this.queryChroma(query, 100, whereFilter);
chromaSucceeded = true; // Chroma didn't throw error
happy_path_error__with_fallback(`[mcp-server] ChromaDB returned ${chromaResults.ids.length} semantic matches`);
logger.debug('SEARCH', 'ChromaDB returned semantic matches', { matchCount: chromaResults.ids.length });
if (chromaResults.ids.length > 0) {
// Step 2: Filter by recency (90 days)
@@ -139,7 +139,7 @@ export class SearchManager {
isRecent: meta && meta.created_at_epoch > ninetyDaysAgo
})).filter(item => item.isRecent);
happy_path_error__with_fallback(`[mcp-server] ${recentMetadata.length} results within 90-day window`);
logger.debug('SEARCH', 'Results within 90-day window', { count: recentMetadata.length });
// Step 3: Categorize IDs by document type
const obsIds: number[] = [];
@@ -157,7 +157,7 @@ export class SearchManager {
}
}
happy_path_error__with_fallback(`[mcp-server] Categorized: ${obsIds.length} obs, ${sessionIds.length} sessions, ${promptIds.length} prompts`);
logger.debug('SEARCH', 'Categorized results by type', { observations: obsIds.length, sessions: sessionIds.length, prompts: promptIds.length });
// Step 4: Hydrate from SQLite with additional filters
if (obsIds.length > 0) {
@@ -172,14 +172,14 @@ export class SearchManager {
prompts = this.sessionStore.getUserPromptsByIds(promptIds, { orderBy: 'date_desc', limit: options.limit });
}
happy_path_error__with_fallback(`[mcp-server] Hydrated ${observations.length} obs, ${sessions.length} sessions, ${prompts.length} prompts from SQLite`);
logger.debug('SEARCH', 'Hydrated results from SQLite', { observations: observations.length, sessions: sessions.length, prompts: prompts.length });
} else {
// Chroma returned 0 results - this is the correct answer, don't fall back to FTS5
happy_path_error__with_fallback(`[mcp-server] ChromaDB found no matches (this is final - NOT falling back to FTS5)`);
logger.debug('SEARCH', 'ChromaDB found no matches (final result, no FTS5 fallback)', {});
}
} catch (chromaError: any) {
happy_path_error__with_fallback('[mcp-server] ChromaDB failed - returning empty results (FTS5 fallback removed):', chromaError.message);
happy_path_error__with_fallback('[mcp-server] Install UVX/Python to enable vector search: https://docs.astral.sh/uv/getting-started/installation/');
logger.debug('SEARCH', 'ChromaDB failed - returning empty results (FTS5 fallback removed)', { error: chromaError.message });
logger.debug('SEARCH', 'Install UVX/Python to enable vector search', { url: 'https://docs.astral.sh/uv/getting-started/installation/' });
// Return empty results - no fallback
observations = [];
sessions = [];
@@ -188,8 +188,8 @@ export class SearchManager {
}
// ChromaDB not initialized - return empty results (no fallback)
else {
happy_path_error__with_fallback(`[mcp-server] ChromaDB not initialized - returning empty results (FTS5 fallback removed)`);
happy_path_error__with_fallback(`[mcp-server] Install UVX/Python to enable vector search: https://docs.astral.sh/uv/getting-started/installation/`);
logger.debug('SEARCH', 'ChromaDB not initialized - returning empty results (FTS5 fallback removed)', {});
logger.debug('SEARCH', 'Install UVX/Python to enable vector search', { url: 'https://docs.astral.sh/uv/getting-started/installation/' });
observations = [];
sessions = [];
prompts = [];
@@ -312,9 +312,9 @@ export class SearchManager {
if (this.chromaSync) {
try {
happy_path_error__with_fallback('[mcp-server] Using hybrid semantic search for timeline query');
logger.debug('SEARCH', 'Using hybrid semantic search for timeline query', {});
const chromaResults = await this.queryChroma(query, 100);
happy_path_error__with_fallback(`[mcp-server] Chroma returned ${chromaResults?.ids?.length ?? 0} semantic matches`);
logger.debug('SEARCH', 'Chroma returned semantic matches for timeline', { matchCount: chromaResults?.ids?.length ?? 0 });
if (chromaResults?.ids && chromaResults.ids.length > 0) {
const ninetyDaysAgo = Date.now() - (90 * 24 * 60 * 60 * 1000);
@@ -328,7 +328,7 @@ export class SearchManager {
}
}
} catch (chromaError: any) {
happy_path_error__with_fallback('[mcp-server] Chroma query failed - no results (FTS5 fallback removed):', chromaError.message);
logger.debug('SEARCH', 'Chroma query failed - no results (FTS5 fallback removed)', { error: chromaError.message });
}
}
@@ -345,7 +345,7 @@ export class SearchManager {
const topResult = results[0];
anchorId = topResult.id;
anchorEpoch = topResult.created_at_epoch;
happy_path_error__with_fallback(`[mcp-server] Query mode: Using observation #${topResult.id} as timeline anchor`);
logger.debug('SEARCH', 'Query mode: Using observation as timeline anchor', { observationId: topResult.id });
timelineData = this.sessionStore.getTimelineAroundObservation(topResult.id, topResult.created_at_epoch, depth_before, depth_after, project);
}
// MODE 2: Anchor-based timeline
@@ -621,7 +621,7 @@ export class SearchManager {
try {
if (query) {
// Semantic search filtered to decision type
happy_path_error__with_fallback('[mcp-server] Using Chroma semantic search with type=decision filter');
logger.debug('SEARCH', 'Using Chroma semantic search with type=decision filter', {});
const chromaResults = await this.queryChroma(query, Math.min((filters.limit || 20) * 2, 100), { type: 'decision' });
const obsIds = chromaResults.ids;
@@ -632,7 +632,7 @@ export class SearchManager {
}
} else {
// No query: get all decisions, rank by "decision" keyword
happy_path_error__with_fallback('[mcp-server] Using metadata-first + semantic ranking for decisions');
logger.debug('SEARCH', 'Using metadata-first + semantic ranking for decisions', {});
const metadataResults = this.sessionSearch.findByType('decision', filters);
if (metadataResults.length > 0) {
@@ -653,7 +653,7 @@ export class SearchManager {
}
}
} catch (chromaError: any) {
happy_path_error__with_fallback('[mcp-server] Chroma search failed, using SQLite fallback:', chromaError.message);
logger.debug('SEARCH', 'Chroma search failed, using SQLite fallback', { error: chromaError.message });
}
}
@@ -709,7 +709,7 @@ export class SearchManager {
// Search for change-type observations and change-related concepts
if (this.chromaSync) {
try {
happy_path_error__with_fallback('[mcp-server] Using hybrid search for change-related observations');
logger.debug('SEARCH', 'Using hybrid search for change-related observations', {});
// Get all observations with type="change" or concepts containing change
const typeResults = this.sessionSearch.findByType('change', filters);
@@ -737,7 +737,7 @@ export class SearchManager {
}
}
} catch (chromaError: any) {
happy_path_error__with_fallback('[mcp-server] Chroma ranking failed, using SQLite order:', chromaError.message);
logger.debug('SEARCH', 'Chroma ranking failed, using SQLite order', { error: chromaError.message });
}
}
@@ -807,7 +807,7 @@ export class SearchManager {
// Search for how-it-works concept observations
if (this.chromaSync) {
try {
happy_path_error__with_fallback('[mcp-server] Using metadata-first + semantic ranking for how-it-works');
logger.debug('SEARCH', 'Using metadata-first + semantic ranking for how-it-works', {});
const metadataResults = this.sessionSearch.findByConcept('how-it-works', filters);
if (metadataResults.length > 0) {
@@ -827,7 +827,7 @@ export class SearchManager {
}
}
} catch (chromaError: any) {
happy_path_error__with_fallback('[mcp-server] Chroma ranking failed, using SQLite order:', chromaError.message);
logger.debug('SEARCH', 'Chroma ranking failed, using SQLite order', { error: chromaError.message });
}
}
@@ -883,11 +883,11 @@ export class SearchManager {
// Vector-first search via ChromaDB
if (this.chromaSync) {
try {
happy_path_error__with_fallback('[mcp-server] Using hybrid semantic search (Chroma + SQLite)');
logger.debug('SEARCH', 'Using hybrid semantic search (Chroma + SQLite)', {});
// Step 1: Chroma semantic search (top 100)
const chromaResults = await this.queryChroma(query, 100);
happy_path_error__with_fallback(`[mcp-server] Chroma returned ${chromaResults.ids.length} semantic matches`);
logger.debug('SEARCH', 'Chroma returned semantic matches', { matchCount: chromaResults.ids.length });
if (chromaResults.ids.length > 0) {
// Step 2: Filter by recency (90 days)
@@ -897,17 +897,17 @@ export class SearchManager {
return meta && meta.created_at_epoch > ninetyDaysAgo;
});
happy_path_error__with_fallback(`[mcp-server] ${recentIds.length} results within 90-day window`);
logger.debug('SEARCH', 'Results within 90-day window', { count: recentIds.length });
// Step 3: Hydrate from SQLite in temporal order
if (recentIds.length > 0) {
const limit = options.limit || 20;
results = this.sessionStore.getObservationsByIds(recentIds, { orderBy: 'date_desc', limit });
happy_path_error__with_fallback(`[mcp-server] Hydrated ${results.length} observations from SQLite`);
logger.debug('SEARCH', 'Hydrated observations from SQLite', { count: results.length });
}
}
} catch (chromaError: any) {
happy_path_error__with_fallback('[mcp-server] Chroma query failed - no results (FTS5 fallback removed):', chromaError.message);
logger.debug('SEARCH', 'Chroma query failed - no results (FTS5 fallback removed)', { error: chromaError.message });
}
}
@@ -960,11 +960,11 @@ export class SearchManager {
// Vector-first search via ChromaDB
if (this.chromaSync) {
try {
happy_path_error__with_fallback('[mcp-server] Using hybrid semantic search for sessions');
logger.debug('SEARCH', 'Using hybrid semantic search for sessions', {});
// Step 1: Chroma semantic search (top 100)
const chromaResults = await this.queryChroma(query, 100, { doc_type: 'session_summary' });
happy_path_error__with_fallback(`[mcp-server] Chroma returned ${chromaResults.ids.length} semantic matches`);
logger.debug('SEARCH', 'Chroma returned semantic matches for sessions', { matchCount: chromaResults.ids.length });
if (chromaResults.ids.length > 0) {
// Step 2: Filter by recency (90 days)
@@ -974,17 +974,17 @@ export class SearchManager {
return meta && meta.created_at_epoch > ninetyDaysAgo;
});
happy_path_error__with_fallback(`[mcp-server] ${recentIds.length} results within 90-day window`);
logger.debug('SEARCH', 'Results within 90-day window', { count: recentIds.length });
// Step 3: Hydrate from SQLite in temporal order
if (recentIds.length > 0) {
const limit = options.limit || 20;
results = this.sessionStore.getSessionSummariesByIds(recentIds, { orderBy: 'date_desc', limit });
happy_path_error__with_fallback(`[mcp-server] Hydrated ${results.length} sessions from SQLite`);
logger.debug('SEARCH', 'Hydrated sessions from SQLite', { count: results.length });
}
}
} catch (chromaError: any) {
happy_path_error__with_fallback('[mcp-server] Chroma query failed - no results (FTS5 fallback removed):', chromaError.message);
logger.debug('SEARCH', 'Chroma query failed - no results (FTS5 fallback removed)', { error: chromaError.message });
}
}
@@ -1037,11 +1037,11 @@ export class SearchManager {
// Vector-first search via ChromaDB
if (this.chromaSync) {
try {
happy_path_error__with_fallback('[mcp-server] Using hybrid semantic search for user prompts');
logger.debug('SEARCH', 'Using hybrid semantic search for user prompts', {});
// Step 1: Chroma semantic search (top 100)
const chromaResults = await this.queryChroma(query, 100, { doc_type: 'user_prompt' });
happy_path_error__with_fallback(`[mcp-server] Chroma returned ${chromaResults.ids.length} semantic matches`);
logger.debug('SEARCH', 'Chroma returned semantic matches for prompts', { matchCount: chromaResults.ids.length });
if (chromaResults.ids.length > 0) {
// Step 2: Filter by recency (90 days)
@@ -1051,17 +1051,17 @@ export class SearchManager {
return meta && meta.created_at_epoch > ninetyDaysAgo;
});
happy_path_error__with_fallback(`[mcp-server] ${recentIds.length} results within 90-day window`);
logger.debug('SEARCH', 'Results within 90-day window', { count: recentIds.length });
// Step 3: Hydrate from SQLite in temporal order
if (recentIds.length > 0) {
const limit = options.limit || 20;
results = this.sessionStore.getUserPromptsByIds(recentIds, { orderBy: 'date_desc', limit });
happy_path_error__with_fallback(`[mcp-server] Hydrated ${results.length} user prompts from SQLite`);
logger.debug('SEARCH', 'Hydrated user prompts from SQLite', { count: results.length });
}
}
} catch (chromaError: any) {
happy_path_error__with_fallback('[mcp-server] Chroma query failed - no results (FTS5 fallback removed):', chromaError.message);
logger.debug('SEARCH', 'Chroma query failed - no results (FTS5 fallback removed)', { error: chromaError.message });
}
}
@@ -1114,11 +1114,11 @@ export class SearchManager {
// Metadata-first, semantic-enhanced search
if (this.chromaSync) {
try {
happy_path_error__with_fallback('[mcp-server] Using metadata-first + semantic ranking for concept search');
logger.debug('SEARCH', 'Using metadata-first + semantic ranking for concept search', {});
// Step 1: SQLite metadata filter (get all IDs with this concept)
const metadataResults = this.sessionSearch.findByConcept(concept, filters);
happy_path_error__with_fallback(`[mcp-server] Found ${metadataResults.length} observations with concept "${concept}"`);
logger.debug('SEARCH', 'Found observations with concept', { concept, count: metadataResults.length });
if (metadataResults.length > 0) {
// Step 2: Chroma semantic ranking (rank by relevance to concept)
@@ -1133,7 +1133,7 @@ export class SearchManager {
}
}
happy_path_error__with_fallback(`[mcp-server] Chroma ranked ${rankedIds.length} results by semantic relevance`);
logger.debug('SEARCH', 'Chroma ranked results by semantic relevance', { count: rankedIds.length });
// Step 3: Hydrate in semantic rank order
if (rankedIds.length > 0) {
@@ -1143,14 +1143,14 @@ export class SearchManager {
}
}
} catch (chromaError: any) {
happy_path_error__with_fallback('[mcp-server] Chroma ranking failed, using SQLite order:', chromaError.message);
logger.debug('SEARCH', 'Chroma ranking failed, using SQLite order', { error: chromaError.message });
// Fall through to SQLite fallback
}
}
// Fall back to SQLite-only if Chroma unavailable or failed
if (results.length === 0) {
happy_path_error__with_fallback('[mcp-server] Using SQLite-only concept search');
logger.debug('SEARCH', 'Using SQLite-only concept search', {});
results = this.sessionSearch.findByConcept(concept, filters);
}
@@ -1204,11 +1204,11 @@ export class SearchManager {
// Metadata-first, semantic-enhanced search for observations
if (this.chromaSync) {
try {
happy_path_error__with_fallback('[mcp-server] Using metadata-first + semantic ranking for file search');
logger.debug('SEARCH', 'Using metadata-first + semantic ranking for file search', {});
// Step 1: SQLite metadata filter (get all results with this file)
const metadataResults = this.sessionSearch.findByFile(filePath, filters);
happy_path_error__with_fallback(`[mcp-server] Found ${metadataResults.observations.length} observations, ${metadataResults.sessions.length} sessions for file "${filePath}"`);
logger.debug('SEARCH', 'Found results for file', { file: filePath, observations: metadataResults.observations.length, sessions: metadataResults.sessions.length });
// Sessions: Keep as-is (already summarized, no semantic ranking needed)
sessions = metadataResults.sessions;
@@ -1227,7 +1227,7 @@ export class SearchManager {
}
}
happy_path_error__with_fallback(`[mcp-server] Chroma ranked ${rankedIds.length} observations by semantic relevance`);
logger.debug('SEARCH', 'Chroma ranked observations by semantic relevance', { count: rankedIds.length });
// Step 3: Hydrate in semantic rank order
if (rankedIds.length > 0) {
@@ -1237,14 +1237,14 @@ export class SearchManager {
}
}
} catch (chromaError: any) {
happy_path_error__with_fallback('[mcp-server] Chroma ranking failed, using SQLite order:', chromaError.message);
logger.debug('SEARCH', 'Chroma ranking failed, using SQLite order', { error: chromaError.message });
// Fall through to SQLite fallback
}
}
// Fall back to SQLite-only if Chroma unavailable or failed
if (observations.length === 0 && sessions.length === 0) {
happy_path_error__with_fallback('[mcp-server] Using SQLite-only file search');
logger.debug('SEARCH', 'Using SQLite-only file search', {});
const results = this.sessionSearch.findByFile(filePath, filters);
observations = results.observations;
sessions = results.sessions;
@@ -1323,11 +1323,11 @@ export class SearchManager {
// Metadata-first, semantic-enhanced search
if (this.chromaSync) {
try {
happy_path_error__with_fallback('[mcp-server] Using metadata-first + semantic ranking for type search');
logger.debug('SEARCH', 'Using metadata-first + semantic ranking for type search', {});
// Step 1: SQLite metadata filter (get all IDs with this type)
const metadataResults = this.sessionSearch.findByType(type, filters);
happy_path_error__with_fallback(`[mcp-server] Found ${metadataResults.length} observations with type "${typeStr}"`);
logger.debug('SEARCH', 'Found observations with type', { type: typeStr, count: metadataResults.length });
if (metadataResults.length > 0) {
// Step 2: Chroma semantic ranking (rank by relevance to type)
@@ -1342,7 +1342,7 @@ export class SearchManager {
}
}
happy_path_error__with_fallback(`[mcp-server] Chroma ranked ${rankedIds.length} results by semantic relevance`);
logger.debug('SEARCH', 'Chroma ranked results by semantic relevance', { count: rankedIds.length });
// Step 3: Hydrate in semantic rank order
if (rankedIds.length > 0) {
@@ -1352,14 +1352,14 @@ export class SearchManager {
}
}
} catch (chromaError: any) {
happy_path_error__with_fallback('[mcp-server] Chroma ranking failed, using SQLite order:', chromaError.message);
logger.debug('SEARCH', 'Chroma ranking failed, using SQLite order', { error: chromaError.message });
// Fall through to SQLite fallback
}
}
// Fall back to SQLite-only if Chroma unavailable or failed
if (results.length === 0) {
happy_path_error__with_fallback('[mcp-server] Using SQLite-only type search');
logger.debug('SEARCH', 'Using SQLite-only type search', {});
results = this.sessionSearch.findByType(type, filters);
}
@@ -1815,9 +1815,9 @@ export class SearchManager {
// Use hybrid search if available
if (this.chromaSync) {
try {
happy_path_error__with_fallback('[mcp-server] Using hybrid semantic search for timeline query');
logger.debug('SEARCH', 'Using hybrid semantic search for timeline query', {});
const chromaResults = await this.queryChroma(query, 100);
happy_path_error__with_fallback(`[mcp-server] Chroma returned ${chromaResults.ids.length} semantic matches`);
logger.debug('SEARCH', 'Chroma returned semantic matches for timeline', { matchCount: chromaResults.ids.length });
if (chromaResults.ids.length > 0) {
// Filter by recency (90 days)
@@ -1827,15 +1827,15 @@ export class SearchManager {
return meta && meta.created_at_epoch > ninetyDaysAgo;
});
happy_path_error__with_fallback(`[mcp-server] ${recentIds.length} results within 90-day window`);
logger.debug('SEARCH', 'Results within 90-day window', { count: recentIds.length });
if (recentIds.length > 0) {
results = this.sessionStore.getObservationsByIds(recentIds, { orderBy: 'date_desc', limit: mode === 'auto' ? 1 : limit });
happy_path_error__with_fallback(`[mcp-server] Hydrated ${results.length} observations from SQLite`);
logger.debug('SEARCH', 'Hydrated observations from SQLite', { count: results.length });
}
}
} catch (chromaError: any) {
happy_path_error__with_fallback('[mcp-server] Chroma query failed - no results (FTS5 fallback removed):', chromaError.message);
logger.debug('SEARCH', 'Chroma query failed - no results (FTS5 fallback removed)', { error: chromaError.message });
}
}
@@ -1886,7 +1886,7 @@ export class SearchManager {
} else {
// Auto mode: Use top result as timeline anchor
const topResult = results[0];
happy_path_error__with_fallback(`[mcp-server] Auto mode: Using observation #${topResult.id} as timeline anchor`);
logger.debug('SEARCH', 'Auto mode: Using observation as timeline anchor', { observationId: topResult.id });
// Get timeline around this observation
const timelineData = this.sessionStore.getTimelineAroundObservation(
+5 -6
View File
@@ -11,7 +11,6 @@
import { EventEmitter } from 'events';
import { DatabaseManager } from './DatabaseManager.js';
import { logger } from '../../utils/logger.js';
import { happy_path_error__with_fallback } from '../../utils/silent-debug.js';
import type { ActiveSession, PendingMessage, ObservationData } from '../worker-types.js';
export class SessionManager {
@@ -43,7 +42,7 @@ export class SessionManager {
// in the database but the in-memory session still has the stale empty value
const dbSession = this.dbManager.getSessionById(sessionDbId);
if (dbSession.project && dbSession.project !== session.project) {
happy_path_error__with_fallback('[SessionManager] Updating project from database', {
logger.debug('SESSION', 'Updating project from database', {
sessionDbId,
oldProject: session.project,
newProject: dbSession.project
@@ -53,7 +52,7 @@ export class SessionManager {
// Update userPrompt for continuation prompts
if (currentUserPrompt) {
happy_path_error__with_fallback('[SessionManager] Updating userPrompt for continuation', {
logger.debug('SESSION', 'Updating userPrompt for continuation', {
sessionDbId,
promptNumber,
oldPrompt: session.userPrompt.substring(0, 80),
@@ -62,7 +61,7 @@ export class SessionManager {
session.userPrompt = currentUserPrompt;
session.lastPromptNumber = promptNumber || session.lastPromptNumber;
} else {
happy_path_error__with_fallback('[SessionManager] No currentUserPrompt provided for existing session', {
logger.debug('SESSION', 'No currentUserPrompt provided for existing session', {
sessionDbId,
promptNumber,
usingCachedPrompt: session.userPrompt.substring(0, 80)
@@ -78,13 +77,13 @@ export class SessionManager {
const userPrompt = currentUserPrompt || dbSession.user_prompt;
if (!currentUserPrompt) {
happy_path_error__with_fallback('[SessionManager] No currentUserPrompt provided for new session, using database', {
logger.debug('SESSION', 'No currentUserPrompt provided for new session, using database', {
sessionDbId,
promptNumber,
dbPrompt: dbSession.user_prompt.substring(0, 80)
});
} else {
happy_path_error__with_fallback('[SessionManager] Initializing session with fresh userPrompt', {
logger.debug('SESSION', 'Initializing session with fresh userPrompt', {
sessionDbId,
promptNumber,
userPrompt: currentUserPrompt.substring(0, 80)
+29 -6
View File
@@ -4,22 +4,45 @@ import { spawnSync } from "child_process";
import { logger } from "../utils/logger.js";
import { HOOK_TIMEOUTS, getTimeout } from "./hook-constants.js";
import { ProcessManager } from "../services/process/ProcessManager.js";
import { SettingsDefaultsManager } from "./SettingsDefaultsManager.js";
const MARKETPLACE_ROOT = path.join(homedir(), '.claude', 'plugins', 'marketplaces', 'thedotmack');
// Named constants for health checks
const HEALTH_CHECK_TIMEOUT_MS = getTimeout(HOOK_TIMEOUTS.HEALTH_CHECK);
// MIGRATION: Hardcoded port to avoid conflicts with PM2 worker on 37777
// TODO: Switch to settings-based port after PM2 is fully removed
const MIGRATION_PORT = 38888;
// Port cache to avoid repeated settings file reads
let cachedPort: number | null = null;
/**
* Get the worker port number
* Currently hardcoded for migration; will use settings after PM2 removal
* Get the worker port number from settings
* Uses CLAUDE_MEM_WORKER_PORT from settings file or default (37777)
* Caches the port value to avoid repeated file reads
*/
export function getWorkerPort(): number {
return MIGRATION_PORT;
if (cachedPort !== null) {
return cachedPort;
}
try {
const settingsPath = path.join(SettingsDefaultsManager.get('CLAUDE_MEM_DATA_DIR'), 'settings.json');
const settings = SettingsDefaultsManager.loadFromFile(settingsPath);
cachedPort = parseInt(settings.CLAUDE_MEM_WORKER_PORT, 10);
return cachedPort;
} catch (error) {
// Fallback to default if settings load fails
logger.debug('SYSTEM', 'Failed to load port from settings, using default', { error });
cachedPort = parseInt(SettingsDefaultsManager.get('CLAUDE_MEM_WORKER_PORT'), 10);
return cachedPort;
}
}
/**
* Clear the cached port value
* Call this when settings are updated to force re-reading from file
*/
export function clearPortCache(): void {
cachedPort = null;
}
/**
+1 -6
View File
@@ -83,12 +83,7 @@ export function clearSilentLog(): void {
try {
appendFileSync(LOG_FILE, `\n${'='.repeat(80)}\n[${new Date().toISOString()}] Log cleared\n${'='.repeat(80)}\n\n`);
} catch (error) {
// Ignore errors
// Expected: Log file may not be writable
}
}
/**
* @deprecated Use happy_path_error__with_fallback instead
* Backward compatibility alias for silentDebug
*/
export const silentDebug = happy_path_error__with_fallback;
+2 -1
View File
@@ -6,9 +6,10 @@
*/
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { sampleObservation, featureObservation } from '../helpers/scenarios.js';
import { getWorkerPort } from '../../src/shared/worker-utils.js';
describe('Context Injection (SessionStart)', () => {
const WORKER_PORT = 37777;
const WORKER_PORT = getWorkerPort();
const PROJECT_NAME = 'claude-mem';
beforeEach(() => {
@@ -14,9 +14,10 @@ import {
grepScenario,
sessionScenario
} from '../helpers/scenarios.js';
import { getWorkerPort } from '../../src/shared/worker-utils.js';
describe('Observation Capture (PostToolUse)', () => {
const WORKER_PORT = 37777;
const WORKER_PORT = getWorkerPort();
beforeEach(() => {
vi.clearAllMocks();
+2 -1
View File
@@ -6,9 +6,10 @@
*/
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { sampleObservation, featureObservation } from '../helpers/scenarios.js';
import { getWorkerPort } from '../../src/shared/worker-utils.js';
describe('Search (MCP Tools)', () => {
const WORKER_PORT = 37777;
const WORKER_PORT = getWorkerPort();
beforeEach(() => {
vi.clearAllMocks();
+2 -1
View File
@@ -6,9 +6,10 @@
*/
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { sessionScenario } from '../helpers/scenarios.js';
import { getWorkerPort } from '../../src/shared/worker-utils.js';
describe('Session Cleanup (SessionEnd)', () => {
const WORKER_PORT = 37777;
const WORKER_PORT = getWorkerPort();
beforeEach(() => {
vi.clearAllMocks();
+2 -1
View File
@@ -6,9 +6,10 @@
*/
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { bashCommandScenario, sessionScenario } from '../helpers/scenarios.js';
import { getWorkerPort } from '../../src/shared/worker-utils.js';
describe('Session Initialization (UserPromptSubmit)', () => {
const WORKER_PORT = 37777;
const WORKER_PORT = getWorkerPort();
beforeEach(() => {
vi.clearAllMocks();
+2 -1
View File
@@ -6,9 +6,10 @@
*/
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { sessionSummaryScenario, sessionScenario } from '../helpers/scenarios.js';
import { getWorkerPort } from '../../src/shared/worker-utils.js';
describe('Session Summary (Stop)', () => {
const WORKER_PORT = 37777;
const WORKER_PORT = getWorkerPort();
beforeEach(() => {
vi.clearAllMocks();
+2 -1
View File
@@ -11,9 +11,10 @@ import {
sessionScenario,
sampleObservation
} from '../helpers/scenarios.js';
import { getWorkerPort } from '../../src/shared/worker-utils.js';
describe('Full Observation Lifecycle', () => {
const WORKER_PORT = 37777;
const WORKER_PORT = getWorkerPort();
let sessionId: string;
beforeEach(() => {