refactor: replace happy_path_error__with_fallback with logger.happyPathError (#313)

- Removed all instances of happy_path_error__with_fallback from various hooks, services, and utilities.
- Introduced logger.happyPathError for consistent logging of unexpected nulls and fallback values.
- Updated the logger utility to include a new happyPathError method with enhanced context and stack trace.
- Deprecated silent-debug utility as all logging functionality has been migrated to the logger.
This commit is contained in:
Alex Newman
2025-12-14 16:56:31 -05:00
committed by GitHub
parent 43db22728e
commit 7fdf5e75ab
24 changed files with 278 additions and 246 deletions
+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 },
''
);
+5 -3
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,9 +177,11 @@ export function buildObservationPrompt(obs: Observation): string {
* Build prompt to generate progress summary
*/
export function buildSummaryPrompt(session: SDKSession): string {
const lastAssistantMessage = session.last_assistant_message || happy_path_error__with_fallback(
const lastAssistantMessage = session.last_assistant_message || logger.happyPathError(
'SDK',
'Missing last_assistant_message in session for summary prompt',
session,
{ sessionId: session.id },
undefined,
''
);
+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';
/**
@@ -51,7 +51,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();
@@ -73,12 +73,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,
@@ -413,7 +413,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);
}
@@ -426,22 +426,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
+20 -9
View File
@@ -1,6 +1,5 @@
import { readFileSync, existsSync } from 'fs';
import { logger } from '../utils/logger.js';
import { happy_path_error__with_fallback } from '../utils/silent-debug.js';
/**
* Extract last message of specified role from transcript JSONL file
@@ -14,9 +13,12 @@ export function extractLastMessage(
stripSystemReminders: boolean = false
): string {
if (!transcriptPath || !existsSync(transcriptPath)) {
happy_path_error__with_fallback(
logger.happyPathError(
'PARSER',
'Transcript path missing or file does not exist',
{ transcriptPath, role }
undefined,
{ transcriptPath, role },
''
);
return '';
}
@@ -24,9 +26,12 @@ export function extractLastMessage(
try {
const content = readFileSync(transcriptPath, 'utf-8').trim();
if (!content) {
happy_path_error__with_fallback(
logger.happyPathError(
'PARSER',
'Transcript file exists but is empty',
{ transcriptPath, role }
undefined,
{ transcriptPath, role },
''
);
return '';
}
@@ -60,9 +65,12 @@ export function extractLastMessage(
// Log if we found the role but the text is empty after processing
if (!text || text.trim() === '') {
happy_path_error__with_fallback(
logger.happyPathError(
'PARSER',
'Found message but content is empty after processing',
{ role, transcriptPath, msgContentType: typeof msgContent, stripSystemReminders }
undefined,
{ role, transcriptPath, msgContentType: typeof msgContent, stripSystemReminders },
''
);
}
@@ -76,9 +84,12 @@ export function extractLastMessage(
// If we searched the whole transcript and didn't find any message of this role
if (!foundMatchingRole) {
happy_path_error__with_fallback(
logger.happyPathError(
'PARSER',
'No message found for role in transcript',
{ role, transcriptPath, totalLines: lines.length }
undefined,
{ role, transcriptPath, totalLines: lines.length },
''
);
}
} catch (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