Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d175f1759c | |||
| d55db524f0 | |||
| 7d44fdb289 | |||
| 812de2940d | |||
| 95edf31c14 | |||
| a9ae89a198 | |||
| 047914d087 | |||
| bdf79a439b | |||
| 99b6b85d67 | |||
| 798dec972e | |||
| 286343fef6 | |||
| 9285826547 | |||
| ce3b3733fa | |||
| cf1c966409 | |||
| 02fef487e7 | |||
| 20d45006c0 | |||
| 4f1cd309fd | |||
| c46e4a341a |
@@ -10,7 +10,7 @@
|
||||
"plugins": [
|
||||
{
|
||||
"name": "claude-mem",
|
||||
"version": "6.0.3",
|
||||
"version": "6.0.8",
|
||||
"source": "./plugin",
|
||||
"description": "Persistent memory system for Claude Code - context compression across sessions"
|
||||
}
|
||||
|
||||
+118
@@ -4,6 +4,124 @@ 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.7] - 2025-11-17
|
||||
|
||||
## Critical Hotfix: Database Migration Issue (#121)
|
||||
|
||||
This is an emergency hotfix addressing a critical database migration bug that prevented claude-mem from loading for some users.
|
||||
|
||||
### What was fixed
|
||||
|
||||
**Issue**: Users were seeing `SqliteError: no such column: discovery_tokens` when starting Claude Code.
|
||||
|
||||
**Root Cause**: The `ensureDiscoveryTokensColumn` migration was using version number 7, which was already taken by another migration (`removeSessionSummariesUniqueConstraint`). This duplicate version number caused migration tracking issues in databases that were upgraded through multiple versions.
|
||||
|
||||
**Fix**:
|
||||
- Changed migration version from 7 to 11 (next available)
|
||||
- Added explicit schema_versions check to prevent unnecessary re-runs
|
||||
- Improved error propagation and documentation
|
||||
|
||||
### Upgrade Instructions
|
||||
|
||||
**If you're experiencing the error:**
|
||||
|
||||
Option 1 - Manual fix (preserves history):
|
||||
```bash
|
||||
sqlite3 ~/.claude-mem/claude-mem.db "ALTER TABLE observations ADD COLUMN discovery_tokens INTEGER DEFAULT 0; ALTER TABLE session_summaries ADD COLUMN discovery_tokens INTEGER DEFAULT 0;"
|
||||
```
|
||||
|
||||
Option 2 - Delete and recreate (loses history):
|
||||
```bash
|
||||
rm ~/.claude-mem/claude-mem.db
|
||||
# Restart Claude Code - database will recreate with correct schema
|
||||
```
|
||||
|
||||
Option 3 - Fresh install:
|
||||
Just upgrade to v6.0.7 and the migration will work correctly.
|
||||
|
||||
### Changes
|
||||
|
||||
- **Fixed**: Database migration version conflict (migration 7 → 11) (#121)
|
||||
- **Improved**: Migration error handling and schema_versions tracking
|
||||
|
||||
### Full Changelog
|
||||
|
||||
See [CHANGELOG.md](https://github.com/thedotmack/claude-mem/blob/main/CHANGELOG.md) for complete version history.
|
||||
|
||||
---
|
||||
|
||||
**Affected Users**: @liadtigloo @notmyself - this release fixes your reported issue. Please try one of the upgrade options above and let me know if the issue persists.
|
||||
|
||||
Thanks to everyone who reported this issue with detailed error logs! 🙏
|
||||
|
||||
## [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
|
||||
|
||||
@@ -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.8
|
||||
|
||||
## IMPORTANT: Skills Are Auto-Invoked
|
||||
|
||||
|
||||
@@ -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).
|
||||
@@ -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.
|
||||
Generated
+2
-2
@@ -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
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "claude-mem",
|
||||
"version": "6.0.3",
|
||||
"version": "6.0.8",
|
||||
"description": "Memory compression system for Claude Code - persist context across sessions",
|
||||
"keywords": [
|
||||
"claude",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "claude-mem",
|
||||
"version": "6.0.3",
|
||||
"version": "6.0.8",
|
||||
"description": "Persistent memory system for Claude Code - seamlessly preserve context across sessions",
|
||||
"author": {
|
||||
"name": "Alex Newman"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
import{stdin as I}from"process";import w from"better-sqlite3";import{join as E,dirname as k,basename as G}from"path";import{homedir as O}from"os";import{existsSync as K,mkdirSync as x}from"fs";import{fileURLToPath as U}from"url";function M(){return typeof __dirname<"u"?__dirname:k(U(import.meta.url))}var q=M(),m=process.env.CLAUDE_MEM_DATA_DIR||E(O(),".claude-mem"),R=process.env.CLAUDE_CONFIG_DIR||E(O(),".claude"),J=E(m,"archives"),Q=E(m,"logs"),z=E(m,"trash"),Z=E(m,"backups"),ee=E(m,"settings.json"),f=E(m,"claude-mem.db"),se=E(m,"vector-db"),te=E(R,"settings.json"),re=E(R,"commands"),ne=E(R,"CLAUDE.md");function L(c){x(c,{recursive:!0})}var h=(n=>(n[n.DEBUG=0]="DEBUG",n[n.INFO=1]="INFO",n[n.WARN=2]="WARN",n[n.ERROR=3]="ERROR",n[n.SILENT=4]="SILENT",n))(h||{}),N=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=h[e]??1,this.useColor=process.stdout.isTTY??!1}correlationId(e,s){return`obs-${e}-${s}`}sessionId(e){return`session-${e}`}formatData(e){if(e==null)return"";if(typeof e=="string")return e;if(typeof e=="number"||typeof e=="boolean")return e.toString();if(typeof e=="object"){if(e instanceof Error)return this.level===0?`${e.message}
|
||||
import{stdin as I}from"process";import w from"better-sqlite3";import{join as m,dirname as k,basename as W}from"path";import{homedir as O}from"os";import{existsSync as K,mkdirSync as x}from"fs";import{fileURLToPath as U}from"url";function M(){return typeof __dirname<"u"?__dirname:k(U(import.meta.url))}var q=M(),E=process.env.CLAUDE_MEM_DATA_DIR||m(O(),".claude-mem"),R=process.env.CLAUDE_CONFIG_DIR||m(O(),".claude"),J=m(E,"archives"),Q=m(E,"logs"),z=m(E,"trash"),Z=m(E,"backups"),ee=m(E,"settings.json"),f=m(E,"claude-mem.db"),se=m(E,"vector-db"),te=m(R,"settings.json"),re=m(R,"commands"),ne=m(R,"CLAUDE.md");function L(c){x(c,{recursive:!0})}var h=(n=>(n[n.DEBUG=0]="DEBUG",n[n.INFO=1]="INFO",n[n.WARN=2]="WARN",n[n.ERROR=3]="ERROR",n[n.SILENT=4]="SILENT",n))(h||{}),N=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=h[e]??1,this.useColor=process.stdout.isTTY??!1}correlationId(e,s){return`obs-${e}-${s}`}sessionId(e){return`session-${e}`}formatData(e){if(e==null)return"";if(typeof e=="string")return e;if(typeof e=="number"||typeof e=="boolean")return e.toString();if(typeof e=="object"){if(e instanceof Error)return this.level===0?`${e.message}
|
||||
${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Object.keys(e);return s.length===0?"{}":s.length<=3?JSON.stringify(e):`{${s.length} keys: ${s.slice(0,3).join(", ")}...}`}return String(e)}formatTool(e,s){if(!s)return e;try{let t=typeof s=="string"?JSON.parse(s):s;if(e==="Bash"&&t.command){let r=t.command.length>50?t.command.substring(0,50)+"...":t.command;return`${e}(${r})`}if(e==="Read"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Edit"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Write"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}return e}catch{return e}}log(e,s,t,r,n){if(e<this.level)return;let o=new Date().toISOString().replace("T"," ").substring(0,23),i=h[e].padEnd(5),d=s.padEnd(6),_="";r?.correlationId?_=`[${r.correlationId}] `:r?.sessionId&&(_=`[session-${r.sessionId}] `);let T="";n!=null&&(this.level===0&&typeof n=="object"?T=`
|
||||
`+JSON.stringify(n,null,2):T=" "+this.formatData(n));let u="";if(r){let{sessionId:l,sdkSessionId:S,correlationId:p,...a}=r;Object.keys(a).length>0&&(u=` {${Object.entries(a).map(([y,D])=>`${y}=${D}`).join(", ")}}`)}let b=`[${o}] [${i}] [${d}] ${_}${t}${u}${T}`;e===3?console.error(b):console.log(b)}debug(e,s,t,r){this.log(0,e,s,t,r)}info(e,s,t,r){this.log(1,e,s,t,r)}warn(e,s,t,r){this.log(2,e,s,t,r)}error(e,s,t,r){this.log(3,e,s,t,r)}dataIn(e,s,t,r){this.info(e,`\u2192 ${s}`,t,r)}dataOut(e,s,t,r){this.info(e,`\u2190 ${s}`,t,r)}success(e,s,t,r){this.info(e,`\u2713 ${s}`,t,r)}failure(e,s,t,r){this.error(e,`\u2717 ${s}`,t,r)}timing(e,s,t,r){this.info(e,`\u23F1 ${s}`,r,{duration:`${t}ms`})}},A=new N;var g=class{db;constructor(){L(m),this.db=new w(f),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.initializeSchema(),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields(),this.makeObservationsTextNullable(),this.createUserPromptsTable(),this.ensureDiscoveryTokensColumn()}initializeSchema(){try{this.db.exec(`
|
||||
`+JSON.stringify(n,null,2):T=" "+this.formatData(n));let u="";if(r){let{sessionId:l,sdkSessionId:S,correlationId:p,...a}=r;Object.keys(a).length>0&&(u=` {${Object.entries(a).map(([y,D])=>`${y}=${D}`).join(", ")}}`)}let b=`[${o}] [${i}] [${d}] ${_}${t}${u}${T}`;e===3?console.error(b):console.log(b)}debug(e,s,t,r){this.log(0,e,s,t,r)}info(e,s,t,r){this.log(1,e,s,t,r)}warn(e,s,t,r){this.log(2,e,s,t,r)}error(e,s,t,r){this.log(3,e,s,t,r)}dataIn(e,s,t,r){this.info(e,`\u2192 ${s}`,t,r)}dataOut(e,s,t,r){this.info(e,`\u2190 ${s}`,t,r)}success(e,s,t,r){this.info(e,`\u2713 ${s}`,t,r)}failure(e,s,t,r){this.error(e,`\u2717 ${s}`,t,r)}timing(e,s,t,r){this.info(e,`\u23F1 ${s}`,r,{duration:`${t}ms`})}},A=new N;var g=class{db;constructor(){L(E),this.db=new w(f),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.initializeSchema(),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields(),this.makeObservationsTextNullable(),this.createUserPromptsTable(),this.ensureDiscoveryTokensColumn()}initializeSchema(){try{this.db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS schema_versions (
|
||||
id INTEGER PRIMARY KEY,
|
||||
version INTEGER UNIQUE NOT NULL,
|
||||
@@ -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
|
||||
@@ -397,5 +397,5 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
JOIN sdk_sessions s ON up.claude_session_id = s.claude_session_id
|
||||
WHERE up.created_at_epoch >= ? AND up.created_at_epoch <= ? ${o.replace("project","s.project")}
|
||||
ORDER BY up.created_at_epoch ASC
|
||||
`;try{let l=this.db.prepare(T).all(d,_,...i),S=this.db.prepare(u).all(d,_,...i),p=this.db.prepare(b).all(d,_,...i);return{observations:l,sessions:S.map(a=>({id:a.id,sdk_session_id:a.sdk_session_id,project:a.project,request:a.request,completed:a.completed,next_steps:a.next_steps,created_at:a.created_at,created_at_epoch:a.created_at_epoch})),prompts:p.map(a=>({id:a.id,claude_session_id:a.claude_session_id,project:a.project,prompt:a.prompt_text,created_at:a.created_at,created_at_epoch:a.created_at_epoch}))}}catch(l){return console.error("[SessionStore] Error querying timeline records:",l.message),{observations:[],sessions:[],prompts:[]}}}close(){this.db.close()}};import F from"path";import{homedir as X}from"os";import{existsSync as B,readFileSync as H}from"fs";function v(){try{let c=F.join(X(),".claude-mem","settings.json");if(B(c)){let e=JSON.parse(H(c,"utf-8")),s=parseInt(e.env?.CLAUDE_MEM_WORKER_PORT,10);if(!isNaN(s))return s}}catch{}return parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10)}async function C(c){console.error("[claude-mem cleanup] Hook fired",{input:c?{session_id:c.session_id,cwd:c.cwd,reason:c.reason}:null}),c||(console.log("No input provided - this script is designed to run as a Claude Code SessionEnd hook"),console.log(`
|
||||
`;try{let l=this.db.prepare(T).all(d,_,...i),S=this.db.prepare(u).all(d,_,...i),p=this.db.prepare(b).all(d,_,...i);return{observations:l,sessions:S.map(a=>({id:a.id,sdk_session_id:a.sdk_session_id,project:a.project,request:a.request,completed:a.completed,next_steps:a.next_steps,created_at:a.created_at,created_at_epoch:a.created_at_epoch})),prompts:p.map(a=>({id:a.id,claude_session_id:a.claude_session_id,project:a.project,prompt:a.prompt_text,created_at:a.created_at,created_at_epoch:a.created_at_epoch}))}}catch(l){return console.error("[SessionStore] Error querying timeline records:",l.message),{observations:[],sessions:[],prompts:[]}}}close(){this.db.close()}};import F from"path";import{homedir as X}from"os";import{existsSync as B,readFileSync as j}from"fs";function v(){try{let c=F.join(X(),".claude-mem","settings.json");if(B(c)){let e=JSON.parse(j(c,"utf-8")),s=parseInt(e.env?.CLAUDE_MEM_WORKER_PORT,10);if(!isNaN(s))return s}}catch{}return parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10)}async function C(c){console.error("[claude-mem cleanup] Hook fired",{input:c?{session_id:c.session_id,cwd:c.cwd,reason:c.reason}:null}),c||(console.log("No input provided - this script is designed to run as a Claude Code SessionEnd hook"),console.log(`
|
||||
Expected input format:`),console.log(JSON.stringify({session_id:"string",cwd:"string",transcript_path:"string",hook_event_name:"SessionEnd",reason:"exit"},null,2)),process.exit(0));let{session_id:e,reason:s}=c;console.error("[claude-mem cleanup] Searching for active SDK session",{session_id:e,reason:s});let t=new g,r=t.findActiveSDKSession(e);r||(console.error("[claude-mem cleanup] No active SDK session found",{session_id:e}),t.close(),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)),console.error("[claude-mem cleanup] Active SDK session found",{session_id:r.id,sdk_session_id:r.sdk_session_id,project:r.project,worker_port:r.worker_port}),t.markSessionCompleted(r.id),console.error("[claude-mem cleanup] Session marked as completed in database"),t.close();try{let n=r.worker_port||v();await fetch(`http://127.0.0.1:${n}/sessions/${r.id}/complete`,{method:"POST",signal:AbortSignal.timeout(1e3)}),console.error("[claude-mem cleanup] Worker notified to stop processing indicator")}catch(n){console.error("[claude-mem cleanup] Failed to notify worker (non-critical):",n)}console.error("[claude-mem cleanup] Cleanup completed successfully"),console.log('{"continue": true, "suppressOutput": true}'),process.exit(0)}if(I.isTTY)C(void 0);else{let c="";I.on("data",e=>c+=e),I.on("end",async()=>{let e=c?JSON.parse(c):void 0;await C(e)})}
|
||||
|
||||
@@ -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,7 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
import Y from"path";import{stdin as D}from"process";import X from"better-sqlite3";import{join as m,dirname as U,basename as J}from"path";import{homedir as f}from"os";import{existsSync as ee,mkdirSync as M}from"fs";import{fileURLToPath as w}from"url";function F(){return typeof __dirname<"u"?__dirname:U(w(import.meta.url))}var te=F(),l=process.env.CLAUDE_MEM_DATA_DIR||m(f(),".claude-mem"),h=process.env.CLAUDE_CONFIG_DIR||m(f(),".claude"),re=m(l,"archives"),ne=m(l,"logs"),oe=m(l,"trash"),ie=m(l,"backups"),ae=m(l,"settings.json"),L=m(l,"claude-mem.db"),de=m(l,"vector-db"),pe=m(h,"settings.json"),ce=m(h,"commands"),_e=m(h,"CLAUDE.md");function A(d){M(d,{recursive:!0})}var N=(n=>(n[n.DEBUG=0]="DEBUG",n[n.INFO=1]="INFO",n[n.WARN=2]="WARN",n[n.ERROR=3]="ERROR",n[n.SILENT=4]="SILENT",n))(N||{}),O=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=N[e]??1,this.useColor=process.stdout.isTTY??!1}correlationId(e,s){return`obs-${e}-${s}`}sessionId(e){return`session-${e}`}formatData(e){if(e==null)return"";if(typeof e=="string")return e;if(typeof e=="number"||typeof e=="boolean")return e.toString();if(typeof e=="object"){if(e instanceof Error)return this.level===0?`${e.message}
|
||||
import z from"path";import{stdin as M}from"process";import W from"better-sqlite3";import{join as m,dirname as X,basename as te}from"path";import{homedir as I}from"os";import{existsSync as ie,mkdirSync as P}from"fs";import{fileURLToPath as H}from"url";function B(){return typeof __dirname<"u"?__dirname:X(H(import.meta.url))}var j=B(),l=process.env.CLAUDE_MEM_DATA_DIR||m(I(),".claude-mem"),h=process.env.CLAUDE_CONFIG_DIR||m(I(),".claude"),de=m(l,"archives"),pe=m(l,"logs"),ce=m(l,"trash"),_e=m(l,"backups"),ue=m(l,"settings.json"),L=m(l,"claude-mem.db"),me=m(l,"vector-db"),Ee=m(h,"settings.json"),le=m(h,"commands"),Te=m(h,"CLAUDE.md");function A(a){P(a,{recursive:!0})}function v(){return m(j,"..","..")}var N=(n=>(n[n.DEBUG=0]="DEBUG",n[n.INFO=1]="INFO",n[n.WARN=2]="WARN",n[n.ERROR=3]="ERROR",n[n.SILENT=4]="SILENT",n))(N||{}),O=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=N[e]??1,this.useColor=process.stdout.isTTY??!1}correlationId(e,s){return`obs-${e}-${s}`}sessionId(e){return`session-${e}`}formatData(e){if(e==null)return"";if(typeof e=="string")return e;if(typeof e=="number"||typeof e=="boolean")return e.toString();if(typeof e=="object"){if(e instanceof Error)return this.level===0?`${e.message}
|
||||
${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Object.keys(e);return s.length===0?"{}":s.length<=3?JSON.stringify(e):`{${s.length} keys: ${s.slice(0,3).join(", ")}...}`}return String(e)}formatTool(e,s){if(!s)return e;try{let t=typeof s=="string"?JSON.parse(s):s;if(e==="Bash"&&t.command){let r=t.command.length>50?t.command.substring(0,50)+"...":t.command;return`${e}(${r})`}if(e==="Read"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Edit"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Write"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}return e}catch{return e}}log(e,s,t,r,n){if(e<this.level)return;let o=new Date().toISOString().replace("T"," ").substring(0,23),i=N[e].padEnd(5),p=s.padEnd(6),u="";r?.correlationId?u=`[${r.correlationId}] `:r?.sessionId&&(u=`[session-${r.sessionId}] `);let c="";n!=null&&(this.level===0&&typeof n=="object"?c=`
|
||||
`+JSON.stringify(n,null,2):c=" "+this.formatData(n));let E="";if(r){let{sessionId:T,sdkSessionId:S,correlationId:_,...a}=r;Object.keys(a).length>0&&(E=` {${Object.entries(a).map(([k,x])=>`${k}=${x}`).join(", ")}}`)}let b=`[${o}] [${i}] [${p}] ${u}${t}${E}${c}`;e===3?console.error(b):console.log(b)}debug(e,s,t,r){this.log(0,e,s,t,r)}info(e,s,t,r){this.log(1,e,s,t,r)}warn(e,s,t,r){this.log(2,e,s,t,r)}error(e,s,t,r){this.log(3,e,s,t,r)}dataIn(e,s,t,r){this.info(e,`\u2192 ${s}`,t,r)}dataOut(e,s,t,r){this.info(e,`\u2190 ${s}`,t,r)}success(e,s,t,r){this.info(e,`\u2713 ${s}`,t,r)}failure(e,s,t,r){this.error(e,`\u2717 ${s}`,t,r)}timing(e,s,t,r){this.info(e,`\u23F1 ${s}`,r,{duration:`${t}ms`})}},v=new O;var R=class{db;constructor(){A(l),this.db=new X(L),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.initializeSchema(),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields(),this.makeObservationsTextNullable(),this.createUserPromptsTable(),this.ensureDiscoveryTokensColumn()}initializeSchema(){try{this.db.exec(`
|
||||
`+JSON.stringify(n,null,2):c=" "+this.formatData(n));let E="";if(r){let{sessionId:T,sdkSessionId:S,correlationId:_,...d}=r;Object.keys(d).length>0&&(E=` {${Object.entries(d).map(([w,F])=>`${w}=${F}`).join(", ")}}`)}let b=`[${o}] [${i}] [${p}] ${u}${t}${E}${c}`;e===3?console.error(b):console.log(b)}debug(e,s,t,r){this.log(0,e,s,t,r)}info(e,s,t,r){this.log(1,e,s,t,r)}warn(e,s,t,r){this.log(2,e,s,t,r)}error(e,s,t,r){this.log(3,e,s,t,r)}dataIn(e,s,t,r){this.info(e,`\u2192 ${s}`,t,r)}dataOut(e,s,t,r){this.info(e,`\u2190 ${s}`,t,r)}success(e,s,t,r){this.info(e,`\u2713 ${s}`,t,r)}failure(e,s,t,r){this.error(e,`\u2717 ${s}`,t,r)}timing(e,s,t,r){this.info(e,`\u23F1 ${s}`,r,{duration:`${t}ms`})}},C=new O;var R=class{db;constructor(){A(l),this.db=new W(L),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.initializeSchema(),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields(),this.makeObservationsTextNullable(),this.createUserPromptsTable(),this.ensureDiscoveryTokensColumn()}initializeSchema(){try{this.db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS schema_versions (
|
||||
id INTEGER PRIMARY KEY,
|
||||
version INTEGER UNIQUE NOT NULL,
|
||||
@@ -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
|
||||
@@ -299,7 +299,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
UPDATE sdk_sessions
|
||||
SET sdk_session_id = ?
|
||||
WHERE id = ? AND sdk_session_id IS NULL
|
||||
`).run(s,e).changes===0?(v.debug("DB","sdk_session_id already set, skipping update",{sessionId:e,sdkSessionId:s}),!1):!0}setWorkerPort(e,s){this.db.prepare(`
|
||||
`).run(s,e).changes===0?(C.debug("DB","sdk_session_id already set, skipping update",{sessionId:e,sdkSessionId:s}),!1):!0}setWorkerPort(e,s){this.db.prepare(`
|
||||
UPDATE sdk_sessions
|
||||
SET worker_port = ?
|
||||
WHERE id = ?
|
||||
@@ -369,7 +369,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
WHERE id >= ? ${o}
|
||||
ORDER BY id ASC
|
||||
LIMIT ?
|
||||
`;try{let _=this.db.prepare(T).all(e,...i,t+1),a=this.db.prepare(S).all(e,...i,r+1);if(_.length===0&&a.length===0)return{observations:[],sessions:[],prompts:[]};p=_.length>0?_[_.length-1].created_at_epoch:s,u=a.length>0?a[a.length-1].created_at_epoch:s}catch(_){return console.error("[SessionStore] Error getting boundary observations:",_.message),{observations:[],sessions:[],prompts:[]}}}else{let T=`
|
||||
`;try{let _=this.db.prepare(T).all(e,...i,t+1),d=this.db.prepare(S).all(e,...i,r+1);if(_.length===0&&d.length===0)return{observations:[],sessions:[],prompts:[]};p=_.length>0?_[_.length-1].created_at_epoch:s,u=d.length>0?d[d.length-1].created_at_epoch:s}catch(_){return console.error("[SessionStore] Error getting boundary observations:",_.message),{observations:[],sessions:[],prompts:[]}}}else{let T=`
|
||||
SELECT created_at_epoch
|
||||
FROM observations
|
||||
WHERE created_at_epoch <= ? ${o}
|
||||
@@ -381,7 +381,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
WHERE created_at_epoch >= ? ${o}
|
||||
ORDER BY created_at_epoch ASC
|
||||
LIMIT ?
|
||||
`;try{let _=this.db.prepare(T).all(s,...i,t),a=this.db.prepare(S).all(s,...i,r+1);if(_.length===0&&a.length===0)return{observations:[],sessions:[],prompts:[]};p=_.length>0?_[_.length-1].created_at_epoch:s,u=a.length>0?a[a.length-1].created_at_epoch:s}catch(_){return console.error("[SessionStore] Error getting boundary timestamps:",_.message),{observations:[],sessions:[],prompts:[]}}}let c=`
|
||||
`;try{let _=this.db.prepare(T).all(s,...i,t),d=this.db.prepare(S).all(s,...i,r+1);if(_.length===0&&d.length===0)return{observations:[],sessions:[],prompts:[]};p=_.length>0?_[_.length-1].created_at_epoch:s,u=d.length>0?d[d.length-1].created_at_epoch:s}catch(_){return console.error("[SessionStore] Error getting boundary timestamps:",_.message),{observations:[],sessions:[],prompts:[]}}}let c=`
|
||||
SELECT *
|
||||
FROM observations
|
||||
WHERE created_at_epoch >= ? AND created_at_epoch <= ? ${o}
|
||||
@@ -397,7 +397,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
JOIN sdk_sessions s ON up.claude_session_id = s.claude_session_id
|
||||
WHERE up.created_at_epoch >= ? AND up.created_at_epoch <= ? ${o.replace("project","s.project")}
|
||||
ORDER BY up.created_at_epoch ASC
|
||||
`;try{let T=this.db.prepare(c).all(p,u,...i),S=this.db.prepare(E).all(p,u,...i),_=this.db.prepare(b).all(p,u,...i);return{observations:T,sessions:S.map(a=>({id:a.id,sdk_session_id:a.sdk_session_id,project:a.project,request:a.request,completed:a.completed,next_steps:a.next_steps,created_at:a.created_at,created_at_epoch:a.created_at_epoch})),prompts:_.map(a=>({id:a.id,claude_session_id:a.claude_session_id,project:a.project,prompt:a.prompt_text,created_at:a.created_at,created_at_epoch:a.created_at_epoch}))}}catch(T){return console.error("[SessionStore] Error querying timeline records:",T.message),{observations:[],sessions:[],prompts:[]}}}close(){this.db.close()}};function H(d,e,s){return d==="PreCompact"?e?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:s.reason||"Pre-compact operation failed",suppressOutput:!0}:d==="SessionStart"?e&&s.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:s.context}}:{continue:!0,suppressOutput:!0}:d==="UserPromptSubmit"||d==="PostToolUse"?{continue:!0,suppressOutput:!0}:d==="Stop"?{continue:!0,suppressOutput:!0}:{continue:e,suppressOutput:!0,...s.reason&&!e?{stopReason:s.reason}:{}}}function C(d,e,s={}){let t=H(d,e,s);return JSON.stringify(t)}import P from"path";import{homedir as B}from"os";import{existsSync as j,readFileSync as $}from"fs";var G=100;function g(){try{let d=P.join(B(),".claude-mem","settings.json");if(j(d)){let e=JSON.parse($(d,"utf-8")),s=parseInt(e.env?.CLAUDE_MEM_WORKER_PORT,10);if(!isNaN(s))return s}}catch{}return parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10)}async function W(){try{let d=g();return(await fetch(`http://127.0.0.1:${d}/health`,{signal:AbortSignal.timeout(G)})).ok}catch{return!1}}async function y(){if(await W())return;let d=g();throw new Error(`Worker service is not responding on port ${d}.
|
||||
`;try{let T=this.db.prepare(c).all(p,u,...i),S=this.db.prepare(E).all(p,u,...i),_=this.db.prepare(b).all(p,u,...i);return{observations:T,sessions:S.map(d=>({id:d.id,sdk_session_id:d.sdk_session_id,project:d.project,request:d.request,completed:d.completed,next_steps:d.next_steps,created_at:d.created_at,created_at_epoch:d.created_at_epoch})),prompts:_.map(d=>({id:d.id,claude_session_id:d.claude_session_id,project:d.project,prompt:d.prompt_text,created_at:d.created_at,created_at_epoch:d.created_at_epoch}))}}catch(T){return console.error("[SessionStore] Error querying timeline records:",T.message),{observations:[],sessions:[],prompts:[]}}}close(){this.db.close()}};function $(a,e,s){return a==="PreCompact"?e?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:s.reason||"Pre-compact operation failed",suppressOutput:!0}:a==="SessionStart"?e&&s.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:s.context}}:{continue:!0,suppressOutput:!0}:a==="UserPromptSubmit"||a==="PostToolUse"?{continue:!0,suppressOutput:!0}:a==="Stop"?{continue:!0,suppressOutput:!0}:{continue:e,suppressOutput:!0,...s.reason&&!e?{stopReason:s.reason}:{}}}function y(a,e,s={}){let t=$(a,e,s);return JSON.stringify(t)}import D from"path";import{homedir as G}from"os";import{existsSync as k,readFileSync as Y}from"fs";import{execSync as K}from"child_process";var V=100,q=500,J=10;function g(){try{let a=D.join(G(),".claude-mem","settings.json");if(k(a)){let e=JSON.parse(Y(a,"utf-8")),s=parseInt(e.env?.CLAUDE_MEM_WORKER_PORT,10);if(!isNaN(s))return s}}catch{}return parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10)}async function x(){try{let a=g();return(await fetch(`http://127.0.0.1:${a}/health`,{signal:AbortSignal.timeout(V)})).ok}catch{return!1}}async function Q(){try{let a=v(),e=D.join(a,"ecosystem.config.cjs");if(!k(e))throw new Error(`Ecosystem config not found at ${e}`);K(`pm2 start "${e}"`,{cwd:a,stdio:"pipe",encoding:"utf-8"});for(let s=0;s<J;s++)if(await new Promise(t=>setTimeout(t,q)),await x())return!0;return!1}catch{return!1}}async function U(){if(await x())return;if(!await Q()){let e=g();throw new Error(`Worker service failed to start on port ${e}.
|
||||
|
||||
If you just updated the plugin, PM2's watch mode should restart automatically.
|
||||
If the problem persists, run: pm2 restart claude-mem-worker`)}async function K(d){if(!d)throw new Error("newHook requires input");let{session_id:e,cwd:s,prompt:t}=d,r=Y.basename(s);await y();let n=new R,o=n.createSDKSession(e,r,t),i=n.incrementPromptCounter(o);n.saveUserPrompt(e,i,t),console.error(`[new-hook] Session ${o}, prompt #${i}`),n.close();let p=g(),u=t.startsWith("/")?t.substring(1):t;try{let c=await fetch(`http://127.0.0.1:${p}/sessions/${o}/init`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({project:r,userPrompt:u,promptNumber:i}),signal:AbortSignal.timeout(5e3)});if(!c.ok){let E=await c.text();throw new Error(`Failed to initialize session: ${c.status} ${E}`)}}catch(c){throw c.cause?.code==="ECONNREFUSED"||c.name==="TimeoutError"||c.message.includes("fetch failed")?new Error("There's a problem with the worker. If you just updated, type `pm2 restart claude-mem-worker` in your terminal to continue"):c}console.log(C("UserPromptSubmit",!0))}var I="";D.on("data",d=>I+=d);D.on("end",async()=>{let d=I?JSON.parse(I):void 0;await K(d)});
|
||||
Try manually running: pm2 start ecosystem.config.cjs
|
||||
Or restart: pm2 restart claude-mem-worker`)}}async function Z(a){if(!a)throw new Error("newHook requires input");let{session_id:e,cwd:s,prompt:t}=a,r=z.basename(s);await U();let n=new R,o=n.createSDKSession(e,r,t),i=n.incrementPromptCounter(o);n.saveUserPrompt(e,i,t),console.error(`[new-hook] Session ${o}, prompt #${i}`),n.close();let p=g(),u=t.startsWith("/")?t.substring(1):t;try{let c=await fetch(`http://127.0.0.1:${p}/sessions/${o}/init`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({project:r,userPrompt:u,promptNumber:i}),signal:AbortSignal.timeout(5e3)});if(!c.ok){let E=await c.text();throw new Error(`Failed to initialize session: ${c.status} ${E}`)}}catch(c){throw c.cause?.code==="ECONNREFUSED"||c.name==="TimeoutError"||c.message.includes("fetch failed")?new Error("There's a problem with the worker. If you just updated, type `pm2 restart claude-mem-worker` in your terminal to continue"):c}console.log(y("UserPromptSubmit",!0))}var f="";M.on("data",a=>f+=a);M.on("end",async()=>{let a=f?JSON.parse(f):void 0;await Z(a)});
|
||||
|
||||
+10
-10
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
import{stdin as D}from"process";import X from"better-sqlite3";import{join as m,dirname as U,basename as J}from"path";import{homedir as A}from"os";import{existsSync as ee,mkdirSync as M}from"fs";import{fileURLToPath as w}from"url";function F(){return typeof __dirname<"u"?__dirname:U(w(import.meta.url))}var te=F(),T=process.env.CLAUDE_MEM_DATA_DIR||m(A(),".claude-mem"),N=process.env.CLAUDE_CONFIG_DIR||m(A(),".claude"),re=m(T,"archives"),oe=m(T,"logs"),ne=m(T,"trash"),ie=m(T,"backups"),ae=m(T,"settings.json"),v=m(T,"claude-mem.db"),de=m(T,"vector-db"),pe=m(N,"settings.json"),ce=m(N,"commands"),_e=m(N,"CLAUDE.md");function C(d){M(d,{recursive:!0})}var O=(o=>(o[o.DEBUG=0]="DEBUG",o[o.INFO=1]="INFO",o[o.WARN=2]="WARN",o[o.ERROR=3]="ERROR",o[o.SILENT=4]="SILENT",o))(O||{}),I=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=O[e]??1,this.useColor=process.stdout.isTTY??!1}correlationId(e,s){return`obs-${e}-${s}`}sessionId(e){return`session-${e}`}formatData(e){if(e==null)return"";if(typeof e=="string")return e;if(typeof e=="number"||typeof e=="boolean")return e.toString();if(typeof e=="object"){if(e instanceof Error)return this.level===0?`${e.message}
|
||||
import{stdin as M}from"process";import W from"better-sqlite3";import{join as m,dirname as X,basename as te}from"path";import{homedir as A}from"os";import{existsSync as ie,mkdirSync as H}from"fs";import{fileURLToPath as P}from"url";function B(){return typeof __dirname<"u"?__dirname:X(P(import.meta.url))}var j=B(),T=process.env.CLAUDE_MEM_DATA_DIR||m(A(),".claude-mem"),N=process.env.CLAUDE_CONFIG_DIR||m(A(),".claude"),de=m(T,"archives"),pe=m(T,"logs"),ce=m(T,"trash"),_e=m(T,"backups"),ue=m(T,"settings.json"),v=m(T,"claude-mem.db"),me=m(T,"vector-db"),Ee=m(N,"settings.json"),le=m(N,"commands"),Te=m(N,"CLAUDE.md");function y(a){H(a,{recursive:!0})}function C(){return m(j,"..","..")}var O=(o=>(o[o.DEBUG=0]="DEBUG",o[o.INFO=1]="INFO",o[o.WARN=2]="WARN",o[o.ERROR=3]="ERROR",o[o.SILENT=4]="SILENT",o))(O||{}),f=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=O[e]??1,this.useColor=process.stdout.isTTY??!1}correlationId(e,s){return`obs-${e}-${s}`}sessionId(e){return`session-${e}`}formatData(e){if(e==null)return"";if(typeof e=="string")return e;if(typeof e=="number"||typeof e=="boolean")return e.toString();if(typeof e=="object"){if(e instanceof Error)return this.level===0?`${e.message}
|
||||
${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Object.keys(e);return s.length===0?"{}":s.length<=3?JSON.stringify(e):`{${s.length} keys: ${s.slice(0,3).join(", ")}...}`}return String(e)}formatTool(e,s){if(!s)return e;try{let t=typeof s=="string"?JSON.parse(s):s;if(e==="Bash"&&t.command){let r=t.command.length>50?t.command.substring(0,50)+"...":t.command;return`${e}(${r})`}if(e==="Read"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Edit"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Write"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}return e}catch{return e}}log(e,s,t,r,o){if(e<this.level)return;let i=new Date().toISOString().replace("T"," ").substring(0,23),n=O[e].padEnd(5),p=s.padEnd(6),u="";r?.correlationId?u=`[${r.correlationId}] `:r?.sessionId&&(u=`[session-${r.sessionId}] `);let E="";o!=null&&(this.level===0&&typeof o=="object"?E=`
|
||||
`+JSON.stringify(o,null,2):E=" "+this.formatData(o));let c="";if(r){let{sessionId:b,sdkSessionId:g,correlationId:_,...a}=r;Object.keys(a).length>0&&(c=` {${Object.entries(a).map(([k,x])=>`${k}=${x}`).join(", ")}}`)}let l=`[${i}] [${n}] [${p}] ${u}${t}${c}${E}`;e===3?console.error(l):console.log(l)}debug(e,s,t,r){this.log(0,e,s,t,r)}info(e,s,t,r){this.log(1,e,s,t,r)}warn(e,s,t,r){this.log(2,e,s,t,r)}error(e,s,t,r){this.log(3,e,s,t,r)}dataIn(e,s,t,r){this.info(e,`\u2192 ${s}`,t,r)}dataOut(e,s,t,r){this.info(e,`\u2190 ${s}`,t,r)}success(e,s,t,r){this.info(e,`\u2713 ${s}`,t,r)}failure(e,s,t,r){this.error(e,`\u2717 ${s}`,t,r)}timing(e,s,t,r){this.info(e,`\u23F1 ${s}`,r,{duration:`${t}ms`})}},S=new I;var R=class{db;constructor(){C(T),this.db=new X(v),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.initializeSchema(),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields(),this.makeObservationsTextNullable(),this.createUserPromptsTable(),this.ensureDiscoveryTokensColumn()}initializeSchema(){try{this.db.exec(`
|
||||
`+JSON.stringify(o,null,2):E=" "+this.formatData(o));let c="";if(r){let{sessionId:b,sdkSessionId:R,correlationId:_,...d}=r;Object.keys(d).length>0&&(c=` {${Object.entries(d).map(([w,F])=>`${w}=${F}`).join(", ")}}`)}let l=`[${i}] [${n}] [${p}] ${u}${t}${c}${E}`;e===3?console.error(l):console.log(l)}debug(e,s,t,r){this.log(0,e,s,t,r)}info(e,s,t,r){this.log(1,e,s,t,r)}warn(e,s,t,r){this.log(2,e,s,t,r)}error(e,s,t,r){this.log(3,e,s,t,r)}dataIn(e,s,t,r){this.info(e,`\u2192 ${s}`,t,r)}dataOut(e,s,t,r){this.info(e,`\u2190 ${s}`,t,r)}success(e,s,t,r){this.info(e,`\u2713 ${s}`,t,r)}failure(e,s,t,r){this.error(e,`\u2717 ${s}`,t,r)}timing(e,s,t,r){this.info(e,`\u23F1 ${s}`,r,{duration:`${t}ms`})}},S=new f;var g=class{db;constructor(){y(T),this.db=new W(v),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.initializeSchema(),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields(),this.makeObservationsTextNullable(),this.createUserPromptsTable(),this.ensureDiscoveryTokensColumn()}initializeSchema(){try{this.db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS schema_versions (
|
||||
id INTEGER PRIMARY KEY,
|
||||
version INTEGER UNIQUE NOT NULL,
|
||||
@@ -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
|
||||
@@ -363,25 +363,25 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
WHERE id <= ? ${i}
|
||||
ORDER BY id DESC
|
||||
LIMIT ?
|
||||
`,g=`
|
||||
`,R=`
|
||||
SELECT id, created_at_epoch
|
||||
FROM observations
|
||||
WHERE id >= ? ${i}
|
||||
ORDER BY id ASC
|
||||
LIMIT ?
|
||||
`;try{let _=this.db.prepare(b).all(e,...n,t+1),a=this.db.prepare(g).all(e,...n,r+1);if(_.length===0&&a.length===0)return{observations:[],sessions:[],prompts:[]};p=_.length>0?_[_.length-1].created_at_epoch:s,u=a.length>0?a[a.length-1].created_at_epoch:s}catch(_){return console.error("[SessionStore] Error getting boundary observations:",_.message),{observations:[],sessions:[],prompts:[]}}}else{let b=`
|
||||
`;try{let _=this.db.prepare(b).all(e,...n,t+1),d=this.db.prepare(R).all(e,...n,r+1);if(_.length===0&&d.length===0)return{observations:[],sessions:[],prompts:[]};p=_.length>0?_[_.length-1].created_at_epoch:s,u=d.length>0?d[d.length-1].created_at_epoch:s}catch(_){return console.error("[SessionStore] Error getting boundary observations:",_.message),{observations:[],sessions:[],prompts:[]}}}else{let b=`
|
||||
SELECT created_at_epoch
|
||||
FROM observations
|
||||
WHERE created_at_epoch <= ? ${i}
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`,g=`
|
||||
`,R=`
|
||||
SELECT created_at_epoch
|
||||
FROM observations
|
||||
WHERE created_at_epoch >= ? ${i}
|
||||
ORDER BY created_at_epoch ASC
|
||||
LIMIT ?
|
||||
`;try{let _=this.db.prepare(b).all(s,...n,t),a=this.db.prepare(g).all(s,...n,r+1);if(_.length===0&&a.length===0)return{observations:[],sessions:[],prompts:[]};p=_.length>0?_[_.length-1].created_at_epoch:s,u=a.length>0?a[a.length-1].created_at_epoch:s}catch(_){return console.error("[SessionStore] Error getting boundary timestamps:",_.message),{observations:[],sessions:[],prompts:[]}}}let E=`
|
||||
`;try{let _=this.db.prepare(b).all(s,...n,t),d=this.db.prepare(R).all(s,...n,r+1);if(_.length===0&&d.length===0)return{observations:[],sessions:[],prompts:[]};p=_.length>0?_[_.length-1].created_at_epoch:s,u=d.length>0?d[d.length-1].created_at_epoch:s}catch(_){return console.error("[SessionStore] Error getting boundary timestamps:",_.message),{observations:[],sessions:[],prompts:[]}}}let E=`
|
||||
SELECT *
|
||||
FROM observations
|
||||
WHERE created_at_epoch >= ? AND created_at_epoch <= ? ${i}
|
||||
@@ -397,7 +397,7 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
JOIN sdk_sessions s ON up.claude_session_id = s.claude_session_id
|
||||
WHERE up.created_at_epoch >= ? AND up.created_at_epoch <= ? ${i.replace("project","s.project")}
|
||||
ORDER BY up.created_at_epoch ASC
|
||||
`;try{let b=this.db.prepare(E).all(p,u,...n),g=this.db.prepare(c).all(p,u,...n),_=this.db.prepare(l).all(p,u,...n);return{observations:b,sessions:g.map(a=>({id:a.id,sdk_session_id:a.sdk_session_id,project:a.project,request:a.request,completed:a.completed,next_steps:a.next_steps,created_at:a.created_at,created_at_epoch:a.created_at_epoch})),prompts:_.map(a=>({id:a.id,claude_session_id:a.claude_session_id,project:a.project,prompt:a.prompt_text,created_at:a.created_at,created_at_epoch:a.created_at_epoch}))}}catch(b){return console.error("[SessionStore] Error querying timeline records:",b.message),{observations:[],sessions:[],prompts:[]}}}close(){this.db.close()}};function H(d,e,s){return d==="PreCompact"?e?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:s.reason||"Pre-compact operation failed",suppressOutput:!0}:d==="SessionStart"?e&&s.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:s.context}}:{continue:!0,suppressOutput:!0}:d==="UserPromptSubmit"||d==="PostToolUse"?{continue:!0,suppressOutput:!0}:d==="Stop"?{continue:!0,suppressOutput:!0}:{continue:e,suppressOutput:!0,...s.reason&&!e?{stopReason:s.reason}:{}}}function f(d,e,s={}){let t=H(d,e,s);return JSON.stringify(t)}import P from"path";import{homedir as B}from"os";import{existsSync as j,readFileSync as G}from"fs";var W=100;function h(){try{let d=P.join(B(),".claude-mem","settings.json");if(j(d)){let e=JSON.parse(G(d,"utf-8")),s=parseInt(e.env?.CLAUDE_MEM_WORKER_PORT,10);if(!isNaN(s))return s}}catch{}return parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10)}async function $(){try{let d=h();return(await fetch(`http://127.0.0.1:${d}/health`,{signal:AbortSignal.timeout(W)})).ok}catch{return!1}}async function y(){if(await $())return;let d=h();throw new Error(`Worker service is not responding on port ${d}.
|
||||
`;try{let b=this.db.prepare(E).all(p,u,...n),R=this.db.prepare(c).all(p,u,...n),_=this.db.prepare(l).all(p,u,...n);return{observations:b,sessions:R.map(d=>({id:d.id,sdk_session_id:d.sdk_session_id,project:d.project,request:d.request,completed:d.completed,next_steps:d.next_steps,created_at:d.created_at,created_at_epoch:d.created_at_epoch})),prompts:_.map(d=>({id:d.id,claude_session_id:d.claude_session_id,project:d.project,prompt:d.prompt_text,created_at:d.created_at,created_at_epoch:d.created_at_epoch}))}}catch(b){return console.error("[SessionStore] Error querying timeline records:",b.message),{observations:[],sessions:[],prompts:[]}}}close(){this.db.close()}};function $(a,e,s){return a==="PreCompact"?e?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:s.reason||"Pre-compact operation failed",suppressOutput:!0}:a==="SessionStart"?e&&s.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:s.context}}:{continue:!0,suppressOutput:!0}:a==="UserPromptSubmit"||a==="PostToolUse"?{continue:!0,suppressOutput:!0}:a==="Stop"?{continue:!0,suppressOutput:!0}:{continue:e,suppressOutput:!0,...s.reason&&!e?{stopReason:s.reason}:{}}}function I(a,e,s={}){let t=$(a,e,s);return JSON.stringify(t)}import D from"path";import{homedir as G}from"os";import{existsSync as k,readFileSync as Y}from"fs";import{execSync as K}from"child_process";var V=100,q=500,J=10;function h(){try{let a=D.join(G(),".claude-mem","settings.json");if(k(a)){let e=JSON.parse(Y(a,"utf-8")),s=parseInt(e.env?.CLAUDE_MEM_WORKER_PORT,10);if(!isNaN(s))return s}}catch{}return parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10)}async function x(){try{let a=h();return(await fetch(`http://127.0.0.1:${a}/health`,{signal:AbortSignal.timeout(V)})).ok}catch{return!1}}async function Q(){try{let a=C(),e=D.join(a,"ecosystem.config.cjs");if(!k(e))throw new Error(`Ecosystem config not found at ${e}`);K(`pm2 start "${e}"`,{cwd:a,stdio:"pipe",encoding:"utf-8"});for(let s=0;s<J;s++)if(await new Promise(t=>setTimeout(t,q)),await x())return!0;return!1}catch{return!1}}async function U(){if(await x())return;if(!await Q()){let e=h();throw new Error(`Worker service failed to start on port ${e}.
|
||||
|
||||
If you just updated the plugin, PM2's watch mode should restart automatically.
|
||||
If the problem persists, run: pm2 restart claude-mem-worker`)}var Y=new Set(["ListMcpResourcesTool","SlashCommand","Skill","TodoWrite","AskUserQuestion"]);async function K(d){if(!d)throw new Error("saveHook requires input");let{session_id:e,cwd:s,tool_name:t,tool_input:r,tool_response:o}=d;if(Y.has(t)){console.log(f("PostToolUse",!0));return}await y();let i=new R,n=i.createSDKSession(e,"",""),p=i.getPromptCounter(n);i.close();let u=S.formatTool(t,r),E=h();S.dataIn("HOOK",`PostToolUse: ${u}`,{sessionId:n,workerPort:E});try{let c=await fetch(`http://127.0.0.1:${E}/sessions/${n}/observations`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({tool_name:t,tool_input:r!==void 0?JSON.stringify(r):"{}",tool_response:o!==void 0?JSON.stringify(o):"{}",prompt_number:p,cwd:s||""}),signal:AbortSignal.timeout(2e3)});if(!c.ok){let l=await c.text();throw S.failure("HOOK","Failed to send observation",{sessionId:n,status:c.status},l),new Error(`Failed to send observation to worker: ${c.status} ${l}`)}S.debug("HOOK","Observation sent successfully",{sessionId:n,toolName:t})}catch(c){throw c.cause?.code==="ECONNREFUSED"||c.name==="TimeoutError"||c.message.includes("fetch failed")?new Error("There's a problem with the worker. If you just updated, type `pm2 restart claude-mem-worker` in your terminal to continue"):c}console.log(f("PostToolUse",!0))}var L="";D.on("data",d=>L+=d);D.on("end",async()=>{let d=L?JSON.parse(L):void 0;await K(d)});
|
||||
Try manually running: pm2 start ecosystem.config.cjs
|
||||
Or restart: pm2 restart claude-mem-worker`)}}var z=new Set(["ListMcpResourcesTool","SlashCommand","Skill","TodoWrite","AskUserQuestion"]);async function Z(a){if(!a)throw new Error("saveHook requires input");let{session_id:e,cwd:s,tool_name:t,tool_input:r,tool_response:o}=a;if(z.has(t)){console.log(I("PostToolUse",!0));return}await U();let i=new g,n=i.createSDKSession(e,"",""),p=i.getPromptCounter(n);i.close();let u=S.formatTool(t,r),E=h();S.dataIn("HOOK",`PostToolUse: ${u}`,{sessionId:n,workerPort:E});try{let c=await fetch(`http://127.0.0.1:${E}/sessions/${n}/observations`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({tool_name:t,tool_input:r!==void 0?JSON.stringify(r):"{}",tool_response:o!==void 0?JSON.stringify(o):"{}",prompt_number:p,cwd:s||""}),signal:AbortSignal.timeout(2e3)});if(!c.ok){let l=await c.text();throw S.failure("HOOK","Failed to send observation",{sessionId:n,status:c.status},l),new Error(`Failed to send observation to worker: ${c.status} ${l}`)}S.debug("HOOK","Observation sent successfully",{sessionId:n,toolName:t})}catch(c){throw c.cause?.code==="ECONNREFUSED"||c.name==="TimeoutError"||c.message.includes("fetch failed")?new Error("There's a problem with the worker. If you just updated, type `pm2 restart claude-mem-worker` in your terminal to continue"):c}console.log(I("PostToolUse",!0))}var L="";M.on("data",a=>L+=a);M.on("end",async()=>{let a=L?JSON.parse(L):void 0;await Z(a)});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
import{stdin as k}from"process";import{readFileSync as x,existsSync as U}from"fs";import j from"better-sqlite3";import{join as m,dirname as F,basename as ne}from"path";import{homedir as y}from"os";import{existsSync as de,mkdirSync as X}from"fs";import{fileURLToPath as H}from"url";function B(){return typeof __dirname<"u"?__dirname:F(H(import.meta.url))}var ce=B(),l=process.env.CLAUDE_MEM_DATA_DIR||m(y(),".claude-mem"),f=process.env.CLAUDE_CONFIG_DIR||m(y(),".claude"),ue=m(l,"archives"),_e=m(l,"logs"),me=m(l,"trash"),Ee=m(l,"backups"),le=m(l,"settings.json"),A=m(l,"claude-mem.db"),Te=m(l,"vector-db"),ge=m(f,"settings.json"),be=m(f,"commands"),Se=m(f,"CLAUDE.md");function v(a){X(a,{recursive:!0})}var O=(n=>(n[n.DEBUG=0]="DEBUG",n[n.INFO=1]="INFO",n[n.WARN=2]="WARN",n[n.ERROR=3]="ERROR",n[n.SILENT=4]="SILENT",n))(O||{}),N=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=O[e]??1,this.useColor=process.stdout.isTTY??!1}correlationId(e,s){return`obs-${e}-${s}`}sessionId(e){return`session-${e}`}formatData(e){if(e==null)return"";if(typeof e=="string")return e;if(typeof e=="number"||typeof e=="boolean")return e.toString();if(typeof e=="object"){if(e instanceof Error)return this.level===0?`${e.message}
|
||||
${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Object.keys(e);return s.length===0?"{}":s.length<=3?JSON.stringify(e):`{${s.length} keys: ${s.slice(0,3).join(", ")}...}`}return String(e)}formatTool(e,s){if(!s)return e;try{let t=typeof s=="string"?JSON.parse(s):s;if(e==="Bash"&&t.command){let r=t.command.length>50?t.command.substring(0,50)+"...":t.command;return`${e}(${r})`}if(e==="Read"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Edit"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Write"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}return e}catch{return e}}log(e,s,t,r,n){if(e<this.level)return;let i=new Date().toISOString().replace("T"," ").substring(0,23),o=O[e].padEnd(5),d=s.padEnd(6),c="";r?.correlationId?c=`[${r.correlationId}] `:r?.sessionId&&(c=`[session-${r.sessionId}] `);let u="";n!=null&&(this.level===0&&typeof n=="object"?u=`
|
||||
`+JSON.stringify(n,null,2):u=" "+this.formatData(n));let E="";if(r){let{sessionId:T,sdkSessionId:S,correlationId:_,...p}=r;Object.keys(p).length>0&&(E=` {${Object.entries(p).map(([M,w])=>`${M}=${w}`).join(", ")}}`)}let b=`[${i}] [${o}] [${d}] ${c}${t}${E}${u}`;e===3?console.error(b):console.log(b)}debug(e,s,t,r){this.log(0,e,s,t,r)}info(e,s,t,r){this.log(1,e,s,t,r)}warn(e,s,t,r){this.log(2,e,s,t,r)}error(e,s,t,r){this.log(3,e,s,t,r)}dataIn(e,s,t,r){this.info(e,`\u2192 ${s}`,t,r)}dataOut(e,s,t,r){this.info(e,`\u2190 ${s}`,t,r)}success(e,s,t,r){this.info(e,`\u2713 ${s}`,t,r)}failure(e,s,t,r){this.error(e,`\u2717 ${s}`,t,r)}timing(e,s,t,r){this.info(e,`\u23F1 ${s}`,r,{duration:`${t}ms`})}},g=new N;var R=class{db;constructor(){v(l),this.db=new j(A),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.initializeSchema(),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields(),this.makeObservationsTextNullable(),this.createUserPromptsTable(),this.ensureDiscoveryTokensColumn()}initializeSchema(){try{this.db.exec(`
|
||||
import{stdin as w}from"process";import{readFileSync as F,existsSync as X}from"fs";import Y from"better-sqlite3";import{join as m,dirname as j,basename as pe}from"path";import{homedir as y}from"os";import{existsSync as Ee,mkdirSync as B}from"fs";import{fileURLToPath as $}from"url";function W(){return typeof __dirname<"u"?__dirname:j($(import.meta.url))}var G=W(),l=process.env.CLAUDE_MEM_DATA_DIR||m(y(),".claude-mem"),f=process.env.CLAUDE_CONFIG_DIR||m(y(),".claude"),Te=m(l,"archives"),ge=m(l,"logs"),Se=m(l,"trash"),be=m(l,"backups"),Re=m(l,"settings.json"),A=m(l,"claude-mem.db"),he=m(l,"vector-db"),fe=m(f,"settings.json"),Oe=m(f,"commands"),Ne=m(f,"CLAUDE.md");function v(a){B(a,{recursive:!0})}function C(){return m(G,"..","..")}var O=(n=>(n[n.DEBUG=0]="DEBUG",n[n.INFO=1]="INFO",n[n.WARN=2]="WARN",n[n.ERROR=3]="ERROR",n[n.SILENT=4]="SILENT",n))(O||{}),N=class{level;useColor;constructor(){let e=process.env.CLAUDE_MEM_LOG_LEVEL?.toUpperCase()||"INFO";this.level=O[e]??1,this.useColor=process.stdout.isTTY??!1}correlationId(e,s){return`obs-${e}-${s}`}sessionId(e){return`session-${e}`}formatData(e){if(e==null)return"";if(typeof e=="string")return e;if(typeof e=="number"||typeof e=="boolean")return e.toString();if(typeof e=="object"){if(e instanceof Error)return this.level===0?`${e.message}
|
||||
${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Object.keys(e);return s.length===0?"{}":s.length<=3?JSON.stringify(e):`{${s.length} keys: ${s.slice(0,3).join(", ")}...}`}return String(e)}formatTool(e,s){if(!s)return e;try{let t=typeof s=="string"?JSON.parse(s):s;if(e==="Bash"&&t.command){let r=t.command.length>50?t.command.substring(0,50)+"...":t.command;return`${e}(${r})`}if(e==="Read"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Edit"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}if(e==="Write"&&t.file_path){let r=t.file_path.split("/").pop()||t.file_path;return`${e}(${r})`}return e}catch{return e}}log(e,s,t,r,n){if(e<this.level)return;let i=new Date().toISOString().replace("T"," ").substring(0,23),o=O[e].padEnd(5),d=s.padEnd(6),p="";r?.correlationId?p=`[${r.correlationId}] `:r?.sessionId&&(p=`[session-${r.sessionId}] `);let u="";n!=null&&(this.level===0&&typeof n=="object"?u=`
|
||||
`+JSON.stringify(n,null,2):u=" "+this.formatData(n));let E="";if(r){let{sessionId:T,sdkSessionId:b,correlationId:_,...c}=r;Object.keys(c).length>0&&(E=` {${Object.entries(c).map(([H,P])=>`${H}=${P}`).join(", ")}}`)}let S=`[${i}] [${o}] [${d}] ${p}${t}${E}${u}`;e===3?console.error(S):console.log(S)}debug(e,s,t,r){this.log(0,e,s,t,r)}info(e,s,t,r){this.log(1,e,s,t,r)}warn(e,s,t,r){this.log(2,e,s,t,r)}error(e,s,t,r){this.log(3,e,s,t,r)}dataIn(e,s,t,r){this.info(e,`\u2192 ${s}`,t,r)}dataOut(e,s,t,r){this.info(e,`\u2190 ${s}`,t,r)}success(e,s,t,r){this.info(e,`\u2713 ${s}`,t,r)}failure(e,s,t,r){this.error(e,`\u2717 ${s}`,t,r)}timing(e,s,t,r){this.info(e,`\u23F1 ${s}`,r,{duration:`${t}ms`})}},g=new N;var R=class{db;constructor(){v(l),this.db=new Y(A),this.db.pragma("journal_mode = WAL"),this.db.pragma("synchronous = NORMAL"),this.db.pragma("foreign_keys = ON"),this.initializeSchema(),this.ensureWorkerPortColumn(),this.ensurePromptTrackingColumns(),this.removeSessionSummariesUniqueConstraint(),this.addObservationHierarchicalFields(),this.makeObservationsTextNullable(),this.createUserPromptsTable(),this.ensureDiscoveryTokensColumn()}initializeSchema(){try{this.db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS schema_versions (
|
||||
id INTEGER PRIMARY KEY,
|
||||
version INTEGER UNIQUE NOT NULL,
|
||||
@@ -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
|
||||
@@ -357,31 +357,31 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
WHERE up.id IN (${o})
|
||||
ORDER BY up.created_at_epoch ${n}
|
||||
${i}
|
||||
`).all(...e)}getTimelineAroundTimestamp(e,s=10,t=10,r){return this.getTimelineAroundObservation(null,e,s,t,r)}getTimelineAroundObservation(e,s,t=10,r=10,n){let i=n?"AND project = ?":"",o=n?[n]:[],d,c;if(e!==null){let T=`
|
||||
`).all(...e)}getTimelineAroundTimestamp(e,s=10,t=10,r){return this.getTimelineAroundObservation(null,e,s,t,r)}getTimelineAroundObservation(e,s,t=10,r=10,n){let i=n?"AND project = ?":"",o=n?[n]:[],d,p;if(e!==null){let T=`
|
||||
SELECT id, created_at_epoch
|
||||
FROM observations
|
||||
WHERE id <= ? ${i}
|
||||
ORDER BY id DESC
|
||||
LIMIT ?
|
||||
`,S=`
|
||||
`,b=`
|
||||
SELECT id, created_at_epoch
|
||||
FROM observations
|
||||
WHERE id >= ? ${i}
|
||||
ORDER BY id ASC
|
||||
LIMIT ?
|
||||
`;try{let _=this.db.prepare(T).all(e,...o,t+1),p=this.db.prepare(S).all(e,...o,r+1);if(_.length===0&&p.length===0)return{observations:[],sessions:[],prompts:[]};d=_.length>0?_[_.length-1].created_at_epoch:s,c=p.length>0?p[p.length-1].created_at_epoch:s}catch(_){return console.error("[SessionStore] Error getting boundary observations:",_.message),{observations:[],sessions:[],prompts:[]}}}else{let T=`
|
||||
`;try{let _=this.db.prepare(T).all(e,...o,t+1),c=this.db.prepare(b).all(e,...o,r+1);if(_.length===0&&c.length===0)return{observations:[],sessions:[],prompts:[]};d=_.length>0?_[_.length-1].created_at_epoch:s,p=c.length>0?c[c.length-1].created_at_epoch:s}catch(_){return console.error("[SessionStore] Error getting boundary observations:",_.message),{observations:[],sessions:[],prompts:[]}}}else{let T=`
|
||||
SELECT created_at_epoch
|
||||
FROM observations
|
||||
WHERE created_at_epoch <= ? ${i}
|
||||
ORDER BY created_at_epoch DESC
|
||||
LIMIT ?
|
||||
`,S=`
|
||||
`,b=`
|
||||
SELECT created_at_epoch
|
||||
FROM observations
|
||||
WHERE created_at_epoch >= ? ${i}
|
||||
ORDER BY created_at_epoch ASC
|
||||
LIMIT ?
|
||||
`;try{let _=this.db.prepare(T).all(s,...o,t),p=this.db.prepare(S).all(s,...o,r+1);if(_.length===0&&p.length===0)return{observations:[],sessions:[],prompts:[]};d=_.length>0?_[_.length-1].created_at_epoch:s,c=p.length>0?p[p.length-1].created_at_epoch:s}catch(_){return console.error("[SessionStore] Error getting boundary timestamps:",_.message),{observations:[],sessions:[],prompts:[]}}}let u=`
|
||||
`;try{let _=this.db.prepare(T).all(s,...o,t),c=this.db.prepare(b).all(s,...o,r+1);if(_.length===0&&c.length===0)return{observations:[],sessions:[],prompts:[]};d=_.length>0?_[_.length-1].created_at_epoch:s,p=c.length>0?c[c.length-1].created_at_epoch:s}catch(_){return console.error("[SessionStore] Error getting boundary timestamps:",_.message),{observations:[],sessions:[],prompts:[]}}}let u=`
|
||||
SELECT *
|
||||
FROM observations
|
||||
WHERE created_at_epoch >= ? AND created_at_epoch <= ? ${i}
|
||||
@@ -391,28 +391,28 @@ ${e.stack}`:e.message;if(Array.isArray(e))return`[${e.length} items]`;let s=Obje
|
||||
FROM session_summaries
|
||||
WHERE created_at_epoch >= ? AND created_at_epoch <= ? ${i}
|
||||
ORDER BY created_at_epoch ASC
|
||||
`,b=`
|
||||
`,S=`
|
||||
SELECT up.*, s.project, s.sdk_session_id
|
||||
FROM user_prompts up
|
||||
JOIN sdk_sessions s ON up.claude_session_id = s.claude_session_id
|
||||
WHERE up.created_at_epoch >= ? AND up.created_at_epoch <= ? ${i.replace("project","s.project")}
|
||||
ORDER BY up.created_at_epoch ASC
|
||||
`;try{let T=this.db.prepare(u).all(d,c,...o),S=this.db.prepare(E).all(d,c,...o),_=this.db.prepare(b).all(d,c,...o);return{observations:T,sessions:S.map(p=>({id:p.id,sdk_session_id:p.sdk_session_id,project:p.project,request:p.request,completed:p.completed,next_steps:p.next_steps,created_at:p.created_at,created_at_epoch:p.created_at_epoch})),prompts:_.map(p=>({id:p.id,claude_session_id:p.claude_session_id,project:p.project,prompt:p.prompt_text,created_at:p.created_at,created_at_epoch:p.created_at_epoch}))}}catch(T){return console.error("[SessionStore] Error querying timeline records:",T.message),{observations:[],sessions:[],prompts:[]}}}close(){this.db.close()}};function P(a,e,s){return a==="PreCompact"?e?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:s.reason||"Pre-compact operation failed",suppressOutput:!0}:a==="SessionStart"?e&&s.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:s.context}}:{continue:!0,suppressOutput:!0}:a==="UserPromptSubmit"||a==="PostToolUse"?{continue:!0,suppressOutput:!0}:a==="Stop"?{continue:!0,suppressOutput:!0}:{continue:e,suppressOutput:!0,...s.reason&&!e?{stopReason:s.reason}:{}}}function C(a,e,s={}){let t=P(a,e,s);return JSON.stringify(t)}import $ from"path";import{homedir as G}from"os";import{existsSync as W,readFileSync as Y}from"fs";var K=100;function h(){try{let a=$.join(G(),".claude-mem","settings.json");if(W(a)){let e=JSON.parse(Y(a,"utf-8")),s=parseInt(e.env?.CLAUDE_MEM_WORKER_PORT,10);if(!isNaN(s))return s}}catch{}return parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10)}async function q(){try{let a=h();return(await fetch(`http://127.0.0.1:${a}/health`,{signal:AbortSignal.timeout(K)})).ok}catch{return!1}}async function D(){if(await q())return;let a=h();throw new Error(`Worker service is not responding on port ${a}.
|
||||
`;try{let T=this.db.prepare(u).all(d,p,...o),b=this.db.prepare(E).all(d,p,...o),_=this.db.prepare(S).all(d,p,...o);return{observations:T,sessions:b.map(c=>({id:c.id,sdk_session_id:c.sdk_session_id,project:c.project,request:c.request,completed:c.completed,next_steps:c.next_steps,created_at:c.created_at,created_at_epoch:c.created_at_epoch})),prompts:_.map(c=>({id:c.id,claude_session_id:c.claude_session_id,project:c.project,prompt:c.prompt_text,created_at:c.created_at,created_at_epoch:c.created_at_epoch}))}}catch(T){return console.error("[SessionStore] Error querying timeline records:",T.message),{observations:[],sessions:[],prompts:[]}}}close(){this.db.close()}};function K(a,e,s){return a==="PreCompact"?e?{continue:!0,suppressOutput:!0}:{continue:!1,stopReason:s.reason||"Pre-compact operation failed",suppressOutput:!0}:a==="SessionStart"?e&&s.context?{continue:!0,suppressOutput:!0,hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:s.context}}:{continue:!0,suppressOutput:!0}:a==="UserPromptSubmit"||a==="PostToolUse"?{continue:!0,suppressOutput:!0}:a==="Stop"?{continue:!0,suppressOutput:!0}:{continue:e,suppressOutput:!0,...s.reason&&!e?{stopReason:s.reason}:{}}}function D(a,e,s={}){let t=K(a,e,s);return JSON.stringify(t)}import k from"path";import{homedir as q}from"os";import{existsSync as x,readFileSync as V}from"fs";import{execSync as J}from"child_process";var Q=100,z=500,Z=10;function h(){try{let a=k.join(q(),".claude-mem","settings.json");if(x(a)){let e=JSON.parse(V(a,"utf-8")),s=parseInt(e.env?.CLAUDE_MEM_WORKER_PORT,10);if(!isNaN(s))return s}}catch{}return parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10)}async function U(){try{let a=h();return(await fetch(`http://127.0.0.1:${a}/health`,{signal:AbortSignal.timeout(Q)})).ok}catch{return!1}}async function ee(){try{let a=C(),e=k.join(a,"ecosystem.config.cjs");if(!x(e))throw new Error(`Ecosystem config not found at ${e}`);J(`pm2 start "${e}"`,{cwd:a,stdio:"pipe",encoding:"utf-8"});for(let s=0;s<Z;s++)if(await new Promise(t=>setTimeout(t,z)),await U())return!0;return!1}catch{return!1}}async function M(){if(await U())return;if(!await ee()){let e=h();throw new Error(`Worker service failed to start on port ${e}.
|
||||
|
||||
If you just updated the plugin, PM2's watch mode should restart automatically.
|
||||
If the problem persists, run: pm2 restart claude-mem-worker`)}import{appendFileSync as V}from"fs";import{homedir as J}from"os";import{join as Q}from"path";var z=Q(J(),".claude-mem","silent.log");function I(a,e,s=""){let t=new Date().toISOString(),o=((new Error().stack||"").split(`
|
||||
`)[2]||"").match(/at\s+(?:.*\s+)?\(?([^:]+):(\d+):(\d+)\)?/),d=o?`${o[1].split("/").pop()}:${o[2]}`:"unknown",c=`[${t}] [${d}] ${a}`;if(e!==void 0)try{c+=` ${JSON.stringify(e)}`}catch(u){c+=` [stringify error: ${u}]`}c+=`
|
||||
`;try{V(z,c)}catch(u){console.error("[silent-debug] Failed to write to log:",u)}return s}function Z(a){if(!a||!U(a))return"";try{let e=x(a,"utf-8").trim();if(!e)return"";let s=e.split(`
|
||||
Try manually running: pm2 start ecosystem.config.cjs
|
||||
Or restart: pm2 restart claude-mem-worker`)}}import{appendFileSync as se}from"fs";import{homedir as te}from"os";import{join as re}from"path";var ne=re(te(),".claude-mem","silent.log");function I(a,e,s=""){let t=new Date().toISOString(),o=((new Error().stack||"").split(`
|
||||
`)[2]||"").match(/at\s+(?:.*\s+)?\(?([^:]+):(\d+):(\d+)\)?/),d=o?`${o[1].split("/").pop()}:${o[2]}`:"unknown",p=`[${t}] [${d}] ${a}`;if(e!==void 0)try{p+=` ${JSON.stringify(e)}`}catch(u){p+=` [stringify error: ${u}]`}p+=`
|
||||
`;try{se(ne,p)}catch(u){console.error("[silent-debug] Failed to write to log:",u)}return s}function oe(a){if(!a||!X(a))return"";try{let e=F(a,"utf-8").trim();if(!e)return"";let s=e.split(`
|
||||
`);for(let t=s.length-1;t>=0;t--)try{let r=JSON.parse(s[t]);if(r.type==="user"&&r.message?.content){let n=r.message.content;if(typeof n=="string")return n;if(Array.isArray(n))return n.filter(o=>o.type==="text").map(o=>o.text).join(`
|
||||
`)}}catch{continue}}catch(e){g.error("HOOK","Failed to read transcript",{transcriptPath:a},e)}return""}function ee(a){if(!a||!U(a))return"";try{let e=x(a,"utf-8").trim();if(!e)return"";let s=e.split(`
|
||||
`)}}catch{continue}}catch(e){g.error("HOOK","Failed to read transcript",{transcriptPath:a},e)}return""}function ie(a){if(!a||!X(a))return"";try{let e=F(a,"utf-8").trim();if(!e)return"";let s=e.split(`
|
||||
`);for(let t=s.length-1;t>=0;t--)try{let r=JSON.parse(s[t]);if(r.type==="assistant"&&r.message?.content){let n="",i=r.message.content;return typeof i=="string"?n=i:Array.isArray(i)&&(n=i.filter(d=>d.type==="text").map(d=>d.text).join(`
|
||||
`)),n=n.replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g,""),n=n.replace(/\n{3,}/g,`
|
||||
|
||||
`).trim(),n}}catch{continue}}catch(e){g.error("HOOK","Failed to read transcript",{transcriptPath:a},e)}return""}async function se(a){if(!a)throw new Error("summaryHook requires input");let{session_id:e}=a;await D();let s=new R,t=s.createSDKSession(e,"",""),r=s.getPromptCounter(t),n=s.db.prepare(`
|
||||
`).trim(),n}}catch{continue}}catch(e){g.error("HOOK","Failed to read transcript",{transcriptPath:a},e)}return""}async function ae(a){if(!a)throw new Error("summaryHook requires input");let{session_id:e}=a;await M();let s=new R,t=s.createSDKSession(e,"",""),r=s.getPromptCounter(t),n=s.db.prepare(`
|
||||
SELECT id, claude_session_id, sdk_session_id, project
|
||||
FROM sdk_sessions WHERE id = ?
|
||||
`).get(t),i=s.db.prepare(`
|
||||
SELECT COUNT(*) as count
|
||||
FROM observations
|
||||
WHERE sdk_session_id = ?
|
||||
`).get(n?.sdk_session_id);I("[summary-hook] Session diagnostics",{claudeSessionId:e,sessionDbId:t,sdkSessionId:n?.sdk_session_id,project:n?.project,promptNumber:r,observationCount:i?.count||0,transcriptPath:a.transcript_path}),s.close();let o=h(),d=Z(a.transcript_path||""),c=ee(a.transcript_path||"");I("[summary-hook] Extracted messages",{hasLastUserMessage:!!d,hasLastAssistantMessage:!!c,lastAssistantPreview:c.substring(0,200),lastAssistantLength:c.length}),g.dataIn("HOOK","Stop: Requesting summary",{sessionId:t,workerPort:o,promptNumber:r,hasLastUserMessage:!!d,hasLastAssistantMessage:!!c});try{let u=await fetch(`http://127.0.0.1:${o}/sessions/${t}/summarize`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({prompt_number:r,last_user_message:d,last_assistant_message:c}),signal:AbortSignal.timeout(2e3)});if(!u.ok){let E=await u.text();throw g.failure("HOOK","Failed to generate summary",{sessionId:t,status:u.status},E),new Error(`Failed to request summary from worker: ${u.status} ${E}`)}g.debug("HOOK","Summary request sent successfully",{sessionId:t})}catch(u){throw u.cause?.code==="ECONNREFUSED"||u.name==="TimeoutError"||u.message.includes("fetch failed")?new Error("There's a problem with the worker. If you just updated, type `pm2 restart claude-mem-worker` in your terminal to continue"):u}finally{await fetch(`http://127.0.0.1:${o}/api/processing`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({isProcessing:!1})})}console.log(C("Stop",!0))}var L="";k.on("data",a=>L+=a);k.on("end",async()=>{let a=L?JSON.parse(L):void 0;await se(a)});
|
||||
`).get(n?.sdk_session_id);I("[summary-hook] Session diagnostics",{claudeSessionId:e,sessionDbId:t,sdkSessionId:n?.sdk_session_id,project:n?.project,promptNumber:r,observationCount:i?.count||0,transcriptPath:a.transcript_path}),s.close();let o=h(),d=oe(a.transcript_path||""),p=ie(a.transcript_path||"");I("[summary-hook] Extracted messages",{hasLastUserMessage:!!d,hasLastAssistantMessage:!!p,lastAssistantPreview:p.substring(0,200),lastAssistantLength:p.length}),g.dataIn("HOOK","Stop: Requesting summary",{sessionId:t,workerPort:o,promptNumber:r,hasLastUserMessage:!!d,hasLastAssistantMessage:!!p});try{let u=await fetch(`http://127.0.0.1:${o}/sessions/${t}/summarize`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({prompt_number:r,last_user_message:d,last_assistant_message:p}),signal:AbortSignal.timeout(2e3)});if(!u.ok){let E=await u.text();throw g.failure("HOOK","Failed to generate summary",{sessionId:t,status:u.status},E),new Error(`Failed to request summary from worker: ${u.status} ${E}`)}g.debug("HOOK","Summary request sent successfully",{sessionId:t})}catch(u){throw u.cause?.code==="ECONNREFUSED"||u.name==="TimeoutError"||u.message.includes("fetch failed")?new Error("There's a problem with the worker. If you just updated, type `pm2 restart claude-mem-worker` in your terminal to continue"):u}finally{await fetch(`http://127.0.0.1:${o}/api/processing`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({isProcessing:!1})})}console.log(D("Stop",!0))}var L="";w.on("data",a=>L+=a);w.on("end",async()=>{let a=L?JSON.parse(L):void 0;await ae(a)});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
import{execSync as u}from"child_process";import{join as r}from"path";import{homedir as s}from"os";import{existsSync as l}from"fs";import i from"path";import{homedir as a}from"os";import{existsSync as c,readFileSync as p}from"fs";function n(){try{let e=i.join(a(),".claude-mem","settings.json");if(c(e)){let t=JSON.parse(p(e,"utf-8")),o=parseInt(t.env?.CLAUDE_MEM_WORKER_PORT,10);if(!isNaN(o))return o}}catch{}return parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10)}var m=r(s(),".claude","plugins","marketplaces","thedotmack"),d=r(m,"node_modules");l(d)||(console.error(`
|
||||
import{execSync as _}from"child_process";import{join as i}from"path";import{homedir as p}from"os";import{existsSync as x}from"fs";import l from"path";import{homedir as f}from"os";import{existsSync as g,readFileSync as h}from"fs";import{join as t,dirname as m,basename as y}from"path";import{homedir as c}from"os";import{fileURLToPath as u}from"url";function d(){return typeof __dirname<"u"?__dirname:m(u(import.meta.url))}var A=d(),e=process.env.CLAUDE_MEM_DATA_DIR||t(c(),".claude-mem"),s=process.env.CLAUDE_CONFIG_DIR||t(c(),".claude"),P=t(e,"archives"),w=t(e,"logs"),C=t(e,"trash"),I=t(e,"backups"),b=t(e,"settings.json"),v=t(e,"claude-mem.db"),U=t(e,"vector-db"),M=t(s,"settings.json"),O=t(s,"commands"),L=t(s,"CLAUDE.md");function a(){try{let o=l.join(f(),".claude-mem","settings.json");if(g(o)){let n=JSON.parse(h(o,"utf-8")),r=parseInt(n.env?.CLAUDE_MEM_WORKER_PORT,10);if(!isNaN(r))return r}}catch{}return parseInt(process.env.CLAUDE_MEM_WORKER_PORT||"37777",10)}var D=i(p(),".claude","plugins","marketplaces","thedotmack"),k=i(D,"node_modules");x(k)||(console.error(`
|
||||
---
|
||||
\u{1F389} Note: This appears under Plugin Hook Error, but it's not an error. That's the only option for
|
||||
user messages in Claude Code UI until a better method is provided.
|
||||
@@ -17,15 +17,15 @@ Dependencies have been installed in the background. This only happens once.
|
||||
Thank you for installing Claude-Mem!
|
||||
|
||||
This message was not added to your startup context, so you can continue working as normal.
|
||||
`),process.exit(3));try{let e=r(s(),".claude","plugins","marketplaces","thedotmack","plugin","scripts","context-hook.js"),t=u(`node "${e}" --colors`,{encoding:"utf8"}),o=n();console.error(`
|
||||
`),process.exit(3));try{let o=i(p(),".claude","plugins","marketplaces","thedotmack","plugin","scripts","context-hook.js"),n=_(`node "${o}" --colors`,{encoding:"utf8"}),r=a();console.error(`
|
||||
|
||||
\u{1F4DD} Claude-Mem Context Loaded
|
||||
\u2139\uFE0F Note: This appears as stderr but is informational only
|
||||
|
||||
`+t+`
|
||||
`+n+`
|
||||
|
||||
\u{1F4AC} Feedback & Support
|
||||
https://github.com/thedotmack/claude-mem/discussions/110
|
||||
|
||||
\u{1F4FA} Watch live in browser http://localhost:${o}/
|
||||
`)}catch(e){console.error(`\u274C Failed to load context display: ${e}`)}process.exit(3);
|
||||
\u{1F4FA} Watch live in browser http://localhost:${r}/
|
||||
`)}catch(o){console.error(`\u274C Failed to load context display: ${o}`)}process.exit(3);
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,273 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Endless Mode Token Economics Calculator
|
||||
*
|
||||
* Simulates the recursive/cumulative token savings from Endless Mode by
|
||||
* "playing the tape through" with real observation data from SQLite.
|
||||
*
|
||||
* Key Insight:
|
||||
* - Discovery tokens are ALWAYS spent (creating observations)
|
||||
* - But Endless Mode feeds compressed observations as context instead of full tool outputs
|
||||
* - Savings compound recursively - each tool benefits from ALL previous compressions
|
||||
*/
|
||||
|
||||
const observationsData = [{"id":10136,"type":"decision","title":"Token Accounting Function for Recursive Continuation Pattern","discovery_tokens":4037,"created_at_epoch":1763360747429,"compressed_size":1613},
|
||||
{"id":10135,"type":"discovery","title":"Sequential Thinking Analysis of Token Economics Calculator","discovery_tokens":1439,"created_at_epoch":1763360651617,"compressed_size":1812},
|
||||
{"id":10134,"type":"discovery","title":"Recent Context Query Execution","discovery_tokens":1273,"created_at_epoch":1763360646273,"compressed_size":1228},
|
||||
{"id":10133,"type":"discovery","title":"Token Data Query Execution and Historical Context","discovery_tokens":11878,"created_at_epoch":1763360642485,"compressed_size":1924},
|
||||
{"id":10132,"type":"discovery","title":"Token Data Query and Script Validation Request","discovery_tokens":4167,"created_at_epoch":1763360628269,"compressed_size":903},
|
||||
{"id":10131,"type":"discovery","title":"Endless Mode Token Economics Analysis Output: Complete Infrastructure Impact","discovery_tokens":2458,"created_at_epoch":1763360553238,"compressed_size":2166},
|
||||
{"id":10130,"type":"change","title":"Integration of Actual Compute Savings Analysis into Main Execution Flow","discovery_tokens":11031,"created_at_epoch":1763360545347,"compressed_size":1032},
|
||||
{"id":10129,"type":"discovery","title":"Prompt Caching Economics: User Cost vs. Anthropic Compute Cost Divergence","discovery_tokens":20059,"created_at_epoch":1763360540854,"compressed_size":1802},
|
||||
{"id":10128,"type":"discovery","title":"Token Caching Cost Analysis Across AI Model Providers","discovery_tokens":3506,"created_at_epoch":1763360478133,"compressed_size":1245},
|
||||
{"id":10127,"type":"discovery","title":"Endless Mode Token Economics Calculator Successfully Integrated Prompt Caching Cost Model","discovery_tokens":3481,"created_at_epoch":1763360384055,"compressed_size":2444},
|
||||
{"id":10126,"type":"bugfix","title":"Fix Return Statement Variable Names in playTheTapeThrough Function","discovery_tokens":8326,"created_at_epoch":1763360374566,"compressed_size":1250},
|
||||
{"id":10125,"type":"change","title":"Redesign Timeline Display to Show Fresh/Cached Token Breakdown and Real Dollar Costs","discovery_tokens":12999,"created_at_epoch":1763360368843,"compressed_size":2004},
|
||||
{"id":10124,"type":"change","title":"Replace Estimated Cost Model with Actual Caching-Based Costs in Anthropic Scale Analysis","discovery_tokens":12867,"created_at_epoch":1763360361147,"compressed_size":2064},
|
||||
{"id":10123,"type":"change","title":"Pivot Session Length Comparison Table from Token to Cost Metrics","discovery_tokens":9746,"created_at_epoch":1763360352992,"compressed_size":1652},
|
||||
{"id":10122,"type":"change","title":"Add Dual Reporting: Token Count vs Actual Cost in Comparison Output","discovery_tokens":9602,"created_at_epoch":1763360346495,"compressed_size":1640},
|
||||
{"id":10121,"type":"change","title":"Apply Prompt Caching Cost Model to Endless Mode Calculation Function","discovery_tokens":9963,"created_at_epoch":1763360339238,"compressed_size":2003},
|
||||
{"id":10120,"type":"change","title":"Integrate Prompt Caching Cost Calculations into Without-Endless-Mode Function","discovery_tokens":8652,"created_at_epoch":1763360332046,"compressed_size":1701},
|
||||
{"id":10119,"type":"change","title":"Display Prompt Caching Pricing in Initial Calculator Output","discovery_tokens":6669,"created_at_epoch":1763360325882,"compressed_size":1188},
|
||||
{"id":10118,"type":"change","title":"Add Prompt Caching Pricing Model to Token Economics Calculator","discovery_tokens":10433,"created_at_epoch":1763360320552,"compressed_size":1264},
|
||||
{"id":10117,"type":"discovery","title":"Claude API Prompt Caching Cost Optimization Factor","discovery_tokens":3439,"created_at_epoch":1763360210175,"compressed_size":1142},
|
||||
{"id":10116,"type":"discovery","title":"Endless Mode Token Economics Verified at Scale","discovery_tokens":2855,"created_at_epoch":1763360144039,"compressed_size":2184},
|
||||
{"id":10115,"type":"feature","title":"Token Economics Calculator for Endless Mode Sessions","discovery_tokens":13468,"created_at_epoch":1763360134068,"compressed_size":1858},
|
||||
{"id":10114,"type":"decision","title":"Token Accounting for Recursive Session Continuations","discovery_tokens":3550,"created_at_epoch":1763360052317,"compressed_size":1478},
|
||||
{"id":10113,"type":"discovery","title":"Performance and Token Optimization Impact Analysis for Endless Mode","discovery_tokens":3464,"created_at_epoch":1763359862175,"compressed_size":1259},
|
||||
{"id":10112,"type":"change","title":"Endless Mode Blocking Hooks & Transcript Transformation Plan Document Created","discovery_tokens":17312,"created_at_epoch":1763359465307,"compressed_size":2181},
|
||||
{"id":10111,"type":"change","title":"Plan Document Creation for Morning Implementation","discovery_tokens":3652,"created_at_epoch":1763359347166,"compressed_size":843},
|
||||
{"id":10110,"type":"decision","title":"Blocking vs Non-Blocking Behavior by Mode","discovery_tokens":3652,"created_at_epoch":1763359347165,"compressed_size":797},
|
||||
{"id":10109,"type":"decision","title":"Tool Use and Observation Processing Architecture: Non-Blocking vs Blocking","discovery_tokens":3472,"created_at_epoch":1763359247045,"compressed_size":1349},
|
||||
{"id":10108,"type":"feature","title":"SessionManager.getMessageIterator implements event-driven async generator with graceful abort handling","discovery_tokens":2417,"created_at_epoch":1763359189299,"compressed_size":2016},
|
||||
{"id":10107,"type":"feature","title":"SessionManager implements event-driven session lifecycle with auto-initialization and zero-latency queue notifications","discovery_tokens":4734,"created_at_epoch":1763359165608,"compressed_size":2781},
|
||||
{"id":10106,"type":"discovery","title":"Two distinct uses of transcript data: live data flow vs session initialization","discovery_tokens":2933,"created_at_epoch":1763359156448,"compressed_size":2015},
|
||||
{"id":10105,"type":"discovery","title":"Transcript initialization pattern identified for compressed context on session resume","discovery_tokens":2933,"created_at_epoch":1763359156447,"compressed_size":2536},
|
||||
{"id":10104,"type":"feature","title":"SDKAgent implements event-driven message generator with continuation prompt logic and Endless Mode integration","discovery_tokens":6148,"created_at_epoch":1763359140399,"compressed_size":3241},
|
||||
{"id":10103,"type":"discovery","title":"Endless Mode architecture documented with phased implementation plan and context economics","discovery_tokens":5296,"created_at_epoch":1763359127954,"compressed_size":3145},
|
||||
{"id":10102,"type":"feature","title":"Save hook enhanced to extract and forward tool_use_id for Endless Mode linking","discovery_tokens":3294,"created_at_epoch":1763359115848,"compressed_size":2125},
|
||||
{"id":10101,"type":"feature","title":"TransformLayer implements Endless Mode context compression via observation substitution","discovery_tokens":4637,"created_at_epoch":1763359108317,"compressed_size":2629},
|
||||
{"id":10100,"type":"feature","title":"EndlessModeConfig implemented for loading Endless Mode settings from files and environment","discovery_tokens":2313,"created_at_epoch":1763359099972,"compressed_size":2125},
|
||||
{"id":10098,"type":"change","title":"User prompts wrapped with semantic XML structure in buildInitPrompt and buildContinuationPrompt","discovery_tokens":7806,"created_at_epoch":1763359091460,"compressed_size":1585},
|
||||
{"id":10099,"type":"discovery","title":"Session persistence mechanism relies on SDK internal state without context reload","discovery_tokens":7806,"created_at_epoch":1763359091460,"compressed_size":1883},
|
||||
{"id":10097,"type":"change","title":"Worker service session init now extracts userPrompt and promptNumber from request body","discovery_tokens":7806,"created_at_epoch":1763359091459,"compressed_size":1148},
|
||||
{"id":10096,"type":"feature","title":"SessionManager enhanced to accept dynamic userPrompt updates during multi-turn conversations","discovery_tokens":7806,"created_at_epoch":1763359091457,"compressed_size":1528},
|
||||
{"id":10095,"type":"discovery","title":"Five lifecycle hooks integrate claude-mem at critical session boundaries","discovery_tokens":6625,"created_at_epoch":1763359074808,"compressed_size":1570},
|
||||
{"id":10094,"type":"discovery","title":"PostToolUse hook is real-time observation creation point, not delayed processing","discovery_tokens":6625,"created_at_epoch":1763359074807,"compressed_size":2371},
|
||||
{"id":10093,"type":"discovery","title":"PostToolUse hook timing and compression integration options explored","discovery_tokens":1696,"created_at_epoch":1763359062088,"compressed_size":1605},
|
||||
{"id":10092,"type":"discovery","title":"Transcript transformation strategy for endless mode identified","discovery_tokens":6112,"created_at_epoch":1763359057563,"compressed_size":1968},
|
||||
{"id":10091,"type":"decision","title":"Finalized Transcript Compression Implementation Strategy","discovery_tokens":1419,"created_at_epoch":1763358943803,"compressed_size":1556},
|
||||
{"id":10090,"type":"discovery","title":"UserPromptSubmit Hook as Compression Integration Point","discovery_tokens":1546,"created_at_epoch":1763358931936,"compressed_size":1621},
|
||||
{"id":10089,"type":"decision","title":"Hypothesis 5 Selected: UserPromptSubmit Hook for Transcript Compression","discovery_tokens":1465,"created_at_epoch":1763358920209,"compressed_size":1918}];
|
||||
|
||||
// Estimate original tool output size from discovery tokens
|
||||
// Heuristic: discovery_tokens roughly correlates with original content size
|
||||
// Assumption: If it took 10k tokens to analyze, original was probably 15-30k tokens
|
||||
function estimateOriginalToolOutputSize(discoveryTokens) {
|
||||
// Conservative multiplier: 2x (original content was 2x the discovery cost)
|
||||
// This accounts for: reading the tool output + analyzing it + generating observation
|
||||
return discoveryTokens * 2;
|
||||
}
|
||||
|
||||
// Convert compressed_size (character count) to approximate token count
|
||||
// Rough heuristic: 1 token ≈ 4 characters for English text
|
||||
function charsToTokens(chars) {
|
||||
return Math.ceil(chars / 4);
|
||||
}
|
||||
|
||||
/**
|
||||
* Simulate session WITHOUT Endless Mode (current behavior)
|
||||
* Each continuation carries ALL previous full tool outputs in context
|
||||
*/
|
||||
function calculateWithoutEndlessMode(observations) {
|
||||
let cumulativeContextTokens = 0;
|
||||
let totalDiscoveryTokens = 0;
|
||||
let totalContinuationTokens = 0;
|
||||
const timeline = [];
|
||||
|
||||
observations.forEach((obs, index) => {
|
||||
const toolNumber = index + 1;
|
||||
const originalToolSize = estimateOriginalToolOutputSize(obs.discovery_tokens);
|
||||
|
||||
// Discovery cost (creating observation from full tool output)
|
||||
const discoveryCost = obs.discovery_tokens;
|
||||
totalDiscoveryTokens += discoveryCost;
|
||||
|
||||
// Continuation cost: Re-process ALL previous tool outputs + current one
|
||||
// This is the key recursive cost
|
||||
cumulativeContextTokens += originalToolSize;
|
||||
const continuationCost = cumulativeContextTokens;
|
||||
totalContinuationTokens += continuationCost;
|
||||
|
||||
timeline.push({
|
||||
tool: toolNumber,
|
||||
obsId: obs.id,
|
||||
title: obs.title.substring(0, 60),
|
||||
originalSize: originalToolSize,
|
||||
discoveryCost,
|
||||
contextSize: cumulativeContextTokens,
|
||||
continuationCost,
|
||||
totalCostSoFar: totalDiscoveryTokens + totalContinuationTokens
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
totalDiscoveryTokens,
|
||||
totalContinuationTokens,
|
||||
totalTokens: totalDiscoveryTokens + totalContinuationTokens,
|
||||
timeline
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Simulate session WITH Endless Mode
|
||||
* Each continuation carries ALL previous COMPRESSED observations in context
|
||||
*/
|
||||
function calculateWithEndlessMode(observations) {
|
||||
let cumulativeContextTokens = 0;
|
||||
let totalDiscoveryTokens = 0;
|
||||
let totalContinuationTokens = 0;
|
||||
const timeline = [];
|
||||
|
||||
observations.forEach((obs, index) => {
|
||||
const toolNumber = index + 1;
|
||||
const originalToolSize = estimateOriginalToolOutputSize(obs.discovery_tokens);
|
||||
const compressedSize = charsToTokens(obs.compressed_size);
|
||||
|
||||
// Discovery cost (same as without Endless Mode - still need to create observation)
|
||||
const discoveryCost = obs.discovery_tokens;
|
||||
totalDiscoveryTokens += discoveryCost;
|
||||
|
||||
// KEY DIFFERENCE: Add COMPRESSED size to context, not original size
|
||||
cumulativeContextTokens += compressedSize;
|
||||
const continuationCost = cumulativeContextTokens;
|
||||
totalContinuationTokens += continuationCost;
|
||||
|
||||
const compressionRatio = ((originalToolSize - compressedSize) / originalToolSize * 100).toFixed(1);
|
||||
|
||||
timeline.push({
|
||||
tool: toolNumber,
|
||||
obsId: obs.id,
|
||||
title: obs.title.substring(0, 60),
|
||||
originalSize: originalToolSize,
|
||||
compressedSize,
|
||||
compressionRatio: `${compressionRatio}%`,
|
||||
discoveryCost,
|
||||
contextSize: cumulativeContextTokens,
|
||||
continuationCost,
|
||||
totalCostSoFar: totalDiscoveryTokens + totalContinuationTokens
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
totalDiscoveryTokens,
|
||||
totalContinuationTokens,
|
||||
totalTokens: totalDiscoveryTokens + totalContinuationTokens,
|
||||
timeline
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Play the tape through - show token-by-token progression
|
||||
*/
|
||||
function playTheTapeThrough(observations) {
|
||||
console.log('\n' + '='.repeat(100));
|
||||
console.log('ENDLESS MODE TOKEN ECONOMICS CALCULATOR');
|
||||
console.log('Playing the tape through with REAL observation data');
|
||||
console.log('='.repeat(100) + '\n');
|
||||
|
||||
console.log(`📊 Dataset: ${observations.length} observations from live sessions\n`);
|
||||
|
||||
// Calculate both scenarios
|
||||
const without = calculateWithoutEndlessMode(observations);
|
||||
const withMode = calculateWithEndlessMode(observations);
|
||||
|
||||
// Show first 10 tools from each scenario side by side
|
||||
console.log('🎬 TAPE PLAYBACK: First 10 Tools\n');
|
||||
console.log('WITHOUT Endless Mode (Current) | WITH Endless Mode (Proposed)');
|
||||
console.log('-'.repeat(100));
|
||||
|
||||
for (let i = 0; i < Math.min(10, observations.length); i++) {
|
||||
const w = without.timeline[i];
|
||||
const e = withMode.timeline[i];
|
||||
|
||||
console.log(`\nTool #${w.tool}: ${w.title}`);
|
||||
console.log(` Original: ${w.originalSize.toLocaleString()}t | Compressed: ${e.compressedSize.toLocaleString()}t (${e.compressionRatio} saved)`);
|
||||
console.log(` Context: ${w.contextSize.toLocaleString()}t | Context: ${e.contextSize.toLocaleString()}t`);
|
||||
console.log(` Total: ${w.totalCostSoFar.toLocaleString()}t | Total: ${e.totalCostSoFar.toLocaleString()}t`);
|
||||
}
|
||||
|
||||
// Summary table
|
||||
console.log('\n' + '='.repeat(100));
|
||||
console.log('📈 FINAL TOTALS\n');
|
||||
|
||||
console.log('WITHOUT Endless Mode (Current):');
|
||||
console.log(` Discovery tokens: ${without.totalDiscoveryTokens.toLocaleString()}t (creating observations)`);
|
||||
console.log(` Continuation tokens: ${without.totalContinuationTokens.toLocaleString()}t (context accumulation)`);
|
||||
console.log(` TOTAL TOKENS: ${without.totalTokens.toLocaleString()}t`);
|
||||
|
||||
console.log('\nWITH Endless Mode:');
|
||||
console.log(` Discovery tokens: ${withMode.totalDiscoveryTokens.toLocaleString()}t (same - still create observations)`);
|
||||
console.log(` Continuation tokens: ${withMode.totalContinuationTokens.toLocaleString()}t (COMPRESSED context)`);
|
||||
console.log(` TOTAL TOKENS: ${withMode.totalTokens.toLocaleString()}t`);
|
||||
|
||||
const tokensSaved = without.totalTokens - withMode.totalTokens;
|
||||
const percentSaved = (tokensSaved / without.totalTokens * 100).toFixed(1);
|
||||
|
||||
console.log('\n💰 SAVINGS:');
|
||||
console.log(` Tokens saved: ${tokensSaved.toLocaleString()}t`);
|
||||
console.log(` Percentage saved: ${percentSaved}%`);
|
||||
console.log(` Efficiency gain: ${(without.totalTokens / withMode.totalTokens).toFixed(2)}x`);
|
||||
|
||||
// Anthropic scale calculation
|
||||
console.log('\n' + '='.repeat(100));
|
||||
console.log('🌍 ANTHROPIC SCALE IMPACT\n');
|
||||
|
||||
// Conservative assumptions
|
||||
const activeUsers = 100000; // Claude Code users
|
||||
const sessionsPerWeek = 10; // Per user
|
||||
const toolsPerSession = observations.length; // Use our actual data
|
||||
const weeklyToolUses = activeUsers * sessionsPerWeek * toolsPerSession;
|
||||
|
||||
const avgTokensPerToolWithout = without.totalTokens / observations.length;
|
||||
const avgTokensPerToolWith = withMode.totalTokens / observations.length;
|
||||
|
||||
const weeklyTokensWithout = weeklyToolUses * avgTokensPerToolWithout;
|
||||
const weeklyTokensWith = weeklyToolUses * avgTokensPerToolWith;
|
||||
const weeklyTokensSaved = weeklyTokensWithout - weeklyTokensWith;
|
||||
|
||||
console.log('Assumptions:');
|
||||
console.log(` Active Claude Code users: ${activeUsers.toLocaleString()}`);
|
||||
console.log(` Sessions per user/week: ${sessionsPerWeek}`);
|
||||
console.log(` Tools per session: ${toolsPerSession}`);
|
||||
console.log(` Weekly tool uses: ${weeklyToolUses.toLocaleString()}`);
|
||||
|
||||
console.log('\nWeekly Compute:');
|
||||
console.log(` Without Endless Mode: ${(weeklyTokensWithout / 1e9).toFixed(2)} billion tokens`);
|
||||
console.log(` With Endless Mode: ${(weeklyTokensWith / 1e9).toFixed(2)} billion tokens`);
|
||||
console.log(` Weekly savings: ${(weeklyTokensSaved / 1e9).toFixed(2)} billion tokens (${percentSaved}%)`);
|
||||
|
||||
const annualTokensSaved = weeklyTokensSaved * 52;
|
||||
console.log(` Annual savings: ${(annualTokensSaved / 1e12).toFixed(2)} TRILLION tokens`);
|
||||
|
||||
console.log('\n💡 What this means:');
|
||||
console.log(` • ${percentSaved}% reduction in Claude Code inference costs`);
|
||||
console.log(` • ${(without.totalTokens / withMode.totalTokens).toFixed(1)}x more users served with same infrastructure`);
|
||||
console.log(` • Massive energy/compute savings at scale`);
|
||||
console.log(` • Longer sessions = better UX without economic penalty`);
|
||||
|
||||
console.log('\n' + '='.repeat(100) + '\n');
|
||||
|
||||
return {
|
||||
without,
|
||||
withMode,
|
||||
tokensSaved,
|
||||
percentSaved,
|
||||
weeklyTokensSaved,
|
||||
annualTokensSaved
|
||||
};
|
||||
}
|
||||
|
||||
// Run the calculation
|
||||
playTheTapeThrough(observationsData);
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import path from "path";
|
||||
import { homedir } from "os";
|
||||
import { existsSync, readFileSync } from "fs";
|
||||
import { execSync } from "child_process";
|
||||
import { getPackageRoot } from "./paths.js";
|
||||
|
||||
// Named constants for health checks
|
||||
const HEALTH_CHECK_TIMEOUT_MS = 100;
|
||||
const WORKER_STARTUP_WAIT_MS = 500;
|
||||
const WORKER_STARTUP_RETRIES = 10;
|
||||
|
||||
/**
|
||||
* Get the worker port number
|
||||
@@ -38,20 +42,61 @@ async function isWorkerHealthy(): Promise<boolean> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the worker using PM2
|
||||
*/
|
||||
async function startWorker(): Promise<boolean> {
|
||||
try {
|
||||
// Find the ecosystem config file (built version in plugin/)
|
||||
const pluginRoot = getPackageRoot();
|
||||
const ecosystemPath = path.join(pluginRoot, 'ecosystem.config.cjs');
|
||||
|
||||
if (!existsSync(ecosystemPath)) {
|
||||
throw new Error(`Ecosystem config not found at ${ecosystemPath}`);
|
||||
}
|
||||
|
||||
// Start using PM2 with the ecosystem config
|
||||
// CRITICAL: Must set cwd to pluginRoot so PM2 starts from marketplace directory
|
||||
execSync(`pm2 start "${ecosystemPath}"`, {
|
||||
cwd: pluginRoot,
|
||||
stdio: 'pipe',
|
||||
encoding: 'utf-8'
|
||||
});
|
||||
|
||||
// Wait for worker to become healthy
|
||||
for (let i = 0; i < WORKER_STARTUP_RETRIES; i++) {
|
||||
await new Promise(resolve => setTimeout(resolve, WORKER_STARTUP_WAIT_MS));
|
||||
if (await isWorkerHealthy()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (error) {
|
||||
// Failed to start worker
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure worker service is running
|
||||
* Checks health and fails with instructions if not healthy
|
||||
* PM2's watch mode handles auto-restarts automatically
|
||||
* Checks health and auto-starts if not running
|
||||
*/
|
||||
export async function ensureWorkerRunning(): Promise<void> {
|
||||
// Check if already healthy
|
||||
if (await isWorkerHealthy()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const port = getWorkerPort();
|
||||
throw new Error(
|
||||
`Worker service is not responding on port ${port}.\n\n` +
|
||||
`If you just updated the plugin, PM2's watch mode should restart automatically.\n` +
|
||||
`If the problem persists, run: pm2 restart claude-mem-worker`
|
||||
);
|
||||
// Try to start the worker
|
||||
const started = await startWorker();
|
||||
|
||||
if (!started) {
|
||||
const port = getWorkerPort();
|
||||
throw new Error(
|
||||
`Worker service failed to start on port ${port}.\n\n` +
|
||||
`Try manually running: pm2 start ecosystem.config.cjs\n` +
|
||||
`Or restart: pm2 restart claude-mem-worker`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user