Compare commits

..

12 Commits

Author SHA1 Message Date
Alex Newman 047914d087 chore: Bump version to 6.0.7
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-17 13:46:12 -05:00
Alex Newman bdf79a439b fix: Change discovery_tokens migration from version 7 to 11
Root cause: The ensureDiscoveryTokensColumn migration was using version 7,
which was already taken by removeSessionSummariesUniqueConstraint. This
duplicate version number caused migration tracking issues in some databases.

Changes:
- Updated migration version from 7 to 11 (next available)
- Added schema_versions check to prevent unnecessary re-runs
- Updated comments to clarify the version number conflict
- Added error propagation (already present, but now more reliable)

This resolves issue #121 where users were seeing "no such column: discovery_tokens"
errors after upgrading to v6.0.6.

Affected users can manually add the columns:
  ALTER TABLE observations ADD COLUMN discovery_tokens INTEGER DEFAULT 0;
  ALTER TABLE session_summaries ADD COLUMN discovery_tokens INTEGER DEFAULT 0;

Or wait for v6.0.7 release which includes this fix.

Fixes #121

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-17 13:43:34 -05:00
Alex Newman 99b6b85d67 docs: Update CHANGELOG.md for v6.0.6
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-17 13:19:43 -05:00
Alex Newman 798dec972e chore: Bump version to 6.0.6
Critical bugfix for database migration issue (Issue #121)

Changes:
- Fix migration logic to always verify column existence
- Remove early return that trusted schema_versions alone
- Ensures discovery_tokens columns exist before queries
- Prevents "no such column" errors for all users

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-17 13:18:49 -05:00
Alex Newman 286343fef6 Delete implementation plans and memory leak documentation files
- Removed `IMPLEMENTATION_PLAN_ROI_METRICS.md` which detailed the implementation plan for ROI metrics and discovery cost tracking.
- Deleted `MEMORY_LEAK_FIXES.md` and `MEMORY_LEAK_SUMMARY.md` that contained information on memory leak fixes and their summaries.
2025-11-16 23:34:08 -05:00
Alex Newman 9285826547 feat: implement Endless Mode for real-time context compression in Claude sessions 2025-11-16 23:19:43 -05:00
Alex Newman ce3b3733fa docs: Update CHANGELOG.md for v6.0.5 2025-11-16 22:39:50 -05:00
Alex Newman cf1c966409 chore: Bump version to 6.0.5
Automatic cleanup of orphaned MCP server processes on worker startup
Removed manual cleanup notice from session context
Self-healing maintenance on every worker restart

Generated with Claude Code
2025-11-16 22:39:06 -05:00
Alex Newman 02fef487e7 feat: add cleanup for orphaned MCP server processes on startup
- Implemented a new method `cleanupOrphanedProcesses` to identify and terminate orphaned `uvx` processes from previous sessions.
- Integrated the cleanup method into the `start` process of the WorkerService to ensure a clean environment at startup.
- Added logging for process cleanup actions and handled potential errors gracefully without failing the service startup.
2025-11-16 22:36:39 -05:00
Alex Newman 20d45006c0 docs: Update CHANGELOG.md for v6.0.4
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-16 22:21:53 -05:00
Alex Newman 4f1cd309fd chore: Bump version to 6.0.4
Fix memory leaks from orphaned uvx/python processes

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-16 22:20:57 -05:00
Copilot c46e4a341a Fix memory leaks from orphaned uvx/python processes (#120)
This fixes memory leak, will remove one unnecessary MCP after this in a new PR but this is mission critical fix

* Initial plan

* Fix memory leaks: Add proper cleanup for ChromaSync and search server processes

Co-authored-by: thedotmack <683968+thedotmack@users.noreply.github.com>

* Add comprehensive process cleanup and PM2 configuration improvements

Co-authored-by: thedotmack <683968+thedotmack@users.noreply.github.com>

* Add comprehensive summary and recommendations for memory leak fixes

Co-authored-by: thedotmack <683968+thedotmack@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: thedotmack <683968+thedotmack@users.noreply.github.com>
2025-11-16 22:16:41 -05:00
21 changed files with 745 additions and 1093 deletions
+1 -1
View File
@@ -10,7 +10,7 @@
"plugins": [
{
"name": "claude-mem",
"version": "6.0.3",
"version": "6.0.7",
"source": "./plugin",
"description": "Persistent memory system for Claude Code - context compression across sessions"
}
+68
View File
@@ -4,6 +4,74 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [6.0.6] - 2025-11-17
## Critical Bugfix Release
### Fixed
- **Database Migration**: Fixed critical bug where `discovery_tokens` migration logic trusted `schema_versions` table without verifying actual column existence (#121)
- Migration now always checks if columns exist before queries, preventing "no such column" errors
- Safe for all users - auto-migrates on next Claude Code session without data loss
### Technical Details
- Removed early return based on `schema_versions` check that could skip actual column verification
- Migration now uses `PRAGMA table_info()` to verify column existence before every query
- Ensures idempotent, safe schema migrations for SQLite databases
### Impact
- Users experiencing "SqliteError: no such column: discovery_tokens" will be automatically fixed
- No manual intervention or database backup required
- Update to v6.0.6 via marketplace or `git pull` and restart Claude Code
**Affected Users**: All users who upgraded to v6.0.5 and experienced the migration error
## [6.0.5] - 2025-11-17
## Changes
### Automatic MCP Server Cleanup
- Automatic cleanup of orphaned MCP server processes on worker startup
- Self-healing maintenance runs on every worker restart
- Prevents orphaned process accumulation and resource leaks
### Improvements
- Removed manual cleanup notice from session context
- Streamlined worker initialization process
## What's Fixed
- Memory leaks from orphaned uvx/python processes are now prevented automatically
- Workers self-heal on every restart without manual intervention
---
**Release Date**: November 16, 2025
**Plugin Version**: 6.0.5
## [6.0.4] - 2025-11-17
**Patch Release**
Fixes memory leaks from orphaned uvx/python processes that could accumulate during ChromaDB operations.
**Changes:**
- Fixed process cleanup in ChromaDB sync operations to prevent orphaned processes
- Improved resource management for external process spawning
**Full Changelog:** https://github.com/thedotmack/claude-mem/compare/v6.0.3...v6.0.4
## [6.0.3] - 2025-11-16
## What's Changed
Documentation alignment release - merged PR #116 fixing hybrid search architecture documentation.
### Documentation Updates
- Added comprehensive guide
- Updated technical architecture documentation to reflect hybrid ChromaDB + SQLite + timeline context flow
- Fixed skill operation guides to accurately describe semantic search capabilities
**Full Changelog**: https://github.com/thedotmack/claude-mem/compare/v6.0.2...v6.0.3
## [6.0.2] - 2025-11-14
## Changes
+1 -1
View File
@@ -6,7 +6,7 @@ Claude-mem is a Claude Code plugin providing persistent memory across sessions.
**Your Role**: You are working on the plugin itself. When users interact with Claude Code with this plugin installed, your observations get captured and become their persistent memory.
**Current Version**: 6.0.3
**Current Version**: 6.0.7
## IMPORTANT: Skills Are Auto-Invoked
-434
View File
@@ -1,434 +0,0 @@
# Hybrid Search Architecture: Problem-Solution Document
**Date:** 2025-01-15
**Author:** Claude Code (Session handoff document)
**Purpose:** Comprehensive fix guide for hybrid search architecture documentation and implementation
---
## Executive Summary
The claude-mem hybrid search architecture is **correctly implemented in code** but **incorrectly documented** in skill guides. Additionally, the workflow is missing the final "instant context timeline" step that completes the human memory analogy.
**Quick Status:**
- ✅ Backend code (`search-server.ts`): ChromaDB first, SQLite temporal sort
- ❌ Skill operation guides: Describe FTS5 as primary search method
- ❌ Missing feature: Automatic timeline context retrieval (before/after observations)
- ✅ Landing page: Recently corrected
- ⚠️ Documentation: Needs validation and potential refinement
---
## The Intended Architecture (User's Vision)
### Storage Flow
```
User Action
1. SQLite Insert (FAST, synchronous)
- Immediate persistence
- Available for querying instantly
2. ChromaDB Sync (BACKGROUND, asynchronous)
- Worker generates embeddings
- Takes time but doesn't block user
- Uses OpenAI text-embedding-3-small
```
**Why this design:**
- Users don't wait for embedding generation
- SQLite provides immediate access
- ChromaDB catches up in background for semantic search
### Search Flow (3-Layer Sequential Architecture)
```
User Query: "How did we implement authentication?"
LAYER 1: Semantic Retrieval (ChromaDB)
- Vector similarity search
- Returns observation IDs (not full records)
- Top 100 semantic matches
- 90-day recency filter applied
LAYER 2: Temporal Ordering (SQLite)
- Takes IDs from Layer 1
- Hydrates full records from SQLite
- Sorts by created_at_epoch DESC
- Returns NEWEST relevant observation
LAYER 3: Instant Context Timeline (SQLite) [MISSING IN CURRENT IMPLEMENTATION]
- Takes top observation ID from Layer 2
- Retrieves N observations BEFORE that point
- Retrieves N observations AFTER that point
- Provides temporal context: "what led here" + "what happened next"
Present to User
- Most relevant observation
- Timeline showing before/after context
- Mimics human memory
```
**Why ChromaDB can't do it alone:**
- ChromaDB doesn't efficiently support date range queries sorted by time
- SQLite excels at temporal operations (ORDER BY created_at_epoch)
- Need both: ChromaDB for semantic, SQLite for temporal
**Why the timeline matters:**
> LLMs don't experience time linearly like humans do. Humans remember: "I did X, which led to Y, then Z happened." The instant context timeline gives LLMs this temporal awareness that humans experience naturally.
### Fallback Behavior
```
IF ChromaDB unavailable OR no results:
FTS5 Keyword Search (SQLite)
- Full-text search on observations_fts
- Basic keyword matching
- Ensures backward compatibility
- Fallback for older systems
```
**FTS5 is NOT "optional"** - it's the fallback mechanism for when ChromaDB isn't available or returns no results.
---
## Current State Analysis
### ✅ What's Correct: Backend Implementation
**File:** `/Users/alexnewman/Scripts/claude-mem/src/servers/search-server.ts`
**Lines:** 360-396 (search_observations handler)
The code DOES implement Layers 1 & 2 correctly:
```typescript
// Step 1: ChromaDB semantic search (top 100)
if (chromaClient) {
const chromaResults = await queryChroma(query, 100);
// Step 2: Filter by 90-day recency
const ninetyDaysAgo = Date.now() - (90 * 24 * 60 * 60 * 1000);
const recentIds = chromaResults.ids.filter((_id, idx) => {
const meta = chromaResults.metadatas[idx];
return meta && meta.created_at_epoch > ninetyDaysAgo;
});
// Step 3: Hydrate from SQLite with temporal ordering
results = store.getObservationsByIds(recentIds, {
orderBy: 'date_desc',
limit
});
}
// Fallback to FTS5 if ChromaDB unavailable
if (results.length === 0) {
results = search.searchObservations(query, options); // FTS5
}
```
**What this gets right:**
- ChromaDB semantic search FIRST (not FTS5)
- 90-day recency filter
- SQLite temporal ordering (`orderBy: 'date_desc'`)
- FTS5 fallback for reliability
### ❌ What's Wrong: Skill Operation Guides
**File:** `/Users/alexnewman/Scripts/claude-mem/plugin/skills/mem-search/operations/observations.md`
**Current Title:** "Search Observations (Full-Text)"
**Current Description:** "Search all observations using natural language queries."
**Current Line 351:** `query: z.string().describe('Search query for FTS5 full-text search')`
**The Problem:**
- Describes FTS5 as the search method
- No mention of ChromaDB semantic search
- Misleading title "Full-Text" implies keyword-only
- Examples don't show the ChromaDB → SQLite flow
**Impact:**
- Claude thinks it's doing FTS5 keyword search
- Doesn't understand it's semantic vector search
- Can't explain the architecture to users correctly
### ⚠️ What's Missing: Layer 3 (Instant Context Timeline)
The current implementation stops at Layer 2 (temporal ordering). It doesn't automatically:
1. Identify the MOST relevant observation (it returns a sorted list)
2. Retrieve observations BEFORE that point in time
3. Retrieve observations AFTER that point in time
4. Present the timeline context to the user
**Why this matters:**
The timeline is the **killer feature** that mimics human memory. Without it, users get:
- ❌ A sorted list of relevant observations
- ❌ No context about what led there
- ❌ No context about what happened next
With timeline, users get:
- ✅ The MOST relevant observation
- ✅ Context: "You did A and B before this"
- ✅ Context: "After this, you did C and D"
- ✅ Complete narrative like human memory
### 📋 Documentation Status
**Recently Fixed (✅):**
- `/Users/alexnewman/Scripts/claude-mem/docs/context/mem-search-technical-architecture.md`
- Now describes 3-layer sequential flow
- Includes human memory analogy
- Positions ChromaDB as primary
**Landing Page (✅):**
- `/Users/alexnewman/Scripts/claude-mem-pro/src/components/landing/Features.tsx`
- `/Users/alexnewman/Scripts/claude-mem-pro/src/components/landing/QuickBenefits.tsx`
- `/Users/alexnewman/Scripts/claude-mem-pro/src/components/landing/Architecture.tsx`
- All updated to describe ChromaDB-first architecture
- "Remember Like a Human" messaging added
- Timeline feature highlighted
**Needs Review:**
- SKILL.md technical notes (line 172)
- All operation guides in `/operations/` directory
- Common workflows documentation
---
## Required Fixes
### Fix 1: Update Skill Operation Guides
**Files to modify:**
- `/Users/alexnewman/Scripts/claude-mem/plugin/skills/mem-search/operations/observations.md`
- `/Users/alexnewman/Scripts/claude-mem/plugin/skills/mem-search/operations/common-workflows.md`
**Changes needed:**
1. **observations.md:**
- Change title: "Search Observations (Full-Text)" → "Search Observations (Semantic + Temporal)"
- Update description: Explain ChromaDB semantic search as primary
- Update command examples to explain hybrid flow
- Add note: "Uses ChromaDB vector search with SQLite temporal ordering. FTS5 used as fallback."
2. **common-workflows.md:**
- Update "Workflow 2: Finding Specific Bug Fixes" to explain ChromaDB → SQLite flow
- Add new workflow: "Workflow N: Getting Timeline Context Around Relevant Observations"
**Example of corrected observations.md header:**
```markdown
# Search Observations (Semantic + Temporal)
Search observations using ChromaDB vector similarity with SQLite temporal ordering.
## Architecture
**3-Layer Hybrid Search:**
1. **ChromaDB semantic retrieval** - Finds what's semantically relevant (vector similarity)
2. **90-day recency filter** - Prioritizes recent work
3. **SQLite temporal ordering** - Sorts by time, returns newest relevant
**Fallback:** If ChromaDB unavailable, falls back to FTS5 keyword search.
## When to Use
- User asks: "How did we implement authentication?"
- User asks: "What bugs did we fix?"
- Looking for past work by meaning/topic (not just keywords)
```
### Fix 2: Implement Layer 3 (Instant Context Timeline)
**Option A: Add to existing search_observations handler**
Modify `/Users/alexnewman/Scripts/claude-mem/src/servers/search-server.ts` line ~396:
```typescript
// After getting sorted results, if user wants timeline context
if (results.length > 0 && options.includeTimeline) {
const topObservation = results[0];
const depth_before = options.timelineDepthBefore || 5;
const depth_after = options.timelineDepthAfter || 5;
// Get observations before and after
const timeline = store.getTimelineContext(
topObservation.id,
depth_before,
depth_after
);
return {
topResult: topObservation,
timeline: timeline,
format: format
};
}
```
**Option B: Use existing timeline-by-query operation**
The `/api/timeline/by-query` endpoint already implements search + timeline. Could:
1. Make it the DEFAULT recommended operation in skill guides
2. Update operation guides to emphasize this as primary workflow
3. Position observations search as "timeline-less" alternative
**Recommendation:** Option B is faster - leverage existing `timeline-by-query` endpoint and update skill guides to make it the primary workflow.
### Fix 3: Update SKILL.md Technical Notes
**File:** `/Users/alexnewman/Scripts/claude-mem/plugin/skills/mem-search/SKILL.md`
**Line 172:**
**Current:**
```markdown
- **Search engine:** FTS5 full-text search + structured filters
```
**Change to:**
```markdown
- **Search engine:** ChromaDB vector search (primary) + SQLite temporal ordering + instant context timeline (3-layer sequential architecture)
```
### Fix 4: Update search_observations Description
**File:** `/Users/alexnewman/Scripts/claude-mem/src/servers/search-server.ts`
**Line 349:**
**Current:**
```typescript
description: 'Search observations using full-text search across titles, narratives...'
```
**Change to:**
```typescript
description: 'Search observations using hybrid semantic search (ChromaDB vector similarity + SQLite temporal ordering). Falls back to FTS5 keyword search if ChromaDB unavailable. IMPORTANT: Always use index format first...'
```
**Line 351:**
**Current:**
```typescript
query: z.string().describe('Search query for FTS5 full-text search'),
```
**Change to:**
```typescript
query: z.string().describe('Search query (semantic vector search via ChromaDB, falls back to FTS5 if unavailable)'),
```
---
## Implementation Checklist
Use this checklist when executing fixes:
### Phase 1: Core Documentation
- [ ] Update `observations.md` title and description
- [ ] Update `observations.md` architecture explanation
- [ ] Update `observations.md` examples to mention ChromaDB
- [ ] Update `common-workflows.md` to explain hybrid flow
- [ ] Update `SKILL.md` line 172 technical notes
- [ ] Verify all operation guides mention ChromaDB correctly
### Phase 2: Backend Updates
- [ ] Update `search-server.ts` search_observations description (line 349)
- [ ] Update `search-server.ts` query parameter description (line 351)
- [ ] Add code comments explaining 3-layer flow
- [ ] Consider adding `includeTimeline` option to search_observations
### Phase 3: Timeline Integration
- [ ] Review timeline-by-query operation
- [ ] Update skill guides to recommend timeline-by-query as primary workflow
- [ ] Add example: "When you need context, use timeline-by-query instead of observations search"
- [ ] Update quick reference table in SKILL.md to highlight timeline-by-query
### Phase 4: Validation
- [ ] Test search behavior with ChromaDB enabled
- [ ] Test fallback behavior with ChromaDB disabled
- [ ] Verify skill guides accurately describe behavior
- [ ] Ensure landing page messaging aligns with skill guides
- [ ] Check that human memory analogy is consistent everywhere
---
## Key Messaging (Use Consistently)
### Value Proposition
"3-layer hybrid search mimics human memory: ChromaDB semantic retrieval finds what's relevant → SQLite temporal ordering identifies when → instant context timeline shows what led there and what came next."
### Technical Architecture
"ChromaDB vector search handles semantic understanding (what's relevant), SQLite handles temporal queries (when it happened, what's newest), and timeline context provides before/after observations (what led there, what happened next)."
### Why It Matters
"LLMs don't experience time linearly like humans do. Claude-mem gives them temporal context: not just 'you implemented authentication,' but 'you researched OAuth libraries, then implemented JWT auth, then fixed a token expiration bug.' Complete narrative, like human memory."
### ChromaDB Role
"ChromaDB is the PRIMARY search mechanism for semantic understanding. FTS5 is the FALLBACK for backward compatibility and reliability when ChromaDB is unavailable."
---
## Files Reference
**Skill Guides (Primary Fixes):**
- `/Users/alexnewman/Scripts/claude-mem/plugin/skills/mem-search/SKILL.md`
- `/Users/alexnewman/Scripts/claude-mem/plugin/skills/mem-search/operations/observations.md`
- `/Users/alexnewman/Scripts/claude-mem/plugin/skills/mem-search/operations/timeline-by-query.md`
- `/Users/alexnewman/Scripts/claude-mem/plugin/skills/mem-search/operations/common-workflows.md`
**Backend Code (Minor Updates):**
- `/Users/alexnewman/Scripts/claude-mem/src/servers/search-server.ts`
**Documentation (Validation):**
- `/Users/alexnewman/Scripts/claude-mem/docs/context/mem-search-technical-architecture.md`
**Landing Page (Already Fixed):**
- `/Users/alexnewman/Scripts/claude-mem-pro/src/components/landing/Features.tsx`
- `/Users/alexnewman/Scripts/claude-mem-pro/src/components/landing/QuickBenefits.tsx`
- `/Users/alexnewman/Scripts/claude-mem-pro/src/components/landing/Architecture.tsx`
---
## Questions for User (If Needed)
1. **Timeline Integration Approach:**
- Option A: Modify search_observations to add `includeTimeline` parameter
- Option B: Emphasize timeline-by-query as primary workflow in guides
- User preference?
2. **Backward Compatibility:**
- Should FTS5 fallback be MORE prominent in docs for older systems?
- Or keep it as "implementation detail"?
3. **Progressive Disclosure:**
- Should timeline context ALWAYS be included?
- Or only when user explicitly asks for context?
---
## Success Criteria
When these fixes are complete:
1. ✅ Skill operation guides accurately describe ChromaDB-first architecture
2. ✅ No references to "FTS5 as primary search method"
3. ✅ Timeline feature integrated into standard workflow
4. ✅ Human memory analogy present in key documentation
5. ✅ Consistent messaging across skill guides, docs, and landing page
6. ✅ Backend code comments explain 3-layer flow clearly
7. ✅ Users understand: "This is semantic search with temporal context, not just keyword search"
---
## Notes for Next Claude
- The user has already clarified the architecture thoroughly
- Backend code is already correct - focus on documentation/guides
- Landing page recently updated - validate for consistency
- Timeline-by-query endpoint already exists - leverage it
- Key insight: This mimics human memory through temporal context
- ChromaDB is PRIMARY, not optional. FTS5 is FALLBACK, not primary.
**Start with:** Reading this document fully, then update skill operation guides first (highest impact).
-614
View File
@@ -1,614 +0,0 @@
# Implementation Plan: ROI Metrics & Discovery Cost Tracking
**Feature**: Display token discovery costs alongside observations to demonstrate knowledge reuse ROI
**Branch**: `enhancement/roi`
**Issue**: #104
**Priority**: HIGH (needed for YC application amendment)
---
## Executive Summary
Capture token usage from Agent SDK, store as "discovery cost" with each observation, and display metrics in SessionStart context to prove that claude-mem reduces token consumption by 50-75% through knowledge reuse.
### The Value Proposition
**Session 1**: Claude spends 4,000 tokens discovering "how Stop hooks work"
**Sessions 2-5**: Claude reads 163-token observation instead of re-discovering
**Savings**: 15,348 tokens (77% reduction) over 5 sessions
This feature makes that ROI **visible and measurable** for both users and Claude.
---
## Architecture Overview
```
Agent SDK Messages (with usage)
SDKAgent captures usage data
ActiveSession tracks cumulative tokens
Observations stored with discovery_tokens
Context hook displays metrics
User/Claude sees ROI
```
---
## Implementation Steps
### Phase 1: Capture Token Usage from Agent SDK
**File**: `src/services/worker/SDKAgent.ts`
**Changes**:
1. Extract usage data from assistant messages (lines 64-86)
2. Track cumulative session tokens in ActiveSession
3. Pass cumulative tokens when storing observations
**Code Changes**:
```typescript
// Line ~70: After extracting textContent, add:
const usage = message.message.usage;
if (usage) {
session.cumulativeInputTokens += usage.input_tokens || 0;
session.cumulativeOutputTokens += usage.output_tokens || 0;
// Cache creation counts as discovery, cache read doesn't
if (usage.cache_creation_input_tokens) {
session.cumulativeInputTokens += usage.cache_creation_input_tokens;
}
logger.debug('SDK', 'Token usage captured', {
sessionId: session.sessionDbId,
inputTokens: usage.input_tokens,
outputTokens: usage.output_tokens,
cumulativeInput: session.cumulativeInputTokens,
cumulativeOutput: session.cumulativeOutputTokens
});
}
```
```typescript
// Line ~213-218: Pass discovery tokens when storing
const { id: obsId, createdAtEpoch } = this.dbManager.getSessionStore().storeObservation(
session.claudeSessionId,
session.project,
obs,
session.lastPromptNumber,
session.cumulativeInputTokens + session.cumulativeOutputTokens // Add discovery cost
);
```
**Edge Cases**:
- Handle missing usage data (default to 0)
- Cache tokens: `cache_creation_input_tokens` counts as discovery, `cache_read_input_tokens` doesn't
- Multiple observations per response: Each gets snapshot of cumulative tokens at creation time
---
### Phase 2: Update ActiveSession Type
**File**: `src/services/worker-types.ts`
**Changes**: Add token tracking fields to ActiveSession interface
```typescript
export interface ActiveSession {
sessionDbId: number;
sdkSessionId: string | null;
claudeSessionId: string;
project: string;
userPrompt: string;
lastPromptNumber: number;
pendingMessages: PendingMessage[];
abortController: AbortController;
startTime: number;
cumulativeInputTokens: number; // NEW: Track input tokens
cumulativeOutputTokens: number; // NEW: Track output tokens
}
```
**Initialization**: When creating new session in SessionManager.initializeSession, set:
```typescript
cumulativeInputTokens: 0,
cumulativeOutputTokens: 0
```
---
### Phase 3: Database Schema Migration
**File**: `src/services/sqlite/migrations.ts`
**Add Migration**: Create migration #8 (next available number)
```typescript
{
version: 8,
name: 'add_discovery_tokens',
up: (db: Database) => {
// Add discovery_tokens to observations
db.exec(`
ALTER TABLE observations
ADD COLUMN discovery_tokens INTEGER DEFAULT 0;
`);
// Add discovery_tokens to summaries
db.exec(`
ALTER TABLE summaries
ADD COLUMN discovery_tokens INTEGER DEFAULT 0;
`);
logger.info('DB', 'Migration 8: Added discovery_tokens columns');
}
}
```
**Why summaries too?**: Summaries represent accumulated session work, so they should also show total discovery cost.
---
### Phase 4: Update SessionStore
**File**: `src/services/sqlite/SessionStore.ts`
**Changes**:
1. Update `storeObservation` signature (around line ~1000):
```typescript
storeObservation(
sessionId: string,
project: string,
observation: ParsedObservation,
promptNumber: number,
discoveryTokens: number = 0 // NEW parameter
): { id: number; createdAtEpoch: number }
```
2. Update INSERT statement to include discovery_tokens:
```typescript
const stmt = this.db.prepare(`
INSERT INTO observations (
session_id,
project,
type,
title,
subtitle,
narrative,
facts,
concepts,
files_read,
files_modified,
prompt_number,
discovery_tokens, -- NEW
created_at_epoch
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
const result = stmt.run(
sessionId,
project,
observation.type,
observation.title,
observation.subtitle || '',
observation.narrative || '',
JSON.stringify(observation.facts || []),
JSON.stringify(observation.concepts || []),
JSON.stringify(observation.files || []),
JSON.stringify([]),
promptNumber,
discoveryTokens, // NEW
createdAtEpoch
);
```
3. Update `storeSummary` similarly (around line ~1150):
```typescript
storeSummary(
sessionId: string,
project: string,
summary: ParsedSummary,
promptNumber: number,
discoveryTokens: number = 0 // NEW parameter
): { id: number; createdAtEpoch: number }
```
---
### Phase 5: Update Database Types
**File**: `src/services/sqlite/types.ts`
**Changes**: Add discovery_tokens to DBObservation and DBSummary interfaces
```typescript
export interface DBObservation {
id: number;
session_id: string;
project: string;
type: 'decision' | 'bugfix' | 'feature' | 'refactor' | 'discovery' | 'change';
title: string;
subtitle: string;
narrative: string | null;
facts: string; // JSON array
concepts: string; // JSON array
files_read: string; // JSON array
files_modified: string; // JSON array
prompt_number: number;
discovery_tokens: number; // NEW
created_at_epoch: number;
}
export interface DBSummary {
id: number;
session_id: string;
request: string;
investigated: string | null;
learned: string | null;
completed: string | null;
next_steps: string | null;
notes: string | null;
project: string;
prompt_number: number;
discovery_tokens: number; // NEW
created_at_epoch: number;
}
```
---
### Phase 6: Update Search Queries
**File**: `src/services/sqlite/SessionSearch.ts`
**Changes**: Ensure all SELECT queries include discovery_tokens
Example (around line ~50, searchObservations):
```typescript
SELECT
o.id,
o.session_id,
o.project,
o.type,
o.title,
o.subtitle,
o.narrative,
o.facts,
o.concepts,
o.files_read,
o.files_modified,
o.prompt_number,
o.discovery_tokens, -- NEW
o.created_at_epoch,
...
```
**Affected methods**:
- `searchObservations`
- `getRecentObservations`
- `getObservationsByType`
- `getObservationsByConcept`
- `getObservationsByFile`
- All other observation query methods
---
### Phase 7: Update Context Hook Display
**File**: `src/hooks/context-hook.ts`
**Changes**: Display discovery costs and ROI metrics in SessionStart context
**Section 1: Add Aggregate Metrics** (insert after line ~250, before observation table)
```typescript
// Calculate aggregate metrics
const totalObservations = observations.length;
const totalReadTokens = observations.reduce((sum, obs) => {
// Estimate read tokens from observation size
const obsSize = (obs.title?.length || 0) +
(obs.subtitle?.length || 0) +
(obs.narrative?.length || 0) +
JSON.stringify(obs.facts || []).length;
return sum + Math.ceil(obsSize / 4); // Rough token estimate
}, 0);
const totalDiscoveryTokens = observations.reduce((sum, obs) => sum + (obs.discovery_tokens || 0), 0);
const savings = totalDiscoveryTokens - totalReadTokens;
const savingsPercent = totalDiscoveryTokens > 0
? Math.round((savings / totalDiscoveryTokens) * 100)
: 0;
output += `\n📊 **Context Economics**:\n`;
output += ` - Loading: ${totalObservations} observations (${totalReadTokens.toLocaleString()} tokens to read)\n`;
output += ` - Discovery investment: ${totalDiscoveryTokens.toLocaleString()} tokens spent by previous sessions\n`;
output += ` - Your savings: ${savings.toLocaleString()} tokens (${savingsPercent}% reduction from reuse)\n\n`;
```
**Section 2: Update Table Format** (around line ~280)
```typescript
output += `| ID | Time | T | Title | Read | Discovery |\n`;
output += `|----|------|---|-------|------|-----------||\n`;
for (const obs of observations) {
const time = formatTime(obs.created_at_epoch);
const emoji = getTypeEmoji(obs.type);
const title = truncate(obs.title, 50);
// Estimate read tokens (observation size in tokens)
const obsSize = (obs.title?.length || 0) +
(obs.subtitle?.length || 0) +
(obs.narrative?.length || 0) +
JSON.stringify(obs.facts || []).length;
const readTokens = Math.ceil(obsSize / 4);
const discoveryTokens = obs.discovery_tokens || 0;
const discoveryDisplay = discoveryTokens > 0
? `🔍 ${discoveryTokens.toLocaleString()}`
: '-';
output += `| #${obs.id} | ${time} | ${emoji} | ${title} | ~${readTokens} | ${discoveryDisplay} |\n`;
}
```
**Section 3: Add Footer Explanation** (after table)
```typescript
output += `\n💡 **Column Key**:\n`;
output += ` - **Read**: Tokens to read this observation (cost to learn it now)\n`;
output += ` - **Discovery**: Tokens Previous Claude spent exploring/researching this topic\n`;
output += `\n**ROI**: Reading these learnings instead of re-discovering saves ${savingsPercent}% tokens\n`;
```
**Edge Case**: Handle old observations without discovery_tokens (show '-' or 0)
---
### Phase 8: Update Chroma Sync (Optional)
**File**: `src/services/sync/ChromaSync.ts`
**Changes**: Include discovery_tokens in vector metadata
```typescript
// Around line ~100, syncObservation metadata
metadata: {
session_id: sessionId,
project: project,
type: observation.type,
title: observation.title,
prompt_number: promptNumber,
discovery_tokens: discoveryTokens, // NEW
created_at_epoch: createdAtEpoch,
...
}
```
**Why?**: Enables semantic search to factor in discovery cost for relevance scoring (future enhancement)
---
## Testing Plan
### Unit Tests
1. **Token Capture Test**:
- Mock Agent SDK response with usage data
- Verify ActiveSession.cumulativeTokens increments correctly
- Test cache token handling (creation counts, read doesn't)
2. **Storage Test**:
- Create observation with discovery_tokens
- Verify database stores correctly
- Query back and verify field present
3. **Display Test**:
- Create test observations with varying discovery costs
- Run context-hook
- Verify metrics calculate correctly
- Verify table displays both Read and Discovery columns
### Integration Tests
1. **Full Session Flow**:
- Start new session
- Trigger multiple tool executions
- Generate observations
- Verify cumulative tokens accumulate
- Check context displays metrics
2. **Migration Test**:
- Backup existing database
- Run migration #8
- Verify columns added
- Verify existing data intact (discovery_tokens = 0)
- Test new observations store correctly
### Manual Testing
1. **Real Usage Scenario**:
- Start fresh Claude Code session
- Perform research task (read files, search codebase)
- Generate observations via claude-mem
- Check database for discovery_tokens values
- Start new session, verify context shows metrics
2. **YC Demo Data**:
- Run 5 sessions on same topic
- Collect token data for each session
- Calculate actual ROI (Session 1 cost vs Sessions 2-5)
- Screenshot metrics for YC application
---
## Rollout Plan
### Phase 1: Data Collection (Week 1)
- Deploy migration and token capture
- Run without displaying metrics yet
- Verify data quality and accuracy
- Fix any issues with token tracking
### Phase 2: Display Metrics (Week 2)
- Enable context hook display
- Gather user feedback
- Iterate on presentation format
- Document any edge cases
### Phase 3: YC Application (Week 2-3)
- Collect empirical data from real usage
- Generate charts/graphs showing ROI
- Write case study with actual numbers
- Amend YC application with proof
### Phase 4: Public Launch (Week 4)
- Blog post explaining the feature
- Update README with ROI metrics
- Submit to HN/Reddit with data
- Reach out to Anthropic with findings
---
## Success Metrics
**Technical Success**:
- ✅ Token capture accuracy: >95% of SDK responses captured
- ✅ Database migration: 0 data loss, all observations migrated
- ✅ Display accuracy: Metrics match raw data within 5%
**Business Success**:
- ✅ Demonstrate 50-75% token reduction across 10+ sessions
- ✅ YC application strengthened with empirical data
- ✅ User/Claude understanding of ROI improves (survey/feedback)
**Strategic Success**:
- ✅ Proof that memory optimization reduces infrastructure needs
- ✅ Data compelling enough for Anthropic partnership discussion
- ✅ Foundation for enterprise licensing ROI calculator
---
## Open Questions
1. **Token Attribution**:
- Should each observation get cumulative session tokens, or split proportionally?
- **Decision**: Use cumulative (simpler, shows total cost at that point)
2. **Cache Tokens**:
- How to handle cache_read_input_tokens in ROI calculation?
- **Decision**: Don't count cache reads as discovery (they're already discovered)
3. **Display Format**:
- Show raw token counts or human-readable format (K, M)?
- **Decision**: Use toLocaleString() for readability (e.g., "4,000" not "4K")
4. **Pricing Display**:
- Should we show dollar costs too, or just tokens?
- **Decision**: Tokens only initially. Pricing varies by model/plan, adds complexity
5. **Historical Data**:
- What to do with old observations without discovery_tokens?
- **Decision**: Show as 0 or '-', document limitation
---
## Files Modified Summary
**Core Implementation**:
- `src/services/worker/SDKAgent.ts` - Capture usage, pass to storage
- `src/services/worker-types.ts` - Add cumulative token fields
- `src/services/sqlite/migrations.ts` - Migration #8 for discovery_tokens
- `src/services/sqlite/SessionStore.ts` - Store discovery tokens
- `src/services/sqlite/types.ts` - Update interfaces
- `src/services/sqlite/SessionSearch.ts` - Include in queries
- `src/hooks/context-hook.ts` - Display metrics
**Optional**:
- `src/services/sync/ChromaSync.ts` - Include in vector metadata
- `src/services/worker/SessionManager.ts` - Initialize cumulative tokens
**Documentation**:
- `CLAUDE.md` - Update with new feature
- `README.md` - Add ROI metrics section
- Issue #104 - Track implementation progress
---
## Timeline Estimate
**Day 1** (Tomorrow):
- [ ] Create branch ✅
- [ ] Write implementation plan ✅
- [ ] Phase 1: Capture token usage (2 hours)
- [ ] Phase 2: Update types (30 min)
- [ ] Phase 3: Database migration (1 hour)
**Day 2**:
- [ ] Phase 4: Update SessionStore (1 hour)
- [ ] Phase 5: Update types (30 min)
- [ ] Phase 6: Update search queries (1 hour)
- [ ] Testing: Unit tests (2 hours)
**Day 3**:
- [ ] Phase 7: Update context hook display (2 hours)
- [ ] Testing: Integration tests (2 hours)
- [ ] Manual testing and iteration (2 hours)
**Day 4**:
- [ ] Collect real usage data (ongoing throughout day)
- [ ] Generate YC metrics/charts (2 hours)
- [ ] Amend YC application (2 hours)
- [ ] Documentation updates (1 hour)
**Total**: ~20 hours of development over 4 days
---
## Risk Mitigation
**Risk 1**: Agent SDK usage data incomplete or missing
**Mitigation**: Default to 0, log warnings, don't break existing functionality
**Risk 2**: Migration fails on large databases
**Mitigation**: Test on database copy first, add rollback mechanism
**Risk 3**: Token estimates inaccurate
**Mitigation**: Document methodology, provide "rough estimate" disclaimer
**Risk 4**: Display too noisy/overwhelming
**Mitigation**: Make display configurable via settings, start collapsed
**Risk 5**: YC data not compelling enough
**Mitigation**: Run on diverse projects, cherry-pick best examples, be honest about limitations
---
## Next Steps
1. ✅ Create branch `enhancement/roi`
2. ✅ Write implementation plan
3. Start Phase 1: Implement token capture in SDKAgent.ts
4. Run manual test to verify usage data captured
5. Continue through phases sequentially
6. Collect data for YC application by end of week
---
## Notes for Tomorrow
**Start here**: `src/services/worker/SDKAgent.ts` line 64-86
**Key insight**: `message.message.usage` contains the token data
**Don't forget**: Initialize cumulative tokens to 0 in SessionManager
**Test with**: Simple session that reads a few files and creates 1-2 observations
**The goal**: By end of week, have real numbers showing 50-75% token savings to prove the hypothesis and strengthen YC application.
---
*This plan represents ~20 hours of focused development. Prioritize getting Phase 1-7 working correctly over perfection. The YC data is the critical deliverable.*
@@ -0,0 +1,427 @@
# Endless Mode: Real-Time Context Compression Plan
## Executive Summary
"Endless Mode" is an optional feature that enables Claude sessions to run indefinitely by transparently compressing tool use transcripts in real-time. Using an in-memory transformation layer in the worker service, heavy tool outputs are dynamically replaced with lightweight observations during session resume—without modifying the immutable source transcripts. This allows sessions to continue for weeks or months without hitting context window limits, while preserving full conversation history and maintaining zero risk of data corruption.
---
## Problem Statement
### Current Behavior
Claude sessions accumulate full tool transcripts in the context window:
- File reads: 5k-10k tokens per read
- Bash outputs: 1k-5k tokens per command
- Search results: 2k-8k tokens per search
- Total context limit: ~200k tokens
When the context window fills, users must start a new session, losing conversational continuity.
### What Happens Today
1. Tool executes during session
2. PostToolUse hook captures tool data
3. Worker creates compressed observation (~200-500 tokens)
4. **But**: Full tool transcript stays in Claude's context window
5. **Observation only helps next session** via SessionStart injection
### The Gap
Observations exist and are created in real-time, but they're not used to compress the **current** session's context. We have the compressed data, we just don't apply it to the active session.
---
## Proposed Solution: Endless Mode
### Core Concept
When a session resumes (either after restart or during continuation), **transform messages in memory** by replacing heavy tool use content with lightweight observations before feeding them to the Agent SDK. The source transcript remains immutable on disk.
### Architecture Principle
**Immutable Storage + Ephemeral Transform = Safe Compression**
```
Disk (never modified) Memory (transform) Agent SDK
────────────────────── ────────────────────── ────────────────
transcript.jsonl Load messages Resume session
tool_use_abc → Look up observation → with compressed
tool_use_def Replace content context
tool_use_xyz Feed to SDK
```
### Key Properties
1. **Immutable**: Original transcripts never modified
2. **Non-destructive**: Full history preserved on disk
3. **No duplication**: No forks, no copies
4. **Transparent**: User sees same conversation, compression is under the hood
5. **Optional**: Feature flag allows users to opt-in/out
6. **Reversible**: Can always read original transcript
---
## How It Works
### Session Resume Flow (Endless Mode Enabled)
```
1. User continues session / Claude Code restarts
2. Worker service intercepts resume request
3. Load transcript JSONL from disk (immutable)
4. Transform Loop:
For each message in transcript:
- If tool_use message:
- Query SQLite: SELECT observation WHERE tool_use_id = ?
- Replace tool content with observation (facts, narrative, concepts)
- If other message type:
- Pass through unchanged
5. Feed transformed messages to Agent SDK
6. Agent SDK resumes session with compressed context
7. New tool uses append to original transcript (normal flow)
8. Next resume: Loop repeats, new tool uses also get compressed
```
### Session Resume Flow (Endless Mode Disabled)
```
1. User continues session
2. Load transcript JSONL from disk
3. Feed messages directly to Agent SDK (no transformation)
4. Session resumes with full tool transcripts (current behavior)
```
---
## Implementation Plan
### Phase 1: Foundation (Week 1)
**Goal**: Set up infrastructure for transformation layer
Tasks:
1. Add `tool_use_id` column to observations table (SQLite schema migration)
2. Update PostToolUse hook to capture and store tool_use_id
3. Create `TransformLayer` class in worker service
4. Add `CLAUDE_MEM_ENDLESS_MODE` environment variable (default: false)
5. Write tests for observation lookup by tool_use_id
**Deliverable**: Database schema updated, tool_use_ids being captured
### Phase 2: Transform Logic (Week 2)
**Goal**: Build message transformation engine
Tasks:
1. Implement `TransformLayer.transformMessages(messages)` function
2. Tool use detection logic (identify tool_use messages in transcript)
3. Observation lookup and replacement logic
4. Fallback handling (if observation missing, keep original content)
5. Message serialization/deserialization
**Deliverable**: Working transform function that compresses messages in memory
### Phase 3: Agent SDK Integration (Week 2-3)
**Goal**: Wire transform layer into session resume flow
Tasks:
1. Identify where worker service resumes Agent SDK sessions
2. Inject transform layer before session resume
3. Add feature flag check (only transform if endless mode enabled)
4. Logging and instrumentation (track compression ratios, transform time)
5. Error handling and graceful degradation
**Deliverable**: Worker service can resume sessions with compressed context
### Phase 4: Testing & Validation (Week 3-4)
**Goal**: Verify endless mode works correctly
Tasks:
1. Create test session with 50+ tool uses
2. Enable endless mode and resume session
3. Verify context window usage (should be dramatically lower)
4. Test conversation quality (does Claude have enough context?)
5. Measure performance (transform latency, lookup speed)
6. Edge case testing (missing observations, malformed transcripts)
**Deliverable**: Endless mode working in test environment
### Phase 5: Beta Release (Week 4+)
**Goal**: Release to power users for feedback
Tasks:
1. Documentation (how to enable, what to expect, how to disable)
2. Add endless mode toggle to viewer UI
3. Monitoring and observability (track usage, failures, compression stats)
4. Collect feedback from beta users
5. Iterate based on real-world usage
**Deliverable**: Endless mode available as opt-in beta feature
---
## Technical Requirements
### Database Schema
```sql
-- Add to observations table
ALTER TABLE observations ADD COLUMN tool_use_id TEXT UNIQUE;
CREATE INDEX idx_observations_tool_use_id ON observations(tool_use_id);
```
### Worker Service API
```typescript
interface TransformLayerConfig {
enabled: boolean; // CLAUDE_MEM_ENDLESS_MODE
fallbackToOriginal: boolean; // If observation missing, use full content
maxLookupTime: number; // Timeout for SQLite queries
}
class TransformLayer {
constructor(config: TransformLayerConfig, db: SessionStore);
// Main transform function
async transformMessages(messages: Message[]): Promise<Message[]>;
// Helper functions
private async lookupObservation(toolUseId: string): Promise<Observation | null>;
private replaceToolContent(message: Message, observation: Observation): Message;
private isToolUseMessage(message: Message): boolean;
}
```
### Agent SDK Integration Point
```typescript
// In worker service session resume logic
async function resumeSession(sessionId: string, transcriptPath: string) {
const messages = await loadTranscript(transcriptPath);
// Transform layer (only if endless mode enabled)
const transformedMessages = config.endlessMode
? await transformLayer.transformMessages(messages)
: messages;
// Resume with transformed (or original) messages
return await agentSDK.resumeSession({
sessionId,
messages: transformedMessages
});
}
```
---
## Risks and Mitigations
### Risk 1: Information Loss
**Risk**: Compressed observations may lose critical details that Claude needs to reference later.
**Mitigation**:
- Make endless mode optional (users can disable if quality degrades)
- Improve observation quality (better prompts, more comprehensive facts)
- Hybrid approach: Keep recent N tool uses in full, compress older ones
- Monitor conversation quality metrics
### Risk 2: Transform Performance
**Risk**: Looking up observations for 100+ tool uses during resume could be slow.
**Mitigation**:
- Index tool_use_id in SQLite (O(log n) lookups)
- Batch queries (single SELECT with IN clause)
- Measure and optimize (target <100ms for typical session)
- Cache observations in memory during session
### Risk 3: Missing Observations
**Risk**: Tool use executed but observation not yet created (async worker lag).
**Mitigation**:
- Fallback to original content if observation missing
- Log when fallback occurs (helps identify worker performance issues)
- Allow observations to be created retroactively
- Consider synchronous observation creation for critical tools
### Risk 4: Transcript Corruption
**Risk**: Bug in transform layer could corrupt user conversations.
**Mitigation**:
- **Never modify source transcripts** (read-only)
- Transform happens in memory only
- Extensive testing before beta release
- Feature flag allows instant disable if issues found
- Keep full audit trail in logs
### Risk 5: Agent SDK Compatibility
**Risk**: Agent SDK updates could break transform layer integration.
**Mitigation**:
- Document exact Agent SDK version requirements
- Monitor Agent SDK release notes
- Test against new SDK versions before upgrading
- Graceful degradation if SDK changes detected
---
## Success Criteria
### Proof of Concept Success
- [ ] Transform layer successfully compresses a 50-tool-use session
- [ ] Context window usage reduced by 80%+ compared to uncompressed
- [ ] Session resumes without errors
- [ ] Conversation quality remains high (subjective evaluation)
### Beta Release Success
- [ ] 10+ users running endless mode without issues
- [ ] Average context savings: 85%+ across all sessions
- [ ] Transform latency: <200ms for typical resume
- [ ] Zero transcript corruption incidents
- [ ] Positive user feedback on conversation continuity
### Production Success
- [ ] Endless mode becomes default setting
- [ ] Sessions running for weeks/months without context issues
- [ ] Context window exhaustion becomes rare edge case
- [ ] User-reported "session too long" issues drop to near zero
- [ ] Transform layer performance scales to 1000+ tool use sessions
---
## Configuration
### Environment Variables
```bash
# Enable endless mode (default: false)
CLAUDE_MEM_ENDLESS_MODE=true
# Fallback behavior if observation missing (default: true)
CLAUDE_MEM_TRANSFORM_FALLBACK=true
# Max time to wait for observation lookup (default: 500ms)
CLAUDE_MEM_TRANSFORM_TIMEOUT=500
# Keep recent N tool uses uncompressed (default: 0, compress all)
CLAUDE_MEM_TRANSFORM_KEEP_RECENT=0
```
### User Controls
```typescript
// Future: UI toggle in viewer
interface EndlessModeSettings {
enabled: boolean;
keepRecentToolUses: number; // Hybrid mode
fallbackToOriginal: boolean;
}
```
---
## Context Economics: Before vs. After
### Example Session (50 tool uses)
**Before (Endless Mode OFF):**
```
File reads: 10 × 8,000 tokens = 80,000 tokens
Bash outputs: 20 × 2,000 tokens = 40,000 tokens
Searches: 15 × 4,000 tokens = 60,000 tokens
Other tools: 5 × 1,000 tokens = 5,000 tokens
──────────────────────────────────────────────────
Total: 185,000 tokens
Context remaining: 15,000 tokens (92% full)
```
**After (Endless Mode ON):**
```
File reads: 10 × 300 tokens = 3,000 tokens
Bash outputs: 20 × 250 tokens = 5,000 tokens
Searches: 15 × 400 tokens = 6,000 tokens
Other tools: 5 × 200 tokens = 1,000 tokens
──────────────────────────────────────────────────
Total: 15,000 tokens
Context remaining: 185,000 tokens (7.5% full)
Savings: 170,000 tokens (92% reduction)
```
**Session Longevity:**
- Before: ~50 tool uses before context full
- After: ~600+ tool uses before context full
- **12x longer sessions**
---
## Next Steps
### Immediate Actions (This Week)
1. **Database Migration**: Add tool_use_id column to observations table
2. **Hook Update**: Modify PostToolUse hook to capture tool_use_id from Agent SDK
3. **Architecture Validation**: Confirm where Agent SDK session resume happens in worker service
4. **Prototype**: Build minimal TransformLayer class with observation lookup
### Short Term (Next 2 Weeks)
1. Implement complete transform logic
2. Wire into worker service resume flow
3. Add endless mode feature flag
4. Test with real sessions
### Medium Term (Next Month)
1. Beta release to power users
2. Gather feedback and iterate
3. Performance optimization
4. Documentation and user guides
### Long Term (Future)
1. Make endless mode default
2. Hybrid sliding window (keep recent tools uncompressed)
3. Selective compression by tool type
4. Auto-tune compression based on context usage patterns
---
## Open Questions
1. **Tool Use ID Format**: What does the Agent SDK's tool_use_id look like? Is it UUID, hash, or sequential?
2. **Transcript Format**: What's the exact JSONL schema for tool_use messages? Where is the content we'll replace?
3. **Resume Hook Point**: Where exactly in the worker service does session resume happen? Is there a clear integration point?
4. **Observation Delay**: How long between PostToolUse firing and observation being available in SQLite? Does this affect resume?
5. **Feature Flag Storage**: Environment variable, or persist user preference in database?
---
## Conclusion
Endless Mode transforms claude-mem from a "memory between sessions" system into a "continuous compression engine" that enables truly infinite sessions. By leveraging the observations we're already creating in real-time and applying them as an ephemeral transformation layer during resume, we can extend session longevity by 10-12x without any risk to user data.
The key architectural insight is **immutability**: by never modifying source transcripts and performing all compression in memory, we get the benefits of context window optimization without the risks of data corruption or loss. Combined with the optional nature of the feature, this provides a safe, reversible path to fundamentally better session continuity.
This is the natural evolution of claude-mem: from remembering what happened before, to making it possible to never stop.
+10 -2
View File
@@ -31,8 +31,16 @@ module.exports = {
'*.log',
'*.db',
'*.db-*',
'.git'
]
'.git',
'vector-db', // Ignore Chroma vector DB files
'.claude-mem' // Ignore data directory
],
// Allow extra time for graceful shutdown (cleanup of child processes)
kill_timeout: 5000,
// Wait before restarting to allow full cleanup
wait_ready: true,
// Shutdown signal (SIGTERM for graceful shutdown)
kill_signal: 'SIGTERM'
}
]
};
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "claude-mem",
"version": "5.5.1",
"version": "6.0.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "claude-mem",
"version": "5.5.1",
"version": "6.0.3",
"license": "AGPL-3.0",
"dependencies": {
"@anthropic-ai/claude-agent-sdk": "^0.1.27",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "claude-mem",
"version": "6.0.3",
"version": "6.0.7",
"description": "Memory compression system for Claude Code - persist context across sessions",
"keywords": [
"claude",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "claude-mem",
"version": "6.0.3",
"version": "6.0.7",
"description": "Persistent memory system for Claude Code - seamlessly preserve context across sessions",
"author": {
"name": "Alex Newman"
+1 -1
View File
@@ -166,7 +166,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
INSERT INTO user_prompts_fts(rowid, prompt_text)
VALUES (new.id, new.prompt_text);
END;
`),this.db.exec("COMMIT"),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(10,new Date().toISOString()),console.error("[SessionStore] Successfully created user_prompts table with FTS5 support")}catch(t){throw this.db.exec("ROLLBACK"),t}}catch(e){console.error("[SessionStore] Migration error (create user_prompts table):",e.message)}}ensureDiscoveryTokensColumn(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(7))return;this.db.pragma("table_info(observations)").some(o=>o.name==="discovery_tokens")||(this.db.exec("ALTER TABLE observations ADD COLUMN discovery_tokens INTEGER DEFAULT 0"),console.error("[SessionStore] Added discovery_tokens column to observations table")),this.db.pragma("table_info(session_summaries)").some(o=>o.name==="discovery_tokens")||(this.db.exec("ALTER TABLE session_summaries ADD COLUMN discovery_tokens INTEGER DEFAULT 0"),console.error("[SessionStore] Added discovery_tokens column to session_summaries table")),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(7,new Date().toISOString())}catch(e){console.error("[SessionStore] Discovery tokens migration error:",e.message)}}getRecentSummaries(e,s=10){return this.db.prepare(`
`),this.db.exec("COMMIT"),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(10,new Date().toISOString()),console.error("[SessionStore] Successfully created user_prompts table with FTS5 support")}catch(t){throw this.db.exec("ROLLBACK"),t}}catch(e){console.error("[SessionStore] Migration error (create user_prompts table):",e.message)}}ensureDiscoveryTokensColumn(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(11))return;this.db.pragma("table_info(observations)").some(o=>o.name==="discovery_tokens")||(this.db.exec("ALTER TABLE observations ADD COLUMN discovery_tokens INTEGER DEFAULT 0"),console.error("[SessionStore] Added discovery_tokens column to observations table")),this.db.pragma("table_info(session_summaries)").some(o=>o.name==="discovery_tokens")||(this.db.exec("ALTER TABLE session_summaries ADD COLUMN discovery_tokens INTEGER DEFAULT 0"),console.error("[SessionStore] Added discovery_tokens column to session_summaries table")),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(11,new Date().toISOString())}catch(e){throw console.error("[SessionStore] Discovery tokens migration error:",e.message),e}}getRecentSummaries(e,s=10){return this.db.prepare(`
SELECT
request, investigated, learned, completed, next_steps,
files_read, files_edited, notes, prompt_number, created_at
+1 -1
View File
@@ -166,7 +166,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
INSERT INTO user_prompts_fts(rowid, prompt_text)
VALUES (new.id, new.prompt_text);
END;
`),this.db.exec("COMMIT"),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(10,new Date().toISOString()),console.error("[SessionStore] Successfully created user_prompts table with FTS5 support")}catch(t){throw this.db.exec("ROLLBACK"),t}}catch(e){console.error("[SessionStore] Migration error (create user_prompts table):",e.message)}}ensureDiscoveryTokensColumn(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(7))return;this.db.pragma("table_info(observations)").some(a=>a.name==="discovery_tokens")||(this.db.exec("ALTER TABLE observations ADD COLUMN discovery_tokens INTEGER DEFAULT 0"),console.error("[SessionStore] Added discovery_tokens column to observations table")),this.db.pragma("table_info(session_summaries)").some(a=>a.name==="discovery_tokens")||(this.db.exec("ALTER TABLE session_summaries ADD COLUMN discovery_tokens INTEGER DEFAULT 0"),console.error("[SessionStore] Added discovery_tokens column to session_summaries table")),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(7,new Date().toISOString())}catch(e){console.error("[SessionStore] Discovery tokens migration error:",e.message)}}getRecentSummaries(e,s=10){return this.db.prepare(`
`),this.db.exec("COMMIT"),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(10,new Date().toISOString()),console.error("[SessionStore] Successfully created user_prompts table with FTS5 support")}catch(t){throw this.db.exec("ROLLBACK"),t}}catch(e){console.error("[SessionStore] Migration error (create user_prompts table):",e.message)}}ensureDiscoveryTokensColumn(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(11))return;this.db.pragma("table_info(observations)").some(a=>a.name==="discovery_tokens")||(this.db.exec("ALTER TABLE observations ADD COLUMN discovery_tokens INTEGER DEFAULT 0"),console.error("[SessionStore] Added discovery_tokens column to observations table")),this.db.pragma("table_info(session_summaries)").some(a=>a.name==="discovery_tokens")||(this.db.exec("ALTER TABLE session_summaries ADD COLUMN discovery_tokens INTEGER DEFAULT 0"),console.error("[SessionStore] Added discovery_tokens column to session_summaries table")),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(11,new Date().toISOString())}catch(e){throw console.error("[SessionStore] Discovery tokens migration error:",e.message),e}}getRecentSummaries(e,s=10){return this.db.prepare(`
SELECT
request, investigated, learned, completed, next_steps,
files_read, files_edited, notes, prompt_number, created_at
+1 -1
View File
@@ -166,7 +166,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
INSERT INTO user_prompts_fts(rowid, prompt_text)
VALUES (new.id, new.prompt_text);
END;
`),this.db.exec("COMMIT"),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(10,new Date().toISOString()),console.error("[SessionStore] Successfully created user_prompts table with FTS5 support")}catch(t){throw this.db.exec("ROLLBACK"),t}}catch(e){console.error("[SessionStore] Migration error (create user_prompts table):",e.message)}}ensureDiscoveryTokensColumn(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(7))return;this.db.pragma("table_info(observations)").some(o=>o.name==="discovery_tokens")||(this.db.exec("ALTER TABLE observations ADD COLUMN discovery_tokens INTEGER DEFAULT 0"),console.error("[SessionStore] Added discovery_tokens column to observations table")),this.db.pragma("table_info(session_summaries)").some(o=>o.name==="discovery_tokens")||(this.db.exec("ALTER TABLE session_summaries ADD COLUMN discovery_tokens INTEGER DEFAULT 0"),console.error("[SessionStore] Added discovery_tokens column to session_summaries table")),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(7,new Date().toISOString())}catch(e){console.error("[SessionStore] Discovery tokens migration error:",e.message)}}getRecentSummaries(e,s=10){return this.db.prepare(`
`),this.db.exec("COMMIT"),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(10,new Date().toISOString()),console.error("[SessionStore] Successfully created user_prompts table with FTS5 support")}catch(t){throw this.db.exec("ROLLBACK"),t}}catch(e){console.error("[SessionStore] Migration error (create user_prompts table):",e.message)}}ensureDiscoveryTokensColumn(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(11))return;this.db.pragma("table_info(observations)").some(o=>o.name==="discovery_tokens")||(this.db.exec("ALTER TABLE observations ADD COLUMN discovery_tokens INTEGER DEFAULT 0"),console.error("[SessionStore] Added discovery_tokens column to observations table")),this.db.pragma("table_info(session_summaries)").some(o=>o.name==="discovery_tokens")||(this.db.exec("ALTER TABLE session_summaries ADD COLUMN discovery_tokens INTEGER DEFAULT 0"),console.error("[SessionStore] Added discovery_tokens column to session_summaries table")),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(11,new Date().toISOString())}catch(e){throw console.error("[SessionStore] Discovery tokens migration error:",e.message),e}}getRecentSummaries(e,s=10){return this.db.prepare(`
SELECT
request, investigated, learned, completed, next_steps,
files_read, files_edited, notes, prompt_number, created_at
+1 -1
View File
@@ -166,7 +166,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
INSERT INTO user_prompts_fts(rowid, prompt_text)
VALUES (new.id, new.prompt_text);
END;
`),this.db.exec("COMMIT"),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(10,new Date().toISOString()),console.error("[SessionStore] Successfully created user_prompts table with FTS5 support")}catch(t){throw this.db.exec("ROLLBACK"),t}}catch(e){console.error("[SessionStore] Migration error (create user_prompts table):",e.message)}}ensureDiscoveryTokensColumn(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(7))return;this.db.pragma("table_info(observations)").some(i=>i.name==="discovery_tokens")||(this.db.exec("ALTER TABLE observations ADD COLUMN discovery_tokens INTEGER DEFAULT 0"),console.error("[SessionStore] Added discovery_tokens column to observations table")),this.db.pragma("table_info(session_summaries)").some(i=>i.name==="discovery_tokens")||(this.db.exec("ALTER TABLE session_summaries ADD COLUMN discovery_tokens INTEGER DEFAULT 0"),console.error("[SessionStore] Added discovery_tokens column to session_summaries table")),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(7,new Date().toISOString())}catch(e){console.error("[SessionStore] Discovery tokens migration error:",e.message)}}getRecentSummaries(e,s=10){return this.db.prepare(`
`),this.db.exec("COMMIT"),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(10,new Date().toISOString()),console.error("[SessionStore] Successfully created user_prompts table with FTS5 support")}catch(t){throw this.db.exec("ROLLBACK"),t}}catch(e){console.error("[SessionStore] Migration error (create user_prompts table):",e.message)}}ensureDiscoveryTokensColumn(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(11))return;this.db.pragma("table_info(observations)").some(i=>i.name==="discovery_tokens")||(this.db.exec("ALTER TABLE observations ADD COLUMN discovery_tokens INTEGER DEFAULT 0"),console.error("[SessionStore] Added discovery_tokens column to observations table")),this.db.pragma("table_info(session_summaries)").some(i=>i.name==="discovery_tokens")||(this.db.exec("ALTER TABLE session_summaries ADD COLUMN discovery_tokens INTEGER DEFAULT 0"),console.error("[SessionStore] Added discovery_tokens column to session_summaries table")),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(11,new Date().toISOString())}catch(e){throw console.error("[SessionStore] Discovery tokens migration error:",e.message),e}}getRecentSummaries(e,s=10){return this.db.prepare(`
SELECT
request, investigated, learned, completed, next_steps,
files_read, files_edited, notes, prompt_number, created_at
+1 -1
View File
@@ -166,7 +166,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
INSERT INTO user_prompts_fts(rowid, prompt_text)
VALUES (new.id, new.prompt_text);
END;
`),this.db.exec("COMMIT"),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(10,new Date().toISOString()),console.error("[SessionStore] Successfully created user_prompts table with FTS5 support")}catch(t){throw this.db.exec("ROLLBACK"),t}}catch(e){console.error("[SessionStore] Migration error (create user_prompts table):",e.message)}}ensureDiscoveryTokensColumn(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(7))return;this.db.pragma("table_info(observations)").some(i=>i.name==="discovery_tokens")||(this.db.exec("ALTER TABLE observations ADD COLUMN discovery_tokens INTEGER DEFAULT 0"),console.error("[SessionStore] Added discovery_tokens column to observations table")),this.db.pragma("table_info(session_summaries)").some(i=>i.name==="discovery_tokens")||(this.db.exec("ALTER TABLE session_summaries ADD COLUMN discovery_tokens INTEGER DEFAULT 0"),console.error("[SessionStore] Added discovery_tokens column to session_summaries table")),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(7,new Date().toISOString())}catch(e){console.error("[SessionStore] Discovery tokens migration error:",e.message)}}getRecentSummaries(e,s=10){return this.db.prepare(`
`),this.db.exec("COMMIT"),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(10,new Date().toISOString()),console.error("[SessionStore] Successfully created user_prompts table with FTS5 support")}catch(t){throw this.db.exec("ROLLBACK"),t}}catch(e){console.error("[SessionStore] Migration error (create user_prompts table):",e.message)}}ensureDiscoveryTokensColumn(){try{if(this.db.prepare("SELECT version FROM schema_versions WHERE version = ?").get(11))return;this.db.pragma("table_info(observations)").some(i=>i.name==="discovery_tokens")||(this.db.exec("ALTER TABLE observations ADD COLUMN discovery_tokens INTEGER DEFAULT 0"),console.error("[SessionStore] Added discovery_tokens column to observations table")),this.db.pragma("table_info(session_summaries)").some(i=>i.name==="discovery_tokens")||(this.db.exec("ALTER TABLE session_summaries ADD COLUMN discovery_tokens INTEGER DEFAULT 0"),console.error("[SessionStore] Added discovery_tokens column to session_summaries table")),this.db.prepare("INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)").run(11,new Date().toISOString())}catch(e){throw console.error("[SessionStore] Discovery tokens migration error:",e.message),e}}getRecentSummaries(e,s=10){return this.db.prepare(`
SELECT
request, investigated, learned, completed, next_steps,
files_read, files_edited, notes, prompt_number, created_at
File diff suppressed because one or more lines are too long
+41
View File
@@ -1740,6 +1740,47 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
}
});
// Cleanup function to properly terminate all child processes
async function cleanup() {
console.error('[search-server] Shutting down...');
// Close Chroma client (terminates uvx/python processes)
if (chromaClient) {
try {
await chromaClient.close();
console.error('[search-server] Chroma client closed');
} catch (error: any) {
console.error('[search-server] Error closing Chroma client:', error.message);
}
}
// Close database connections
if (search) {
try {
search.close();
console.error('[search-server] SessionSearch closed');
} catch (error: any) {
console.error('[search-server] Error closing SessionSearch:', error.message);
}
}
if (store) {
try {
store.close();
console.error('[search-server] SessionStore closed');
} catch (error: any) {
console.error('[search-server] Error closing SessionStore:', error.message);
}
}
console.error('[search-server] Shutdown complete');
process.exit(0);
}
// Register cleanup handlers for graceful shutdown
process.on('SIGTERM', cleanup);
process.on('SIGINT', cleanup);
// Start the server
async function main() {
// Start the MCP server FIRST (critical - must start before blocking operations)
+8 -5
View File
@@ -494,12 +494,14 @@ export class SessionStore {
}
/**
* Ensure discovery_tokens column exists (migration 7)
* Ensure discovery_tokens column exists (migration 11)
* CRITICAL: This migration was incorrectly using version 7 (which was already taken by removeSessionSummariesUniqueConstraint)
* The duplicate version number may have caused migration tracking issues in some databases
*/
private ensureDiscoveryTokensColumn(): void {
try {
// Check if migration already applied
const applied = this.db.prepare('SELECT version FROM schema_versions WHERE version = ?').get(7) as {version: number} | undefined;
// Check if migration already applied to avoid unnecessary re-runs
const applied = this.db.prepare('SELECT version FROM schema_versions WHERE version = ?').get(11) as {version: number} | undefined;
if (applied) return;
// Check if discovery_tokens column exists in observations table
@@ -520,10 +522,11 @@ export class SessionStore {
console.error('[SessionStore] Added discovery_tokens column to session_summaries table');
}
// Record migration
this.db.prepare('INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)').run(7, new Date().toISOString());
// Record migration only after successful column verification/addition
this.db.prepare('INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)').run(11, new Date().toISOString());
} catch (error: any) {
console.error('[SessionStore] Discovery tokens migration error:', error.message);
throw error; // Re-throw to prevent silent failures
}
}
+46 -1
View File
@@ -180,10 +180,45 @@ export class WorkerService {
this.app.get('/api/search/help', this.handleSearchHelp.bind(this));
}
/**
* Cleanup orphaned MCP server processes (uvx/chroma) from previous sessions
*/
private async cleanupOrphanedProcesses(): Promise<void> {
try {
const { execSync } = await import('child_process');
// Find orphaned uvx processes (which spawn chroma servers)
try {
const processes = execSync('pgrep -fl uvx', { encoding: 'utf-8', stdio: 'pipe' }).trim();
if (processes) {
const processCount = processes.split('\n').length;
logger.info('WORKER', 'Cleaning up orphaned MCP processes', { count: processCount });
// Kill the processes
execSync('pkill -f uvx', { stdio: 'pipe' });
logger.success('WORKER', `Cleaned up ${processCount} orphaned MCP server processes`);
}
} catch (error: any) {
// pgrep returns exit code 1 if no processes found (not an error)
if (error.status === 1) {
logger.debug('WORKER', 'No orphaned MCP processes to clean up');
} else {
throw error;
}
}
} catch (error) {
// Don't fail startup if cleanup fails
logger.warn('WORKER', 'Failed to cleanup orphaned processes (non-fatal)', {}, error as Error);
}
}
/**
* Start the worker service
*/
async start(): Promise<void> {
// Cleanup orphaned processes from previous sessions
await this.cleanupOrphanedProcesses();
// Initialize database (once, stays open)
await this.dbManager.initialize();
@@ -215,6 +250,16 @@ export class WorkerService {
// Shutdown all active sessions
await this.sessionManager.shutdownAll();
// Close MCP client connection (terminates search server process)
if (this.mcpClient) {
try {
await this.mcpClient.close();
logger.info('SYSTEM', 'MCP client closed');
} catch (error) {
logger.error('SYSTEM', 'Failed to close MCP client', {}, error as Error);
}
}
// Close HTTP server
if (this.server) {
await new Promise<void>((resolve, reject) => {
@@ -222,7 +267,7 @@ export class WorkerService {
});
}
// Close database connection
// Close database connection (includes ChromaSync cleanup)
await this.dbManager.close();
logger.info('SYSTEM', 'Worker shutdown complete');
+15 -3
View File
@@ -30,16 +30,28 @@ export class DatabaseManager {
// Initialize ChromaSync
this.chromaSync = new ChromaSync('claude-mem');
// Start background backfill (fire-and-forget)
this.chromaSync.ensureBackfilled().catch(() => {});
// Start background backfill (fire-and-forget, with error logging)
this.chromaSync.ensureBackfilled().catch((error) => {
logger.error('DB', 'Chroma backfill failed (non-fatal)', {}, error);
});
logger.info('DB', 'Database initialized');
}
/**
* Close database connection
* Close database connection and cleanup all resources
*/
async close(): Promise<void> {
// Close ChromaSync first (terminates uvx/python processes)
if (this.chromaSync) {
try {
await this.chromaSync.close();
this.chromaSync = null;
} catch (error) {
logger.error('DB', 'Failed to close ChromaSync', {}, error as Error);
}
}
if (this.sessionStore) {
this.sessionStore.close();
this.sessionStore = null;
+95
View File
@@ -0,0 +1,95 @@
#!/bin/bash
# Test script to verify process cleanup
# This script tests that uvx/python processes are properly cleaned up
set -e
echo "=== Process Cleanup Test ==="
echo ""
# Function to count uvx/python processes
count_processes() {
local count=$(ps aux | grep -E "(uvx|python.*chroma)" | grep -v grep | wc -l)
echo "$count"
}
# Initial count
echo "1. Initial process count:"
initial=$(count_processes)
echo " uvx/python/chroma processes: $initial"
echo ""
# Start a node process that creates ChromaSync
echo "2. Starting test process that creates ChromaSync..."
cat > /tmp/test-chroma-cleanup.mjs << 'EOF'
import { ChromaSync } from './src/services/sync/ChromaSync.js';
const sync = new ChromaSync('test-project');
console.log('[TEST] ChromaSync created, connecting...');
// Try to connect (this spawns uvx process)
try {
await sync.ensureBackfilled();
console.log('[TEST] Backfill started');
} catch (error) {
console.log('[TEST] Backfill failed (expected if no data):', error.message);
}
// Wait a bit for process to start
await new Promise(resolve => setTimeout(resolve, 2000));
const countBefore = parseInt(process.env.COUNT_BEFORE || '0');
const countAfter = process.argv[2];
console.log('[TEST] Process count before:', countBefore);
// Close the sync (should terminate uvx process)
console.log('[TEST] Closing ChromaSync...');
await sync.close();
// Wait for process to terminate
await new Promise(resolve => setTimeout(resolve, 1000));
console.log('[TEST] ChromaSync closed, process should be terminated');
process.exit(0);
EOF
# Run test
COUNT_BEFORE=$initial node /tmp/test-chroma-cleanup.mjs 2>&1 &
TEST_PID=$!
# Wait for process to spawn
sleep 3
# Count during execution
during=$(count_processes)
echo " During execution: $during processes"
echo ""
# Wait for test to complete
wait $TEST_PID 2>/dev/null || true
# Wait a bit for cleanup
sleep 2
# Final count
echo "3. Final process count:"
final=$(count_processes)
echo " uvx/python/chroma processes: $final"
echo ""
# Check if we leaked processes
leaked=$((final - initial))
if [ $leaked -gt 0 ]; then
echo "❌ FAIL: Leaked $leaked process(es)"
echo ""
echo "Current processes:"
ps aux | grep -E "(uvx|python.*chroma)" | grep -v grep
exit 1
else
echo "✅ PASS: No process leaks detected"
fi
# Cleanup
rm -f /tmp/test-chroma-cleanup.mjs