Merge main into feat/chroma-http-server

Resolve conflicts between Chroma HTTP server PR and main branch changes
(folder CLAUDE.md, exclusion settings, Zscaler SSL, transport cleanup).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Alex Newman
2026-02-13 21:02:54 -05:00
257 changed files with 18546 additions and 5184 deletions
-4
View File
@@ -1,7 +1,3 @@
<claude-mem-context>
# Recent Activity
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
*No recent activity*
</claude-mem-context>
-60
View File
@@ -1,60 +0,0 @@
<claude-mem-context>
# Recent Activity
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
### Jan 13, 2026
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #40137 | 11:48 PM | ⚖️ | User Requests Reversion: Restore Token Creators to Bottom of Page | ~412 |
| #40136 | " | 🟣 | Token Creators Added as Circular Avatars in Token Details Card | ~469 |
| #40135 | 11:47 PM | 🔵 | Token Details Card Still Uses Creator Profile Picture as Fallback | ~397 |
| #40134 | " | 🔄 | Reverted Token Creators From Title Bar Back to Original Ticker Layout | ~418 |
| #40133 | " | 🔵 | User Clarification: Title Bar Refers to Token Details Card Component | ~376 |
| #40132 | " | 🔵 | User Seeking Location Context: What Comes After Header Component | ~370 |
| #40131 | " | 🔵 | Critical Clarification: User Wants Creators in Browser Window Title Bar Area | ~373 |
| #40130 | 11:46 PM | 🔵 | User Clarification: Title Bar Refers to TokenTicker Component, Not Page Header | ~346 |
| #40129 | " | ✅ | Build Verification Passed After Reverting Header Changes | ~368 |
| #40128 | " | 🟣 | Token Creators Relocated to Title Bar Next to Ticker | ~500 |
| #40127 | 11:45 PM | 🔄 | Reverted Token Creator Circles from Header - Removed Title Bar Implementation | ~376 |
| #40125 | 11:44 PM | ✅ | Production Build Successfully Compiled After UI Refactoring | ~338 |
| #40124 | " | 🔄 | Removed Unused User Icon Import from lucide-react | ~328 |
| #40123 | " | 🔄 | Identified Unused User Icon Import After Creator Section Removal | ~311 |
| #40122 | 11:43 PM | 🔄 | Removed Duplicate Token Creators Section from Bottom of Page | ~340 |
| #40121 | " | 🟣 | Token Creators Relocated to Title Bar as Circular Avatars | ~472 |
| #40120 | " | 🔵 | Token Creator Display Structure Located | ~332 |
| #40118 | 11:41 PM | 🔴 | Pause-on-Hover Styling Not Applied | ~326 |
| #40117 | " | ✅ | Ticker Repositioned to Top of Page Outside Container | ~304 |
| #40116 | 11:40 PM | ✅ | Ticker Repositioned to Top of Page | ~326 |
| #40112 | 11:39 PM | 🔴 | Marquee Animations Fixed by Moving Outside Tailwind Theme Block | ~373 |
| #40107 | 11:35 PM | 🟣 | Token Ticker Integrated into Dashboard | ~362 |
| #40106 | " | 🟣 | Token Selection Handler for Ticker Clicks | ~306 |
| #40105 | 11:34 PM | ✅ | TokenTicker Import Added to Homepage | ~145 |
| #40094 | 11:33 PM | 🟣 | Marquee Animation Keyframes Added to Global CSS | ~319 |
| #40092 | " | 🔵 | Tailwind CSS 4 Configuration in Global Styles | ~288 |
| #40082 | 11:32 PM | 🔵 | Existing Bagalytics Token Analytics Dashboard | ~394 |
| #40053 | 11:13 PM | 🔵 | Complete Codebase Exploration for Caching Implementation | ~550 |
| #40051 | 11:12 PM | 🔵 | Main Dashboard Component Architecture | ~503 |
| #40044 | 11:08 PM | ✅ | Production build successfully compiled with UI improvements | ~260 |
| #40043 | 11:07 PM | 🔴 | Replaced simulated fee history with real hourly data from API | ~377 |
| #40042 | " | 🔴 | Removed simulated fee history generator function and state | ~353 |
| #40041 | " | 🔄 | Added HourlyFee interface and hourlyFees field to TokenData for real historical data support | ~297 |
| #40032 | 10:52 PM | 🟣 | Added manual refresh button and last updated timestamp to Fee Projections card | ~335 |
| #40031 | " | 🟣 | Added timestamp tracking for data refresh updates | ~256 |
| #40030 | " | ✅ | Replaced DollarSign icon component with money bag emoji in header logo | ~243 |
| #40029 | " | 🟣 | Added timestamp tracking state for data updates | ~236 |
| #40028 | " | ✅ | Added horizontal padding to token address input field | ~237 |
| #40027 | 10:49 PM | 🟣 | Added "24h Stats" label to trading activity metrics section | ~267 |
| #40026 | 10:48 PM | ✅ | Replaced misleading price volatility alert with factual trading activity metrics | ~390 |
| #40025 | 10:46 PM | 🟣 | Replaced misleading price volatility alert with comprehensive trading activity metrics | ~391 |
| #40024 | " | 🔵 | Identified misleading price volatility alert in UI | ~358 |
| #40021 | 10:39 PM | 🔄 | Restructured Fees Chart Card to Remove CardContent Wrapper | ~361 |
| #40020 | " | 🔄 | Simplified Fees Chart Card by Removing CardHeader and CardTitle Components | ~365 |
| #40019 | 10:38 PM | ✅ | Standardized Fee Projections Card Padding | ~288 |
| #40018 | " | ✅ | Standardized Token Details Card Padding | ~285 |
| #40017 | " | 🔄 | Simplified MetricCard Component by Removing CardHeader and CardContent Wrappers | ~370 |
| #40016 | " | 🔴 | Fixed 24h Fees Card Border and Cleaned Up Unnecessary Classes | ~354 |
| #40015 | " | ✅ | Finalized Lifetime Fees Card with Explicit Border and Unified Hover State | ~368 |
| #40014 | " | 🔴 | Added Missing Border Utility to Token Creators Card | ~321 |
</claude-mem-context>
-187
View File
@@ -1,187 +0,0 @@
<claude-mem-context>
# Recent Activity
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
### Nov 21, 2025
**cleanup-duplicates.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #13458 | 4:05 PM | 🔵 | Comments already document multiple observations per tool_use_id design | ~394 |
**restore-endless-mode.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #13229 | 1:36 PM | 🔵 | Dead Code Analysis: Deferred Transformation Experiment | ~613 |
| #13228 | 1:33 PM | 🔵 | Endless Mode Restoration CLI Tool | ~601 |
### Nov 22, 2025
**restore-endless-mode.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #14203 | 1:05 AM | 🔵 | Endless Mode Feature Branch Contains Major Additions | ~566 |
### Dec 5, 2025
**run.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #20355 | 6:49 PM | 🔵 | Runtime launcher added for dynamic Bun/Node selection | ~260 |
| #20290 | 6:06 PM | 🔵 | Runtime Launcher Script for Dynamic Execution | ~269 |
| #20140 | 4:08 PM | 🔄 | Eliminated code duplication in run.ts by importing from runtime.ts | ~376 |
| #20135 | 4:03 PM | ⚖️ | Proposed Refactoring: 77% Code Reduction While Maintaining Functionality | ~411 |
| #20133 | 4:02 PM | ⚖️ | Runtime Implementation Analysis: Four Categories of Issues Identified | ~406 |
| #20130 | " | ⚖️ | Code Duplication Deemed Unnecessary Due to Bundling | ~359 |
| #20127 | 4:01 PM | 🔵 | Dual Purpose Runtime System Revealed | ~335 |
| #20126 | " | 🔵 | Invalid Justification for Duplication Analysis | ~295 |
| #20125 | " | 🔵 | Code Duplication Issue in Runtime Implementation | ~317 |
| #20123 | " | 🔵 | Source TypeScript Runtime Launcher Implementation | ~313 |
| #20120 | 3:58 PM | 🔵 | PR 169 Changes Overview | ~314 |
| #20110 | 3:55 PM | 🔵 | PR 169 adds Bun runtime support with automatic detection | ~461 |
### Dec 8, 2025
**restore-endless-mode.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #21893 | 3:30 PM | 🔵 | Endless Mode Transcript Restoration CLI Tool | ~345 |
### Dec 9, 2025
**import-xml-observations.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #22929 | 3:04 PM | ⚖️ | Silent Failure Pattern Conversion Strategy | ~471 |
| #22928 | 3:03 PM | 🔵 | Silent Failure Pattern Audit Results | ~372 |
| #22927 | 3:01 PM | 🔵 | Silent Failure Pattern Detection Across Codebase | ~352 |
### Dec 10, 2025
**import-xml-observations.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #23755 | 9:05 PM | 🔵 | Import XML Observations Utility Uses SessionStore Directly | ~280 |
**cleanup-duplicates.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #23754 | 9:05 PM | 🔵 | Cleanup Duplicates Utility Uses SessionStore Directly | ~234 |
### Dec 11, 2025
**import-xml-observations.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #23959 | 1:58 PM | 🔵 | TypeScript Codebase Architecture Mapped | ~337 |
### Dec 13, 2025
**import-xml-observations.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #25321 | 9:12 PM | 🔵 | Console.error Usage Found in 29 Files | ~366 |
### Dec 16, 2025
**import-xml-observations.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #27502 | 4:19 PM | 🔵 | Observation Storage Architecture Located | ~403 |
### Dec 18, 2025
**import-xml-observations.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #29773 | 7:01 PM | 🔵 | Observation Type Definitions Across Codebase | ~362 |
### Dec 19, 2025
**cleanup-duplicates.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #29926 | 6:25 PM | 🔵 | cleanup-duplicates.ts Only Deletes From SQLite, Not Chroma | ~340 |
### Dec 21, 2025
**cleanup-duplicates.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #31603 | 8:21 PM | 🔵 | Complete Console.* Statement Audit Across Codebase | ~813 |
| #31599 | 8:19 PM | 🔵 | 136 console logging statements found in TypeScript source files | ~538 |
### Dec 24, 2025
**import-xml-observations.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #32184 | 7:20 PM | 🔄 | Direct SQL replaced method call for SDK session ID update | ~259 |
| #32183 | " | 🔄 | Simplified database update in XML import script | ~254 |
| #32100 | 5:08 PM | 🔵 | storeObservation method usage spans three TypeScript files | ~237 |
### Dec 25, 2025
**import-xml-observations.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #32558 | 8:18 PM | 🔵 | Identified files containing 'summary' or 'Summary' | ~167 |
| #32456 | 5:41 PM | ✅ | Completed merge of main branch into feature/titans-phase1-3 | ~354 |
### Dec 27, 2025
**import-xml-observations.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #33082 | 6:45 PM | 🔵 | User directory path patterns in codebase | ~362 |
### Dec 28, 2025
**cleanup-duplicates.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #33636 | 11:35 PM | ✅ | Major Documentation and Code Cleanup Removed 4,929 Lines | ~381 |
| #33590 | 11:11 PM | 🔵 | Database Migration Renamed sdk_session_id to memory_session_id | ~387 |
**import-xml-observations.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #33439 | 10:15 PM | 🔄 | Extended Session ID Renaming to Additional Codebase Components | ~352 |
### Dec 29, 2025
**cleanup-duplicates.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #33675 | 12:02 AM | 🔄 | Major Documentation and Code Cleanup in MCP Clarity Branch | ~491 |
### Jan 1, 2026
**import-xml-observations.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #35553 | 9:50 PM | 🔵 | storeObservation method usage across codebase | ~315 |
| #35515 | 9:14 PM | ✅ | Wave 1 Extended Fixes Committed: 5 Critical Error Handling Issues Resolved | ~409 |
| #35501 | 9:10 PM | 🔵 | Wave 1 Verification Issue: Anti-Pattern Detector Not Recognizing Fixes | ~497 |
| #35500 | 9:09 PM | 🟣 | Wave 1 Complete: All 4 Empty Catch Blocks Fixed | ~511 |
| #35493 | 9:08 PM | 🔴 | Wave 1 Fix 1/4: XML Importer Empty Catch Block Fixed | ~392 |
| #35492 | " | ✅ | Wave 1 Fix 1/4: Added Logger Import to XML Importer | ~248 |
| #35488 | 9:07 PM | 🔵 | Wave 1 Target File: XML Observation Importer Structure | ~424 |
| #35485 | 9:06 PM | ⚖️ | Comprehensive error handling remediation plan completed and submitted for approval | ~555 |
| #35465 | 9:01 PM | 🔵 | Empty catch block in XML observations import script | ~281 |
### Jan 2, 2026
**import-xml-observations.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #35985 | 5:16 PM | 🔵 | Alignment logging implemented across session lifecycle touchpoints | ~377 |
### Jan 3, 2026
**import-xml-observations.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #36296 | 8:04 PM | 🔵 | TypeScript Compilation Check: Pre-Existing Errors Unrelated to Refactoring | ~621 |
</claude-mem-context>
-2
View File
@@ -1,8 +1,6 @@
<claude-mem-context>
# Recent Activity
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
### Dec 10, 2025
| ID | Time | T | Title | Read |
-4
View File
@@ -1,7 +1,3 @@
<claude-mem-context>
# Recent Activity
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
*No recent activity*
</claude-mem-context>
+540
View File
@@ -0,0 +1,540 @@
/**
* CLAUDE.md Generation and Cleanup Commands
*
* Shared module for CLAUDE.md file management that can be invoked from:
* - CLI: `claude-mem generate` / `claude-mem clean`
* - Worker service API endpoints
*
* Provides two main operations:
* - generateClaudeMd: Regenerate CLAUDE.md files for folders with observations
* - cleanClaudeMd: Remove auto-generated content from CLAUDE.md files
*/
import { Database } from 'bun:sqlite';
import path from 'path';
import os from 'os';
import {
existsSync,
writeFileSync,
readFileSync,
renameSync,
unlinkSync,
readdirSync
} from 'fs';
import { execSync } from 'child_process';
import { SettingsDefaultsManager } from '../shared/SettingsDefaultsManager.js';
import { formatTime, groupByDate } from '../shared/timeline-formatting.js';
import { isDirectChild } from '../shared/path-utils.js';
import { logger } from '../utils/logger.js';
const DB_PATH = path.join(os.homedir(), '.claude-mem', 'claude-mem.db');
const SETTINGS_PATH = path.join(os.homedir(), '.claude-mem', 'settings.json');
interface ObservationRow {
id: number;
title: string | null;
subtitle: string | null;
narrative: string | null;
facts: string | null;
type: string;
created_at: string;
created_at_epoch: number;
files_modified: string | null;
files_read: string | null;
project: string;
discovery_tokens: number | null;
}
// Type icon map (matches ModeManager)
const TYPE_ICONS: Record<string, string> = {
'bugfix': '🔴',
'feature': '🟣',
'refactor': '🔄',
'change': '✅',
'discovery': '🔵',
'decision': '⚖️',
'session': '🎯',
'prompt': '💬'
};
function getTypeIcon(type: string): string {
return TYPE_ICONS[type] || '📝';
}
function estimateTokens(obs: ObservationRow): number {
const size = (obs.title?.length || 0) +
(obs.subtitle?.length || 0) +
(obs.narrative?.length || 0) +
(obs.facts?.length || 0);
return Math.ceil(size / 4);
}
/**
* Get tracked folders using git ls-files.
* Respects .gitignore and only returns folders within the project.
*/
function getTrackedFolders(workingDir: string): Set<string> {
const folders = new Set<string>();
try {
const output = execSync('git ls-files', {
cwd: workingDir,
encoding: 'utf-8',
maxBuffer: 50 * 1024 * 1024
});
const files = output.trim().split('\n').filter(f => f);
for (const file of files) {
const absPath = path.join(workingDir, file);
let dir = path.dirname(absPath);
while (dir.length > workingDir.length && dir.startsWith(workingDir)) {
folders.add(dir);
dir = path.dirname(dir);
}
}
} catch (error) {
logger.warn('CLAUDE_MD', 'git ls-files failed, falling back to directory walk', { error: String(error) });
walkDirectoriesWithIgnore(workingDir, folders);
}
return folders;
}
/**
* Fallback directory walker that skips common ignored patterns.
*/
function walkDirectoriesWithIgnore(dir: string, folders: Set<string>, depth: number = 0): void {
if (depth > 10) return;
const ignorePatterns = [
'node_modules', '.git', '.next', 'dist', 'build', '.cache',
'__pycache__', '.venv', 'venv', '.idea', '.vscode', 'coverage',
'.claude-mem', '.open-next', '.turbo'
];
try {
const entries = readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
if (!entry.isDirectory()) continue;
if (ignorePatterns.includes(entry.name)) continue;
if (entry.name.startsWith('.') && entry.name !== '.claude') continue;
const fullPath = path.join(dir, entry.name);
folders.add(fullPath);
walkDirectoriesWithIgnore(fullPath, folders, depth + 1);
}
} catch {
// Ignore permission errors
}
}
/**
* Check if an observation has any files that are direct children of the folder.
*/
function hasDirectChildFile(obs: ObservationRow, folderPath: string): boolean {
const checkFiles = (filesJson: string | null): boolean => {
if (!filesJson) return false;
try {
const files = JSON.parse(filesJson);
if (Array.isArray(files)) {
return files.some(f => isDirectChild(f, folderPath));
}
} catch {}
return false;
};
return checkFiles(obs.files_modified) || checkFiles(obs.files_read);
}
/**
* Query observations for a specific folder.
* Only returns observations with files directly in the folder (not in subfolders).
*/
function findObservationsByFolder(db: Database, relativeFolderPath: string, project: string, limit: number): ObservationRow[] {
const queryLimit = limit * 3;
const sql = `
SELECT o.*, o.discovery_tokens
FROM observations o
WHERE o.project = ?
AND (o.files_modified LIKE ? OR o.files_read LIKE ?)
ORDER BY o.created_at_epoch DESC
LIMIT ?
`;
// Database stores paths with forward slashes (git-normalized)
const normalizedFolderPath = relativeFolderPath.split(path.sep).join('/');
const likePattern = `%"${normalizedFolderPath}/%`;
const allMatches = db.prepare(sql).all(project, likePattern, likePattern, queryLimit) as ObservationRow[];
return allMatches.filter(obs => hasDirectChildFile(obs, relativeFolderPath)).slice(0, limit);
}
/**
* Extract relevant file from an observation for display.
* Only returns files that are direct children of the folder.
*/
function extractRelevantFile(obs: ObservationRow, relativeFolder: string): string {
if (obs.files_modified) {
try {
const modified = JSON.parse(obs.files_modified);
if (Array.isArray(modified)) {
for (const file of modified) {
if (isDirectChild(file, relativeFolder)) {
return path.basename(file);
}
}
}
} catch {}
}
if (obs.files_read) {
try {
const read = JSON.parse(obs.files_read);
if (Array.isArray(read)) {
for (const file of read) {
if (isDirectChild(file, relativeFolder)) {
return path.basename(file);
}
}
}
} catch {}
}
return 'General';
}
/**
* Format observations for CLAUDE.md content.
*/
function formatObservationsForClaudeMd(observations: ObservationRow[], folderPath: string): string {
const lines: string[] = [];
lines.push('# Recent Activity');
lines.push('');
lines.push('<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->');
lines.push('');
if (observations.length === 0) {
lines.push('*No recent activity*');
return lines.join('\n');
}
const byDate = groupByDate(observations, obs => obs.created_at);
for (const [day, dayObs] of byDate) {
lines.push(`### ${day}`);
lines.push('');
const byFile = new Map<string, ObservationRow[]>();
for (const obs of dayObs) {
const file = extractRelevantFile(obs, folderPath);
if (!byFile.has(file)) byFile.set(file, []);
byFile.get(file)!.push(obs);
}
for (const [file, fileObs] of byFile) {
lines.push(`**${file}**`);
lines.push('| ID | Time | T | Title | Read |');
lines.push('|----|------|---|-------|------|');
let lastTime = '';
for (const obs of fileObs) {
const time = formatTime(obs.created_at_epoch);
const timeDisplay = time === lastTime ? '"' : time;
lastTime = time;
const icon = getTypeIcon(obs.type);
const title = obs.title || 'Untitled';
const tokens = estimateTokens(obs);
lines.push(`| #${obs.id} | ${timeDisplay} | ${icon} | ${title} | ~${tokens} |`);
}
lines.push('');
}
}
return lines.join('\n').trim();
}
/**
* Write CLAUDE.md file with tagged content preservation.
* Only writes to folders that exist — never creates directories.
*/
function writeClaudeMdToFolder(folderPath: string, newContent: string): void {
const claudeMdPath = path.join(folderPath, 'CLAUDE.md');
const tempFile = `${claudeMdPath}.tmp`;
if (!existsSync(folderPath)) {
throw new Error(`Folder does not exist: ${folderPath}`);
}
let existingContent = '';
if (existsSync(claudeMdPath)) {
existingContent = readFileSync(claudeMdPath, 'utf-8');
}
const startTag = '<claude-mem-context>';
const endTag = '</claude-mem-context>';
let finalContent: string;
if (!existingContent) {
finalContent = `${startTag}\n${newContent}\n${endTag}`;
} else {
const startIdx = existingContent.indexOf(startTag);
const endIdx = existingContent.indexOf(endTag);
if (startIdx !== -1 && endIdx !== -1) {
finalContent = existingContent.substring(0, startIdx) +
`${startTag}\n${newContent}\n${endTag}` +
existingContent.substring(endIdx + endTag.length);
} else {
finalContent = existingContent + `\n\n${startTag}\n${newContent}\n${endTag}`;
}
}
writeFileSync(tempFile, finalContent);
renameSync(tempFile, claudeMdPath);
}
/**
* Regenerate CLAUDE.md for a single folder.
*/
function regenerateFolder(
db: Database,
absoluteFolder: string,
relativeFolder: string,
project: string,
dryRun: boolean,
workingDir: string,
observationLimit: number
): { success: boolean; observationCount: number; error?: string } {
try {
if (!existsSync(absoluteFolder)) {
return { success: false, observationCount: 0, error: 'Folder no longer exists' };
}
// Validate folder is within project root (prevent path traversal)
const resolvedFolder = path.resolve(absoluteFolder);
const resolvedWorkingDir = path.resolve(workingDir);
if (!resolvedFolder.startsWith(resolvedWorkingDir + path.sep)) {
return { success: false, observationCount: 0, error: 'Path escapes project root' };
}
const observations = findObservationsByFolder(db, relativeFolder, project, observationLimit);
if (observations.length === 0) {
return { success: false, observationCount: 0, error: 'No observations for folder' };
}
if (dryRun) {
return { success: true, observationCount: observations.length };
}
const formatted = formatObservationsForClaudeMd(observations, relativeFolder);
writeClaudeMdToFolder(absoluteFolder, formatted);
return { success: true, observationCount: observations.length };
} catch (error) {
return { success: false, observationCount: 0, error: String(error) };
}
}
/**
* Generate CLAUDE.md files for all folders with observations.
*
* @param dryRun - If true, only report what would be done without writing files
* @returns Exit code (0 for success, 1 for error)
*/
export async function generateClaudeMd(dryRun: boolean): Promise<number> {
try {
const workingDir = process.cwd();
const settings = SettingsDefaultsManager.loadFromFile(SETTINGS_PATH);
const observationLimit = parseInt(settings.CLAUDE_MEM_CONTEXT_OBSERVATIONS, 10) || 50;
logger.info('CLAUDE_MD', 'Starting CLAUDE.md generation', {
workingDir,
dryRun,
observationLimit
});
const project = path.basename(workingDir);
const trackedFolders = getTrackedFolders(workingDir);
if (trackedFolders.size === 0) {
logger.info('CLAUDE_MD', 'No folders found in project');
return 0;
}
logger.info('CLAUDE_MD', `Found ${trackedFolders.size} folders in project`);
if (!existsSync(DB_PATH)) {
logger.info('CLAUDE_MD', 'Database not found, no observations to process');
return 0;
}
const db = new Database(DB_PATH, { readonly: true, create: false });
let successCount = 0;
let skipCount = 0;
let errorCount = 0;
const foldersArray = Array.from(trackedFolders).sort();
for (const absoluteFolder of foldersArray) {
const relativeFolder = path.relative(workingDir, absoluteFolder);
const result = regenerateFolder(
db,
absoluteFolder,
relativeFolder,
project,
dryRun,
workingDir,
observationLimit
);
if (result.success) {
logger.debug('CLAUDE_MD', `Processed folder: ${relativeFolder}`, {
observationCount: result.observationCount
});
successCount++;
} else if (result.error?.includes('No observations')) {
skipCount++;
} else {
logger.warn('CLAUDE_MD', `Error processing folder: ${relativeFolder}`, {
error: result.error
});
errorCount++;
}
}
db.close();
logger.info('CLAUDE_MD', 'CLAUDE.md generation complete', {
totalFolders: foldersArray.length,
withObservations: successCount,
noObservations: skipCount,
errors: errorCount,
dryRun
});
return 0;
} catch (error) {
logger.error('CLAUDE_MD', 'Fatal error during CLAUDE.md generation', {
error: String(error)
});
return 1;
}
}
/**
* Clean up auto-generated CLAUDE.md files.
*
* For each file with <claude-mem-context> tags:
* - Strip the tagged section
* - If empty after stripping, delete the file
* - If has remaining content, save the stripped version
*
* @param dryRun - If true, only report what would be done without modifying files
* @returns Exit code (0 for success, 1 for error)
*/
export async function cleanClaudeMd(dryRun: boolean): Promise<number> {
try {
const workingDir = process.cwd();
logger.info('CLAUDE_MD', 'Starting CLAUDE.md cleanup', {
workingDir,
dryRun
});
const filesToProcess: string[] = [];
function walkForClaudeMd(dir: string): void {
const ignorePatterns = [
'node_modules', '.git', '.next', 'dist', 'build', '.cache',
'__pycache__', '.venv', 'venv', '.idea', '.vscode', 'coverage',
'.claude-mem', '.open-next', '.turbo'
];
try {
const entries = readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
if (!ignorePatterns.includes(entry.name)) {
walkForClaudeMd(fullPath);
}
} else if (entry.name === 'CLAUDE.md') {
try {
const content = readFileSync(fullPath, 'utf-8');
if (content.includes('<claude-mem-context>')) {
filesToProcess.push(fullPath);
}
} catch {
// Skip files we can't read
}
}
}
} catch {
// Ignore permission errors
}
}
walkForClaudeMd(workingDir);
if (filesToProcess.length === 0) {
logger.info('CLAUDE_MD', 'No CLAUDE.md files with auto-generated content found');
return 0;
}
logger.info('CLAUDE_MD', `Found ${filesToProcess.length} CLAUDE.md files with auto-generated content`);
let deletedCount = 0;
let cleanedCount = 0;
let errorCount = 0;
for (const file of filesToProcess) {
const relativePath = path.relative(workingDir, file);
try {
const content = readFileSync(file, 'utf-8');
const stripped = content.replace(/<claude-mem-context>[\s\S]*?<\/claude-mem-context>/g, '').trim();
if (stripped === '') {
if (!dryRun) {
unlinkSync(file);
}
logger.debug('CLAUDE_MD', `${dryRun ? '[DRY-RUN] Would delete' : 'Deleted'} (empty): ${relativePath}`);
deletedCount++;
} else {
if (!dryRun) {
writeFileSync(file, stripped);
}
logger.debug('CLAUDE_MD', `${dryRun ? '[DRY-RUN] Would clean' : 'Cleaned'}: ${relativePath}`);
cleanedCount++;
}
} catch (error) {
logger.warn('CLAUDE_MD', `Error processing ${relativePath}`, { error: String(error) });
errorCount++;
}
}
logger.info('CLAUDE_MD', 'CLAUDE.md cleanup complete', {
deleted: deletedCount,
cleaned: cleanedCount,
errors: errorCount,
dryRun
});
return 0;
} catch (error) {
logger.error('CLAUDE_MD', 'Fatal error during CLAUDE.md cleanup', {
error: String(error)
});
return 1;
}
}
-4
View File
@@ -1,7 +1,3 @@
<claude-mem-context>
# Recent Activity
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
*No recent activity*
</claude-mem-context>
+40 -14
View File
@@ -8,11 +8,23 @@
import type { EventHandler, NormalizedHookInput, HookResult } from '../types.js';
import { ensureWorkerRunning, getWorkerPort } from '../../shared/worker-utils.js';
import { getProjectContext } from '../../utils/project-name.js';
import { HOOK_EXIT_CODES } from '../../shared/hook-constants.js';
import { logger } from '../../utils/logger.js';
export const contextHandler: EventHandler = {
async execute(input: NormalizedHookInput): Promise<HookResult> {
// Ensure worker is running before any other logic
await ensureWorkerRunning();
const workerReady = await ensureWorkerRunning();
if (!workerReady) {
// Worker not available - return empty context gracefully
return {
hookSpecificOutput: {
hookEventName: 'SessionStart',
additionalContext: ''
},
exitCode: HOOK_EXIT_CODES.SUCCESS
};
}
const cwd = input.cwd ?? process.cwd();
const context = getProjectContext(cwd);
@@ -24,20 +36,34 @@ export const contextHandler: EventHandler = {
// Note: Removed AbortSignal.timeout due to Windows Bun cleanup issue (libuv assertion)
// Worker service has its own timeouts, so client-side timeout is redundant
const response = await fetch(url);
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Context generation failed: ${response.status}`);
}
const result = await response.text();
const additionalContext = result.trim();
return {
hookSpecificOutput: {
hookEventName: 'SessionStart',
additionalContext
if (!response.ok) {
// Log but don't throw — context fetch failure should not block session start
logger.warn('HOOK', 'Context generation failed, returning empty', { status: response.status });
return {
hookSpecificOutput: { hookEventName: 'SessionStart', additionalContext: '' },
exitCode: HOOK_EXIT_CODES.SUCCESS
};
}
};
const result = await response.text();
const additionalContext = result.trim();
return {
hookSpecificOutput: {
hookEventName: 'SessionStart',
additionalContext
}
};
} catch (error) {
// Worker unreachable — return empty context gracefully
logger.warn('HOOK', 'Context fetch error, returning empty', { error: error instanceof Error ? error.message : String(error) });
return {
hookSpecificOutput: { hookEventName: 'SessionStart', additionalContext: '' },
exitCode: HOOK_EXIT_CODES.SUCCESS
};
}
}
};
+30 -17
View File
@@ -8,11 +8,16 @@
import type { EventHandler, NormalizedHookInput, HookResult } from '../types.js';
import { ensureWorkerRunning, getWorkerPort } from '../../shared/worker-utils.js';
import { logger } from '../../utils/logger.js';
import { HOOK_EXIT_CODES } from '../../shared/hook-constants.js';
export const fileEditHandler: EventHandler = {
async execute(input: NormalizedHookInput): Promise<HookResult> {
// Ensure worker is running before any other logic
await ensureWorkerRunning();
const workerReady = await ensureWorkerRunning();
if (!workerReady) {
// Worker not available - skip file edit observation gracefully
return { continue: true, suppressOutput: true, exitCode: HOOK_EXIT_CODES.SUCCESS };
}
const { sessionId, cwd, filePath, edits } = input;
@@ -34,25 +39,33 @@ export const fileEditHandler: EventHandler = {
// Send to worker as an observation with file edit metadata
// The observation handler on the worker will process this appropriately
const response = await fetch(`http://127.0.0.1:${port}/api/sessions/observations`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
contentSessionId: sessionId,
tool_name: 'write_file',
tool_input: { filePath, edits },
tool_response: { success: true },
cwd
})
// Note: Removed signal to avoid Windows Bun cleanup issue (libuv assertion)
});
try {
const response = await fetch(`http://127.0.0.1:${port}/api/sessions/observations`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
contentSessionId: sessionId,
tool_name: 'write_file',
tool_input: { filePath, edits },
tool_response: { success: true },
cwd
})
// Note: Removed signal to avoid Windows Bun cleanup issue (libuv assertion)
});
if (!response.ok) {
throw new Error(`File edit observation storage failed: ${response.status}`);
if (!response.ok) {
// Log but don't throw — file edit observation failure should not block editing
logger.warn('HOOK', 'File edit observation storage failed, skipping', { status: response.status, filePath });
return { continue: true, suppressOutput: true, exitCode: HOOK_EXIT_CODES.SUCCESS };
}
logger.debug('HOOK', 'File edit observation sent successfully', { filePath });
} catch (error) {
// Worker unreachable — skip file edit observation gracefully
logger.warn('HOOK', 'File edit observation fetch error, skipping', { error: error instanceof Error ? error.message : String(error) });
return { continue: true, suppressOutput: true, exitCode: HOOK_EXIT_CODES.SUCCESS };
}
logger.debug('HOOK', 'File edit observation sent successfully', { filePath });
return { continue: true, suppressOutput: true };
}
};
+24 -11
View File
@@ -5,26 +5,30 @@
*/
import type { EventHandler } from '../types.js';
import { HOOK_EXIT_CODES } from '../../shared/hook-constants.js';
import { contextHandler } from './context.js';
import { sessionInitHandler } from './session-init.js';
import { observationHandler } from './observation.js';
import { summarizeHandler } from './summarize.js';
import { userMessageHandler } from './user-message.js';
import { fileEditHandler } from './file-edit.js';
import { sessionCompleteHandler } from './session-complete.js';
export type EventType =
| 'context' // SessionStart - inject context
| 'session-init' // UserPromptSubmit - initialize session
| 'observation' // PostToolUse - save observation
| 'summarize' // Stop - generate summary
| 'user-message' // SessionStart (parallel) - display to user
| 'file-edit'; // Cursor afterFileEdit
| 'context' // SessionStart - inject context
| 'session-init' // UserPromptSubmit - initialize session
| 'observation' // PostToolUse - save observation
| 'summarize' // Stop - generate summary (phase 1)
| 'session-complete' // Stop - complete session (phase 2) - fixes #842
| 'user-message' // SessionStart (parallel) - display to user
| 'file-edit'; // Cursor afterFileEdit
const handlers: Record<EventType, EventHandler> = {
'context': contextHandler,
'session-init': sessionInitHandler,
'observation': observationHandler,
'summarize': summarizeHandler,
'session-complete': sessionCompleteHandler,
'user-message': userMessageHandler,
'file-edit': fileEditHandler
};
@@ -32,14 +36,22 @@ const handlers: Record<EventType, EventHandler> = {
/**
* Get the event handler for a given event type.
*
* Returns a no-op handler for unknown event types instead of throwing (fix #984).
* Claude Code may send new event types that the plugin doesn't handle yet —
* throwing would surface as a BLOCKING_ERROR to the user.
*
* @param eventType The type of event to handle
* @returns The appropriate EventHandler
* @throws Error if event type is not recognized
* @returns The appropriate EventHandler, or a no-op handler for unknown types
*/
export function getEventHandler(eventType: EventType): EventHandler {
const handler = handlers[eventType];
export function getEventHandler(eventType: string): EventHandler {
const handler = handlers[eventType as EventType];
if (!handler) {
throw new Error(`Unknown event type: ${eventType}`);
console.error(`[claude-mem] Unknown event type: ${eventType}, returning no-op`);
return {
async execute() {
return { continue: true, suppressOutput: true, exitCode: HOOK_EXIT_CODES.SUCCESS };
}
};
}
return handler;
}
@@ -51,3 +63,4 @@ export { observationHandler } from './observation.js';
export { summarizeHandler } from './summarize.js';
export { userMessageHandler } from './user-message.js';
export { fileEditHandler } from './file-edit.js';
export { sessionCompleteHandler } from './session-complete.js';
+41 -18
View File
@@ -7,11 +7,19 @@
import type { EventHandler, NormalizedHookInput, HookResult } from '../types.js';
import { ensureWorkerRunning, getWorkerPort } from '../../shared/worker-utils.js';
import { logger } from '../../utils/logger.js';
import { HOOK_EXIT_CODES } from '../../shared/hook-constants.js';
import { isProjectExcluded } from '../../utils/project-filter.js';
import { SettingsDefaultsManager } from '../../shared/SettingsDefaultsManager.js';
import { USER_SETTINGS_PATH } from '../../shared/paths.js';
export const observationHandler: EventHandler = {
async execute(input: NormalizedHookInput): Promise<HookResult> {
// Ensure worker is running before any other logic
await ensureWorkerRunning();
const workerReady = await ensureWorkerRunning();
if (!workerReady) {
// Worker not available - skip observation gracefully
return { continue: true, suppressOutput: true, exitCode: HOOK_EXIT_CODES.SUCCESS };
}
const { sessionId, cwd, toolName, toolInput, toolResponse } = input;
@@ -32,25 +40,40 @@ export const observationHandler: EventHandler = {
throw new Error(`Missing cwd in PostToolUse hook input for session ${sessionId}, tool ${toolName}`);
}
// Send to worker - worker handles privacy check and database operations
const response = await fetch(`http://127.0.0.1:${port}/api/sessions/observations`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
contentSessionId: sessionId,
tool_name: toolName,
tool_input: toolInput,
tool_response: toolResponse,
cwd
})
// Note: Removed signal to avoid Windows Bun cleanup issue (libuv assertion)
});
if (!response.ok) {
throw new Error(`Observation storage failed: ${response.status}`);
// Check if project is excluded from tracking
const settings = SettingsDefaultsManager.loadFromFile(USER_SETTINGS_PATH);
if (isProjectExcluded(cwd, settings.CLAUDE_MEM_EXCLUDED_PROJECTS)) {
logger.debug('HOOK', 'Project excluded from tracking, skipping observation', { cwd, toolName });
return { continue: true, suppressOutput: true };
}
logger.debug('HOOK', 'Observation sent successfully', { toolName });
// Send to worker - worker handles privacy check and database operations
try {
const response = await fetch(`http://127.0.0.1:${port}/api/sessions/observations`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
contentSessionId: sessionId,
tool_name: toolName,
tool_input: toolInput,
tool_response: toolResponse,
cwd
})
// Note: Removed signal to avoid Windows Bun cleanup issue (libuv assertion)
});
if (!response.ok) {
// Log but don't throw — observation storage failure should not block tool use
logger.warn('HOOK', 'Observation storage failed, skipping', { status: response.status, toolName });
return { continue: true, suppressOutput: true, exitCode: HOOK_EXIT_CODES.SUCCESS };
}
logger.debug('HOOK', 'Observation sent successfully', { toolName });
} catch (error) {
// Worker unreachable — skip observation gracefully
logger.warn('HOOK', 'Observation fetch error, skipping', { error: error instanceof Error ? error.message : String(error) });
return { continue: true, suppressOutput: true, exitCode: HOOK_EXIT_CODES.SUCCESS };
}
return { continue: true, suppressOutput: true };
}
+66
View File
@@ -0,0 +1,66 @@
/**
* Session Complete Handler - Stop (Phase 2)
*
* Completes the session after summarize has been queued.
* This removes the session from the active sessions map, allowing
* the orphan reaper to clean up any remaining subprocess.
*
* Fixes Issue #842: Orphan reaper starts but never reaps because
* sessions stay in the active sessions map forever.
*/
import type { EventHandler, NormalizedHookInput, HookResult } from '../types.js';
import { ensureWorkerRunning, getWorkerPort } from '../../shared/worker-utils.js';
import { logger } from '../../utils/logger.js';
export const sessionCompleteHandler: EventHandler = {
async execute(input: NormalizedHookInput): Promise<HookResult> {
// Ensure worker is running
const workerReady = await ensureWorkerRunning();
if (!workerReady) {
// Worker not available — skip session completion gracefully
return { continue: true, suppressOutput: true };
}
const { sessionId } = input;
const port = getWorkerPort();
if (!sessionId) {
logger.warn('HOOK', 'session-complete: Missing sessionId, skipping');
return { continue: true, suppressOutput: true };
}
logger.info('HOOK', '→ session-complete: Removing session from active map', {
workerPort: port,
contentSessionId: sessionId
});
try {
// Call the session complete endpoint by contentSessionId
const response = await fetch(`http://127.0.0.1:${port}/api/sessions/complete`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
contentSessionId: sessionId
})
});
if (!response.ok) {
const text = await response.text();
logger.warn('HOOK', 'session-complete: Failed to complete session', {
status: response.status,
body: text
});
} else {
logger.info('HOOK', 'Session completed successfully', { contentSessionId: sessionId });
}
} catch (error) {
// Log but don't fail - session may already be gone
logger.warn('HOOK', 'session-complete: Error completing session', {
error: (error as Error).message
});
}
return { continue: true, suppressOutput: true };
}
};
+26 -8
View File
@@ -8,18 +8,33 @@ import type { EventHandler, NormalizedHookInput, HookResult } from '../types.js'
import { ensureWorkerRunning, getWorkerPort } from '../../shared/worker-utils.js';
import { getProjectName } from '../../utils/project-name.js';
import { logger } from '../../utils/logger.js';
import { HOOK_EXIT_CODES } from '../../shared/hook-constants.js';
import { isProjectExcluded } from '../../utils/project-filter.js';
import { SettingsDefaultsManager } from '../../shared/SettingsDefaultsManager.js';
import { USER_SETTINGS_PATH } from '../../shared/paths.js';
export const sessionInitHandler: EventHandler = {
async execute(input: NormalizedHookInput): Promise<HookResult> {
// Ensure worker is running before any other logic
await ensureWorkerRunning();
const { sessionId, cwd, prompt } = input;
if (!prompt) {
throw new Error('sessionInitHandler requires prompt');
const workerReady = await ensureWorkerRunning();
if (!workerReady) {
// Worker not available - skip session init gracefully
return { continue: true, suppressOutput: true, exitCode: HOOK_EXIT_CODES.SUCCESS };
}
const { sessionId, cwd, prompt: rawPrompt } = input;
// Check if project is excluded from tracking
const settings = SettingsDefaultsManager.loadFromFile(USER_SETTINGS_PATH);
if (cwd && isProjectExcluded(cwd, settings.CLAUDE_MEM_EXCLUDED_PROJECTS)) {
logger.info('HOOK', 'Project excluded from tracking', { cwd });
return { continue: true, suppressOutput: true };
}
// Handle image-only prompts (where text prompt is empty/undefined)
// Use placeholder so sessions still get created and tracked for memory
const prompt = (!rawPrompt || !rawPrompt.trim()) ? '[media prompt]' : rawPrompt;
const project = getProjectName(cwd);
const port = getWorkerPort();
@@ -38,7 +53,9 @@ export const sessionInitHandler: EventHandler = {
});
if (!initResponse.ok) {
throw new Error(`Session initialization failed: ${initResponse.status}`);
// Log but don't throw - a worker 500 should not block the user's prompt
logger.failure('HOOK', `Session initialization failed: ${initResponse.status}`, { contentSessionId: sessionId, project });
return { continue: true, suppressOutput: true, exitCode: HOOK_EXIT_CODES.SUCCESS };
}
const initResult = await initResponse.json() as {
@@ -81,7 +98,8 @@ export const sessionInitHandler: EventHandler = {
});
if (!response.ok) {
throw new Error(`SDK agent start failed: ${response.status}`);
// Log but don't throw - SDK agent failure should not block the user's prompt
logger.failure('HOOK', `SDK agent start failed: ${response.status}`, { sessionDbId, promptNumber });
}
} else if (input.platform === 'cursor') {
logger.debug('HOOK', 'session-init: Skipping SDK agent init for Cursor platform', { sessionDbId, promptNumber });
+21 -11
View File
@@ -7,14 +7,21 @@
*/
import type { EventHandler, NormalizedHookInput, HookResult } from '../types.js';
import { ensureWorkerRunning, getWorkerPort } from '../../shared/worker-utils.js';
import { ensureWorkerRunning, getWorkerPort, fetchWithTimeout } from '../../shared/worker-utils.js';
import { logger } from '../../utils/logger.js';
import { extractLastMessage } from '../../shared/transcript-parser.js';
import { HOOK_EXIT_CODES, HOOK_TIMEOUTS, getTimeout } from '../../shared/hook-constants.js';
const SUMMARIZE_TIMEOUT_MS = getTimeout(HOOK_TIMEOUTS.DEFAULT);
export const summarizeHandler: EventHandler = {
async execute(input: NormalizedHookInput): Promise<HookResult> {
// Ensure worker is running before any other logic
await ensureWorkerRunning();
const workerReady = await ensureWorkerRunning();
if (!workerReady) {
// Worker not available - skip summary gracefully
return { continue: true, suppressOutput: true, exitCode: HOOK_EXIT_CODES.SUCCESS };
}
const { sessionId, transcriptPath } = input;
@@ -36,15 +43,18 @@ export const summarizeHandler: EventHandler = {
});
// Send to worker - worker handles privacy check and database operations
const response = await fetch(`http://127.0.0.1:${port}/api/sessions/summarize`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
contentSessionId: sessionId,
last_assistant_message: lastAssistantMessage
})
// Note: Removed signal to avoid Windows Bun cleanup issue (libuv assertion)
});
const response = await fetchWithTimeout(
`http://127.0.0.1:${port}/api/sessions/summarize`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
contentSessionId: sessionId,
last_assistant_message: lastAssistantMessage
}),
},
SUMMARIZE_TIMEOUT_MS
);
if (!response.ok) {
// Return standard response even on failure (matches original behavior)
+34 -22
View File
@@ -1,8 +1,8 @@
/**
* User Message Handler - SessionStart (parallel)
*
* Extracted from user-message-hook.ts - displays context info to user via stderr.
* Uses exit code 3 to show user message without injecting into Claude's context.
* Displays context info to user via stderr.
* Uses exit code 0 (SUCCESS) - stderr is not shown to Claude with exit 0.
*/
import { basename } from 'path';
@@ -13,34 +13,46 @@ import { HOOK_EXIT_CODES } from '../../shared/hook-constants.js';
export const userMessageHandler: EventHandler = {
async execute(input: NormalizedHookInput): Promise<HookResult> {
// Ensure worker is running
await ensureWorkerRunning();
const workerReady = await ensureWorkerRunning();
if (!workerReady) {
// Worker not available — skip user message gracefully
return { exitCode: HOOK_EXIT_CODES.SUCCESS };
}
const port = getWorkerPort();
const project = basename(input.cwd ?? process.cwd());
// Fetch formatted context directly from worker API
// Note: Removed AbortSignal.timeout to avoid Windows Bun cleanup issue (libuv assertion)
const response = await fetch(
`http://127.0.0.1:${port}/api/context/inject?project=${encodeURIComponent(project)}&colors=true`,
{ method: 'GET' }
);
try {
const response = await fetch(
`http://127.0.0.1:${port}/api/context/inject?project=${encodeURIComponent(project)}&colors=true`,
{ method: 'GET' }
);
if (!response.ok) {
throw new Error(`Failed to fetch context: ${response.status}`);
if (!response.ok) {
// Don't throw - context fetch failure should not block the user's prompt
return { exitCode: HOOK_EXIT_CODES.SUCCESS };
}
const output = await response.text();
// Write to stderr for user visibility
// Note: Using process.stderr.write instead of console.error to avoid
// Claude Code treating this as a hook error. The actual hook output
// goes to stdout via hook-command.ts JSON serialization.
process.stderr.write(
"\n\n" + String.fromCodePoint(0x1F4DD) + " Claude-Mem Context Loaded\n\n" +
output +
"\n\n" + String.fromCodePoint(0x1F4A1) + " Wrap any message with <private> ... </private> to prevent storing sensitive information.\n" +
"\n" + String.fromCodePoint(0x1F4AC) + " Community https://discord.gg/J4wttp9vDu" +
`\n` + String.fromCodePoint(0x1F4FA) + ` Watch live in browser http://localhost:${port}/\n`
);
} catch (error) {
// Worker unreachable — skip user message gracefully
// User message context error is non-critical — skip gracefully
}
const output = await response.text();
// Write to stderr for user visibility (Claude Code UI shows stderr)
console.error(
"\n\n" + String.fromCodePoint(0x1F4DD) + " Claude-Mem Context Loaded\n" +
" " + String.fromCodePoint(0x2139, 0xFE0F) + " Note: This appears as stderr but is informational only\n\n" +
output +
"\n\n" + String.fromCodePoint(0x1F4A1) + " New! Wrap all or part of any message with <private> ... </private> to prevent storing sensitive information in your observation history.\n" +
"\n" + String.fromCodePoint(0x1F4AC) + " Community https://discord.gg/J4wttp9vDu" +
`\n` + String.fromCodePoint(0x1F4FA) + ` Watch live in browser http://localhost:${port}/\n`
);
return { exitCode: HOOK_EXIT_CODES.USER_MESSAGE_ONLY };
return { exitCode: HOOK_EXIT_CODES.SUCCESS };
}
};
+81 -5
View File
@@ -3,7 +3,68 @@ import { getPlatformAdapter } from './adapters/index.js';
import { getEventHandler } from './handlers/index.js';
import { HOOK_EXIT_CODES } from '../shared/hook-constants.js';
export async function hookCommand(platform: string, event: string): Promise<void> {
export interface HookCommandOptions {
/** If true, don't call process.exit() - let caller handle process lifecycle */
skipExit?: boolean;
}
/**
* Classify whether an error indicates the worker is unavailable (graceful degradation)
* vs a handler/client bug (blocking error that developers need to see).
*
* Exit 0 (graceful degradation):
* - Transport failures: ECONNREFUSED, ECONNRESET, EPIPE, ETIMEDOUT, fetch failed
* - Timeout errors: timed out, timeout
* - Server errors: HTTP 5xx status codes
*
* Exit 2 (blocking error — handler/client bug):
* - HTTP 4xx status codes (bad request, not found, validation error)
* - Programming errors (TypeError, ReferenceError, SyntaxError)
* - All other unexpected errors
*/
export function isWorkerUnavailableError(error: unknown): boolean {
const message = error instanceof Error ? error.message : String(error);
const lower = message.toLowerCase();
// Transport failures — worker unreachable
const transportPatterns = [
'econnrefused',
'econnreset',
'epipe',
'etimedout',
'enotfound',
'econnaborted',
'enetunreach',
'ehostunreach',
'fetch failed',
'unable to connect',
'socket hang up',
];
if (transportPatterns.some(p => lower.includes(p))) return true;
// Timeout errors — worker didn't respond in time
if (lower.includes('timed out') || lower.includes('timeout')) return true;
// HTTP 5xx server errors — worker has internal problems
if (/failed:\s*5\d{2}/.test(message) || /status[:\s]+5\d{2}/.test(message)) return true;
// HTTP 429 (rate limit) — treat as transient unavailability, not a bug
if (/failed:\s*429/.test(message) || /status[:\s]+429/.test(message)) return true;
// HTTP 4xx client errors — our bug, NOT worker unavailability
if (/failed:\s*4\d{2}/.test(message) || /status[:\s]+4\d{2}/.test(message)) return false;
// Programming errors — code bugs, not worker unavailability
// Note: TypeError('fetch failed') already handled by transport patterns above
if (error instanceof TypeError || error instanceof ReferenceError || error instanceof SyntaxError) {
return false;
}
// Default: treat unknown errors as blocking (conservative — surface bugs)
return false;
}
export async function hookCommand(platform: string, event: string, options: HookCommandOptions = {}): Promise<number> {
try {
const adapter = getPlatformAdapter(platform);
const handler = getEventHandler(event);
@@ -15,11 +76,26 @@ export async function hookCommand(platform: string, event: string): Promise<void
const output = adapter.formatOutput(result);
console.log(JSON.stringify(output));
process.exit(result.exitCode ?? HOOK_EXIT_CODES.SUCCESS);
const exitCode = result.exitCode ?? HOOK_EXIT_CODES.SUCCESS;
if (!options.skipExit) {
process.exit(exitCode);
}
return exitCode;
} catch (error) {
if (isWorkerUnavailableError(error)) {
// Worker unavailable — degrade gracefully, don't block the user
console.error(`[claude-mem] Worker unavailable, skipping hook: ${error instanceof Error ? error.message : error}`);
if (!options.skipExit) {
process.exit(HOOK_EXIT_CODES.SUCCESS); // = 0 (graceful)
}
return HOOK_EXIT_CODES.SUCCESS;
}
// Handler/client bug — show as blocking error so developers see it
console.error(`Hook error: ${error}`);
// Use exit code 2 (blocking error) so users see the error message
// Exit code 1 only shows in verbose mode per Claude Code docs
process.exit(HOOK_EXIT_CODES.BLOCKING_ERROR); // = 2
if (!options.skipExit) {
process.exit(HOOK_EXIT_CODES.BLOCKING_ERROR); // = 2
}
return HOOK_EXIT_CODES.BLOCKING_ERROR;
}
}
+170 -8
View File
@@ -1,16 +1,178 @@
// Stdin reading utility extracted from hook patterns
// See src/hooks/save-hook.ts for the original pattern
// Stdin reading utility for Claude Code hooks
//
// Problem: Claude Code doesn't close stdin after writing hook input,
// so stdin.on('end') never fires and hooks hang indefinitely (#727).
//
// Solution: JSON is self-delimiting. We detect complete JSON by attempting
// to parse after each chunk. Once we have valid JSON, we resolve immediately
// without waiting for EOF. This is the proper fix, not a timeout workaround.
/**
* Check if stdin is available and readable.
*
* Bun has a bug where accessing process.stdin can crash with EINVAL
* if Claude Code doesn't provide a valid stdin file descriptor (#646).
* This function safely checks if stdin is usable.
*/
function isStdinAvailable(): boolean {
try {
const stdin = process.stdin;
// If stdin is a TTY, we're running interactively (not from Claude Code hook)
if (stdin.isTTY) {
return false;
}
// Accessing stdin.readable triggers Bun's lazy initialization.
// If we get here without throwing, stdin is available.
// Note: We don't check the value since Node/Bun don't reliably set it to false.
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
stdin.readable;
return true;
} catch {
// Bun crashed trying to access stdin (EINVAL from fstat)
// This is expected when Claude Code doesn't provide valid stdin
return false;
}
}
/**
* Try to parse the accumulated input as JSON.
* Returns the parsed value if successful, undefined if incomplete/invalid.
*/
function tryParseJson(input: string): { success: true; value: unknown } | { success: false } {
const trimmed = input.trim();
if (!trimmed) {
return { success: false };
}
try {
const value = JSON.parse(trimmed);
return { success: true, value };
} catch {
// JSON is incomplete or invalid
return { success: false };
}
}
// Safety timeout - only kicks in if JSON never completes (malformed input).
// This should rarely/never be hit in normal operation since we detect complete JSON.
const SAFETY_TIMEOUT_MS = 30000;
// Short delay after last data chunk to try parsing
// This handles the case where JSON arrives in multiple chunks
const PARSE_DELAY_MS = 50;
export async function readJsonFromStdin(): Promise<unknown> {
// First, check if stdin is even available
// This catches the Bun EINVAL crash from issue #646
if (!isStdinAvailable()) {
return undefined;
}
return new Promise((resolve, reject) => {
let input = '';
process.stdin.on('data', (chunk) => input += chunk);
process.stdin.on('end', () => {
let resolved = false;
let parseDelayId: ReturnType<typeof setTimeout> | null = null;
const cleanup = () => {
try {
resolve(input.trim() ? JSON.parse(input) : undefined);
} catch (e) {
reject(new Error(`Failed to parse hook input: ${e}`));
process.stdin.removeAllListeners('data');
process.stdin.removeAllListeners('end');
process.stdin.removeAllListeners('error');
} catch {
// Ignore cleanup errors
}
});
};
const resolveWith = (value: unknown) => {
if (resolved) return;
resolved = true;
if (parseDelayId) clearTimeout(parseDelayId);
clearTimeout(safetyTimeoutId);
cleanup();
resolve(value);
};
const rejectWith = (error: Error) => {
if (resolved) return;
resolved = true;
if (parseDelayId) clearTimeout(parseDelayId);
clearTimeout(safetyTimeoutId);
cleanup();
reject(error);
};
const tryResolveWithJson = () => {
const result = tryParseJson(input);
if (result.success) {
resolveWith(result.value);
return true;
}
return false;
};
// Safety timeout - fallback if JSON never completes
const safetyTimeoutId = setTimeout(() => {
if (!resolved) {
// Try one final parse attempt
if (!tryResolveWithJson()) {
// If we have data but it's not valid JSON, that's an error
if (input.trim()) {
rejectWith(new Error(`Incomplete JSON after ${SAFETY_TIMEOUT_MS}ms: ${input.slice(0, 100)}...`));
} else {
// No data received - resolve with undefined
resolveWith(undefined);
}
}
}
}, SAFETY_TIMEOUT_MS);
try {
process.stdin.on('data', (chunk) => {
input += chunk;
// Clear any pending parse delay
if (parseDelayId) {
clearTimeout(parseDelayId);
parseDelayId = null;
}
// Try to parse immediately - if JSON is complete, resolve now
if (tryResolveWithJson()) {
return;
}
// If immediate parse failed, set a short delay and try again
// This handles multi-chunk delivery where the last chunk completes the JSON
parseDelayId = setTimeout(() => {
tryResolveWithJson();
}, PARSE_DELAY_MS);
});
process.stdin.on('end', () => {
// stdin closed - parse whatever we have
if (!resolved) {
if (!tryResolveWithJson()) {
// Empty or invalid - resolve with undefined
resolveWith(input.trim() ? undefined : undefined);
}
}
});
process.stdin.on('error', () => {
if (!resolved) {
// Don't reject on stdin errors - just return undefined
// This is more graceful for hook execution
resolveWith(undefined);
}
});
} catch {
// If attaching listeners fails (Bun stdin issue), resolve with undefined
resolved = true;
clearTimeout(safetyTimeoutId);
cleanup();
resolve(undefined);
}
});
}
-71
View File
@@ -1,71 +0,0 @@
<claude-mem-context>
# Recent Activity
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
### Nov 25, 2025
**api.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #15545 | 8:37 PM | 🔵 | API Constants File Contains Single Comment Reference | ~227 |
### Dec 7, 2025
**observation-metadata.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #21685 | 9:48 PM | 🔵 | Configuration Defaults and Environment Variables | ~558 |
### Dec 9, 2025
**observation-metadata.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #22672 | 12:10 PM | 🔵 | Observation Type System with Six Types and Seven Concepts | ~505 |
### Dec 11, 2025
**observation-metadata.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #23959 | 1:58 PM | 🔵 | TypeScript Codebase Architecture Mapped | ~337 |
### Dec 18, 2025
**observation-metadata.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #29773 | 7:01 PM | 🔵 | Observation Type Definitions Across Codebase | ~362 |
| #29248 | 12:15 AM | ⚖️ | RAGTIME domain-agnostic architecture design for claude-mem | ~590 |
| #29229 | 12:08 AM | 🔵 | Claude-Mem Observation Type System Architecture Mapped | ~552 |
| #29220 | 12:04 AM | 🔵 | Observation Type and Concept Taxonomy | ~355 |
### Dec 21, 2025
**observation-metadata.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #31747 | 10:43 PM | 🔵 | PR #412 Code Review Identifies Two Critical Bugs in Mode System | ~545 |
| #31433 | 6:58 PM | 🔄 | Simplified observation-metadata.ts to use hardcoded defaults | ~330 |
| #31429 | 6:57 PM | 🔄 | Removed unused emoji mapping constants from observation metadata | ~245 |
| #31423 | 6:50 PM | 🔵 | Observation Metadata Constants File Structure | ~327 |
| #31329 | 5:45 PM | 🔵 | Observation Metadata Integration Across Services and UI | ~403 |
| #31328 | " | 🔵 | Settings Defaults Manager Uses Observation Metadata Constants | ~286 |
| #31327 | " | 🔵 | Observation Metadata Constants - Core Type and Concept Definitions | ~369 |
### Dec 25, 2025
**observation-metadata.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #32701 | 9:00 PM | 🔵 | Test Coverage Report Generated | ~471 |
### Jan 2, 2026
**observation-metadata.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #35875 | 2:39 PM | 🔵 | Logging UI Architecture Mapped | ~599 |
| #35836 | 2:30 PM | 🔵 | Observation metadata constants for types and concepts | ~280 |
</claude-mem-context>
-105
View File
@@ -1,105 +0,0 @@
<claude-mem-context>
# Recent Activity
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
### Dec 9, 2025
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #23126 | 6:40 PM | ✅ | Removed SKIP_TOOLS Check from saveHook Function | ~288 |
| #23125 | " | 🔵 | SKIP_TOOLS Reference Still Present in saveHook Function | ~224 |
| #23124 | " | ✅ | Removed SKIP_TOOLS Constant from save-hook.ts | ~297 |
| #23123 | 6:39 PM | 🔵 | Current SKIP_TOOLS Implementation in save-hook.ts | ~397 |
| #23122 | " | 🔴 | Hardened Spinner Stop Mechanism with Timeout and Logging | ~361 |
| #23121 | " | 🔵 | Current Spinner Stop Implementation in summary-hook.ts | ~348 |
| #23118 | 6:38 PM | ✅ | Phase 6: StopInput Interface Type Safety Restored | ~248 |
| #23117 | 6:37 PM | ✅ | Phase 6: PostToolUseInput Interface Type Safety Restored | ~222 |
| #23116 | " | ✅ | Phase 6: UserPromptSubmitInput Interface Type Safety Restored | ~216 |
| #23115 | " | ✅ | Phase 6: SessionStartInput Interface Type Safety Restored | ~341 |
| #23114 | " | 🔵 | Current State of context-hook.ts Interface | ~409 |
| #23113 | " | 🔵 | Current State of summary-hook.ts Interface and Spinner Stop | ~397 |
| #23112 | " | 🔵 | Current State of save-hook.ts Interface and SKIP_TOOLS | ~395 |
| #23111 | " | 🔵 | Current State of new-hook.ts Interface | ~381 |
| #23076 | 6:27 PM | ✅ | Added Comment Explaining Exit Code 3 in user-message-hook.ts | ~245 |
| #23075 | 6:26 PM | ✅ | Deleted Expired Announcement Code from user-message-hook.ts | ~354 |
| #23074 | " | ✅ | Replaced Verbose Manual Mode Help with Error in cleanup-hook.ts | ~222 |
| #23073 | " | ✅ | Removed cwd from cleanup-hook Debug Logging | ~177 |
### Dec 10, 2025
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #23407 | 2:14 PM | 🔵 | New Hook Implementation Structure | ~264 |
### Dec 13, 2025
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #25389 | 9:30 PM | 🔴 | Save Hook Error Logging Enhanced With Tool Context | ~361 |
| #25388 | " | 🔴 | New Hook Now Logs SDK Agent Start Errors | ~344 |
| #25387 | 9:29 PM | 🔴 | New Hook Now Logs Session Initialization Errors | ~350 |
| #25386 | " | 🔴 | Context Hook Now Logs Error Text Before Throwing | ~338 |
| #25385 | " | ✅ | Added Logger Import to New Hook | ~249 |
| #25384 | " | ✅ | Added Logger Import to Context Hook | ~223 |
| #25383 | " | 🔵 | New Hook Has Two Silent Failure Points | ~392 |
| #25382 | 9:28 PM | 🔵 | Save Hook Has Partial Error Logging | ~351 |
| #25381 | " | 🔵 | Summary Hook Has Partial Error Logging | ~345 |
| #25380 | " | 🔵 | Context Hook Silent Failure Pattern Confirmed | ~354 |
### Dec 14, 2025
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #26730 | 11:24 PM | 🔵 | Context Hook TypeScript Source Shows EnsureWorkerRunning as First Action | ~441 |
| #26729 | " | 🔵 | Context Hook TypeScript Source Calls ensureWorkerRunning Before API Requests | ~411 |
| #26260 | 8:32 PM | 🔵 | User Message Hook Calls Context Inject with colors=true Parameter | ~300 |
| #26244 | 8:29 PM | 🔵 | Context Hook Delegates to Worker API Context Endpoint | ~260 |
| #25692 | 4:24 PM | 🔵 | Summary hook extracts last user and assistant messages from transcript file before sending to worker | ~465 |
### Dec 17, 2025
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #28449 | 4:23 PM | 🔵 | New Hook Session Initialization Flow | ~385 |
### Dec 19, 2025
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #30105 | 8:11 PM | 🔵 | Hook Response Utility Standardizes Hook Output Format | ~387 |
| #30103 | 8:10 PM | 🔵 | Context Hook Injects Mode-Based Memory Context During SessionStart | ~460 |
### Dec 20, 2025
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #31085 | 7:59 PM | 🔵 | Summary Hook Uses session_id from Hook Input | ~315 |
### Dec 27, 2025
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #33216 | 9:07 PM | 🔵 | UserPromptSubmit Hook (new-hook.ts) Initializes Session and Starts SDK Agent via Two HTTP Endpoints | ~735 |
| #33211 | 9:04 PM | 🔵 | User Message Hook Displays Context Info via stderr in Parallel with Context Injection | ~476 |
| #33210 | 9:03 PM | 🔵 | Summary Hook (summary-hook.ts) Extracts Messages and Triggers Summarization | ~479 |
| #33209 | " | 🔵 | SessionStart Hook (context-hook.ts) Fetches Context Injection via HTTP | ~520 |
### Jan 7, 2026
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #38235 | 7:42 PM | ✅ | Deprecated User Message Hook Source File | ~399 |
| #38176 | 7:26 PM | ⚖️ | Plan Created to Merge User Message into Context Hook JSON Output | ~536 |
| #38175 | " | 🔵 | Complete Claude-Mem Hook Output Architecture Documented | ~530 |
| #38174 | " | 🔵 | UserPromptSubmit Hook Initializes Sessions and Strips Slash Commands | ~480 |
| #38173 | 7:25 PM | 🔵 | Standard Hook Response Pattern for Non-SessionStart Hooks | ~343 |
| #38172 | 7:22 PM | 🔵 | Claude Code Hook Output Architecture Clarified - Exit Code Pattern is Correct for User-Only Display | ~523 |
| #38170 | 7:21 PM | 🔵 | User-Message-Hook TypeScript Source Shows Exit Code 1 Strategy for User-Only Display | ~203 |
### Jan 9, 2026
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #38783 | 4:59 PM | 🔵 | Standard Hook Response Pattern | ~263 |
</claude-mem-context>
-21
View File
@@ -1,21 +0,0 @@
<claude-mem-context>
# Recent Activity
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
### Jan 5, 2026
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #37703 | 6:01 PM | 🔵 | ParsedObservation files_read and files_modified are string arrays parsed from XML | ~330 |
| #37701 | " | 🔵 | Complete cwd data flow traced from hooks through observation processing | ~447 |
### Jan 7, 2026
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #38467 | 10:29 PM | ⚖️ | Log Level Audit Strategy: Tighten ERROR Messages for Runtime Issue Discovery | ~464 |
| #38454 | 10:26 PM | 🔵 | happyPathError usage pattern in summary prompt generation | ~421 |
| #38405 | 10:07 PM | ⚖️ | DEBUG Log Level Analysis - One Message Requires WARN Promotion | ~819 |
| #38404 | 10:06 PM | ⚖️ | Log Level Audit Analysis - WARN to ERROR Promotion Criteria Established | ~769 |
</claude-mem-context>
+14 -6
View File
@@ -162,9 +162,13 @@ export function parseSummary(text: string, sessionId?: number): ParsedSummary |
/**
* Extract a simple field value from XML content
* Returns null for missing or empty/whitespace-only fields
*
* Uses non-greedy match to handle nested tags and code snippets (Issue #798)
*/
function extractField(content: string, fieldName: string): string | null {
const regex = new RegExp(`<${fieldName}>([^<]*)</${fieldName}>`);
// Use [\s\S]*? to match any character including newlines, non-greedily
// This handles nested XML tags like <item>...</item> inside the field
const regex = new RegExp(`<${fieldName}>([\\s\\S]*?)</${fieldName}>`);
const match = regex.exec(content);
if (!match) return null;
@@ -174,12 +178,13 @@ function extractField(content: string, fieldName: string): string | null {
/**
* Extract array of elements from XML content
* Handles nested tags and code snippets (Issue #798)
*/
function extractArrayElements(content: string, arrayName: string, elementName: string): string[] {
const elements: string[] = [];
// Match the array block
const arrayRegex = new RegExp(`<${arrayName}>(.*?)</${arrayName}>`, 's');
// Match the array block using [\s\S]*? for nested content
const arrayRegex = new RegExp(`<${arrayName}>([\\s\\S]*?)</${arrayName}>`);
const arrayMatch = arrayRegex.exec(content);
if (!arrayMatch) {
@@ -188,11 +193,14 @@ function extractArrayElements(content: string, arrayName: string, elementName: s
const arrayContent = arrayMatch[1];
// Extract individual elements
const elementRegex = new RegExp(`<${elementName}>([^<]+)</${elementName}>`, 'g');
// Extract individual elements using [\s\S]*? for nested content
const elementRegex = new RegExp(`<${elementName}>([\\s\\S]*?)</${elementName}>`, 'g');
let elementMatch;
while ((elementMatch = elementRegex.exec(arrayContent)) !== null) {
elements.push(elementMatch[1].trim());
const trimmed = elementMatch[1].trim();
if (trimmed) {
elements.push(trimmed);
}
}
return elements;
-100
View File
@@ -1,100 +0,0 @@
<claude-mem-context>
# Recent Activity
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
### Nov 6, 2025
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #4185 | 10:25 PM | 🔴 | Prefixed unused id parameters with underscore in filter callbacks | ~299 |
### Nov 8, 2025
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #5539 | 10:20 PM | 🔵 | Harsh critical audit of context-hook reveals systematic anti-patterns | ~3154 |
| #5497 | 9:29 PM | 🔵 | Harsh critical audit of context-hook reveals systematic anti-patterns | ~2815 |
### Nov 9, 2025
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #5757 | 5:16 PM | 🔵 | MCP search server exposes 9 tools consuming ~2,000-3,000 tokens per session | ~421 |
| #5754 | 5:14 PM | 🔵 | MCP search server provides 9 search tools with hybrid semantic/FTS5 | ~402 |
### Nov 10, 2025
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #6250 | 12:54 PM | 🔵 | MCP Search Server Connection Failure Reported | ~329 |
### Nov 17, 2025
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #10744 | 11:47 PM | ✅ | Search Query Parameter Made Optional for Filter-Only Queries | ~373 |
| #10572 | 7:47 PM | 🟣 | Unified cross-type search with search_everything tool | ~501 |
| #10571 | 7:46 PM | 🔵 | Search server architecture and hybrid search implementation | ~553 |
### Nov 18, 2025
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #11462 | 7:55 PM | 🔵 | Ready to Apply Fix to Contextualize Handler | ~261 |
| #11460 | " | 🔴 | Identified Root Cause of Contextualize Endpoint Bug | ~413 |
| #11454 | 7:54 PM | 🔵 | Unified Search Handler Shows Correct Pattern for Filter-Only Queries | ~334 |
| #11447 | " | 🔵 | Contextualize Handler Calls Search Methods with Query='*' | ~279 |
| #11432 | 7:52 PM | 🔵 | Contextualize Handler Formats Results with Sections | ~286 |
| #11431 | 7:51 PM | 🔵 | Confirmed Empty Results Trigger in Contextualize Handler | ~289 |
| #11430 | " | 🔵 | Contextualize Handler Implementation Uses Search Methods | ~424 |
| #11429 | " | 🔵 | Search Server Defines Six Main Search Tools | ~358 |
| #11428 | " | 🔵 | Contextualize Tool Definition Found in Search Server | ~357 |
| #11332 | 3:55 PM | 🔵 | Comprehensive FTS5 Removal Audit Completed for Architecture Migration | ~792 |
| #11206 | 3:01 PM | 🔵 | mem-search skill architecture and migration details retrieved in full format | ~538 |
| #11181 | 4:09 AM | 🔵 | Store methods for ID-based lookups exist but not exposed as MCP tools | ~495 |
| #11013 | 2:12 AM | 🔵 | Search Server Implements Three-Path Query Strategy with ChromaDB Primary and FTS5 Fallback | ~462 |
### Nov 28, 2025
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #16711 | 4:34 PM | 🟣 | include_inactive Parameter Extracted in Search Handler | ~369 |
| #16710 | " | 🔵 | Search Tool Schema Definition with Type and Filter Parameters | ~527 |
| #16708 | " | 🔵 | Search Server MCP Tool Architecture and ChromaDB Integration | ~491 |
| #16682 | 4:10 PM | 🔵 | Comprehensive Exploration Task Completed on Observation System | ~601 |
### Dec 14, 2025
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #26238 | 8:28 PM | 🔵 | MCP Server Architecture Maps Tools to Worker API Endpoints | ~355 |
| #26138 | 7:55 PM | ✅ | Updated Comment to Reference progressive_description Tool | ~238 |
| #26137 | " | ✅ | Completed Tool Description Minimization - All 9 Tools Updated | ~335 |
| #26136 | " | ✅ | Minimized Get Session Tool Description | ~218 |
| #26135 | " | ✅ | Minimized Get Batch Observations Tool Description | ~258 |
| #26134 | " | ✅ | Minimized Get Observation Tool Description | ~228 |
| #26133 | " | ✅ | Minimized Get Context Timeline Tool Description | ~245 |
| #26132 | 7:54 PM | ✅ | Minimized Get Recent Context Tool Description | ~214 |
| #26131 | " | ✅ | Minimized Timeline Tool Description | ~232 |
| #26130 | " | ✅ | Minimized Search Tool Description | ~235 |
| #26129 | " | ✅ | Renamed progressive_ix Tool to progressive_description with Minimized Description | ~296 |
| #26128 | " | ✅ | Renamed Tool Endpoint Mapping from progressive_ix to progressive_description | ~229 |
| #26127 | " | ✅ | Completed Format Parameter Removal from All Four MCP Tools | ~318 |
| #26126 | 7:53 PM | ✅ | Removed Format Parameter from Get Recent Context Tool Schema | ~244 |
| #26125 | " | ✅ | Removed Format Parameter from Timeline Tool Schema | ~248 |
| #26124 | " | ✅ | Removed Format Parameter from Search Tool Schema | ~283 |
| #26123 | " | 🔵 | Current MCP Server Tool Schema Analysis | ~337 |
| #25815 | 5:31 PM | 🔵 | Comprehensive MCP Server and SKILL.md Structure Analysis | ~575 |
| #25807 | 5:30 PM | 🔵 | MCP Server Architecture with 14 HTTP-Delegating Tools | ~545 |
| #25788 | 5:15 PM | 🔵 | MCP Server Capabilities and Request Handlers | ~256 |
### Dec 17, 2025
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #29078 | 10:16 PM | ✅ | Updated get_recent_context tool schema to accept dynamic parameters | ~318 |
| #29077 | 10:15 PM | ✅ | Updated timeline tool schema to accept dynamic parameters | ~292 |
| #29076 | " | ✅ | Updated search tool schema to accept dynamic parameters | ~315 |
| #28923 | 7:28 PM | 🔵 | MCP Server Architecture: Thin HTTP Wrapper Pattern | ~402 |
</claude-mem-context>
+25
View File
@@ -233,6 +233,31 @@ NEVER fetch full details without filtering first. 10x token savings.`,
handler: async (args: any) => {
return await callWorkerAPIPost('/api/observations/batch', args);
}
},
{
name: 'save_memory',
description: 'Save a manual memory/observation for semantic search. Use this to remember important information.',
inputSchema: {
type: 'object',
properties: {
text: {
type: 'string',
description: 'Content to remember (required)'
},
title: {
type: 'string',
description: 'Short title (auto-generated from text if omitted)'
},
project: {
type: 'string',
description: 'Project name (uses "claude-mem" if omitted)'
}
},
required: ['text']
},
handler: async (args: any) => {
return await callWorkerAPIPost('/api/memory/save', args);
}
}
];
-2
View File
@@ -1,8 +1,6 @@
<claude-mem-context>
# Recent Activity
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
### Dec 10, 2025
| ID | Time | T | Title | Read |
-7
View File
@@ -1,7 +0,0 @@
<claude-mem-context>
# Recent Activity
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
*No recent activity*
</claude-mem-context>
+3 -2
View File
@@ -5,10 +5,10 @@
*/
import path from 'path';
import { homedir } from 'os';
import { existsSync, readFileSync } from 'fs';
import { SessionStore } from '../sqlite/SessionStore.js';
import { logger } from '../../utils/logger.js';
import { CLAUDE_CONFIG_DIR } from '../../shared/paths.js';
import type {
ContextConfig,
Observation,
@@ -203,7 +203,8 @@ export function getPriorSessionMessages(
const priorSessionId = priorSessionObs.memory_session_id;
const dashedCwd = cwdToDashed(cwd);
const transcriptPath = path.join(homedir(), '.claude', 'projects', dashedCwd, `${priorSessionId}.jsonl`);
// Use CLAUDE_CONFIG_DIR to support custom Claude config directories
const transcriptPath = path.join(CLAUDE_CONFIG_DIR, 'projects', dashedCwd, `${priorSessionId}.jsonl`);
return extractPriorMessages(transcriptPath);
}
-65
View File
@@ -1,65 +0,0 @@
<claude-mem-context>
# Recent Activity
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
### Jan 3, 2026
**MarkdownFormatter.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #36562 | 9:49 PM | 🟣 | Phase 4 Context Generation Tests Completed | ~524 |
| #36561 | " | 🟣 | Phase 4 Context Generation Test Suite Completion | ~606 |
| #36557 | 9:47 PM | 🟣 | MarkdownFormatter Test Suite Created | ~520 |
| #36553 | 9:43 PM | 🔵 | MarkdownFormatter Rendering Functions | ~445 |
| #36552 | " | 🔵 | Context Generation API Documentation for Phase 4 | ~496 |
| #36292 | 8:04 PM | 🔄 | Phase 4 Module Inventory: 12 Files Created in Context Architecture | ~571 |
**ColorFormatter.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #36390 | 8:50 PM | 🔄 | Comprehensive Monolith Refactor with Modular Architecture | ~724 |
### Jan 4, 2026
**ColorFormatter.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #36949 | 2:45 AM | 🟣 | Added Timestamp to Empty State Context Header | ~268 |
| #36947 | 2:44 AM | 🔵 | ColorFormatter Header Rendering Location Found | ~235 |
| #36946 | " | 🟣 | Context Header Timestamp Display | ~322 |
| #36944 | " | 🔵 | ColorFormatter Architecture - Terminal Context Display | ~374 |
**MarkdownFormatter.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #36948 | 2:44 AM | 🔴 | Add Timestamp to Empty State Context Header | ~270 |
| #36945 | " | 🟣 | Context Header Now Displays Current Date and Time | ~303 |
| #36943 | 2:43 AM | 🔵 | MarkdownFormatter Structure for Context Injection | ~346 |
| #36942 | " | 🔵 | Recent Context Feature Architecture | ~300 |
### Jan 5, 2026
**ColorFormatter.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #38048 | 9:45 PM | 🔴 | PR #558 - Comprehensive Bug Fix and Test Quality Improvement | ~585 |
| #37582 | 4:53 PM | 🔴 | Updated ColorFormatter Second mem-search Reference - Phase 2 Complete | ~398 |
| #37581 | " | 🔴 | Updated ColorFormatter First mem-search Reference | ~362 |
| #37577 | 4:52 PM | 🔵 | ColorFormatter Contains Outdated mem-search References | ~395 |
| #37530 | 4:43 PM | 🔵 | Issue #544 Confirmed in ColorFormatter Second Location | ~344 |
**MarkdownFormatter.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #37617 | 5:32 PM | ⚖️ | PR #558 Review Requirements Categorized by Priority | ~637 |
| #37613 | 5:31 PM | 🔵 | PR #558 Review Feedback Analysis | ~544 |
| #37586 | 4:54 PM | 🔴 | Phase 2 Committed - mem-search Hint Messages Fixed | ~375 |
| #37583 | 4:53 PM | 🔴 | Phase 2 Complete - All mem-search References Updated | ~394 |
| #37580 | " | 🔴 | Updated MarkdownFormatter Second mem-search Reference | ~360 |
| #37579 | " | 🔴 | Updated MarkdownFormatter First mem-search Reference | ~350 |
| #37576 | 4:52 PM | 🔵 | MarkdownFormatter Contains Outdated mem-search References | ~372 |
| #37555 | 4:49 PM | 🔵 | Issue #544 Message Locations and Fix Pattern Documented | ~463 |
| #37545 | 4:47 PM | ✅ | Issue #544 Analysis Report Created for mem-search Skill Messaging Problem | ~480 |
| #37529 | 4:42 PM | 🔵 | Issue #544 Misleading mem-search Skill Reference Located | ~368 |
</claude-mem-context>
-26
View File
@@ -1,26 +0,0 @@
<claude-mem-context>
# Recent Activity
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
### Jan 3, 2026
**FooterRenderer.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #36390 | 8:50 PM | 🔄 | Comprehensive Monolith Refactor with Modular Architecture | ~724 |
| #36283 | 8:02 PM | 🔄 | Phase 4: FooterRenderer Extracted with Conditional Display Logic | ~464 |
**TimelineRenderer.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #36292 | 8:04 PM | 🔄 | Phase 4 Module Inventory: 12 Files Created in Context Architecture | ~571 |
| #36281 | 8:01 PM | 🔄 | Phase 4: TimelineRenderer Extracted with Dual Format Support | ~531 |
### Jan 5, 2026
**FooterRenderer.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #37545 | 4:47 PM | ✅ | Issue #544 Analysis Report Created for mem-search Skill Messaging Problem | ~480 |
</claude-mem-context>
-37
View File
@@ -1,37 +0,0 @@
<claude-mem-context>
# Recent Activity
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
### Jan 4, 2026
**FolderDiscovery.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #37021 | 4:59 PM | ✅ | Deleted Redundant Folder Index Service Directory | ~299 |
| #37011 | 4:50 PM | 🔵 | FolderDiscovery extracts folders from observations and applies depth, exclusion, and activity filters | ~433 |
**ClaudeMdGenerator.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #37012 | 4:51 PM | 🔵 | ClaudeMdGenerator writes tag-wrapped timeline markdown while preserving manual content | ~446 |
| #36981 | 4:25 PM | 🔵 | ClaudeMdGenerator creates and updates CLAUDE.md files with timeline content | ~336 |
**types.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #37010 | 4:50 PM | 🔵 | Type definitions specify folder-index configuration schema and timeline data structures | ~349 |
**FolderTimelineCompiler.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #37009 | 4:50 PM | 🔵 | FolderTimelineCompiler queries database and groups activity chronologically by date | ~419 |
| #37002 | 4:45 PM | 🔴 | Fixed session file deduplication and summary selection in FolderTimelineCompiler | ~306 |
| #37001 | " | 🔴 | Fixed FolderTimelineCompiler to generate concise summaries and deduplicate files | ~284 |
**FolderIndexOrchestrator.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #37008 | 4:50 PM | 🔵 | FolderIndexOrchestrator implements event-driven regeneration triggered by observation saves | ~418 |
| #36983 | 4:26 PM | 🔵 | FolderIndexOrchestrator coordinates automatic CLAUDE.md regeneration after observation saves | ~367 |
</claude-mem-context>
-2
View File
@@ -1,8 +1,6 @@
<claude-mem-context>
# Recent Activity
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
### Jan 4, 2026
| ID | Time | T | Title | Read |
+9 -6
View File
@@ -10,9 +10,9 @@
*/
import path from 'path';
import { homedir } from 'os';
import { readFileSync } from 'fs';
import { logger } from '../../utils/logger.js';
import { MARKETPLACE_ROOT } from '../../shared/paths.js';
/**
* Check if a port is in use by querying the health endpoint
@@ -29,17 +29,21 @@ export async function isPortInUse(port: number): Promise<boolean> {
}
/**
* Wait for the worker to become fully ready (passes readiness check)
* Wait for the worker HTTP server to become responsive (liveness check)
* Uses /api/health instead of /api/readiness because:
* - /api/health returns 200 as soon as HTTP server is listening
* - /api/readiness waits for full initialization (MCP connection can take 5+ minutes)
* See: https://github.com/thedotmack/claude-mem/issues/811
* @param port Worker port to check
* @param timeoutMs Maximum time to wait in milliseconds
* @returns true if worker became ready, false if timeout
* @returns true if worker became responsive, false if timeout
*/
export async function waitForHealth(port: number, timeoutMs: number = 30000): Promise<boolean> {
const start = Date.now();
while (Date.now() - start < timeoutMs) {
try {
// Note: Removed AbortSignal.timeout to avoid Windows Bun cleanup issue (libuv assertion)
const response = await fetch(`http://127.0.0.1:${port}/api/readiness`);
const response = await fetch(`http://127.0.0.1:${port}/api/health`);
if (response.ok) return true;
} catch (error) {
// [ANTI-PATTERN IGNORED]: Retry loop - expected failures during startup, will retry
@@ -96,8 +100,7 @@ export async function httpShutdown(port: number): Promise<boolean> {
* This is the "expected" version that should be running
*/
export function getInstalledPluginVersion(): string {
const marketplaceRoot = path.join(homedir(), '.claude', 'plugins', 'marketplaces', 'thedotmack');
const packageJsonPath = path.join(marketplaceRoot, 'package.json');
const packageJsonPath = path.join(MARKETPLACE_ROOT, 'package.json');
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
return packageJson.version;
}
+197 -49
View File
@@ -22,6 +22,17 @@ const execAsync = promisify(exec);
const DATA_DIR = path.join(homedir(), '.claude-mem');
const PID_FILE = path.join(DATA_DIR, 'worker.pid');
// Orphaned process cleanup patterns and thresholds
// These are claude-mem processes that can accumulate if not properly terminated
const ORPHAN_PROCESS_PATTERNS = [
'mcp-server.cjs', // Main MCP server process
'worker-service.cjs', // Background worker daemon
'chroma-mcp' // ChromaDB MCP subprocess
];
// Only kill processes older than this to avoid killing the current session
const ORPHAN_MAX_AGE_MINUTES = 30;
export interface PidInfo {
pid: number;
port: number;
@@ -66,7 +77,11 @@ export function removePidFile(): void {
}
/**
* Get platform-adjusted timeout (Windows socket cleanup is slower)
* Get platform-adjusted timeout for worker-side socket operations (2.0x on Windows).
*
* Note: Two platform multiplier functions exist intentionally:
* - getTimeout() in hook-constants.ts uses 1.5x for hook-side operations (fast path)
* - getPlatformTimeout() here uses 2.0x for worker-side socket operations (slower path)
*/
export function getPlatformTimeout(baseMs: number): number {
const WINDOWS_MULTIPLIER = 2.0;
@@ -90,7 +105,7 @@ export async function getChildProcesses(parentPid: number): Promise<number[]> {
try {
// PowerShell Get-Process instead of WMIC (deprecated in Windows 11)
const cmd = `powershell -NoProfile -NonInteractive -Command "Get-Process | Where-Object { \\$_.ParentProcessId -eq ${parentPid} } | Select-Object -ExpandProperty Id"`;
const cmd = `powershell -NoProfile -NonInteractive -Command "Get-Process | Where-Object { $_.ParentProcessId -eq ${parentPid} } | Select-Object -ExpandProperty Id"`;
const { stdout } = await execAsync(cmd, { timeout: HOOK_TIMEOUTS.POWERSHELL_COMMAND });
// PowerShell outputs just numbers (one per line), simpler than WMIC's "ProcessId=1234" format
return stdout
@@ -162,55 +177,119 @@ export async function waitForProcessesExit(pids: number[], timeoutMs: number): P
}
/**
* Clean up orphaned chroma-mcp processes from previous worker sessions
* Prevents process accumulation and memory leaks
* Parse process elapsed time from ps etime format: [[DD-]HH:]MM:SS
* Returns age in minutes, or -1 if parsing fails
*/
export function parseElapsedTime(etime: string): number {
if (!etime || etime.trim() === '') return -1;
const cleaned = etime.trim();
let totalMinutes = 0;
// DD-HH:MM:SS format
const dayMatch = cleaned.match(/^(\d+)-(\d+):(\d+):(\d+)$/);
if (dayMatch) {
totalMinutes = parseInt(dayMatch[1], 10) * 24 * 60 +
parseInt(dayMatch[2], 10) * 60 +
parseInt(dayMatch[3], 10);
return totalMinutes;
}
// HH:MM:SS format
const hourMatch = cleaned.match(/^(\d+):(\d+):(\d+)$/);
if (hourMatch) {
totalMinutes = parseInt(hourMatch[1], 10) * 60 + parseInt(hourMatch[2], 10);
return totalMinutes;
}
// MM:SS format
const minMatch = cleaned.match(/^(\d+):(\d+)$/);
if (minMatch) {
return parseInt(minMatch[1], 10);
}
return -1;
}
/**
* Clean up orphaned claude-mem processes from previous worker sessions
*
* Targets mcp-server.cjs, worker-service.cjs, and chroma-mcp processes
* that survived a previous daemon crash. Only kills processes older than
* ORPHAN_MAX_AGE_MINUTES to avoid killing the current session.
*
* The periodic ProcessRegistry reaper handles in-session orphans;
* this function handles cross-session orphans at startup.
*/
export async function cleanupOrphanedProcesses(): Promise<void> {
const isWindows = process.platform === 'win32';
const pids: number[] = [];
const currentPid = process.pid;
const pidsToKill: number[] = [];
try {
if (isWindows) {
// Windows: Use PowerShell Get-CimInstance instead of WMIC (deprecated in Windows 11)
const cmd = `powershell -NoProfile -NonInteractive -Command "Get-CimInstance Win32_Process | Where-Object { \\$_.Name -like '*python*' -and \\$_.CommandLine -like '*chroma-mcp*' } | Select-Object -ExpandProperty ProcessId"`;
// Windows: Use PowerShell Get-CimInstance with JSON output for age filtering
const patternConditions = ORPHAN_PROCESS_PATTERNS
.map(p => `$_.CommandLine -like '*${p}*'`)
.join(' -or ');
const cmd = `powershell -NoProfile -NonInteractive -Command "Get-CimInstance Win32_Process | Where-Object { (${patternConditions}) -and $_.ProcessId -ne ${currentPid} } | Select-Object ProcessId, CreationDate | ConvertTo-Json"`;
const { stdout } = await execAsync(cmd, { timeout: HOOK_TIMEOUTS.POWERSHELL_COMMAND });
if (!stdout.trim()) {
logger.debug('SYSTEM', 'No orphaned chroma-mcp processes found (Windows)');
if (!stdout.trim() || stdout.trim() === 'null') {
logger.debug('SYSTEM', 'No orphaned claude-mem processes found (Windows)');
return;
}
// PowerShell outputs just numbers (one per line), simpler than WMIC's "ProcessId=1234" format
const lines = stdout
.split('\n')
.map(line => line.trim())
.filter(line => line.length > 0 && /^\d+$/.test(line));
const processes = JSON.parse(stdout);
const processList = Array.isArray(processes) ? processes : [processes];
const now = Date.now();
for (const line of lines) {
const pid = parseInt(line, 10);
// SECURITY: Validate PID is positive integer before adding to list
if (!isNaN(pid) && Number.isInteger(pid) && pid > 0) {
pids.push(pid);
for (const proc of processList) {
const pid = proc.ProcessId;
// SECURITY: Validate PID is positive integer and not current process
if (!Number.isInteger(pid) || pid <= 0 || pid === currentPid) continue;
// Parse Windows WMI date format: /Date(1234567890123)/
const creationMatch = proc.CreationDate?.match(/\/Date\((\d+)\)\//);
if (creationMatch) {
const creationTime = parseInt(creationMatch[1], 10);
const ageMinutes = (now - creationTime) / (1000 * 60);
if (ageMinutes >= ORPHAN_MAX_AGE_MINUTES) {
pidsToKill.push(pid);
logger.debug('SYSTEM', 'Found orphaned process', { pid, ageMinutes: Math.round(ageMinutes) });
}
}
}
} else {
// Unix: Use ps aux | grep
const { stdout } = await execAsync('ps aux | grep "chroma-mcp" | grep -v grep || true');
// Unix: Use ps with elapsed time for age-based filtering
const patternRegex = ORPHAN_PROCESS_PATTERNS.join('|');
const { stdout } = await execAsync(
`ps -eo pid,etime,command | grep -E "${patternRegex}" | grep -v grep || true`
);
if (!stdout.trim()) {
logger.debug('SYSTEM', 'No orphaned chroma-mcp processes found (Unix)');
logger.debug('SYSTEM', 'No orphaned claude-mem processes found (Unix)');
return;
}
const lines = stdout.trim().split('\n');
for (const line of lines) {
const parts = line.trim().split(/\s+/);
if (parts.length > 1) {
const pid = parseInt(parts[1], 10);
// SECURITY: Validate PID is positive integer before adding to list
if (!isNaN(pid) && Number.isInteger(pid) && pid > 0) {
pids.push(pid);
}
// Parse: " 1234 01:23:45 /path/to/process"
const match = line.trim().match(/^(\d+)\s+(\S+)\s+(.*)$/);
if (!match) continue;
const pid = parseInt(match[1], 10);
const etime = match[2];
// SECURITY: Validate PID is positive integer and not current process
if (!Number.isInteger(pid) || pid <= 0 || pid === currentPid) continue;
const ageMinutes = parseElapsedTime(etime);
if (ageMinutes >= ORPHAN_MAX_AGE_MINUTES) {
pidsToKill.push(pid);
logger.debug('SYSTEM', 'Found orphaned process', { pid, ageMinutes, command: match[3].substring(0, 80) });
}
}
}
@@ -220,19 +299,20 @@ export async function cleanupOrphanedProcesses(): Promise<void> {
return;
}
if (pids.length === 0) {
if (pidsToKill.length === 0) {
return;
}
logger.info('SYSTEM', 'Cleaning up orphaned chroma-mcp processes', {
logger.info('SYSTEM', 'Cleaning up orphaned claude-mem processes', {
platform: isWindows ? 'Windows' : 'Unix',
count: pids.length,
pids
count: pidsToKill.length,
pids: pidsToKill,
maxAgeMinutes: ORPHAN_MAX_AGE_MINUTES
});
// Kill all found processes
if (isWindows) {
for (const pid of pids) {
for (const pid of pidsToKill) {
// SECURITY: Double-check PID validation before using in taskkill command
if (!Number.isInteger(pid) || pid <= 0) {
logger.warn('SYSTEM', 'Skipping invalid PID', { pid });
@@ -246,7 +326,7 @@ export async function cleanupOrphanedProcesses(): Promise<void> {
}
}
} else {
for (const pid of pids) {
for (const pid of pidsToKill) {
try {
process.kill(pid, 'SIGKILL');
} catch (error) {
@@ -256,16 +336,16 @@ export async function cleanupOrphanedProcesses(): Promise<void> {
}
}
logger.info('SYSTEM', 'Orphaned processes cleaned up', { count: pids.length });
logger.info('SYSTEM', 'Orphaned processes cleaned up', { count: pidsToKill.length });
}
/**
* Spawn a detached daemon process
* Returns the child PID or undefined if spawn failed
*
* On Windows, uses WMIC to spawn a truly independent process that
* survives parent exit without console popups. WMIC creates processes
* that are not associated with the parent's console.
* On Windows, uses PowerShell Start-Process with -WindowStyle Hidden to spawn
* a truly independent process without console popups. Unlike WMIC, PowerShell
* inherits environment variables from the parent process.
*
* On Unix, uses standard detached spawn.
*
@@ -285,28 +365,46 @@ export function spawnDaemon(
};
if (isWindows) {
// Use WMIC to spawn a process that's independent of the parent console
// This avoids the console popup that occurs with detached: true
// Paths must be individually quoted for WMIC when they contain spaces
// Use PowerShell Start-Process to spawn a hidden, independent process
// Unlike WMIC, PowerShell inherits environment variables from parent
// -WindowStyle Hidden prevents console popup
const execPath = process.execPath;
const script = scriptPath;
// WMIC command format: wmic process call create "\"path1\" \"path2\" args"
const command = `wmic process call create "\\"${execPath}\\" \\"${script}\\" --daemon"`;
const psCommand = `Start-Process -FilePath '${execPath}' -ArgumentList '${script}','--daemon' -WindowStyle Hidden`;
try {
execSync(command, {
execSync(`powershell -NoProfile -Command "${psCommand}"`, {
stdio: 'ignore',
windowsHide: true
windowsHide: true,
env
});
// WMIC returns immediately, we can't get the spawned PID easily
// Worker will write its own PID file after listen()
return 0;
} catch {
return undefined;
}
}
// Unix: standard detached spawn
// Unix: Use setsid to create a new session, fully detaching from the
// controlling terminal. This prevents SIGHUP from reaching the daemon
// even if the in-process SIGHUP handler somehow fails (belt-and-suspenders).
// Fall back to standard detached spawn if setsid is not available.
const setsidPath = '/usr/bin/setsid';
if (existsSync(setsidPath)) {
const child = spawn(setsidPath, [process.execPath, scriptPath, '--daemon'], {
detached: true,
stdio: 'ignore',
env
});
if (child.pid === undefined) {
return undefined;
}
child.unref();
return child.pid;
}
// Fallback: standard detached spawn (macOS, systems without setsid)
const child = spawn(process.execPath, [scriptPath, '--daemon'], {
detached: true,
stdio: 'ignore',
@@ -322,6 +420,56 @@ export function spawnDaemon(
return child.pid;
}
/**
* Check if a process with the given PID is alive.
*
* Uses the process.kill(pid, 0) idiom: signal 0 doesn't send a signal,
* it just checks if the process exists and is reachable.
*
* EPERM is treated as "alive" because it means the process exists but
* belongs to a different user/session (common in multi-user setups).
* PID 0 (Windows WMIC sentinel for unknown PID) is treated as alive.
*/
export function isProcessAlive(pid: number): boolean {
// PID 0 is the Windows WMIC sentinel value — process was spawned but PID unknown
if (pid === 0) return true;
// Invalid PIDs are not alive
if (!Number.isInteger(pid) || pid < 0) return false;
try {
process.kill(pid, 0);
return true;
} catch (error: unknown) {
const code = (error as NodeJS.ErrnoException).code;
// EPERM = process exists but different user/session — treat as alive
if (code === 'EPERM') return true;
// ESRCH = no such process — it's dead
return false;
}
}
/**
* Read the PID file and remove it if the recorded process is dead (stale).
*
* This is a cheap operation: one filesystem read + one signal-0 check.
* Called at the top of ensureWorkerStarted() to clean up after WSL2
* hibernate, OOM kills, or other ungraceful worker deaths.
*/
export function cleanStalePidFile(): void {
const pidInfo = readPidFile();
if (!pidInfo) return;
if (!isProcessAlive(pidInfo.pid)) {
logger.info('SYSTEM', 'Removing stale PID file (worker process is dead)', {
pid: pidInfo.pid,
port: pidInfo.port,
startedAt: pidInfo.startedAt
});
removePidFile();
}
}
/**
* Create signal handler factory for graceful shutdown
* Returns a handler function that can be passed to process.on('SIGTERM') etc.
-7
View File
@@ -1,7 +0,0 @@
<claude-mem-context>
# Recent Activity
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
*No recent activity*
</claude-mem-context>
@@ -16,6 +16,7 @@ import { exec } from 'child_process';
import { promisify } from 'util';
import { logger } from '../../utils/logger.js';
import { getWorkerPort } from '../../shared/worker-utils.js';
import { DATA_DIR, MARKETPLACE_ROOT, CLAUDE_CONFIG_DIR } from '../../shared/paths.js';
import {
readCursorRegistry as readCursorRegistryFromFile,
writeCursorRegistry as writeCursorRegistryToFile,
@@ -27,7 +28,6 @@ import type { CursorInstallTarget, CursorHooksJson, CursorMcpConfig, Platform }
const execAsync = promisify(exec);
// Standard paths
const DATA_DIR = path.join(homedir(), '.claude-mem');
const CURSOR_REGISTRY_FILE = path.join(DATA_DIR, 'cursor-projects.json');
// ============================================================================
@@ -128,12 +128,12 @@ export async function updateCursorContextForProject(projectName: string, port: n
/**
* Find cursor-hooks directory
* Searches in order: marketplace install, source repo
* Checks for both bash (common.sh) and PowerShell (common.ps1) scripts
* Checks for hooks.json (unified CLI mode) or legacy shell scripts
*/
export function findCursorHooksDir(): string | null {
const possiblePaths = [
// Marketplace install location
path.join(homedir(), '.claude', 'plugins', 'marketplaces', 'thedotmack', 'cursor-hooks'),
path.join(MARKETPLACE_ROOT, 'cursor-hooks'),
// Development/source location (relative to built worker-service.cjs in plugin/scripts/)
path.join(path.dirname(__filename), '..', '..', 'cursor-hooks'),
// Alternative dev location
@@ -141,8 +141,10 @@ export function findCursorHooksDir(): string | null {
];
for (const p of possiblePaths) {
// Check for either bash or PowerShell common script
if (existsSync(path.join(p, 'common.sh')) || existsSync(path.join(p, 'common.ps1'))) {
// Check for hooks.json (unified CLI mode) or legacy shell scripts
if (existsSync(path.join(p, 'hooks.json')) ||
existsSync(path.join(p, 'common.sh')) ||
existsSync(path.join(p, 'common.ps1'))) {
return p;
}
}
@@ -156,7 +158,7 @@ export function findCursorHooksDir(): string | null {
export function findMcpServerPath(): string | null {
const possiblePaths = [
// Marketplace install location
path.join(homedir(), '.claude', 'plugins', 'marketplaces', 'thedotmack', 'plugin', 'scripts', 'mcp-server.cjs'),
path.join(MARKETPLACE_ROOT, 'plugin', 'scripts', 'mcp-server.cjs'),
// Development/source location (relative to built worker-service.cjs in plugin/scripts/)
path.join(path.dirname(__filename), 'mcp-server.cjs'),
// Alternative dev location
@@ -178,7 +180,7 @@ export function findMcpServerPath(): string | null {
export function findWorkerServicePath(): string | null {
const possiblePaths = [
// Marketplace install location
path.join(homedir(), '.claude', 'plugins', 'marketplaces', 'thedotmack', 'plugin', 'scripts', 'worker-service.cjs'),
path.join(MARKETPLACE_ROOT, 'plugin', 'scripts', 'worker-service.cjs'),
// Development/source location (relative to built worker-service.cjs in plugin/scripts/)
path.join(path.dirname(__filename), 'worker-service.cjs'),
// Alternative dev location
@@ -193,6 +195,37 @@ export function findWorkerServicePath(): string | null {
return null;
}
/**
* Find the Bun executable path
* Required because worker-service.cjs uses bun:sqlite which is Bun-specific
* Searches common installation locations across platforms
*/
export function findBunPath(): string {
const possiblePaths = [
// Standard user install location (most common)
path.join(homedir(), '.bun', 'bin', 'bun'),
// Global install locations
'/usr/local/bin/bun',
'/usr/bin/bun',
// Windows locations
...(process.platform === 'win32' ? [
path.join(homedir(), '.bun', 'bin', 'bun.exe'),
path.join(process.env.LOCALAPPDATA || '', 'bun', 'bun.exe'),
] : []),
];
for (const p of possiblePaths) {
if (p && existsSync(p)) {
return p;
}
}
// Fallback to 'bun' and hope it's in PATH
// This allows the installation to proceed even if we can't find bun
// The user will get a clear error when the hook runs if bun isn't available
return 'bun';
}
/**
* Get the target directory for Cursor hooks based on install target
*/
@@ -312,15 +345,21 @@ export async function installCursorHooks(_sourceDir: string, target: CursorInsta
// Generate hooks.json with unified CLI commands
const hooksJsonPath = path.join(targetDir, 'hooks.json');
// Find bun executable - required because worker-service.cjs uses bun:sqlite
const bunPath = findBunPath();
const escapedBunPath = bunPath.replace(/\\/g, '\\\\');
// Use the absolute path to worker-service.cjs
// Escape backslashes for JSON on Windows
const escapedWorkerPath = workerServicePath.replace(/\\/g, '\\\\');
// Helper to create hook command using unified CLI
// Helper to create hook command using unified CLI with bun runtime
const makeHookCommand = (command: string) => {
return `node "${escapedWorkerPath}" hook cursor ${command}`;
return `"${escapedBunPath}" "${escapedWorkerPath}" hook cursor ${command}`;
};
console.log(` Using Bun runtime: ${bunPath}`);
const hooksJson: CursorHooksJson = {
version: 1,
hooks: {
@@ -356,7 +395,7 @@ export async function installCursorHooks(_sourceDir: string, target: CursorInsta
Installation complete!
Hooks installed to: ${targetDir}/hooks.json
Using unified CLI: node worker-service.cjs hook cursor <command>
Using unified CLI: bun worker-service.cjs hook cursor <command>
Next steps:
1. Start claude-mem worker: claude-mem start
@@ -532,7 +571,7 @@ export function checkCursorHooksStatus(): number {
const firstCommand = hooksContent?.hooks?.beforeSubmitPrompt?.[0]?.command || '';
if (firstCommand.includes('worker-service.cjs') && firstCommand.includes('hook cursor')) {
console.log(` Mode: Unified CLI (node worker-service.cjs)`);
console.log(` Mode: Unified CLI (bun worker-service.cjs)`);
} else {
// Detect legacy shell scripts
const bashScripts = ['session-init.sh', 'context-inject.sh', 'save-observation.sh'];
@@ -596,8 +635,8 @@ export async function detectClaudeCode(): Promise<boolean> {
logger.debug('SYSTEM', 'Claude CLI not in PATH', {}, error as Error);
}
// Check for Claude Code plugin directory
const pluginDir = path.join(homedir(), '.claude', 'plugins');
// Check for Claude Code plugin directory (respects CLAUDE_CONFIG_DIR)
const pluginDir = path.join(CLAUDE_CONFIG_DIR, 'plugins');
if (existsSync(pluginDir)) {
return true;
}
-97
View File
@@ -1,97 +0,0 @@
<claude-mem-context>
# Recent Activity
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
### Dec 17, 2025
**ProcessManager.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #28932 | 7:30 PM | 🔵 | ProcessManager Architecture and Platform-Specific Process Spawning | ~523 |
| #28929 | " | 🔵 | ProcessManager Usage Across Codebase | ~319 |
| #28747 | 6:25 PM | 🔵 | Branch Diff Analysis - 26 Files Modified | ~374 |
| #28730 | 6:21 PM | 🔵 | Worker Wrapper Solves Windows Zombie Port Problem | ~416 |
| #28729 | " | 🔵 | Windows Worker Wrapper Architecture | ~222 |
| #28721 | 6:18 PM | 🔵 | Final Solution - Worker Wrapper Architecture Successfully Deployed | ~474 |
| #28719 | " | 🔵 | Initial Windows Worker Problem Analysis - Three Interconnected Issues | ~564 |
| #28714 | 6:15 PM | 🔴 | Windows Zombie Port Problem Resolved with Wrapper Process Architecture | ~421 |
| #28711 | 6:13 PM | 🔵 | Overview of Changes Between main and HEAD Branch | ~347 |
| #28660 | 5:31 PM | 🔵 | Branch Modifies 26 Files with Net Addition of 346 Lines | ~445 |
| #28644 | 5:24 PM | ✅ | Modified 27 files with 693 additions and 239 deletions for Windows support | ~447 |
### Dec 18, 2025
**ProcessManager.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #29622 | 5:41 PM | 🔵 | Validation Patterns Across HTTP Routes and Core Services | ~488 |
### Dec 20, 2025
**ProcessManager.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #31066 | 7:53 PM | 🔵 | Comprehensive KISS Principle Audit of Hooks and Worker Services | ~788 |
| #31020 | 7:28 PM | 🔄 | Inlined single-use timeout constants in ProcessManager | ~390 |
| #31013 | 7:27 PM | 🔵 | Comment Analysis Identified Stale FTS5 References and Documentation Gaps | ~681 |
| #31012 | 7:25 PM | 🔴 | Silent Failure Review Identified Regression in getWorkerPort() Error Handling | ~659 |
| #31010 | " | ⚖️ | PR #400 Approved After Comprehensive Code Review | ~594 |
| #31000 | 7:22 PM | 🔄 | ProcessManager timeout constants inlined to literal values | ~356 |
| #30993 | 7:20 PM | 🔴 | ProcessManager getPidInfo() enhanced with error logging | ~290 |
| #30990 | 7:19 PM | 🔵 | PR 400 achieves net deletion of 395 lines across 31 files | ~338 |
| #30988 | " | 🔵 | PR 400 modifies 31 files across hooks, services, and utilities | ~316 |
| #30986 | " | 🔵 | PR #400 File Scope: 31 Files Across Hooks, Services, and Utilities | ~526 |
| #30953 | 7:02 PM | 🔄 | Removed Single-Use Timeout Constants in ProcessManager | ~306 |
| #30949 | " | 🔴 | Fixed undefined constant in ProcessManager waitForExit | ~245 |
| #30948 | 7:01 PM | 🔵 | Windows Process Shutdown Strategy in ProcessManager | ~302 |
| #30907 | 6:46 PM | 🔴 | ProcessManager PID File Corruption Now Logs Warnings | ~326 |
| #30905 | 6:45 PM | 🔴 | ProcessManager getPidInfo Error Visibility | ~333 |
| #30902 | " | 🔴 | Added logging to PID file error handling in ProcessManager | ~260 |
| #30901 | 6:44 PM | 🔵 | Windows Graceful Shutdown via HTTP and Wrapper IPC | ~269 |
| #30900 | " | 🔵 | Platform-Specific Worker Script Selection | ~262 |
| #30899 | " | 🔵 | getPidInfo Usage Pattern in ProcessManager | ~206 |
| #30898 | " | 🔵 | ProcessManager PID File Management Implementation | ~249 |
| #30774 | 5:58 PM | 🔵 | ProcessManager Handles Cross-Platform Worker Lifecycle with Windows Workarounds | ~559 |
| #32307 | 5:56 PM | 🔵 | Worker Service Code Audit: 14 Issues Found Across Validation, Data Structures, and Complexity | ~793 |
| #30673 | 5:08 PM | 🔴 | Windows Worker Stop/Restart Reliability Improvements | ~376 |
| #30663 | 5:07 PM | 🔵 | Cross-Platform Support Across 12 Files | ~307 |
### Dec 24, 2025
**ProcessManager.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #32071 | 3:24 PM | ⚖️ | Worker Startup Architecture Redesigned | ~380 |
| #32070 | " | 🔵 | ProcessManager Worker Spawning Architecture | ~428 |
| #32059 | 3:17 PM | ⚖️ | Worker Startup Refactored with File-Based Locking for Concurrent Hooks | ~552 |
### Dec 26, 2025
**ProcessManager.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #32855 | 7:04 PM | 🔄 | Consolidated worker process management into single service | ~322 |
| #32837 | 6:25 PM | 🔵 | Deleted ProcessManager.ts contained comprehensive PID file infrastructure | ~430 |
| #32814 | 6:05 PM | ✅ | Increased All Timeout Limits to Maximum Values for Slow Systems | ~385 |
### Dec 28, 2025
**ProcessManager.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #33370 | 3:47 PM | 🔵 | ToxMox Wrapper Architecture Deleted December 26, Six Days After Implementation | ~506 |
| #33369 | 3:46 PM | 🔵 | ToxMox December 17 Commit Introduced Wrapper Architecture to Fix Windows Zombie Port Bug | ~632 |
| #33368 | 3:45 PM | 🔵 | ToxMox December 20 Commit Improved Windows Worker Restart Reliability and Logging | ~487 |
| #33294 | 3:08 PM | ✅ | ToxMox Contributions Documented in Comprehensive Markdown Report | ~603 |
| #33284 | 3:07 PM | 🔄 | Consolidated Worker Lifecycle Management (-580 Lines) | ~327 |
| #33270 | 2:59 PM | ⚖️ | Self-Spawn Pattern Chosen for Worker Lifecycle | ~418 |
### Jan 6, 2026
**ProcessManager.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #38108 | 12:15 AM | 🔵 | Complete Windows Zombie Port Bug Technical Deep Dive | ~935 |
| #38105 | 12:14 AM | 🔵 | Windows Console Popup Flash Issue Documented and Fixed | ~455 |
</claude-mem-context>
-7
View File
@@ -1,7 +0,0 @@
<claude-mem-context>
# Recent Activity
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
*No recent activity*
</claude-mem-context>
+58 -7
View File
@@ -3,6 +3,15 @@ import { PendingMessageStore, PersistentPendingMessage } from '../sqlite/Pending
import type { PendingMessageWithId } from '../worker-types.js';
import { logger } from '../../utils/logger.js';
const IDLE_TIMEOUT_MS = 3 * 60 * 1000; // 3 minutes
export interface CreateIteratorOptions {
sessionDbId: number;
signal: AbortSignal;
/** Called when idle timeout occurs - should trigger abort to kill subprocess */
onIdleTimeout?: () => void;
}
export class SessionQueueProcessor {
constructor(
private store: PendingMessageStore,
@@ -14,8 +23,15 @@ export class SessionQueueProcessor {
* Uses atomic claim-and-delete to prevent duplicates.
* The queue is a pure buffer: claim it, delete it, process in memory.
* Waits for 'message' event when queue is empty.
*
* CRITICAL: Calls onIdleTimeout callback after 3 minutes of inactivity.
* The callback should trigger abortController.abort() to kill the SDK subprocess.
* Just returning from the iterator is NOT enough - the subprocess stays alive!
*/
async *createIterator(sessionDbId: number, signal: AbortSignal): AsyncIterableIterator<PendingMessageWithId> {
async *createIterator(options: CreateIteratorOptions): AsyncIterableIterator<PendingMessageWithId> {
const { sessionDbId, signal, onIdleTimeout } = options;
let lastActivityTime = Date.now();
while (!signal.aborted) {
try {
// Atomically claim AND DELETE next message from DB
@@ -23,11 +39,29 @@ export class SessionQueueProcessor {
const persistentMessage = this.store.claimAndDelete(sessionDbId);
if (persistentMessage) {
// Reset activity time when we successfully yield a message
lastActivityTime = Date.now();
// Yield the message for processing (it's already deleted from queue)
yield this.toPendingMessageWithId(persistentMessage);
} else {
// Queue empty - wait for wake-up event
await this.waitForMessage(signal);
// Queue empty - wait for wake-up event or timeout
const receivedMessage = await this.waitForMessage(signal, IDLE_TIMEOUT_MS);
if (!receivedMessage && !signal.aborted) {
// Timeout occurred - check if we've been idle too long
const idleDuration = Date.now() - lastActivityTime;
if (idleDuration >= IDLE_TIMEOUT_MS) {
logger.info('SESSION', 'Idle timeout reached, triggering abort to kill subprocess', {
sessionDbId,
idleDurationMs: idleDuration,
thresholdMs: IDLE_TIMEOUT_MS
});
onIdleTimeout?.();
return;
}
// Reset timer on spurious wakeup - queue is empty but duration check failed
lastActivityTime = Date.now();
}
}
} catch (error) {
if (signal.aborted) return;
@@ -47,25 +81,42 @@ export class SessionQueueProcessor {
};
}
private waitForMessage(signal: AbortSignal): Promise<void> {
return new Promise<void>((resolve) => {
/**
* Wait for a message event or timeout.
* @param signal - AbortSignal to cancel waiting
* @param timeoutMs - Maximum time to wait before returning
* @returns true if a message was received, false if timeout occurred
*/
private waitForMessage(signal: AbortSignal, timeoutMs: number = IDLE_TIMEOUT_MS): Promise<boolean> {
return new Promise<boolean>((resolve) => {
let timeoutId: ReturnType<typeof setTimeout> | undefined;
const onMessage = () => {
cleanup();
resolve();
resolve(true); // Message received
};
const onAbort = () => {
cleanup();
resolve(); // Resolve to let the loop check signal.aborted and exit
resolve(false); // Aborted, let loop check signal.aborted
};
const onTimeout = () => {
cleanup();
resolve(false); // Timeout occurred
};
const cleanup = () => {
if (timeoutId !== undefined) {
clearTimeout(timeoutId);
}
this.events.off('message', onMessage);
signal.removeEventListener('abort', onAbort);
};
this.events.once('message', onMessage);
signal.addEventListener('abort', onAbort, { once: true });
timeoutId = setTimeout(onTimeout, timeoutMs);
});
}
}
-7
View File
@@ -1,7 +0,0 @@
<claude-mem-context>
# Recent Activity
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
*No recent activity*
</claude-mem-context>
+21 -4
View File
@@ -30,6 +30,19 @@ export interface RouteHandler {
setupRoutes(app: Application): void;
}
/**
* AI provider status for health endpoint
*/
export interface AiStatus {
provider: string;
authMethod: string;
lastInteraction: {
timestamp: number;
success: boolean;
error?: string;
} | null;
}
/**
* Options for initializing the server
*/
@@ -42,6 +55,10 @@ export interface ServerOptions {
onShutdown: () => Promise<void>;
/** Restart function for admin endpoints */
onRestart: () => Promise<void>;
/** Filesystem path to the worker entry point */
workerPath: string;
/** Callback to get current AI provider status */
getAiStatus: () => AiStatus;
}
/**
@@ -140,20 +157,20 @@ export class Server {
* Setup core system routes (health, readiness, version, admin)
*/
private setupCoreRoutes(): void {
// Test build ID for debugging which build is running
const TEST_BUILD_ID = 'TEST-008-wrapper-ipc';
// Health check endpoint - always responds, even during initialization
this.app.get('/api/health', (_req: Request, res: Response) => {
res.status(200).json({
status: 'ok',
build: TEST_BUILD_ID,
version: BUILT_IN_VERSION,
workerPath: this.options.workerPath,
uptime: Date.now() - this.startTime,
managed: process.env.CLAUDE_MEM_MANAGED === 'true',
hasIpc: typeof process.send === 'function',
platform: process.platform,
pid: process.pid,
initialized: this.options.getInitializationComplete(),
mcpReady: this.options.getMcpReady(),
ai: this.options.getAiStatus(),
});
});
+88 -2
View File
@@ -1,7 +1,93 @@
<claude-mem-context>
# Recent Activity
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
### Dec 8, 2025
*No recent activity*
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #22310 | 9:46 PM | 🟣 | Complete Hook Lifecycle Documentation Generated | ~603 |
| #22305 | 9:45 PM | 🔵 | Session Summary Storage and Status Lifecycle | ~472 |
| #22304 | " | 🔵 | Session Creation Idempotency and Observation Storage | ~481 |
| #22303 | " | 🔵 | SessionStore CRUD Operations for Hook Integration | ~392 |
| #22300 | 9:44 PM | 🔵 | SessionStore Database Management and Schema Migrations | ~455 |
| #22299 | " | 🔵 | Database Schema and Entity Types | ~460 |
| #21976 | 5:24 PM | 🟣 | storeObservation Saves tool_use_id to Database | ~298 |
### Dec 10, 2025
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #23808 | 10:42 PM | 🔵 | migrations.ts Already Migrated to bun:sqlite | ~312 |
| #23807 | " | 🔵 | SessionSearch.ts Already Migrated to bun:sqlite | ~321 |
| #23805 | " | 🔵 | Database.ts Already Migrated to bun:sqlite | ~290 |
| #23784 | 9:59 PM | ✅ | SessionStore.ts db.pragma() Converted to db.query().all() Pattern | ~198 |
| #23783 | 9:58 PM | ✅ | SessionStore.ts Migration004 Multi-Statement db.exec() Converted to db.run() | ~220 |
| #23782 | " | ✅ | SessionStore.ts initializeSchema() db.exec() Converted to db.run() | ~197 |
| #23781 | " | ✅ | SessionStore.ts Constructor PRAGMA Calls Converted to db.run() | ~215 |
| #23780 | " | ✅ | SessionStore.ts Type Annotation Updated | ~183 |
| #23779 | " | ✅ | SessionStore.ts Import Updated to bun:sqlite | ~237 |
| #23778 | 9:57 PM | ✅ | Database.ts Import Updated to bun:sqlite | ~177 |
| #23777 | " | 🔵 | SessionStore.ts Current Implementation - better-sqlite3 Import and API Usage | ~415 |
| #23776 | " | 🔵 | migrations.ts Current Implementation - better-sqlite3 Import | ~285 |
| #23775 | " | 🔵 | Database.ts Current Implementation - better-sqlite3 Import | ~286 |
| #23774 | " | 🔵 | SessionSearch.ts Current Implementation - better-sqlite3 Import | ~309 |
| #23671 | 8:36 PM | 🔵 | getUserPromptsByIds Method Implementation with Filtering and Ordering | ~326 |
| #23670 | " | 🔵 | getUserPromptsByIds Method Location in SessionStore | ~145 |
| #23635 | 8:10 PM | 🔴 | Fixed SessionStore.ts Concepts Filter SQL Parameter Bug | ~297 |
| #23634 | " | 🔵 | SessionStore.ts Concepts Filter Bug Confirmed at Line 849 | ~356 |
| #23522 | 5:27 PM | 🔵 | Complete TypeScript Type Definitions for Database Entities | ~433 |
| #23521 | " | 🔵 | Database Schema Structure with 7 Migration Versions | ~461 |
### Dec 18, 2025
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #29868 | 8:19 PM | 🔵 | SessionStore Architecture Review for Mode Metadata Addition | ~350 |
| #29243 | 12:13 AM | 🔵 | Observations Table Schema Migration: Text Field Made Nullable | ~496 |
| #29241 | 12:12 AM | 🔵 | Migration001: Core Schema for Sessions, Memories, Overviews, Diagnostics, Transcripts | ~555 |
| #29238 | 12:11 AM | 🔵 | Observation Type Schema Evolution: Five to Six Types | ~331 |
| #29237 | " | 🔵 | SQLite SessionStore with Schema Migrations and WAL Mode | ~520 |
### Dec 21, 2025
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #31622 | 8:26 PM | 🔄 | Completed SessionStore logging standardization | ~270 |
| #31621 | " | 🔄 | Standardized error logging for boundary timestamps query | ~253 |
| #31620 | " | 🔄 | Standardized error logging in getTimelineAroundObservation | ~252 |
| #31619 | " | 🔄 | Replaced console.log with logger.debug in SessionStore | ~263 |
### Dec 27, 2025
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #33213 | 9:04 PM | 🔵 | SessionStore Implements KISS Session ID Threading via INSERT OR IGNORE Pattern | ~673 |
### Dec 28, 2025
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #33548 | 10:59 PM | ✅ | Reverted memory_session_id NULL Initialization to contentSessionId Placeholder | ~421 |
| #33546 | 10:57 PM | 🔴 | Fixed createSDKSession to Initialize memory_session_id as NULL | ~406 |
| #33545 | " | 🔵 | createSDKSession Sets memory_session_id Equal to content_session_id Initially | ~378 |
| #33544 | " | 🔵 | SessionStore Migration 17 Already Renamed Session ID Columns | ~451 |
### Jan 2, 2026
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #36028 | 9:20 PM | 🔄 | Try-Catch Block Removed from Database Migration | ~291 |
### Jan 3, 2026
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #36653 | 11:03 PM | 🔵 | storeObservation Method Signature Shows Parameter Named memorySessionId | ~474 |
| #36652 | " | 🔵 | createSDKSession Implementation Confirms NULL Initialization With Security Rationale | ~488 |
| #36650 | 11:02 PM | 🔵 | Phase 1 Analysis Reveals Implementation-Test Mismatch on NULL vs Placeholder Initialization | ~687 |
| #36649 | " | 🔵 | SessionStore Implementation Reveals NULL-Based Memory Session ID Initialization Pattern | ~770 |
| #36175 | 6:52 PM | ✅ | MigrationRunner Re-exported from Migrations.ts | ~405 |
| #36172 | " | 🔵 | Migrations.ts Contains Legacy Migration System | ~650 |
| #36163 | 6:48 PM | 🔵 | SessionStore Method Inventory and Extraction Boundaries | ~692 |
| #36162 | 6:47 PM | 🔵 | SessionStore Architecture and Migration History | ~593 |
</claude-mem-context>
+62 -6
View File
@@ -77,12 +77,13 @@ export class PendingMessageStore {
}
/**
* Atomically claim and DELETE the next pending message.
* Finds oldest pending -> returns it -> deletes from queue.
* The queue is a pure buffer: claim it, delete it, process in memory.
* Atomically claim the next pending message by marking it as 'processing'.
* CRITICAL FIX: Does NOT delete - message stays in DB until confirmProcessed() is called.
* This prevents message loss if the generator crashes mid-processing.
* Uses a transaction to prevent race conditions.
*/
claimAndDelete(sessionDbId: number): PersistentPendingMessage | null {
const now = Date.now();
const claimTx = this.db.transaction((sessionId: number) => {
const peekStmt = this.db.prepare(`
SELECT * FROM pending_messages
@@ -93,9 +94,14 @@ export class PendingMessageStore {
const msg = peekStmt.get(sessionId) as PersistentPendingMessage | null;
if (msg) {
// Delete immediately - no "processing" state needed
const deleteStmt = this.db.prepare('DELETE FROM pending_messages WHERE id = ?');
deleteStmt.run(msg.id);
// CRITICAL FIX: Mark as 'processing' instead of deleting
// Message will be deleted by confirmProcessed() after successful store
const updateStmt = this.db.prepare(`
UPDATE pending_messages
SET status = 'processing', started_processing_at_epoch = ?
WHERE id = ?
`);
updateStmt.run(now, msg.id);
// Log claim with minimal info (avoid logging full payload)
logger.info('QUEUE', `CLAIMED | sessionDbId=${sessionId} | messageId=${msg.id} | type=${msg.message_type}`, {
@@ -108,6 +114,39 @@ export class PendingMessageStore {
return claimTx(sessionDbId) as PersistentPendingMessage | null;
}
/**
* Confirm a message was successfully processed - DELETE it from the queue.
* CRITICAL: Only call this AFTER the observation/summary has been stored to DB.
* This prevents message loss on generator crash.
*/
confirmProcessed(messageId: number): void {
const stmt = this.db.prepare('DELETE FROM pending_messages WHERE id = ?');
const result = stmt.run(messageId);
if (result.changes > 0) {
logger.debug('QUEUE', `CONFIRMED | messageId=${messageId} | deleted from queue`);
}
}
/**
* Reset stale 'processing' messages back to 'pending' for retry.
* Called on worker startup and periodically to recover from crashes.
* @param thresholdMs Messages processing longer than this are considered stale (default: 5 minutes)
* @returns Number of messages reset
*/
resetStaleProcessingMessages(thresholdMs: number = 5 * 60 * 1000): number {
const cutoff = Date.now() - thresholdMs;
const stmt = this.db.prepare(`
UPDATE pending_messages
SET status = 'pending', started_processing_at_epoch = NULL
WHERE status = 'processing' AND started_processing_at_epoch < ?
`);
const result = stmt.run(cutoff);
if (result.changes > 0) {
logger.info('QUEUE', `RESET_STALE | count=${result.changes} | thresholdMs=${thresholdMs}`);
}
return result.changes;
}
/**
* Get all pending messages for session (ordered by creation time)
*/
@@ -204,6 +243,23 @@ export class PendingMessageStore {
return result.changes;
}
/**
* Mark all pending and processing messages for a session as failed (abandoned).
* Used when SDK session is terminated and no fallback agent is available:
* prevents the session from appearing in getSessionsWithPendingMessages forever.
* @returns Number of messages marked failed
*/
markAllSessionMessagesAbandoned(sessionDbId: number): number {
const now = Date.now();
const stmt = this.db.prepare(`
UPDATE pending_messages
SET status = 'failed', failed_at_epoch = ?
WHERE session_db_id = ? AND status IN ('pending', 'processing')
`);
const result = stmt.run(now, sessionDbId);
return result.changes;
}
/**
* Abort a specific message (delete from queue)
*/
+268 -19
View File
@@ -47,6 +47,7 @@ export class SessionStore {
this.renameSessionIdColumns();
this.repairSessionIdColumnRename();
this.addFailedAtEpochColumn();
this.addOnUpdateCascadeToForeignKeys();
}
/**
@@ -98,10 +99,10 @@ export class SessionStore {
memory_session_id TEXT NOT NULL,
project TEXT NOT NULL,
text TEXT NOT NULL,
type TEXT NOT NULL CHECK(type IN ('decision', 'bugfix', 'feature', 'refactor', 'discovery')),
type TEXT NOT NULL,
created_at TEXT NOT NULL,
created_at_epoch INTEGER NOT NULL,
FOREIGN KEY(memory_session_id) REFERENCES sdk_sessions(memory_session_id) ON DELETE CASCADE
FOREIGN KEY(memory_session_id) REFERENCES sdk_sessions(memory_session_id) ON DELETE CASCADE ON UPDATE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_observations_sdk_session ON observations(memory_session_id);
@@ -123,7 +124,7 @@ export class SessionStore {
notes TEXT,
created_at TEXT NOT NULL,
created_at_epoch INTEGER NOT NULL,
FOREIGN KEY(memory_session_id) REFERENCES sdk_sessions(memory_session_id) ON DELETE CASCADE
FOREIGN KEY(memory_session_id) REFERENCES sdk_sessions(memory_session_id) ON DELETE CASCADE ON UPDATE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_session_summaries_sdk_session ON session_summaries(memory_session_id);
@@ -341,7 +342,7 @@ export class SessionStore {
memory_session_id TEXT NOT NULL,
project TEXT NOT NULL,
text TEXT,
type TEXT NOT NULL CHECK(type IN ('decision', 'bugfix', 'feature', 'refactor', 'discovery', 'change')),
type TEXT NOT NULL,
title TEXT,
subtitle TEXT,
facts TEXT,
@@ -645,11 +646,191 @@ export class SessionStore {
this.db.prepare('INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)').run(20, new Date().toISOString());
}
/**
* Add ON UPDATE CASCADE to FK constraints on observations and session_summaries (migration 21)
*
* Both tables have FK(memory_session_id) -> sdk_sessions(memory_session_id) with ON DELETE CASCADE
* but missing ON UPDATE CASCADE. This causes FK constraint violations when code updates
* sdk_sessions.memory_session_id while child rows still reference the old value.
*
* SQLite doesn't support ALTER TABLE for FK changes, so we recreate both tables.
*/
private addOnUpdateCascadeToForeignKeys(): void {
const applied = this.db.prepare('SELECT version FROM schema_versions WHERE version = ?').get(21) as SchemaVersion | undefined;
if (applied) return;
logger.debug('DB', 'Adding ON UPDATE CASCADE to FK constraints on observations and session_summaries');
// PRAGMA foreign_keys must be set outside a transaction
this.db.run('PRAGMA foreign_keys = OFF');
this.db.run('BEGIN TRANSACTION');
try {
// ==========================================
// 1. Recreate observations table
// ==========================================
// Drop FTS triggers first (they reference the observations table)
this.db.run('DROP TRIGGER IF EXISTS observations_ai');
this.db.run('DROP TRIGGER IF EXISTS observations_ad');
this.db.run('DROP TRIGGER IF EXISTS observations_au');
this.db.run(`
CREATE TABLE observations_new (
id INTEGER PRIMARY KEY AUTOINCREMENT,
memory_session_id TEXT NOT NULL,
project TEXT NOT NULL,
text TEXT,
type TEXT NOT NULL,
title TEXT,
subtitle TEXT,
facts TEXT,
narrative TEXT,
concepts TEXT,
files_read TEXT,
files_modified TEXT,
prompt_number INTEGER,
discovery_tokens INTEGER DEFAULT 0,
created_at TEXT NOT NULL,
created_at_epoch INTEGER NOT NULL,
FOREIGN KEY(memory_session_id) REFERENCES sdk_sessions(memory_session_id) ON DELETE CASCADE ON UPDATE CASCADE
)
`);
this.db.run(`
INSERT INTO observations_new
SELECT id, memory_session_id, project, text, type, title, subtitle, facts,
narrative, concepts, files_read, files_modified, prompt_number,
discovery_tokens, created_at, created_at_epoch
FROM observations
`);
this.db.run('DROP TABLE observations');
this.db.run('ALTER TABLE observations_new RENAME TO observations');
// Recreate indexes
this.db.run(`
CREATE INDEX idx_observations_sdk_session ON observations(memory_session_id);
CREATE INDEX idx_observations_project ON observations(project);
CREATE INDEX idx_observations_type ON observations(type);
CREATE INDEX idx_observations_created ON observations(created_at_epoch DESC);
`);
// Recreate FTS triggers only if observations_fts exists
// (SessionSearch.ensureFTSTables creates it on first use with IF NOT EXISTS)
const hasFTS = (this.db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='observations_fts'").all() as { name: string }[]).length > 0;
if (hasFTS) {
this.db.run(`
CREATE TRIGGER IF NOT EXISTS observations_ai AFTER INSERT ON observations BEGIN
INSERT INTO observations_fts(rowid, title, subtitle, narrative, text, facts, concepts)
VALUES (new.id, new.title, new.subtitle, new.narrative, new.text, new.facts, new.concepts);
END;
CREATE TRIGGER IF NOT EXISTS observations_ad AFTER DELETE ON observations BEGIN
INSERT INTO observations_fts(observations_fts, rowid, title, subtitle, narrative, text, facts, concepts)
VALUES('delete', old.id, old.title, old.subtitle, old.narrative, old.text, old.facts, old.concepts);
END;
CREATE TRIGGER IF NOT EXISTS observations_au AFTER UPDATE ON observations BEGIN
INSERT INTO observations_fts(observations_fts, rowid, title, subtitle, narrative, text, facts, concepts)
VALUES('delete', old.id, old.title, old.subtitle, old.narrative, old.text, old.facts, old.concepts);
INSERT INTO observations_fts(rowid, title, subtitle, narrative, text, facts, concepts)
VALUES (new.id, new.title, new.subtitle, new.narrative, new.text, new.facts, new.concepts);
END;
`);
}
// ==========================================
// 2. Recreate session_summaries table
// ==========================================
this.db.run(`
CREATE TABLE session_summaries_new (
id INTEGER PRIMARY KEY AUTOINCREMENT,
memory_session_id TEXT NOT NULL,
project TEXT NOT NULL,
request TEXT,
investigated TEXT,
learned TEXT,
completed TEXT,
next_steps TEXT,
files_read TEXT,
files_edited TEXT,
notes TEXT,
prompt_number INTEGER,
discovery_tokens INTEGER DEFAULT 0,
created_at TEXT NOT NULL,
created_at_epoch INTEGER NOT NULL,
FOREIGN KEY(memory_session_id) REFERENCES sdk_sessions(memory_session_id) ON DELETE CASCADE ON UPDATE CASCADE
)
`);
this.db.run(`
INSERT INTO session_summaries_new
SELECT id, memory_session_id, project, request, investigated, learned,
completed, next_steps, files_read, files_edited, notes,
prompt_number, discovery_tokens, created_at, created_at_epoch
FROM session_summaries
`);
// Drop session_summaries FTS triggers before dropping the table
this.db.run('DROP TRIGGER IF EXISTS session_summaries_ai');
this.db.run('DROP TRIGGER IF EXISTS session_summaries_ad');
this.db.run('DROP TRIGGER IF EXISTS session_summaries_au');
this.db.run('DROP TABLE session_summaries');
this.db.run('ALTER TABLE session_summaries_new RENAME TO session_summaries');
// Recreate indexes
this.db.run(`
CREATE INDEX idx_session_summaries_sdk_session ON session_summaries(memory_session_id);
CREATE INDEX idx_session_summaries_project ON session_summaries(project);
CREATE INDEX idx_session_summaries_created ON session_summaries(created_at_epoch DESC);
`);
// Recreate session_summaries FTS triggers if FTS table exists
const hasSummariesFTS = (this.db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='session_summaries_fts'").all() as { name: string }[]).length > 0;
if (hasSummariesFTS) {
this.db.run(`
CREATE TRIGGER IF NOT EXISTS session_summaries_ai AFTER INSERT ON session_summaries BEGIN
INSERT INTO session_summaries_fts(rowid, request, investigated, learned, completed, next_steps, notes)
VALUES (new.id, new.request, new.investigated, new.learned, new.completed, new.next_steps, new.notes);
END;
CREATE TRIGGER IF NOT EXISTS session_summaries_ad AFTER DELETE ON session_summaries BEGIN
INSERT INTO session_summaries_fts(session_summaries_fts, rowid, request, investigated, learned, completed, next_steps, notes)
VALUES('delete', old.id, old.request, old.investigated, old.learned, old.completed, old.next_steps, old.notes);
END;
CREATE TRIGGER IF NOT EXISTS session_summaries_au AFTER UPDATE ON session_summaries BEGIN
INSERT INTO session_summaries_fts(session_summaries_fts, rowid, request, investigated, learned, completed, next_steps, notes)
VALUES('delete', old.id, old.request, old.investigated, old.learned, old.completed, old.next_steps, old.notes);
INSERT INTO session_summaries_fts(rowid, request, investigated, learned, completed, next_steps, notes)
VALUES (new.id, new.request, new.investigated, new.learned, new.completed, new.next_steps, new.notes);
END;
`);
}
// Record migration
this.db.prepare('INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)').run(21, new Date().toISOString());
this.db.run('COMMIT');
this.db.run('PRAGMA foreign_keys = ON');
logger.debug('DB', 'Successfully added ON UPDATE CASCADE to FK constraints');
} catch (error) {
this.db.run('ROLLBACK');
this.db.run('PRAGMA foreign_keys = ON');
throw error;
}
}
/**
* Update the memory session ID for a session
* Called by SDKAgent when it captures the session ID from the first SDK message
* Also used to RESET to null on stale resume failures (worker-service.ts)
*/
updateMemorySessionId(sessionDbId: number, memorySessionId: string): void {
updateMemorySessionId(sessionDbId: number, memorySessionId: string | null): void {
this.db.prepare(`
UPDATE sdk_sessions
SET memory_session_id = ?
@@ -657,6 +838,37 @@ export class SessionStore {
`).run(memorySessionId, sessionDbId);
}
/**
* Ensures memory_session_id is registered in sdk_sessions before FK-constrained INSERT.
* This fixes Issue #846 where observations fail after worker restart because the
* SDK generates a new memory_session_id but it's not registered in the parent table
* before child records try to reference it.
*
* @param sessionDbId - The database ID of the session
* @param memorySessionId - The memory session ID to ensure is registered
*/
ensureMemorySessionIdRegistered(sessionDbId: number, memorySessionId: string): void {
const session = this.db.prepare(`
SELECT id, memory_session_id FROM sdk_sessions WHERE id = ?
`).get(sessionDbId) as { id: number; memory_session_id: string | null } | undefined;
if (!session) {
throw new Error(`Session ${sessionDbId} not found in sdk_sessions`);
}
if (session.memory_session_id !== memorySessionId) {
this.db.prepare(`
UPDATE sdk_sessions SET memory_session_id = ? WHERE id = ?
`).run(memorySessionId, sessionDbId);
logger.info('DB', 'Registered memory_session_id before storage (FK fix)', {
sessionDbId,
oldId: session.memory_session_id,
newId: memorySessionId
});
}
}
/**
* Get recent session summaries for a project
*/
@@ -1151,31 +1363,40 @@ export class SessionStore {
* - Prompt #2+: session_id exists INSERT ignored, fetch existing ID
* - Result: Same database ID returned for all prompts in conversation
*
* WHY THIS MATTERS:
* - NO "does session exist?" checks needed anywhere
* - NO risk of creating duplicate sessions
* - ALL hooks automatically connected via session_id
* - SAVE hook observations go to correct session (same session_id)
* - SDKAgent continuation prompt has correct context (same session_id)
*
* This is KISS in action: Trust the database UNIQUE constraint and
* INSERT OR IGNORE to handle both creation and lookup elegantly.
* Pure get-or-create: never modifies memory_session_id.
* Multi-terminal isolation is handled by ON UPDATE CASCADE at the schema level.
*/
createSDKSession(contentSessionId: string, project: string, userPrompt: string): number {
const now = new Date();
const nowEpoch = now.getTime();
// Pure INSERT OR IGNORE - no updates, no complexity
// Session reuse: Return existing session ID if already created for this contentSessionId.
const existing = this.db.prepare(`
SELECT id FROM sdk_sessions WHERE content_session_id = ?
`).get(contentSessionId) as { id: number } | undefined;
if (existing) {
// Backfill project if session was created by another hook with empty project
if (project) {
this.db.prepare(`
UPDATE sdk_sessions SET project = ?
WHERE content_session_id = ? AND (project IS NULL OR project = '')
`).run(project, contentSessionId);
}
return existing.id;
}
// New session - insert fresh row
// NOTE: memory_session_id starts as NULL. It is captured by SDKAgent from the first SDK
// response and stored via updateMemorySessionId(). CRITICAL: memory_session_id must NEVER
// equal contentSessionId - that would inject memory messages into the user's transcript!
// response and stored via ensureMemorySessionIdRegistered(). CRITICAL: memory_session_id
// must NEVER equal contentSessionId - that would inject memory messages into the user's transcript!
this.db.prepare(`
INSERT OR IGNORE INTO sdk_sessions
INSERT INTO sdk_sessions
(content_session_id, memory_session_id, project, user_prompt, started_at, started_at_epoch, status)
VALUES (?, NULL, ?, ?, ?, ?, 'active')
`).run(contentSessionId, project, userPrompt, now.toISOString(), nowEpoch);
// Return existing or new ID
// Return new ID
const row = this.db.prepare('SELECT id FROM sdk_sessions WHERE content_session_id = ?')
.get(contentSessionId) as { id: number };
return row.id;
@@ -1907,6 +2128,34 @@ export class SessionStore {
return stmt.get(id) || null;
}
/**
* Get or create a manual session for storing user-created observations
* Manual sessions use a predictable ID format: "manual-{project}"
*/
getOrCreateManualSession(project: string): string {
const memorySessionId = `manual-${project}`;
const contentSessionId = `manual-content-${project}`;
const existing = this.db.prepare(
'SELECT memory_session_id FROM sdk_sessions WHERE memory_session_id = ?'
).get(memorySessionId) as { memory_session_id: string } | undefined;
if (existing) {
return memorySessionId;
}
// Create new manual session
const now = new Date();
this.db.prepare(`
INSERT INTO sdk_sessions (memory_session_id, content_session_id, project, started_at, started_at_epoch, status)
VALUES (?, ?, ?, ?, ?, 'active')
`).run(memorySessionId, contentSessionId, project, now.toISOString(), now.getTime());
logger.info('SESSION', 'Created manual session', { memorySessionId, project });
return memorySessionId;
}
/**
* Close the database connection
*/
-14
View File
@@ -1,14 +0,0 @@
<claude-mem-context>
# Recent Activity
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
### Jan 3, 2026
**bulk.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #36670 | 11:37 PM | ✅ | Resolved merge conflicts by accepting branch changes for 39 files | ~435 |
| #36469 | 9:04 PM | 🔵 | Bulk Import with Duplicate Detection | ~451 |
| #36390 | 8:50 PM | 🔄 | Comprehensive Monolith Refactor with Modular Architecture | ~724 |
</claude-mem-context>
+1 -1
View File
@@ -259,7 +259,7 @@ export const migration004: Migration = {
memory_session_id TEXT NOT NULL,
project TEXT NOT NULL,
text TEXT NOT NULL,
type TEXT NOT NULL CHECK(type IN ('decision', 'bugfix', 'feature', 'refactor', 'discovery')),
type TEXT NOT NULL,
created_at TEXT NOT NULL,
created_at_epoch INTEGER NOT NULL,
FOREIGN KEY(memory_session_id) REFERENCES sdk_sessions(memory_session_id) ON DELETE CASCADE
-15
View File
@@ -1,15 +0,0 @@
<claude-mem-context>
# Recent Activity
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
### Jan 3, 2026
**runner.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #36487 | 9:13 PM | 🔴 | Fixed Foreign Key Constraint Issues in Observations Test Suite | ~677 |
| #36390 | 8:50 PM | 🔄 | Comprehensive Monolith Refactor with Modular Architecture | ~724 |
| #36353 | 8:42 PM | 🔵 | Multiple observation table definitions found across codebase | ~280 |
| #36323 | 8:25 PM | 🔵 | Message Queue Architecture Scope Expanded | ~302 |
</claude-mem-context>
+2 -2
View File
@@ -82,7 +82,7 @@ export class MigrationRunner {
memory_session_id TEXT NOT NULL,
project TEXT NOT NULL,
text TEXT NOT NULL,
type TEXT NOT NULL CHECK(type IN ('decision', 'bugfix', 'feature', 'refactor', 'discovery')),
type TEXT NOT NULL,
created_at TEXT NOT NULL,
created_at_epoch INTEGER NOT NULL,
FOREIGN KEY(memory_session_id) REFERENCES sdk_sessions(memory_session_id) ON DELETE CASCADE
@@ -325,7 +325,7 @@ export class MigrationRunner {
memory_session_id TEXT NOT NULL,
project TEXT NOT NULL,
text TEXT,
type TEXT NOT NULL CHECK(type IN ('decision', 'bugfix', 'feature', 'refactor', 'discovery', 'change')),
type TEXT NOT NULL,
title TEXT,
subtitle TEXT,
facts TEXT,
@@ -1,33 +0,0 @@
<claude-mem-context>
# Recent Activity
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
### Jan 3, 2026
**files.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #36670 | 11:37 PM | ✅ | Resolved merge conflicts by accepting branch changes for 39 files | ~435 |
| #36453 | 9:02 PM | 🔵 | Session File Aggregation | ~384 |
| #36390 | 8:50 PM | 🔄 | Comprehensive Monolith Refactor with Modular Architecture | ~724 |
**store.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #36483 | 9:11 PM | 🟣 | Observations Module Test Suite Implemented | ~716 |
| #36445 | 9:01 PM | 🔵 | Observation Storage with Timestamp Override | ~444 |
**types.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #36470 | 9:06 PM | 🔵 | SQLite Module API Documentation Verified for Test Implementation | ~765 |
| #36447 | 9:02 PM | 🔵 | Observation Type Definitions | ~459 |
### Jan 4, 2026
**types.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #36770 | 12:42 AM | 🔵 | Export Script Type Duplication Analysis Complete | ~555 |
</claude-mem-context>
-32
View File
@@ -1,32 +0,0 @@
<claude-mem-context>
# Recent Activity
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
### Jan 3, 2026
**get.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #36670 | 11:37 PM | ✅ | Resolved merge conflicts by accepting branch changes for 39 files | ~435 |
| #36464 | 9:04 PM | 🔵 | User Prompt Retrieval Functions | ~471 |
| #36390 | 8:50 PM | 🔄 | Comprehensive Monolith Refactor with Modular Architecture | ~724 |
**store.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #36485 | 9:12 PM | 🟣 | Prompts Module Test Suite Implemented | ~680 |
| #36466 | 9:04 PM | 🔵 | User Prompt Storage | ~363 |
**types.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #36470 | 9:06 PM | 🔵 | SQLite Module API Documentation Verified for Test Implementation | ~765 |
### Jan 4, 2026
**types.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #36770 | 12:42 AM | 🔵 | Export Script Type Duplication Analysis Complete | ~555 |
</claude-mem-context>
-7
View File
@@ -1,7 +0,0 @@
<claude-mem-context>
# Recent Activity
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
*No recent activity*
</claude-mem-context>
+25 -12
View File
@@ -14,12 +14,8 @@ import { logger } from '../../../utils/logger.js';
* - Prompt #2+: session_id exists -> INSERT ignored, fetch existing ID
* - Result: Same database ID returned for all prompts in conversation
*
* WHY THIS MATTERS:
* - NO "does session exist?" checks needed anywhere
* - NO risk of creating duplicate sessions
* - ALL hooks automatically connected via session_id
* - SAVE hook observations go to correct session (same session_id)
* - SDKAgent continuation prompt has correct context (same session_id)
* Pure get-or-create: never modifies memory_session_id.
* Multi-terminal isolation is handled by ON UPDATE CASCADE at the schema level.
*/
export function createSDKSession(
db: Database,
@@ -30,17 +26,33 @@ export function createSDKSession(
const now = new Date();
const nowEpoch = now.getTime();
// Pure INSERT OR IGNORE - no updates, no complexity
// Check for existing session
const existing = db.prepare(`
SELECT id FROM sdk_sessions WHERE content_session_id = ?
`).get(contentSessionId) as { id: number } | undefined;
if (existing) {
// Backfill project if session was created by another hook with empty project
if (project) {
db.prepare(`
UPDATE sdk_sessions SET project = ?
WHERE content_session_id = ? AND (project IS NULL OR project = '')
`).run(project, contentSessionId);
}
return existing.id;
}
// New session - insert fresh row
// NOTE: memory_session_id starts as NULL. It is captured by SDKAgent from the first SDK
// response and stored via updateMemorySessionId(). CRITICAL: memory_session_id must NEVER
// equal contentSessionId - that would inject memory messages into the user's transcript!
// response and stored via ensureMemorySessionIdRegistered(). CRITICAL: memory_session_id
// must NEVER equal contentSessionId - that would inject memory messages into the user's transcript!
db.prepare(`
INSERT OR IGNORE INTO sdk_sessions
INSERT INTO sdk_sessions
(content_session_id, memory_session_id, project, user_prompt, started_at, started_at_epoch, status)
VALUES (?, NULL, ?, ?, ?, ?, 'active')
`).run(contentSessionId, project, userPrompt, now.toISOString(), nowEpoch);
// Return existing or new ID
// Return new ID
const row = db.prepare('SELECT id FROM sdk_sessions WHERE content_session_id = ?')
.get(contentSessionId) as { id: number };
return row.id;
@@ -49,11 +61,12 @@ export function createSDKSession(
/**
* Update the memory session ID for a session
* Called by SDKAgent when it captures the session ID from the first SDK message
* Also used to RESET to null on stale resume failures (worker-service.ts)
*/
export function updateMemorySessionId(
db: Database,
sessionDbId: number,
memorySessionId: string
memorySessionId: string | null
): void {
db.prepare(`
UPDATE sdk_sessions
-32
View File
@@ -1,32 +0,0 @@
<claude-mem-context>
# Recent Activity
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
### Jan 3, 2026
**get.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #36670 | 11:37 PM | ✅ | Resolved merge conflicts by accepting branch changes for 39 files | ~435 |
| #36390 | 8:50 PM | 🔄 | Comprehensive Monolith Refactor with Modular Architecture | ~724 |
**store.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #36484 | 9:11 PM | 🟣 | Summaries Module Test Suite Implemented | ~708 |
| #36461 | 9:03 PM | 🔵 | Summary Storage with Timestamp Override | ~439 |
**types.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #36470 | 9:06 PM | 🔵 | SQLite Module API Documentation Verified for Test Implementation | ~765 |
| #36457 | 9:03 PM | 🔵 | Summary Type Hierarchy | ~426 |
### Jan 4, 2026
**types.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #36770 | 12:42 AM | 🔵 | Export Script Type Duplication Analysis Complete | ~555 |
</claude-mem-context>
-7
View File
@@ -1,7 +0,0 @@
<claude-mem-context>
# Recent Activity
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
*No recent activity*
</claude-mem-context>
-68
View File
@@ -1,68 +0,0 @@
<claude-mem-context>
# Recent Activity
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
### Nov 3, 2025
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #3465 | 6:26 PM | ⚖️ | PR preparation for hybrid search feature ready for submission | ~521 |
| #3460 | 6:18 PM | ✅ | Suppressed stderr output from Chroma MCP transport | ~231 |
| #3350 | 3:33 PM | ✅ | Document splitting strategy improves semantic search precision by vectorizing field-level content | ~701 |
| #3346 | " | 🟣 | ChromaSync service provides automatic real-time vector database synchronization | ~699 |
| #3345 | " | 🟣 | Completed ChromaDB hybrid search integration with semantic search across all content types | ~762 |
| #3323 | 3:01 PM | 🟣 | Integrated user prompt backfill into ChromaSync.backfill() | ~257 |
| #3322 | " | 🟣 | Implemented real-time user prompt sync to ChromaDB | ~275 |
| #3321 | " | ✅ | Added StoredUserPrompt interface to ChromaSync | ~179 |
### Nov 4, 2025
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #3645 | 3:03 PM | 🔵 | Observation Counter Removal Validated Safe for Chroma Integration | ~504 |
| #3643 | " | 🔵 | Chroma Document ID Structure and Granular Field Splitting | ~410 |
| #3642 | " | 🔵 | Observation Counter Independence from Chroma Import Process | ~440 |
### Nov 11, 2025
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #6992 | 6:28 PM | ⚖️ | Comprehensive Windows Issue Investigation and Fix Strategy | ~631 |
| #6986 | 6:26 PM | 🔵 | ChromaSync UVX Connection Configuration Analysis | ~333 |
| #6953 | 5:49 PM | 🔵 | ChromaSync Relies on uvx Python Package Runner Instead of npx | ~326 |
| #6952 | 5:48 PM | 🔵 | ChromaSync Uses uvx Command for MCP Server on All Platforms | ~368 |
### Dec 5, 2025
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #20401 | 7:18 PM | 🔵 | ChromaSync service synchronizes observations and summaries to vector database for semantic search | ~521 |
### Dec 13, 2025
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #25190 | 8:04 PM | 🔴 | Enhanced close() Method to Terminate Transport Subprocess | ~417 |
| #25189 | 8:03 PM | 🔄 | Store Transport Reference in ensureConnection Method | ~284 |
| #25188 | " | 🔄 | Added Transport Reference to ChromaSync Class | ~268 |
| #25187 | " | 🔵 | ChromaSync Has close() Method But May Not Be Called | ~277 |
| #25186 | " | 🔵 | ChromaSync Process Spawning via StdioClientTransport | ~355 |
| #25117 | 7:39 PM | 🟣 | Automatic Collection Migration for Embedding Function Changes | ~493 |
| #25116 | " | 🔄 | Collection Name Changed to Lazy Initialization | ~126 |
| #25115 | " | 🔵 | ChromaSync Service Current Implementation Analysis | ~454 |
| #25092 | 7:20 PM | 🟣 | ChromaSync Now Reads Embedding Function from Settings | ~394 |
| #25090 | 7:19 PM | 🔵 | Located Hardcoded Embedding Function in ChromaSync | ~345 |
### Dec 17, 2025
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #28547 | 4:49 PM | 🔴 | Fixed Windows subprocess zombie process issue in ChromaSync | ~368 |
| #28546 | " | ✅ | Added child_process import to ChromaSync | ~215 |
| #28545 | 4:48 PM | 🟣 | Subprocess PID Extraction for Windows Process Management | ~385 |
| #28544 | " | ✅ | Child Process PID Tracking Added to ChromaSync | ~239 |
| #28543 | " | 🔵 | ChromaSync Service Architecture | ~337 |
| #28542 | " | 🟣 | Windows Console Window Hiding for Chroma MCP Transport | ~308 |
| #28468 | 4:25 PM | 🔵 | ChromaSync Fail-Fast MCP Vector Database Integration | ~501 |
</claude-mem-context>
+65
View File
@@ -0,0 +1,65 @@
import { DEFAULT_CONFIG_PATH, DEFAULT_STATE_PATH, expandHomePath, loadTranscriptWatchConfig, writeSampleConfig } from './config.js';
import { TranscriptWatcher } from './watcher.js';
function getArgValue(args: string[], name: string): string | null {
const index = args.indexOf(name);
if (index === -1) return null;
return args[index + 1] ?? null;
}
export async function runTranscriptCommand(subcommand: string | undefined, args: string[]): Promise<number> {
switch (subcommand) {
case 'init': {
const configPath = getArgValue(args, '--config') ?? DEFAULT_CONFIG_PATH;
writeSampleConfig(configPath);
console.log(`Created sample config: ${expandHomePath(configPath)}`);
return 0;
}
case 'watch': {
const configPath = getArgValue(args, '--config') ?? DEFAULT_CONFIG_PATH;
let config;
try {
config = loadTranscriptWatchConfig(configPath);
} catch (error) {
if (error instanceof Error && error.message.includes('not found')) {
writeSampleConfig(configPath);
console.log(`Created sample config: ${expandHomePath(configPath)}`);
config = loadTranscriptWatchConfig(configPath);
} else {
throw error;
}
}
const statePath = expandHomePath(config.stateFile ?? DEFAULT_STATE_PATH);
const watcher = new TranscriptWatcher(config, statePath);
await watcher.start();
console.log('Transcript watcher running. Press Ctrl+C to stop.');
const shutdown = () => {
watcher.stop();
process.exit(0);
};
process.on('SIGINT', shutdown);
process.on('SIGTERM', shutdown);
return await new Promise(() => undefined);
}
case 'validate': {
const configPath = getArgValue(args, '--config') ?? DEFAULT_CONFIG_PATH;
try {
loadTranscriptWatchConfig(configPath);
} catch (error) {
if (error instanceof Error && error.message.includes('not found')) {
writeSampleConfig(configPath);
console.log(`Created sample config: ${expandHomePath(configPath)}`);
loadTranscriptWatchConfig(configPath);
} else {
throw error;
}
}
console.log(`Config OK: ${expandHomePath(configPath)}`);
return 0;
}
default:
console.log('Usage: claude-mem transcript <init|watch|validate> [--config <path>]');
return 1;
}
}
+137
View File
@@ -0,0 +1,137 @@
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
import { homedir } from 'os';
import { join, dirname } from 'path';
import type { TranscriptSchema, TranscriptWatchConfig } from './types.js';
export const DEFAULT_CONFIG_PATH = join(homedir(), '.claude-mem', 'transcript-watch.json');
export const DEFAULT_STATE_PATH = join(homedir(), '.claude-mem', 'transcript-watch-state.json');
const CODEX_SAMPLE_SCHEMA: TranscriptSchema = {
name: 'codex',
version: '0.2',
description: 'Schema for Codex session JSONL files under ~/.codex/sessions.',
events: [
{
name: 'session-meta',
match: { path: 'type', equals: 'session_meta' },
action: 'session_context',
fields: {
sessionId: 'payload.id',
cwd: 'payload.cwd'
}
},
{
name: 'turn-context',
match: { path: 'type', equals: 'turn_context' },
action: 'session_context',
fields: {
cwd: 'payload.cwd'
}
},
{
name: 'user-message',
match: { path: 'payload.type', equals: 'user_message' },
action: 'session_init',
fields: {
prompt: 'payload.message'
}
},
{
name: 'assistant-message',
match: { path: 'payload.type', equals: 'agent_message' },
action: 'assistant_message',
fields: {
message: 'payload.message'
}
},
{
name: 'tool-use',
match: { path: 'payload.type', in: ['function_call', 'custom_tool_call', 'web_search_call'] },
action: 'tool_use',
fields: {
toolId: 'payload.call_id',
toolName: {
coalesce: [
'payload.name',
{ value: 'web_search' }
]
},
toolInput: {
coalesce: [
'payload.arguments',
'payload.input',
'payload.action'
]
}
}
},
{
name: 'tool-result',
match: { path: 'payload.type', in: ['function_call_output', 'custom_tool_call_output'] },
action: 'tool_result',
fields: {
toolId: 'payload.call_id',
toolResponse: 'payload.output'
}
},
{
name: 'session-end',
match: { path: 'payload.type', equals: 'turn_aborted' },
action: 'session_end'
}
]
};
export const SAMPLE_CONFIG: TranscriptWatchConfig = {
version: 1,
schemas: {
codex: CODEX_SAMPLE_SCHEMA
},
watches: [
{
name: 'codex',
path: '~/.codex/sessions/**/*.jsonl',
schema: 'codex',
startAtEnd: true,
context: {
mode: 'agents',
path: '~/.codex/AGENTS.md',
updateOn: ['session_start', 'session_end']
}
}
],
stateFile: DEFAULT_STATE_PATH
};
export function expandHomePath(inputPath: string): string {
if (!inputPath) return inputPath;
if (inputPath.startsWith('~')) {
return join(homedir(), inputPath.slice(1));
}
return inputPath;
}
export function loadTranscriptWatchConfig(path = DEFAULT_CONFIG_PATH): TranscriptWatchConfig {
const resolvedPath = expandHomePath(path);
if (!existsSync(resolvedPath)) {
throw new Error(`Transcript watch config not found: ${resolvedPath}`);
}
const raw = readFileSync(resolvedPath, 'utf-8');
const parsed = JSON.parse(raw) as TranscriptWatchConfig;
if (!parsed.version || !parsed.watches) {
throw new Error(`Invalid transcript watch config: ${resolvedPath}`);
}
if (!parsed.stateFile) {
parsed.stateFile = DEFAULT_STATE_PATH;
}
return parsed;
}
export function writeSampleConfig(path = DEFAULT_CONFIG_PATH): void {
const resolvedPath = expandHomePath(path);
const dir = dirname(resolvedPath);
if (!existsSync(dir)) {
mkdirSync(dir, { recursive: true });
}
writeFileSync(resolvedPath, JSON.stringify(SAMPLE_CONFIG, null, 2));
}
+151
View File
@@ -0,0 +1,151 @@
import type { FieldSpec, MatchRule, TranscriptSchema, WatchTarget } from './types.js';
interface ResolveContext {
watch: WatchTarget;
schema: TranscriptSchema;
session?: Record<string, unknown>;
}
function parsePath(path: string): Array<string | number> {
const cleaned = path.trim().replace(/^\$\.?/, '');
if (!cleaned) return [];
const tokens: Array<string | number> = [];
const parts = cleaned.split('.');
for (const part of parts) {
const regex = /([^[\]]+)|\[(\d+)\]/g;
let match: RegExpExecArray | null;
while ((match = regex.exec(part)) !== null) {
if (match[1]) {
tokens.push(match[1]);
} else if (match[2]) {
tokens.push(parseInt(match[2], 10));
}
}
}
return tokens;
}
export function getValueByPath(input: unknown, path: string): unknown {
if (!path) return undefined;
const tokens = parsePath(path);
let current: any = input;
for (const token of tokens) {
if (current === null || current === undefined) return undefined;
current = current[token as any];
}
return current;
}
function isEmptyValue(value: unknown): boolean {
return value === undefined || value === null || value === '';
}
function resolveFromContext(path: string, ctx: ResolveContext): unknown {
if (path.startsWith('$watch.')) {
const key = path.slice('$watch.'.length);
return (ctx.watch as any)[key];
}
if (path.startsWith('$schema.')) {
const key = path.slice('$schema.'.length);
return (ctx.schema as any)[key];
}
if (path.startsWith('$session.')) {
const key = path.slice('$session.'.length);
return ctx.session ? (ctx.session as any)[key] : undefined;
}
if (path === '$cwd') return ctx.watch.workspace;
if (path === '$project') return ctx.watch.project;
return undefined;
}
export function resolveFieldSpec(
spec: FieldSpec | undefined,
entry: unknown,
ctx: ResolveContext
): unknown {
if (spec === undefined) return undefined;
if (typeof spec === 'string') {
const fromContext = resolveFromContext(spec, ctx);
if (fromContext !== undefined) return fromContext;
return getValueByPath(entry, spec);
}
if (spec.coalesce && Array.isArray(spec.coalesce)) {
for (const candidate of spec.coalesce) {
const value = resolveFieldSpec(candidate, entry, ctx);
if (!isEmptyValue(value)) return value;
}
}
if (spec.path) {
const fromContext = resolveFromContext(spec.path, ctx);
if (fromContext !== undefined) return fromContext;
const value = getValueByPath(entry, spec.path);
if (!isEmptyValue(value)) return value;
}
if (spec.value !== undefined) return spec.value;
if (spec.default !== undefined) return spec.default;
return undefined;
}
export function resolveFields(
fields: Record<string, FieldSpec> | undefined,
entry: unknown,
ctx: ResolveContext
): Record<string, unknown> {
const resolved: Record<string, unknown> = {};
if (!fields) return resolved;
for (const [key, spec] of Object.entries(fields)) {
resolved[key] = resolveFieldSpec(spec, entry, ctx);
}
return resolved;
}
export function matchesRule(
entry: unknown,
rule: MatchRule | undefined,
schema: TranscriptSchema
): boolean {
if (!rule) return true;
const path = rule.path || schema.eventTypePath || 'type';
const value = path ? getValueByPath(entry, path) : undefined;
if (rule.exists) {
if (value === undefined || value === null || value === '') return false;
}
if (rule.equals !== undefined) {
return value === rule.equals;
}
if (rule.in && Array.isArray(rule.in)) {
return rule.in.includes(value);
}
if (rule.contains !== undefined) {
return typeof value === 'string' && value.includes(rule.contains);
}
if (rule.regex) {
try {
const regex = new RegExp(rule.regex);
return regex.test(String(value ?? ''));
} catch {
return false;
}
}
return true;
}
+371
View File
@@ -0,0 +1,371 @@
import { sessionInitHandler } from '../../cli/handlers/session-init.js';
import { observationHandler } from '../../cli/handlers/observation.js';
import { fileEditHandler } from '../../cli/handlers/file-edit.js';
import { sessionCompleteHandler } from '../../cli/handlers/session-complete.js';
import { ensureWorkerRunning, getWorkerPort } from '../../shared/worker-utils.js';
import { logger } from '../../utils/logger.js';
import { getProjectContext, getProjectName } from '../../utils/project-name.js';
import { writeAgentsMd } from '../../utils/agents-md-utils.js';
import { resolveFieldSpec, resolveFields, matchesRule } from './field-utils.js';
import { expandHomePath } from './config.js';
import type { TranscriptSchema, WatchTarget, SchemaEvent } from './types.js';
interface SessionState {
sessionId: string;
cwd?: string;
project?: string;
lastUserMessage?: string;
lastAssistantMessage?: string;
pendingTools: Map<string, { name?: string; input?: unknown }>;
}
interface PendingTool {
id?: string;
name?: string;
input?: unknown;
response?: unknown;
}
export class TranscriptEventProcessor {
private sessions = new Map<string, SessionState>();
async processEntry(
entry: unknown,
watch: WatchTarget,
schema: TranscriptSchema,
sessionIdOverride?: string | null
): Promise<void> {
for (const event of schema.events) {
if (!matchesRule(entry, event.match, schema)) continue;
await this.handleEvent(entry, watch, schema, event, sessionIdOverride ?? undefined);
}
}
private getSessionKey(watch: WatchTarget, sessionId: string): string {
return `${watch.name}:${sessionId}`;
}
private getOrCreateSession(watch: WatchTarget, sessionId: string): SessionState {
const key = this.getSessionKey(watch, sessionId);
let session = this.sessions.get(key);
if (!session) {
session = {
sessionId,
pendingTools: new Map()
};
this.sessions.set(key, session);
}
return session;
}
private resolveSessionId(
entry: unknown,
watch: WatchTarget,
schema: TranscriptSchema,
event: SchemaEvent,
sessionIdOverride?: string
): string | null {
const ctx = { watch, schema } as any;
const fieldSpec = event.fields?.sessionId ?? (schema.sessionIdPath ? { path: schema.sessionIdPath } : undefined);
const resolved = resolveFieldSpec(fieldSpec, entry, ctx);
if (typeof resolved === 'string' && resolved.trim()) return resolved;
if (typeof resolved === 'number') return String(resolved);
if (sessionIdOverride && sessionIdOverride.trim()) return sessionIdOverride;
return null;
}
private resolveCwd(
entry: unknown,
watch: WatchTarget,
schema: TranscriptSchema,
event: SchemaEvent,
session: SessionState
): string | undefined {
const ctx = { watch, schema, session } as any;
const fieldSpec = event.fields?.cwd ?? (schema.cwdPath ? { path: schema.cwdPath } : undefined);
const resolved = resolveFieldSpec(fieldSpec, entry, ctx);
if (typeof resolved === 'string' && resolved.trim()) return resolved;
if (watch.workspace) return watch.workspace;
return session.cwd;
}
private resolveProject(
entry: unknown,
watch: WatchTarget,
schema: TranscriptSchema,
event: SchemaEvent,
session: SessionState
): string | undefined {
const ctx = { watch, schema, session } as any;
const fieldSpec = event.fields?.project ?? (schema.projectPath ? { path: schema.projectPath } : undefined);
const resolved = resolveFieldSpec(fieldSpec, entry, ctx);
if (typeof resolved === 'string' && resolved.trim()) return resolved;
if (watch.project) return watch.project;
if (session.cwd) return getProjectName(session.cwd);
return session.project;
}
private async handleEvent(
entry: unknown,
watch: WatchTarget,
schema: TranscriptSchema,
event: SchemaEvent,
sessionIdOverride?: string
): Promise<void> {
const sessionId = this.resolveSessionId(entry, watch, schema, event, sessionIdOverride);
if (!sessionId) {
logger.debug('TRANSCRIPT', 'Skipping event without sessionId', { event: event.name, watch: watch.name });
return;
}
const session = this.getOrCreateSession(watch, sessionId);
const cwd = this.resolveCwd(entry, watch, schema, event, session);
if (cwd) session.cwd = cwd;
const project = this.resolveProject(entry, watch, schema, event, session);
if (project) session.project = project;
const fields = resolveFields(event.fields, entry, { watch, schema, session });
switch (event.action) {
case 'session_context':
this.applySessionContext(session, fields);
break;
case 'session_init':
await this.handleSessionInit(session, fields);
if (watch.context?.updateOn?.includes('session_start')) {
await this.updateContext(session, watch);
}
break;
case 'user_message':
if (typeof fields.message === 'string') session.lastUserMessage = fields.message;
if (typeof fields.prompt === 'string') session.lastUserMessage = fields.prompt;
break;
case 'assistant_message':
if (typeof fields.message === 'string') session.lastAssistantMessage = fields.message;
break;
case 'tool_use':
await this.handleToolUse(session, fields);
break;
case 'tool_result':
await this.handleToolResult(session, fields);
break;
case 'observation':
await this.sendObservation(session, fields);
break;
case 'file_edit':
await this.sendFileEdit(session, fields);
break;
case 'session_end':
await this.handleSessionEnd(session, watch);
break;
default:
break;
}
}
private applySessionContext(session: SessionState, fields: Record<string, unknown>): void {
const cwd = typeof fields.cwd === 'string' ? fields.cwd : undefined;
const project = typeof fields.project === 'string' ? fields.project : undefined;
if (cwd) session.cwd = cwd;
if (project) session.project = project;
}
private async handleSessionInit(session: SessionState, fields: Record<string, unknown>): Promise<void> {
const prompt = typeof fields.prompt === 'string' ? fields.prompt : '';
const cwd = session.cwd ?? process.cwd();
if (prompt) {
session.lastUserMessage = prompt;
}
await sessionInitHandler.execute({
sessionId: session.sessionId,
cwd,
prompt,
platform: 'transcript'
});
}
private async handleToolUse(session: SessionState, fields: Record<string, unknown>): Promise<void> {
const toolId = typeof fields.toolId === 'string' ? fields.toolId : undefined;
const toolName = typeof fields.toolName === 'string' ? fields.toolName : undefined;
const toolInput = this.maybeParseJson(fields.toolInput);
const toolResponse = this.maybeParseJson(fields.toolResponse);
const pending: PendingTool = { id: toolId, name: toolName, input: toolInput, response: toolResponse };
if (toolId) {
session.pendingTools.set(toolId, { name: pending.name, input: pending.input });
}
if (toolName === 'apply_patch' && typeof toolInput === 'string') {
const files = this.parseApplyPatchFiles(toolInput);
for (const filePath of files) {
await this.sendFileEdit(session, {
filePath,
edits: [{ type: 'apply_patch', patch: toolInput }]
});
}
}
if (toolResponse !== undefined && toolName) {
await this.sendObservation(session, {
toolName,
toolInput,
toolResponse
});
}
}
private async handleToolResult(session: SessionState, fields: Record<string, unknown>): Promise<void> {
const toolId = typeof fields.toolId === 'string' ? fields.toolId : undefined;
const toolName = typeof fields.toolName === 'string' ? fields.toolName : undefined;
const toolResponse = this.maybeParseJson(fields.toolResponse);
let toolInput: unknown = this.maybeParseJson(fields.toolInput);
let name = toolName;
if (toolId && session.pendingTools.has(toolId)) {
const pending = session.pendingTools.get(toolId)!;
toolInput = pending.input ?? toolInput;
name = name ?? pending.name;
session.pendingTools.delete(toolId);
}
if (name) {
await this.sendObservation(session, {
toolName: name,
toolInput,
toolResponse
});
}
}
private async sendObservation(session: SessionState, fields: Record<string, unknown>): Promise<void> {
const toolName = typeof fields.toolName === 'string' ? fields.toolName : undefined;
if (!toolName) return;
await observationHandler.execute({
sessionId: session.sessionId,
cwd: session.cwd ?? process.cwd(),
toolName,
toolInput: this.maybeParseJson(fields.toolInput),
toolResponse: this.maybeParseJson(fields.toolResponse),
platform: 'transcript'
});
}
private async sendFileEdit(session: SessionState, fields: Record<string, unknown>): Promise<void> {
const filePath = typeof fields.filePath === 'string' ? fields.filePath : undefined;
if (!filePath) return;
await fileEditHandler.execute({
sessionId: session.sessionId,
cwd: session.cwd ?? process.cwd(),
filePath,
edits: Array.isArray(fields.edits) ? fields.edits : undefined,
platform: 'transcript'
});
}
private maybeParseJson(value: unknown): unknown {
if (typeof value !== 'string') return value;
const trimmed = value.trim();
if (!trimmed) return value;
if (!(trimmed.startsWith('{') || trimmed.startsWith('['))) return value;
try {
return JSON.parse(trimmed);
} catch {
return value;
}
}
private parseApplyPatchFiles(patch: string): string[] {
const files: string[] = [];
const lines = patch.split('\n');
for (const line of lines) {
const trimmed = line.trim();
if (trimmed.startsWith('*** Update File: ')) {
files.push(trimmed.replace('*** Update File: ', '').trim());
} else if (trimmed.startsWith('*** Add File: ')) {
files.push(trimmed.replace('*** Add File: ', '').trim());
} else if (trimmed.startsWith('*** Delete File: ')) {
files.push(trimmed.replace('*** Delete File: ', '').trim());
} else if (trimmed.startsWith('*** Move to: ')) {
files.push(trimmed.replace('*** Move to: ', '').trim());
} else if (trimmed.startsWith('+++ ')) {
const path = trimmed.replace('+++ ', '').replace(/^b\//, '').trim();
if (path && path !== '/dev/null') files.push(path);
}
}
return Array.from(new Set(files));
}
private async handleSessionEnd(session: SessionState, watch: WatchTarget): Promise<void> {
await this.queueSummary(session);
await sessionCompleteHandler.execute({
sessionId: session.sessionId,
cwd: session.cwd ?? process.cwd(),
platform: 'transcript'
});
await this.updateContext(session, watch);
session.pendingTools.clear();
const key = this.getSessionKey(watch, session.sessionId);
this.sessions.delete(key);
}
private async queueSummary(session: SessionState): Promise<void> {
const workerReady = await ensureWorkerRunning();
if (!workerReady) return;
const port = getWorkerPort();
const lastAssistantMessage = session.lastAssistantMessage ?? '';
try {
await fetch(`http://127.0.0.1:${port}/api/sessions/summarize`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
contentSessionId: session.sessionId,
last_assistant_message: lastAssistantMessage
})
});
} catch (error) {
logger.warn('TRANSCRIPT', 'Summary request failed', {
error: error instanceof Error ? error.message : String(error)
});
}
}
private async updateContext(session: SessionState, watch: WatchTarget): Promise<void> {
if (!watch.context) return;
if (watch.context.mode !== 'agents') return;
const workerReady = await ensureWorkerRunning();
if (!workerReady) return;
const cwd = session.cwd ?? watch.workspace;
if (!cwd) return;
const context = getProjectContext(cwd);
const projectsParam = context.allProjects.join(',');
const port = getWorkerPort();
try {
const response = await fetch(
`http://127.0.0.1:${port}/api/context/inject?projects=${encodeURIComponent(projectsParam)}`
);
if (!response.ok) return;
const content = (await response.text()).trim();
if (!content) return;
const agentsPath = expandHomePath(watch.context.path ?? `${cwd}/AGENTS.md`);
writeAgentsMd(agentsPath, content);
logger.debug('TRANSCRIPT', 'Updated AGENTS.md context', { agentsPath, watch: watch.name });
} catch (error) {
logger.warn('TRANSCRIPT', 'Failed to update AGENTS.md context', {
error: error instanceof Error ? error.message : String(error)
});
}
}
}
+40
View File
@@ -0,0 +1,40 @@
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
import { dirname } from 'path';
import { logger } from '../../utils/logger.js';
export interface TranscriptWatchState {
offsets: Record<string, number>;
}
export function loadWatchState(statePath: string): TranscriptWatchState {
try {
if (!existsSync(statePath)) {
return { offsets: {} };
}
const raw = readFileSync(statePath, 'utf-8');
const parsed = JSON.parse(raw) as TranscriptWatchState;
if (!parsed.offsets) return { offsets: {} };
return parsed;
} catch (error) {
logger.warn('TRANSCRIPT', 'Failed to load watch state, starting fresh', {
statePath,
error: error instanceof Error ? error.message : String(error)
});
return { offsets: {} };
}
}
export function saveWatchState(statePath: string, state: TranscriptWatchState): void {
try {
const dir = dirname(statePath);
if (!existsSync(dir)) {
mkdirSync(dir, { recursive: true });
}
writeFileSync(statePath, JSON.stringify(state, null, 2));
} catch (error) {
logger.warn('TRANSCRIPT', 'Failed to save watch state', {
statePath,
error: error instanceof Error ? error.message : String(error)
});
}
}
+70
View File
@@ -0,0 +1,70 @@
export type FieldSpec =
| string
| {
path?: string;
value?: unknown;
coalesce?: FieldSpec[];
default?: unknown;
};
export interface MatchRule {
path?: string;
equals?: unknown;
in?: unknown[];
contains?: string;
exists?: boolean;
regex?: string;
}
export type EventAction =
| 'session_init'
| 'session_context'
| 'user_message'
| 'assistant_message'
| 'tool_use'
| 'tool_result'
| 'observation'
| 'file_edit'
| 'session_end';
export interface SchemaEvent {
name: string;
match?: MatchRule;
action: EventAction;
fields?: Record<string, FieldSpec>;
}
export interface TranscriptSchema {
name: string;
version?: string;
description?: string;
eventTypePath?: string;
sessionIdPath?: string;
cwdPath?: string;
projectPath?: string;
events: SchemaEvent[];
}
export interface WatchContextConfig {
mode: 'agents';
path?: string;
updateOn?: Array<'session_start' | 'session_end'>;
}
export interface WatchTarget {
name: string;
path: string;
schema: string | TranscriptSchema;
workspace?: string;
project?: string;
context?: WatchContextConfig;
rescanIntervalMs?: number;
startAtEnd?: boolean;
}
export interface TranscriptWatchConfig {
version: 1;
schemas?: Record<string, TranscriptSchema>;
watches: WatchTarget[];
stateFile?: string;
}
+224
View File
@@ -0,0 +1,224 @@
import { existsSync, statSync, watch as fsWatch, createReadStream } from 'fs';
import { basename, join } from 'path';
import { globSync } from 'glob';
import { logger } from '../../utils/logger.js';
import { expandHomePath } from './config.js';
import { loadWatchState, saveWatchState, type TranscriptWatchState } from './state.js';
import type { TranscriptWatchConfig, TranscriptSchema, WatchTarget } from './types.js';
import { TranscriptEventProcessor } from './processor.js';
interface TailState {
offset: number;
partial: string;
}
class FileTailer {
private watcher: ReturnType<typeof fsWatch> | null = null;
private tailState: TailState;
constructor(
private filePath: string,
initialOffset: number,
private onLine: (line: string) => Promise<void>,
private onOffset: (offset: number) => void
) {
this.tailState = { offset: initialOffset, partial: '' };
}
start(): void {
this.readNewData().catch(() => undefined);
this.watcher = fsWatch(this.filePath, { persistent: true }, () => {
this.readNewData().catch(() => undefined);
});
}
close(): void {
this.watcher?.close();
this.watcher = null;
}
private async readNewData(): Promise<void> {
if (!existsSync(this.filePath)) return;
let size = 0;
try {
size = statSync(this.filePath).size;
} catch {
return;
}
if (size < this.tailState.offset) {
this.tailState.offset = 0;
}
if (size === this.tailState.offset) return;
const stream = createReadStream(this.filePath, {
start: this.tailState.offset,
end: size - 1,
encoding: 'utf8'
});
let data = '';
for await (const chunk of stream) {
data += chunk as string;
}
this.tailState.offset = size;
this.onOffset(this.tailState.offset);
const combined = this.tailState.partial + data;
const lines = combined.split('\n');
this.tailState.partial = lines.pop() ?? '';
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed) continue;
await this.onLine(trimmed);
}
}
}
export class TranscriptWatcher {
private processor = new TranscriptEventProcessor();
private tailers = new Map<string, FileTailer>();
private state: TranscriptWatchState;
private rescanTimers: Array<NodeJS.Timeout> = [];
constructor(private config: TranscriptWatchConfig, private statePath: string) {
this.state = loadWatchState(statePath);
}
async start(): Promise<void> {
for (const watch of this.config.watches) {
await this.setupWatch(watch);
}
}
stop(): void {
for (const tailer of this.tailers.values()) {
tailer.close();
}
this.tailers.clear();
for (const timer of this.rescanTimers) {
clearInterval(timer);
}
this.rescanTimers = [];
}
private async setupWatch(watch: WatchTarget): Promise<void> {
const schema = this.resolveSchema(watch);
if (!schema) {
logger.warn('TRANSCRIPT', 'Missing schema for watch', { watch: watch.name });
return;
}
const resolvedPath = expandHomePath(watch.path);
const files = this.resolveWatchFiles(resolvedPath);
for (const filePath of files) {
await this.addTailer(filePath, watch, schema);
}
const rescanIntervalMs = watch.rescanIntervalMs ?? 5000;
const timer = setInterval(async () => {
const newFiles = this.resolveWatchFiles(resolvedPath);
for (const filePath of newFiles) {
if (!this.tailers.has(filePath)) {
await this.addTailer(filePath, watch, schema);
}
}
}, rescanIntervalMs);
this.rescanTimers.push(timer);
}
private resolveSchema(watch: WatchTarget): TranscriptSchema | null {
if (typeof watch.schema === 'string') {
return this.config.schemas?.[watch.schema] ?? null;
}
return watch.schema;
}
private resolveWatchFiles(inputPath: string): string[] {
if (this.hasGlob(inputPath)) {
return globSync(inputPath, { nodir: true, absolute: true });
}
if (existsSync(inputPath)) {
try {
const stat = statSync(inputPath);
if (stat.isDirectory()) {
const pattern = join(inputPath, '**', '*.jsonl');
return globSync(pattern, { nodir: true, absolute: true });
}
return [inputPath];
} catch {
return [];
}
}
return [];
}
private hasGlob(inputPath: string): boolean {
return /[*?[\]{}()]/.test(inputPath);
}
private async addTailer(filePath: string, watch: WatchTarget, schema: TranscriptSchema): Promise<void> {
if (this.tailers.has(filePath)) return;
const sessionIdOverride = this.extractSessionIdFromPath(filePath);
let offset = this.state.offsets[filePath] ?? 0;
if (offset === 0 && watch.startAtEnd) {
try {
offset = statSync(filePath).size;
} catch {
offset = 0;
}
}
const tailer = new FileTailer(
filePath,
offset,
async (line: string) => {
await this.handleLine(line, watch, schema, filePath, sessionIdOverride);
},
(newOffset: number) => {
this.state.offsets[filePath] = newOffset;
saveWatchState(this.statePath, this.state);
}
);
tailer.start();
this.tailers.set(filePath, tailer);
logger.info('TRANSCRIPT', 'Watching transcript file', {
file: filePath,
watch: watch.name,
schema: schema.name
});
}
private async handleLine(
line: string,
watch: WatchTarget,
schema: TranscriptSchema,
filePath: string,
sessionIdOverride?: string | null
): Promise<void> {
try {
const entry = JSON.parse(line);
await this.processor.processEntry(entry, watch, schema, sessionIdOverride ?? undefined);
} catch (error) {
logger.debug('TRANSCRIPT', 'Failed to parse transcript line', {
watch: watch.name,
file: basename(filePath)
}, error as Error);
}
}
private extractSessionIdFromPath(filePath: string): string | null {
const match = filePath.match(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i);
return match ? match[0] : null;
}
}
+519 -81
View File
@@ -10,12 +10,54 @@
*/
import path from 'path';
import { existsSync, writeFileSync, unlinkSync, statSync } from 'fs';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
import { getWorkerPort, getWorkerHost } from '../shared/worker-utils.js';
import { HOOK_TIMEOUTS } from '../shared/hook-constants.js';
import { SettingsDefaultsManager } from '../shared/SettingsDefaultsManager.js';
import { getAuthMethodDescription } from '../shared/EnvManager.js';
import { logger } from '../utils/logger.js';
import { ChromaServerManager } from './sync/ChromaServerManager.js';
// Windows: avoid repeated spawn popups when startup fails (issue #921)
const WINDOWS_SPAWN_COOLDOWN_MS = 2 * 60 * 1000;
function getWorkerSpawnLockPath(): string {
return path.join(SettingsDefaultsManager.get('CLAUDE_MEM_DATA_DIR'), '.worker-start-attempted');
}
function shouldSkipSpawnOnWindows(): boolean {
if (process.platform !== 'win32') return false;
const lockPath = getWorkerSpawnLockPath();
if (!existsSync(lockPath)) return false;
try {
const modifiedTimeMs = statSync(lockPath).mtimeMs;
return Date.now() - modifiedTimeMs < WINDOWS_SPAWN_COOLDOWN_MS;
} catch {
return false;
}
}
function markWorkerSpawnAttempted(): void {
if (process.platform !== 'win32') return;
try {
writeFileSync(getWorkerSpawnLockPath(), '', 'utf-8');
} catch {
// Best-effort lock file — failure to write shouldn't block startup
}
}
function clearWorkerSpawnAttempted(): void {
if (process.platform !== 'win32') return;
try {
const lockPath = getWorkerSpawnLockPath();
if (existsSync(lockPath)) unlinkSync(lockPath);
} catch {
// Best-effort cleanup
}
}
// Version injected at build time by esbuild define
declare const __DEFAULT_PACKAGE_VERSION__: string;
const packageVersion = typeof __DEFAULT_PACKAGE_VERSION__ !== 'undefined' ? __DEFAULT_PACKAGE_VERSION__ : '0.0.0-dev';
@@ -27,6 +69,7 @@ import {
removePidFile,
getPlatformTimeout,
cleanupOrphanedProcesses,
cleanStalePidFile,
spawnDaemon,
createSignalHandler
} from './infrastructure/ProcessManager.js';
@@ -53,8 +96,8 @@ import { DatabaseManager } from './worker/DatabaseManager.js';
import { SessionManager } from './worker/SessionManager.js';
import { SSEBroadcaster } from './worker/SSEBroadcaster.js';
import { SDKAgent } from './worker/SDKAgent.js';
import { GeminiAgent } from './worker/GeminiAgent.js';
import { OpenRouterAgent } from './worker/OpenRouterAgent.js';
import { GeminiAgent, isGeminiSelected, isGeminiAvailable } from './worker/GeminiAgent.js';
import { OpenRouterAgent, isOpenRouterSelected, isOpenRouterAvailable } from './worker/OpenRouterAgent.js';
import { PaginationHelper } from './worker/PaginationHelper.js';
import { SettingsManager } from './worker/SettingsManager.js';
import { SearchManager } from './worker/SearchManager.js';
@@ -69,6 +112,7 @@ import { DataRoutes } from './worker/http/routes/DataRoutes.js';
import { SearchRoutes } from './worker/http/routes/SearchRoutes.js';
import { SettingsRoutes } from './worker/http/routes/SettingsRoutes.js';
import { LogsRoutes } from './worker/http/routes/LogsRoutes.js';
import { MemoryRoutes } from './worker/http/routes/MemoryRoutes.js';
// Process management for zombie cleanup (Issue #737)
import { startOrphanReaper, reapOrphanedProcesses } from './worker/ProcessRegistry.js';
@@ -131,6 +175,14 @@ export class WorkerService {
// Orphan reaper cleanup function (Issue #737)
private stopOrphanReaper: (() => void) | null = null;
// AI interaction tracking for health endpoint
private lastAiInteraction: {
timestamp: number;
success: boolean;
provider: string;
error?: string;
} | null = null;
constructor() {
// Initialize the promise that will resolve when background initialization completes
this.initializationComplete = new Promise((resolve) => {
@@ -154,6 +206,7 @@ export class WorkerService {
this.broadcastProcessingStatus();
});
// Initialize MCP client
// Empty capabilities object: this client only calls tools, doesn't expose any
this.mcpClient = new Client({
@@ -166,7 +219,24 @@ export class WorkerService {
getInitializationComplete: () => this.initializationCompleteFlag,
getMcpReady: () => this.mcpReady,
onShutdown: () => this.shutdown(),
onRestart: () => this.shutdown()
onRestart: () => this.shutdown(),
workerPath: __filename,
getAiStatus: () => {
let provider = 'claude';
if (isOpenRouterSelected() && isOpenRouterAvailable()) provider = 'openrouter';
else if (isGeminiSelected() && isGeminiAvailable()) provider = 'gemini';
return {
provider,
authMethod: getAuthMethodDescription(),
lastInteraction: this.lastAiInteraction
? {
timestamp: this.lastAiInteraction.timestamp,
success: this.lastAiInteraction.success,
...(this.lastAiInteraction.error && { error: this.lastAiInteraction.error }),
}
: null,
};
},
});
// Register route handlers
@@ -191,35 +261,74 @@ export class WorkerService {
this.isShuttingDown = shutdownRef.value;
handler('SIGINT');
});
// SIGHUP: sent by kernel when controlling terminal closes.
// Daemon mode: ignore it (survive parent shell exit).
// Interactive mode: treat like SIGTERM (graceful shutdown).
if (process.platform !== 'win32') {
if (process.argv.includes('--daemon')) {
process.on('SIGHUP', () => {
logger.debug('SYSTEM', 'Ignoring SIGHUP in daemon mode');
});
} else {
process.on('SIGHUP', () => {
this.isShuttingDown = shutdownRef.value;
handler('SIGHUP');
});
}
}
}
/**
* Register all route handlers with the server
*/
private registerRoutes(): void {
// Standard routes
this.server.registerRoutes(new ViewerRoutes(this.sseBroadcaster, this.dbManager, this.sessionManager));
this.server.registerRoutes(new SessionRoutes(this.sessionManager, this.dbManager, this.sdkAgent, this.geminiAgent, this.openRouterAgent, this.sessionEventBroadcaster, this));
this.server.registerRoutes(new DataRoutes(this.paginationHelper, this.dbManager, this.sessionManager, this.sseBroadcaster, this, this.startTime));
this.server.registerRoutes(new SettingsRoutes(this.settingsManager));
this.server.registerRoutes(new LogsRoutes());
// IMPORTANT: Middleware must be registered BEFORE routes (Express processes in order)
// Early handler for /api/context/inject to avoid 404 during startup
// Early handler for /api/context/inject — fail open if not yet initialized
this.server.app.get('/api/context/inject', async (req, res, next) => {
const timeoutMs = 300000; // 5 minute timeout for slow systems
const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Initialization timeout')), timeoutMs)
);
await Promise.race([this.initializationComplete, timeoutPromise]);
if (!this.searchRoutes) {
res.status(503).json({ error: 'Search routes not initialized' });
if (!this.initializationCompleteFlag || !this.searchRoutes) {
logger.warn('SYSTEM', 'Context requested before initialization complete, returning empty');
res.status(200).json({ content: [{ type: 'text', text: '' }] });
return;
}
next(); // Delegate to SearchRoutes handler
});
// Guard ALL /api/* routes during initialization — wait for DB with timeout
// Exceptions: /api/health, /api/readiness, /api/version (handled by Server.ts core routes)
// and /api/context/inject (handled above with fail-open)
this.server.app.use('/api', async (req, res, next) => {
if (this.initializationCompleteFlag) {
next();
return;
}
const timeoutMs = 30000;
const timeoutPromise = new Promise<void>((_, reject) =>
setTimeout(() => reject(new Error('Database initialization timeout')), timeoutMs)
);
try {
await Promise.race([this.initializationComplete, timeoutPromise]);
next();
} catch (error) {
logger.error('HTTP', `Request to ${req.method} ${req.path} rejected — DB not initialized`, {}, error as Error);
res.status(503).json({
error: 'Service initializing',
message: 'Database is still initializing, please retry'
});
}
});
// Standard routes (registered AFTER guard middleware)
this.server.registerRoutes(new ViewerRoutes(this.sseBroadcaster, this.dbManager, this.sessionManager));
this.server.registerRoutes(new SessionRoutes(this.sessionManager, this.dbManager, this.sdkAgent, this.geminiAgent, this.openRouterAgent, this.sessionEventBroadcaster, this));
this.server.registerRoutes(new DataRoutes(this.paginationHelper, this.dbManager, this.sessionManager, this.sseBroadcaster, this, this.startTime));
this.server.registerRoutes(new SettingsRoutes(this.settingsManager));
this.server.registerRoutes(new LogsRoutes());
this.server.registerRoutes(new MemoryRoutes(this.dbManager, 'claude-mem'));
}
/**
@@ -293,13 +402,12 @@ export class WorkerService {
await this.dbManager.initialize();
// Recover stuck messages from previous crashes
// Reset any messages that were processing when worker died
const { PendingMessageStore } = await import('./sqlite/PendingMessageStore.js');
const pendingStore = new PendingMessageStore(this.dbManager.getSessionStore().db, 3);
const STUCK_THRESHOLD_MS = 5 * 60 * 1000;
const resetCount = pendingStore.resetStuckMessages(STUCK_THRESHOLD_MS);
const resetCount = pendingStore.resetStaleProcessingMessages(0); // 0 = reset ALL processing
if (resetCount > 0) {
logger.info('SYSTEM', `Recovered ${resetCount} stuck messages from previous session`, { thresholdMinutes: 5 });
logger.info('SYSTEM', `Reset ${resetCount} stale processing messages to pending`);
}
// Initialize search services
@@ -366,8 +474,24 @@ export class WorkerService {
}
}
/**
* Get the appropriate agent based on provider settings.
* Same logic as SessionRoutes.getActiveAgent() for consistency.
*/
private getActiveAgent(): SDKAgent | GeminiAgent | OpenRouterAgent {
if (isOpenRouterSelected() && isOpenRouterAvailable()) {
return this.openRouterAgent;
}
if (isGeminiSelected() && isGeminiAvailable()) {
return this.geminiAgent;
}
return this.sdkAgent;
}
/**
* Start a session processor
* On SDK resume failure (terminated session), falls back to Gemini/OpenRouter if available,
* otherwise marks messages abandoned and removes session so queue does not grow unbounded.
*/
private startSessionProcessor(
session: ReturnType<typeof this.sessionManager.getSession>,
@@ -376,21 +500,203 @@ export class WorkerService {
if (!session) return;
const sid = session.sessionDbId;
logger.info('SYSTEM', `Starting generator (${source})`, { sessionId: sid });
const agent = this.getActiveAgent();
const providerName = agent.constructor.name;
session.generatorPromise = this.sdkAgent.startSession(session, this)
.catch(error => {
// Before starting generator, check if AbortController is already aborted
// This can happen after a previous generator was aborted but the session still has pending work
if (session.abortController.signal.aborted) {
logger.debug('SYSTEM', 'Replacing aborted AbortController before starting generator', {
sessionId: session.sessionDbId
});
session.abortController = new AbortController();
}
// Track whether generator failed with an unrecoverable error to prevent infinite restart loops
let hadUnrecoverableError = false;
let sessionFailed = false;
logger.info('SYSTEM', `Starting generator (${source}) using ${providerName}`, { sessionId: sid });
session.generatorPromise = agent.startSession(session, this)
.catch(async (error: unknown) => {
const errorMessage = (error as Error)?.message || '';
// Detect unrecoverable errors that should NOT trigger restart
// These errors will fail immediately on retry, causing infinite loops
const unrecoverablePatterns = [
'Claude executable not found',
'CLAUDE_CODE_PATH',
'ENOENT',
'spawn',
'Invalid API key',
];
if (unrecoverablePatterns.some(pattern => errorMessage.includes(pattern))) {
hadUnrecoverableError = true;
this.lastAiInteraction = {
timestamp: Date.now(),
success: false,
provider: providerName,
error: errorMessage,
};
logger.error('SDK', 'Unrecoverable generator error - will NOT restart', {
sessionId: session.sessionDbId,
project: session.project,
errorMessage
});
return;
}
// Fallback for terminated SDK sessions (provider abstraction)
if (this.isSessionTerminatedError(error)) {
logger.warn('SDK', 'SDK resume failed, falling back to standalone processing', {
sessionId: session.sessionDbId,
project: session.project,
reason: error instanceof Error ? error.message : String(error)
});
return this.runFallbackForTerminatedSession(session, error);
}
// Detect stale resume failures - SDK session context was lost
if ((errorMessage.includes('aborted by user') || errorMessage.includes('No conversation found'))
&& session.memorySessionId) {
logger.warn('SDK', 'Detected stale resume failure, clearing memorySessionId for fresh start', {
sessionId: session.sessionDbId,
memorySessionId: session.memorySessionId,
errorMessage
});
// Clear stale memorySessionId and force fresh init on next attempt
this.dbManager.getSessionStore().updateMemorySessionId(session.sessionDbId, null);
session.memorySessionId = null;
session.forceInit = true;
}
logger.error('SDK', 'Session generator failed', {
sessionId: session.sessionDbId,
project: session.project
project: session.project,
provider: providerName
}, error as Error);
sessionFailed = true;
this.lastAiInteraction = {
timestamp: Date.now(),
success: false,
provider: providerName,
error: errorMessage,
};
throw error;
})
.finally(() => {
session.generatorPromise = null;
// Record successful AI interaction if no error occurred
if (!sessionFailed && !hadUnrecoverableError) {
this.lastAiInteraction = {
timestamp: Date.now(),
success: true,
provider: providerName,
};
}
// Do NOT restart after unrecoverable errors - prevents infinite loops
if (hadUnrecoverableError) {
logger.warn('SYSTEM', 'Skipping restart due to unrecoverable error', {
sessionId: session.sessionDbId
});
this.broadcastProcessingStatus();
return;
}
// Check if there's pending work that needs processing with a fresh AbortController
const { PendingMessageStore } = require('./sqlite/PendingMessageStore.js');
const pendingStore = new PendingMessageStore(this.dbManager.getSessionStore().db, 3);
const pendingCount = pendingStore.getPendingCount(session.sessionDbId);
if (pendingCount > 0) {
logger.info('SYSTEM', 'Pending work remains after generator exit, restarting with fresh AbortController', {
sessionId: session.sessionDbId,
pendingCount
});
// Reset AbortController for restart
session.abortController = new AbortController();
// Restart processor
this.startSessionProcessor(session, 'pending-work-restart');
}
this.broadcastProcessingStatus();
});
}
/**
* Match errors that indicate the Claude Code process/session is gone (resume impossible).
* Used to trigger graceful fallback instead of leaving pending messages stuck forever.
*/
private isSessionTerminatedError(error: unknown): boolean {
const msg = error instanceof Error ? error.message : String(error);
const normalized = msg.toLowerCase();
return (
normalized.includes('process aborted by user') ||
normalized.includes('processtransport') ||
normalized.includes('not ready for writing') ||
normalized.includes('session generator failed') ||
normalized.includes('claude code process')
);
}
/**
* When SDK resume fails due to terminated session: try Gemini then OpenRouter to drain
* pending messages; if no fallback available, mark messages abandoned and remove session.
*/
private async runFallbackForTerminatedSession(
session: ReturnType<typeof this.sessionManager.getSession>,
_originalError: unknown
): Promise<void> {
if (!session) return;
const sessionDbId = session.sessionDbId;
// Fallback agents need memorySessionId for storeObservations
if (!session.memorySessionId) {
const syntheticId = `fallback-${sessionDbId}-${Date.now()}`;
session.memorySessionId = syntheticId;
this.dbManager.getSessionStore().updateMemorySessionId(sessionDbId, syntheticId);
}
if (isGeminiAvailable()) {
try {
await this.geminiAgent.startSession(session, this);
return;
} catch (e) {
logger.warn('SDK', 'Fallback Gemini failed, trying OpenRouter', {
sessionId: sessionDbId,
error: e instanceof Error ? e.message : String(e)
});
}
}
if (isOpenRouterAvailable()) {
try {
await this.openRouterAgent.startSession(session, this);
return;
} catch (e) {
logger.warn('SDK', 'Fallback OpenRouter failed', {
sessionId: sessionDbId,
error: e instanceof Error ? e.message : String(e)
});
}
}
// No fallback or both failed: mark messages abandoned and remove session so queue doesn't grow
const pendingStore = this.sessionManager.getPendingMessageStore();
const abandoned = pendingStore.markAllSessionMessagesAbandoned(sessionDbId);
if (abandoned > 0) {
logger.warn('SDK', 'No fallback available; marked pending messages abandoned', {
sessionId: sessionDbId,
abandoned
});
}
this.sessionManager.removeSessionImmediate(sessionDbId);
this.sessionEventBroadcaster.broadcastSessionCompleted(sessionDbId);
}
/**
* Process pending session queues
*/
@@ -402,6 +708,46 @@ export class WorkerService {
}> {
const { PendingMessageStore } = await import('./sqlite/PendingMessageStore.js');
const pendingStore = new PendingMessageStore(this.dbManager.getSessionStore().db, 3);
const sessionStore = this.dbManager.getSessionStore();
// Clean up stale 'active' sessions before processing
// Sessions older than 6 hours without activity are likely orphaned
const STALE_SESSION_THRESHOLD_MS = 6 * 60 * 60 * 1000;
const staleThreshold = Date.now() - STALE_SESSION_THRESHOLD_MS;
try {
const staleSessionIds = sessionStore.db.prepare(`
SELECT id FROM sdk_sessions
WHERE status = 'active' AND started_at_epoch < ?
`).all(staleThreshold) as { id: number }[];
if (staleSessionIds.length > 0) {
const ids = staleSessionIds.map(r => r.id);
const placeholders = ids.map(() => '?').join(',');
sessionStore.db.prepare(`
UPDATE sdk_sessions
SET status = 'failed', completed_at_epoch = ?
WHERE id IN (${placeholders})
`).run(Date.now(), ...ids);
logger.info('SYSTEM', `Marked ${ids.length} stale sessions as failed`);
const msgResult = sessionStore.db.prepare(`
UPDATE pending_messages
SET status = 'failed', failed_at_epoch = ?
WHERE status = 'pending'
AND session_db_id IN (${placeholders})
`).run(Date.now(), ...ids);
if (msgResult.changes > 0) {
logger.info('SYSTEM', `Marked ${msgResult.changes} pending messages from stale sessions as failed`);
}
}
} catch (error) {
logger.error('SYSTEM', 'Failed to clean up stale sessions', {}, error as Error);
}
const orphanedSessionIds = pendingStore.getSessionsWithPendingMessages();
const result = {
@@ -486,6 +832,86 @@ export class WorkerService {
}
}
// ============================================================================
// Reusable Worker Startup Logic
// ============================================================================
/**
* Ensures the worker is started and healthy.
* This function can be called by both 'start' and 'hook' commands.
*
* @param port - The port the worker should run on
* @returns true if worker is healthy (existing or newly started), false on failure
*/
async function ensureWorkerStarted(port: number): Promise<boolean> {
// Clean stale PID file first (cheap: 1 fs read + 1 signal-0 check)
cleanStalePidFile();
// Check if worker is already running and healthy
if (await waitForHealth(port, 1000)) {
const versionCheck = await checkVersionMatch(port);
if (!versionCheck.matches) {
logger.info('SYSTEM', 'Worker version mismatch detected - auto-restarting', {
pluginVersion: versionCheck.pluginVersion,
workerVersion: versionCheck.workerVersion
});
await httpShutdown(port);
const freed = await waitForPortFree(port, getPlatformTimeout(HOOK_TIMEOUTS.PORT_IN_USE_WAIT));
if (!freed) {
logger.error('SYSTEM', 'Port did not free up after shutdown for version mismatch restart', { port });
return false;
}
removePidFile();
} else {
logger.info('SYSTEM', 'Worker already running and healthy');
return true;
}
}
// Check if port is in use by something else
const portInUse = await isPortInUse(port);
if (portInUse) {
logger.info('SYSTEM', 'Port in use, waiting for worker to become healthy');
const healthy = await waitForHealth(port, getPlatformTimeout(HOOK_TIMEOUTS.PORT_IN_USE_WAIT));
if (healthy) {
logger.info('SYSTEM', 'Worker is now healthy');
return true;
}
logger.error('SYSTEM', 'Port in use but worker not responding to health checks');
return false;
}
// Windows: skip spawn if a recent attempt already failed (prevents repeated bun.exe popups, issue #921)
if (shouldSkipSpawnOnWindows()) {
logger.warn('SYSTEM', 'Worker unavailable on Windows — skipping spawn (recent attempt failed within cooldown)');
return false;
}
// Spawn new worker daemon
logger.info('SYSTEM', 'Starting worker daemon');
markWorkerSpawnAttempted();
const pid = spawnDaemon(__filename, port);
if (pid === undefined) {
logger.error('SYSTEM', 'Failed to spawn worker daemon');
return false;
}
// PID file is written by the worker itself after listen() succeeds
// This is race-free and works correctly on Windows where cmd.exe PID is useless
const healthy = await waitForHealth(port, getPlatformTimeout(HOOK_TIMEOUTS.POST_SPAWN_WAIT));
if (!healthy) {
removePidFile();
logger.error('SYSTEM', 'Worker failed to start (health check timeout)');
return false;
}
clearWorkerSpawnAttempted();
logger.info('SYSTEM', 'Worker started successfully');
return true;
}
// ============================================================================
// CLI Entry Point
// ============================================================================
@@ -504,58 +930,12 @@ async function main() {
switch (command) {
case 'start': {
if (await waitForHealth(port, 1000)) {
const versionCheck = await checkVersionMatch(port);
if (!versionCheck.matches) {
logger.info('SYSTEM', 'Worker version mismatch detected - auto-restarting', {
pluginVersion: versionCheck.pluginVersion,
workerVersion: versionCheck.workerVersion
});
await httpShutdown(port);
const freed = await waitForPortFree(port, getPlatformTimeout(15000));
if (!freed) {
logger.error('SYSTEM', 'Port did not free up after shutdown for version mismatch restart', { port });
exitWithStatus('error', 'Port did not free after version mismatch restart');
}
removePidFile();
} else {
logger.info('SYSTEM', 'Worker already running and healthy');
exitWithStatus('ready');
}
const success = await ensureWorkerStarted(port);
if (success) {
exitWithStatus('ready');
} else {
exitWithStatus('error', 'Failed to start worker');
}
const portInUse = await isPortInUse(port);
if (portInUse) {
logger.info('SYSTEM', 'Port in use, waiting for worker to become healthy');
const healthy = await waitForHealth(port, getPlatformTimeout(15000));
if (healthy) {
logger.info('SYSTEM', 'Worker is now healthy');
exitWithStatus('ready');
}
logger.error('SYSTEM', 'Port in use but worker not responding to health checks');
exitWithStatus('error', 'Port in use but worker not responding');
}
logger.info('SYSTEM', 'Starting worker daemon');
const pid = spawnDaemon(__filename, port);
if (pid === undefined) {
logger.error('SYSTEM', 'Failed to spawn worker daemon');
exitWithStatus('error', 'Failed to spawn worker daemon');
}
// PID file is written by the worker itself after listen() succeeds
// This is race-free and works correctly on Windows where cmd.exe PID is useless
const healthy = await waitForHealth(port, getPlatformTimeout(30000));
if (!healthy) {
removePidFile();
logger.error('SYSTEM', 'Worker failed to start (health check timeout)');
exitWithStatus('error', 'Worker failed to start (health check timeout)');
}
logger.info('SYSTEM', 'Worker started successfully');
exitWithStatus('ready');
}
case 'stop': {
@@ -592,7 +972,7 @@ async function main() {
// PID file is written by the worker itself after listen() succeeds
// This is race-free and works correctly on Windows where cmd.exe PID is useless
const healthy = await waitForHealth(port, getPlatformTimeout(30000));
const healthy = await waitForHealth(port, getPlatformTimeout(HOOK_TIMEOUTS.POST_SPAWN_WAIT));
if (!healthy) {
removePidFile();
logger.error('SYSTEM', 'Worker failed to restart');
@@ -626,21 +1006,79 @@ async function main() {
}
case 'hook': {
// Auto-start worker if not running
const workerReady = await ensureWorkerStarted(port);
if (!workerReady) {
logger.warn('SYSTEM', 'Worker failed to start before hook, handler will retry');
}
// Existing logic unchanged
const platform = process.argv[3];
const event = process.argv[4];
if (!platform || !event) {
console.error('Usage: claude-mem hook <platform> <event>');
console.error('Platforms: claude-code, cursor, raw');
console.error('Events: context, session-init, observation, summarize, user-message');
console.error('Events: context, session-init, observation, summarize, session-complete');
process.exit(1);
}
// Check if worker is already running on port
const portInUse = await isPortInUse(port);
let startedWorkerInProcess = false;
if (!portInUse) {
// Port free - start worker IN THIS PROCESS (no spawn!)
// This process becomes the worker and stays alive
try {
logger.info('SYSTEM', 'Starting worker in-process for hook', { event });
const worker = new WorkerService();
await worker.start();
startedWorkerInProcess = true;
// Worker is now running in this process on the port
} catch (error) {
logger.failure('SYSTEM', 'Worker failed to start in hook', {}, error as Error);
removePidFile();
process.exit(0);
}
}
// If port in use, we'll use HTTP to the existing worker
const { hookCommand } = await import('../cli/hook-command.js');
await hookCommand(platform, event);
// If we started the worker in this process, skip process.exit() so we stay alive as the worker
await hookCommand(platform, event, { skipExit: startedWorkerInProcess });
// Note: if we started worker in-process, this process stays alive as the worker
// The break allows the event loop to continue serving requests
break;
}
case 'generate': {
const dryRun = process.argv.includes('--dry-run');
const { generateClaudeMd } = await import('../cli/claude-md-commands.js');
const result = await generateClaudeMd(dryRun);
process.exit(result);
}
case 'clean': {
const dryRun = process.argv.includes('--dry-run');
const { cleanClaudeMd } = await import('../cli/claude-md-commands.js');
const result = await cleanClaudeMd(dryRun);
process.exit(result);
}
case '--daemon':
default: {
// Prevent daemon from dying silently on unhandled errors.
// The HTTP server can continue serving even if a background task throws.
process.on('unhandledRejection', (reason) => {
logger.error('SYSTEM', 'Unhandled rejection in daemon', {
reason: reason instanceof Error ? reason.message : String(reason)
});
});
process.on('uncaughtException', (error) => {
logger.error('SYSTEM', 'Uncaught exception in daemon', {}, error as Error);
// Don't exit — keep the HTTP server running
});
const worker = new WorkerService();
worker.start().catch((error) => {
logger.failure('SYSTEM', 'Worker failed to start', {}, error as Error);
+5
View File
@@ -33,6 +33,11 @@ export interface ActiveSession {
earliestPendingTimestamp: number | null; // Original timestamp of earliest pending message (for accurate observation timestamps)
conversationHistory: ConversationMessage[]; // Shared conversation history for provider switching
currentProvider: 'claude' | 'gemini' | 'openrouter' | null; // Track which provider is currently running
consecutiveRestarts: number; // Track consecutive restart attempts to prevent infinite loops
forceInit?: boolean; // Force fresh SDK session (skip resume)
// CLAIM-CONFIRM FIX: Track IDs of messages currently being processed
// These IDs will be confirmed (deleted) after successful storage
processingMessageIds: number[];
}
export interface PendingMessage {
+3 -2
View File
@@ -7,11 +7,12 @@
import { execSync, spawnSync } from 'child_process';
import { existsSync, unlinkSync } from 'fs';
import { homedir } from 'os';
import { join } from 'path';
import { logger } from '../../utils/logger.js';
import { MARKETPLACE_ROOT } from '../../shared/paths.js';
const INSTALLED_PLUGIN_PATH = join(homedir(), '.claude', 'plugins', 'marketplaces', 'thedotmack');
// Alias for code clarity - this is the installed plugin path
const INSTALLED_PLUGIN_PATH = MARKETPLACE_ROOT;
/**
* Validate branch name to prevent command injection
-2
View File
@@ -1,8 +1,6 @@
<claude-mem-context>
# Recent Activity
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
### Dec 10, 2025
| ID | Time | T | Title | Read |
+33 -6
View File
@@ -17,6 +17,7 @@ import { SessionManager } from './SessionManager.js';
import { logger } from '../../utils/logger.js';
import { buildInitPrompt, buildObservationPrompt, buildSummaryPrompt, buildContinuationPrompt } from '../../sdk/prompts.js';
import { SettingsDefaultsManager } from '../../shared/SettingsDefaultsManager.js';
import { getCredential } from '../../shared/EnvManager.js';
import type { ActiveSession, ConversationMessage } from '../worker-types.js';
import { ModeManager } from '../domain/ModeManager.js';
import {
@@ -37,7 +38,7 @@ export type GeminiModel =
| 'gemini-2.5-pro'
| 'gemini-2.0-flash'
| 'gemini-2.0-flash-lite'
| 'gemini-3-flash';
| 'gemini-3-flash-preview';
// Free tier RPM limits by model (requests per minute)
const GEMINI_RPM_LIMITS: Record<GeminiModel, number> = {
@@ -46,7 +47,7 @@ const GEMINI_RPM_LIMITS: Record<GeminiModel, number> = {
'gemini-2.5-pro': 5,
'gemini-2.0-flash': 15,
'gemini-2.0-flash-lite': 30,
'gemini-3-flash': 5,
'gemini-3-flash-preview': 5,
};
// Track last request time for rate limiting
@@ -133,6 +134,14 @@ export class GeminiAgent {
throw new Error('Gemini API key not configured. Set CLAUDE_MEM_GEMINI_API_KEY in settings or GEMINI_API_KEY environment variable.');
}
// Generate synthetic memorySessionId (Gemini is stateless, doesn't return session IDs)
if (!session.memorySessionId) {
const syntheticMemorySessionId = `gemini-${session.contentSessionId}-${Date.now()}`;
session.memorySessionId = syntheticMemorySessionId;
this.dbManager.getSessionStore().updateMemorySessionId(session.sessionDbId, syntheticMemorySessionId);
logger.info('SESSION', `MEMORY_ID_GENERATED | sessionDbId=${session.sessionDbId} | provider=Gemini`);
}
// Load active mode
const mode = ModeManager.getInstance().getActiveMode();
@@ -177,6 +186,10 @@ export class GeminiAgent {
let lastCwd: string | undefined;
for await (const message of this.sessionManager.getMessageIterator(session.sessionDbId)) {
// CLAIM-CONFIRM: Track message ID for confirmProcessed() after successful storage
// The message is now in 'processing' status in DB until ResponseProcessor calls confirmProcessed()
session.processingMessageIds.push(message._persistentId);
// Capture cwd from each message for worktree support
if (message.cwd) {
lastCwd = message.cwd;
@@ -191,6 +204,12 @@ export class GeminiAgent {
session.lastPromptNumber = message.prompt_number;
}
// CRITICAL: Check memorySessionId BEFORE making expensive LLM call
// This prevents wasting tokens when we won't be able to store the result anyway
if (!session.memorySessionId) {
throw new Error('Cannot process observations: memorySessionId not yet captured. This session may need to be reinitialized.');
}
// Build observation prompt
const obsPrompt = buildObservationPrompt({
id: 0,
@@ -229,6 +248,11 @@ export class GeminiAgent {
);
} else if (message.type === 'summarize') {
// CRITICAL: Check memorySessionId BEFORE making expensive LLM call
if (!session.memorySessionId) {
throw new Error('Cannot process summary: memorySessionId not yet captured. This session may need to be reinitialized.');
}
// Build summary prompt
const summaryPrompt = buildSummaryPrompt({
id: session.sessionDbId,
@@ -367,13 +391,15 @@ export class GeminiAgent {
/**
* Get Gemini configuration from settings or environment
* Issue #733: Uses centralized ~/.claude-mem/.env for credentials, not random project .env files
*/
private getGeminiConfig(): { apiKey: string; model: GeminiModel; rateLimitingEnabled: boolean } {
const settingsPath = path.join(homedir(), '.claude-mem', 'settings.json');
const settings = SettingsDefaultsManager.loadFromFile(settingsPath);
// API key: check settings first, then environment variable
const apiKey = settings.CLAUDE_MEM_GEMINI_API_KEY || process.env.GEMINI_API_KEY || '';
// API key: check settings first, then centralized claude-mem .env (NOT process.env)
// This prevents Issue #733 where random project .env files could interfere
const apiKey = settings.CLAUDE_MEM_GEMINI_API_KEY || getCredential('GEMINI_API_KEY') || '';
// Model: from settings or default, with validation
const defaultModel: GeminiModel = 'gemini-2.5-flash';
@@ -384,7 +410,7 @@ export class GeminiAgent {
'gemini-2.5-pro',
'gemini-2.0-flash',
'gemini-2.0-flash-lite',
'gemini-3-flash',
'gemini-3-flash-preview',
];
let model: GeminiModel;
@@ -407,11 +433,12 @@ export class GeminiAgent {
/**
* Check if Gemini is available (has API key configured)
* Issue #733: Uses centralized ~/.claude-mem/.env, not random project .env files
*/
export function isGeminiAvailable(): boolean {
const settingsPath = path.join(homedir(), '.claude-mem', 'settings.json');
const settings = SettingsDefaultsManager.loadFromFile(settingsPath);
return !!(settings.CLAUDE_MEM_GEMINI_API_KEY || process.env.GEMINI_API_KEY);
return !!(settings.CLAUDE_MEM_GEMINI_API_KEY || getCredential('GEMINI_API_KEY'));
}
/**
+30 -3
View File
@@ -17,6 +17,7 @@ import { logger } from '../../utils/logger.js';
import { buildInitPrompt, buildObservationPrompt, buildSummaryPrompt, buildContinuationPrompt } from '../../sdk/prompts.js';
import { SettingsDefaultsManager } from '../../shared/SettingsDefaultsManager.js';
import { USER_SETTINGS_PATH } from '../../shared/paths.js';
import { getCredential } from '../../shared/EnvManager.js';
import type { ActiveSession, ConversationMessage } from '../worker-types.js';
import { ModeManager } from '../domain/ModeManager.js';
import {
@@ -91,6 +92,14 @@ export class OpenRouterAgent {
throw new Error('OpenRouter API key not configured. Set CLAUDE_MEM_OPENROUTER_API_KEY in settings or OPENROUTER_API_KEY environment variable.');
}
// Generate synthetic memorySessionId (OpenRouter is stateless, doesn't return session IDs)
if (!session.memorySessionId) {
const syntheticMemorySessionId = `openrouter-${session.contentSessionId}-${Date.now()}`;
session.memorySessionId = syntheticMemorySessionId;
this.dbManager.getSessionStore().updateMemorySessionId(session.sessionDbId, syntheticMemorySessionId);
logger.info('SESSION', `MEMORY_ID_GENERATED | sessionDbId=${session.sessionDbId} | provider=OpenRouter`);
}
// Load active mode
const mode = ModeManager.getInstance().getActiveMode();
@@ -136,6 +145,10 @@ export class OpenRouterAgent {
// Process pending messages
for await (const message of this.sessionManager.getMessageIterator(session.sessionDbId)) {
// CLAIM-CONFIRM: Track message ID for confirmProcessed() after successful storage
// The message is now in 'processing' status in DB until ResponseProcessor calls confirmProcessed()
session.processingMessageIds.push(message._persistentId);
// Capture cwd from messages for proper worktree support
if (message.cwd) {
lastCwd = message.cwd;
@@ -149,6 +162,12 @@ export class OpenRouterAgent {
session.lastPromptNumber = message.prompt_number;
}
// CRITICAL: Check memorySessionId BEFORE making expensive LLM call
// This prevents wasting tokens when we won't be able to store the result anyway
if (!session.memorySessionId) {
throw new Error('Cannot process observations: memorySessionId not yet captured. This session may need to be reinitialized.');
}
// Build observation prompt
const obsPrompt = buildObservationPrompt({
id: 0,
@@ -187,6 +206,11 @@ export class OpenRouterAgent {
);
} else if (message.type === 'summarize') {
// CRITICAL: Check memorySessionId BEFORE making expensive LLM call
if (!session.memorySessionId) {
throw new Error('Cannot process summary: memorySessionId not yet captured. This session may need to be reinitialized.');
}
// Build summary prompt
const summaryPrompt = buildSummaryPrompt({
id: session.sessionDbId,
@@ -409,13 +433,15 @@ export class OpenRouterAgent {
/**
* Get OpenRouter configuration from settings or environment
* Issue #733: Uses centralized ~/.claude-mem/.env for credentials, not random project .env files
*/
private getOpenRouterConfig(): { apiKey: string; model: string; siteUrl?: string; appName?: string } {
const settingsPath = USER_SETTINGS_PATH;
const settings = SettingsDefaultsManager.loadFromFile(settingsPath);
// API key: check settings first, then environment variable
const apiKey = settings.CLAUDE_MEM_OPENROUTER_API_KEY || process.env.OPENROUTER_API_KEY || '';
// API key: check settings first, then centralized claude-mem .env (NOT process.env)
// This prevents Issue #733 where random project .env files could interfere
const apiKey = settings.CLAUDE_MEM_OPENROUTER_API_KEY || getCredential('OPENROUTER_API_KEY') || '';
// Model: from settings or default
const model = settings.CLAUDE_MEM_OPENROUTER_MODEL || 'xiaomi/mimo-v2-flash:free';
@@ -430,11 +456,12 @@ export class OpenRouterAgent {
/**
* Check if OpenRouter is available (has API key configured)
* Issue #733: Uses centralized ~/.claude-mem/.env, not random project .env files
*/
export function isOpenRouterAvailable(): boolean {
const settingsPath = USER_SETTINGS_PATH;
const settings = SettingsDefaultsManager.loadFromFile(settingsPath);
return !!(settings.CLAUDE_MEM_OPENROUTER_API_KEY || process.env.OPENROUTER_API_KEY);
return !!(settings.CLAUDE_MEM_OPENROUTER_API_KEY || getCredential('OPENROUTER_API_KEY'));
}
/**
+86
View File
@@ -121,6 +121,86 @@ export async function ensureProcessExit(tracked: TrackedProcess, timeoutMs: numb
unregisterProcess(pid);
}
/**
* Kill idle daemon children (claude processes spawned by worker-service)
*
* These are SDK-spawned claude processes that completed their work but
* didn't terminate properly. They remain as children of the worker-service
* daemon, consuming memory without doing useful work.
*
* Criteria for cleanup:
* - Process name is "claude"
* - Parent PID is the worker-service daemon (this process)
* - Process has 0% CPU (idle)
* - Process has been running for more than 2 minutes
*/
async function killIdleDaemonChildren(): Promise<number> {
if (process.platform === 'win32') {
// Windows: Different process model, skip for now
return 0;
}
const daemonPid = process.pid;
let killed = 0;
try {
const { stdout } = await execAsync(
'ps -eo pid,ppid,%cpu,etime,comm 2>/dev/null | grep "claude$" || true'
);
for (const line of stdout.trim().split('\n')) {
if (!line) continue;
const parts = line.trim().split(/\s+/);
if (parts.length < 5) continue;
const [pidStr, ppidStr, cpuStr, etime] = parts;
const pid = parseInt(pidStr, 10);
const ppid = parseInt(ppidStr, 10);
const cpu = parseFloat(cpuStr);
// Skip if not a child of this daemon
if (ppid !== daemonPid) continue;
// Skip if actively using CPU
if (cpu > 0) continue;
// Parse elapsed time to minutes
// Formats: MM:SS, HH:MM:SS, D-HH:MM:SS
let minutes = 0;
const dayMatch = etime.match(/^(\d+)-(\d+):(\d+):(\d+)$/);
const hourMatch = etime.match(/^(\d+):(\d+):(\d+)$/);
const minMatch = etime.match(/^(\d+):(\d+)$/);
if (dayMatch) {
minutes = parseInt(dayMatch[1], 10) * 24 * 60 +
parseInt(dayMatch[2], 10) * 60 +
parseInt(dayMatch[3], 10);
} else if (hourMatch) {
minutes = parseInt(hourMatch[1], 10) * 60 +
parseInt(hourMatch[2], 10);
} else if (minMatch) {
minutes = parseInt(minMatch[1], 10);
}
// Kill if idle for more than 2 minutes
if (minutes >= 2) {
logger.info('PROCESS', `Killing idle daemon child PID ${pid} (idle ${minutes}m)`, { pid, minutes });
try {
process.kill(pid, 'SIGKILL');
killed++;
} catch {
// Already dead or permission denied
}
}
}
} catch {
// No matches or command error
}
return killed;
}
/**
* Kill system-level orphans (ppid=1 on Unix)
* These are Claude processes whose parent died unexpectedly
@@ -179,6 +259,9 @@ export async function reapOrphanedProcesses(activeSessionIds: Set<number>): Prom
// System-level: find ppid=1 orphans
killed += await killSystemOrphans();
// Daemon children: find idle SDK processes that didn't terminate
killed += await killIdleDaemonChildren();
return killed;
}
@@ -187,6 +270,9 @@ export async function reapOrphanedProcesses(activeSessionIds: Set<number>): Prom
*
* The SDK's spawnClaudeCodeProcess option allows us to intercept subprocess
* creation and capture the PID before the SDK hides it.
*
* NOTE: Session isolation is handled via the `cwd` option in SDKAgent.ts,
* NOT via CLAUDE_CONFIG_DIR (which breaks authentication).
*/
export function createPidCapturingSpawn(sessionDbId: number) {
return (spawnOptions: {
+79 -18
View File
@@ -16,7 +16,8 @@ import { SessionManager } from './SessionManager.js';
import { logger } from '../../utils/logger.js';
import { buildInitPrompt, buildObservationPrompt, buildSummaryPrompt, buildContinuationPrompt } from '../../sdk/prompts.js';
import { SettingsDefaultsManager } from '../../shared/SettingsDefaultsManager.js';
import { USER_SETTINGS_PATH } from '../../shared/paths.js';
import { USER_SETTINGS_PATH, OBSERVER_SESSIONS_DIR, ensureDir } from '../../shared/paths.js';
import { buildIsolatedEnv, getAuthMethodDescription } from '../../shared/EnvManager.js';
import type { ActiveSession, SDKUserMessage } from '../worker-types.js';
import { ModeManager } from '../domain/ModeManager.js';
import { processAgentResponse, type WorkerRef } from './agents/index.js';
@@ -71,24 +72,42 @@ export class SDKAgent {
// CRITICAL: Only resume if:
// 1. memorySessionId exists (was captured from a previous SDK response)
// 2. lastPromptNumber > 1 (this is a continuation within the same SDK session)
// 3. forceInit is NOT set (stale session recovery clears this)
// On worker restart or crash recovery, memorySessionId may exist from a previous
// SDK session but we must NOT resume because the SDK context was lost.
// NEVER use contentSessionId for resume - that would inject messages into the user's transcript!
const hasRealMemorySessionId = !!session.memorySessionId;
const shouldResume = hasRealMemorySessionId && session.lastPromptNumber > 1 && !session.forceInit;
// Clear forceInit after using it
if (session.forceInit) {
logger.info('SDK', 'forceInit flag set, starting fresh SDK session', {
sessionDbId: session.sessionDbId,
previousMemorySessionId: session.memorySessionId
});
session.forceInit = false;
}
// Build isolated environment from ~/.claude-mem/.env
// This prevents Issue #733: random ANTHROPIC_API_KEY from project .env files
// being used instead of the configured auth method (CLI subscription or explicit API key)
const isolatedEnv = buildIsolatedEnv();
const authMethod = getAuthMethodDescription();
logger.info('SDK', 'Starting SDK query', {
sessionDbId: session.sessionDbId,
contentSessionId: session.contentSessionId,
memorySessionId: session.memorySessionId,
hasRealMemorySessionId,
resume_parameter: hasRealMemorySessionId ? session.memorySessionId : '(none - fresh start)',
lastPromptNumber: session.lastPromptNumber
shouldResume,
resume_parameter: shouldResume ? session.memorySessionId : '(none - fresh start)',
lastPromptNumber: session.lastPromptNumber,
authMethod
});
// Debug-level alignment logs for detailed tracing
if (session.lastPromptNumber > 1) {
const willResume = hasRealMemorySessionId;
logger.debug('SDK', `[ALIGNMENT] Resume Decision | contentSessionId=${session.contentSessionId} | memorySessionId=${session.memorySessionId} | prompt#=${session.lastPromptNumber} | hasRealMemorySessionId=${hasRealMemorySessionId} | willResume=${willResume} | resumeWith=${willResume ? session.memorySessionId : 'NONE'}`);
logger.debug('SDK', `[ALIGNMENT] Resume Decision | contentSessionId=${session.contentSessionId} | memorySessionId=${session.memorySessionId} | prompt#=${session.lastPromptNumber} | hasRealMemorySessionId=${hasRealMemorySessionId} | shouldResume=${shouldResume} | resumeWith=${shouldResume ? session.memorySessionId : 'NONE'}`);
} else {
// INIT prompt - never resume even if memorySessionId exists (stale from previous session)
const hasStaleMemoryId = hasRealMemorySessionId;
@@ -101,39 +120,58 @@ export class SDKAgent {
// Run Agent SDK query loop
// Only resume if we have a captured memory session ID
// Use custom spawn to capture PIDs for zombie process cleanup (Issue #737)
// Use dedicated cwd to isolate observer sessions from user's `claude --resume` list
ensureDir(OBSERVER_SESSIONS_DIR);
// CRITICAL: Pass isolated env to prevent Issue #733 (API key pollution from project .env files)
const queryResult = query({
prompt: messageGenerator,
options: {
model: modelId,
// Only resume if BOTH: (1) we have a memorySessionId AND (2) this isn't the first prompt
// On worker restart, memorySessionId may exist from a previous SDK session but we
// need to start fresh since the SDK context was lost
...(hasRealMemorySessionId && session.lastPromptNumber > 1 && { resume: session.memorySessionId }),
// Isolate observer sessions - they'll appear under project "observer-sessions"
// instead of polluting user's actual project resume lists
cwd: OBSERVER_SESSIONS_DIR,
// Only resume if shouldResume is true (memorySessionId exists, not first prompt, not forceInit)
...(shouldResume && { resume: session.memorySessionId }),
disallowedTools,
abortController: session.abortController,
pathToClaudeCodeExecutable: claudePath,
// Custom spawn function captures PIDs to fix zombie process accumulation
spawnClaudeCodeProcess: createPidCapturingSpawn(session.sessionDbId)
spawnClaudeCodeProcess: createPidCapturingSpawn(session.sessionDbId),
env: isolatedEnv // Use isolated credentials from ~/.claude-mem/.env, not process.env
}
});
// Process SDK messages
for await (const message of queryResult) {
// Capture memory session ID from first SDK message (any type has session_id)
// This enables resume for subsequent generator starts within the same user session
if (!session.memorySessionId && message.session_id) {
// Capture or update memory session ID from SDK message
// IMPORTANT: The SDK may return a DIFFERENT session_id on resume than what we sent!
// We must always sync the DB to match what the SDK actually uses.
//
// MULTI-TERMINAL COLLISION FIX (FK constraint bug):
// Use ensureMemorySessionIdRegistered() instead of updateMemorySessionId() because:
// 1. It's idempotent - safe to call multiple times
// 2. It verifies the update happened (SELECT before UPDATE)
// 3. Consistent with ResponseProcessor's usage pattern
// This ensures FK constraint compliance BEFORE any observations are stored.
if (message.session_id && message.session_id !== session.memorySessionId) {
const previousId = session.memorySessionId;
session.memorySessionId = message.session_id;
// Persist to database for cross-restart recovery
this.dbManager.getSessionStore().updateMemorySessionId(
// Persist to database IMMEDIATELY for FK constraint compliance
// This must happen BEFORE any observations referencing this ID are stored
this.dbManager.getSessionStore().ensureMemorySessionIdRegistered(
session.sessionDbId,
message.session_id
);
// Verify the update by reading back from DB
const verification = this.dbManager.getSessionStore().getSessionById(session.sessionDbId);
const dbVerified = verification?.memory_session_id === message.session_id;
logger.info('SESSION', `MEMORY_ID_CAPTURED | sessionDbId=${session.sessionDbId} | memorySessionId=${message.session_id} | dbVerified=${dbVerified}`, {
const logMessage = previousId
? `MEMORY_ID_CHANGED | sessionDbId=${session.sessionDbId} | from=${previousId} | to=${message.session_id} | dbVerified=${dbVerified}`
: `MEMORY_ID_CAPTURED | sessionDbId=${session.sessionDbId} | memorySessionId=${message.session_id} | dbVerified=${dbVerified}`;
logger.info('SESSION', logMessage, {
sessionId: session.sessionDbId,
memorySessionId: message.session_id
memorySessionId: message.session_id,
previousId
});
if (!dbVerified) {
logger.error('SESSION', `MEMORY_ID_MISMATCH | sessionDbId=${session.sessionDbId} | expected=${message.session_id} | got=${verification?.memory_session_id}`, {
@@ -141,7 +179,7 @@ export class SDKAgent {
});
}
// Debug-level alignment log for detailed tracing
logger.debug('SDK', `[ALIGNMENT] Captured | contentSessionId=${session.contentSessionId} → memorySessionId=${message.session_id} | Future prompts will resume with this ID`);
logger.debug('SDK', `[ALIGNMENT] ${previousId ? 'Updated' : 'Captured'} | contentSessionId=${session.contentSessionId} → memorySessionId=${message.session_id} | Future prompts will resume with this ID`);
}
// Handle assistant messages
@@ -151,6 +189,14 @@ export class SDKAgent {
? content.filter((c: any) => c.type === 'text').map((c: any) => c.text).join('\n')
: typeof content === 'string' ? content : '';
// Check for context overflow - prevents infinite retry loops
if (textContent.includes('prompt is too long') ||
textContent.includes('context window')) {
logger.error('SDK', 'Context overflow detected - terminating session');
session.abortController.abort();
return;
}
const responseSize = textContent.length;
// Capture token state BEFORE updating (for delta calculation)
@@ -195,6 +241,17 @@ export class SDKAgent {
}, truncatedResponse);
}
// Detect fatal context overflow and terminate gracefully (issue #870)
if (typeof textContent === 'string' && textContent.includes('Prompt is too long')) {
throw new Error('Claude session context overflow: prompt is too long');
}
// Detect invalid API key — SDK returns this as response text, not an error.
// Throw so it surfaces in health endpoint and prevents silent failures.
if (typeof textContent === 'string' && textContent.includes('Invalid API key')) {
throw new Error('Invalid API key: check your API key configuration in ~/.claude-mem/settings.json or ~/.claude-mem/.env');
}
// Parse and process response using shared ResponseProcessor
await processAgentResponse(
textContent,
@@ -297,6 +354,10 @@ export class SDKAgent {
// Consume pending messages from SessionManager (event-driven, no polling)
for await (const message of this.sessionManager.getMessageIterator(session.sessionDbId)) {
// CLAIM-CONFIRM: Track message ID for confirmProcessed() after successful storage
// The message is now in 'processing' status in DB until ResponseProcessor calls confirmProcessed()
session.processingMessageIds.push(message._persistentId);
// Capture cwd from each message for worktree support
if (message.cwd) {
cwdTracker.lastCwd = message.cwd;
+53 -6
View File
@@ -106,6 +106,15 @@ export class SessionManager {
memory_session_id: dbSession.memory_session_id
});
// Log warning if we're discarding a stale memory_session_id (Issue #817)
if (dbSession.memory_session_id) {
logger.warn('SESSION', `Discarding stale memory_session_id from previous worker instance (Issue #817)`, {
sessionDbId,
staleMemorySessionId: dbSession.memory_session_id,
reason: 'SDK context lost on worker restart - will capture new ID'
});
}
// Use currentUserPrompt if provided, otherwise fall back to database (first prompt)
const userPrompt = currentUserPrompt || dbSession.user_prompt;
@@ -124,11 +133,15 @@ export class SessionManager {
}
// Create active session
// Load memorySessionId from database if previously captured (enables resume across restarts)
// CRITICAL: Do NOT load memorySessionId from database here (Issue #817)
// When creating a new in-memory session, any database memory_session_id is STALE
// because the SDK context was lost when the worker restarted. The SDK agent will
// capture a new memorySessionId on the first response and persist it.
// Loading stale memory_session_id causes "No conversation found" crashes on resume.
session = {
sessionDbId,
contentSessionId: dbSession.content_session_id,
memorySessionId: dbSession.memory_session_id || null,
memorySessionId: null, // Always start fresh - SDK will capture new ID
project: dbSession.project,
userPrompt,
pendingMessages: [],
@@ -140,13 +153,16 @@ export class SessionManager {
cumulativeOutputTokens: 0,
earliestPendingTimestamp: null,
conversationHistory: [], // Initialize empty - will be populated by agents
currentProvider: null // Will be set when generator starts
currentProvider: null, // Will be set when generator starts
consecutiveRestarts: 0, // Track consecutive restart attempts to prevent infinite loops
processingMessageIds: [] // CLAIM-CONFIRM: Track message IDs for confirmProcessed()
};
logger.debug('SESSION', 'Creating new session object', {
logger.debug('SESSION', 'Creating new session object (memorySessionId cleared to prevent stale resume)', {
sessionDbId,
contentSessionId: dbSession.content_session_id,
memorySessionId: dbSession.memory_session_id || '(none - fresh session)',
dbMemorySessionId: dbSession.memory_session_id || '(none in DB)',
memorySessionId: '(cleared - will capture fresh from SDK)',
lastPromptNumber: promptNumber || this.dbManager.getSessionStore().getPromptNumberFromUserPrompts(dbSession.content_session_id)
});
@@ -303,6 +319,28 @@ export class SessionManager {
}
}
/**
* Remove session from in-memory maps and notify without awaiting generator.
* Used when SDK resume fails and we give up (no fallback): avoids deadlock
* from deleteSession() awaiting the same generator promise we're inside.
*/
removeSessionImmediate(sessionDbId: number): void {
const session = this.sessions.get(sessionDbId);
if (!session) return;
this.sessions.delete(sessionDbId);
this.sessionQueues.delete(sessionDbId);
logger.info('SESSION', 'Session removed (orphaned after SDK termination)', {
sessionId: sessionDbId,
project: session.project
});
if (this.onSessionDeletedCallback) {
this.onSessionDeletedCallback();
}
}
/**
* Shutdown all active sessions
*/
@@ -378,7 +416,16 @@ export class SessionManager {
const processor = new SessionQueueProcessor(this.getPendingStore(), emitter);
// Use the robust iterator - messages are deleted on claim (no tracking needed)
for await (const message of processor.createIterator(sessionDbId, session.abortController.signal)) {
// CRITICAL: Pass onIdleTimeout callback that triggers abort to kill the subprocess
// Without this, the iterator returns but the Claude subprocess stays alive as a zombie
for await (const message of processor.createIterator({
sessionDbId,
signal: session.abortController.signal,
onIdleTimeout: () => {
logger.info('SESSION', 'Triggering abort due to idle timeout to kill subprocess', { sessionDbId });
session.abortController.abort();
}
})) {
// Track earliest timestamp for accurate observation timestamps
// This ensures backlog messages get their original timestamps, not current time
if (session.earliestPendingTimestamp === null) {
-7
View File
@@ -1,7 +0,0 @@
<claude-mem-context>
# Recent Activity
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
*No recent activity*
</claude-mem-context>
+44 -14
View File
@@ -16,6 +16,8 @@ import { parseObservations, parseSummary, type ParsedObservation, type ParsedSum
import { updateCursorContextForProject } from '../../integrations/CursorHooksInstaller.js';
import { updateFolderClaudeMdFiles } from '../../../utils/claude-md-utils.js';
import { getWorkerPort } from '../../../shared/worker-utils.js';
import { SettingsDefaultsManager } from '../../../shared/SettingsDefaultsManager.js';
import { USER_SETTINGS_PATH } from '../../../shared/paths.js';
import type { ActiveSession } from '../../worker-types.js';
import type { DatabaseManager } from '../DatabaseManager.js';
import type { SessionManager } from '../SessionManager.js';
@@ -74,6 +76,14 @@ export async function processAgentResponse(
throw new Error('Cannot store observations: memorySessionId not yet captured');
}
// SAFETY NET (Issue #846 / Multi-terminal FK fix):
// The PRIMARY fix is in SDKAgent.ts where ensureMemorySessionIdRegistered() is called
// immediately when the SDK returns a memory_session_id. This call is a defensive safety net
// in case the DB was somehow not updated (race condition, crash, etc.).
// In multi-terminal scenarios, createSDKSession() now resets memory_session_id to NULL
// for each new generator, ensuring clean isolation.
sessionStore.ensureMemorySessionIdRegistered(session.sessionDbId, session.memorySessionId);
// Log pre-storage with session ID chain for verification
logger.info('DB', `STORING | sessionDbId=${session.sessionDbId} | memorySessionId=${session.memorySessionId} | obsCount=${observations.length} | hasSummary=${!!summaryForStore}`, {
sessionId: session.sessionDbId,
@@ -98,6 +108,18 @@ export async function processAgentResponse(
memorySessionId: session.memorySessionId
});
// CLAIM-CONFIRM: Now that storage succeeded, confirm all processing messages (delete from queue)
// This is the critical step that prevents message loss on generator crash
const pendingStore = sessionManager.getPendingMessageStore();
for (const messageId of session.processingMessageIds) {
pendingStore.confirmProcessed(messageId);
}
if (session.processingMessageIds.length > 0) {
logger.debug('QUEUE', `CONFIRMED_BATCH | sessionDbId=${session.sessionDbId} | count=${session.processingMessageIds.length} | ids=[${session.processingMessageIds.join(',')}]`);
}
// Clear the tracking array after confirmation
session.processingMessageIds = [];
// AFTER transaction commits - async operations (can fail safely without data loss)
await syncAndBroadcastObservations(
observations,
@@ -215,21 +237,29 @@ async function syncAndBroadcastObservations(
// Update folder CLAUDE.md files for touched folders (fire-and-forget)
// This runs per-observation batch to ensure folders are updated as work happens
const allFilePaths: string[] = [];
for (const obs of observations) {
allFilePaths.push(...(obs.files_modified || []));
allFilePaths.push(...(obs.files_read || []));
}
// Only runs if CLAUDE_MEM_FOLDER_CLAUDEMD_ENABLED is true (default: false)
const settings = SettingsDefaultsManager.loadFromFile(USER_SETTINGS_PATH);
// Handle both string 'true' and boolean true from JSON settings
const settingValue = settings.CLAUDE_MEM_FOLDER_CLAUDEMD_ENABLED;
const folderClaudeMdEnabled = settingValue === 'true' || settingValue === true;
if (allFilePaths.length > 0) {
updateFolderClaudeMdFiles(
allFilePaths,
session.project,
getWorkerPort(),
projectRoot
).catch(error => {
logger.warn('FOLDER_INDEX', 'CLAUDE.md update failed (non-critical)', { project: session.project }, error as Error);
});
if (folderClaudeMdEnabled) {
const allFilePaths: string[] = [];
for (const obs of observations) {
allFilePaths.push(...(obs.files_modified || []));
allFilePaths.push(...(obs.files_read || []));
}
if (allFilePaths.length > 0) {
updateFolderClaudeMdFiles(
allFilePaths,
session.project,
getWorkerPort(),
projectRoot
).catch(error => {
logger.warn('FOLDER_INDEX', 'CLAUDE.md update failed (non-critical)', { project: session.project }, error as Error);
});
}
}
}
-7
View File
@@ -1,7 +0,0 @@
<claude-mem-context>
# Recent Activity
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
*No recent activity*
</claude-mem-context>
-7
View File
@@ -1,7 +0,0 @@
<claude-mem-context>
# Recent Activity
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
*No recent activity*
</claude-mem-context>
+15 -2
View File
@@ -24,8 +24,21 @@ export function createMiddleware(
// JSON parsing with 50mb limit
middlewares.push(express.json({ limit: '50mb' }));
// CORS
middlewares.push(cors());
// CORS - restrict to localhost origins only
middlewares.push(cors({
origin: (origin, callback) => {
// Allow: requests without Origin header (hooks, curl, CLI tools)
// Allow: localhost and 127.0.0.1 origins
if (!origin ||
origin.startsWith('http://localhost:') ||
origin.startsWith('http://127.0.0.1:')) {
callback(null, true);
} else {
callback(new Error('CORS not allowed'));
}
},
credentials: false
}));
// HTTP request/response logging
middlewares.push((req: Request, res: Response, next: NextFunction) => {
-97
View File
@@ -1,97 +0,0 @@
<claude-mem-context>
# Recent Activity
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
### Dec 5, 2025
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #20734 | 9:08 PM | 🔵 | SearchRoutes Context Injection Endpoint with Dynamic Import | ~614 |
| #20548 | 8:21 PM | 🔵 | Context generator imported from services directory in worker | ~334 |
| #20547 | " | 🔵 | Context injection route implementation in SearchRoutes.ts | ~289 |
### Dec 7, 2025
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #21742 | 10:16 PM | 🔵 | SessionRoutes Analysis: Identified 10+ Scattered Broadcast Calls | ~540 |
### Dec 8, 2025
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #22301 | 9:44 PM | 🔵 | Privacy Validation in Observation Processing | ~399 |
| #22296 | 9:43 PM | 🔵 | SessionRoutes HTTP Endpoints and SDK Agent Lifecycle | ~442 |
| #22222 | 8:29 PM | 🔵 | Found waiting logic in SessionRoutes but it may not be working correctly | ~359 |
| #22005 | 5:40 PM | 🔵 | handleObservationsByClaudeId Current Implementation | ~443 |
| #22004 | " | 🔵 | Legacy Observation Handling Pattern Identified | ~337 |
| #22003 | " | 🔵 | SessionRoutes Architecture Confirmed | ~354 |
| #21969 | 5:22 PM | 🟣 | Worker Routes Pass tool_use_id to SessionManager Queue | ~290 |
| #21968 | " | ✅ | Worker Endpoint Extracts toolUseId from Observation Request | ~243 |
| #21962 | 5:21 PM | 🟣 | Implemented handleGetObservationsForToolUse Endpoint Handler | ~325 |
| #21961 | " | 🟣 | Added GET Endpoint for Fetching Observations by Tool Use ID | ~272 |
| #21951 | 5:18 PM | 🔵 | Worker SessionRoutes Architecture and Endpoints Reviewed | ~418 |
| #21948 | 5:09 PM | 🟣 | Implemented PreToolUse Endpoint Handler | ~334 |
| #21947 | 5:07 PM | 🟣 | Added PreToolUse Route Registration | ~287 |
### Dec 9, 2025
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #23143 | 6:42 PM | ✅ | Updated Skip Tools Logic to Use USER_SETTINGS_PATH Constant | ~150 |
| #23142 | " | ✅ | Fixed Settings Path Import in SessionRoutes | ~148 |
| #23140 | 6:41 PM | 🟣 | Implemented Skip Tools Filtering in Observations Endpoint | ~386 |
| #23138 | " | ✅ | Added SettingsDefaultsManager and Paths Imports to SessionRoutes | ~222 |
| #23136 | " | 🔵 | SessionRoutes handleObservationsByClaudeId Handler Structure | ~329 |
| #23007 | 4:02 PM | 🔵 | Settings Write Implementation Using Nested Schema | ~398 |
| #22859 | 2:28 PM | 🔴 | Fixed Python Version Validation to Support 3.10+ | ~322 |
| #22854 | 2:27 PM | 🔵 | Located Python Version Validation Regex in SettingsRoutes | ~316 |
### Dec 10, 2025
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #23593 | 5:52 PM | 🔵 | SearchRoutes Handler Pattern | ~268 |
| #23588 | 5:51 PM | 🔵 | Search Routes HTTP API Integration | ~281 |
### Dec 14, 2025
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #26253 | 8:31 PM | 🔵 | SearchRoutes Confirms Context Endpoints Use generateContext, Search Uses SearchManager | ~397 |
| #25689 | 4:23 PM | 🔵 | SessionRoutes queueSummarize receives messages but doesn't persist them to database | ~496 |
### Dec 15, 2025
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #27043 | 6:04 PM | 🔵 | Subagent confirms no version switcher UI exists, only orphaned backend infrastructure | ~539 |
| #27041 | 6:03 PM | 🔵 | Branch switching code isolated to two backend files, no frontend UI components | ~473 |
| #27037 | 6:02 PM | 🔵 | Branch switching functionality exists in SettingsRoutes with UI switcher removal intent | ~463 |
### Dec 16, 2025
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #27414 | 3:25 PM | 🔵 | Batch Observations Endpoint Already Implemented | ~330 |
### Dec 19, 2025
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #30077 | 8:05 PM | 🔵 | SessionRoutes HTTP API Manages SDK Agent Lifecycle and Message Queue | ~516 |
### Dec 26, 2025
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #32949 | 10:55 PM | 🔵 | Complete settings persistence flow for Xiaomi MIMO v2 Flash model | ~320 |
| #32939 | 10:53 PM | 🔵 | Settings API routes handle model configuration persistence | ~288 |
### Dec 30, 2025
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #34491 | 2:28 PM | 🔵 | SessionRoutes Implements Multi-Provider Agent Management | ~635 |
</claude-mem-context>
@@ -0,0 +1,93 @@
/**
* Memory Routes
*
* Handles manual memory/observation saving.
* POST /api/memory/save - Save a manual memory observation
*/
import express, { Request, Response } from 'express';
import { BaseRouteHandler } from '../BaseRouteHandler.js';
import { logger } from '../../../../utils/logger.js';
import type { DatabaseManager } from '../../DatabaseManager.js';
export class MemoryRoutes extends BaseRouteHandler {
constructor(
private dbManager: DatabaseManager,
private defaultProject: string
) {
super();
}
setupRoutes(app: express.Application): void {
app.post('/api/memory/save', this.handleSaveMemory.bind(this));
}
/**
* POST /api/memory/save - Save a manual memory/observation
* Body: { text: string, title?: string, project?: string }
*/
private handleSaveMemory = this.wrapHandler(async (req: Request, res: Response): Promise<void> => {
const { text, title, project } = req.body;
const targetProject = project || this.defaultProject;
if (!text || typeof text !== 'string' || text.trim().length === 0) {
this.badRequest(res, 'text is required and must be non-empty');
return;
}
const sessionStore = this.dbManager.getSessionStore();
const chromaSync = this.dbManager.getChromaSync();
// 1. Get or create manual session for project
const memorySessionId = sessionStore.getOrCreateManualSession(targetProject);
// 2. Build observation
const observation = {
type: 'discovery', // Use existing valid type
title: title || text.substring(0, 60).trim() + (text.length > 60 ? '...' : ''),
subtitle: 'Manual memory',
facts: [] as string[],
narrative: text,
concepts: [] as string[],
files_read: [] as string[],
files_modified: [] as string[]
};
// 3. Store to SQLite
const result = sessionStore.storeObservation(
memorySessionId,
targetProject,
observation,
0, // promptNumber
0 // discoveryTokens
);
logger.info('HTTP', 'Manual observation saved', {
id: result.id,
project: targetProject,
title: observation.title
});
// 4. Sync to ChromaDB (async, fire-and-forget)
chromaSync.syncObservation(
result.id,
memorySessionId,
targetProject,
observation,
0,
result.createdAtEpoch,
0
).catch(err => {
logger.error('CHROMA', 'ChromaDB sync failed', { id: result.id }, err as Error);
});
// 5. Return success
res.json({
success: true,
id: result.id,
title: observation.title,
project: targetProject,
message: `Memory saved as observation #${result.id}`
});
});
}
@@ -24,6 +24,8 @@ import { USER_SETTINGS_PATH } from '../../../../shared/paths.js';
export class SessionRoutes extends BaseRouteHandler {
private completionHandler: SessionCompletionHandler;
private spawnInProgress = new Map<number, boolean>();
private crashRecoveryScheduled = new Set<number>();
constructor(
private sessionManager: SessionManager,
@@ -91,10 +93,17 @@ export class SessionRoutes extends BaseRouteHandler {
const session = this.sessionManager.getSession(sessionDbId);
if (!session) return;
// GUARD: Prevent duplicate spawns
if (this.spawnInProgress.get(sessionDbId)) {
logger.debug('SESSION', 'Spawn already in progress, skipping', { sessionDbId, source });
return;
}
const selectedProvider = this.getSelectedProvider();
// Start generator if not running
if (!session.generatorPromise) {
this.spawnInProgress.set(sessionDbId, true);
this.startGeneratorWithProvider(session, selectedProvider, source);
return;
}
@@ -122,12 +131,26 @@ export class SessionRoutes extends BaseRouteHandler {
): void {
if (!session) return;
// Reset AbortController if it was previously aborted
// This fixes the bug where a session gets stuck in an infinite "Generator aborted" loop
// after its AbortController was aborted (e.g., from a previous generator exit)
if (session.abortController.signal.aborted) {
logger.debug('SESSION', 'Resetting aborted AbortController before starting generator', {
sessionId: session.sessionDbId
});
session.abortController = new AbortController();
}
const agent = provider === 'openrouter' ? this.openRouterAgent : (provider === 'gemini' ? this.geminiAgent : this.sdkAgent);
const agentName = provider === 'openrouter' ? 'OpenRouter' : (provider === 'gemini' ? 'Gemini' : 'Claude SDK');
// Use database count for accurate telemetry (in-memory array is always empty due to FK constraint fix)
const pendingStore = this.sessionManager.getPendingMessageStore();
const actualQueueDepth = pendingStore.getPendingCount(session.sessionDbId);
logger.info('SESSION', `Generator auto-starting (${source}) using ${agentName}`, {
sessionId: session.sessionDbId,
queueDepth: session.pendingMessages.length,
queueDepth: actualQueueDepth,
historyLength: session.conversationHistory.length
});
@@ -163,6 +186,7 @@ export class SessionRoutes extends BaseRouteHandler {
})
.finally(() => {
const sessionDbId = session.sessionDbId;
this.spawnInProgress.delete(sessionDbId);
const wasAborted = session.abortController.signal.aborted;
if (wasAborted) {
@@ -175,16 +199,43 @@ export class SessionRoutes extends BaseRouteHandler {
session.currentProvider = null;
this.workerService.broadcastProcessingStatus();
// Crash recovery: If not aborted and still has work, restart
// Crash recovery: If not aborted and still has work, restart (with limit)
if (!wasAborted) {
try {
const pendingStore = this.sessionManager.getPendingMessageStore();
const pendingCount = pendingStore.getPendingCount(sessionDbId);
// CRITICAL: Limit consecutive restarts to prevent infinite loops
// This prevents runaway API costs when there's a persistent error (e.g., memorySessionId not captured)
const MAX_CONSECUTIVE_RESTARTS = 3;
if (pendingCount > 0) {
// GUARD: Prevent duplicate crash recovery spawns
if (this.crashRecoveryScheduled.has(sessionDbId)) {
logger.debug('SESSION', 'Crash recovery already scheduled', { sessionDbId });
return;
}
session.consecutiveRestarts = (session.consecutiveRestarts || 0) + 1;
if (session.consecutiveRestarts > MAX_CONSECUTIVE_RESTARTS) {
logger.error('SESSION', `CRITICAL: Generator restart limit exceeded - stopping to prevent runaway costs`, {
sessionId: sessionDbId,
pendingCount,
consecutiveRestarts: session.consecutiveRestarts,
maxRestarts: MAX_CONSECUTIVE_RESTARTS,
action: 'Generator will NOT restart. Check logs for root cause. Messages remain in pending state.'
});
// Don't restart - abort to prevent further API calls
session.abortController.abort();
return;
}
logger.info('SESSION', `Restarting generator after crash/exit with pending work`, {
sessionId: sessionDbId,
pendingCount
pendingCount,
consecutiveRestarts: session.consecutiveRestarts,
maxRestarts: MAX_CONSECUTIVE_RESTARTS
});
// Abort OLD controller before replacing to prevent child process leaks
@@ -192,16 +243,24 @@ export class SessionRoutes extends BaseRouteHandler {
session.abortController = new AbortController();
oldController.abort();
// Small delay before restart
this.crashRecoveryScheduled.add(sessionDbId);
// Exponential backoff: 1s, 2s, 4s for subsequent restarts
const backoffMs = Math.min(1000 * Math.pow(2, session.consecutiveRestarts - 1), 8000);
// Delay before restart with exponential backoff
setTimeout(() => {
this.crashRecoveryScheduled.delete(sessionDbId);
const stillExists = this.sessionManager.getSession(sessionDbId);
if (stillExists && !stillExists.generatorPromise) {
this.startGeneratorWithProvider(stillExists, this.getSelectedProvider(), 'crash-recovery');
}
}, 1000);
}, backoffMs);
} else {
// No pending work - abort to kill the child process
session.abortController.abort();
// Reset restart counter on successful completion
session.consecutiveRestarts = 0;
logger.debug('SESSION', 'Aborted controller after natural completion', {
sessionId: sessionDbId
});
@@ -231,6 +290,7 @@ export class SessionRoutes extends BaseRouteHandler {
app.post('/api/sessions/init', this.handleSessionInitByClaudeId.bind(this));
app.post('/api/sessions/observations', this.handleObservationsByClaudeId.bind(this));
app.post('/api/sessions/summarize', this.handleSummarizeByClaudeId.bind(this));
app.post('/api/sessions/complete', this.handleCompleteByClaudeId.bind(this));
}
/**
@@ -291,8 +351,8 @@ export class SessionRoutes extends BaseRouteHandler {
});
}
// Start agent in background using the helper method
this.startGeneratorWithProvider(session, this.getSelectedProvider(), 'init');
// Idempotent: ensure generator is running (matches handleObservations / handleSummarize)
this.ensureGeneratorRunning(sessionDbId, 'init');
// Broadcast session started event
this.eventBroadcaster.broadcastSessionStarted(sessionDbId, session.project);
@@ -362,11 +422,15 @@ export class SessionRoutes extends BaseRouteHandler {
return;
}
// Use database count for accurate queue length (in-memory array is always empty due to FK constraint fix)
const pendingStore = this.sessionManager.getPendingMessageStore();
const queueLength = pendingStore.getPendingCount(sessionDbId);
res.json({
status: 'active',
sessionDbId,
project: session.project,
queueLength: session.pendingMessages.length,
queueLength,
uptime: Date.now() - session.startTime
});
});
@@ -531,6 +595,54 @@ export class SessionRoutes extends BaseRouteHandler {
res.json({ status: 'queued' });
});
/**
* Complete session by contentSessionId (session-complete hook uses this)
* POST /api/sessions/complete
* Body: { contentSessionId }
*
* Removes session from active sessions map, allowing orphan reaper to
* clean up any remaining subprocesses.
*
* Fixes Issue #842: Sessions stay in map forever, reaper thinks all active.
*/
private handleCompleteByClaudeId = this.wrapHandler(async (req: Request, res: Response): Promise<void> => {
const { contentSessionId } = req.body;
logger.info('HTTP', '→ POST /api/sessions/complete', { contentSessionId });
if (!contentSessionId) {
return this.badRequest(res, 'Missing contentSessionId');
}
const store = this.dbManager.getSessionStore();
// Look up sessionDbId from contentSessionId (createSDKSession is idempotent)
// Pass empty strings - we only need the ID lookup, not to create a new session
const sessionDbId = store.createSDKSession(contentSessionId, '', '');
// Check if session is in the active sessions map
const activeSession = this.sessionManager.getSession(sessionDbId);
if (!activeSession) {
// Session may not be in memory (already completed or never initialized)
logger.debug('SESSION', 'session-complete: Session not in active map', {
contentSessionId,
sessionDbId
});
res.json({ status: 'skipped', reason: 'not_active' });
return;
}
// Complete the session (removes from active sessions map)
await this.completionHandler.completeByDbId(sessionDbId);
logger.info('SESSION', 'Session completed via API', {
contentSessionId,
sessionDbId
});
res.json({ status: 'completed', sessionDbId });
});
/**
* Initialize session by contentSessionId (new-hook uses this)
* POST /api/sessions/init
@@ -121,6 +121,7 @@ export class SettingsRoutes extends BaseRouteHandler {
// Feature Toggles
'CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY',
'CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE',
'CLAUDE_MEM_FOLDER_CLAUDEMD_ENABLED',
];
for (const key of settingKeys) {
@@ -241,9 +242,9 @@ export class SettingsRoutes extends BaseRouteHandler {
// Validate CLAUDE_MEM_GEMINI_MODEL
if (settings.CLAUDE_MEM_GEMINI_MODEL) {
const validGeminiModels = ['gemini-2.5-flash-lite', 'gemini-2.5-flash', 'gemini-3-flash'];
const validGeminiModels = ['gemini-2.5-flash-lite', 'gemini-2.5-flash', 'gemini-3-flash-preview'];
if (!validGeminiModels.includes(settings.CLAUDE_MEM_GEMINI_MODEL)) {
return { valid: false, error: 'CLAUDE_MEM_GEMINI_MODEL must be one of: gemini-2.5-flash-lite, gemini-2.5-flash, gemini-3-flash' };
return { valid: false, error: 'CLAUDE_MEM_GEMINI_MODEL must be one of: gemini-2.5-flash-lite, gemini-2.5-flash, gemini-3-flash-preview' };
}
}
-7
View File
@@ -1,7 +0,0 @@
<claude-mem-context>
# Recent Activity
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
*No recent activity*
</claude-mem-context>
@@ -1,21 +0,0 @@
<claude-mem-context>
# Recent Activity
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
### Jan 3, 2026
**DateFilter.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #36670 | 11:37 PM | ✅ | Resolved merge conflicts by accepting branch changes for 39 files | ~435 |
| #36523 | 9:34 PM | 🔴 | Fixed TypeScript Type Import Issues in Worker Services | ~386 |
| #36519 | " | 🔴 | Fixed Type Import Issues Preventing Worker Tests | ~308 |
| #36516 | 9:33 PM | 🔴 | Fixed TypeScript Type Import Issues in Worker Search Modules | ~377 |
| #36390 | 8:50 PM | 🔄 | Comprehensive Monolith Refactor with Modular Architecture | ~724 |
**ProjectFilter.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #36529 | 9:34 PM | 🔵 | Search Module Architecture Discovery | ~302 |
</claude-mem-context>
@@ -1,7 +0,0 @@
<claude-mem-context>
# Recent Activity
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
*No recent activity*
</claude-mem-context>
@@ -167,6 +167,13 @@ export class ChromaSearchStrategy extends BaseSearchStrategy implements SearchSt
/**
* Filter results by recency (90-day window)
*
* IMPORTANT: ChromaSync.queryChroma() returns deduplicated `ids` (unique sqlite_ids)
* but the `metadatas` array may contain multiple entries per sqlite_id (e.g., one
* observation can have narrative + multiple facts as separate Chroma documents).
*
* This method iterates over the deduplicated `ids` and finds the first matching
* metadata for each ID to avoid array misalignment issues.
*/
private filterByRecency(chromaResults: {
ids: number[];
@@ -174,10 +181,19 @@ export class ChromaSearchStrategy extends BaseSearchStrategy implements SearchSt
}): Array<{ id: number; meta: ChromaMetadata }> {
const cutoff = Date.now() - SEARCH_CONSTANTS.RECENCY_WINDOW_MS;
return chromaResults.metadatas
.map((meta, idx) => ({
id: chromaResults.ids[idx],
meta
// Build a map from sqlite_id to first metadata for efficient lookup
const metadataByIdMap = new Map<number, ChromaMetadata>();
for (const meta of chromaResults.metadatas) {
if (meta?.sqlite_id !== undefined && !metadataByIdMap.has(meta.sqlite_id)) {
metadataByIdMap.set(meta.sqlite_id, meta);
}
}
// Iterate over deduplicated ids and get corresponding metadata
return chromaResults.ids
.map(id => ({
id,
meta: metadataByIdMap.get(id) as ChromaMetadata
}))
.filter(item => item.meta && item.meta.created_at_epoch > cutoff);
}
-103
View File
@@ -1,103 +0,0 @@
<claude-mem-context>
# Recent Activity
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
### Dec 7, 2025
**SessionCompletionHandler.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #21829 | 11:05 PM | 🔄 | Massive refactor adds 8,671 lines and removes 5,585 lines across 60 files | ~619 |
| #21825 | 11:00 PM | 🔵 | SessionCompletionHandler methods called 3 times in SessionRoutes | ~342 |
| #21824 | 10:59 PM | 🔵 | SessionEventBroadcaster methods called 7 times across SessionRoutes and SessionCompletionHandler | ~398 |
| #21822 | " | 🔵 | SessionEventBroadcaster instantiated in WorkerService and injected into routes and handlers | ~372 |
| #21818 | 10:58 PM | 🔵 | SessionCompletionHandler is instantiated in SessionRoutes | ~282 |
| #21817 | " | 🔵 | SessionCompletionHandler consolidates session completion logic | ~414 |
| #21807 | 10:49 PM | ⚖️ | KISS Audit Identified 587 Lines of Ceremonial Complexity | ~699 |
| #21794 | 10:46 PM | 🔵 | SessionCompletionHandler Consolidates Duplicate Completion Logic | ~341 |
| #21764 | 10:23 PM | ✅ | Phase 4 Build and Deployment Successful | ~376 |
| #21759 | 10:21 PM | 🟣 | SessionCompletionHandler Service Created | ~426 |
### Dec 11, 2025
**SessionCompletionHandler.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #23962 | 1:59 PM | 🔵 | Services Layer Implements Full Backend Architecture | ~490 |
### Dec 14, 2025
**SessionCompletionHandler.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #26088 | 7:32 PM | 🔵 | API Endpoint Architecture Discovery | ~416 |
### Dec 20, 2025
**SessionCompletionHandler.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #30725 | 5:12 PM | 🔵 | Revealed extensive work-in-progress changes across hook and worker systems | ~479 |
| #30569 | 4:56 PM | 🔄 | SessionCompletionHandler Broadcasting Implementation | ~264 |
| #30568 | " | 🔄 | SessionCompletionHandler Event Broadcasting Refactor | ~282 |
| #30566 | " | 🔵 | Session Completion Handler Consolidation | ~323 |
### Dec 24, 2025
**SessionCompletionHandler.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #32350 | 8:42 PM | 🔵 | Detailed Cleanup Hook Evolution Documentation Retrieved | ~597 |
| #32316 | 8:41 PM | 🔄 | Removed markSessionComplete method from DatabaseManager | ~251 |
| #32194 | 7:42 PM | 🔵 | Session completion handler implementation analysis | ~329 |
| #32193 | " | 🔵 | Session completion endpoint usage across codebase | ~278 |
| #32182 | 7:15 PM | 🔄 | Removed markSessionComplete database call from session completion flow | ~316 |
| #32179 | 7:11 PM | 🔄 | SessionCompletionHandler switched to direct SQL query | ~273 |
| #32153 | 6:40 PM | 🔵 | Session Identifier Architecture Across Codebase | ~529 |
### Dec 25, 2025
**SessionCompletionHandler.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #32597 | 8:40 PM | 🔵 | Identified session completion mechanism and potential method discrepancy | ~470 |
| #32456 | 5:41 PM | ✅ | Completed merge of main branch into feature/titans-phase1-3 | ~354 |
| #32198 | 7:41 PM | 🔄 | Removed redundant SessionEnd cleanup hook | ~317 |
### Dec 27, 2025
**SessionCompletionHandler.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #33099 | 7:10 PM | 🔵 | SessionCompletionHandler Manual Session Termination Flow | ~348 |
### Dec 28, 2025
**SessionCompletionHandler.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #33328 | 3:10 PM | 🟣 | Merged centralized logger and session continuity diagnostics to main | ~397 |
| #33280 | 3:07 PM | 🔄 | Logger coverage refactor for background services | ~428 |
### Dec 30, 2025
**SessionCompletionHandler.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #34388 | 1:40 PM | 🔵 | SessionCompletionHandler Relies on SessionManager Abort Without Process Cleanup | ~309 |
### Dec 31, 2025
**SessionCompletionHandler.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #34707 | 4:45 PM | 🔵 | SessionCompletionHandler Aborts SDK Agent During Cleanup | ~291 |
### Jan 2, 2026
**SessionCompletionHandler.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #35951 | 4:42 PM | 🔵 | Multi-Layer Service Architecture Discovery | ~395 |
</claude-mem-context>
-77
View File
@@ -1,77 +0,0 @@
<claude-mem-context>
# Recent Activity
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
### Dec 7, 2025
**PrivacyCheckValidator.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #21829 | 11:05 PM | 🔄 | Massive refactor adds 8,671 lines and removes 5,585 lines across 60 files | ~619 |
| #21820 | 10:59 PM | 🔵 | PrivacyCheckValidator used twice in SessionRoutes for observation and summarize endpoints | ~303 |
| #21814 | 10:58 PM | 🔵 | PrivacyCheckValidator centralizes user prompt privacy validation | ~359 |
| #21807 | 10:49 PM | ⚖️ | KISS Audit Identified 587 Lines of Ceremonial Complexity | ~699 |
| #21797 | 10:46 PM | 🔵 | PrivacyCheckValidator Implements Single Validation Method | ~349 |
| #21770 | 10:36 PM | 🟣 | Implemented PrivacyCheckValidator for Centralized Privacy Validation | ~318 |
### Dec 8, 2025
**PrivacyCheckValidator.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #22274 | 9:22 PM | 🔵 | Event-Driven Architecture for SDK Response Coordination Fully Mapped | ~1136 |
| #22270 | 9:12 PM | 🔵 | DRY violations identified in endless-mode-v7.1 branch | ~553 |
### Dec 9, 2025
**PrivacyCheckValidator.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #22808 | 2:01 PM | 🔵 | Logger Utility Pattern Identified | ~300 |
| #22750 | 1:27 PM | 🔵 | PrivacyCheckValidator Centralizes Privacy Logic | ~450 |
### Dec 11, 2025
**PrivacyCheckValidator.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #23962 | 1:59 PM | 🔵 | Services Layer Implements Full Backend Architecture | ~490 |
### Dec 20, 2025
**PrivacyCheckValidator.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #30609 | 5:01 PM | 🔄 | Phase 4: Eliminated Over-Engineering in Hook/Worker System | ~504 |
| #30598 | 5:00 PM | 🔄 | Removed PrivacyCheckValidator module | ~201 |
| #30549 | 4:53 PM | 🔵 | PrivacyCheckValidator for User Prompt Filtering | ~325 |
### Dec 24, 2025
**PrivacyCheckValidator.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #32153 | 6:40 PM | 🔵 | Session Identifier Architecture Across Codebase | ~529 |
### Dec 25, 2025
**PrivacyCheckValidator.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #32580 | 8:22 PM | 🔵 | Grep for resetStuckMessages and processing | ~242 |
### Dec 28, 2025
**PrivacyCheckValidator.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #33439 | 10:15 PM | 🔄 | Extended Session ID Renaming to Additional Codebase Components | ~352 |
### Jan 2, 2026
**PrivacyCheckValidator.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #35951 | 4:42 PM | 🔵 | Multi-Layer Service Architecture Discovery | ~395 |
</claude-mem-context>
-2
View File
@@ -1,8 +1,6 @@
<claude-mem-context>
# Recent Activity
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
### Nov 10, 2025
| ID | Time | T | Title | Read |
+272
View File
@@ -0,0 +1,272 @@
/**
* EnvManager - Centralized environment variable management for claude-mem
*
* Provides isolated credential storage in ~/.claude-mem/.env
* This ensures claude-mem uses its own configured credentials,
* not random ANTHROPIC_API_KEY values from project .env files.
*
* Issue #733: SDK was auto-discovering API keys from user's shell environment,
* causing memory operations to bill personal API accounts instead of CLI subscription.
*/
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
import { join, dirname } from 'path';
import { homedir } from 'os';
import { logger } from '../utils/logger.js';
// Path to claude-mem's centralized .env file
const DATA_DIR = join(homedir(), '.claude-mem');
export const ENV_FILE_PATH = join(DATA_DIR, '.env');
// Environment variables to STRIP from subprocess environment (blocklist approach)
// Only ANTHROPIC_API_KEY is stripped because it's the specific variable that causes
// Issue #733: project .env files set ANTHROPIC_API_KEY which the SDK auto-discovers,
// causing memory operations to bill personal API accounts instead of CLI subscription.
//
// All other env vars (ANTHROPIC_AUTH_TOKEN, ANTHROPIC_BASE_URL, system vars, etc.)
// are passed through to avoid breaking CLI authentication, proxies, and platform features.
const BLOCKED_ENV_VARS = [
'ANTHROPIC_API_KEY', // Issue #733: Prevent auto-discovery from project .env files
];
// Credential keys that claude-mem manages
export const MANAGED_CREDENTIAL_KEYS = [
'ANTHROPIC_API_KEY',
'GEMINI_API_KEY',
'OPENROUTER_API_KEY',
];
export interface ClaudeMemEnv {
// Credentials (optional - empty means use CLI billing for Claude)
ANTHROPIC_API_KEY?: string;
GEMINI_API_KEY?: string;
OPENROUTER_API_KEY?: string;
}
/**
* Parse a .env file content into key-value pairs
*/
function parseEnvFile(content: string): Record<string, string> {
const result: Record<string, string> = {};
for (const line of content.split('\n')) {
const trimmed = line.trim();
// Skip empty lines and comments
if (!trimmed || trimmed.startsWith('#')) continue;
// Parse KEY=value format
const eqIndex = trimmed.indexOf('=');
if (eqIndex === -1) continue;
const key = trimmed.slice(0, eqIndex).trim();
let value = trimmed.slice(eqIndex + 1).trim();
// Remove surrounding quotes if present
if ((value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))) {
value = value.slice(1, -1);
}
if (key) {
result[key] = value;
}
}
return result;
}
/**
* Serialize key-value pairs to .env file format
*/
function serializeEnvFile(env: Record<string, string>): string {
const lines: string[] = [
'# claude-mem credentials',
'# This file stores API keys for claude-mem memory agent',
'# Edit this file or use claude-mem settings to configure',
'',
];
for (const [key, value] of Object.entries(env)) {
if (value) {
// Quote values that contain spaces or special characters
const needsQuotes = /[\s#=]/.test(value);
lines.push(`${key}=${needsQuotes ? `"${value}"` : value}`);
}
}
return lines.join('\n') + '\n';
}
/**
* Load credentials from ~/.claude-mem/.env
* Returns empty object if file doesn't exist (means use CLI billing)
*/
export function loadClaudeMemEnv(): ClaudeMemEnv {
if (!existsSync(ENV_FILE_PATH)) {
return {};
}
try {
const content = readFileSync(ENV_FILE_PATH, 'utf-8');
const parsed = parseEnvFile(content);
// Only return managed credential keys
const result: ClaudeMemEnv = {};
if (parsed.ANTHROPIC_API_KEY) result.ANTHROPIC_API_KEY = parsed.ANTHROPIC_API_KEY;
if (parsed.GEMINI_API_KEY) result.GEMINI_API_KEY = parsed.GEMINI_API_KEY;
if (parsed.OPENROUTER_API_KEY) result.OPENROUTER_API_KEY = parsed.OPENROUTER_API_KEY;
return result;
} catch (error) {
logger.warn('ENV', 'Failed to load .env file', { path: ENV_FILE_PATH }, error as Error);
return {};
}
}
/**
* Save credentials to ~/.claude-mem/.env
*/
export function saveClaudeMemEnv(env: ClaudeMemEnv): void {
try {
// Ensure directory exists
if (!existsSync(DATA_DIR)) {
mkdirSync(DATA_DIR, { recursive: true });
}
// Load existing to preserve any extra keys
const existing = existsSync(ENV_FILE_PATH)
? parseEnvFile(readFileSync(ENV_FILE_PATH, 'utf-8'))
: {};
// Update with new values
const updated: Record<string, string> = { ...existing };
// Only update managed keys
if (env.ANTHROPIC_API_KEY !== undefined) {
if (env.ANTHROPIC_API_KEY) {
updated.ANTHROPIC_API_KEY = env.ANTHROPIC_API_KEY;
} else {
delete updated.ANTHROPIC_API_KEY;
}
}
if (env.GEMINI_API_KEY !== undefined) {
if (env.GEMINI_API_KEY) {
updated.GEMINI_API_KEY = env.GEMINI_API_KEY;
} else {
delete updated.GEMINI_API_KEY;
}
}
if (env.OPENROUTER_API_KEY !== undefined) {
if (env.OPENROUTER_API_KEY) {
updated.OPENROUTER_API_KEY = env.OPENROUTER_API_KEY;
} else {
delete updated.OPENROUTER_API_KEY;
}
}
writeFileSync(ENV_FILE_PATH, serializeEnvFile(updated), 'utf-8');
} catch (error) {
logger.error('ENV', 'Failed to save .env file', { path: ENV_FILE_PATH }, error as Error);
throw error;
}
}
/**
* Build a clean environment for spawning SDK subprocesses
*
* Uses a BLOCKLIST approach: inherits the full process environment but strips
* only ANTHROPIC_API_KEY to prevent Issue #733 (accidental billing from project .env files).
*
* All other variables pass through, including:
* - ANTHROPIC_AUTH_TOKEN (CLI subscription auth)
* - ANTHROPIC_BASE_URL (custom proxy endpoints)
* - Platform-specific vars (USERPROFILE, XDG_*, etc.)
*
* If claude-mem has an explicit ANTHROPIC_API_KEY in ~/.claude-mem/.env, it's re-injected
* after stripping, so the managed credential takes precedence over any ambient value.
*
* @param includeCredentials - Whether to include API keys from ~/.claude-mem/.env (default: true)
*/
export function buildIsolatedEnv(includeCredentials: boolean = true): Record<string, string> {
// 1. Start with full process environment
const isolatedEnv: Record<string, string> = {};
for (const [key, value] of Object.entries(process.env)) {
if (value !== undefined && !BLOCKED_ENV_VARS.includes(key)) {
isolatedEnv[key] = value;
}
}
// 2. Override SDK entrypoint marker
isolatedEnv.CLAUDE_CODE_ENTRYPOINT = 'sdk-ts';
// 3. Re-inject managed credentials from claude-mem's .env file
if (includeCredentials) {
const credentials = loadClaudeMemEnv();
// Only add ANTHROPIC_API_KEY if explicitly configured in claude-mem
// If not configured, CLI billing will be used (via ANTHROPIC_AUTH_TOKEN passthrough)
if (credentials.ANTHROPIC_API_KEY) {
isolatedEnv.ANTHROPIC_API_KEY = credentials.ANTHROPIC_API_KEY;
}
// Note: GEMINI_API_KEY and OPENROUTER_API_KEY pass through from process.env,
// but claude-mem's .env takes precedence if configured
if (credentials.GEMINI_API_KEY) {
isolatedEnv.GEMINI_API_KEY = credentials.GEMINI_API_KEY;
}
if (credentials.OPENROUTER_API_KEY) {
isolatedEnv.OPENROUTER_API_KEY = credentials.OPENROUTER_API_KEY;
}
// 4. Pass through Claude CLI's OAuth token if available (fallback for CLI subscription billing)
// When no ANTHROPIC_API_KEY is configured, the spawned CLI uses subscription billing
// which requires either ~/.claude/.credentials.json or CLAUDE_CODE_OAUTH_TOKEN.
// The worker inherits this token from the Claude Code session that started it.
if (!isolatedEnv.ANTHROPIC_API_KEY && process.env.CLAUDE_CODE_OAUTH_TOKEN) {
isolatedEnv.CLAUDE_CODE_OAUTH_TOKEN = process.env.CLAUDE_CODE_OAUTH_TOKEN;
}
}
return isolatedEnv;
}
/**
* Get a specific credential from claude-mem's .env
* Returns undefined if not set (which means use default/CLI billing)
*/
export function getCredential(key: keyof ClaudeMemEnv): string | undefined {
const env = loadClaudeMemEnv();
return env[key];
}
/**
* Set a specific credential in claude-mem's .env
* Pass empty string to remove the credential
*/
export function setCredential(key: keyof ClaudeMemEnv, value: string): void {
const env = loadClaudeMemEnv();
env[key] = value || undefined;
saveClaudeMemEnv(env);
}
/**
* Check if claude-mem has an Anthropic API key configured
* If false, it means CLI billing should be used
*/
export function hasAnthropicApiKey(): boolean {
const env = loadClaudeMemEnv();
return !!env.ANTHROPIC_API_KEY;
}
/**
* Get auth method description for logging
*/
export function getAuthMethodDescription(): string {
if (hasAnthropicApiKey()) {
return 'API key (from ~/.claude-mem/.env)';
}
if (process.env.CLAUDE_CODE_OAUTH_TOKEN) {
return 'Claude Code OAuth token (from parent process)';
}
return 'Claude Code CLI (subscription billing)';
}
+40 -7
View File
@@ -20,8 +20,9 @@ export interface SettingsDefaults {
CLAUDE_MEM_SKIP_TOOLS: string;
// AI Provider Configuration
CLAUDE_MEM_PROVIDER: string; // 'claude' | 'gemini' | 'openrouter'
CLAUDE_MEM_CLAUDE_AUTH_METHOD: string; // 'cli' | 'api' - how Claude provider authenticates
CLAUDE_MEM_GEMINI_API_KEY: string;
CLAUDE_MEM_GEMINI_MODEL: string; // 'gemini-2.5-flash-lite' | 'gemini-2.5-flash' | 'gemini-3-flash'
CLAUDE_MEM_GEMINI_MODEL: string; // 'gemini-2.5-flash-lite' | 'gemini-2.5-flash' | 'gemini-3-flash-preview'
CLAUDE_MEM_GEMINI_RATE_LIMITING_ENABLED: string; // 'true' | 'false' - enable rate limiting for free tier
CLAUDE_MEM_OPENROUTER_API_KEY: string;
CLAUDE_MEM_OPENROUTER_MODEL: string;
@@ -50,6 +51,10 @@ export interface SettingsDefaults {
// Feature Toggles
CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY: string;
CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE: string;
CLAUDE_MEM_FOLDER_CLAUDEMD_ENABLED: string;
// Exclusion Settings
CLAUDE_MEM_EXCLUDED_PROJECTS: string; // Comma-separated glob patterns for excluded project paths
CLAUDE_MEM_FOLDER_MD_EXCLUDE: string; // JSON array of folder paths to exclude from CLAUDE.md generation
// Chroma Vector Database Configuration
CLAUDE_MEM_CHROMA_MODE: string; // 'local' | 'remote'
CLAUDE_MEM_CHROMA_HOST: string;
@@ -73,6 +78,7 @@ export class SettingsDefaultsManager {
CLAUDE_MEM_SKIP_TOOLS: 'ListMcpResourcesTool,SlashCommand,Skill,TodoWrite,AskUserQuestion',
// AI Provider Configuration
CLAUDE_MEM_PROVIDER: 'claude', // Default to Claude
CLAUDE_MEM_CLAUDE_AUTH_METHOD: 'cli', // Default to CLI subscription billing (not API key)
CLAUDE_MEM_GEMINI_API_KEY: '', // Empty by default, can be set via UI or env
CLAUDE_MEM_GEMINI_MODEL: 'gemini-2.5-flash-lite', // Default Gemini model (highest free tier RPM)
CLAUDE_MEM_GEMINI_RATE_LIMITING_ENABLED: 'true', // Rate limiting ON by default for free tier users
@@ -103,6 +109,10 @@ export class SettingsDefaultsManager {
// Feature Toggles
CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY: 'true',
CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE: 'false',
CLAUDE_MEM_FOLDER_CLAUDEMD_ENABLED: 'false',
// Exclusion Settings
CLAUDE_MEM_EXCLUDED_PROJECTS: '', // Comma-separated glob patterns for excluded project paths
CLAUDE_MEM_FOLDER_MD_EXCLUDE: '[]', // JSON array of folder paths to exclude from CLAUDE.md generation
// Chroma Vector Database Configuration
CLAUDE_MEM_CHROMA_MODE: 'local', // 'local' starts npx chroma run, 'remote' connects to existing server
CLAUDE_MEM_CHROMA_HOST: '127.0.0.1',
@@ -138,16 +148,36 @@ export class SettingsDefaultsManager {
/**
* Get a boolean default value
* Handles both string 'true' and boolean true from JSON
*/
static getBool(key: keyof SettingsDefaults): boolean {
const value = this.get(key);
return value === 'true';
return value === 'true' || value === true;
}
/**
* Apply environment variable overrides to settings
* Environment variables take highest priority over file and defaults
*/
private static applyEnvOverrides(settings: SettingsDefaults): SettingsDefaults {
const result = { ...settings };
for (const key of Object.keys(this.DEFAULTS) as Array<keyof SettingsDefaults>) {
if (process.env[key] !== undefined) {
result[key] = process.env[key]!;
}
}
return result;
}
/**
* Load settings from file with fallback to defaults
* Returns merged settings with defaults as fallback
* Handles all errors (missing file, corrupted JSON, permissions) by returning defaults
* Returns merged settings with proper priority: process.env > settings file > defaults
* Handles all errors (missing file, corrupted JSON, permissions) gracefully
*
* Configuration Priority:
* 1. Environment variables (highest priority)
* 2. Settings file (~/.claude-mem/settings.json)
* 3. Default values (lowest priority)
*/
static loadFromFile(settingsPath: string): SettingsDefaults {
try {
@@ -164,7 +194,8 @@ export class SettingsDefaultsManager {
} catch (error) {
console.warn('[SETTINGS] Failed to create settings file, using in-memory defaults:', settingsPath, error);
}
return defaults;
// Still apply env var overrides even when file doesn't exist
return this.applyEnvOverrides(defaults);
}
const settingsData = readFileSync(settingsPath, 'utf-8');
@@ -194,10 +225,12 @@ export class SettingsDefaultsManager {
}
}
return result;
// Apply environment variable overrides (highest priority)
return this.applyEnvOverrides(result);
} catch (error) {
console.warn('[SETTINGS] Failed to load settings, using defaults:', settingsPath, error);
return this.getAllDefaults();
// Still apply env var overrides even on error
return this.applyEnvOverrides(this.getAllDefaults());
}
}
}
+6 -3
View File
@@ -1,11 +1,12 @@
export const HOOK_TIMEOUTS = {
DEFAULT: 300000, // Standard HTTP timeout (5 min for slow systems)
HEALTH_CHECK: 30000, // Worker health check (30s for slow systems)
HEALTH_CHECK: 3000, // Worker health check (3s — healthy worker responds in <100ms)
POST_SPAWN_WAIT: 5000, // Wait for daemon to start after spawn (starts in <1s on Linux)
PORT_IN_USE_WAIT: 3000, // Wait when port occupied but health failing
WORKER_STARTUP_WAIT: 1000,
WORKER_STARTUP_RETRIES: 300,
PRE_RESTART_SETTLE_DELAY: 2000, // Give files time to sync before restart
POWERSHELL_COMMAND: 10000, // PowerShell process enumeration (10s - typically completes in <1s)
WINDOWS_MULTIPLIER: 1.5 // Platform-specific adjustment
WINDOWS_MULTIPLIER: 1.5 // Platform-specific adjustment for hook-side operations
} as const;
/**
@@ -21,6 +22,8 @@ export const HOOK_EXIT_CODES = {
FAILURE: 1,
/** Blocking error - for SessionStart, shows stderr to user only */
BLOCKING_ERROR: 2,
/** Show stderr to user only, don't inject into context. Used by user-message handler (Cursor). */
USER_MESSAGE_ONLY: 3,
} as const;
export function getTimeout(baseTimeout: number): number {
+7
View File
@@ -28,6 +28,9 @@ export const DATA_DIR = SettingsDefaultsManager.get('CLAUDE_MEM_DATA_DIR');
// Note: CLAUDE_CONFIG_DIR is a Claude Code setting, not claude-mem, so leave as env var
export const CLAUDE_CONFIG_DIR = process.env.CLAUDE_CONFIG_DIR || join(homedir(), '.claude');
// Plugin installation directory - respects CLAUDE_CONFIG_DIR for users with custom Claude locations
export const MARKETPLACE_ROOT = join(CLAUDE_CONFIG_DIR, 'plugins', 'marketplaces', 'thedotmack');
// Data subdirectories
export const ARCHIVES_DIR = join(DATA_DIR, 'archives');
export const LOGS_DIR = join(DATA_DIR, 'logs');
@@ -38,6 +41,10 @@ export const USER_SETTINGS_PATH = join(DATA_DIR, 'settings.json');
export const DB_PATH = join(DATA_DIR, 'claude-mem.db');
export const VECTOR_DB_DIR = join(DATA_DIR, 'vector-db');
// Observer sessions directory - used as cwd for SDK queries
// Sessions here won't appear in user's `claude --resume` for their actual projects
export const OBSERVER_SESSIONS_DIR = join(DATA_DIR, 'observer-sessions');
// Claude integration paths
export const CLAUDE_SETTINGS_PATH = join(CLAUDE_CONFIG_DIR, 'settings.json');
export const CLAUDE_COMMANDS_DIR = join(CLAUDE_CONFIG_DIR, 'commands');
+1 -1
View File
@@ -58,7 +58,7 @@ export function extractLastMessage(
// If we searched the whole transcript and didn't find any message of this role
if (!foundMatchingRole) {
throw new Error(`No message found for role '${role}' in transcript: ${transcriptPath}`);
return '';
}
return '';
+102 -46
View File
@@ -1,15 +1,45 @@
import path from "path";
import { homedir } from "os";
import { readFileSync } from "fs";
import { logger } from "../utils/logger.js";
import { HOOK_TIMEOUTS, getTimeout } from "./hook-constants.js";
import { SettingsDefaultsManager } from "./SettingsDefaultsManager.js";
import { getWorkerRestartInstructions } from "../utils/error-messages.js";
const MARKETPLACE_ROOT = path.join(homedir(), '.claude', 'plugins', 'marketplaces', 'thedotmack');
import { MARKETPLACE_ROOT } from "./paths.js";
// Named constants for health checks
const HEALTH_CHECK_TIMEOUT_MS = getTimeout(HOOK_TIMEOUTS.HEALTH_CHECK);
// Allow env var override for users on slow systems (e.g., CLAUDE_MEM_HEALTH_TIMEOUT_MS=10000)
const HEALTH_CHECK_TIMEOUT_MS = (() => {
const envVal = process.env.CLAUDE_MEM_HEALTH_TIMEOUT_MS;
if (envVal) {
const parsed = parseInt(envVal, 10);
if (Number.isFinite(parsed) && parsed >= 500 && parsed <= 300000) {
return parsed;
}
// Invalid env var — log once and use default
logger.warn('SYSTEM', 'Invalid CLAUDE_MEM_HEALTH_TIMEOUT_MS, using default', {
value: envVal, min: 500, max: 300000
});
}
return getTimeout(HOOK_TIMEOUTS.HEALTH_CHECK);
})();
/**
* Fetch with a timeout using Promise.race instead of AbortSignal.
* AbortSignal.timeout() causes a libuv assertion crash in Bun on Windows,
* so we use a racing setTimeout pattern that avoids signal cleanup entirely.
* The orphaned fetch is harmless since the process exits shortly after.
*/
export function fetchWithTimeout(url: string, init: RequestInit = {}, timeoutMs: number): Promise<Response> {
return new Promise((resolve, reject) => {
const timeoutId = setTimeout(
() => reject(new Error(`Request timed out after ${timeoutMs}ms`)),
timeoutMs
);
fetch(url, init).then(
response => { clearTimeout(timeoutId); resolve(response); },
err => { clearTimeout(timeoutId); reject(err); }
);
});
}
// Cache to avoid repeated settings file reads
let cachedPort: number | null = null;
@@ -57,23 +87,38 @@ export function clearPortCache(): void {
}
/**
* Check if worker is responsive and fully initialized by trying the readiness endpoint
* Changed from /health to /api/readiness to ensure MCP initialization is complete
* Check if worker HTTP server is responsive
* Uses /api/health (liveness) instead of /api/readiness because:
* - Hooks have 15-second timeout, but full initialization can take 5+ minutes (MCP connection)
* - /api/health returns 200 as soon as HTTP server is up (sufficient for hook communication)
* - /api/readiness returns 503 until full initialization completes (too slow for hooks)
* See: https://github.com/thedotmack/claude-mem/issues/811
*/
async function isWorkerHealthy(): Promise<boolean> {
const port = getWorkerPort();
// Note: Removed AbortSignal.timeout to avoid Windows Bun cleanup issue (libuv assertion)
const response = await fetch(`http://127.0.0.1:${port}/api/readiness`);
const response = await fetchWithTimeout(
`http://127.0.0.1:${port}/api/health`, {}, HEALTH_CHECK_TIMEOUT_MS
);
return response.ok;
}
/**
* Get the current plugin version from package.json
* Get the current plugin version from package.json.
* Returns 'unknown' on ENOENT/EBUSY (shutdown race condition, fix #1042).
*/
function getPluginVersion(): string {
const packageJsonPath = path.join(MARKETPLACE_ROOT, 'package.json');
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
return packageJson.version;
try {
const packageJsonPath = path.join(MARKETPLACE_ROOT, 'package.json');
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
return packageJson.version;
} catch (error: unknown) {
const code = (error as NodeJS.ErrnoException).code;
if (code === 'ENOENT' || code === 'EBUSY') {
logger.debug('SYSTEM', 'Could not read plugin version (shutdown race)', { code });
return 'unknown';
}
throw error;
}
}
/**
@@ -81,8 +126,9 @@ function getPluginVersion(): string {
*/
async function getWorkerVersion(): Promise<string> {
const port = getWorkerPort();
// Note: Removed AbortSignal.timeout to avoid Windows Bun cleanup issue (libuv assertion)
const response = await fetch(`http://127.0.0.1:${port}/api/version`);
const response = await fetchWithTimeout(
`http://127.0.0.1:${port}/api/version`, {}, HEALTH_CHECK_TIMEOUT_MS
);
if (!response.ok) {
throw new Error(`Failed to get worker version: ${response.status}`);
}
@@ -93,18 +139,33 @@ async function getWorkerVersion(): Promise<string> {
/**
* Check if worker version matches plugin version
* Note: Auto-restart on version mismatch is now handled in worker-service.ts start command (issue #484)
* This function logs for informational purposes only
* This function logs for informational purposes only.
* Skips comparison when either version is 'unknown' (fix #1042 avoids restart loops).
*/
async function checkWorkerVersion(): Promise<void> {
const pluginVersion = getPluginVersion();
const workerVersion = await getWorkerVersion();
try {
const pluginVersion = getPluginVersion();
if (pluginVersion !== workerVersion) {
// Just log debug info - auto-restart handles the mismatch in worker-service.ts
logger.debug('SYSTEM', 'Version check', {
pluginVersion,
workerVersion,
note: 'Mismatch will be auto-restarted by worker-service start command'
// Skip version check if plugin version couldn't be read (shutdown race)
if (pluginVersion === 'unknown') return;
const workerVersion = await getWorkerVersion();
// Skip version check if worker version is 'unknown' (avoids restart loops)
if (workerVersion === 'unknown') return;
if (pluginVersion !== workerVersion) {
// Just log debug info - auto-restart handles the mismatch in worker-service.ts
logger.debug('SYSTEM', 'Version check', {
pluginVersion,
workerVersion,
note: 'Mismatch will be auto-restarted by worker-service start command'
});
}
} catch (error) {
// Version check is informational — don't fail the hook
logger.debug('SYSTEM', 'Version check failed', {
error: error instanceof Error ? error.message : String(error)
});
}
}
@@ -112,30 +173,25 @@ async function checkWorkerVersion(): Promise<void> {
/**
* Ensure worker service is running
* Polls until worker is ready (assumes worker-service.cjs start was called by hooks.json)
* Quick health check - returns false if worker not healthy (doesn't block)
* Port might be in use by another process, or worker might not be started yet
*/
export async function ensureWorkerRunning(): Promise<void> {
const maxRetries = 75; // 15 seconds total
const pollInterval = 200;
for (let i = 0; i < maxRetries; i++) {
try {
if (await isWorkerHealthy()) {
await checkWorkerVersion(); // logs warning on mismatch, doesn't restart
return;
}
} catch (e) {
logger.debug('SYSTEM', 'Worker health check failed, will retry', {
attempt: i + 1,
maxRetries,
error: e instanceof Error ? e.message : String(e)
});
export async function ensureWorkerRunning(): Promise<boolean> {
// Quick health check (single attempt, no polling)
try {
if (await isWorkerHealthy()) {
await checkWorkerVersion(); // logs warning on mismatch, doesn't restart
return true; // Worker healthy
}
await new Promise(r => setTimeout(r, pollInterval));
} catch (e) {
// Not healthy - log for debugging
logger.debug('SYSTEM', 'Worker health check failed', {
error: e instanceof Error ? e.message : String(e)
});
}
throw new Error(getWorkerRestartInstructions({
port: getWorkerPort(),
customPrefix: 'Worker did not become ready within 15 seconds.'
}));
// Port might be in use by something else, or worker not started
// Return false but don't throw - let caller decide how to handle
logger.warn('SYSTEM', 'Worker not healthy, hook will proceed gracefully');
return false;
}
-169
View File
@@ -1,169 +0,0 @@
<claude-mem-context>
# Recent Activity
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
### Nov 21, 2025
**transcript.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #13598 | 5:39 PM | 🔵 | Full transcript schema structure with all content type definitions revealed | ~493 |
| #13593 | 5:38 PM | 🔵 | Transcript schema defines five transcript entry types with comprehensive structures | ~610 |
### Nov 23, 2025
**transcript.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #14665 | 7:25 PM | 🔵 | Tool Use ID Now Available in Hook Input Schema | ~395 |
| #14613 | 6:07 PM | 🔵 | Transcript Transformation System Analyzed for Test Creation | ~632 |
| #14580 | 5:37 PM | 🔵 | AssistantMessage type definition includes all required schema fields | ~395 |
### Nov 25, 2025
**transcript.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #15387 | 5:35 PM | 🔵 | Existing Transcript Processing Infrastructure with TypeScript Parser | ~430 |
| #15022 | 2:37 PM | ✅ | README.md Updated with Empirically Validated 50-60% Token Reduction | ~336 |
| #15003 | 2:28 PM | 🔵 | Comprehensive Transcript Processing Infrastructure | ~309 |
### Dec 7, 2025
**database.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #21829 | 11:05 PM | 🔄 | Massive refactor adds 8,671 lines and removes 5,585 lines across 60 files | ~619 |
| #21659 | 9:35 PM | 🟣 | Database Type Definitions Created for Type Safety | ~355 |
### Dec 8, 2025
**transcript.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #22273 | 9:22 PM | 🔵 | Transcript Modification Flow in Endless Mode Fully Traced | ~1061 |
| #22035 | 6:00 PM | 🔵 | Identified tool_use_id tracking across 13 TypeScript modules | ~329 |
**database.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #22047 | 6:03 PM | 🔵 | Database types define schema interfaces for SQLite query results with better-sqlite3 type safety | ~620 |
| #21925 | 4:55 PM | 🔵 | Database Schema Types Review | ~350 |
### Dec 10, 2025
**database.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #23734 | 9:03 PM | 🔵 | Database Type Definitions Identified | ~290 |
### Dec 11, 2025
**database.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #23959 | 1:58 PM | 🔵 | TypeScript Codebase Architecture Mapped | ~337 |
### Dec 12, 2025
**database.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #24394 | 7:24 PM | 🟣 | Phase 6 Agent Completed Git Commit and PR Update | ~444 |
| #24391 | " | ✅ | Committed Complete Migration from better-sqlite3 to bun:sqlite | ~436 |
| #24390 | 7:23 PM | ✅ | Staged All 19 Modified Files for Git Commit | ~301 |
| #24389 | " | 🟣 | Phase 5 Agent Completed Full System Verification | ~726 |
| #24388 | 7:22 PM | 🟣 | Phase 5 Complete - All Verification Passed for Production Deployment | ~600 |
| #24387 | " | 🔵 | Uncommitted Changes Identified Across Documentation and Core Services | ~368 |
### Dec 14, 2025
**database.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #26543 | 10:21 PM | 🔵 | Comprehensive Schema Migration from Legacy to SDK Architecture | ~902 |
| #26527 | 10:18 PM | 🔵 | Database Type Definitions with Schema and Record Interfaces | ~564 |
| #26523 | 10:17 PM | 🔵 | Shared Database Type Definitions | ~399 |
| #25904 | 6:21 PM | 🔵 | Database Type Definitions for SQLite Records | ~396 |
### Dec 16, 2025
**transcript.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #28038 | 8:05 PM | 🔵 | Transcript Type Definitions | ~412 |
| #28027 | 8:04 PM | 🔵 | Transcript Type Definitions | ~395 |
### Dec 18, 2025
**database.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #29773 | 7:01 PM | 🔵 | Observation Type Definitions Across Codebase | ~362 |
### Dec 20, 2025
**transcript.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #30785 | 6:01 PM | 🔵 | Transcript Type System Models Claude Code JSONL Structure | ~563 |
**database.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #30784 | 6:01 PM | 🔵 | Database Type System Defines SQLite Schema Contracts | ~538 |
| #30272 | 3:23 PM | 🟣 | Added UserPromptWithContext TypeScript interface | ~230 |
**bun-sqlite.d.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #30332 | 3:39 PM | 🟣 | Created PR #399 for TypeScript Compilation Fixes | ~293 |
| #30330 | 3:38 PM | 🔴 | TypeScript errors fixed across 21 files with type definitions added | ~378 |
| #30321 | 3:34 PM | ✅ | Removed Ragtime Script and Staged Bun-SQLite Types | ~229 |
| #30315 | 3:33 PM | 🔴 | Comprehensive TypeScript Error Resolution | ~560 |
| #30302 | 3:30 PM | 🔴 | Corrected Bun SQLite Transaction Method Signature | ~333 |
| #30294 | 3:27 PM | 🔴 | Fixed bun:sqlite type declarations to match actual runtime API | ~313 |
| #30268 | 3:22 PM | 🔴 | Added TypeScript declarations for bun:sqlite module | ~276 |
### Dec 22, 2025
**mode-config.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #31951 | 8:17 PM | 🟣 | Mode System with Inheritance and Multilingual Support | ~776 |
### Dec 24, 2025
**database.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #32158 | 6:42 PM | 🔵 | Database Type Definitions and Identifier Patterns | ~479 |
| #32157 | 6:41 PM | 🔵 | Shared Type Definitions Use SDK Session ID | ~300 |
### Dec 25, 2025
**database.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #32558 | 8:18 PM | 🔵 | Identified files containing 'summary' or 'Summary' | ~167 |
### Dec 28, 2025
**database.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #33438 | 10:15 PM | 🔄 | Bulk Rename Session ID Fields Across Entire Codebase | ~384 |
### Jan 4, 2026
**database.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #36824 | 1:00 AM | 🔵 | Database Types Define Canonical Record Interfaces | ~654 |
| #36775 | 12:43 AM | 🔵 | Database Record Type Definitions | ~542 |
| #36770 | 12:42 AM | 🔵 | Export Script Type Duplication Analysis Complete | ~555 |
| #36761 | 12:36 AM | ✅ | Created Implementation Plans for Four GitHub Issues | ~507 |
| #36760 | 12:34 AM | ✅ | Created Issue #531 Report: Export Script Type Duplication | ~430 |
| #36758 | " | 🔵 | Issue #531 Root Cause - 73 Lines of Duplicated Export Type Definitions | ~529 |
</claude-mem-context>
-39
View File
@@ -1,39 +0,0 @@
<claude-mem-context>
# Recent Activity
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
### Dec 25, 2025
**viewer-template.html**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #32516 | 6:58 PM | 🟣 | Spinning favicon animation during processing | ~347 |
### Dec 26, 2025
**viewer-template.html**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #32977 | 11:13 PM | 🔵 | User questions Save button placement in settings modal | ~172 |
| #32976 | 11:12 PM | 🔵 | UI button placement questioned in settings modal | ~166 |
### Jan 1, 2026
**viewer-template.html**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #35609 | 10:53 PM | ✅ | Removed logs-btn from Mobile Responsive Styles | ~221 |
| #35608 | " | ✅ | Removed logs-icon Class from Icon Sizing Styles | ~173 |
| #35607 | " | ✅ | Removed Legacy Logs Button Styles from CSS | ~217 |
| #35606 | " | 🟣 | Complete CSS Implementation for Chrome DevTools-Style Console Drawer | ~650 |
### Jan 2, 2026
**viewer-template.html**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #35901 | 2:49 PM | 🔵 | PR #525 File Changes Summary | ~376 |
| #35878 | 2:40 PM | 🔵 | Console Drawer Styling and Structure in HTML Template | ~531 |
| #35814 | 2:25 PM | 🟣 | Console Filter Bar and Log Line Styling Added | ~328 |
</claude-mem-context>
-138
View File
@@ -1,138 +0,0 @@
<claude-mem-context>
# Recent Activity
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
### Dec 15, 2025
**App.tsx**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #27281 | 11:00 PM | 🟣 | Queue Monitoring System Extracted to Separate Branch | ~452 |
| #27280 | 10:59 PM | 🔵 | Queue Infrastructure Changes Staged in Branch | ~313 |
| #27270 | 10:57 PM | ✅ | Extracted additional queue integration files from PR-335 | ~271 |
### Dec 16, 2025
**App.tsx**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #28084 | 8:11 PM | 🔵 | Viewer App Component Architecture | ~461 |
| #28076 | 8:10 PM | 🔵 | Viewer App Component Architecture | ~477 |
### Dec 18, 2025
**types.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #29773 | 7:01 PM | 🔵 | Observation Type Definitions Across Codebase | ~362 |
### Dec 20, 2025
**types.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #30330 | 3:38 PM | 🔴 | TypeScript errors fixed across 21 files with type definitions added | ~378 |
| #30288 | 3:26 PM | 🔴 | Added queueDepth property to StreamEvent interface | ~229 |
| #30287 | " | 🔵 | Complete type definitions for viewer data models and events | ~409 |
| #30286 | " | 🔵 | StreamEvent interface defined in viewer types | ~188 |
### Dec 21, 2025
**types.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #31422 | 6:50 PM | 🔵 | Observation Metadata Constants Usage Across Codebase | ~366 |
| #31329 | 5:45 PM | 🔵 | Observation Metadata Integration Across Services and UI | ~403 |
### Dec 22, 2025
**App.tsx**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #31790 | 4:39 PM | 🔵 | Identified files that interact with ModeManager and prompts | ~332 |
### Dec 24, 2025
**types.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #32156 | 6:41 PM | 🔵 | UI Layer Session Identifier Usage | ~256 |
### Dec 25, 2025
**types.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #32789 | 9:49 PM | 🟣 | Gemini AI Provider Integration Merged to Main | ~409 |
| #32640 | 8:46 PM | 🟣 | Renamed "Billing Enabled" setting to "Rate Limiting" with inverted logic | ~546 |
| #32627 | 8:44 PM | ✅ | Renamed billing setting in UI `Settings` interface | ~214 |
| #32621 | " | ✅ | Added `CLAUDE_MEM_GEMINI_RATE_LIMITING_ENABLED` to UI Settings interface | ~214 |
| #32614 | 8:43 PM | 🔵 | Confirmed absence of billing setting in UI `Settings` interface | ~233 |
| #32605 | 8:42 PM | 🔵 | Identified UI `Settings` interface and absence of specific billing toggle | ~265 |
| #32603 | " | 🔵 | Identified widespread use of "Gemini" across application components | ~312 |
| #32580 | 8:22 PM | 🔵 | Grep for resetStuckMessages and processing | ~242 |
| #32559 | 8:18 PM | 🔵 | Listed files changed in the current branch | ~169 |
| #32558 | " | 🔵 | Identified files containing 'summary' or 'Summary' | ~167 |
### Dec 26, 2025
**types.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #32925 | 10:26 PM | 🔵 | OpenRouter Provider Integration Proposed in PR 448 | ~543 |
| #32923 | 10:18 PM | 🔵 | OpenRouter Integration Architecture in claude-mem | ~610 |
| #32913 | 10:05 PM | 🔵 | PR #448 Code Review: OpenRouter Provider Integration | ~523 |
| #32910 | 10:01 PM | 🔵 | Viewer types define UI data structures with SSE event types and unified feed items | ~501 |
### Dec 28, 2025
**types.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #33439 | 10:15 PM | 🔄 | Extended Session ID Renaming to Additional Codebase Components | ~352 |
### Jan 1, 2026
**App.tsx**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #35605 | 10:52 PM | 🟣 | Added Floating Console Toggle Button | ~339 |
| #35604 | " | ✅ | Removed onLogsToggle Prop from Header Invocation | ~231 |
| #35600 | 10:51 PM | ✅ | JSX Component Usage Updated to LogsDrawer | ~231 |
| #35599 | " | ✅ | App Component Import Updated for LogsDrawer | ~206 |
### Jan 2, 2026
**App.tsx**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #35875 | 2:39 PM | 🔵 | Logging UI Architecture Mapped | ~599 |
| #35819 | 2:28 PM | 🔵 | LogsDrawer component integrated in App.tsx | ~235 |
**types.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #35824 | 2:29 PM | 🔵 | Type definitions for viewer data models | ~267 |
### Jan 4, 2026
**types.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #36757 | 12:33 AM | 🔵 | Issue #511 Root Cause Identified - Gemini-3-Flash Configuration Mismatch | ~416 |
| #36754 | " | 🔵 | Gemini-3-Flash Model Already Supported | ~301 |
### Jan 5, 2026
**CLAUDE.md**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #38078 | 9:54 PM | ✅ | CLAUDE.md Documentation Cleanup - 1,233 Lines Removed Across 18 Files | ~590 |
**types.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #37995 | 9:01 PM | 🔵 | CLAUDE_MEM_WORKER_HOST setting implementation pattern | ~304 |
| #37990 | 9:00 PM | 🔵 | CLAUDE_MEM_WORKER_HOST setting used across 19 files | ~289 |
</claude-mem-context>
-117
View File
@@ -1,117 +0,0 @@
<claude-mem-context>
# Recent Activity
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
### Dec 25, 2025
**ContextSettingsModal.tsx**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #32789 | 9:49 PM | 🟣 | Gemini AI Provider Integration Merged to Main | ~409 |
| #32640 | 8:46 PM | 🟣 | Renamed "Billing Enabled" setting to "Rate Limiting" with inverted logic | ~546 |
| #32630 | 8:45 PM | ✅ | Updated UI toggle for rate limiting in `ContextSettingsModal` | ~345 |
| #32622 | 8:44 PM | ✅ | Updated UI toggle for rate limiting in `ContextSettingsModal` | ~375 |
| #32615 | 8:43 PM | 🔵 | Confirmed UI text for "Billing Enabled" toggle | ~244 |
| #32613 | " | 🔵 | Comprehensive identification of `CLAUDE_MEM_GEMINI_BILLING_ENABLED` usage | ~407 |
| #32608 | 8:42 PM | 🔵 | Identified UI component for "Billing Enabled" toggle | ~348 |
| #32604 | " | 🔵 | Identified "billing" setting and its direct link to rate limiting | ~405 |
| #32603 | " | 🔵 | Identified widespread use of "Gemini" across application components | ~312 |
| #32559 | 8:18 PM | 🔵 | Listed files changed in the current branch | ~169 |
| #32550 | 7:31 PM | 🟣 | Added Gemini Billing Enabled toggle in UI | ~189 |
### Dec 26, 2025
**ContextSettingsModal.tsx**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #32977 | 11:13 PM | 🔵 | User questions Save button placement in settings modal | ~172 |
| #32976 | 11:12 PM | 🔵 | UI button placement questioned in settings modal | ~166 |
| #32978 | 11:04 PM | 🔵 | ContextSettingsModal component structure examined | ~199 |
| #32979 | " | 🔵 | Save button implemented in modal footer section | ~195 |
| #32980 | " | 🔄 | Save button moved inside modal body structure | ~182 |
| #32925 | 10:26 PM | 🔵 | OpenRouter Provider Integration Proposed in PR 448 | ~543 |
| #32923 | 10:18 PM | 🔵 | OpenRouter Integration Architecture in claude-mem | ~610 |
| #32922 | 10:17 PM | 🔵 | OpenRouter Integration in Settings UI | ~362 |
| #32913 | 10:05 PM | 🔵 | PR #448 Code Review: OpenRouter Provider Integration | ~523 |
| #32909 | 10:01 PM | 🔵 | ContextSettingsModal provides rich UI for settings with live preview and provider configuration | ~611 |
### Dec 28, 2025
**ContextSettingsModal.tsx**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #33279 | 3:07 PM | ✅ | Changed Default OpenRouter Model to Free Tier Option | ~285 |
### Dec 30, 2025
**ContextSettingsModal.tsx**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #34384 | 1:39 PM | 🔵 | UI Provides "haiku" as Model Selection Option | ~296 |
### Jan 1, 2026
**LogsModal.tsx**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #35668 | 11:40 PM | ✅ | Main branch updated with major feature additions | ~377 |
| #35598 | 10:51 PM | 🔄 | Logs UI Refactored from Modal to Bottom Drawer | ~505 |
| #35597 | 10:29 PM | 🟣 | Logs Viewer Modal Component Created | ~299 |
**Header.tsx**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #35603 | 10:52 PM | 🟣 | Removed Logs Button from Header UI | ~274 |
| #35602 | " | ✅ | Removed onLogsToggle from Header Destructuring | ~215 |
| #35601 | " | 🔄 | Removed onLogsToggle Prop from Header Interface | ~191 |
### Jan 2, 2026
**LogsModal.tsx**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #35999 | 5:32 PM | 🟣 | Alignment Quick Filter Added to Console Logs UI | ~308 |
| #35982 | 5:09 PM | ✅ | Built and deployed claude-mem version 8.5.4 with LogsModal UI component | ~295 |
| #35981 | 5:08 PM | 🟣 | Implemented alignment quick filter UI in LogsModal | ~281 |
| #35980 | " | 🟣 | Alignment-only filter added to LogsModal | ~323 |
| #35979 | " | 🟣 | Added alignment filter state to LogsModal component | ~219 |
| #35978 | " | 🔵 | LogsModal viewer component with filtering and parsing capabilities | ~447 |
| #35904 | 2:49 PM | 🟣 | Implemented Structured Log Parsing in LogsModal | ~400 |
| #35901 | " | 🔵 | PR #525 File Changes Summary | ~376 |
| #35879 | 2:41 PM | 🟣 | Enhanced LogsModal with Parsing, Filtering, Colors, Icons, and Auto-Scroll | ~726 |
| #35877 | 2:40 PM | 🔵 | LogsDrawer Component Current Implementation | ~426 |
| #35820 | 2:28 PM | 🔵 | Current LogsDrawer implementation analyzed | ~289 |
**Header.tsx**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #35875 | 2:39 PM | 🔵 | Logging UI Architecture Mapped | ~599 |
| #35839 | 2:30 PM | 🔵 | Header component with project filter dropdown | ~245 |
**SummaryCard.tsx**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #35834 | 2:30 PM | 🔵 | SummaryCard demonstrates icon usage pattern | ~281 |
**Feed.tsx**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #35832 | 2:29 PM | 🔵 | Feed component with intersection observer for infinite scroll | ~300 |
**ObservationCard.tsx**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #35828 | 2:29 PM | 🔵 | ObservationCard styling and interaction patterns | ~275 |
### Jan 4, 2026
**ContextSettingsModal.tsx**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #36781 | 12:45 AM | 🔵 | Complete GeminiAgent Model Configuration Gap Analysis | ~552 |
| #36773 | 12:43 AM | 🔵 | Gemini Model Options in Settings UI | ~490 |
| #36757 | 12:33 AM | 🔵 | Issue #511 Root Cause Identified - Gemini-3-Flash Configuration Mismatch | ~416 |
| #36756 | " | 🔵 | UI Component Also Lists gemini-3-flash | ~339 |
| #36754 | " | 🔵 | Gemini-3-Flash Model Already Supported | ~301 |
</claude-mem-context>
@@ -465,7 +465,7 @@ export function ContextSettingsModal({
>
<option value="gemini-2.5-flash-lite">gemini-2.5-flash-lite (10 RPM free)</option>
<option value="gemini-2.5-flash">gemini-2.5-flash (5 RPM free)</option>
<option value="gemini-3-flash">gemini-3-flash (5 RPM free)</option>
<option value="gemini-3-flash-preview">gemini-3-flash-preview (5 RPM free)</option>
</select>
</FormField>
<div className="toggle-group" style={{ marginTop: '8px' }}>
+7 -1
View File
@@ -1,5 +1,6 @@
import React, { useMemo, useRef, useLayoutEffect, useState } from 'react';
import AnsiToHtml from 'ansi-to-html';
import DOMPurify from 'dompurify';
interface TerminalPreviewProps {
content: string;
@@ -26,7 +27,12 @@ export function TerminalPreview({ content, isLoading = false, className = '' }:
scrollTopRef.current = preRef.current.scrollTop;
}
if (!content) return '';
return ansiConverter.toHtml(content);
const convertedHtml = ansiConverter.toHtml(content);
return DOMPurify.sanitize(convertedHtml, {
ALLOWED_TAGS: ['span', 'div', 'br'],
ALLOWED_ATTR: ['style', 'class'],
ALLOW_DATA_ATTR: false
});
}, [content]);
// Restore scroll position after render
+1 -189
View File
@@ -1,197 +1,9 @@
<claude-mem-context>
# Recent Activity
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
### Nov 17, 2025
**api.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #10372 | 3:32 PM | 🔵 | Existing API endpoint naming conventions in api.ts constants | ~340 |
### Nov 21, 2025
**ui.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #13110 | 1:05 AM | 🔵 | Magic Numbers Identified Across Codebase | ~327 |
### Nov 22, 2025
**settings.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #14203 | 1:05 AM | 🔵 | Endless Mode Feature Branch Contains Major Additions | ~566 |
### Nov 23, 2025
**api.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #14555 | 5:21 PM | 🔵 | Missing Stats Fields Causing UI Errors | ~288 |
### Nov 26, 2025
**settings.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #16173 | 10:06 PM | 🔵 | Configuration System Analysis for Domain Profile Support | ~621 |
### Dec 2, 2025
**api.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #19210 | 5:57 PM | 🔵 | Viewer UI Technology Stack Assessment | ~534 |
### Dec 7, 2025
**settings.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #21733 | 10:10 PM | 🔵 | UI Viewer Contains Duplicate DEFAULT_SETTINGS Object | ~375 |
| #21686 | 9:49 PM | 🔵 | DRY Audit Reveals 600+ Lines of Duplicated Code in Worker Service | ~605 |
| #21685 | 9:48 PM | 🔵 | Configuration Defaults and Environment Variables | ~558 |
### Dec 9, 2025
**settings.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #22637 | 11:46 AM | 🔵 | Comprehensive inventory of CLAUDE_MEM_ environment variable usage across codebase | ~619 |
| #22606 | 11:33 AM | 🔵 | Hardcoded port 37777 found across multiple codebase locations | ~371 |
### Dec 11, 2025
**settings.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #23959 | 1:58 PM | 🔵 | TypeScript Codebase Architecture Mapped | ~337 |
| #23947 | 1:40 PM | 🔵 | Comprehensive Port Configuration Audit Complete | ~532 |
| #23940 | 1:38 PM | 🔵 | UI Constants Duplicate Settings Defaults | ~357 |
| #23935 | 1:37 PM | 🔵 | CLAUDE_MEM_WORKER_PORT Environment Variable Usage Pattern | ~456 |
| #23933 | 1:36 PM | 🔵 | Comprehensive Port 37777 References Across Documentation and Code | ~427 |
### Dec 12, 2025
**settings.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #24602 | 10:20 PM | 🟣 | Completed PR #236 Security Fix Implementation | ~463 |
| #24596 | 10:18 PM | 🟣 | Merged Localhost-Only Binding Security Feature to Main | ~413 |
### Dec 13, 2025
**settings.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #25075 | 7:09 PM | ⚖️ | Complete System Analysis for Embedding Function Configuration | ~497 |
| #25074 | " | 🔵 | Complete Settings System Architecture via Exploration Agent | ~532 |
| #25045 | 7:02 PM | 🔵 | UI Viewer Settings Constants | ~296 |
### Dec 14, 2025
**settings.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #26202 | 8:17 PM | ✅ | UI viewer constants updated to match Sonnet default model | ~301 |
| #26199 | " | 🔵 | UI viewer default settings configuration examined | ~358 |
| #26198 | " | 🔵 | CLAUDE_MEM_MODEL configuration found throughout codebase | ~403 |
**api.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #26088 | 7:32 PM | 🔵 | API Endpoint Architecture Discovery | ~416 |
### Dec 16, 2025
**api.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #28083 | 8:10 PM | 🔵 | API Endpoint Constants | ~360 |
**settings.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #27992 | 8:01 PM | 🔵 | UI Default Settings Constants | ~376 |
### Dec 18, 2025
**settings.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #29229 | 12:08 AM | 🔵 | Claude-Mem Observation Type System Architecture Mapped | ~552 |
| #29226 | 12:06 AM | 🔵 | Default Settings Configuration | ~395 |
### Dec 21, 2025
**settings.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #31422 | 6:50 PM | 🔵 | Observation Metadata Constants Usage Across Codebase | ~366 |
| #31329 | 5:45 PM | 🔵 | Observation Metadata Integration Across Services and UI | ~403 |
### Dec 25, 2025
**settings.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #32789 | 9:49 PM | 🟣 | Gemini AI Provider Integration Merged to Main | ~409 |
| #32640 | 8:46 PM | 🟣 | Renamed "Billing Enabled" setting to "Rate Limiting" with inverted logic | ~546 |
| #32620 | 8:44 PM | ✅ | Renamed billing setting and updated default in UI constants | ~233 |
| #32613 | 8:43 PM | 🔵 | Comprehensive identification of `CLAUDE_MEM_GEMINI_BILLING_ENABLED` usage | ~407 |
| #32606 | 8:42 PM | 🔵 | Default value for `CLAUDE_MEM_GEMINI_BILLING_ENABLED` identified | ~216 |
| #32603 | " | 🔵 | Identified widespread use of "Gemini" across application components | ~312 |
| #32602 | " | 🔵 | Identified potential settings configuration files | ~224 |
| #32559 | 8:18 PM | 🔵 | Listed files changed in the current branch | ~169 |
**api.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #32580 | 8:22 PM | 🔵 | Grep for resetStuckMessages and processing | ~242 |
### Dec 26, 2025
**settings.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #32925 | 10:26 PM | 🔵 | OpenRouter Provider Integration Proposed in PR 448 | ~543 |
| #32923 | 10:18 PM | 🔵 | OpenRouter Integration Architecture in claude-mem | ~610 |
| #32921 | 10:16 PM | ✅ | Updated UI default settings for OpenRouter model | ~214 |
| #32913 | 10:05 PM | 🔵 | PR #448 Code Review: OpenRouter Provider Integration | ~523 |
### Dec 28, 2025
**settings.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #33279 | 3:07 PM | ✅ | Changed Default OpenRouter Model to Free Tier Option | ~285 |
### Jan 2, 2026
**ui.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #35822 | 2:29 PM | 🔵 | UI constants structure examined | ~191 |
### Jan 4, 2026
**settings.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #36781 | 12:45 AM | 🔵 | Complete GeminiAgent Model Configuration Gap Analysis | ~552 |
### Jan 5, 2026
**CLAUDE.md**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #38078 | 9:54 PM | ✅ | CLAUDE.md Documentation Cleanup - 1,233 Lines Removed Across 18 Files | ~590 |
**settings.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #37995 | 9:01 PM | 🔵 | CLAUDE_MEM_WORKER_HOST setting implementation pattern | ~304 |
| #37990 | 9:00 PM | 🔵 | CLAUDE_MEM_WORKER_HOST setting used across 19 files | ~289 |
| #32982 | 11:04 PM | 🔵 | Read default settings configuration file | ~233 |
</claude-mem-context>
+4
View File
@@ -36,4 +36,8 @@ export const DEFAULT_SETTINGS = {
// Feature Toggles
CLAUDE_MEM_CONTEXT_SHOW_LAST_SUMMARY: 'true',
CLAUDE_MEM_CONTEXT_SHOW_LAST_MESSAGE: 'false',
// Exclusion Settings
CLAUDE_MEM_EXCLUDED_PROJECTS: '',
CLAUDE_MEM_FOLDER_MD_EXCLUDE: '[]',
} as const;
-181
View File
@@ -1,181 +0,0 @@
<claude-mem-context>
# Recent Activity
<!-- This section is auto-generated by claude-mem. Edit content outside the tags. -->
### Dec 13, 2025
**useSettings.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #25321 | 9:12 PM | 🔵 | Console.error Usage Found in 29 Files | ~366 |
| #25075 | 7:09 PM | ⚖️ | Complete System Analysis for Embedding Function Configuration | ~497 |
| #25059 | 7:05 PM | 🔵 | UI Settings Hook Pattern | ~320 |
**useGitHubStars.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #25319 | 9:12 PM | 🔵 | Error Throw Locations Identified Across Codebase | ~302 |
### Dec 14, 2025
**useSettings.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #26198 | 8:17 PM | 🔵 | CLAUDE_MEM_MODEL configuration found throughout codebase | ~403 |
### Dec 15, 2025
**useNotifications.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #27281 | 11:00 PM | 🟣 | Queue Monitoring System Extracted to Separate Branch | ~452 |
| #27280 | 10:59 PM | 🔵 | Queue Infrastructure Changes Staged in Branch | ~313 |
| #27279 | " | ✅ | Queue UI Components Built Successfully | ~277 |
**useQueue.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #27272 | 10:58 PM | ✅ | Queue UI Components Extracted to Working Directory | ~235 |
| #27263 | 10:55 PM | ✅ | Extracted queue-specific files from PR-335 | ~284 |
| #27247 | 10:34 PM | 🔵 | useQueue Hook API Integration | ~370 |
**useSSE.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #27248 | 10:34 PM | 🔵 | Server-Sent Events Hook for Real-Time Queue Updates | ~406 |
### Dec 19, 2025
**useContextPreview.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #29947 | 7:03 PM | 🔵 | Context injection endpoint usage across system | ~387 |
### Dec 20, 2025
**useContextPreview.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #30330 | 3:38 PM | 🔴 | TypeScript errors fixed across 21 files with type definitions added | ~378 |
| #30278 | 3:24 PM | 🔴 | Added TypeScript type assertion for projects API response | ~205 |
**usePagination.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #30312 | 3:32 PM | 🔴 | Added explicit type arguments to usePaginationFor calls | ~314 |
| #30311 | " | 🔴 | Updated JSON response type assertion to use generic type | ~317 |
| #30310 | 3:31 PM | 🔴 | Added generic type parameter to usePaginationFor hook | ~356 |
**useStats.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #30291 | 3:26 PM | 🔴 | Added TypeScript type assertion for stats API response | ~239 |
| #30290 | " | 🔵 | useStats hook fetches worker and database statistics | ~288 |
**useSSE.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #30289 | 3:26 PM | 🔴 | Added non-null assertion for data.observation in new_observation handler | ~264 |
| #30285 | 3:25 PM | 🔵 | useSSE hook manages EventSource connection with auto-reconnect capability | ~282 |
| #30284 | " | 🔵 | useSSE hook handles multiple server-sent event types | ~288 |
**useSettings.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #30283 | 3:25 PM | 🔴 | Added TypeScript type assertion for saveSettings API response | ~249 |
| #30282 | " | 🔴 | Added TypeScript type annotation for settings API response | ~82 |
| #30281 | " | 🔵 | useSettings hook lacks type safety for API responses | ~245 |
**useGitHubStars.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #30280 | 3:25 PM | ✅ | Standardized TypeScript type assertion syntax in useGitHubStars | ~204 |
| #30279 | 3:24 PM | 🔵 | useGitHubStars hook already has proper TypeScript typing | ~249 |
### Dec 21, 2025
**useContextPreview.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #31603 | 8:21 PM | 🔵 | Complete Console.* Statement Audit Across Codebase | ~813 |
| #31599 | 8:19 PM | 🔵 | 136 console logging statements found in TypeScript source files | ~538 |
**useSettings.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #31422 | 6:50 PM | 🔵 | Observation Metadata Constants Usage Across Codebase | ~366 |
| #31329 | 5:45 PM | 🔵 | Observation Metadata Integration Across Services and UI | ~403 |
### Dec 22, 2025
**useXFollowers.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #31969 | 9:32 PM | 🟣 | X (Twitter) Follower Count Hook Created | ~210 |
### Dec 25, 2025
**useSettings.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #32789 | 9:49 PM | 🟣 | Gemini AI Provider Integration Merged to Main | ~409 |
| #32559 | 8:18 PM | 🔵 | Listed files changed in the current branch | ~169 |
**useSpinningFavicon.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #32646 | 8:48 PM | 🔵 | Existing Spinning Favicon Implementation | ~297 |
| #32516 | 6:58 PM | 🟣 | Spinning favicon animation during processing | ~347 |
**useSSE.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #32580 | 8:22 PM | 🔵 | Grep for resetStuckMessages and processing | ~242 |
| #32558 | 8:18 PM | 🔵 | Identified files containing 'summary' or 'Summary' | ~167 |
### Jan 1, 2026
**useContextPreview.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #35688 | 11:49 PM | 🔵 | Context Preview Hook with Project Selection | ~472 |
**useSettings.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #35686 | 11:49 PM | 🔵 | Settings Management Hook Implementation | ~529 |
| #35485 | 9:06 PM | ⚖️ | Comprehensive error handling remediation plan completed and submitted for approval | ~555 |
| #35469 | 9:02 PM | 🔵 | Proper error handling in settings save function | ~268 |
**useSSE.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #35684 | 11:49 PM | 🔵 | Server-Sent Events Hook Implementation | ~484 |
**usePagination.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #35683 | 11:48 PM | 🔵 | Pagination Hook Implementation Structure | ~439 |
### Jan 2, 2026
**usePagination.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #35875 | 2:39 PM | 🔵 | Logging UI Architecture Mapped | ~599 |
| #35838 | 2:30 PM | 🔵 | Pagination hook pattern with offset tracking and filter reset | ~272 |
### Jan 5, 2026
**CLAUDE.md**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #38078 | 9:54 PM | ✅ | CLAUDE.md Documentation Cleanup - 1,233 Lines Removed Across 18 Files | ~590 |
**useSettings.ts**
| ID | Time | T | Title | Read |
|----|------|---|-------|------|
| #37995 | 9:01 PM | 🔵 | CLAUDE_MEM_WORKER_HOST setting implementation pattern | ~304 |
| #37990 | 9:00 PM | 🔵 | CLAUDE_MEM_WORKER_HOST setting used across 19 files | ~289 |
</claude-mem-context>

Some files were not shown because too many files have changed in this diff Show More