fix: Chroma connection errors and remove dead last_user_message code (#525)

* fix: distinguish connection errors from collection-not-found in ChromaSync

Previously, ensureCollection() caught ALL errors from chroma_get_collection_info
and assumed they meant "collection doesn't exist". This caused connection errors
like "Not connected" to trigger unnecessary collection creation attempts.

Now connection-related errors are re-thrown immediately instead of being
misinterpreted as missing collections.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: improve error handling for Chroma connection and collection creation

* fix: remove dead last_user_message from summarize flow

The last_user_message field was extracted from transcripts but never used.
In Claude Code transcripts, "user" type messages are mostly tool_results,
not actual user input. The user's original request is already stored in
user_prompts table.

This removes the false warning "Missing last_user_message when queueing
summary" which was complaining about missing data that didn't exist and
wasn't needed.

Changes:
- summary-hook: Only extract last_assistant_message
- SessionRoutes: Remove last_user_message from request body handling
- SessionManager.queueSummarize: Remove lastUserMessage parameter
- PendingMessage interface: Remove last_user_message field
- SDKSession interface: Remove last_user_message field
- All agents: Remove last_user_message from buildSummaryPrompt calls

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* build artifacts for plugin

* Enhance error handling across multiple services

- Improved logging in `BranchManager.ts` to capture recovery checkout failures.
- Updated `PaginationHelper.ts` to log when file paths are plain strings instead of valid JSON.
- Enhanced error logging in `SDKAgent.ts` for Claude executable detection failures.
- Added logging for plain string handling in `SearchManager.ts` for files read and edited.
- Improved logging in `paths.ts` for git root detection failures.
- Enhanced JSON parsing error handling in `timeline-formatting.ts` with previews of failed inputs.
- Updated `transcript-parser.ts` to log summary of parse errors after processing transcript lines.
- Established a baseline for error handling practices in `error-handling-baseline.txt`.
- Documented error handling anti-pattern rules in `CLAUDE.md` to prevent silent failures and improve code quality.

* Add error handling anti-pattern detection script and guidelines

- Introduced `detect-error-handling-antipatterns.ts` to identify common error handling issues in TypeScript code.
- Created comprehensive documentation in `CLAUDE.md` outlining forbidden patterns, allowed patterns, and critical path protection rules.
- Implemented checks for empty catch blocks, logging practices, and try-catch block sizes to prevent silent failures and improve debugging.
- Established a reporting mechanism to summarize detected anti-patterns with severity levels.

* feat: add console filter bar and log line parsing with filtering capabilities

- Introduced a console filter bar with options to filter logs by level and component.
- Implemented parsing of log lines to extract structured data including timestamp, level, component, and correlation ID.
- Added functionality to toggle individual and all levels/components for filtering.
- Enhanced log line rendering with color coding based on log level and special message types.
- Improved responsiveness of the filter bar for smaller screens.

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Alex Newman
2026-01-02 14:45:50 -05:00
committed by GitHub
parent 65d1b52400
commit ad8ac7970d
29 changed files with 1842 additions and 449 deletions
+2 -4
View File
@@ -200,8 +200,7 @@ function extractPriorMessages(transcriptPath: string): { userMessage: string; as
}
}
} catch (parseError) {
// Expected: malformed JSON lines in transcript
// Not logging - this loops through many lines, logging each would be excessive
logger.debug('PARSER', 'Skipping malformed transcript line', { lineIndex: i }, parseError as Error);
continue;
}
}
@@ -229,8 +228,7 @@ export async function generateContext(input?: ContextInput, useColors: boolean =
try {
unlinkSync(VERSION_MARKER_PATH);
} catch (unlinkError) {
// Marker might not exist - expected during first run
// Not logging - this is a normal case during initial setup
logger.debug('SYSTEM', 'Marker file cleanup failed (may not exist)', {}, unlinkError as Error);
}
logger.error('SYSTEM', 'Native module rebuild needed - restart Claude Code to auto-fix');
return '';
+2 -5
View File
@@ -14,7 +14,6 @@ export interface PersistentPendingMessage {
tool_input: string | null;
tool_response: string | null;
cwd: string | null;
last_user_message: string | null;
last_assistant_message: string | null;
prompt_number: number | null;
status: 'pending' | 'processing' | 'processed' | 'failed';
@@ -59,9 +58,9 @@ export class PendingMessageStore {
INSERT INTO pending_messages (
session_db_id, content_session_id, message_type,
tool_name, tool_input, tool_response, cwd,
last_user_message, last_assistant_message,
last_assistant_message,
prompt_number, status, retry_count, created_at_epoch
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending', 0, ?)
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending', 0, ?)
`);
const result = stmt.run(
@@ -72,7 +71,6 @@ export class PendingMessageStore {
message.tool_input ? JSON.stringify(message.tool_input) : null,
message.tool_response ? JSON.stringify(message.tool_response) : null,
message.cwd || null,
message.last_user_message || null,
message.last_assistant_message || null,
message.prompt_number || null,
now
@@ -422,7 +420,6 @@ export class PendingMessageStore {
tool_response: persistent.tool_response ? JSON.parse(persistent.tool_response) : undefined,
prompt_number: persistent.prompt_number || undefined,
cwd: persistent.cwd || undefined,
last_user_message: persistent.last_user_message || undefined,
last_assistant_message: persistent.last_assistant_message || undefined
};
}
+4 -1
View File
@@ -165,7 +165,10 @@ export class ChromaSync {
logger.debug('CHROMA_SYNC', 'Collection exists', { collection: this.collectionName });
} catch (error) {
// Collection doesn't exist, create it
// Log the FULL error - don't try to guess what type it is
logger.warn('CHROMA_SYNC', 'Collection check failed, attempting to create', { collection: this.collectionName }, error as Error);
// Try to create collection - if this also fails, we'll see that error too
logger.info('CHROMA_SYNC', 'Creating collection', { collection: this.collectionName });
try {
+31 -32
View File
@@ -66,8 +66,8 @@ function removePidFile(): void {
try {
if (existsSync(PID_FILE)) unlinkSync(PID_FILE);
} catch (error) {
// PID file removal is cleanup - log but don't fail shutdown
logger.warn('SYSTEM', 'Failed to remove PID file', { path: PID_FILE, error: (error as Error).message });
logger.warn('SYSTEM', 'Failed to remove PID file', { path: PID_FILE }, error as Error);
return; // Non-critical cleanup, OK to fail
}
}
@@ -129,8 +129,8 @@ export async function updateCursorContextForProject(projectName: string, port: n
writeContextFile(entry.workspacePath, context);
logger.debug('CURSOR', 'Updated context file', { projectName, workspacePath: entry.workspacePath });
} catch (error) {
// Context update is non-critical - log and continue
logger.warn('CURSOR', 'Failed to update context file', { projectName, error: (error as Error).message });
logger.warn('CURSOR', 'Failed to update context file', { projectName }, error as Error);
return; // Non-critical context update, OK to fail
}
}
@@ -150,8 +150,7 @@ async function isPortInUse(port: number): Promise<boolean> {
const response = await fetch(`http://127.0.0.1:${port}/api/health`);
return response.ok;
} catch (error) {
// Expected: port is free or service not responding
// Not logging - this is called frequently for health checks
// [ANTI-PATTERN IGNORED]: Health check polls every 500ms, logging would flood
return false;
}
}
@@ -164,10 +163,8 @@ async function waitForHealth(port: number, timeoutMs: number = 30000): Promise<b
const response = await fetch(`http://127.0.0.1:${port}/api/readiness`);
if (response.ok) return true;
} catch (error) {
logger.debug('SYSTEM', 'Service not ready yet, will retry', {
port,
error: error instanceof Error ? error.message : String(error)
});
// [ANTI-PATTERN IGNORED]: Retry loop - expected failures during startup, will retry
logger.debug('SYSTEM', 'Service not ready yet, will retry', { port }, error as Error);
}
await new Promise(r => setTimeout(r, 500));
}
@@ -370,7 +367,7 @@ export class WorkerService {
process.exit(0);
} catch (error) {
logger.error('SYSTEM', 'Error during shutdown', {}, error as Error);
process.exit(1);
process.exit(1); // Exit with error code - this terminates execution
}
};
@@ -459,6 +456,7 @@ export class WorkerService {
}]
});
} catch (error) {
// [POSSIBLY RELEVANT]: API must respond even on error, log full error and return error response
logger.error('WORKER', 'Failed to load instructions', { topic, operation }, error as Error);
res.status(500).json({
content: [{
@@ -543,6 +541,7 @@ export class WorkerService {
// This avoids code duplication and "headers already sent" errors
next();
} catch (error) {
// [POSSIBLY RELEVANT]: API must respond even on error, log full error and return error response
logger.error('WORKER', 'Context inject handler failed', {}, error as Error);
if (!res.headersSent) {
res.status(500).json({ error: error instanceof Error ? error.message : 'Internal server error' });
@@ -621,19 +620,17 @@ export class WorkerService {
try {
execSync(`taskkill /PID ${pid} /T /F`, { timeout: 60000, stdio: 'ignore' });
} catch (error) {
logger.debug('SYSTEM', 'Failed to kill process, may have already exited', {
pid,
error: error instanceof Error ? error.message : String(error)
});
// [ANTI-PATTERN IGNORED]: Cleanup loop - process may have exited, continue to next PID
logger.debug('SYSTEM', 'Failed to kill process, may have already exited', { pid }, error as Error);
}
}
} else {
for (const pid of pids) {
try {
process.kill(pid, 'SIGKILL');
} catch {
// Process already exited - expected during cleanup
logger.debug('SYSTEM', 'Process already exited', { pid });
} catch (error) {
// [ANTI-PATTERN IGNORED]: Cleanup loop - process may have exited, continue to next PID
logger.debug('SYSTEM', 'Process already exited', { pid }, error as Error);
}
}
}
@@ -838,7 +835,7 @@ export class WorkerService {
// Small delay between sessions to avoid rate limiting
await new Promise(resolve => setTimeout(resolve, 100));
} catch (error) {
// Recovery is best-effort - skip failed sessions and continue with others
// [ANTI-PATTERN IGNORED]: Recovery loop - skip failed session, continue to next
logger.warn('SYSTEM', `Failed to process session ${sessionDbId}`, {}, error as Error);
result.sessionsSkipped++;
}
@@ -986,9 +983,9 @@ export class WorkerService {
process.kill(pid, 'SIGKILL');
}
logger.info('SYSTEM', 'Killed process', { pid });
} catch {
// Process may have already exited - continue shutdown
logger.debug('SYSTEM', 'Process already exited during force kill', { pid });
} catch (error) {
// [ANTI-PATTERN IGNORED]: Shutdown cleanup - process already exited, continue
logger.debug('SYSTEM', 'Process already exited during force kill', { pid }, error as Error);
}
}
@@ -1004,8 +1001,7 @@ export class WorkerService {
process.kill(pid, 0);
return true;
} catch (error) {
// Expected: process has exited
// Not logging - this is called in a tight loop during cleanup
// [ANTI-PATTERN IGNORED]: Tight loop checking 100s of PIDs every 100ms during cleanup
return false;
}
});
@@ -1094,8 +1090,9 @@ async function runInteractiveSetup(): Promise<number> {
if (existsSync(settingsPath)) {
try {
settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
} catch {
// Start fresh if corrupt
} catch (error) {
// [ANTI-PATTERN IGNORED]: Fallback behavior - corrupt settings, continue with defaults
logger.debug('SETUP', 'Corrupt settings file, starting fresh', { path: settingsPath }, error as Error);
}
}
@@ -1300,8 +1297,9 @@ async function detectClaudeCode(): Promise<boolean> {
if (stdout.trim()) {
return true;
}
} catch {
// CLI not found
} catch (error) {
// [ANTI-PATTERN IGNORED]: Fallback behavior - CLI not found, continue to directory check
logger.debug('SYSTEM', 'Claude CLI not in PATH', {}, error as Error);
}
// Check for Claude Code plugin directory
@@ -1413,8 +1411,8 @@ function configureCursorMcp(target: string): number {
config.mcpServers = {};
}
} catch (error) {
// Start fresh if corrupt
logger.warn('SYSTEM', 'Corrupt mcp.json, creating new config', { path: mcpJsonPath, error: error instanceof Error ? error.message : String(error) });
// [ANTI-PATTERN IGNORED]: Fallback behavior - corrupt config, continue with empty
logger.warn('SYSTEM', 'Corrupt mcp.json, creating new config', { path: mcpJsonPath }, error as Error);
config = { mcpServers: {} };
}
}
@@ -1669,8 +1667,9 @@ ${context}
}
}
}
} catch {
// Worker not running - that's ok, context will be generated after first session
} catch (error) {
// [ANTI-PATTERN IGNORED]: Fallback behavior - worker not running, use placeholder
logger.debug('CURSOR', 'Worker not running during install', {}, error as Error);
}
if (!contextGenerated) {
-1
View File
@@ -43,7 +43,6 @@ export interface PendingMessage {
tool_response?: any;
prompt_number?: number;
cwd?: string;
last_user_message?: string;
last_assistant_message?: string;
}
+3 -2
View File
@@ -240,8 +240,9 @@ export async function switchBranch(targetBranch: string): Promise<SwitchResult>
if (info.branch && isValidBranchName(info.branch)) {
execGit(['checkout', info.branch]);
}
} catch {
// Recovery failed, user needs manual intervention
} catch (recoveryError) {
// [POSSIBLY RELEVANT]: Recovery checkout failed, user needs manual intervention - already logging main error above
logger.warn('BRANCH', 'Recovery checkout also failed', { originalBranch: info.branch }, recoveryError as Error);
}
return {
-1
View File
@@ -229,7 +229,6 @@ export class GeminiAgent {
memory_session_id: session.memorySessionId,
project: session.project,
user_prompt: session.userPrompt,
last_user_message: message.last_user_message || '',
last_assistant_message: message.last_assistant_message || ''
}, mode);
-1
View File
@@ -188,7 +188,6 @@ export class OpenRouterAgent {
memory_session_id: session.memorySessionId,
project: session.project,
user_prompt: session.userPrompt,
last_user_message: message.last_user_message || '',
last_assistant_message: message.last_assistant_message || ''
}, mode);
+1 -2
View File
@@ -52,8 +52,7 @@ export class PaginationHelper {
// Return as JSON string
return JSON.stringify(strippedPaths);
} catch (err) {
// Expected: file paths may not be valid JSON (plain string)
// Not logging - normal fallback for non-JSON file path strings
logger.debug('WORKER', 'File paths is plain string, using as-is', {}, err as Error);
return filePathsStr;
}
}
+2 -2
View File
@@ -289,7 +289,6 @@ export class SDKAgent {
memory_session_id: session.memorySessionId,
project: session.project,
user_prompt: session.userPrompt,
last_user_message: message.last_user_message || '',
last_assistant_message: message.last_assistant_message || ''
}, mode);
@@ -544,7 +543,8 @@ export class SDKAgent {
if (claudePath) return claudePath;
} catch (error) {
logger.debug('SDK', 'Claude executable auto-detection failed', error);
// [ANTI-PATTERN IGNORED]: Fallback behavior - which/where failed, continue to throw clear error
logger.debug('SDK', 'Claude executable auto-detection failed', {}, error as Error);
}
throw new Error('Claude executable not found. Please either:\n1. Add "claude" to your system PATH, or\n2. Set CLAUDE_CODE_PATH in ~/.claude-mem/settings.json');
+2 -4
View File
@@ -1401,8 +1401,7 @@ export class SearchManager {
lines.push(`**Files Read:** ${filesRead.join(', ')}`);
}
} catch (error) {
// Expected: files_read may not be valid JSON (plain string)
// Not logging - normal fallback for plain text file lists
logger.debug('WORKER', 'files_read is plain string, using as-is', {}, error as Error);
if (summary.files_read.trim()) {
lines.push(`**Files Read:** ${summary.files_read}`);
}
@@ -1417,8 +1416,7 @@ export class SearchManager {
lines.push(`**Files Edited:** ${filesEdited.join(', ')}`);
}
} catch (error) {
// Expected: files_edited may not be valid JSON (plain string)
// Not logging - normal fallback for plain text file lists
logger.debug('WORKER', 'files_edited is plain string, using as-is', {}, error as Error);
if (summary.files_edited.trim()) {
lines.push(`**Files Edited:** ${summary.files_edited}`);
}
+1 -2
View File
@@ -234,7 +234,7 @@ export class SessionManager {
* CRITICAL: Persists to database FIRST before adding to in-memory queue.
* This ensures summarize requests survive worker crashes.
*/
queueSummarize(sessionDbId: number, lastUserMessage: string, lastAssistantMessage?: string): void {
queueSummarize(sessionDbId: number, lastAssistantMessage?: string): void {
// Auto-initialize from database if needed (handles worker restarts)
let session = this.sessions.get(sessionDbId);
if (!session) {
@@ -244,7 +244,6 @@ export class SessionManager {
// CRITICAL: Persist to database FIRST
const message: PendingMessage = {
type: 'summarize',
last_user_message: lastUserMessage,
last_assistant_message: lastAssistantMessage
};
@@ -338,9 +338,9 @@ export class SessionRoutes extends BaseRouteHandler {
const sessionDbId = this.parseIntParam(req, res, 'sessionDbId');
if (sessionDbId === null) return;
const { last_user_message, last_assistant_message } = req.body;
const { last_assistant_message } = req.body;
this.sessionManager.queueSummarize(sessionDbId, last_user_message, last_assistant_message);
this.sessionManager.queueSummarize(sessionDbId, last_assistant_message);
// CRITICAL: Ensure SDK agent is running to consume the queue
this.ensureGeneratorRunning(sessionDbId, 'summarize');
@@ -492,12 +492,12 @@ export class SessionRoutes extends BaseRouteHandler {
/**
* Queue summarize by contentSessionId (summary-hook uses this)
* POST /api/sessions/summarize
* Body: { contentSessionId, last_user_message, last_assistant_message }
* Body: { contentSessionId, last_assistant_message }
*
* Checks privacy, queues summarize request for SDK agent
*/
private handleSummarizeByClaudeId = this.wrapHandler((req: Request, res: Response): void => {
const { contentSessionId, last_user_message, last_assistant_message } = req.body;
const { contentSessionId, last_assistant_message } = req.body;
if (!contentSessionId) {
return this.badRequest(res, 'Missing contentSessionId');
@@ -523,17 +523,7 @@ export class SessionRoutes extends BaseRouteHandler {
}
// Queue summarize
this.sessionManager.queueSummarize(
sessionDbId,
last_user_message || logger.happyPathError(
'SESSION',
'Missing last_user_message when queueing summary in SessionRoutes',
{ sessionId: sessionDbId },
undefined,
''
),
last_assistant_message
);
this.sessionManager.queueSummarize(sessionDbId, last_assistant_message);
// Ensure SDK agent is running
this.ensureGeneratorRunning(sessionDbId, 'summarize');