Merge branch 'main' into feature/mem-search-enhancements

Resolved conflicts in built files by rebuilding from merged source.
All plugin/scripts files regenerated from current source code.

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Alex Newman
2025-12-14 21:33:55 -05:00
24 changed files with 328 additions and 264 deletions
+1 -1
View File
@@ -44,7 +44,7 @@
"worker:stop": "bun plugin/scripts/worker-cli.js stop",
"worker:restart": "bun plugin/scripts/worker-cli.js restart",
"worker:status": "bun plugin/scripts/worker-cli.js status",
"worker:logs": "tail -f ~/.claude-mem/logs/worker-$(date +%Y-%m-%d).log",
"worker:logs": "tail -n 50 ~/.claude-mem/logs/worker-$(date +%Y-%m-%d).log",
"changelog:generate": "node scripts/generate-changelog.js",
"usage:analyze": "node scripts/analyze-usage.js",
"usage:today": "node scripts/analyze-usage.js $(date +%Y-%m-%d)",
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
File diff suppressed because one or more lines are too long
+3 -15
View File
@@ -8,7 +8,6 @@
import { stdin } from 'process';
import { ensureWorkerRunning, getWorkerPort } from '../shared/worker-utils.js';
import { happy_path_error__with_fallback } from '../utils/silent-debug.js';
import { HOOK_TIMEOUTS } from '../shared/hook-constants.js';
export interface SessionEndInput {
@@ -23,11 +22,6 @@ async function cleanupHook(input?: SessionEndInput): Promise<void> {
// Ensure worker is running before any other logic
await ensureWorkerRunning();
happy_path_error__with_fallback('[cleanup-hook] Hook fired', {
session_id: input?.session_id,
reason: input?.reason
});
if (!input) {
throw new Error('cleanup-hook requires input from Claude Code');
}
@@ -48,18 +42,12 @@ async function cleanupHook(input?: SessionEndInput): Promise<void> {
signal: AbortSignal.timeout(HOOK_TIMEOUTS.DEFAULT)
});
if (response.ok) {
const result = await response.json();
happy_path_error__with_fallback('[cleanup-hook] Session cleanup completed', result);
} else {
if (!response.ok) {
// Non-fatal - session might not exist
happy_path_error__with_fallback('[cleanup-hook] Session not found or already cleaned up');
console.error('[cleanup-hook] Session not found or already cleaned up');
}
} catch (error: any) {
// Worker might not be running - that's okay
happy_path_error__with_fallback('[cleanup-hook] Worker not reachable (non-critical)', {
error: error.message
});
// Worker might not be running - that's okay (non-critical)
}
console.log('{"continue": true, "suppressOutput": true}');
-7
View File
@@ -2,7 +2,6 @@ import path from 'path';
import { stdin } from 'process';
import { createHookResponse } from './hook-response.js';
import { ensureWorkerRunning, getWorkerPort } from '../shared/worker-utils.js';
import { happy_path_error__with_fallback } from '../utils/silent-debug.js';
import { handleWorkerError } from '../shared/hook-error-handler.js';
import { handleFetchError } from './shared/error-handler.js';
@@ -27,12 +26,6 @@ async function newHook(input?: UserPromptSubmitInput): Promise<void> {
const { session_id, cwd, prompt } = input;
const project = path.basename(cwd);
happy_path_error__with_fallback('[new-hook] Input received', {
session_id,
project,
prompt_length: prompt?.length
});
const port = getWorkerPort();
// Initialize session via HTTP - handles DB operations and privacy checks
+3 -2
View File
@@ -11,7 +11,6 @@ import { createHookResponse } from './hook-response.js';
import { logger } from '../utils/logger.js';
import { ensureWorkerRunning, getWorkerPort } from '../shared/worker-utils.js';
import { HOOK_TIMEOUTS } from '../shared/hook-constants.js';
import { happy_path_error__with_fallback } from '../utils/silent-debug.js';
import { handleWorkerError } from '../shared/hook-error-handler.js';
import { handleFetchError } from './shared/error-handler.js';
@@ -54,8 +53,10 @@ async function saveHook(input?: PostToolUseInput): Promise<void> {
tool_name,
tool_input,
tool_response,
cwd: cwd || happy_path_error__with_fallback(
cwd: cwd || logger.happyPathError(
'HOOK',
'Missing cwd in PostToolUse hook input',
undefined,
{ session_id, tool_name },
''
)
+3 -2
View File
@@ -14,7 +14,6 @@ import { createHookResponse } from './hook-response.js';
import { logger } from '../utils/logger.js';
import { ensureWorkerRunning, getWorkerPort } from '../shared/worker-utils.js';
import { HOOK_TIMEOUTS } from '../shared/hook-constants.js';
import { happy_path_error__with_fallback } from '../utils/silent-debug.js';
import { handleWorkerError } from '../shared/hook-error-handler.js';
import { handleFetchError } from './shared/error-handler.js';
import { extractLastMessage } from '../shared/transcript-parser.js';
@@ -41,8 +40,10 @@ async function summaryHook(input?: StopInput): Promise<void> {
const port = getWorkerPort();
// Extract last user AND assistant messages from transcript
const transcriptPath = input.transcript_path || happy_path_error__with_fallback(
const transcriptPath = input.transcript_path || logger.happyPathError(
'HOOK',
'Missing transcript_path in Stop hook input',
undefined,
{ session_id },
''
);
+6 -4
View File
@@ -3,7 +3,7 @@
* Generates prompts for the Claude Agent SDK memory worker
*/
import { happy_path_error__with_fallback } from '../utils/silent-debug.js';
import { logger } from '../utils/logger.js';
export interface Observation {
id: number;
@@ -177,10 +177,12 @@ export function buildObservationPrompt(obs: Observation): string {
* Build prompt to generate progress summary
*/
export function buildSummaryPrompt(session: SDKSession): string {
const lastAssistantMessage = happy_path_error__with_fallback(
const lastAssistantMessage = session.last_assistant_message || logger.happyPathError(
'SDK',
'Missing last_assistant_message in session for summary prompt',
session,
session.last_assistant_message || ''
{ sessionId: session.id },
undefined,
''
);
return `PROGRESS SUMMARY CHECKPOINT
+11 -11
View File
@@ -14,7 +14,7 @@ import {
} from '@modelcontextprotocol/sdk/types.js';
import { z } from 'zod';
import { zodToJsonSchema } from 'zod-to-json-schema';
import { happy_path_error__with_fallback } from '../utils/silent-debug.js';
import { logger } from '../utils/logger.js';
import { getWorkerPort, getWorkerHost } from '../shared/worker-utils.js';
/**
@@ -42,7 +42,7 @@ async function callWorkerAPI(
endpoint: string,
params: Record<string, any>
): Promise<{ content: Array<{ type: 'text'; text: string }>; isError?: boolean }> {
happy_path_error__with_fallback('[mcp-server] → Worker API', { endpoint, params });
logger.debug('SYSTEM', '→ Worker API', undefined, { endpoint, params });
try {
const searchParams = new URLSearchParams();
@@ -64,12 +64,12 @@ async function callWorkerAPI(
const data = await response.json() as { content: Array<{ type: 'text'; text: string }>; isError?: boolean };
happy_path_error__with_fallback('[mcp-server] ← Worker API success', { endpoint });
logger.debug('SYSTEM', '← Worker API success', undefined, { endpoint });
// Worker returns { content: [...] } format directly
return data;
} catch (error: any) {
happy_path_error__with_fallback('[mcp-server] ← Worker API error', { endpoint, error: error.message });
logger.error('SYSTEM', '← Worker API error', undefined, { endpoint, error: error.message });
return {
content: [{
type: 'text' as const,
@@ -361,7 +361,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
// Cleanup function
async function cleanup() {
happy_path_error__with_fallback('[mcp-server] Shutting down...');
logger.info('SYSTEM', 'MCP server shutting down');
process.exit(0);
}
@@ -374,22 +374,22 @@ async function main() {
// Start the MCP server
const transport = new StdioServerTransport();
await server.connect(transport);
happy_path_error__with_fallback('[mcp-server] Claude-mem search server started');
logger.info('SYSTEM', 'Claude-mem search server started');
// Check Worker availability in background
setTimeout(async () => {
const workerAvailable = await verifyWorkerConnection();
if (!workerAvailable) {
happy_path_error__with_fallback('[mcp-server] WARNING: Worker not available at', WORKER_BASE_URL);
happy_path_error__with_fallback('[mcp-server] Tools will fail until Worker is started');
happy_path_error__with_fallback('[mcp-server] Start Worker with: npm run worker:restart');
logger.warn('SYSTEM', 'Worker not available', undefined, { workerUrl: WORKER_BASE_URL });
logger.warn('SYSTEM', 'Tools will fail until Worker is started');
logger.warn('SYSTEM', 'Start Worker with: npm run worker:restart');
} else {
happy_path_error__with_fallback('[mcp-server] Worker available at', WORKER_BASE_URL);
logger.info('SYSTEM', 'Worker available', undefined, { workerUrl: WORKER_BASE_URL });
}
}, 0);
}
main().catch((error) => {
happy_path_error__with_fallback('[mcp-server] Fatal error:', error);
logger.error('SYSTEM', 'Fatal error', undefined, error);
process.exit(1);
});
+4 -3
View File
@@ -15,7 +15,6 @@ import { SessionStore } from '../sqlite/SessionStore.js';
import { logger } from '../../utils/logger.js';
import { SettingsDefaultsManager } from '../../shared/SettingsDefaultsManager.js';
import { USER_SETTINGS_PATH } from '../../shared/paths.js';
import { happy_path_error__with_fallback } from '../../utils/silent-debug.js';
import path from 'path';
import os from 'os';
@@ -780,9 +779,11 @@ export class ChromaSync {
arguments: arguments_obj
});
const resultText = happy_path_error__with_fallback(
const resultText = logger.happyPathError(
'CHROMA',
'Missing text in MCP chroma_query_documents result',
{ project: this.project, query_text: query },
{ project: this.project },
{ query_text: query },
result.content[0]?.text || ''
);
-1
View File
@@ -14,7 +14,6 @@ import path from 'path';
import { DatabaseManager } from './DatabaseManager.js';
import { SessionManager } from './SessionManager.js';
import { logger } from '../../utils/logger.js';
import { happy_path_error__with_fallback } from '../../utils/silent-debug.js';
import { parseObservations, parseSummary } from '../../sdk/parser.js';
import { buildInitPrompt, buildObservationPrompt, buildSummaryPrompt, buildContinuationPrompt } from '../../sdk/prompts.js';
import { SettingsDefaultsManager } from '../../shared/SettingsDefaultsManager.js';
@@ -9,7 +9,6 @@ import express, { Request, Response } from 'express';
import { getWorkerPort } from '../../../../shared/worker-utils.js';
import { logger } from '../../../../utils/logger.js';
import { stripMemoryTagsFromJson, stripMemoryTagsFromPrompt } from '../../../../utils/tag-stripping.js';
import { happy_path_error__with_fallback } from '../../../../utils/silent-debug.js';
import { SessionManager } from '../../SessionManager.js';
import { DatabaseManager } from '../../DatabaseManager.js';
import { SDKAgent } from '../../SDKAgent.js';
@@ -342,9 +341,11 @@ export class SessionRoutes extends BaseRouteHandler {
tool_input: cleanedToolInput,
tool_response: cleanedToolResponse,
prompt_number: promptNumber,
cwd: cwd || happy_path_error__with_fallback(
cwd: cwd || logger.happyPathError(
'SESSION',
'Missing cwd when queueing observation in SessionRoutes',
{ sessionDbId, tool_name },
{ sessionId: sessionDbId },
{ tool_name },
''
)
});
@@ -394,9 +395,11 @@ export class SessionRoutes extends BaseRouteHandler {
// Queue summarize
this.sessionManager.queueSummarize(
sessionDbId,
last_user_message || happy_path_error__with_fallback(
last_user_message || logger.happyPathError(
'SESSION',
'Missing last_user_message when queueing summary in SessionRoutes',
{ sessionDbId },
{ sessionId: sessionDbId },
undefined,
''
),
last_assistant_message
+61 -18
View File
@@ -13,42 +13,85 @@ export function extractLastMessage(
stripSystemReminders: boolean = false
): string {
if (!transcriptPath || !existsSync(transcriptPath)) {
logger.happyPathError(
'PARSER',
'Transcript path missing or file does not exist',
undefined,
{ transcriptPath, role },
''
);
return '';
}
try {
const content = readFileSync(transcriptPath, 'utf-8').trim();
if (!content) return '';
if (!content) {
logger.happyPathError(
'PARSER',
'Transcript file exists but is empty',
undefined,
{ transcriptPath, role },
''
);
return '';
}
const lines = content.split('\n');
let foundMatchingRole = false;
for (let i = lines.length - 1; i >= 0; i--) {
try {
const line = JSON.parse(lines[i]);
if (line.type === role && line.message?.content) {
let text = '';
const msgContent = line.message.content;
if (line.type === role) {
foundMatchingRole = true;
if (typeof msgContent === 'string') {
text = msgContent;
} else if (Array.isArray(msgContent)) {
text = msgContent
.filter((c: any) => c.type === 'text')
.map((c: any) => c.text)
.join('\n');
if (line.message?.content) {
let text = '';
const msgContent = line.message.content;
if (typeof msgContent === 'string') {
text = msgContent;
} else if (Array.isArray(msgContent)) {
text = msgContent
.filter((c: any) => c.type === 'text')
.map((c: any) => c.text)
.join('\n');
}
if (stripSystemReminders) {
text = text.replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g, '');
text = text.replace(/\n{3,}/g, '\n\n').trim();
}
// Log if we found the role but the text is empty after processing
if (!text || text.trim() === '') {
logger.happyPathError(
'PARSER',
'Found message but content is empty after processing',
undefined,
{ role, transcriptPath, msgContentType: typeof msgContent, stripSystemReminders },
''
);
}
return text;
}
if (stripSystemReminders) {
text = text.replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g, '');
text = text.replace(/\n{3,}/g, '\n\n').trim();
}
return text;
}
} catch {
continue;
}
}
// If we searched the whole transcript and didn't find any message of this role
if (!foundMatchingRole) {
logger.happyPathError(
'PARSER',
'No message found for role in transcript',
undefined,
{ role, transcriptPath, totalLines: lines.length },
''
);
}
} catch (error) {
logger.error('HOOK', 'Failed to read transcript', { transcriptPath }, error as Error);
}
+52
View File
@@ -251,6 +251,58 @@ class Logger {
timing(component: Component, message: string, durationMs: number, context?: LogContext): void {
this.info(component, `${message}`, context, { duration: `${durationMs}ms` });
}
/**
* Happy Path Error - logs when the expected "happy path" fails but we have a fallback
*
* Semantic meaning: "When the happy path fails, this is an error, but we have a fallback."
*
* Use for:
* Unexpected null/undefined values that should theoretically never happen
* Defensive coding where silent fallback is acceptable
* Situations where you want to track unexpected nulls without breaking execution
*
* DO NOT use for:
* Nullable fields with valid default behavior (use direct || defaults)
* Critical validation failures (use logger.warn or throw Error)
* Try-catch blocks where error is already logged (redundant)
*
* @param component - Component where error occurred
* @param message - Error message describing what went wrong
* @param context - Optional context (sessionId, correlationId, etc)
* @param data - Optional data to include
* @param fallback - Value to return (defaults to empty string)
* @returns The fallback value
*/
happyPathError<T = string>(
component: Component,
message: string,
context?: LogContext,
data?: any,
fallback: T = '' as T
): T {
// Capture stack trace to get caller location
const stack = new Error().stack || '';
const stackLines = stack.split('\n');
// Line 0: "Error"
// Line 1: "at happyPathError ..."
// Line 2: "at <CALLER> ..." <- We want this one
const callerLine = stackLines[2] || '';
const callerMatch = callerLine.match(/at\s+(?:.*\s+)?\(?([^:]+):(\d+):(\d+)\)?/);
const location = callerMatch
? `${callerMatch[1].split('/').pop()}:${callerMatch[2]}`
: 'unknown';
// Log as a warning with location info
const enhancedContext = {
...context,
location
};
this.warn(component, `[HAPPY-PATH] ${message}`, enhancedContext, data);
return fallback;
}
}
// Export singleton instance
+8 -25
View File
@@ -1,33 +1,16 @@
/**
* Happy Path Error With Fallback
*
* Semantic meaning: "When the happy path fails, this is an error, but we have a fallback."
* @deprecated This function is deprecated. Use logger.happyPathError() instead.
* All usages have been migrated to the new logger system which consolidates logs
* into the regular worker logs instead of separate silent.log files.
*
* Logs to ~/.claude-mem/silent.log and returns a fallback value.
* Check logs with `npm run logs:silent`
* Migration example:
* OLD: happy_path_error__with_fallback('Missing value', { data }, 'default')
* NEW: logger.happyPathError('COMPONENT', 'Missing value', undefined, { data }, 'default')
*
* Use happy_path_error__with_fallback for:
* Unexpected null/undefined values that should theoretically never happen
* Defensive coding where silent fallback is acceptable
* Situations where you want to track unexpected nulls without breaking execution
*
* DO NOT use for:
* Nullable fields with valid default behavior (use direct || defaults)
* Critical validation failures (use logger.warn or throw Error)
* Try-catch blocks where error is already logged (redundant)
*
* Good examples:
* // Truly unexpected null (should never happen in theory)
* const id = session.id || happy_path_error__with_fallback('session.id missing', { session });
*
* Bad examples (use direct defaults instead):
* // Nullable field with valid empty default
* const title = obs.title || happy_path_error__with_fallback('obs.title missing', { obs }, '(untitled)');
* // BETTER: const title = obs.title || '(untitled)';
*
* // Array that can validly be undefined/null
* const count = obs.files?.length ?? (happy_path_error__with_fallback('obs.files missing', { obs }), 0);
* // BETTER: const count = obs.files?.length ?? 0;
* See: src/utils/logger.ts for the new happyPathError method
* Issue: #312 - Consolidate silent logs into regular worker logs
*/
import { appendFileSync } from 'fs';
+5 -5
View File
@@ -11,7 +11,7 @@
* This keeps the worker service simple and follows one-way data stream.
*/
import { happy_path_error__with_fallback } from './silent-debug.js';
import { logger } from './logger.js';
/**
* Maximum number of tags allowed in a single content block
@@ -41,14 +41,14 @@ function countTags(content: string): number {
*/
export function stripMemoryTagsFromJson(content: string): string {
if (typeof content !== 'string') {
happy_path_error__with_fallback('[tag-stripping] received non-string for JSON context:', { type: typeof content });
logger.happyPathError('SYSTEM', 'received non-string for JSON context', undefined, { type: typeof content }, '{}');
return '{}'; // Safe default for JSON context
}
// ReDoS protection: limit tag count before regex processing
const tagCount = countTags(content);
if (tagCount > MAX_TAG_COUNT) {
happy_path_error__with_fallback('[tag-stripping] tag count exceeds limit, truncating:', {
logger.warn('SYSTEM', 'tag count exceeds limit', undefined, {
tagCount,
maxAllowed: MAX_TAG_COUNT,
contentLength: content.length
@@ -73,14 +73,14 @@ export function stripMemoryTagsFromJson(content: string): string {
*/
export function stripMemoryTagsFromPrompt(content: string): string {
if (typeof content !== 'string') {
happy_path_error__with_fallback('[tag-stripping] received non-string for prompt context:', { type: typeof content });
logger.happyPathError('SYSTEM', 'received non-string for prompt context', undefined, { type: typeof content }, '');
return ''; // Safe default for prompt content
}
// ReDoS protection: limit tag count before regex processing
const tagCount = countTags(content);
if (tagCount > MAX_TAG_COUNT) {
happy_path_error__with_fallback('[tag-stripping] tag count exceeds limit, truncating:', {
logger.warn('SYSTEM', 'tag count exceeds limit', undefined, {
tagCount,
maxAllowed: MAX_TAG_COUNT,
contentLength: content.length