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:
@@ -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}');
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 },
|
||||
''
|
||||
)
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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 || ''
|
||||
);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user