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:stop": "bun plugin/scripts/worker-cli.js stop",
"worker:restart": "bun plugin/scripts/worker-cli.js restart", "worker:restart": "bun plugin/scripts/worker-cli.js restart",
"worker:status": "bun plugin/scripts/worker-cli.js status", "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", "changelog:generate": "node scripts/generate-changelog.js",
"usage:analyze": "node scripts/analyze-usage.js", "usage:analyze": "node scripts/analyze-usage.js",
"usage:today": "node scripts/analyze-usage.js $(date +%Y-%m-%d)", "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 { stdin } from 'process';
import { ensureWorkerRunning, getWorkerPort } from '../shared/worker-utils.js'; 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'; import { HOOK_TIMEOUTS } from '../shared/hook-constants.js';
export interface SessionEndInput { export interface SessionEndInput {
@@ -23,11 +22,6 @@ async function cleanupHook(input?: SessionEndInput): Promise<void> {
// Ensure worker is running before any other logic // Ensure worker is running before any other logic
await ensureWorkerRunning(); await ensureWorkerRunning();
happy_path_error__with_fallback('[cleanup-hook] Hook fired', {
session_id: input?.session_id,
reason: input?.reason
});
if (!input) { if (!input) {
throw new Error('cleanup-hook requires input from Claude Code'); 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) signal: AbortSignal.timeout(HOOK_TIMEOUTS.DEFAULT)
}); });
if (response.ok) { if (!response.ok) {
const result = await response.json();
happy_path_error__with_fallback('[cleanup-hook] Session cleanup completed', result);
} else {
// Non-fatal - session might not exist // 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) { } catch (error: any) {
// Worker might not be running - that's okay // Worker might not be running - that's okay (non-critical)
happy_path_error__with_fallback('[cleanup-hook] Worker not reachable (non-critical)', {
error: error.message
});
} }
console.log('{"continue": true, "suppressOutput": true}'); console.log('{"continue": true, "suppressOutput": true}');
-7
View File
@@ -2,7 +2,6 @@ import path from 'path';
import { stdin } from 'process'; import { stdin } from 'process';
import { createHookResponse } from './hook-response.js'; import { createHookResponse } from './hook-response.js';
import { ensureWorkerRunning, getWorkerPort } from '../shared/worker-utils.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 { handleWorkerError } from '../shared/hook-error-handler.js';
import { handleFetchError } from './shared/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 { session_id, cwd, prompt } = input;
const project = path.basename(cwd); const project = path.basename(cwd);
happy_path_error__with_fallback('[new-hook] Input received', {
session_id,
project,
prompt_length: prompt?.length
});
const port = getWorkerPort(); const port = getWorkerPort();
// Initialize session via HTTP - handles DB operations and privacy checks // 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 { logger } from '../utils/logger.js';
import { ensureWorkerRunning, getWorkerPort } from '../shared/worker-utils.js'; import { ensureWorkerRunning, getWorkerPort } from '../shared/worker-utils.js';
import { HOOK_TIMEOUTS } from '../shared/hook-constants.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 { handleWorkerError } from '../shared/hook-error-handler.js';
import { handleFetchError } from './shared/error-handler.js'; import { handleFetchError } from './shared/error-handler.js';
@@ -54,8 +53,10 @@ async function saveHook(input?: PostToolUseInput): Promise<void> {
tool_name, tool_name,
tool_input, tool_input,
tool_response, tool_response,
cwd: cwd || happy_path_error__with_fallback( cwd: cwd || logger.happyPathError(
'HOOK',
'Missing cwd in PostToolUse hook input', 'Missing cwd in PostToolUse hook input',
undefined,
{ session_id, tool_name }, { 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 { logger } from '../utils/logger.js';
import { ensureWorkerRunning, getWorkerPort } from '../shared/worker-utils.js'; import { ensureWorkerRunning, getWorkerPort } from '../shared/worker-utils.js';
import { HOOK_TIMEOUTS } from '../shared/hook-constants.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 { handleWorkerError } from '../shared/hook-error-handler.js';
import { handleFetchError } from './shared/error-handler.js'; import { handleFetchError } from './shared/error-handler.js';
import { extractLastMessage } from '../shared/transcript-parser.js'; import { extractLastMessage } from '../shared/transcript-parser.js';
@@ -41,8 +40,10 @@ async function summaryHook(input?: StopInput): Promise<void> {
const port = getWorkerPort(); const port = getWorkerPort();
// Extract last user AND assistant messages from transcript // 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', 'Missing transcript_path in Stop hook input',
undefined,
{ session_id }, { session_id },
'' ''
); );
+6 -4
View File
@@ -3,7 +3,7 @@
* Generates prompts for the Claude Agent SDK memory worker * 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 { export interface Observation {
id: number; id: number;
@@ -177,10 +177,12 @@ export function buildObservationPrompt(obs: Observation): string {
* Build prompt to generate progress summary * Build prompt to generate progress summary
*/ */
export function buildSummaryPrompt(session: SDKSession): string { 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', 'Missing last_assistant_message in session for summary prompt',
session, { sessionId: session.id },
session.last_assistant_message || '' undefined,
''
); );
return `PROGRESS SUMMARY CHECKPOINT return `PROGRESS SUMMARY CHECKPOINT
+11 -11
View File
@@ -14,7 +14,7 @@ import {
} from '@modelcontextprotocol/sdk/types.js'; } from '@modelcontextprotocol/sdk/types.js';
import { z } from 'zod'; import { z } from 'zod';
import { zodToJsonSchema } from 'zod-to-json-schema'; 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'; import { getWorkerPort, getWorkerHost } from '../shared/worker-utils.js';
/** /**
@@ -42,7 +42,7 @@ async function callWorkerAPI(
endpoint: string, endpoint: string,
params: Record<string, any> params: Record<string, any>
): Promise<{ content: Array<{ type: 'text'; text: string }>; isError?: boolean }> { ): 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 { try {
const searchParams = new URLSearchParams(); 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 }; 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 // Worker returns { content: [...] } format directly
return data; return data;
} catch (error: any) { } 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 { return {
content: [{ content: [{
type: 'text' as const, type: 'text' as const,
@@ -361,7 +361,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
// Cleanup function // Cleanup function
async function cleanup() { async function cleanup() {
happy_path_error__with_fallback('[mcp-server] Shutting down...'); logger.info('SYSTEM', 'MCP server shutting down');
process.exit(0); process.exit(0);
} }
@@ -374,22 +374,22 @@ async function main() {
// Start the MCP server // Start the MCP server
const transport = new StdioServerTransport(); const transport = new StdioServerTransport();
await server.connect(transport); 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 // Check Worker availability in background
setTimeout(async () => { setTimeout(async () => {
const workerAvailable = await verifyWorkerConnection(); const workerAvailable = await verifyWorkerConnection();
if (!workerAvailable) { if (!workerAvailable) {
happy_path_error__with_fallback('[mcp-server] WARNING: Worker not available at', WORKER_BASE_URL); logger.warn('SYSTEM', 'Worker not available', undefined, { workerUrl: WORKER_BASE_URL });
happy_path_error__with_fallback('[mcp-server] Tools will fail until Worker is started'); logger.warn('SYSTEM', 'Tools will fail until Worker is started');
happy_path_error__with_fallback('[mcp-server] Start Worker with: npm run worker:restart'); logger.warn('SYSTEM', 'Start Worker with: npm run worker:restart');
} else { } 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); }, 0);
} }
main().catch((error) => { main().catch((error) => {
happy_path_error__with_fallback('[mcp-server] Fatal error:', error); logger.error('SYSTEM', 'Fatal error', undefined, error);
process.exit(1); process.exit(1);
}); });
+4 -3
View File
@@ -15,7 +15,6 @@ import { SessionStore } from '../sqlite/SessionStore.js';
import { logger } from '../../utils/logger.js'; import { logger } from '../../utils/logger.js';
import { SettingsDefaultsManager } from '../../shared/SettingsDefaultsManager.js'; import { SettingsDefaultsManager } from '../../shared/SettingsDefaultsManager.js';
import { USER_SETTINGS_PATH } from '../../shared/paths.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 path from 'path';
import os from 'os'; import os from 'os';
@@ -780,9 +779,11 @@ export class ChromaSync {
arguments: arguments_obj arguments: arguments_obj
}); });
const resultText = happy_path_error__with_fallback( const resultText = logger.happyPathError(
'CHROMA',
'Missing text in MCP chroma_query_documents result', 'Missing text in MCP chroma_query_documents result',
{ project: this.project, query_text: query }, { project: this.project },
{ query_text: query },
result.content[0]?.text || '' result.content[0]?.text || ''
); );
-1
View File
@@ -14,7 +14,6 @@ import path from 'path';
import { DatabaseManager } from './DatabaseManager.js'; import { DatabaseManager } from './DatabaseManager.js';
import { SessionManager } from './SessionManager.js'; import { SessionManager } from './SessionManager.js';
import { logger } from '../../utils/logger.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 { parseObservations, parseSummary } from '../../sdk/parser.js';
import { buildInitPrompt, buildObservationPrompt, buildSummaryPrompt, buildContinuationPrompt } from '../../sdk/prompts.js'; import { buildInitPrompt, buildObservationPrompt, buildSummaryPrompt, buildContinuationPrompt } from '../../sdk/prompts.js';
import { SettingsDefaultsManager } from '../../shared/SettingsDefaultsManager.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 { getWorkerPort } from '../../../../shared/worker-utils.js';
import { logger } from '../../../../utils/logger.js'; import { logger } from '../../../../utils/logger.js';
import { stripMemoryTagsFromJson, stripMemoryTagsFromPrompt } from '../../../../utils/tag-stripping.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 { SessionManager } from '../../SessionManager.js';
import { DatabaseManager } from '../../DatabaseManager.js'; import { DatabaseManager } from '../../DatabaseManager.js';
import { SDKAgent } from '../../SDKAgent.js'; import { SDKAgent } from '../../SDKAgent.js';
@@ -342,9 +341,11 @@ export class SessionRoutes extends BaseRouteHandler {
tool_input: cleanedToolInput, tool_input: cleanedToolInput,
tool_response: cleanedToolResponse, tool_response: cleanedToolResponse,
prompt_number: promptNumber, prompt_number: promptNumber,
cwd: cwd || happy_path_error__with_fallback( cwd: cwd || logger.happyPathError(
'SESSION',
'Missing cwd when queueing observation in SessionRoutes', '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 // Queue summarize
this.sessionManager.queueSummarize( this.sessionManager.queueSummarize(
sessionDbId, sessionDbId,
last_user_message || happy_path_error__with_fallback( last_user_message || logger.happyPathError(
'SESSION',
'Missing last_user_message when queueing summary in SessionRoutes', 'Missing last_user_message when queueing summary in SessionRoutes',
{ sessionDbId }, { sessionId: sessionDbId },
undefined,
'' ''
), ),
last_assistant_message last_assistant_message
+61 -18
View File
@@ -13,42 +13,85 @@ export function extractLastMessage(
stripSystemReminders: boolean = false stripSystemReminders: boolean = false
): string { ): string {
if (!transcriptPath || !existsSync(transcriptPath)) { if (!transcriptPath || !existsSync(transcriptPath)) {
logger.happyPathError(
'PARSER',
'Transcript path missing or file does not exist',
undefined,
{ transcriptPath, role },
''
);
return ''; return '';
} }
try { try {
const content = readFileSync(transcriptPath, 'utf-8').trim(); 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'); const lines = content.split('\n');
let foundMatchingRole = false;
for (let i = lines.length - 1; i >= 0; i--) { for (let i = lines.length - 1; i >= 0; i--) {
try { try {
const line = JSON.parse(lines[i]); const line = JSON.parse(lines[i]);
if (line.type === role && line.message?.content) { if (line.type === role) {
let text = ''; foundMatchingRole = true;
const msgContent = line.message.content;
if (typeof msgContent === 'string') { if (line.message?.content) {
text = msgContent; let text = '';
} else if (Array.isArray(msgContent)) { const msgContent = line.message.content;
text = msgContent
.filter((c: any) => c.type === 'text') if (typeof msgContent === 'string') {
.map((c: any) => c.text) text = msgContent;
.join('\n'); } 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 { } catch {
continue; 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) { } catch (error) {
logger.error('HOOK', 'Failed to read transcript', { transcriptPath }, error as 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 { timing(component: Component, message: string, durationMs: number, context?: LogContext): void {
this.info(component, `${message}`, context, { duration: `${durationMs}ms` }); 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 // Export singleton instance
+8 -25
View File
@@ -1,33 +1,16 @@
/** /**
* Happy Path Error With Fallback * 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. * Migration example:
* Check logs with `npm run logs:silent` * 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: * See: src/utils/logger.ts for the new happyPathError method
* Unexpected null/undefined values that should theoretically never happen * Issue: #312 - Consolidate silent logs into regular worker logs
* 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;
*/ */
import { appendFileSync } from 'fs'; import { appendFileSync } from 'fs';
+5 -5
View File
@@ -11,7 +11,7 @@
* This keeps the worker service simple and follows one-way data stream. * 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 * 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 { export function stripMemoryTagsFromJson(content: string): string {
if (typeof content !== '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 return '{}'; // Safe default for JSON context
} }
// ReDoS protection: limit tag count before regex processing // ReDoS protection: limit tag count before regex processing
const tagCount = countTags(content); const tagCount = countTags(content);
if (tagCount > MAX_TAG_COUNT) { 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, tagCount,
maxAllowed: MAX_TAG_COUNT, maxAllowed: MAX_TAG_COUNT,
contentLength: content.length contentLength: content.length
@@ -73,14 +73,14 @@ export function stripMemoryTagsFromJson(content: string): string {
*/ */
export function stripMemoryTagsFromPrompt(content: string): string { export function stripMemoryTagsFromPrompt(content: string): string {
if (typeof content !== '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 return ''; // Safe default for prompt content
} }
// ReDoS protection: limit tag count before regex processing // ReDoS protection: limit tag count before regex processing
const tagCount = countTags(content); const tagCount = countTags(content);
if (tagCount > MAX_TAG_COUNT) { 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, tagCount,
maxAllowed: MAX_TAG_COUNT, maxAllowed: MAX_TAG_COUNT,
contentLength: content.length contentLength: content.length