diff --git a/docs/context/CHROMA.md b/docs/context/CHROMA.md
deleted file mode 100644
index 950e36ad..00000000
--- a/docs/context/CHROMA.md
+++ /dev/null
@@ -1,542 +0,0 @@
-# Chroma Vector Database - Hybrid Semantic Search
-
-## Overview
-
-Claude-Mem v5.0.0 introduced **Chroma**, a vector database that enables semantic search across your memory stream. Combined with SQLite's FTS5 keyword search, this creates a powerful **hybrid search architecture** that finds contextually relevant observations using both meaning and keywords.
-
-**Key Benefits:**
-- 🧠 **Semantic Search** - Find observations by meaning, not just keywords
-- 🔍 **Hybrid Architecture** - Combines semantic similarity with keyword matching
-- ⏱️ **Recency Filtering** - Focus on recent 90 days for relevant context
-- ⚡ **Fast Performance** - Semantic search under 200ms with 8,000+ documents
-- 🔄 **Auto-Sync** - ChromaSync service keeps vectors updated automatically
-
-## What is Chroma?
-
-[ChromaDB](https://www.trychroma.com/) is an open-source vector database designed for AI applications. It stores text as **vector embeddings** - mathematical representations that capture semantic meaning.
-
-**Example:**
-```
-Query: "authentication bug"
-Keyword Match: Must contain both "authentication" AND "bug"
-Semantic Match: Also finds "login error", "auth failure", "sign-in issue"
-```
-
-Semantic search understands that "authentication bug" is conceptually similar to "login error" even though they share no keywords.
-
-## Architecture
-
-### Hybrid Search Flow
-
-```
-┌──────────────────────────────────────────────────────────────┐
-│ User Query: "How does authentication work?" │
-└──────────────────────────────────────────────────────────────┘
- ↓
- ┌─────────────────┴─────────────────┐
- ↓ ↓
-┌──────────────────────┐ ┌──────────────────────┐
-│ Chroma Semantic │ │ SQLite FTS5 │
-│ Vector Similarity │ │ Keyword Search │
-│ │ │ │
-│ Finds conceptually │ │ Finds exact/fuzzy │
-│ similar observations │ │ keyword matches │
-└──────────────────────┘ └──────────────────────┘
- ↓ ↓
- └─────────────────┬─────────────────┘
- ↓
- ┌─────────────────────────────────┐
- │ Merge Results │
- │ - Deduplicate by ID │
- │ - Sort by relevance + recency │
- │ - Filter by 90-day window │
- └─────────────────────────────────┘
- ↓
- ┌─────────────────────────────────┐
- │ Return Top Matches │
- │ Semantic + Keyword combined │
- └─────────────────────────────────┘
-```
-
-### ChromaSync Service
-
-The **ChromaSync** service (`src/services/sync/ChromaSync.ts`) automatically synchronizes observations to Chroma:
-
-**When Observations Are Synced:**
-1. **Session Summary** - After each session completes, all new observations synced
-2. **Worker Startup** - On initialization, checks for unsynced observations
-3. **Manual Trigger** - Can force sync via internal API (development only)
-
-**What Gets Embedded:**
-- Observation ID (unique identifier)
-- Title (compressed learning statement)
-- Narrative (detailed explanation)
-- Project path (for project-specific filtering)
-- Timestamp (for recency filtering)
-- Concepts (semantic tags)
-- File references (associated code files)
-
-**Embedding Model:**
-- Currently using Chroma's default embedding function
-- Future: Configurable embedding models (e.g., OpenAI, sentence-transformers)
-
-### Data Structure
-
-**SQLite (Source of Truth):**
-```sql
-CREATE TABLE observations (
- id INTEGER PRIMARY KEY,
- title TEXT,
- narrative TEXT,
- facts TEXT,
- concepts TEXT,
- files TEXT,
- type TEXT,
- projectPath TEXT,
- createdAt INTEGER
-);
-```
-
-**Chroma (Vector Embeddings):**
-```typescript
-{
- ids: ["obs_12345"],
- embeddings: [[0.123, -0.456, ...]], // 384-dimensional vector
- documents: ["Title: Authentication flow\nNarrative: Implemented..."],
- metadatas: [{
- type: "feature",
- project: "claude-mem",
- timestamp: 1698765432000,
- concepts: "pattern,architecture"
- }]
-}
-```
-
-## How Semantic Search Works
-
-### Vector Embeddings
-
-Text converted to high-dimensional vectors that capture meaning:
-
-```
-"user authentication" → [0.12, -0.34, 0.56, ..., 0.78]
-"login system" → [0.15, -0.32, 0.54, ..., 0.81]
-"database schema" → [-0.45, 0.67, -0.23, ..., 0.12]
-```
-
-Notice: "user authentication" and "login system" have similar vectors (close in vector space), while "database schema" is distant.
-
-### Similarity Search
-
-Chroma uses **cosine similarity** to find nearest neighbors:
-
-```typescript
-// Query embedding
-query: "authentication bug"
-query_vector: [0.14, -0.33, 0.55, ..., 0.79]
-
-// Find observations with similar vectors
-results = chroma.query(
- query_vector,
- n_results: 10,
- where: { timestamp: { $gte: now - 90_days } }
-)
-```
-
-**Result Ranking:**
-- Higher cosine similarity = more semantically similar
-- Filtered by 90-day recency window
-- Combined with keyword matches from FTS5
-
-## 90-Day Recency Filtering
-
-Why 90 days?
-
-**Rationale:**
-- Recent context more likely relevant to current work
-- Prevents very old observations from diluting results
-- Balances completeness with relevance
-- Reduces vector search space for faster queries
-
-**Implementation:**
-```typescript
-const ninetyDaysAgo = Date.now() - (90 * 24 * 60 * 60 * 1000);
-
-// Chroma metadata filter
-where: {
- timestamp: { $gte: ninetyDaysAgo }
-}
-
-// SQLite WHERE clause
-WHERE createdAt >= ?
-```
-
-**Configurable?**
-- Not currently user-configurable
-- Hard-coded in `src/servers/search-server.ts`
-- Future: Add `CLAUDE_MEM_RECENCY_DAYS` environment variable
-
-## MCP Tool Integration
-
-All 9 MCP search tools benefit from hybrid search:
-
-### search_observations (Hybrid)
-
-```typescript
-// Keyword-only (v4.x)
-search_observations(query: "authentication")
-// Returns: Observations containing "authentication"
-
-// Hybrid semantic + keyword (v5.x)
-search_observations(query: "authentication")
-// Returns: Observations with "authentication" PLUS semantically similar:
-// - "login system"
-// - "user credentials"
-// - "session management"
-```
-
-### get_timeline_by_query (Semantic-First)
-
-```typescript
-// Uses Chroma to find best match, then builds timeline
-get_timeline_by_query(
- query: "when did we implement the viewer UI?",
- mode: "auto",
- depth_before: 10,
- depth_after: 10
-)
-
-// Chroma finds: Observation #4057 "Web-Based Viewer UI for Real-Time Memory Stream"
-// Returns: Timeline with 10 observations before + anchor + 10 after
-```
-
-### Benefits Across All Tools
-
-- **find_by_concept**: Semantic similarity finds related concepts
-- **find_by_file**: Finds semantically similar code changes
-- **find_by_type**: Better relevance ranking within type
-- **get_recent_context**: Prioritizes semantically relevant recent context
-
-## Performance
-
-### Benchmarks (8,279 vector documents)
-
-| Operation | Time | Notes |
-|-----------|------|-------|
-| **Semantic Query** | 150-200ms | 90-day window, top 10 results |
-| **Keyword Query (FTS5)** | 5-10ms | Full-text search |
-| **Hybrid Query** | 160-220ms | Combined semantic + keyword |
-| **Initial Sync** | 2-5 min | First-time embedding of all observations |
-| **Incremental Sync** | 100-500ms | 1-10 new observations per session |
-
-### Memory Usage
-
-- **Chroma DB Size**: ~50MB for 8,000 observations
-- **Embeddings**: 384 dimensions × 4 bytes = 1.5KB per observation
-- **Metadata**: ~500 bytes per observation (project, type, timestamp)
-- **Total**: ~2KB per observation in Chroma
-
-### Optimization Tips
-
-1. **Reduce vector dimensions**: Use smaller embedding models (future)
-2. **Adjust recency window**: Narrow to 30/60 days for faster queries
-3. **Limit result count**: Request fewer results (n_results=5 vs 10)
-4. **Project filtering**: Add project filter to metadata query
-
-## Installation & Dependencies
-
-### Python Requirement
-
-Chroma requires Python 3.7+ installed:
-
-**Check Python:**
-```bash
-python3 --version
-# Should show: Python 3.7.x or higher
-```
-
-**Install Python (if needed):**
-- **macOS**: `brew install python3`
-- **Windows**: Download from [python.org](https://www.python.org/downloads/)
-- **Linux**: `apt-get install python3` or `yum install python3`
-
-### ChromaDB Installation
-
-Chroma installed automatically as npm dependency:
-
-```bash
-npm install
-# Installs: chromadb (Python package via node-gyp bindings)
-```
-
-**Manual Installation (if auto-install fails):**
-```bash
-pip3 install chromadb
-```
-
-### Troubleshooting Installation
-
-**Error: "Python not found"**
-```bash
-# Set Python path explicitly
-export PYTHON=/usr/local/bin/python3
-npm install
-```
-
-**Error: "chromadb module not found"**
-```bash
-# Reinstall chromadb
-pip3 install --upgrade chromadb
-
-# Verify installation
-python3 -c "import chromadb; print(chromadb.__version__)"
-```
-
-**Error: "node-gyp build failed"**
-```bash
-# Install build tools
-# macOS: xcode-select --install
-# Windows: npm install --global windows-build-tools
-# Linux: apt-get install build-essential
-```
-
-## Configuration
-
-### Environment Variables
-
-Currently no user-configurable settings. Future options:
-
-```json
-// Proposed for future versions
-{
- "env": {
- "CLAUDE_MEM_CHROMA_ENABLED": "true", // Enable/disable Chroma
- "CLAUDE_MEM_CHROMA_PATH": "~/.claude-mem/chroma", // DB location
- "CLAUDE_MEM_EMBEDDING_MODEL": "default", // Embedding model choice
- "CLAUDE_MEM_RECENCY_DAYS": "90", // Recency window
- "CLAUDE_MEM_VECTOR_DIM": "384" // Embedding dimensions
- }
-}
-```
-
-### Disabling Chroma (Future)
-
-To disable semantic search and use keyword-only:
-
-```json
-{
- "env": {
- "CLAUDE_MEM_CHROMA_ENABLED": "false"
- }
-}
-```
-
-Falls back to SQLite FTS5 keyword search only.
-
-## Database Maintenance
-
-### Location
-
-```
-~/.claude-mem/chroma/
-├── chroma.sqlite3 # Chroma metadata database
-└── index/ # Vector index files
- └── *.bin # Binary vector data
-```
-
-### Backup
-
-```bash
-# Backup entire Chroma directory
-cp -r ~/.claude-mem/chroma ~/.claude-mem/chroma.backup
-
-# Restore from backup
-rm -rf ~/.claude-mem/chroma
-cp -r ~/.claude-mem/chroma.backup ~/.claude-mem/chroma
-```
-
-### Reset Chroma (Force Resync)
-
-```bash
-# Delete Chroma database
-rm -rf ~/.claude-mem/chroma
-
-# Restart worker to trigger full resync
-npm run worker:restart
-
-# Check logs for sync progress
-npm run worker:logs
-```
-
-**Note**: Resync can take 2-5 minutes for thousands of observations.
-
-### Disk Space Management
-
-**Chroma grows with observations:**
-- 1,000 observations ≈ 5MB
-- 10,000 observations ≈ 50MB
-- 100,000 observations ≈ 500MB
-
-**Cleanup old observations:**
-```sql
--- Delete observations older than 1 year
--- This will trigger Chroma resync on next startup
-sqlite3 ~/.claude-mem/claude-mem.db \
- "DELETE FROM observations WHERE createdAt < strftime('%s', 'now', '-1 year') * 1000;"
-```
-
-## Advanced Usage
-
-### Direct Chroma Queries (Development)
-
-For debugging or custom queries:
-
-```typescript
-import { ChromaSync } from './services/sync/ChromaSync';
-
-const sync = new ChromaSync();
-await sync.initialize();
-
-// Query Chroma directly
-const results = await sync.query({
- queryTexts: ["authentication implementation"],
- nResults: 10,
- where: {
- type: "feature",
- timestamp: { $gte: Date.now() - 90_days }
- }
-});
-
-console.log(results.ids, results.distances, results.documents);
-```
-
-### Custom Embedding Models (Future)
-
-Chroma supports multiple embedding models:
-
-```typescript
-// Future configuration
-const sync = new ChromaSync({
- embeddingModel: "sentence-transformers/all-MiniLM-L6-v2", // Smaller, faster
- // or: "text-embedding-ada-002" (OpenAI, requires API key)
- // or: "all-mpnet-base-v2" (Higher quality, slower)
-});
-```
-
-### Metadata Filtering
-
-Chroma supports advanced metadata queries:
-
-```typescript
-// Find observations by type and project
-results = await sync.query({
- queryTexts: ["API design"],
- where: {
- $and: [
- { type: { $in: ["decision", "feature"] } },
- { project: "claude-mem" }
- ]
- }
-});
-
-// Find recent observations
-results = await sync.query({
- queryTexts: ["database schema"],
- where: {
- timestamp: { $gte: Date.now() - 30_days }
- }
-});
-```
-
-## Comparison: Semantic vs Keyword Search
-
-| Aspect | Semantic (Chroma) | Keyword (FTS5) |
-|--------|-------------------|----------------|
-| **Speed** | 150-200ms | 5-10ms |
-| **Accuracy** | High (meaning-based) | Medium (exact match) |
-| **Storage** | ~2KB per observation | ~500 bytes per observation |
-| **Conceptual Matching** | ✅ Yes | ❌ No |
-| **Exact Match** | ❌ Not guaranteed | ✅ Always |
-| **Typo Tolerance** | ✅ High | ⚠️ Limited (fuzzy) |
-| **Dependencies** | Python + chromadb | None (SQLite built-in) |
-| **Recency Bias** | ✅ Built-in (90 days) | Manual filtering |
-
-**Best Practice:** Use hybrid search (both) for optimal results.
-
-## Troubleshooting
-
-### "Chroma not found" Error
-
-**Symptom:** Worker logs show "Chroma not available, using keyword-only search"
-
-**Solution:**
-```bash
-# Check Python installation
-python3 --version
-
-# Reinstall chromadb
-pip3 install chromadb
-
-# Restart worker
-npm run worker:restart
-```
-
-### Slow Query Performance
-
-**Symptom:** Searches taking >1 second
-
-**Solutions:**
-1. Reduce recency window (edit `src/servers/search-server.ts`)
-2. Limit result count (`nResults: 5` instead of 10)
-3. Add project filter to narrow search space
-4. Check Chroma index size (may need rebuild)
-
-### Out of Memory Errors
-
-**Symptom:** Worker crashes with "JavaScript heap out of memory"
-
-**Solution:**
-```bash
-# Increase Node.js heap size
-export NODE_OPTIONS="--max-old-space-size=4096"
-
-# Restart worker
-npm run worker:restart
-```
-
-### Sync Taking Too Long
-
-**Symptom:** Initial Chroma sync takes >10 minutes
-
-**Possible Causes:**
-- Large number of observations (>10,000)
-- Slow embedding model
-- Limited CPU resources
-
-**Solutions:**
-1. Let it complete (one-time cost)
-2. Delete very old observations to reduce count
-3. Close resource-intensive apps during sync
-
-## Future Enhancements
-
-Potential improvements for future versions:
-
-- **Configurable Recency**: User-defined recency window (30/60/90/365 days)
-- **Custom Embeddings**: Choose embedding model (quality vs speed trade-off)
-- **Incremental Updates**: Update existing vectors instead of full resync
-- **Semantic Filters**: Search by semantic concept ("all architectural decisions")
-- **Multi-Language Support**: Embeddings optimized for non-English code/docs
-- **Clustering**: Auto-cluster related observations for discovery
-- **Visualization**: 2D/3D visualization of vector space (similar observations near each other)
-
-## Resources
-
-- **ChromaDB Documentation**: https://docs.trychroma.com/
-- **Source Code**: `src/services/sync/ChromaSync.ts`
-- **Search Server**: `src/servers/search-server.ts`
-- **Python Package**: https://pypi.org/project/chromadb/
-
----
-
-**Powered by ChromaDB** | **Hybrid Semantic + Keyword Search** | **90-Day Recency Window**
diff --git a/docs/context/CLAUDE.md b/docs/context/CLAUDE.md
deleted file mode 100644
index d3128e1b..00000000
--- a/docs/context/CLAUDE.md
+++ /dev/null
@@ -1,104 +0,0 @@
-# Claude-Mem Context Documentation
-
-## What This Folder Is
-
-This `docs/context/` folder contains **internal documentation** - planning documents, design references, audits, and work-in-progress materials that support development but are NOT user-facing.
-
-## Folder Structure
-
-```
-docs/
-├── public/ ← User-facing Mintlify docs (DO NOT put internal docs there)
-│ └── *.mdx - Official documentation
-└── context/ ← You are here (Internal documentation)
- ├── *.md - Planning docs, audits, references
- ├── *-plan.md - Implementation plans
- ├── *-audit.md - Code audits and reviews
- ├── agent-sdk-*.md - SDK reference materials
- └── subdirs/ - Organized by topic
-```
-
-## What Belongs Here
-
-**Internal Documentation** (`.md` format):
-- Planning documents (`*-plan.md`, `*-outline.md`)
-- Implementation analysis (`*-audit.md`, `*-code-reference.md`)
-- Error tracking (`typescript-errors.md`)
-- Design documents not ready for public docs
-- PR review responses
-- Reference materials (like `agent-sdk-ref.md`)
-- Work-in-progress documentation
-- Technical investigations and postmortems
-- Architecture analysis documents
-
-**Examples from this folder:**
-- `mem-search-technical-architecture.md` - Deep technical reference
-- `search-architecture-analysis.md` - Implementation analysis
-- `agent-sdk-ref.md` - SDK reference for developers
-- `typescript-errors.md` - Error tracking during development
-- `worker-service-architecture.md` - Internal architecture notes
-- `processing-indicator-audit.md` - Code audit document
-
-## What Does NOT Belong Here
-
-**User-Facing Documentation** goes in `/docs/public/`:
-- User guides and tutorials
-- Official architecture documentation
-- Installation instructions
-- Configuration guides
-- Best practices for users
-- Troubleshooting guides
-
-**Rule of Thumb:**
-- If a user would read it → `/docs/public/` (as `.mdx`)
-- If only developers/contributors need it → `/docs/context/` (as `.md`)
-
-## File Organization
-
-### By Type
-- `*-plan.md` - Implementation plans for features
-- `*-audit.md` - Code audits and reviews
-- `*-postmortem.md` - Analysis of issues or incidents
-- `*-reference.md` - Technical reference materials
-- `*-analysis.md` - Architecture or design analysis
-
-### By Topic
-- Create subdirectories for related documents
-- Example: `claude-code/` for Claude Code specific docs
-- Example: `architecture/` for internal architecture notes
-
-## Development Workflow
-
-### When to Create Context Docs
-
-1. **Planning Phase** - Before implementing a feature
- - Create `feature-name-plan.md`
- - Outline implementation steps
- - Document decisions and tradeoffs
-
-2. **During Development** - Track issues and decisions
- - Create `feature-name-audit.md` for code reviews
- - Update `typescript-errors.md` for build issues
- - Document gotchas in topic-specific files
-
-3. **After Implementation** - Preserve knowledge
- - Create `feature-name-postmortem.md` if issues occurred
- - Update architecture analysis documents
- - Archive plan docs (don't delete - useful for history)
-
-### Graduating to Public Docs
-
-When internal docs are polished enough for users:
-1. Convert `.md` to `.mdx` format
-2. Add Mintlify frontmatter
-3. Move to appropriate `/docs/public/` subdirectory
-4. Add to `docs.json` navigation
-5. Keep original in `/docs/context/` for reference
-
-## Summary
-
-**Simple Rule**:
-- `/docs/context/` = Internal docs, plans, references, audits ← YOU ARE HERE
-- `/docs/public/` = Official user documentation (Mintlify .mdx files)
-
-**Purpose**: This folder preserves development context, design decisions, and technical knowledge that helps contributors understand WHY things work the way they do, even if users don't need those details.
diff --git a/docs/context/CWD_CONTEXT_FIX.md b/docs/context/CWD_CONTEXT_FIX.md
deleted file mode 100644
index 24b5ddc5..00000000
--- a/docs/context/CWD_CONTEXT_FIX.md
+++ /dev/null
@@ -1,164 +0,0 @@
-# CWD Context Fix - Technical Documentation
-
-## Overview
-
-This fix adds working directory (CWD) context propagation through the entire claude-mem pipeline, enabling the SDK agent to have spatial awareness of which directory/repository it's observing.
-
-## Problem Statement
-
-Previously, the SDK agent would:
-- Search wrong repositories when analyzing file operations
-- Report "file not found" for files that actually exist
-- Lack context about which project was being worked on
-- Generate inaccurate observations due to spatial confusion
-
-## Solution
-
-The CWD information now flows through the entire system:
-
-```
-Hook Input (cwd) → Worker API (cwd) → SessionManager (cwd) → SDK Agent (tool_cwd)
-```
-
-## Data Flow
-
-### 1. Hook Layer (`save-hook.ts`)
-```typescript
-export interface PostToolUseInput {
- session_id: string;
- cwd: string; // ← Captured from Claude Code
- tool_name: string;
- tool_input: any;
- tool_response: any;
-}
-```
-
-The hook extracts `cwd` and includes it in the worker API request:
-```typescript
-body: JSON.stringify({
- tool_name,
- tool_input,
- tool_response,
- prompt_number,
- cwd: cwd || '' // ← Passed to worker
-})
-```
-
-### 2. Worker Service (`worker-service.ts`)
-```typescript
-const { tool_name, tool_input, tool_response, prompt_number, cwd } = req.body;
-
-this.sessionManager.queueObservation(sessionDbId, {
- tool_name,
- tool_input,
- tool_response,
- prompt_number,
- cwd // ← Forwarded to queue
-});
-```
-
-### 3. Session Manager (`SessionManager.ts`)
-```typescript
-session.pendingMessages.push({
- type: 'observation',
- tool_name: data.tool_name,
- tool_input: data.tool_input,
- tool_response: data.tool_response,
- prompt_number: data.prompt_number,
- cwd: data.cwd // ← Included in message queue
-});
-```
-
-### 4. SDK Agent (`SDKAgent.ts`)
-```typescript
-content: buildObservationPrompt({
- id: 0,
- tool_name: message.tool_name!,
- tool_input: JSON.stringify(message.tool_input),
- tool_output: JSON.stringify(message.tool_response),
- created_at_epoch: Date.now(),
- cwd: message.cwd // ← Passed to prompt builder
-})
-```
-
-### 5. Prompt Generation (`prompts.ts`)
-```typescript
-return `
- ${obs.tool_name}
- ${new Date(obs.created_at_epoch).toISOString()}${obs.cwd ? `
- ${obs.cwd}` : ''} // ← Included in XML
- ${JSON.stringify(toolInput, null, 2)}
- ${JSON.stringify(toolOutput, null, 2)}
-`;
-```
-
-## SDK Agent Prompt Changes
-
-The init prompt now includes a "SPATIAL AWARENESS" section:
-
-```
-SPATIAL AWARENESS: Tool executions include the working directory (tool_cwd) to help you understand:
-- Which repository/project is being worked on
-- Where files are located relative to the project root
-- How to match requested paths to actual execution paths
-```
-
-## Example Usage
-
-When a user executes a read operation in `/home/user/my-project`:
-
-```xml
-
- ReadTool
- 2025-11-10T19:18:03.065Z
- /home/user/my-project
-
- {
- "path": "src/index.ts"
- }
-
-
- {
- "content": "export default..."
- }
-
-
-```
-
-The SDK agent now knows:
-1. The operation happened in `/home/user/my-project`
-2. The file `src/index.ts` is relative to that directory
-3. Which repository context to search when generating observations
-
-## Testing
-
-8 comprehensive tests validate the CWD propagation:
-
-```bash
-npx tsx --test tests/cwd-propagation.test.ts
-```
-
-All tests verify:
-- Type interfaces include `cwd` fields
-- Hook extracts and passes `cwd`
-- Worker accepts and forwards `cwd`
-- SDK agent includes `cwd` in prompts
-- End-to-end flow is correct
-
-## Benefits
-
-1. **Spatial Awareness**: SDK agent knows which directory/repository it's observing
-2. **Accurate Path Matching**: Can verify if requested paths match executed paths
-3. **Better Summaries**: Won't search wrong repositories or report false negatives
-4. **Works with All Models**: Even Haiku benefits from correct context (no need for Opus workaround)
-
-## Backward Compatibility
-
-- `cwd` is optional in all interfaces (`cwd?: string`)
-- Missing `cwd` values are handled gracefully (defaults to empty string)
-- Existing observations without `cwd` continue to work
-- No database migration required (CWD is transient, not persisted)
-
-## Related Issues
-
-Fixes issue #73 (CWD context missing from SDK agent)
diff --git a/docs/context/PR_REVIEW_RESPONSE.md b/docs/context/PR_REVIEW_RESPONSE.md
deleted file mode 100644
index 38c7c105..00000000
--- a/docs/context/PR_REVIEW_RESPONSE.md
+++ /dev/null
@@ -1,219 +0,0 @@
-# Response to PR Review #47
-
-## Executive Summary
-
-Thank you for the thorough review. Most of the "issues" identified are actually **intentional architectural decisions** made to solve production failures. The comprehensive analysis docs (JUST-FUCKING-RUN-IT.md, LINE-BY-LINE-CASCADING-BULLSHIT.md) document why these changes were necessary.
-
-However, you've identified **2 legitimate issues** that need fixing:
-1. ✅ **Race condition in worker startup** - Valid concern, needs fixing
-2. ✅ **Watch mode in production** - Appears to be unintentional leftover from development
-
-The other concerns are **working as intended** based on documented architectural decisions.
-
----
-
-## Detailed Response to Each Concern
-
-### ⚠️ Issue #1: Race Condition in Worker Health Check - **VALID CONCERN**
-
-**Review Comment**: "The spawn() call inside the close event handler is non-blocking, but the function returns immediately. Hooks may attempt HTTP requests before worker has started."
-
-**Our Response**: **You're absolutely right**. This is a legitimate race condition we need to fix.
-
-**However**, the suggested fixes (async/await health check, retry loops) are exactly what we intentionally removed because they were causing production failures (see Observation #3602, #3600).
-
-**Proposed Solution**:
-The hooks already have proper error handling for `ECONNREFUSED` with actionable user messages:
-```typescript
-if (error.cause?.code === 'ECONNREFUSED' || error.name === 'TimeoutError' || error.message.includes('fetch failed')) {
- throw new Error("There's a problem with the worker. If you just updated, type `pm2 restart claude-mem-worker` in your terminal to continue");
-}
-```
-
-We should either:
-1. Document this as expected behavior (fire-and-forget spawn)
-2. Add a single synchronous `pm2 list` check after spawn to verify startup
-3. Keep the current approach and rely on hook error messages
-
-**We will NOT re-add**: Retry loops, health check polling, or arbitrary delays. Those caused the 100% failure rate we just fixed.
-
----
-
-### ⚠️ Issue #2: Removed Health Endpoint Information - **INTENTIONAL**
-
-**Review Comment**: "This removes useful debugging information. When troubleshooting production issues, knowing the PID, active sessions count, and port would be valuable."
-
-**Our Documentation**:
-- **Observation #3616**: "Simplified Health Check Endpoint to Minimal Response"
-- **Observation #3601**: "Minimum Parameters = Minimum Bugs"
-- **Observation #3600**: "Comprehensive Analysis of Cascading Architectural Problems"
-
-**Why We Did This**:
-1. **HTTP 200 = Alive**: If the endpoint responds, the worker is healthy. Period.
-2. **Diagnostic fields provided no actionable value**: PID, activeSessions, chromaSynced didn't help debug the actual production failures
-3. **Part of 87% code reduction**: worker-utils.ts went from 113 lines → 15 lines
-4. **Health checks were hiding real problems**: Retry logic masked that startup sequence was broken
-
-**Original Problem**:
-- Worker startup: 4-5 seconds (actual)
-- Health check timeout: 3 seconds (configured)
-- Result: **100% user failure rate**
-
-The detailed health response didn't help diagnose this - fixing the startup sequence (HTTP server first) did.
-
-**Response**: **Will not change**. The health endpoint serves one purpose: availability signal. Use PM2 commands for diagnostics:
-- `pm2 list` - See PID, status, memory
-- `pm2 logs claude-mem-worker` - See application logs
-- `npm run worker:logs` - Convenience wrapper
-
----
-
-### ⚠️ Issue #3: Auto-Session Creation Without Validation - **NEEDS FIXING**
-
-**Review Comment**: "Uses non-null assertion (dbSession!) without checking if dbSession is actually null. If getSessionById() returns null, this will throw at runtime."
-
-**Our Response**: **You're absolutely right**. This is a legitimate bug.
-
-**Action Required**: Add null checks to `handleObservation` and `handleSummarize` like already exist in `handleInit`:
-```typescript
-const dbSession = db.getSessionById(sessionDbId);
-if (!dbSession) {
- db.close();
- res.status(404).json({ error: 'Session not found in database' });
- return;
-}
-```
-
-**This needs to be fixed before merge.**
-
----
-
-### ⚠️ Issue #4: Removed Observation Counter - **INTENTIONAL**
-
-**Review Comment**: "Was this used for generating correlation IDs for logging? If so, is there now no way to correlate observations within a session for debugging?"
-
-**Our Documentation**:
-- **Observation #3621-3627**: Complete removal of observation counter and correlation IDs
-- **Observation #3602**: "Architectural Decision: Remove Health Checks and Arbitrary Delays"
-- **Observation #3612**: "Worker Service Simplification Strategy"
-
-**Why We Removed It**:
-1. **Over-engineering**: Provided per-observation tracking when session-level identification was sufficient
-2. **Part of cascading complexity**: Correlation IDs were monitoring infrastructure for complexity that shouldn't exist
-3. **Session-level debugging is sufficient**: Most issues diagnosed by knowing which session, not which observation #5 within that session
-4. **Database IDs provide uniqueness**: Once stored, observations have DB IDs for precise identification
-
-**The Problem It Was Solving (That No Longer Needs Solving)**:
-- Tracking individual observations through worker pipeline
-- Monitoring Chroma sync success/failure per observation
-- Detailed per-observation timing metrics
-
-**Why That's Unnecessary**:
-- Session-level logging is sufficient for debugging
-- Database IDs provide uniqueness after storage
-- The monitoring was masking real problems (startup sequence)
-
-**Response**: **Will not change**. This was part of the simplification strategy that fixed production failures.
-
----
-
-### ⚠️ Issue #5: PM2 Watch Mode in Production - **VALID CONCERN**
-
-**Review Comment**: "Watch mode causes PM2 to restart the process whenever files change. This is useful during development but potentially problematic in production."
-
-**Our Investigation**:
-- **Observation #3631**: Documents what watch mode does, but **no observation documents WHY we enabled it**
-- **Observation #3611**: PM2 config was "drastically simplified" by removing 21 unnecessary parameters
-- **Watch mode was kept** during this aggressive simplification
-
-**Conclusion**: **This appears to be unintentional** - likely enabled for development and inadvertently left enabled.
-
-**Action Required**: Either:
-1. **Disable watch mode** (recommended) - Users aren't developing, they're using the plugin
-2. **Document it as intentional** if there's a reason we want auto-restart on file changes
-
-**This should be addressed before merge** - likely by disabling watch mode.
-
----
-
-### ⚠️ Issue #6: Duplicate Port Constant - **ACKNOWLEDGED**
-
-**Review Comment**: "FIXED_PORT constant is defined in 5 places. Creates maintenance burden."
-
-**Our Response**: **Fair point**. This is technical debt we can clean up.
-
-**However**, it's low priority because:
-- Port is unlikely to change
-- All values are currently consistent
-- Not causing production issues
-
-**Action**: Add to backlog for post-merge cleanup. Export from worker-utils.ts and import elsewhere.
-
----
-
-## Summary of Actions
-
-### Must Fix Before Merge:
-1. ✅ **Add null checks to auto-session creation** in handleObservation and handleSummarize
-2. ✅ **Decide on watch mode** - Disable unless there's documented reason to keep it
-
-### Will Not Change (Intentional Decisions):
-1. ❌ **Health endpoint simplification** - Part of solving 100% failure rate
-2. ❌ **Removed observation counter** - Part of simplification strategy
-3. ❌ **Removed health check system** - Was causing production failures
-4. ❌ **Fire-and-forget worker spawn** - Hooks have proper error handling
-
-### Race Condition Discussion Needed:
-1. 🤔 **Worker startup race condition** - Valid concern, but retry loops caused the original failures. Options:
- - Keep current approach (hooks handle ECONNREFUSED gracefully)
- - Add single synchronous `pm2 list` check after spawn
- - Document as expected behavior
-
-### Nice to Have (Post-Merge):
-1. 📋 **Consolidate FIXED_PORT constant** - Technical debt cleanup
-
----
-
-## Key Documentation References
-
-The architectural decisions are comprehensively documented in:
-
-1. **JUST-FUCKING-RUN-IT.md** (Observation #3602)
- - Architectural decision to remove health checks
- - Philosophy: Trust PM2, let HTTP timeouts be the health check
-
-2. **LINE-BY-LINE-CASCADING-BULLSHIT.md** (Observation #3600)
- - Root cause analysis of how health checks caused 100% failure rate
- - Documents cascade from arbitrary 3000ms timeout → retry loops → race conditions
-
-3. **MINIMUM-PARAMETERS.md** (Observation #3601)
- - Quantified impact: 21 unnecessary PM2 parameters, ~160 lines deleted
- - Philosophy: "Minimum parameters = minimum bugs"
-
-4. **STUPID-SHIT-THAT-BROKE-PRODUCTION.md** (Observation #3597)
- - 8 critical issues causing 100% user failure rate
- - Includes worker crashing on Chroma failures despite data already in SQLite
-
-These documents explain **why** the simplifications were necessary - they weren't arbitrary removal of useful features, they were targeted fixes for production failures.
-
----
-
-## Production Context
-
-**Before This PR**:
-- 100% user failure rate after v4.x release
-- Worker startup took 4-5 seconds but health checks timed out at 3 seconds
-- `stdio: 'ignore'` eliminated all debugging visibility
-- Worker crashed on Chroma failures despite data safely in SQLite
-- ChromaSync initialized in constructor, blocking HTTP server
-- 113 lines of health check code with retry loops masking real problems
-
-**After This PR**:
-- HTTP server starts immediately
-- Worker stays alive through Chroma failures (graceful degradation)
-- Errors are visible (`stdio: 'inherit'`)
-- Worker-utils.ts: 113 lines → 15 lines (87% reduction)
-- Hooks have proper error handling with actionable user messages
-- System works with just SQLite FTS5, Chroma is optional enhancement
-
-The "removed observability" was actually **removed complexity that was hiding problems**, not helping diagnose them.
diff --git a/docs/context/agent-sdk-ref.md b/docs/context/agent-sdk-ref.md
deleted file mode 100644
index 28925155..00000000
--- a/docs/context/agent-sdk-ref.md
+++ /dev/null
@@ -1,1797 +0,0 @@
-# Agent SDK reference - TypeScript
-
-> Complete API reference for the TypeScript Agent SDK, including all functions, types, and interfaces.
-
-
-
-## Installation
-
-```bash theme={null}
-npm install @anthropic-ai/claude-agent-sdk
-```
-
-## Functions
-
-### `query()`
-
-The primary function for interacting with Claude Code. Creates an async generator that streams messages as they arrive.
-
-```ts theme={null}
-function query({
- prompt,
- options
-}: {
- prompt: string | AsyncIterable;
- options?: Options;
-}): Query
-```
-
-#### Parameters
-
-| Parameter | Type | Description |
-| :-------- | :--------------------------------------------------------------- | :---------------------------------------------------------------- |
-| `prompt` | `string \| AsyncIterable<`[`SDKUserMessage`](#sdkusermessage)`>` | The input prompt as a string or async iterable for streaming mode |
-| `options` | [`Options`](#options) | Optional configuration object (see Options type below) |
-
-#### Returns
-
-Returns a [`Query`](#query-1) object that extends `AsyncGenerator<`[`SDKMessage`](#sdkmessage)`, void>` with additional methods.
-
-### `tool()`
-
-Creates a type-safe MCP tool definition for use with SDK MCP servers.
-
-```ts theme={null}
-function tool(
- name: string,
- description: string,
- inputSchema: Schema,
- handler: (args: z.infer>, extra: unknown) => Promise
-): SdkMcpToolDefinition
-```
-
-#### Parameters
-
-| Parameter | Type | Description |
-| :------------ | :---------------------------------------------------------------- | :---------------------------------------------- |
-| `name` | `string` | The name of the tool |
-| `description` | `string` | A description of what the tool does |
-| `inputSchema` | `Schema extends ZodRawShape` | Zod schema defining the tool's input parameters |
-| `handler` | `(args, extra) => Promise<`[`CallToolResult`](#calltoolresult)`>` | Async function that executes the tool logic |
-
-### `createSdkMcpServer()`
-
-Creates an MCP server instance that runs in the same process as your application.
-
-```ts theme={null}
-function createSdkMcpServer(options: {
- name: string;
- version?: string;
- tools?: Array>;
-}): McpSdkServerConfigWithInstance
-```
-
-#### Parameters
-
-| Parameter | Type | Description |
-| :---------------- | :---------------------------- | :------------------------------------------------------- |
-| `options.name` | `string` | The name of the MCP server |
-| `options.version` | `string` | Optional version string |
-| `options.tools` | `Array` | Array of tool definitions created with [`tool()`](#tool) |
-
-## Types
-
-### `Options`
-
-Configuration object for the `query()` function.
-
-| Property | Type | Default | Description |
-| :--------------------------- | :------------------------------------------------------------------------------------------------ | :------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `abortController` | `AbortController` | `new AbortController()` | Controller for cancelling operations |
-| `additionalDirectories` | `string[]` | `[]` | Additional directories Claude can access |
-| `agents` | `Record` | `undefined` | Programmatically define subagents |
-| `allowedTools` | `string[]` | All tools | List of allowed tool names |
-| `canUseTool` | [`CanUseTool`](#canusetool) | `undefined` | Custom permission function for tool usage |
-| `continue` | `boolean` | `false` | Continue the most recent conversation |
-| `cwd` | `string` | `process.cwd()` | Current working directory |
-| `disallowedTools` | `string[]` | `[]` | List of disallowed tool names |
-| `env` | `Dict` | `process.env` | Environment variables |
-| `executable` | `'bun' \| 'deno' \| 'node'` | Auto-detected | JavaScript runtime to use |
-| `executableArgs` | `string[]` | `[]` | Arguments to pass to the executable |
-| `extraArgs` | `Record` | `{}` | Additional arguments |
-| `fallbackModel` | `string` | `undefined` | Model to use if primary fails |
-| `forkSession` | `boolean` | `false` | When resuming with `resume`, fork to a new session ID instead of continuing the original session |
-| `hooks` | `Partial>` | `{}` | Hook callbacks for events |
-| `includePartialMessages` | `boolean` | `false` | Include partial message events |
-| `maxThinkingTokens` | `number` | `undefined` | Maximum tokens for thinking process |
-| `maxTurns` | `number` | `undefined` | Maximum conversation turns |
-| `mcpServers` | `Record` | `{}` | MCP server configurations |
-| `model` | `string` | Default from CLI | Claude model to use |
-| `pathToClaudeCodeExecutable` | `string` | Auto-detected | Path to Claude Code executable |
-| `permissionMode` | [`PermissionMode`](#permissionmode) | `'default'` | Permission mode for the session |
-| `permissionPromptToolName` | `string` | `undefined` | MCP tool name for permission prompts |
-| `plugins` | [`SdkPluginConfig`](#sdkpluginconfig)`[]` | `[]` | Load custom plugins from local paths. See [Plugins](/en/api/agent-sdk/plugins) for details |
-| `resume` | `string` | `undefined` | Session ID to resume |
-| `settingSources` | [`SettingSource`](#settingsource)`[]` | `[]` (no settings) | Control which filesystem settings to load. When omitted, no settings are loaded. **Note:** Must include `'project'` to load CLAUDE.md files |
-| `stderr` | `(data: string) => void` | `undefined` | Callback for stderr output |
-| `strictMcpConfig` | `boolean` | `false` | Enforce strict MCP validation |
-| `systemPrompt` | `string \| { type: 'preset'; preset: 'claude_code'; append?: string }` | `undefined` (empty prompt) | System prompt configuration. Pass a string for custom prompt, or `{ type: 'preset', preset: 'claude_code' }` to use Claude Code's system prompt. When using the preset object form, add `append` to extend the system prompt with additional instructions |
-
-### `Query`
-
-Interface returned by the `query()` function.
-
-```ts theme={null}
-interface Query extends AsyncGenerator {
- interrupt(): Promise;
- setPermissionMode(mode: PermissionMode): Promise;
-}
-```
-
-#### Methods
-
-| Method | Description |
-| :-------------------- | :------------------------------------------------------------------- |
-| `interrupt()` | Interrupts the query (only available in streaming input mode) |
-| `setPermissionMode()` | Changes the permission mode (only available in streaming input mode) |
-
-### `AgentDefinition`
-
-Configuration for a subagent defined programmatically.
-
-```ts theme={null}
-type AgentDefinition = {
- description: string;
- tools?: string[];
- prompt: string;
- model?: 'sonnet' | 'opus' | 'haiku' | 'inherit';
-}
-```
-
-| Field | Required | Description |
-| :------------ | :------- | :------------------------------------------------------------- |
-| `description` | Yes | Natural language description of when to use this agent |
-| `tools` | No | Array of allowed tool names. If omitted, inherits all tools |
-| `prompt` | Yes | The agent's system prompt |
-| `model` | No | Model override for this agent. If omitted, uses the main model |
-
-### `SettingSource`
-
-Controls which filesystem-based configuration sources the SDK loads settings from.
-
-```ts theme={null}
-type SettingSource = 'user' | 'project' | 'local';
-```
-
-| Value | Description | Location |
-| :---------- | :------------------------------------------- | :---------------------------- |
-| `'user'` | Global user settings | `~/.claude/settings.json` |
-| `'project'` | Shared project settings (version controlled) | `.claude/settings.json` |
-| `'local'` | Local project settings (gitignored) | `.claude/settings.local.json` |
-
-#### Default behavior
-
-When `settingSources` is **omitted** or **undefined**, the SDK does **not** load any filesystem settings. This provides isolation for SDK applications.
-
-#### Why use settingSources?
-
-**Load all filesystem settings (legacy behavior):**
-
-```typescript theme={null}
-// Load all settings like SDK v0.0.x did
-const result = query({
- prompt: "Analyze this code",
- options: {
- settingSources: ['user', 'project', 'local'] // Load all settings
- }
-});
-```
-
-**Load only specific setting sources:**
-
-```typescript theme={null}
-// Load only project settings, ignore user and local
-const result = query({
- prompt: "Run CI checks",
- options: {
- settingSources: ['project'] // Only .claude/settings.json
- }
-});
-```
-
-**Testing and CI environments:**
-
-```typescript theme={null}
-// Ensure consistent behavior in CI by excluding local settings
-const result = query({
- prompt: "Run tests",
- options: {
- settingSources: ['project'], // Only team-shared settings
- permissionMode: 'bypassPermissions'
- }
-});
-```
-
-**SDK-only applications:**
-
-```typescript theme={null}
-// Define everything programmatically (default behavior)
-// No filesystem dependencies - settingSources defaults to []
-const result = query({
- prompt: "Review this PR",
- options: {
- // settingSources: [] is the default, no need to specify
- agents: { /* ... */ },
- mcpServers: { /* ... */ },
- allowedTools: ['Read', 'Grep', 'Glob']
- }
-});
-```
-
-**Loading CLAUDE.md project instructions:**
-
-```typescript theme={null}
-// Load project settings to include CLAUDE.md files
-const result = query({
- prompt: "Add a new feature following project conventions",
- options: {
- systemPrompt: {
- type: 'preset',
- preset: 'claude_code' // Required to use CLAUDE.md
- },
- settingSources: ['project'], // Loads CLAUDE.md from project directory
- allowedTools: ['Read', 'Write', 'Edit']
- }
-});
-```
-
-#### Settings precedence
-
-When multiple sources are loaded, settings are merged with this precedence (highest to lowest):
-
-1. Local settings (`.claude/settings.local.json`)
-2. Project settings (`.claude/settings.json`)
-3. User settings (`~/.claude/settings.json`)
-
-Programmatic options (like `agents`, `allowedTools`) always override filesystem settings.
-
-### `PermissionMode`
-
-```ts theme={null}
-type PermissionMode =
- | 'default' // Standard permission behavior
- | 'acceptEdits' // Auto-accept file edits
- | 'bypassPermissions' // Bypass all permission checks
- | 'plan' // Planning mode - no execution
-```
-
-### `CanUseTool`
-
-Custom permission function type for controlling tool usage.
-
-```ts theme={null}
-type CanUseTool = (
- toolName: string,
- input: ToolInput,
- options: {
- signal: AbortSignal;
- suggestions?: PermissionUpdate[];
- }
-) => Promise;
-```
-
-### `PermissionResult`
-
-Result of a permission check.
-
-```ts theme={null}
-type PermissionResult =
- | {
- behavior: 'allow';
- updatedInput: ToolInput;
- updatedPermissions?: PermissionUpdate[];
- }
- | {
- behavior: 'deny';
- message: string;
- interrupt?: boolean;
- }
-```
-
-### `McpServerConfig`
-
-Configuration for MCP servers.
-
-```ts theme={null}
-type McpServerConfig =
- | McpStdioServerConfig
- | McpSSEServerConfig
- | McpHttpServerConfig
- | McpSdkServerConfigWithInstance;
-```
-
-#### `McpStdioServerConfig`
-
-```ts theme={null}
-type McpStdioServerConfig = {
- type?: 'stdio';
- command: string;
- args?: string[];
- env?: Record;
-}
-```
-
-#### `McpSSEServerConfig`
-
-```ts theme={null}
-type McpSSEServerConfig = {
- type: 'sse';
- url: string;
- headers?: Record;
-}
-```
-
-#### `McpHttpServerConfig`
-
-```ts theme={null}
-type McpHttpServerConfig = {
- type: 'http';
- url: string;
- headers?: Record;
-}
-```
-
-#### `McpSdkServerConfigWithInstance`
-
-```ts theme={null}
-type McpSdkServerConfigWithInstance = {
- type: 'sdk';
- name: string;
- instance: McpServer;
-}
-```
-
-### `SdkPluginConfig`
-
-Configuration for loading plugins in the SDK.
-
-```ts theme={null}
-type SdkPluginConfig = {
- type: 'local';
- path: string;
-}
-```
-
-| Field | Type | Description |
-| :----- | :-------- | :--------------------------------------------------------- |
-| `type` | `'local'` | Must be `'local'` (only local plugins currently supported) |
-| `path` | `string` | Absolute or relative path to the plugin directory |
-
-**Example:**
-
-```ts theme={null}
-plugins: [
- { type: 'local', path: './my-plugin' },
- { type: 'local', path: '/absolute/path/to/plugin' }
-]
-```
-
-For complete information on creating and using plugins, see [Plugins](/en/api/agent-sdk/plugins).
-
-## Message Types
-
-### `SDKMessage`
-
-Union type of all possible messages returned by the query.
-
-```ts theme={null}
-type SDKMessage =
- | SDKAssistantMessage
- | SDKUserMessage
- | SDKUserMessageReplay
- | SDKResultMessage
- | SDKSystemMessage
- | SDKPartialAssistantMessage
- | SDKCompactBoundaryMessage;
-```
-
-### `SDKAssistantMessage`
-
-Assistant response message.
-
-```ts theme={null}
-type SDKAssistantMessage = {
- type: 'assistant';
- uuid: UUID;
- session_id: string;
- message: APIAssistantMessage; // From Anthropic SDK
- parent_tool_use_id: string | null;
-}
-```
-
-### `SDKUserMessage`
-
-User input message.
-
-```ts theme={null}
-type SDKUserMessage = {
- type: 'user';
- uuid?: UUID;
- session_id: string;
- message: APIUserMessage; // From Anthropic SDK
- parent_tool_use_id: string | null;
-}
-```
-
-### `SDKUserMessageReplay`
-
-Replayed user message with required UUID.
-
-```ts theme={null}
-type SDKUserMessageReplay = {
- type: 'user';
- uuid: UUID;
- session_id: string;
- message: APIUserMessage;
- parent_tool_use_id: string | null;
-}
-```
-
-### `SDKResultMessage`
-
-Final result message.
-
-```ts theme={null}
-type SDKResultMessage =
- | {
- type: 'result';
- subtype: 'success';
- uuid: UUID;
- session_id: string;
- duration_ms: number;
- duration_api_ms: number;
- is_error: boolean;
- num_turns: number;
- result: string;
- total_cost_usd: number;
- usage: NonNullableUsage;
- permission_denials: SDKPermissionDenial[];
- }
- | {
- type: 'result';
- subtype: 'error_max_turns' | 'error_during_execution';
- uuid: UUID;
- session_id: string;
- duration_ms: number;
- duration_api_ms: number;
- is_error: boolean;
- num_turns: number;
- total_cost_usd: number;
- usage: NonNullableUsage;
- permission_denials: SDKPermissionDenial[];
- }
-```
-
-### `SDKSystemMessage`
-
-System initialization message.
-
-```ts theme={null}
-type SDKSystemMessage = {
- type: 'system';
- subtype: 'init';
- uuid: UUID;
- session_id: string;
- apiKeySource: ApiKeySource;
- cwd: string;
- tools: string[];
- mcp_servers: {
- name: string;
- status: string;
- }[];
- model: string;
- permissionMode: PermissionMode;
- slash_commands: string[];
- output_style: string;
-}
-```
-
-### `SDKPartialAssistantMessage`
-
-Streaming partial message (only when `includePartialMessages` is true).
-
-```ts theme={null}
-type SDKPartialAssistantMessage = {
- type: 'stream_event';
- event: RawMessageStreamEvent; // From Anthropic SDK
- parent_tool_use_id: string | null;
- uuid: UUID;
- session_id: string;
-}
-```
-
-### `SDKCompactBoundaryMessage`
-
-Message indicating a conversation compaction boundary.
-
-```ts theme={null}
-type SDKCompactBoundaryMessage = {
- type: 'system';
- subtype: 'compact_boundary';
- uuid: UUID;
- session_id: string;
- compact_metadata: {
- trigger: 'manual' | 'auto';
- pre_tokens: number;
- };
-}
-```
-
-### `SDKPermissionDenial`
-
-Information about a denied tool use.
-
-```ts theme={null}
-type SDKPermissionDenial = {
- tool_name: string;
- tool_use_id: string;
- tool_input: ToolInput;
-}
-```
-
-## Hook Types
-
-### `HookEvent`
-
-Available hook events.
-
-```ts theme={null}
-type HookEvent =
- | 'PreToolUse'
- | 'PostToolUse'
- | 'Notification'
- | 'UserPromptSubmit'
- | 'SessionStart'
- | 'SessionEnd'
- | 'Stop'
- | 'SubagentStop'
- | 'PreCompact';
-```
-
-### `HookCallback`
-
-Hook callback function type.
-
-```ts theme={null}
-type HookCallback = (
- input: HookInput, // Union of all hook input types
- toolUseID: string | undefined,
- options: { signal: AbortSignal }
-) => Promise;
-```
-
-### `HookCallbackMatcher`
-
-Hook configuration with optional matcher.
-
-```ts theme={null}
-interface HookCallbackMatcher {
- matcher?: string;
- hooks: HookCallback[];
-}
-```
-
-### `HookInput`
-
-Union type of all hook input types.
-
-```ts theme={null}
-type HookInput =
- | PreToolUseHookInput
- | PostToolUseHookInput
- | NotificationHookInput
- | UserPromptSubmitHookInput
- | SessionStartHookInput
- | SessionEndHookInput
- | StopHookInput
- | SubagentStopHookInput
- | PreCompactHookInput;
-```
-
-### `BaseHookInput`
-
-Base interface that all hook input types extend.
-
-```ts theme={null}
-type BaseHookInput = {
- session_id: string;
- transcript_path: string;
- cwd: string;
- permission_mode?: string;
-}
-```
-
-#### `PreToolUseHookInput`
-
-```ts theme={null}
-type PreToolUseHookInput = BaseHookInput & {
- hook_event_name: 'PreToolUse';
- tool_name: string;
- tool_input: ToolInput;
-}
-```
-
-#### `PostToolUseHookInput`
-
-```ts theme={null}
-type PostToolUseHookInput = BaseHookInput & {
- hook_event_name: 'PostToolUse';
- tool_name: string;
- tool_input: ToolInput;
- tool_response: ToolOutput;
-}
-```
-
-#### `NotificationHookInput`
-
-```ts theme={null}
-type NotificationHookInput = BaseHookInput & {
- hook_event_name: 'Notification';
- message: string;
- title?: string;
-}
-```
-
-#### `UserPromptSubmitHookInput`
-
-```ts theme={null}
-type UserPromptSubmitHookInput = BaseHookInput & {
- hook_event_name: 'UserPromptSubmit';
- prompt: string;
-}
-```
-
-#### `SessionStartHookInput`
-
-```ts theme={null}
-type SessionStartHookInput = BaseHookInput & {
- hook_event_name: 'SessionStart';
- source: 'startup' | 'resume' | 'clear' | 'compact';
-}
-```
-
-#### `SessionEndHookInput`
-
-```ts theme={null}
-type SessionEndHookInput = BaseHookInput & {
- hook_event_name: 'SessionEnd';
- reason: 'clear' | 'logout' | 'prompt_input_exit' | 'other';
-}
-```
-
-#### `StopHookInput`
-
-```ts theme={null}
-type StopHookInput = BaseHookInput & {
- hook_event_name: 'Stop';
- stop_hook_active: boolean;
-}
-```
-
-#### `SubagentStopHookInput`
-
-```ts theme={null}
-type SubagentStopHookInput = BaseHookInput & {
- hook_event_name: 'SubagentStop';
- stop_hook_active: boolean;
-}
-```
-
-#### `PreCompactHookInput`
-
-```ts theme={null}
-type PreCompactHookInput = BaseHookInput & {
- hook_event_name: 'PreCompact';
- trigger: 'manual' | 'auto';
- custom_instructions: string | null;
-}
-```
-
-### `HookJSONOutput`
-
-Hook return value.
-
-```ts theme={null}
-type HookJSONOutput = AsyncHookJSONOutput | SyncHookJSONOutput;
-```
-
-#### `AsyncHookJSONOutput`
-
-```ts theme={null}
-type AsyncHookJSONOutput = {
- async: true;
- asyncTimeout?: number;
-}
-```
-
-#### `SyncHookJSONOutput`
-
-```ts theme={null}
-type SyncHookJSONOutput = {
- continue?: boolean;
- suppressOutput?: boolean;
- stopReason?: string;
- decision?: 'approve' | 'block';
- systemMessage?: string;
- reason?: string;
- hookSpecificOutput?:
- | {
- hookEventName: 'PreToolUse';
- permissionDecision?: 'allow' | 'deny' | 'ask';
- permissionDecisionReason?: string;
- }
- | {
- hookEventName: 'UserPromptSubmit';
- additionalContext?: string;
- }
- | {
- hookEventName: 'SessionStart';
- additionalContext?: string;
- }
- | {
- hookEventName: 'PostToolUse';
- additionalContext?: string;
- };
-}
-```
-
-## Tool Input Types
-
-Documentation of input schemas for all built-in Claude Code tools. These types are exported from `@anthropic-ai/claude-agent-sdk` and can be used for type-safe tool interactions.
-
-### `ToolInput`
-
-**Note:** This is a documentation-only type for clarity. It represents the union of all tool input types.
-
-```ts theme={null}
-type ToolInput =
- | AgentInput
- | BashInput
- | BashOutputInput
- | FileEditInput
- | FileReadInput
- | FileWriteInput
- | GlobInput
- | GrepInput
- | KillShellInput
- | NotebookEditInput
- | WebFetchInput
- | WebSearchInput
- | TodoWriteInput
- | ExitPlanModeInput
- | ListMcpResourcesInput
- | ReadMcpResourceInput;
-```
-
-### Task
-
-**Tool name:** `Task`
-
-```ts theme={null}
-interface AgentInput {
- /**
- * A short (3-5 word) description of the task
- */
- description: string;
- /**
- * The task for the agent to perform
- */
- prompt: string;
- /**
- * The type of specialized agent to use for this task
- */
- subagent_type: string;
-}
-```
-
-Launches a new agent to handle complex, multi-step tasks autonomously.
-
-### Bash
-
-**Tool name:** `Bash`
-
-```ts theme={null}
-interface BashInput {
- /**
- * The command to execute
- */
- command: string;
- /**
- * Optional timeout in milliseconds (max 600000)
- */
- timeout?: number;
- /**
- * Clear, concise description of what this command does in 5-10 words
- */
- description?: string;
- /**
- * Set to true to run this command in the background
- */
- run_in_background?: boolean;
-}
-```
-
-Executes bash commands in a persistent shell session with optional timeout and background execution.
-
-### BashOutput
-
-**Tool name:** `BashOutput`
-
-```ts theme={null}
-interface BashOutputInput {
- /**
- * The ID of the background shell to retrieve output from
- */
- bash_id: string;
- /**
- * Optional regex to filter output lines
- */
- filter?: string;
-}
-```
-
-Retrieves output from a running or completed background bash shell.
-
-### Edit
-
-**Tool name:** `Edit`
-
-```ts theme={null}
-interface FileEditInput {
- /**
- * The absolute path to the file to modify
- */
- file_path: string;
- /**
- * The text to replace
- */
- old_string: string;
- /**
- * The text to replace it with (must be different from old_string)
- */
- new_string: string;
- /**
- * Replace all occurrences of old_string (default false)
- */
- replace_all?: boolean;
-}
-```
-
-Performs exact string replacements in files.
-
-### Read
-
-**Tool name:** `Read`
-
-```ts theme={null}
-interface FileReadInput {
- /**
- * The absolute path to the file to read
- */
- file_path: string;
- /**
- * The line number to start reading from
- */
- offset?: number;
- /**
- * The number of lines to read
- */
- limit?: number;
-}
-```
-
-Reads files from the local filesystem, including text, images, PDFs, and Jupyter notebooks.
-
-### Write
-
-**Tool name:** `Write`
-
-```ts theme={null}
-interface FileWriteInput {
- /**
- * The absolute path to the file to write
- */
- file_path: string;
- /**
- * The content to write to the file
- */
- content: string;
-}
-```
-
-Writes a file to the local filesystem, overwriting if it exists.
-
-### Glob
-
-**Tool name:** `Glob`
-
-```ts theme={null}
-interface GlobInput {
- /**
- * The glob pattern to match files against
- */
- pattern: string;
- /**
- * The directory to search in (defaults to cwd)
- */
- path?: string;
-}
-```
-
-Fast file pattern matching that works with any codebase size.
-
-### Grep
-
-**Tool name:** `Grep`
-
-```ts theme={null}
-interface GrepInput {
- /**
- * The regular expression pattern to search for
- */
- pattern: string;
- /**
- * File or directory to search in (defaults to cwd)
- */
- path?: string;
- /**
- * Glob pattern to filter files (e.g. "*.js")
- */
- glob?: string;
- /**
- * File type to search (e.g. "js", "py", "rust")
- */
- type?: string;
- /**
- * Output mode: "content", "files_with_matches", or "count"
- */
- output_mode?: 'content' | 'files_with_matches' | 'count';
- /**
- * Case insensitive search
- */
- '-i'?: boolean;
- /**
- * Show line numbers (for content mode)
- */
- '-n'?: boolean;
- /**
- * Lines to show before each match
- */
- '-B'?: number;
- /**
- * Lines to show after each match
- */
- '-A'?: number;
- /**
- * Lines to show before and after each match
- */
- '-C'?: number;
- /**
- * Limit output to first N lines/entries
- */
- head_limit?: number;
- /**
- * Enable multiline mode
- */
- multiline?: boolean;
-}
-```
-
-Powerful search tool built on ripgrep with regex support.
-
-### KillBash
-
-**Tool name:** `KillBash`
-
-```ts theme={null}
-interface KillShellInput {
- /**
- * The ID of the background shell to kill
- */
- shell_id: string;
-}
-```
-
-Kills a running background bash shell by its ID.
-
-### NotebookEdit
-
-**Tool name:** `NotebookEdit`
-
-```ts theme={null}
-interface NotebookEditInput {
- /**
- * The absolute path to the Jupyter notebook file
- */
- notebook_path: string;
- /**
- * The ID of the cell to edit
- */
- cell_id?: string;
- /**
- * The new source for the cell
- */
- new_source: string;
- /**
- * The type of the cell (code or markdown)
- */
- cell_type?: 'code' | 'markdown';
- /**
- * The type of edit (replace, insert, delete)
- */
- edit_mode?: 'replace' | 'insert' | 'delete';
-}
-```
-
-Edits cells in Jupyter notebook files.
-
-### WebFetch
-
-**Tool name:** `WebFetch`
-
-```ts theme={null}
-interface WebFetchInput {
- /**
- * The URL to fetch content from
- */
- url: string;
- /**
- * The prompt to run on the fetched content
- */
- prompt: string;
-}
-```
-
-Fetches content from a URL and processes it with an AI model.
-
-### WebSearch
-
-**Tool name:** `WebSearch`
-
-```ts theme={null}
-interface WebSearchInput {
- /**
- * The search query to use
- */
- query: string;
- /**
- * Only include results from these domains
- */
- allowed_domains?: string[];
- /**
- * Never include results from these domains
- */
- blocked_domains?: string[];
-}
-```
-
-Searches the web and returns formatted results.
-
-### TodoWrite
-
-**Tool name:** `TodoWrite`
-
-```ts theme={null}
-interface TodoWriteInput {
- /**
- * The updated todo list
- */
- todos: Array<{
- /**
- * The task description
- */
- content: string;
- /**
- * The task status
- */
- status: 'pending' | 'in_progress' | 'completed';
- /**
- * Active form of the task description
- */
- activeForm: string;
- }>;
-}
-```
-
-Creates and manages a structured task list for tracking progress.
-
-### ExitPlanMode
-
-**Tool name:** `ExitPlanMode`
-
-```ts theme={null}
-interface ExitPlanModeInput {
- /**
- * The plan to run by the user for approval
- */
- plan: string;
-}
-```
-
-Exits planning mode and prompts the user to approve the plan.
-
-### ListMcpResources
-
-**Tool name:** `ListMcpResources`
-
-```ts theme={null}
-interface ListMcpResourcesInput {
- /**
- * Optional server name to filter resources by
- */
- server?: string;
-}
-```
-
-Lists available MCP resources from connected servers.
-
-### ReadMcpResource
-
-**Tool name:** `ReadMcpResource`
-
-```ts theme={null}
-interface ReadMcpResourceInput {
- /**
- * The MCP server name
- */
- server: string;
- /**
- * The resource URI to read
- */
- uri: string;
-}
-```
-
-Reads a specific MCP resource from a server.
-
-## Tool Output Types
-
-Documentation of output schemas for all built-in Claude Code tools. These types represent the actual response data returned by each tool.
-
-### `ToolOutput`
-
-**Note:** This is a documentation-only type for clarity. It represents the union of all tool output types.
-
-```ts theme={null}
-type ToolOutput =
- | TaskOutput
- | BashOutput
- | BashOutputToolOutput
- | EditOutput
- | ReadOutput
- | WriteOutput
- | GlobOutput
- | GrepOutput
- | KillBashOutput
- | NotebookEditOutput
- | WebFetchOutput
- | WebSearchOutput
- | TodoWriteOutput
- | ExitPlanModeOutput
- | ListMcpResourcesOutput
- | ReadMcpResourceOutput;
-```
-
-### Task
-
-**Tool name:** `Task`
-
-```ts theme={null}
-interface TaskOutput {
- /**
- * Final result message from the subagent
- */
- result: string;
- /**
- * Token usage statistics
- */
- usage?: {
- input_tokens: number;
- output_tokens: number;
- cache_creation_input_tokens?: number;
- cache_read_input_tokens?: number;
- };
- /**
- * Total cost in USD
- */
- total_cost_usd?: number;
- /**
- * Execution duration in milliseconds
- */
- duration_ms?: number;
-}
-```
-
-Returns the final result from the subagent after completing the delegated task.
-
-### Bash
-
-**Tool name:** `Bash`
-
-```ts theme={null}
-interface BashOutput {
- /**
- * Combined stdout and stderr output
- */
- output: string;
- /**
- * Exit code of the command
- */
- exitCode: number;
- /**
- * Whether the command was killed due to timeout
- */
- killed?: boolean;
- /**
- * Shell ID for background processes
- */
- shellId?: string;
-}
-```
-
-Returns command output with exit status. Background commands return immediately with a shellId.
-
-### BashOutput
-
-**Tool name:** `BashOutput`
-
-```ts theme={null}
-interface BashOutputToolOutput {
- /**
- * New output since last check
- */
- output: string;
- /**
- * Current shell status
- */
- status: 'running' | 'completed' | 'failed';
- /**
- * Exit code (when completed)
- */
- exitCode?: number;
-}
-```
-
-Returns incremental output from background shells.
-
-### Edit
-
-**Tool name:** `Edit`
-
-```ts theme={null}
-interface EditOutput {
- /**
- * Confirmation message
- */
- message: string;
- /**
- * Number of replacements made
- */
- replacements: number;
- /**
- * File path that was edited
- */
- file_path: string;
-}
-```
-
-Returns confirmation of successful edits with replacement count.
-
-### Read
-
-**Tool name:** `Read`
-
-```ts theme={null}
-type ReadOutput =
- | TextFileOutput
- | ImageFileOutput
- | PDFFileOutput
- | NotebookFileOutput;
-
-interface TextFileOutput {
- /**
- * File contents with line numbers
- */
- content: string;
- /**
- * Total number of lines in file
- */
- total_lines: number;
- /**
- * Lines actually returned
- */
- lines_returned: number;
-}
-
-interface ImageFileOutput {
- /**
- * Base64 encoded image data
- */
- image: string;
- /**
- * Image MIME type
- */
- mime_type: string;
- /**
- * File size in bytes
- */
- file_size: number;
-}
-
-interface PDFFileOutput {
- /**
- * Array of page contents
- */
- pages: Array<{
- page_number: number;
- text?: string;
- images?: Array<{
- image: string;
- mime_type: string;
- }>;
- }>;
- /**
- * Total number of pages
- */
- total_pages: number;
-}
-
-interface NotebookFileOutput {
- /**
- * Jupyter notebook cells
- */
- cells: Array<{
- cell_type: 'code' | 'markdown';
- source: string;
- outputs?: any[];
- execution_count?: number;
- }>;
- /**
- * Notebook metadata
- */
- metadata?: Record;
-}
-```
-
-Returns file contents in format appropriate to file type.
-
-### Write
-
-**Tool name:** `Write`
-
-```ts theme={null}
-interface WriteOutput {
- /**
- * Success message
- */
- message: string;
- /**
- * Number of bytes written
- */
- bytes_written: number;
- /**
- * File path that was written
- */
- file_path: string;
-}
-```
-
-Returns confirmation after successfully writing the file.
-
-### Glob
-
-**Tool name:** `Glob`
-
-```ts theme={null}
-interface GlobOutput {
- /**
- * Array of matching file paths
- */
- matches: string[];
- /**
- * Number of matches found
- */
- count: number;
- /**
- * Search directory used
- */
- search_path: string;
-}
-```
-
-Returns file paths matching the glob pattern, sorted by modification time.
-
-### Grep
-
-**Tool name:** `Grep`
-
-```ts theme={null}
-type GrepOutput =
- | GrepContentOutput
- | GrepFilesOutput
- | GrepCountOutput;
-
-interface GrepContentOutput {
- /**
- * Matching lines with context
- */
- matches: Array<{
- file: string;
- line_number?: number;
- line: string;
- before_context?: string[];
- after_context?: string[];
- }>;
- /**
- * Total number of matches
- */
- total_matches: number;
-}
-
-interface GrepFilesOutput {
- /**
- * Files containing matches
- */
- files: string[];
- /**
- * Number of files with matches
- */
- count: number;
-}
-
-interface GrepCountOutput {
- /**
- * Match counts per file
- */
- counts: Array<{
- file: string;
- count: number;
- }>;
- /**
- * Total matches across all files
- */
- total: number;
-}
-```
-
-Returns search results in the format specified by output\_mode.
-
-### KillBash
-
-**Tool name:** `KillBash`
-
-```ts theme={null}
-interface KillBashOutput {
- /**
- * Success message
- */
- message: string;
- /**
- * ID of the killed shell
- */
- shell_id: string;
-}
-```
-
-Returns confirmation after terminating the background shell.
-
-### NotebookEdit
-
-**Tool name:** `NotebookEdit`
-
-```ts theme={null}
-interface NotebookEditOutput {
- /**
- * Success message
- */
- message: string;
- /**
- * Type of edit performed
- */
- edit_type: 'replaced' | 'inserted' | 'deleted';
- /**
- * Cell ID that was affected
- */
- cell_id?: string;
- /**
- * Total cells in notebook after edit
- */
- total_cells: number;
-}
-```
-
-Returns confirmation after modifying the Jupyter notebook.
-
-### WebFetch
-
-**Tool name:** `WebFetch`
-
-```ts theme={null}
-interface WebFetchOutput {
- /**
- * AI model's response to the prompt
- */
- response: string;
- /**
- * URL that was fetched
- */
- url: string;
- /**
- * Final URL after redirects
- */
- final_url?: string;
- /**
- * HTTP status code
- */
- status_code?: number;
-}
-```
-
-Returns the AI's analysis of the fetched web content.
-
-### WebSearch
-
-**Tool name:** `WebSearch`
-
-```ts theme={null}
-interface WebSearchOutput {
- /**
- * Search results
- */
- results: Array<{
- title: string;
- url: string;
- snippet: string;
- /**
- * Additional metadata if available
- */
- metadata?: Record;
- }>;
- /**
- * Total number of results
- */
- total_results: number;
- /**
- * The query that was searched
- */
- query: string;
-}
-```
-
-Returns formatted search results from the web.
-
-### TodoWrite
-
-**Tool name:** `TodoWrite`
-
-```ts theme={null}
-interface TodoWriteOutput {
- /**
- * Success message
- */
- message: string;
- /**
- * Current todo statistics
- */
- stats: {
- total: number;
- pending: number;
- in_progress: number;
- completed: number;
- };
-}
-```
-
-Returns confirmation with current task statistics.
-
-### ExitPlanMode
-
-**Tool name:** `ExitPlanMode`
-
-```ts theme={null}
-interface ExitPlanModeOutput {
- /**
- * Confirmation message
- */
- message: string;
- /**
- * Whether user approved the plan
- */
- approved?: boolean;
-}
-```
-
-Returns confirmation after exiting plan mode.
-
-### ListMcpResources
-
-**Tool name:** `ListMcpResources`
-
-```ts theme={null}
-interface ListMcpResourcesOutput {
- /**
- * Available resources
- */
- resources: Array<{
- uri: string;
- name: string;
- description?: string;
- mimeType?: string;
- server: string;
- }>;
- /**
- * Total number of resources
- */
- total: number;
-}
-```
-
-Returns list of available MCP resources.
-
-### ReadMcpResource
-
-**Tool name:** `ReadMcpResource`
-
-```ts theme={null}
-interface ReadMcpResourceOutput {
- /**
- * Resource contents
- */
- contents: Array<{
- uri: string;
- mimeType?: string;
- text?: string;
- blob?: string;
- }>;
- /**
- * Server that provided the resource
- */
- server: string;
-}
-```
-
-Returns the contents of the requested MCP resource.
-
-## Permission Types
-
-### `PermissionUpdate`
-
-Operations for updating permissions.
-
-```ts theme={null}
-type PermissionUpdate =
- | {
- type: 'addRules';
- rules: PermissionRuleValue[];
- behavior: PermissionBehavior;
- destination: PermissionUpdateDestination;
- }
- | {
- type: 'replaceRules';
- rules: PermissionRuleValue[];
- behavior: PermissionBehavior;
- destination: PermissionUpdateDestination;
- }
- | {
- type: 'removeRules';
- rules: PermissionRuleValue[];
- behavior: PermissionBehavior;
- destination: PermissionUpdateDestination;
- }
- | {
- type: 'setMode';
- mode: PermissionMode;
- destination: PermissionUpdateDestination;
- }
- | {
- type: 'addDirectories';
- directories: string[];
- destination: PermissionUpdateDestination;
- }
- | {
- type: 'removeDirectories';
- directories: string[];
- destination: PermissionUpdateDestination;
- }
-```
-
-### `PermissionBehavior`
-
-```ts theme={null}
-type PermissionBehavior = 'allow' | 'deny' | 'ask';
-```
-
-### `PermissionUpdateDestination`
-
-```ts theme={null}
-type PermissionUpdateDestination =
- | 'userSettings' // Global user settings
- | 'projectSettings' // Per-directory project settings
- | 'localSettings' // Gitignored local settings
- | 'session' // Current session only
-```
-
-### `PermissionRuleValue`
-
-```ts theme={null}
-type PermissionRuleValue = {
- toolName: string;
- ruleContent?: string;
-}
-```
-
-## Other Types
-
-### `ApiKeySource`
-
-```ts theme={null}
-type ApiKeySource = 'user' | 'project' | 'org' | 'temporary';
-```
-
-### `ConfigScope`
-
-```ts theme={null}
-type ConfigScope = 'local' | 'user' | 'project';
-```
-
-### `NonNullableUsage`
-
-A version of [`Usage`](#usage) with all nullable fields made non-nullable.
-
-```ts theme={null}
-type NonNullableUsage = {
- [K in keyof Usage]: NonNullable;
-}
-```
-
-### `Usage`
-
-Token usage statistics (from `@anthropic-ai/sdk`).
-
-```ts theme={null}
-type Usage = {
- input_tokens: number | null;
- output_tokens: number | null;
- cache_creation_input_tokens?: number | null;
- cache_read_input_tokens?: number | null;
-}
-```
-
-### `CallToolResult`
-
-MCP tool result type (from `@modelcontextprotocol/sdk/types.js`).
-
-```ts theme={null}
-type CallToolResult = {
- content: Array<{
- type: 'text' | 'image' | 'resource';
- // Additional fields vary by type
- }>;
- isError?: boolean;
-}
-```
-
-### `AbortError`
-
-Custom error class for abort operations.
-
-```ts theme={null}
-class AbortError extends Error {}
-```
-
-## See also
-
-* [SDK overview](/en/api/agent-sdk/overview) - General SDK concepts
-* [Python SDK reference](/en/api/agent-sdk/python) - Python SDK documentation
-* [CLI reference](https://code.claude.com/docs/cli-reference) - Command-line interface
-* [Common workflows](https://code.claude.com/docs/common-workflows) - Step-by-step guides
diff --git a/docs/context/agent-sdk-ts-reference.md b/docs/context/agent-sdk-ts-reference.md
deleted file mode 100644
index ea7710be..00000000
--- a/docs/context/agent-sdk-ts-reference.md
+++ /dev/null
@@ -1,1797 +0,0 @@
-# Agent SDK reference - TypeScript
-
-> Complete API reference for the TypeScript Agent SDK, including all functions, types, and interfaces.
-
-
-
-## Installation
-
-```bash theme={null}
-npm install @anthropic-ai/claude-agent-sdk
-```
-
-## Functions
-
-### `query()`
-
-The primary function for interacting with Claude Code. Creates an async generator that streams messages as they arrive.
-
-```ts theme={null}
-function query({
- prompt,
- options
-}: {
- prompt: string | AsyncIterable;
- options?: Options;
-}): Query
-```
-
-#### Parameters
-
-| Parameter | Type | Description |
-| :-------- | :--------------------------------------------------------------- | :---------------------------------------------------------------- |
-| `prompt` | `string \| AsyncIterable<`[`SDKUserMessage`](#sdkusermessage)`>` | The input prompt as a string or async iterable for streaming mode |
-| `options` | [`Options`](#options) | Optional configuration object (see Options type below) |
-
-#### Returns
-
-Returns a [`Query`](#query-1) object that extends `AsyncGenerator<`[`SDKMessage`](#sdkmessage)`, void>` with additional methods.
-
-### `tool()`
-
-Creates a type-safe MCP tool definition for use with SDK MCP servers.
-
-```ts theme={null}
-function tool(
- name: string,
- description: string,
- inputSchema: Schema,
- handler: (args: z.infer>, extra: unknown) => Promise
-): SdkMcpToolDefinition
-```
-
-#### Parameters
-
-| Parameter | Type | Description |
-| :------------ | :---------------------------------------------------------------- | :---------------------------------------------- |
-| `name` | `string` | The name of the tool |
-| `description` | `string` | A description of what the tool does |
-| `inputSchema` | `Schema extends ZodRawShape` | Zod schema defining the tool's input parameters |
-| `handler` | `(args, extra) => Promise<`[`CallToolResult`](#calltoolresult)`>` | Async function that executes the tool logic |
-
-### `createSdkMcpServer()`
-
-Creates an MCP server instance that runs in the same process as your application.
-
-```ts theme={null}
-function createSdkMcpServer(options: {
- name: string;
- version?: string;
- tools?: Array>;
-}): McpSdkServerConfigWithInstance
-```
-
-#### Parameters
-
-| Parameter | Type | Description |
-| :---------------- | :---------------------------- | :------------------------------------------------------- |
-| `options.name` | `string` | The name of the MCP server |
-| `options.version` | `string` | Optional version string |
-| `options.tools` | `Array` | Array of tool definitions created with [`tool()`](#tool) |
-
-## Types
-
-### `Options`
-
-Configuration object for the `query()` function.
-
-| Property | Type | Default | Description |
-| :--------------------------- | :------------------------------------------------------------------------------------------------ | :------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `abortController` | `AbortController` | `new AbortController()` | Controller for cancelling operations |
-| `additionalDirectories` | `string[]` | `[]` | Additional directories Claude can access |
-| `agents` | `Record` | `undefined` | Programmatically define subagents |
-| `allowedTools` | `string[]` | All tools | List of allowed tool names |
-| `canUseTool` | [`CanUseTool`](#canusetool) | `undefined` | Custom permission function for tool usage |
-| `continue` | `boolean` | `false` | Continue the most recent conversation |
-| `cwd` | `string` | `process.cwd()` | Current working directory |
-| `disallowedTools` | `string[]` | `[]` | List of disallowed tool names |
-| `env` | `Dict` | `process.env` | Environment variables |
-| `executable` | `'bun' \| 'deno' \| 'node'` | Auto-detected | JavaScript runtime to use |
-| `executableArgs` | `string[]` | `[]` | Arguments to pass to the executable |
-| `extraArgs` | `Record` | `{}` | Additional arguments |
-| `fallbackModel` | `string` | `undefined` | Model to use if primary fails |
-| `forkSession` | `boolean` | `false` | When resuming with `resume`, fork to a new session ID instead of continuing the original session |
-| `hooks` | `Partial>` | `{}` | Hook callbacks for events |
-| `includePartialMessages` | `boolean` | `false` | Include partial message events |
-| `maxThinkingTokens` | `number` | `undefined` | Maximum tokens for thinking process |
-| `maxTurns` | `number` | `undefined` | Maximum conversation turns |
-| `mcpServers` | `Record` | `{}` | MCP server configurations |
-| `model` | `string` | Default from CLI | Claude model to use |
-| `pathToClaudeCodeExecutable` | `string` | Auto-detected | Path to Claude Code executable |
-| `permissionMode` | [`PermissionMode`](#permissionmode) | `'default'` | Permission mode for the session |
-| `permissionPromptToolName` | `string` | `undefined` | MCP tool name for permission prompts |
-| `plugins` | [`SdkPluginConfig`](#sdkpluginconfig)`[]` | `[]` | Load custom plugins from local paths. See [Plugins](/en/docs/agent-sdk/plugins) for details |
-| `resume` | `string` | `undefined` | Session ID to resume |
-| `settingSources` | [`SettingSource`](#settingsource)`[]` | `[]` (no settings) | Control which filesystem settings to load. When omitted, no settings are loaded. **Note:** Must include `'project'` to load CLAUDE.md files |
-| `stderr` | `(data: string) => void` | `undefined` | Callback for stderr output |
-| `strictMcpConfig` | `boolean` | `false` | Enforce strict MCP validation |
-| `systemPrompt` | `string \| { type: 'preset'; preset: 'claude_code'; append?: string }` | `undefined` (empty prompt) | System prompt configuration. Pass a string for custom prompt, or `{ type: 'preset', preset: 'claude_code' }` to use Claude Code's system prompt. When using the preset object form, add `append` to extend the system prompt with additional instructions |
-
-### `Query`
-
-Interface returned by the `query()` function.
-
-```ts theme={null}
-interface Query extends AsyncGenerator {
- interrupt(): Promise;
- setPermissionMode(mode: PermissionMode): Promise;
-}
-```
-
-#### Methods
-
-| Method | Description |
-| :-------------------- | :------------------------------------------------------------------- |
-| `interrupt()` | Interrupts the query (only available in streaming input mode) |
-| `setPermissionMode()` | Changes the permission mode (only available in streaming input mode) |
-
-### `AgentDefinition`
-
-Configuration for a subagent defined programmatically.
-
-```ts theme={null}
-type AgentDefinition = {
- description: string;
- tools?: string[];
- prompt: string;
- model?: 'sonnet' | 'opus' | 'haiku' | 'inherit';
-}
-```
-
-| Field | Required | Description |
-| :------------ | :------- | :------------------------------------------------------------- |
-| `description` | Yes | Natural language description of when to use this agent |
-| `tools` | No | Array of allowed tool names. If omitted, inherits all tools |
-| `prompt` | Yes | The agent's system prompt |
-| `model` | No | Model override for this agent. If omitted, uses the main model |
-
-### `SettingSource`
-
-Controls which filesystem-based configuration sources the SDK loads settings from.
-
-```ts theme={null}
-type SettingSource = 'user' | 'project' | 'local';
-```
-
-| Value | Description | Location |
-| :---------- | :------------------------------------------- | :---------------------------- |
-| `'user'` | Global user settings | `~/.claude/settings.json` |
-| `'project'` | Shared project settings (version controlled) | `.claude/settings.json` |
-| `'local'` | Local project settings (gitignored) | `.claude/settings.local.json` |
-
-#### Default behavior
-
-When `settingSources` is **omitted** or **undefined**, the SDK does **not** load any filesystem settings. This provides isolation for SDK applications.
-
-#### Why use settingSources?
-
-**Load all filesystem settings (legacy behavior):**
-
-```typescript theme={null}
-// Load all settings like SDK v0.0.x did
-const result = query({
- prompt: "Analyze this code",
- options: {
- settingSources: ['user', 'project', 'local'] // Load all settings
- }
-});
-```
-
-**Load only specific setting sources:**
-
-```typescript theme={null}
-// Load only project settings, ignore user and local
-const result = query({
- prompt: "Run CI checks",
- options: {
- settingSources: ['project'] // Only .claude/settings.json
- }
-});
-```
-
-**Testing and CI environments:**
-
-```typescript theme={null}
-// Ensure consistent behavior in CI by excluding local settings
-const result = query({
- prompt: "Run tests",
- options: {
- settingSources: ['project'], // Only team-shared settings
- permissionMode: 'bypassPermissions'
- }
-});
-```
-
-**SDK-only applications:**
-
-```typescript theme={null}
-// Define everything programmatically (default behavior)
-// No filesystem dependencies - settingSources defaults to []
-const result = query({
- prompt: "Review this PR",
- options: {
- // settingSources: [] is the default, no need to specify
- agents: { /* ... */ },
- mcpServers: { /* ... */ },
- allowedTools: ['Read', 'Grep', 'Glob']
- }
-});
-```
-
-**Loading CLAUDE.md project instructions:**
-
-```typescript theme={null}
-// Load project settings to include CLAUDE.md files
-const result = query({
- prompt: "Add a new feature following project conventions",
- options: {
- systemPrompt: {
- type: 'preset',
- preset: 'claude_code' // Required to use CLAUDE.md
- },
- settingSources: ['project'], // Loads CLAUDE.md from project directory
- allowedTools: ['Read', 'Write', 'Edit']
- }
-});
-```
-
-#### Settings precedence
-
-When multiple sources are loaded, settings are merged with this precedence (highest to lowest):
-
-1. Local settings (`.claude/settings.local.json`)
-2. Project settings (`.claude/settings.json`)
-3. User settings (`~/.claude/settings.json`)
-
-Programmatic options (like `agents`, `allowedTools`) always override filesystem settings.
-
-### `PermissionMode`
-
-```ts theme={null}
-type PermissionMode =
- | 'default' // Standard permission behavior
- | 'acceptEdits' // Auto-accept file edits
- | 'bypassPermissions' // Bypass all permission checks
- | 'plan' // Planning mode - no execution
-```
-
-### `CanUseTool`
-
-Custom permission function type for controlling tool usage.
-
-```ts theme={null}
-type CanUseTool = (
- toolName: string,
- input: ToolInput,
- options: {
- signal: AbortSignal;
- suggestions?: PermissionUpdate[];
- }
-) => Promise;
-```
-
-### `PermissionResult`
-
-Result of a permission check.
-
-```ts theme={null}
-type PermissionResult =
- | {
- behavior: 'allow';
- updatedInput: ToolInput;
- updatedPermissions?: PermissionUpdate[];
- }
- | {
- behavior: 'deny';
- message: string;
- interrupt?: boolean;
- }
-```
-
-### `McpServerConfig`
-
-Configuration for MCP servers.
-
-```ts theme={null}
-type McpServerConfig =
- | McpStdioServerConfig
- | McpSSEServerConfig
- | McpHttpServerConfig
- | McpSdkServerConfigWithInstance;
-```
-
-#### `McpStdioServerConfig`
-
-```ts theme={null}
-type McpStdioServerConfig = {
- type?: 'stdio';
- command: string;
- args?: string[];
- env?: Record;
-}
-```
-
-#### `McpSSEServerConfig`
-
-```ts theme={null}
-type McpSSEServerConfig = {
- type: 'sse';
- url: string;
- headers?: Record;
-}
-```
-
-#### `McpHttpServerConfig`
-
-```ts theme={null}
-type McpHttpServerConfig = {
- type: 'http';
- url: string;
- headers?: Record;
-}
-```
-
-#### `McpSdkServerConfigWithInstance`
-
-```ts theme={null}
-type McpSdkServerConfigWithInstance = {
- type: 'sdk';
- name: string;
- instance: McpServer;
-}
-```
-
-### `SdkPluginConfig`
-
-Configuration for loading plugins in the SDK.
-
-```ts theme={null}
-type SdkPluginConfig = {
- type: 'local';
- path: string;
-}
-```
-
-| Field | Type | Description |
-| :----- | :-------- | :--------------------------------------------------------- |
-| `type` | `'local'` | Must be `'local'` (only local plugins currently supported) |
-| `path` | `string` | Absolute or relative path to the plugin directory |
-
-**Example:**
-
-```ts theme={null}
-plugins: [
- { type: 'local', path: './my-plugin' },
- { type: 'local', path: '/absolute/path/to/plugin' }
-]
-```
-
-For complete information on creating and using plugins, see [Plugins](/en/docs/agent-sdk/plugins).
-
-## Message Types
-
-### `SDKMessage`
-
-Union type of all possible messages returned by the query.
-
-```ts theme={null}
-type SDKMessage =
- | SDKAssistantMessage
- | SDKUserMessage
- | SDKUserMessageReplay
- | SDKResultMessage
- | SDKSystemMessage
- | SDKPartialAssistantMessage
- | SDKCompactBoundaryMessage;
-```
-
-### `SDKAssistantMessage`
-
-Assistant response message.
-
-```ts theme={null}
-type SDKAssistantMessage = {
- type: 'assistant';
- uuid: UUID;
- session_id: string;
- message: APIAssistantMessage; // From Anthropic SDK
- parent_tool_use_id: string | null;
-}
-```
-
-### `SDKUserMessage`
-
-User input message.
-
-```ts theme={null}
-type SDKUserMessage = {
- type: 'user';
- uuid?: UUID;
- session_id: string;
- message: APIUserMessage; // From Anthropic SDK
- parent_tool_use_id: string | null;
-}
-```
-
-### `SDKUserMessageReplay`
-
-Replayed user message with required UUID.
-
-```ts theme={null}
-type SDKUserMessageReplay = {
- type: 'user';
- uuid: UUID;
- session_id: string;
- message: APIUserMessage;
- parent_tool_use_id: string | null;
-}
-```
-
-### `SDKResultMessage`
-
-Final result message.
-
-```ts theme={null}
-type SDKResultMessage =
- | {
- type: 'result';
- subtype: 'success';
- uuid: UUID;
- session_id: string;
- duration_ms: number;
- duration_api_ms: number;
- is_error: boolean;
- num_turns: number;
- result: string;
- total_cost_usd: number;
- usage: NonNullableUsage;
- permission_denials: SDKPermissionDenial[];
- }
- | {
- type: 'result';
- subtype: 'error_max_turns' | 'error_during_execution';
- uuid: UUID;
- session_id: string;
- duration_ms: number;
- duration_api_ms: number;
- is_error: boolean;
- num_turns: number;
- total_cost_usd: number;
- usage: NonNullableUsage;
- permission_denials: SDKPermissionDenial[];
- }
-```
-
-### `SDKSystemMessage`
-
-System initialization message.
-
-```ts theme={null}
-type SDKSystemMessage = {
- type: 'system';
- subtype: 'init';
- uuid: UUID;
- session_id: string;
- apiKeySource: ApiKeySource;
- cwd: string;
- tools: string[];
- mcp_servers: {
- name: string;
- status: string;
- }[];
- model: string;
- permissionMode: PermissionMode;
- slash_commands: string[];
- output_style: string;
-}
-```
-
-### `SDKPartialAssistantMessage`
-
-Streaming partial message (only when `includePartialMessages` is true).
-
-```ts theme={null}
-type SDKPartialAssistantMessage = {
- type: 'stream_event';
- event: RawMessageStreamEvent; // From Anthropic SDK
- parent_tool_use_id: string | null;
- uuid: UUID;
- session_id: string;
-}
-```
-
-### `SDKCompactBoundaryMessage`
-
-Message indicating a conversation compaction boundary.
-
-```ts theme={null}
-type SDKCompactBoundaryMessage = {
- type: 'system';
- subtype: 'compact_boundary';
- uuid: UUID;
- session_id: string;
- compact_metadata: {
- trigger: 'manual' | 'auto';
- pre_tokens: number;
- };
-}
-```
-
-### `SDKPermissionDenial`
-
-Information about a denied tool use.
-
-```ts theme={null}
-type SDKPermissionDenial = {
- tool_name: string;
- tool_use_id: string;
- tool_input: ToolInput;
-}
-```
-
-## Hook Types
-
-### `HookEvent`
-
-Available hook events.
-
-```ts theme={null}
-type HookEvent =
- | 'PreToolUse'
- | 'PostToolUse'
- | 'Notification'
- | 'UserPromptSubmit'
- | 'SessionStart'
- | 'SessionEnd'
- | 'Stop'
- | 'SubagentStop'
- | 'PreCompact';
-```
-
-### `HookCallback`
-
-Hook callback function type.
-
-```ts theme={null}
-type HookCallback = (
- input: HookInput, // Union of all hook input types
- toolUseID: string | undefined,
- options: { signal: AbortSignal }
-) => Promise;
-```
-
-### `HookCallbackMatcher`
-
-Hook configuration with optional matcher.
-
-```ts theme={null}
-interface HookCallbackMatcher {
- matcher?: string;
- hooks: HookCallback[];
-}
-```
-
-### `HookInput`
-
-Union type of all hook input types.
-
-```ts theme={null}
-type HookInput =
- | PreToolUseHookInput
- | PostToolUseHookInput
- | NotificationHookInput
- | UserPromptSubmitHookInput
- | SessionStartHookInput
- | SessionEndHookInput
- | StopHookInput
- | SubagentStopHookInput
- | PreCompactHookInput;
-```
-
-### `BaseHookInput`
-
-Base interface that all hook input types extend.
-
-```ts theme={null}
-type BaseHookInput = {
- session_id: string;
- transcript_path: string;
- cwd: string;
- permission_mode?: string;
-}
-```
-
-#### `PreToolUseHookInput`
-
-```ts theme={null}
-type PreToolUseHookInput = BaseHookInput & {
- hook_event_name: 'PreToolUse';
- tool_name: string;
- tool_input: ToolInput;
-}
-```
-
-#### `PostToolUseHookInput`
-
-```ts theme={null}
-type PostToolUseHookInput = BaseHookInput & {
- hook_event_name: 'PostToolUse';
- tool_name: string;
- tool_input: ToolInput;
- tool_response: ToolOutput;
-}
-```
-
-#### `NotificationHookInput`
-
-```ts theme={null}
-type NotificationHookInput = BaseHookInput & {
- hook_event_name: 'Notification';
- message: string;
- title?: string;
-}
-```
-
-#### `UserPromptSubmitHookInput`
-
-```ts theme={null}
-type UserPromptSubmitHookInput = BaseHookInput & {
- hook_event_name: 'UserPromptSubmit';
- prompt: string;
-}
-```
-
-#### `SessionStartHookInput`
-
-```ts theme={null}
-type SessionStartHookInput = BaseHookInput & {
- hook_event_name: 'SessionStart';
- source: 'startup' | 'resume' | 'clear' | 'compact';
-}
-```
-
-#### `SessionEndHookInput`
-
-```ts theme={null}
-type SessionEndHookInput = BaseHookInput & {
- hook_event_name: 'SessionEnd';
- reason: 'clear' | 'logout' | 'prompt_input_exit' | 'other';
-}
-```
-
-#### `StopHookInput`
-
-```ts theme={null}
-type StopHookInput = BaseHookInput & {
- hook_event_name: 'Stop';
- stop_hook_active: boolean;
-}
-```
-
-#### `SubagentStopHookInput`
-
-```ts theme={null}
-type SubagentStopHookInput = BaseHookInput & {
- hook_event_name: 'SubagentStop';
- stop_hook_active: boolean;
-}
-```
-
-#### `PreCompactHookInput`
-
-```ts theme={null}
-type PreCompactHookInput = BaseHookInput & {
- hook_event_name: 'PreCompact';
- trigger: 'manual' | 'auto';
- custom_instructions: string | null;
-}
-```
-
-### `HookJSONOutput`
-
-Hook return value.
-
-```ts theme={null}
-type HookJSONOutput = AsyncHookJSONOutput | SyncHookJSONOutput;
-```
-
-#### `AsyncHookJSONOutput`
-
-```ts theme={null}
-type AsyncHookJSONOutput = {
- async: true;
- asyncTimeout?: number;
-}
-```
-
-#### `SyncHookJSONOutput`
-
-```ts theme={null}
-type SyncHookJSONOutput = {
- continue?: boolean;
- suppressOutput?: boolean;
- stopReason?: string;
- decision?: 'approve' | 'block';
- systemMessage?: string;
- reason?: string;
- hookSpecificOutput?:
- | {
- hookEventName: 'PreToolUse';
- permissionDecision?: 'allow' | 'deny' | 'ask';
- permissionDecisionReason?: string;
- }
- | {
- hookEventName: 'UserPromptSubmit';
- additionalContext?: string;
- }
- | {
- hookEventName: 'SessionStart';
- additionalContext?: string;
- }
- | {
- hookEventName: 'PostToolUse';
- additionalContext?: string;
- };
-}
-```
-
-## Tool Input Types
-
-Documentation of input schemas for all built-in Claude Code tools. These types are exported from `@anthropic-ai/claude-agent-sdk` and can be used for type-safe tool interactions.
-
-### `ToolInput`
-
-**Note:** This is a documentation-only type for clarity. It represents the union of all tool input types.
-
-```ts theme={null}
-type ToolInput =
- | AgentInput
- | BashInput
- | BashOutputInput
- | FileEditInput
- | FileReadInput
- | FileWriteInput
- | GlobInput
- | GrepInput
- | KillShellInput
- | NotebookEditInput
- | WebFetchInput
- | WebSearchInput
- | TodoWriteInput
- | ExitPlanModeInput
- | ListMcpResourcesInput
- | ReadMcpResourceInput;
-```
-
-### Task
-
-**Tool name:** `Task`
-
-```ts theme={null}
-interface AgentInput {
- /**
- * A short (3-5 word) description of the task
- */
- description: string;
- /**
- * The task for the agent to perform
- */
- prompt: string;
- /**
- * The type of specialized agent to use for this task
- */
- subagent_type: string;
-}
-```
-
-Launches a new agent to handle complex, multi-step tasks autonomously.
-
-### Bash
-
-**Tool name:** `Bash`
-
-```ts theme={null}
-interface BashInput {
- /**
- * The command to execute
- */
- command: string;
- /**
- * Optional timeout in milliseconds (max 600000)
- */
- timeout?: number;
- /**
- * Clear, concise description of what this command does in 5-10 words
- */
- description?: string;
- /**
- * Set to true to run this command in the background
- */
- run_in_background?: boolean;
-}
-```
-
-Executes bash commands in a persistent shell session with optional timeout and background execution.
-
-### BashOutput
-
-**Tool name:** `BashOutput`
-
-```ts theme={null}
-interface BashOutputInput {
- /**
- * The ID of the background shell to retrieve output from
- */
- bash_id: string;
- /**
- * Optional regex to filter output lines
- */
- filter?: string;
-}
-```
-
-Retrieves output from a running or completed background bash shell.
-
-### Edit
-
-**Tool name:** `Edit`
-
-```ts theme={null}
-interface FileEditInput {
- /**
- * The absolute path to the file to modify
- */
- file_path: string;
- /**
- * The text to replace
- */
- old_string: string;
- /**
- * The text to replace it with (must be different from old_string)
- */
- new_string: string;
- /**
- * Replace all occurrences of old_string (default false)
- */
- replace_all?: boolean;
-}
-```
-
-Performs exact string replacements in files.
-
-### Read
-
-**Tool name:** `Read`
-
-```ts theme={null}
-interface FileReadInput {
- /**
- * The absolute path to the file to read
- */
- file_path: string;
- /**
- * The line number to start reading from
- */
- offset?: number;
- /**
- * The number of lines to read
- */
- limit?: number;
-}
-```
-
-Reads files from the local filesystem, including text, images, PDFs, and Jupyter notebooks.
-
-### Write
-
-**Tool name:** `Write`
-
-```ts theme={null}
-interface FileWriteInput {
- /**
- * The absolute path to the file to write
- */
- file_path: string;
- /**
- * The content to write to the file
- */
- content: string;
-}
-```
-
-Writes a file to the local filesystem, overwriting if it exists.
-
-### Glob
-
-**Tool name:** `Glob`
-
-```ts theme={null}
-interface GlobInput {
- /**
- * The glob pattern to match files against
- */
- pattern: string;
- /**
- * The directory to search in (defaults to cwd)
- */
- path?: string;
-}
-```
-
-Fast file pattern matching that works with any codebase size.
-
-### Grep
-
-**Tool name:** `Grep`
-
-```ts theme={null}
-interface GrepInput {
- /**
- * The regular expression pattern to search for
- */
- pattern: string;
- /**
- * File or directory to search in (defaults to cwd)
- */
- path?: string;
- /**
- * Glob pattern to filter files (e.g. "*.js")
- */
- glob?: string;
- /**
- * File type to search (e.g. "js", "py", "rust")
- */
- type?: string;
- /**
- * Output mode: "content", "files_with_matches", or "count"
- */
- output_mode?: 'content' | 'files_with_matches' | 'count';
- /**
- * Case insensitive search
- */
- '-i'?: boolean;
- /**
- * Show line numbers (for content mode)
- */
- '-n'?: boolean;
- /**
- * Lines to show before each match
- */
- '-B'?: number;
- /**
- * Lines to show after each match
- */
- '-A'?: number;
- /**
- * Lines to show before and after each match
- */
- '-C'?: number;
- /**
- * Limit output to first N lines/entries
- */
- head_limit?: number;
- /**
- * Enable multiline mode
- */
- multiline?: boolean;
-}
-```
-
-Powerful search tool built on ripgrep with regex support.
-
-### KillBash
-
-**Tool name:** `KillBash`
-
-```ts theme={null}
-interface KillShellInput {
- /**
- * The ID of the background shell to kill
- */
- shell_id: string;
-}
-```
-
-Kills a running background bash shell by its ID.
-
-### NotebookEdit
-
-**Tool name:** `NotebookEdit`
-
-```ts theme={null}
-interface NotebookEditInput {
- /**
- * The absolute path to the Jupyter notebook file
- */
- notebook_path: string;
- /**
- * The ID of the cell to edit
- */
- cell_id?: string;
- /**
- * The new source for the cell
- */
- new_source: string;
- /**
- * The type of the cell (code or markdown)
- */
- cell_type?: 'code' | 'markdown';
- /**
- * The type of edit (replace, insert, delete)
- */
- edit_mode?: 'replace' | 'insert' | 'delete';
-}
-```
-
-Edits cells in Jupyter notebook files.
-
-### WebFetch
-
-**Tool name:** `WebFetch`
-
-```ts theme={null}
-interface WebFetchInput {
- /**
- * The URL to fetch content from
- */
- url: string;
- /**
- * The prompt to run on the fetched content
- */
- prompt: string;
-}
-```
-
-Fetches content from a URL and processes it with an AI model.
-
-### WebSearch
-
-**Tool name:** `WebSearch`
-
-```ts theme={null}
-interface WebSearchInput {
- /**
- * The search query to use
- */
- query: string;
- /**
- * Only include results from these domains
- */
- allowed_domains?: string[];
- /**
- * Never include results from these domains
- */
- blocked_domains?: string[];
-}
-```
-
-Searches the web and returns formatted results.
-
-### TodoWrite
-
-**Tool name:** `TodoWrite`
-
-```ts theme={null}
-interface TodoWriteInput {
- /**
- * The updated todo list
- */
- todos: Array<{
- /**
- * The task description
- */
- content: string;
- /**
- * The task status
- */
- status: 'pending' | 'in_progress' | 'completed';
- /**
- * Active form of the task description
- */
- activeForm: string;
- }>;
-}
-```
-
-Creates and manages a structured task list for tracking progress.
-
-### ExitPlanMode
-
-**Tool name:** `ExitPlanMode`
-
-```ts theme={null}
-interface ExitPlanModeInput {
- /**
- * The plan to run by the user for approval
- */
- plan: string;
-}
-```
-
-Exits planning mode and prompts the user to approve the plan.
-
-### ListMcpResources
-
-**Tool name:** `ListMcpResources`
-
-```ts theme={null}
-interface ListMcpResourcesInput {
- /**
- * Optional server name to filter resources by
- */
- server?: string;
-}
-```
-
-Lists available MCP resources from connected servers.
-
-### ReadMcpResource
-
-**Tool name:** `ReadMcpResource`
-
-```ts theme={null}
-interface ReadMcpResourceInput {
- /**
- * The MCP server name
- */
- server: string;
- /**
- * The resource URI to read
- */
- uri: string;
-}
-```
-
-Reads a specific MCP resource from a server.
-
-## Tool Output Types
-
-Documentation of output schemas for all built-in Claude Code tools. These types represent the actual response data returned by each tool.
-
-### `ToolOutput`
-
-**Note:** This is a documentation-only type for clarity. It represents the union of all tool output types.
-
-```ts theme={null}
-type ToolOutput =
- | TaskOutput
- | BashOutput
- | BashOutputToolOutput
- | EditOutput
- | ReadOutput
- | WriteOutput
- | GlobOutput
- | GrepOutput
- | KillBashOutput
- | NotebookEditOutput
- | WebFetchOutput
- | WebSearchOutput
- | TodoWriteOutput
- | ExitPlanModeOutput
- | ListMcpResourcesOutput
- | ReadMcpResourceOutput;
-```
-
-### Task
-
-**Tool name:** `Task`
-
-```ts theme={null}
-interface TaskOutput {
- /**
- * Final result message from the subagent
- */
- result: string;
- /**
- * Token usage statistics
- */
- usage?: {
- input_tokens: number;
- output_tokens: number;
- cache_creation_input_tokens?: number;
- cache_read_input_tokens?: number;
- };
- /**
- * Total cost in USD
- */
- total_cost_usd?: number;
- /**
- * Execution duration in milliseconds
- */
- duration_ms?: number;
-}
-```
-
-Returns the final result from the subagent after completing the delegated task.
-
-### Bash
-
-**Tool name:** `Bash`
-
-```ts theme={null}
-interface BashOutput {
- /**
- * Combined stdout and stderr output
- */
- output: string;
- /**
- * Exit code of the command
- */
- exitCode: number;
- /**
- * Whether the command was killed due to timeout
- */
- killed?: boolean;
- /**
- * Shell ID for background processes
- */
- shellId?: string;
-}
-```
-
-Returns command output with exit status. Background commands return immediately with a shellId.
-
-### BashOutput
-
-**Tool name:** `BashOutput`
-
-```ts theme={null}
-interface BashOutputToolOutput {
- /**
- * New output since last check
- */
- output: string;
- /**
- * Current shell status
- */
- status: 'running' | 'completed' | 'failed';
- /**
- * Exit code (when completed)
- */
- exitCode?: number;
-}
-```
-
-Returns incremental output from background shells.
-
-### Edit
-
-**Tool name:** `Edit`
-
-```ts theme={null}
-interface EditOutput {
- /**
- * Confirmation message
- */
- message: string;
- /**
- * Number of replacements made
- */
- replacements: number;
- /**
- * File path that was edited
- */
- file_path: string;
-}
-```
-
-Returns confirmation of successful edits with replacement count.
-
-### Read
-
-**Tool name:** `Read`
-
-```ts theme={null}
-type ReadOutput =
- | TextFileOutput
- | ImageFileOutput
- | PDFFileOutput
- | NotebookFileOutput;
-
-interface TextFileOutput {
- /**
- * File contents with line numbers
- */
- content: string;
- /**
- * Total number of lines in file
- */
- total_lines: number;
- /**
- * Lines actually returned
- */
- lines_returned: number;
-}
-
-interface ImageFileOutput {
- /**
- * Base64 encoded image data
- */
- image: string;
- /**
- * Image MIME type
- */
- mime_type: string;
- /**
- * File size in bytes
- */
- file_size: number;
-}
-
-interface PDFFileOutput {
- /**
- * Array of page contents
- */
- pages: Array<{
- page_number: number;
- text?: string;
- images?: Array<{
- image: string;
- mime_type: string;
- }>;
- }>;
- /**
- * Total number of pages
- */
- total_pages: number;
-}
-
-interface NotebookFileOutput {
- /**
- * Jupyter notebook cells
- */
- cells: Array<{
- cell_type: 'code' | 'markdown';
- source: string;
- outputs?: any[];
- execution_count?: number;
- }>;
- /**
- * Notebook metadata
- */
- metadata?: Record;
-}
-```
-
-Returns file contents in format appropriate to file type.
-
-### Write
-
-**Tool name:** `Write`
-
-```ts theme={null}
-interface WriteOutput {
- /**
- * Success message
- */
- message: string;
- /**
- * Number of bytes written
- */
- bytes_written: number;
- /**
- * File path that was written
- */
- file_path: string;
-}
-```
-
-Returns confirmation after successfully writing the file.
-
-### Glob
-
-**Tool name:** `Glob`
-
-```ts theme={null}
-interface GlobOutput {
- /**
- * Array of matching file paths
- */
- matches: string[];
- /**
- * Number of matches found
- */
- count: number;
- /**
- * Search directory used
- */
- search_path: string;
-}
-```
-
-Returns file paths matching the glob pattern, sorted by modification time.
-
-### Grep
-
-**Tool name:** `Grep`
-
-```ts theme={null}
-type GrepOutput =
- | GrepContentOutput
- | GrepFilesOutput
- | GrepCountOutput;
-
-interface GrepContentOutput {
- /**
- * Matching lines with context
- */
- matches: Array<{
- file: string;
- line_number?: number;
- line: string;
- before_context?: string[];
- after_context?: string[];
- }>;
- /**
- * Total number of matches
- */
- total_matches: number;
-}
-
-interface GrepFilesOutput {
- /**
- * Files containing matches
- */
- files: string[];
- /**
- * Number of files with matches
- */
- count: number;
-}
-
-interface GrepCountOutput {
- /**
- * Match counts per file
- */
- counts: Array<{
- file: string;
- count: number;
- }>;
- /**
- * Total matches across all files
- */
- total: number;
-}
-```
-
-Returns search results in the format specified by output\_mode.
-
-### KillBash
-
-**Tool name:** `KillBash`
-
-```ts theme={null}
-interface KillBashOutput {
- /**
- * Success message
- */
- message: string;
- /**
- * ID of the killed shell
- */
- shell_id: string;
-}
-```
-
-Returns confirmation after terminating the background shell.
-
-### NotebookEdit
-
-**Tool name:** `NotebookEdit`
-
-```ts theme={null}
-interface NotebookEditOutput {
- /**
- * Success message
- */
- message: string;
- /**
- * Type of edit performed
- */
- edit_type: 'replaced' | 'inserted' | 'deleted';
- /**
- * Cell ID that was affected
- */
- cell_id?: string;
- /**
- * Total cells in notebook after edit
- */
- total_cells: number;
-}
-```
-
-Returns confirmation after modifying the Jupyter notebook.
-
-### WebFetch
-
-**Tool name:** `WebFetch`
-
-```ts theme={null}
-interface WebFetchOutput {
- /**
- * AI model's response to the prompt
- */
- response: string;
- /**
- * URL that was fetched
- */
- url: string;
- /**
- * Final URL after redirects
- */
- final_url?: string;
- /**
- * HTTP status code
- */
- status_code?: number;
-}
-```
-
-Returns the AI's analysis of the fetched web content.
-
-### WebSearch
-
-**Tool name:** `WebSearch`
-
-```ts theme={null}
-interface WebSearchOutput {
- /**
- * Search results
- */
- results: Array<{
- title: string;
- url: string;
- snippet: string;
- /**
- * Additional metadata if available
- */
- metadata?: Record;
- }>;
- /**
- * Total number of results
- */
- total_results: number;
- /**
- * The query that was searched
- */
- query: string;
-}
-```
-
-Returns formatted search results from the web.
-
-### TodoWrite
-
-**Tool name:** `TodoWrite`
-
-```ts theme={null}
-interface TodoWriteOutput {
- /**
- * Success message
- */
- message: string;
- /**
- * Current todo statistics
- */
- stats: {
- total: number;
- pending: number;
- in_progress: number;
- completed: number;
- };
-}
-```
-
-Returns confirmation with current task statistics.
-
-### ExitPlanMode
-
-**Tool name:** `ExitPlanMode`
-
-```ts theme={null}
-interface ExitPlanModeOutput {
- /**
- * Confirmation message
- */
- message: string;
- /**
- * Whether user approved the plan
- */
- approved?: boolean;
-}
-```
-
-Returns confirmation after exiting plan mode.
-
-### ListMcpResources
-
-**Tool name:** `ListMcpResources`
-
-```ts theme={null}
-interface ListMcpResourcesOutput {
- /**
- * Available resources
- */
- resources: Array<{
- uri: string;
- name: string;
- description?: string;
- mimeType?: string;
- server: string;
- }>;
- /**
- * Total number of resources
- */
- total: number;
-}
-```
-
-Returns list of available MCP resources.
-
-### ReadMcpResource
-
-**Tool name:** `ReadMcpResource`
-
-```ts theme={null}
-interface ReadMcpResourceOutput {
- /**
- * Resource contents
- */
- contents: Array<{
- uri: string;
- mimeType?: string;
- text?: string;
- blob?: string;
- }>;
- /**
- * Server that provided the resource
- */
- server: string;
-}
-```
-
-Returns the contents of the requested MCP resource.
-
-## Permission Types
-
-### `PermissionUpdate`
-
-Operations for updating permissions.
-
-```ts theme={null}
-type PermissionUpdate =
- | {
- type: 'addRules';
- rules: PermissionRuleValue[];
- behavior: PermissionBehavior;
- destination: PermissionUpdateDestination;
- }
- | {
- type: 'replaceRules';
- rules: PermissionRuleValue[];
- behavior: PermissionBehavior;
- destination: PermissionUpdateDestination;
- }
- | {
- type: 'removeRules';
- rules: PermissionRuleValue[];
- behavior: PermissionBehavior;
- destination: PermissionUpdateDestination;
- }
- | {
- type: 'setMode';
- mode: PermissionMode;
- destination: PermissionUpdateDestination;
- }
- | {
- type: 'addDirectories';
- directories: string[];
- destination: PermissionUpdateDestination;
- }
- | {
- type: 'removeDirectories';
- directories: string[];
- destination: PermissionUpdateDestination;
- }
-```
-
-### `PermissionBehavior`
-
-```ts theme={null}
-type PermissionBehavior = 'allow' | 'deny' | 'ask';
-```
-
-### `PermissionUpdateDestination`
-
-```ts theme={null}
-type PermissionUpdateDestination =
- | 'userSettings' // Global user settings
- | 'projectSettings' // Per-directory project settings
- | 'localSettings' // Gitignored local settings
- | 'session' // Current session only
-```
-
-### `PermissionRuleValue`
-
-```ts theme={null}
-type PermissionRuleValue = {
- toolName: string;
- ruleContent?: string;
-}
-```
-
-## Other Types
-
-### `ApiKeySource`
-
-```ts theme={null}
-type ApiKeySource = 'user' | 'project' | 'org' | 'temporary';
-```
-
-### `ConfigScope`
-
-```ts theme={null}
-type ConfigScope = 'local' | 'user' | 'project';
-```
-
-### `NonNullableUsage`
-
-A version of [`Usage`](#usage) with all nullable fields made non-nullable.
-
-```ts theme={null}
-type NonNullableUsage = {
- [K in keyof Usage]: NonNullable;
-}
-```
-
-### `Usage`
-
-Token usage statistics (from `@anthropic-ai/sdk`).
-
-```ts theme={null}
-type Usage = {
- input_tokens: number | null;
- output_tokens: number | null;
- cache_creation_input_tokens?: number | null;
- cache_read_input_tokens?: number | null;
-}
-```
-
-### `CallToolResult`
-
-MCP tool result type (from `@modelcontextprotocol/sdk/types.js`).
-
-```ts theme={null}
-type CallToolResult = {
- content: Array<{
- type: 'text' | 'image' | 'resource';
- // Additional fields vary by type
- }>;
- isError?: boolean;
-}
-```
-
-### `AbortError`
-
-Custom error class for abort operations.
-
-```ts theme={null}
-class AbortError extends Error {}
-```
-
-## See also
-
-* [SDK overview](/en/docs/agent-sdk/overview) - General SDK concepts
-* [Python SDK reference](/en/docs/agent-sdk/python) - Python SDK documentation
-* [CLI reference](https://code.claude.com/docs/en/cli-reference) - Command-line interface
-* [Common workflows](https://code.claude.com/docs/en/common-workflows) - Step-by-step guides
diff --git a/docs/context/agent-skills-in-the-sdk.md b/docs/context/agent-skills-in-the-sdk.md
deleted file mode 100644
index 719661cf..00000000
--- a/docs/context/agent-skills-in-the-sdk.md
+++ /dev/null
@@ -1,302 +0,0 @@
-# Agent Skills in the SDK
-
-> Extend Claude with specialized capabilities using Agent Skills in the Claude Agent SDK
-
-## Overview
-
-Agent Skills extend Claude with specialized capabilities that Claude autonomously invokes when relevant. Skills are packaged as `SKILL.md` files containing instructions, descriptions, and optional supporting resources.
-
-For comprehensive information about Skills, including benefits, architecture, and authoring guidelines, see the [Agent Skills overview](/en/docs/agents-and-tools/agent-skills/overview).
-
-## How Skills Work with the SDK
-
-When using the Claude Agent SDK, Skills are:
-
-1. **Defined as filesystem artifacts**: Created as `SKILL.md` files in specific directories (`.claude/skills/`)
-2. **Loaded from filesystem**: Skills are loaded from configured filesystem locations. You must specify `settingSources` (TypeScript) or `setting_sources` (Python) to load Skills from the filesystem
-3. **Automatically discovered**: Once filesystem settings are loaded, Skill metadata is discovered at startup from user and project directories; full content loaded when triggered
-4. **Model-invoked**: Claude autonomously chooses when to use them based on context
-5. **Enabled via allowed\_tools**: Add `"Skill"` to your `allowed_tools` to enable Skills
-
-Unlike subagents (which can be defined programmatically), Skills must be created as filesystem artifacts. The SDK does not provide a programmatic API for registering Skills.
-
-
- **Default behavior**: By default, the SDK does not load any filesystem settings. To use Skills, you must explicitly configure `settingSources: ['user', 'project']` (TypeScript) or `setting_sources=["user", "project"]` (Python) in your options.
-
-
-## Using Skills with the SDK
-
-To use Skills with the SDK, you need to:
-
-1. Include `"Skill"` in your `allowed_tools` configuration
-2. Configure `settingSources`/`setting_sources` to load Skills from the filesystem
-
-Once configured, Claude automatically discovers Skills from the specified directories and invokes them when relevant to the user's request.
-
-
- ```python Python theme={null}
- import asyncio
- from claude_agent_sdk import query, ClaudeAgentOptions
-
- async def main():
- options = ClaudeAgentOptions(
- cwd="/path/to/project", # Project with .claude/skills/
- setting_sources=["user", "project"], # Load Skills from filesystem
- allowed_tools=["Skill", "Read", "Write", "Bash"] # Enable Skill tool
- )
-
- async for message in query(
- prompt="Help me process this PDF document",
- options=options
- ):
- print(message)
-
- asyncio.run(main())
- ```
-
- ```typescript TypeScript theme={null}
- import { query } from "@anthropic-ai/claude-agent-sdk";
-
- for await (const message of query({
- prompt: "Help me process this PDF document",
- options: {
- cwd: "/path/to/project", // Project with .claude/skills/
- settingSources: ["user", "project"], // Load Skills from filesystem
- allowedTools: ["Skill", "Read", "Write", "Bash"] // Enable Skill tool
- }
- })) {
- console.log(message);
- }
- ```
-
-
-## Skill Locations
-
-Skills are loaded from filesystem directories based on your `settingSources`/`setting_sources` configuration:
-
-* **Project Skills** (`.claude/skills/`): Shared with your team via git - loaded when `setting_sources` includes `"project"`
-* **User Skills** (`~/.claude/skills/`): Personal Skills across all projects - loaded when `setting_sources` includes `"user"`
-* **Plugin Skills**: Bundled with installed Claude Code plugins
-
-## Creating Skills
-
-Skills are defined as directories containing a `SKILL.md` file with YAML frontmatter and Markdown content. The `description` field determines when Claude invokes your Skill.
-
-**Example directory structure**:
-
-```bash theme={null}
-.claude/skills/processing-pdfs/
-└── SKILL.md
-```
-
-For complete guidance on creating Skills, including SKILL.md structure, multi-file Skills, and examples, see:
-
-* [Agent Skills in Claude Code](https://code.claude.com/docs/skills): Complete guide with examples
-* [Agent Skills Best Practices](/en/docs/agents-and-tools/agent-skills/best-practices): Authoring guidelines and naming conventions
-
-## Tool Restrictions
-
-
- The `allowed-tools` frontmatter field in SKILL.md is only supported when using Claude Code CLI directly. **It does not apply when using Skills through the SDK**.
-
- When using the SDK, control tool access through the main `allowedTools` option in your query configuration.
-
-
-To restrict tools for Skills in SDK applications, use the `allowedTools` option:
-
-
- Import statements from the first example are assumed in the following code snippets.
-
-
-
- ```python Python theme={null}
- options = ClaudeAgentOptions(
- setting_sources=["user", "project"], # Load Skills from filesystem
- allowed_tools=["Skill", "Read", "Grep", "Glob"] # Restricted toolset
- )
-
- async for message in query(
- prompt="Analyze the codebase structure",
- options=options
- ):
- print(message)
- ```
-
- ```typescript TypeScript theme={null}
- // Skills can only use Read, Grep, and Glob tools
- for await (const message of query({
- prompt: "Analyze the codebase structure",
- options: {
- settingSources: ["user", "project"], // Load Skills from filesystem
- allowedTools: ["Skill", "Read", "Grep", "Glob"] // Restricted toolset
- }
- })) {
- console.log(message);
- }
- ```
-
-
-## Discovering Available Skills
-
-To see which Skills are available in your SDK application, simply ask Claude:
-
-
- ```python Python theme={null}
- options = ClaudeAgentOptions(
- setting_sources=["user", "project"], # Load Skills from filesystem
- allowed_tools=["Skill"]
- )
-
- async for message in query(
- prompt="What Skills are available?",
- options=options
- ):
- print(message)
- ```
-
- ```typescript TypeScript theme={null}
- for await (const message of query({
- prompt: "What Skills are available?",
- options: {
- settingSources: ["user", "project"], // Load Skills from filesystem
- allowedTools: ["Skill"]
- }
- })) {
- console.log(message);
- }
- ```
-
-
-Claude will list the available Skills based on your current working directory and installed plugins.
-
-## Testing Skills
-
-Test Skills by asking questions that match their descriptions:
-
-
- ```python Python theme={null}
- options = ClaudeAgentOptions(
- cwd="/path/to/project",
- setting_sources=["user", "project"], # Load Skills from filesystem
- allowed_tools=["Skill", "Read", "Bash"]
- )
-
- async for message in query(
- prompt="Extract text from invoice.pdf",
- options=options
- ):
- print(message)
- ```
-
- ```typescript TypeScript theme={null}
- for await (const message of query({
- prompt: "Extract text from invoice.pdf",
- options: {
- cwd: "/path/to/project",
- settingSources: ["user", "project"], // Load Skills from filesystem
- allowedTools: ["Skill", "Read", "Bash"]
- }
- })) {
- console.log(message);
- }
- ```
-
-
-Claude automatically invokes the relevant Skill if the description matches your request.
-
-## Troubleshooting
-
-### Skills Not Found
-
-**Check settingSources configuration**: Skills are only loaded when you explicitly configure `settingSources`/`setting_sources`. This is the most common issue:
-
-
- ```python Python theme={null}
- # Wrong - Skills won't be loaded
- options = ClaudeAgentOptions(
- allowed_tools=["Skill"]
- )
-
- # Correct - Skills will be loaded
- options = ClaudeAgentOptions(
- setting_sources=["user", "project"], # Required to load Skills
- allowed_tools=["Skill"]
- )
- ```
-
- ```typescript TypeScript theme={null}
- // Wrong - Skills won't be loaded
- const options = {
- allowedTools: ["Skill"]
- };
-
- // Correct - Skills will be loaded
- const options = {
- settingSources: ["user", "project"], // Required to load Skills
- allowedTools: ["Skill"]
- };
- ```
-
-
-For more details on `settingSources`/`setting_sources`, see the [TypeScript SDK reference](/en/docs/agent-sdk/typescript#settingsource) or [Python SDK reference](/en/docs/agent-sdk/python#settingsource).
-
-**Check working directory**: The SDK loads Skills relative to the `cwd` option. Ensure it points to a directory containing `.claude/skills/`:
-
-
- ```python Python theme={null}
- # Ensure your cwd points to the directory containing .claude/skills/
- options = ClaudeAgentOptions(
- cwd="/path/to/project", # Must contain .claude/skills/
- setting_sources=["user", "project"], # Required to load Skills
- allowed_tools=["Skill"]
- )
- ```
-
- ```typescript TypeScript theme={null}
- // Ensure your cwd points to the directory containing .claude/skills/
- const options = {
- cwd: "/path/to/project", // Must contain .claude/skills/
- settingSources: ["user", "project"], // Required to load Skills
- allowedTools: ["Skill"]
- };
- ```
-
-
-See the "Using Skills with the SDK" section above for the complete pattern.
-
-**Verify filesystem location**:
-
-```bash theme={null}
-# Check project Skills
-ls .claude/skills/*/SKILL.md
-
-# Check personal Skills
-ls ~/.claude/skills/*/SKILL.md
-```
-
-### Skill Not Being Used
-
-**Check the Skill tool is enabled**: Confirm `"Skill"` is in your `allowedTools`.
-
-**Check the description**: Ensure it's specific and includes relevant keywords. See [Agent Skills Best Practices](/en/docs/agents-and-tools/agent-skills/best-practices#writing-effective-descriptions) for guidance on writing effective descriptions.
-
-### Additional Troubleshooting
-
-For general Skills troubleshooting (YAML syntax, debugging, etc.), see the [Claude Code Skills troubleshooting section](https://code.claude.com/docs/skills#troubleshooting).
-
-## Related Documentation
-
-### Skills Guides
-
-* [Agent Skills in Claude Code](https://code.claude.com/docs/skills): Complete Skills guide with creation, examples, and troubleshooting
-* [Agent Skills Overview](/en/docs/agents-and-tools/agent-skills/overview): Conceptual overview, benefits, and architecture
-* [Agent Skills Best Practices](/en/docs/agents-and-tools/agent-skills/best-practices): Authoring guidelines for effective Skills
-* [Agent Skills Cookbook](https://github.com/anthropics/claude-cookbooks/tree/main/skills): Example Skills and templates
-
-### SDK Resources
-
-* [Subagents in the SDK](/en/docs/agent-sdk/subagents): Similar filesystem-based agents with programmatic options
-* [Slash Commands in the SDK](/en/docs/agent-sdk/slash-commands): User-invoked commands
-* [SDK Overview](/en/docs/agent-sdk/overview): General SDK concepts
-* [TypeScript SDK Reference](/en/docs/agent-sdk/typescript): Complete API documentation
-* [Python SDK Reference](/en/docs/agent-sdk/python): Complete API documentation
diff --git a/docs/context/agent-skills.md b/docs/context/agent-skills.md
deleted file mode 100644
index 926df512..00000000
--- a/docs/context/agent-skills.md
+++ /dev/null
@@ -1,607 +0,0 @@
-# Agent Skills
-
-> Create, manage, and share Skills to extend Claude's capabilities in Claude Code.
-
-This guide shows you how to create, use, and manage Agent Skills in Claude Code. Skills are modular capabilities that extend Claude's functionality through organized folders containing instructions, scripts, and resources.
-
-## Prerequisites
-
-* Claude Code version 1.0 or later
-* Basic familiarity with [Claude Code](/en/quickstart)
-
-## What are Agent Skills?
-
-Agent Skills package expertise into discoverable capabilities. Each Skill consists of a `SKILL.md` file with instructions that Claude reads when relevant, plus optional supporting files like scripts and templates.
-
-**How Skills are invoked**: Skills are **model-invoked**—Claude autonomously decides when to use them based on your request and the Skill's description. This is different from slash commands, which are **user-invoked** (you explicitly type `/command` to trigger them).
-
-**Benefits**:
-
-* Extend Claude's capabilities for your specific workflows
-* Share expertise across your team via git
-* Reduce repetitive prompting
-* Compose multiple Skills for complex tasks
-
-Learn more in the [Agent Skills overview](https://docs.claude.com/en/docs/agents-and-tools/agent-skills/overview).
-
-
- For a deep dive into the architecture and real-world applications of Agent Skills, read our engineering blog: [Equipping agents for the real world with Agent Skills](https://www.anthropic.com/engineering/equipping-agents-for-the-real-world-with-agent-skills).
-
-
-## Create a Skill
-
-Skills are stored as directories containing a `SKILL.md` file.
-
-### Personal Skills
-
-Personal Skills are available across all your projects. Store them in `~/.claude/skills/`:
-
-```bash theme={null}
-mkdir -p ~/.claude/skills/my-skill-name
-```
-
-**Use personal Skills for**:
-
-* Your individual workflows and preferences
-* Experimental Skills you're developing
-* Personal productivity tools
-
-### Project Skills
-
-Project Skills are shared with your team. Store them in `.claude/skills/` within your project:
-
-```bash theme={null}
-mkdir -p .claude/skills/my-skill-name
-```
-
-**Use project Skills for**:
-
-* Team workflows and conventions
-* Project-specific expertise
-* Shared utilities and scripts
-
-Project Skills are checked into git and automatically available to team members.
-
-### Plugin Skills
-
-Skills can also come from [Claude Code plugins](/en/plugins). Plugins may bundle Skills that are automatically available when the plugin is installed. These Skills work the same way as personal and project Skills.
-
-## Write SKILL.md
-
-Create a `SKILL.md` file with YAML frontmatter and Markdown content:
-
-```yaml theme={null}
----
-name: your-skill-name
-description: Brief description of what this Skill does and when to use it
----
-
-# Your Skill Name
-
-## Instructions
-Provide clear, step-by-step guidance for Claude.
-
-## Examples
-Show concrete examples of using this Skill.
-```
-
-**Field requirements**:
-
-* `name`: Must use lowercase letters, numbers, and hyphens only (max 64 characters)
-* `description`: Brief description of what the Skill does and when to use it (max 1024 characters)
-
-The `description` field is critical for Claude to discover when to use your Skill. It should include both what the Skill does and when Claude should use it.
-
-See the [best practices guide](https://docs.claude.com/en/docs/agents-and-tools/agent-skills/best-practices) for complete authoring guidance including validation rules.
-
-## Add supporting files
-
-Create additional files alongside SKILL.md:
-
-```
-my-skill/
-├── SKILL.md (required)
-├── reference.md (optional documentation)
-├── examples.md (optional examples)
-├── scripts/
-│ └── helper.py (optional utility)
-└── templates/
- └── template.txt (optional template)
-```
-
-Reference these files from SKILL.md:
-
-````markdown theme={null}
-For advanced usage, see [reference.md](reference.md).
-
-Run the helper script:
-```bash
-python scripts/helper.py input.txt
-```
-````
-
-Claude reads these files only when needed, using progressive disclosure to manage context efficiently.
-
-## Restrict tool access with allowed-tools
-
-Use the `allowed-tools` frontmatter field to limit which tools Claude can use when a Skill is active:
-
-```yaml theme={null}
----
-name: safe-file-reader
-description: Read files without making changes. Use when you need read-only file access.
-allowed-tools: Read, Grep, Glob
----
-
-# Safe File Reader
-
-This Skill provides read-only file access.
-
-## Instructions
-1. Use Read to view file contents
-2. Use Grep to search within files
-3. Use Glob to find files by pattern
-```
-
-When this Skill is active, Claude can only use the specified tools (Read, Grep, Glob) without needing to ask for permission. This is useful for:
-
-* Read-only Skills that shouldn't modify files
-* Skills with limited scope (e.g., only data analysis, no file writing)
-* Security-sensitive workflows where you want to restrict capabilities
-
-If `allowed-tools` is not specified, Claude will ask for permission to use tools as normal, following the standard permission model.
-
-
- `allowed-tools` is only supported for Skills in Claude Code.
-
-
-## View available Skills
-
-Skills are automatically discovered by Claude from three sources:
-
-* Personal Skills: `~/.claude/skills/`
-* Project Skills: `.claude/skills/`
-* Plugin Skills: bundled with installed plugins
-
-**To view all available Skills**, ask Claude directly:
-
-```
-What Skills are available?
-```
-
-or
-
-```
-List all available Skills
-```
-
-This will show all Skills from all sources, including plugin Skills.
-
-**To inspect a specific Skill**, you can also check the filesystem:
-
-```bash theme={null}
-# List personal Skills
-ls ~/.claude/skills/
-
-# List project Skills (if in a project directory)
-ls .claude/skills/
-
-# View a specific Skill's content
-cat ~/.claude/skills/my-skill/SKILL.md
-```
-
-## Test a Skill
-
-After creating a Skill, test it by asking questions that match your description.
-
-**Example**: If your description mentions "PDF files":
-
-```
-Can you help me extract text from this PDF?
-```
-
-Claude autonomously decides to use your Skill if it matches the request—you don't need to explicitly invoke it. The Skill activates automatically based on the context of your question.
-
-## Debug a Skill
-
-If Claude doesn't use your Skill, check these common issues:
-
-### Make description specific
-
-**Too vague**:
-
-```yaml theme={null}
-description: Helps with documents
-```
-
-**Specific**:
-
-```yaml theme={null}
-description: Extract text and tables from PDF files, fill forms, merge documents. Use when working with PDF files or when the user mentions PDFs, forms, or document extraction.
-```
-
-Include both what the Skill does and when to use it in the description.
-
-### Verify file path
-
-**Personal Skills**: `~/.claude/skills/skill-name/SKILL.md`
-**Project Skills**: `.claude/skills/skill-name/SKILL.md`
-
-Check the file exists:
-
-```bash theme={null}
-# Personal
-ls ~/.claude/skills/my-skill/SKILL.md
-
-# Project
-ls .claude/skills/my-skill/SKILL.md
-```
-
-### Check YAML syntax
-
-Invalid YAML prevents the Skill from loading. Verify the frontmatter:
-
-```bash theme={null}
-cat SKILL.md | head -n 10
-```
-
-Ensure:
-
-* Opening `---` on line 1
-* Closing `---` before Markdown content
-* Valid YAML syntax (no tabs, correct indentation)
-
-### View errors
-
-Run Claude Code with debug mode to see Skill loading errors:
-
-```bash theme={null}
-claude --debug
-```
-
-## Share Skills with your team
-
-**Recommended approach**: Distribute Skills through [plugins](/en/plugins).
-
-To share Skills via plugin:
-
-1. Create a plugin with Skills in the `skills/` directory
-2. Add the plugin to a marketplace
-3. Team members install the plugin
-
-For complete instructions, see [Add Skills to your plugin](/en/plugins#add-skills-to-your-plugin).
-
-You can also share Skills directly through project repositories:
-
-### Step 1: Add Skill to your project
-
-Create a project Skill:
-
-```bash theme={null}
-mkdir -p .claude/skills/team-skill
-# Create SKILL.md
-```
-
-### Step 2: Commit to git
-
-```bash theme={null}
-git add .claude/skills/
-git commit -m "Add team Skill for PDF processing"
-git push
-```
-
-### Step 3: Team members get Skills automatically
-
-When team members pull the latest changes, Skills are immediately available:
-
-```bash theme={null}
-git pull
-claude # Skills are now available
-```
-
-## Update a Skill
-
-Edit SKILL.md directly:
-
-```bash theme={null}
-# Personal Skill
-code ~/.claude/skills/my-skill/SKILL.md
-
-# Project Skill
-code .claude/skills/my-skill/SKILL.md
-```
-
-Changes take effect the next time you start Claude Code. If Claude Code is already running, restart it to load the updates.
-
-## Remove a Skill
-
-Delete the Skill directory:
-
-```bash theme={null}
-# Personal
-rm -rf ~/.claude/skills/my-skill
-
-# Project
-rm -rf .claude/skills/my-skill
-git commit -m "Remove unused Skill"
-```
-
-## Best practices
-
-### Keep Skills focused
-
-One Skill should address one capability:
-
-**Focused**:
-
-* "PDF form filling"
-* "Excel data analysis"
-* "Git commit messages"
-
-**Too broad**:
-
-* "Document processing" (split into separate Skills)
-* "Data tools" (split by data type or operation)
-
-### Write clear descriptions
-
-Help Claude discover when to use Skills by including specific triggers in your description:
-
-**Clear**:
-
-```yaml theme={null}
-description: Analyze Excel spreadsheets, create pivot tables, and generate charts. Use when working with Excel files, spreadsheets, or analyzing tabular data in .xlsx format.
-```
-
-**Vague**:
-
-```yaml theme={null}
-description: For files
-```
-
-### Test with your team
-
-Have teammates use Skills and provide feedback:
-
-* Does the Skill activate when expected?
-* Are the instructions clear?
-* Are there missing examples or edge cases?
-
-### Document Skill versions
-
-You can document Skill versions in your SKILL.md content to track changes over time. Add a version history section:
-
-```markdown theme={null}
-# My Skill
-
-## Version History
-- v2.0.0 (2025-10-01): Breaking changes to API
-- v1.1.0 (2025-09-15): Added new features
-- v1.0.0 (2025-09-01): Initial release
-```
-
-This helps team members understand what changed between versions.
-
-## Troubleshooting
-
-### Claude doesn't use my Skill
-
-**Symptom**: You ask a relevant question but Claude doesn't use your Skill.
-
-**Check**: Is the description specific enough?
-
-Vague descriptions make discovery difficult. Include both what the Skill does and when to use it, with key terms users would mention.
-
-**Too generic**:
-
-```yaml theme={null}
-description: Helps with data
-```
-
-**Specific**:
-
-```yaml theme={null}
-description: Analyze Excel spreadsheets, generate pivot tables, create charts. Use when working with Excel files, spreadsheets, or .xlsx files.
-```
-
-**Check**: Is the YAML valid?
-
-Run validation to check for syntax errors:
-
-```bash theme={null}
-# View frontmatter
-cat .claude/skills/my-skill/SKILL.md | head -n 15
-
-# Check for common issues
-# - Missing opening or closing ---
-# - Tabs instead of spaces
-# - Unquoted strings with special characters
-```
-
-**Check**: Is the Skill in the correct location?
-
-```bash theme={null}
-# Personal Skills
-ls ~/.claude/skills/*/SKILL.md
-
-# Project Skills
-ls .claude/skills/*/SKILL.md
-```
-
-### Skill has errors
-
-**Symptom**: The Skill loads but doesn't work correctly.
-
-**Check**: Are dependencies available?
-
-Claude will automatically install required dependencies (or ask for permission to install them) when it needs them.
-
-**Check**: Do scripts have execute permissions?
-
-```bash theme={null}
-chmod +x .claude/skills/my-skill/scripts/*.py
-```
-
-**Check**: Are file paths correct?
-
-Use forward slashes (Unix style) in all paths:
-
-**Correct**: `scripts/helper.py`
-**Wrong**: `scripts\helper.py` (Windows style)
-
-### Multiple Skills conflict
-
-**Symptom**: Claude uses the wrong Skill or seems confused between similar Skills.
-
-**Be specific in descriptions**: Help Claude choose the right Skill by using distinct trigger terms in your descriptions.
-
-Instead of:
-
-```yaml theme={null}
-# Skill 1
-description: For data analysis
-
-# Skill 2
-description: For analyzing data
-```
-
-Use:
-
-```yaml theme={null}
-# Skill 1
-description: Analyze sales data in Excel files and CRM exports. Use for sales reports, pipeline analysis, and revenue tracking.
-
-# Skill 2
-description: Analyze log files and system metrics data. Use for performance monitoring, debugging, and system diagnostics.
-```
-
-## Examples
-
-### Simple Skill (single file)
-
-```
-commit-helper/
-└── SKILL.md
-```
-
-```yaml theme={null}
----
-name: generating-commit-messages
-description: Generates clear commit messages from git diffs. Use when writing commit messages or reviewing staged changes.
----
-
-# Generating Commit Messages
-
-## Instructions
-
-1. Run `git diff --staged` to see changes
-2. I'll suggest a commit message with:
- - Summary under 50 characters
- - Detailed description
- - Affected components
-
-## Best practices
-
-- Use present tense
-- Explain what and why, not how
-```
-
-### Skill with tool permissions
-
-```
-code-reviewer/
-└── SKILL.md
-```
-
-```yaml theme={null}
----
-name: code-reviewer
-description: Review code for best practices and potential issues. Use when reviewing code, checking PRs, or analyzing code quality.
-allowed-tools: Read, Grep, Glob
----
-
-# Code Reviewer
-
-## Review checklist
-
-1. Code organization and structure
-2. Error handling
-3. Performance considerations
-4. Security concerns
-5. Test coverage
-
-## Instructions
-
-1. Read the target files using Read tool
-2. Search for patterns using Grep
-3. Find related files using Glob
-4. Provide detailed feedback on code quality
-```
-
-### Multi-file Skill
-
-```
-pdf-processing/
-├── SKILL.md
-├── FORMS.md
-├── REFERENCE.md
-└── scripts/
- ├── fill_form.py
- └── validate.py
-```
-
-**SKILL.md**:
-
-````yaml theme={null}
----
-name: pdf-processing
-description: Extract text, fill forms, merge PDFs. Use when working with PDF files, forms, or document extraction. Requires pypdf and pdfplumber packages.
----
-
-# PDF Processing
-
-## Quick start
-
-Extract text:
-```python
-import pdfplumber
-with pdfplumber.open("doc.pdf") as pdf:
- text = pdf.pages[0].extract_text()
-```
-
-For form filling, see [FORMS.md](FORMS.md).
-For detailed API reference, see [REFERENCE.md](REFERENCE.md).
-
-## Requirements
-
-Packages must be installed in your environment:
-```bash
-pip install pypdf pdfplumber
-```
-````
-
-
- List required packages in the description. Packages must be installed in your environment before Claude can use them.
-
-
-Claude loads additional files only when needed.
-
-## Next steps
-
-
-
- Write Skills that Claude can use effectively
-
-
-
- Learn how Skills work across Claude products
-
-
-
- Use Skills programmatically with TypeScript and Python
-
-
-
- Create your first Skill
-
-
diff --git a/docs/context/architecture/v3-to-v4.md b/docs/context/architecture/v3-to-v4.md
deleted file mode 100644
index d2fbe56e..00000000
--- a/docs/context/architecture/v3-to-v4.md
+++ /dev/null
@@ -1,987 +0,0 @@
-# Claude-Mem Architecture v3 to v4 Plan (✅ Completed)
-
-This file exists as a reference to explain the path forward from v3 to v4.
-
-## Core Purpose
-
-Create a lightweight, hook-driven memory system that captures important context during Claude Code sessions and makes it available in future sessions.
-
-**Principles:**
-- Hooks should be fast and non-blocking
-- SDK agent synthesizes observations, not just stores raw data
-- Storage should be simple and queryable
-- Users should never notice the memory system working
-
----
-
-## Understanding the Foundation
-
-### What Claude Code Hooks Actually Do
-
-**SessionStart Hook:**
-- Runs when Claude Code starts or resumes
-- Can inject context via stdout (plain text) OR JSON `additionalContext`
-- This is how we show "What's new" to Claude
-
-**UserPromptSubmit Hook:**
-- Runs BEFORE Claude processes the user's message
-- Can inject context via stdout OR JSON `additionalContext`
-- This is where we initialize per-session tracking
-
-**PostToolUse Hook:**
-- Runs AFTER each tool completes successfully
-- Gets both tool input and output
-- Runs in PARALLEL with other matching hooks
-- This is where we observe what Claude is doing
-
-**Stop Hook:**
-- Runs when main agent finishes (NOT on user interrupt)
-- This is where we finalize the session
-- Summary should be structured responses that answer the following:
- - What did user request?
- - What did you investigate?
- - What did you learn?
- - What did you do?
- - What's next?
- - Files read
- - Files edited
- - Notes
-
-### How SDK Streaming Actually Works
-
-**Streaming Input Mode (what we need):**
-- Persistent session with AsyncGenerator
-- Can queue multiple messages
-- Supports interruption via `interrupt()` method
-- Natural multi-turn conversations
-- The SDK maintains conversation state
-
-**Critical insight:** We use "Streaming Input Mode" which creates ONE long-running SDK session per Claude Code session, not multiple short sessions.
-
-**Session ID Management:**
-- Session IDs change with each turn of the conversation
-- Must capture session ID from the initial system message
-- SDK worker needs to track session ID updates continuously, not just capture once
-- The first message in the response stream is a system init message with the session_id
-
----
-
-## Architecture
-
-### Visual Overview
-
-```
-┌─────────────────────────────────────────────────────────────────┐
-│ CLAUDE CODE SESSION │
-│ (Main session - user interacting with Claude Code) │
-│ │
-│ User → Claude → Tools (Read, Edit, Write, Bash, etc.) │
-│ │ │
-│ │ PostToolUse Hook │
-│ ↓ │
-│ claude-mem save │
-│ (queues observation) │
-└─────────────────────────────────────────────────────────────────┘
- │
- │ SQLite observation_queue
- ↓
-┌─────────────────────────────────────────────────────────────────┐
-│ SDK WORKER PROCESS │
-│ (Background process - detached from main session) │
-│ │
-│ ┌─────────────────────────────────────────────┐ │
-│ │ Message Generator (AsyncIterable) │ │
-│ │ - Yields initial prompt │ │
-│ │ - Polls observation_queue │ │
-│ │ - Yields observation prompts │ │
-│ └─────────────────────────────────────────────┘ │
-│ ↓ │
-│ ┌─────────────────────────────────────────────┐ │
-│ │ SDK query() → Claude API │ │
-│ │ Model: claude-sonnet-4-5 │ │
-│ │ No tools needed (text-only synthesis) │ │
-│ └─────────────────────────────────────────────┘ │
-│ ↓ │
-│ ┌─────────────────────────────────────────────┐ │
-│ │ Response Handler │ │
-│ │ - Parses XML blocks │ │
-│ │ - Parses XML blocks │ │
-│ │ - Writes to SQLite tables │ │
-│ └─────────────────────────────────────────────┘ │
-└─────────────────────────────────────────────────────────────────┘
- │
- │ SQLite: observations, session_summaries
- ↓
-┌─────────────────────────────────────────────────────────────────┐
-│ NEXT CLAUDE CODE SESSION │
-│ │
-│ SessionStart Hook → claude-mem context │
-│ (Reads from SQLite and injects context) │
-└─────────────────────────────────────────────────────────────────┘
-```
-
-### What is the SDK agent's job?
-
-The SDK agent is a **synthesis engine**, not a data collector.
-
-It should:
-- Receive tool observations as they happen
-- Extract meaningful patterns and insights
-- Store atomic, searchable observations in SQLite
-- Synthesize a human-readable summary at the end
-
-It should NOT:
-- Store raw tool outputs
-- Try to capture everything
-- Make decisions about what Claude Code should do
-- Block or slow down the main session
-
-### Session Management Strategy
-
-**Built-in SDK Session Resumption:**
-
-The Agent SDK provides native session resumption capabilities. Instead of manually tracking and rebuilding session state, we can leverage the SDK's built-in features:
-
-```typescript
-// Resume a previous SDK session
-const resumedResponse = query({
- prompt: "Continue where we left off",
- options: {
- resume: sdkSessionId // Use the session ID captured from init message
- }
-});
-```
-
-**When to use session resumption:**
-- User interrupts Claude Code and resumes later
-- SDK worker crashes and needs to restart
-- Long-running observations that span multiple Claude Code sessions
-
-**Session state tracking:**
-- Store SDK session ID in database when captured from init message
-- Mark sessions as 'active', 'completed', 'interrupted', or 'failed'
-- Use session status to determine whether to resume or start fresh
-
-### How hooks run in parallel
-
-PostToolUse hooks run in parallel. Handle this by:
-- Make SDK agent calls async and fire-and-forget
-- Use the observation_queue SQLite table to serialize observations
-- SDK worker polls this queue and processes observations sequentially
-
-### What if the user interrupts Claude Code?
-
-Stop hook doesn't run on interrupts. So:
-- Observations stay in queue
-- Next session continues where left off
-- Mark session as 'interrupted' after 24h of inactivity
-
----
-
-## Database Schema
-
-```sql
--- Tracks SDK streaming sessions
-CREATE TABLE sdk_sessions (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- claude_session_id TEXT UNIQUE NOT NULL,
- sdk_session_id TEXT UNIQUE NOT NULL,
- project TEXT NOT NULL,
- user_prompt TEXT,
- started_at TEXT NOT NULL,
- started_at_epoch INTEGER NOT NULL,
- completed_at TEXT,
- completed_at_epoch INTEGER,
- status TEXT CHECK(status IN ('active', 'completed', 'failed'))
-);
-
--- Tracks pending observations (message queue)
-CREATE TABLE observation_queue (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- sdk_session_id TEXT NOT NULL,
- tool_name TEXT NOT NULL,
- tool_input TEXT NOT NULL, -- JSON
- tool_output TEXT NOT NULL, -- JSON
- created_at_epoch INTEGER NOT NULL,
- processed_at_epoch INTEGER,
- FOREIGN KEY(sdk_session_id) REFERENCES sdk_sessions(sdk_session_id)
-);
-
--- Stores extracted observations (what SDK decides is important)
-CREATE TABLE observations (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- sdk_session_id TEXT NOT NULL,
- project TEXT NOT NULL,
- text TEXT NOT NULL,
- type TEXT NOT NULL, -- 'decision' | 'bugfix' | 'feature' | 'refactor' | 'discovery'
- created_at TEXT NOT NULL,
- created_at_epoch INTEGER NOT NULL,
- FOREIGN KEY(sdk_session_id) REFERENCES sdk_sessions(sdk_session_id)
-);
-
-CREATE INDEX idx_observations_project ON observations(project);
-CREATE INDEX idx_observations_created ON observations(created_at_epoch DESC);
-
--- Stores session summaries
-CREATE TABLE session_summaries (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- sdk_session_id TEXT UNIQUE NOT NULL,
- project TEXT NOT NULL,
- summary TEXT NOT NULL,
- created_at TEXT NOT NULL,
- created_at_epoch INTEGER NOT NULL,
- FOREIGN KEY(sdk_session_id) REFERENCES sdk_sessions(sdk_session_id)
-);
-
-CREATE INDEX idx_summaries_project ON session_summaries(project);
-CREATE INDEX idx_summaries_created ON session_summaries(created_at_epoch DESC);
-```
-
----
-
-## Hook Implementation
-
-**IMPORTANT DISTINCTION:**
-
-There are TWO separate hook systems at play here:
-
-1. **Claude Code Hooks** - External command hooks configured in `~/.config/claude-code/settings.json`
- - These hooks observe the MAIN Claude Code session
- - They run as external commands (like `claude-mem save`)
- - This is what we use to capture observations from the user's session
-
-2. **SDK Hooks** - Programmatic hooks configured in TypeScript code via `HookMatcher`
- - These hooks would observe the MEMORY SDK agent's own tool usage
- - They run as TypeScript callbacks within the SDK worker process
- - We're NOT using these (yet) - they're a future enhancement
-
-**Our architecture:** Use Claude Code hooks (external commands) to observe the main session, and run a separate SDK worker process that doesn't need its own hooks.
-
-### 1. SessionStart Hook
-
-**Purpose:** Show user what happened in recent sessions
-
-**Claude Code Hook Config (in settings.json):**
-```json
-{
- "hooks": {
- "SessionStart": [{
- "matcher": "startup",
- "hooks": [{
- "type": "command",
- "command": "claude-mem context"
- }]
- }]
- }
-}
-```
-
-**Command: `claude-mem context`**
-
-Flow:
-1. Read stdin JSON (session_id, cwd, source, etc.)
-2. If source !== "startup", exit immediately
-3. Extract project from cwd basename
-4. Query SQLite for recent summaries:
- ```sql
- SELECT summary, created_at
- FROM session_summaries
- WHERE project = ?
- ORDER BY created_at_epoch DESC
- LIMIT 10
- ```
-5. Format results as human-readable text
-6. Output to stdout (Claude Code automatically injects this)
-7. Exit with code 0
-
-### 2. UserPromptSubmit Hook
-
-**Purpose:** Initialize SDK memory session in background
-
-**Hook config:**
-```json
-{
- "hooks": {
- "UserPromptSubmit": [{
- "hooks": [{
- "type": "command",
- "command": "claude-mem new"
- }]
- }]
- }
-}
-```
-
-**Command: `claude-mem new`**
-
-Flow:
-1. Read stdin JSON (session_id, prompt, cwd, etc.)
-2. Extract project from cwd
-3. Create SDK session record in database
-4. Start SDK session with initialization prompt in background process
-5. Save SDK session ID to database
-6. Output: `{"continue": true, "suppressOutput": true}`
-7. Exit immediately (SDK runs in background daemon/process)
-
-**The Background SDK Process:**
-
-The SDK session should run as a detached background process:
-```typescript
-// In claude-mem new
-const child = spawn('claude-mem', ['sdk-worker', session_id], {
- detached: true,
- stdio: 'ignore'
-});
-child.unref();
-```
-
-The SDK worker:
-```typescript
-// claude-mem sdk-worker
-import { query } from '@anthropic-ai/agent-sdk';
-import type { Query, UserMessage } from '@anthropic-ai/agent-sdk';
-
-async function runSDKWorker(sessionId: string) {
- const session = await loadSessionFromDB(sessionId);
-
- // Track the SDK session ID from the init message
- let sdkSessionId: string | undefined;
- const abortController = new AbortController();
-
- // Message generator yields UserMessage objects (role + content)
- // This matches the SDK's expected format for streaming input mode
- async function* messageGenerator(): AsyncIterable {
- // Initial prompt
- yield {
- role: "user",
- content: buildInitPrompt(session)
- };
-
- // Then listen for queued observations
- while (session.status === 'active' && !abortController.signal.aborted) {
- const observations = await pollObservationQueue(session.sdk_session_id);
-
- for (const obs of observations) {
- yield {
- role: "user",
- content: buildObservationPrompt(obs)
- };
- markObservationProcessed(obs.id);
- }
-
- await sleep(1000); // Poll every second
- }
- }
-
- // Run SDK session with proper streaming interface
- // The query function signature: query({ prompt, options }): Query
- const response: Query = query({
- prompt: messageGenerator(), // AsyncIterable
- options: {
- model: 'claude-sonnet-4-5', // Use documented model name
- disallowedTools: ['Glob', 'Grep', 'ListMcpResourcesTool', 'WebSearch'], // More efficient than filtering in hooks
- maxTurns: 1000,
- cwd: session.cwd,
- abortController: abortController
- }
- });
-
- try {
- // Consume responses from the Query AsyncGenerator
- for await (const msg of response) {
- // Capture session ID from system init message
- if (msg.type === 'system' && msg.subtype === 'init') {
- sdkSessionId = msg.session_id;
- console.log(`SDK session started with ID: ${sdkSessionId}`);
- // Store SDK session ID in database for potential resumption
- await updateSDKSessionId(session.id, sdkSessionId);
- }
-
- if (msg.type === 'text') {
- // Use an XML parser library (e.g., fast-xml-parser or similar) to parse observations and summaries
- // Parse blocks and call storeObservation(session_id, project, type, text)
- // Parse blocks, extract all 8 fields, format and call storeSummary(session_id, project, text)
-
- parseAndStoreObservations(msg.content, session);
- parseAndStoreSummary(msg.content, session);
- }
- }
- } catch (error) {
- if (abortController.signal.aborted) {
- console.log('SDK worker interrupted gracefully');
- } else {
- console.error('SDK worker error:', error);
- await markSessionFailed(session.id, error);
- }
- } finally {
- await cleanupSession(session.id);
- }
-}
-
-// Note: Query and UserMessage types are imported from '@anthropic-ai/agent-sdk'
-// Query extends AsyncGenerator and provides interrupt() and setPermissionMode() methods
-// UserMessage has the shape: { role: "user", content: string }
-```
-
-### 3. PostToolUse Hook
-
-**Purpose:** Queue tool observations for SDK processing
-
-**Hook config:**
-```json
-{
- "hooks": {
- "PostToolUse": [{
- "matcher": "*",
- "hooks": [{
- "type": "command",
- "command": "claude-mem save"
- }]
- }]
- }
-}
-```
-
-**Command: `claude-mem save`**
-
-Flow:
-1. Read stdin JSON (tool_name, tool_input, tool_output, etc.)
-2. Find active SDK session for this project
-3. Insert observation into queue:
- ```sql
- INSERT INTO observation_queue
- (sdk_session_id, tool_name, tool_input, tool_output, created_at_epoch)
- VALUES (?, ?, ?, ?, ?)
- ```
-4. Output: `{"continue": true, "suppressOutput": true}`
-5. Exit immediately
-
-**Observation Filtering:**
-
-Tool filtering is now handled at the SDK level (see SDK Worker section) using `disallowedTools` option. The hook no longer needs to filter tools manually - all tool observations that reach the hook are already relevant.
-
-However, you may still want to skip certain tools for performance reasons:
-```typescript
-// Optional: Skip very frequent or low-value tools
-const SKIP_TOOLS = new Set(['TodoWrite', 'ListMcpResourcesTool']);
-
-if (SKIP_TOOLS.has(tool_name)) {
- console.log('{"continue": true, "suppressOutput": true}');
- process.exit(0);
-}
-```
-
-**Note:** The SDK worker uses `disallowedTools: ['Glob', 'Grep', 'ListMcpResourcesTool', 'WebSearch']` which is more efficient than hook-level filtering because it prevents the SDK agent from using these tools entirely.
-
-### 4. Stop Hook
-
-**Purpose:** Signal SDK to finalize and generate summary
-
-**Hook config:**
-```json
-{
- "hooks": {
- "Stop": [{
- "hooks": [{
- "type": "command",
- "command": "claude-mem summary"
- }]
- }]
- }
-}
-```
-
-**Command: `claude-mem summary`**
-
-Flow:
-1. Read stdin JSON (session_id, cwd, etc.)
-2. Find active SDK session
-3. Insert special "FINALIZE" message into observation queue:
- ```sql
- INSERT INTO observation_queue
- (sdk_session_id, tool_name, tool_input, tool_output, created_at_epoch)
- VALUES (?, 'FINALIZE', '{}', '{}', ?)
- ```
-4. Output: `{"continue": true, "suppressOutput": true}`
-5. Exit immediately
-
-**SDK Worker Handling:**
-
-When SDK worker sees FINALIZE message:
-```typescript
-if (obs.tool_name === 'FINALIZE') {
- yield {
- type: "user",
- message: {
- role: "user",
- content: buildFinalizePrompt(session)
- }
- };
-
- // Wait for SDK to finish processing
- await waitForSDKCompletion();
-
- // Update session status
- await markSessionCompleted(session.id);
-
- // Exit worker
- break;
-}
-```
-
----
-
-## SDK Agent Prompts
-
-### Initialization Prompt
-
-```typescript
-function buildInitPrompt(project: string, sessionId: string, userPrompt: string): string {
- return `You are a memory assistant for the "${project}" project.
-
-SESSION CONTEXT
----------------
-Session ID: ${sessionId}
-User's Goal: ${userPrompt}
-Date: ${new Date().toISOString().split('T')[0]}
-
-YOUR ROLE
----------
-You will observe tool executions during this Claude Code session. Your job is to:
-
-1. Extract meaningful insights (not just raw data)
-2. Store atomic observations in SQLite
-3. Focus on: key decisions, patterns discovered, problems solved, technical insights
-
-WHAT TO CAPTURE
-----------------
-✓ Architecture decisions (e.g., "chose PostgreSQL over MongoDB for ACID guarantees")
-✓ Bug fixes (e.g., "fixed race condition in auth middleware by adding mutex")
-✓ New features (e.g., "implemented JWT refresh token flow")
-✓ Refactorings (e.g., "extracted validation logic into separate service")
-✓ Discoveries (e.g., "found that API rate limit is 100 req/min")
-
-✗ NOT routine operations (reading files, listing directories)
-✗ NOT work-in-progress (only completed work)
-✗ NOT obvious facts (e.g., "TypeScript file has types")
-
-HOW TO STORE OBSERVATIONS
---------------------------
-When you identify something worth remembering, output your observation in this EXACT XML format:
-
-\`\`\`xml
-
- feature
- Implemented JWT token refresh flow with 7-day expiry
-
-\`\`\`
-
-Valid types: decision, bugfix, feature, refactor, discovery
-
-Structure requirements:
-- is the root element
-- must be one of the 5 valid types (single word)
-- contains your concise observation (one sentence preferred)
-- No additional fields or nesting
-
-The SDK worker will parse all blocks from your response using regex and store them in SQLite.
-
-You can include your reasoning before or after the observation block, or just output the observation by itself.
-
-EXAMPLE
--------
-Bad: "Read src/auth.ts file"
-Good: "Implemented JWT token refresh flow with 7-day expiry"
-
-Wait for tool observations. Acknowledge this message briefly.`;
-}
-```
-
-### Observation Prompt
-
-```typescript
-function buildObservationPrompt(obs: Observation): string {
- return `TOOL OBSERVATION
-================
-Tool: ${obs.tool_name}
-Time: ${new Date(obs.created_at_epoch).toISOString()}
-
-Input:
-${JSON.stringify(JSON.parse(obs.tool_input), null, 2)}
-
-Output:
-${JSON.stringify(JSON.parse(obs.tool_output), null, 2)}
-
-ANALYSIS TASK
--------------
-1. Does this observation contain something worth remembering?
-2. If YES: Output the observation in this EXACT XML format:
-
- \`\`\`xml
-
- feature
- Your concise observation here
-
- \`\`\`
-
- Requirements:
- - Use one of these types: decision, bugfix, feature, refactor, discovery
- - Keep text concise (one sentence preferred)
- - No markdown formatting inside
- - No additional XML fields
-
-3. If NO: Just acknowledge and wait for next observation
-
-Remember: Quality over quantity. Only store meaningful insights.`;
-}
-```
-
-### Finalization Prompt
-
-```typescript
-function buildFinalizePrompt(session: SDKSession): string {
- return `SESSION ENDING
-==============
-The Claude Code session is finishing.
-
-FINAL TASK
-----------
-1. Review the observations you've stored this session
-2. Generate a structured summary that answers these questions:
- - What did user request?
- - What did you investigate?
- - What did you learn?
- - What did you do?
- - What's next?
- - Files read
- - Files edited
- - Notes
-
-3. Generate the structured summary and output it in this EXACT XML format:
-
-\`\`\`xml
-
- Implement JWT authentication system
- Existing auth middleware, session management, token storage patterns
- Current system uses session cookies; no JWT support; race condition in middleware
- Implemented JWT token + refresh flow with 7-day expiry; fixed race condition with mutex; added token validation middleware
- Add token revocation API endpoint; write integration tests
-
- src/auth.ts
- src/middleware/session.ts
- src/types/user.ts
-
-
- src/auth.ts
- src/middleware/auth.ts
- src/routes/auth.ts
-
- Token secret stored in .env; refresh tokens use rotation strategy
-
-\`\`\`
-
-Structure requirements:
-- is the root element
-- All 8 child elements are REQUIRED: request, investigated, learned, completed, next_steps, files_read, files_edited, notes
-- and must contain child elements (one per file)
-- If no files were read/edited, use empty tags:
-- Text fields can be multiple sentences but avoid markdown formatting
-- Use underscores in element names: next_steps, files_read, files_edited
-
-The SDK worker will parse the block and extract all fields to store in SQLite.
-
-Generate the summary now in the required XML format.`;
-}
-```
-
----
-
-## Hook Commands Architecture
-
-All four hook commands (`claude-mem context`, `claude-mem new`, `claude-mem save`, `claude-mem summary`) are implemented as standalone TypeScript functions that:
-
-1. **Use bun:sqlite directly** - No spawning child processes or CLI subcommands
-2. **Are self-contained** - Each hook has all the logic it needs
-3. **Share a common database layer** - Import from shared `db.ts` module
-4. **Never call other claude-mem commands** - All functionality via direct library calls
-
-```typescript
-// Example structure
-import { Database } from 'bun:sqlite';
-
-export function contextHook(stdin: HookInput) {
- const db = new Database('~/.claude-mem/db.sqlite');
- // Query and return context directly
- const summaries = db.query('SELECT ...').all();
- console.log(formatContext(summaries));
- db.close();
-}
-
-export function saveHook(stdin: HookInput) {
- const db = new Database('~/.claude-mem/db.sqlite');
- // Insert observation directly
- db.run('INSERT INTO observation_queue ...', params);
- db.close();
- console.log('{"continue": true, "suppressOutput": true}');
-}
-```
-
-**Key principle:** Hooks are fast, synchronous database operations. The SDK worker process is where async/complex logic happens.
-
----
-
-## Background Process Management
-
-The `claude-mem save` hook just queues observations - processing happens in the background SDK worker process that polls the queue continuously.
-
-The SDK worker is spawned by `claude-mem new` as a detached process and runs for the duration of the Claude Code session.
-
-Benefits:
-- Works on all platforms (no systemd/launchd needed)
-- Self-contained (spawned and managed by claude-mem itself)
-- Simple state management (all state in SQLite)
-
----
-
-## Advanced SDK Features
-
-### Permission Integration (Future Enhancement)
-
-The SDK provides a permission system that could be integrated with memory for context-aware decisions:
-
-```typescript
-canUseTool: async (toolName, input) => {
- // Check memory for previous decisions about this tool/context
- const previousDecisions = await queryMemoryForTool(toolName, input);
-
- if (previousDecisions.shouldAllow) {
- return {
- behavior: "allow",
- updatedInput: input
- };
- }
-
- return {
- behavior: "ask_user",
- message: `This tool was previously flagged. Allow anyway?`
- };
-}
-```
-
-This could enable:
-- Learning from previous tool use patterns
-- Automatically allowing/denying based on historical context
-- Providing smart defaults based on project-specific patterns
-
-**Implementation priority:** Low (add after core functionality is stable)
-
-### SDK Hook Configuration (Alternative to Claude Code Hooks)
-
-Instead of using external command hooks via Claude Code settings.json, the SDK supports native hook configuration:
-
-```typescript
-import { HookMatcher } from '@anthropic-ai/agent-sdk';
-
-const response = query({
- prompt: messageGenerator(),
- options: {
- hooks: {
- 'PreToolUse': [
- HookMatcher(matcher='Bash', hooks=[validateBashCommand]),
- HookMatcher(hooks=[logToolUse]) // Applies to all tools
- ],
- 'PostToolUse': [
- HookMatcher(hooks=[captureObservation])
- ]
- }
- }
-});
-
-type HookCallback = (
- input: HookInput,
- toolUseID: string | undefined,
- options: { signal: AbortSignal }
-) => Promise;
-```
-
-**When to use SDK hooks vs Claude Code hooks:**
-- **Claude Code hooks**: For integrating with the main Claude Code session (our current approach)
-- **SDK hooks**: For controlling the memory agent's own tool usage (future enhancement)
-
-**Implementation priority:** Medium (could simplify architecture, but adds complexity to migration)
-
----
-
-## Error Handling
-
-**SDK worker failures:**
-- Each observation processing is atomic
-- Failed observations stay in queue
-- Next worker run retries
-- After 3 failures, mark observation as skipped
-- Use AbortController for graceful cancellation
-
-**Abort signal handling:**
-```typescript
-try {
- for await (const msg of response) {
- if (abortController.signal.aborted) {
- throw new Error('Aborted');
- }
- // Process message
- }
-} catch (error) {
- if (abortController.signal.aborted) {
- // Clean shutdown
- await response.interrupt();
- } else {
- // Actual error
- throw error;
- }
-}
-```
-
-**Database corruption:**
-- SQLite with WAL mode (write-ahead logging)
-- Regular backups to ~/.claude-mem/backups/
-- Automatic recovery from backups
-
-**SDK API failures:**
-- Retry with exponential backoff
-- Don't block main Claude Code session
-- Log errors for debugging
-- Mark session as 'failed' after max retries
-
----
-
-## Implementation Order
-
-1. **Database setup** - Create tables and migration scripts
-2. **Hook commands** - Implement the 4 hook commands (context, new, save, summary)
-3. **SDK worker** - Implement the background worker process with response parsing
-4. **SDK prompts** - Wire up the prompts and message generator
-5. **Test end-to-end** - Run a real Claude Code session and verify it works
-
-Start simple. Get one hook working before moving to the next. Don't try to build everything at once.
-
-**Note:** MCP is only used for retrieval (when Claude Code needs to access stored memories), not for storage. The SDK agent stores data by outputting specially formatted text that the SDK worker parses and writes to SQLite.
-
-### SDK Import Verification
-
-Before implementing, verify the SDK exports match your usage:
-
-```typescript
-// Required imports from @anthropic-ai/agent-sdk
-import { query } from '@anthropic-ai/agent-sdk';
-import type { Query, UserMessage, Options } from '@anthropic-ai/agent-sdk';
-
-// Verify the query function signature:
-// function query(options: { prompt: string | AsyncIterable; options?: Options }): Query
-
-// Verify Query type:
-// interface Query extends AsyncGenerator {
-// interrupt(): Promise;
-// setPermissionMode(mode: PermissionMode): Promise;
-// }
-
-// Verify UserMessage type:
-// type UserMessage = { role: "user"; content: string }
-```
-
-If the SDK exports differ from this structure, adjust the implementation accordingly. The SDK documentation should be the source of truth.
-
----
-
-## Key Corrections from Agent SDK Documentation
-
-This refactor plan has been updated to align with the official Agent SDK documentation. Key corrections include:
-
-### 1. Session ID Management
-- **Before:** Captured session ID once in UserPromptSubmit hook
-- **After:** Capture from system init message and track updates continuously
-- **Why:** Session IDs change with each conversation turn
-
-### 2. Hook Configuration
-- **Before:** Mixed up SDK hook format with Claude Code hook format
-- **After:** Clarified that Claude Code uses settings.json format (external commands); SDK uses TypeScript HookMatcher (programmatic callbacks)
-- **Why:** Two separate hook systems with different purposes and configuration methods
-- **Our approach:** Use Claude Code hooks to observe the main session; SDK hooks are future enhancement
-
-### 3. Message Generator and Query Interface
-- **Before:** Custom SDKMessage type with nested message structure
-- **After:** Simple UserMessage type `{ role: "user", content: string }` yielded from AsyncIterable
-- **Why:** SDK expects AsyncIterable, not a custom wrapper format
-- **Query type:** Properly typed as `Query` which extends AsyncGenerator with interrupt() and setPermissionMode()
-
-### 4. Tool Filtering
-- **Before:** Filter "boring tools" in PostToolUse hook
-- **After:** Use SDK's `disallowedTools` option in query configuration
-- **Why:** More efficient to prevent SDK from using tools entirely
-
-### 5. Model Identifier
-- **Before:** Used `claude-haiku-4-5-20251001` (undocumented)
-- **After:** Use `claude-sonnet-4-5` (documented model name)
-- **Why:** Stick to documented model identifiers for stability
-
-### 6. Error Handling
-- **Before:** Custom error handling without SDK features
-- **After:** Use AbortController and response.interrupt() for graceful cancellation
-- **Why:** SDK provides built-in cancellation mechanisms
-
-### 7. Session Resumption
-- **Before:** Manual session state reconstruction
-- **After:** Leverage SDK's built-in `resume: sessionId` option
-- **Why:** SDK already handles session resumption
-
-### Future Enhancements to Consider
-
-1. **Permission integration** - Use canUseTool callback to make memory-aware decisions
-2. **SDK native hooks** - Replace external command hooks with SDK HookMatcher
-3. **Better session recovery** - Use SDK resumption for interrupted sessions
-
-These corrections ensure our implementation follows Agent SDK best practices and avoids reinventing functionality the SDK already provides.
-
----
-
-## Architecture Validation Summary
-
-This plan has been validated against the official Agent SDK documentation and confirmed to be architecturally sound.
-
-### ✅ Validated Design Decisions
-
-1. **Hook System Usage** - Correctly uses Claude Code external command hooks for observation; SDK programmatic hooks reserved for future enhancement
-2. **Query Function Interface** - Properly implements AsyncIterable for streaming input mode
-3. **Session Management** - Leverages SDK's built-in session resumption instead of manual state reconstruction
-4. **Tool Filtering** - Uses SDK's `disallowedTools` option for efficiency
-5. **Error Handling** - Implements AbortController and interrupt() for graceful cancellation
-6. **Separation of Concerns** - Clean isolation between main Claude Code session and background SDK worker
-
-### 🎯 Architecture Strengths
-
-- **Non-blocking** - Hooks are fast database operations; complex logic happens in background
-- **Queue-based** - Handles parallel hook execution correctly via observation_queue table
-- **Fault-tolerant** - Failed observations stay in queue for retry; graceful degradation
-- **Platform-agnostic** - No dependency on systemd/launchd; works everywhere
-- **Type-safe** - Uses official SDK TypeScript types throughout
-
-### 📋 Pre-Implementation Checklist
-
-Before starting implementation, verify:
-
-1. [ ] Agent SDK installed and accessible: `@anthropic-ai/agent-sdk`
-2. [ ] Verify SDK exports match expected structure (query, Query, UserMessage types)
-3. [ ] SQLite database location decided: `~/.claude-mem/db.sqlite`
-4. [ ] Claude Code settings.json hook configuration tested
-5. [ ] Background process spawning works on target platform (test detached process)
-
-### 🚀 Ready for Implementation
-
-The architecture is validated and ready for implementation. Follow the phased approach:
-
-1. Database setup first (get schema working with bun:sqlite)
-2. Implement hooks one at a time (start with `context`, then `save`)
-3. Build SDK worker with simple message generator
-4. Test end-to-end with a real Claude Code session
-5. Iterate and refine based on real-world usage
-
-**Remember:** Start simple, get one piece working, then build on it. Don't try to implement everything at once.
diff --git a/docs/context/cc-transcript-model-example.py b/docs/context/cc-transcript-model-example.py
deleted file mode 100644
index 6edf4398..00000000
--- a/docs/context/cc-transcript-model-example.py
+++ /dev/null
@@ -1,424 +0,0 @@
-"""Pydantic models for Claude Code transcript JSON structures.
-
-Enhanced to leverage official Anthropic types where beneficial.
-"""
-
-from typing import Any, List, Union, Optional, Dict, Literal, cast
-from pydantic import BaseModel
-
-from anthropic.types import Message as AnthropicMessage
-from anthropic.types import StopReason
-from anthropic.types import Usage as AnthropicUsage
-from anthropic.types.content_block import ContentBlock
-
-
-class TodoItem(BaseModel):
- id: str
- content: str
- status: Literal["pending", "in_progress", "completed"]
- priority: Literal["high", "medium", "low"]
-
-
-class UsageInfo(BaseModel):
- """Token usage information that extends Anthropic's Usage type to handle optional fields."""
-
- input_tokens: Optional[int] = None
- cache_creation_input_tokens: Optional[int] = None
- cache_read_input_tokens: Optional[int] = None
- output_tokens: Optional[int] = None
- service_tier: Optional[str] = None
- server_tool_use: Optional[Dict[str, Any]] = None
-
- def to_anthropic_usage(self) -> Optional[AnthropicUsage]:
- """Convert to Anthropic Usage type if both required fields are present."""
- if self.input_tokens is not None and self.output_tokens is not None:
- return AnthropicUsage(
- input_tokens=self.input_tokens,
- output_tokens=self.output_tokens,
- cache_creation_input_tokens=self.cache_creation_input_tokens,
- cache_read_input_tokens=self.cache_read_input_tokens,
- service_tier=self.service_tier, # type: ignore
- server_tool_use=self.server_tool_use, # type: ignore
- )
- return None
-
- @classmethod
- def from_anthropic_usage(cls, usage: AnthropicUsage) -> "UsageInfo":
- """Create UsageInfo from Anthropic Usage."""
- return cls(
- input_tokens=usage.input_tokens,
- output_tokens=usage.output_tokens,
- cache_creation_input_tokens=usage.cache_creation_input_tokens,
- cache_read_input_tokens=usage.cache_read_input_tokens,
- service_tier=usage.service_tier,
- server_tool_use=usage.server_tool_use.model_dump()
- if usage.server_tool_use
- else None,
- )
-
-
-class TextContent(BaseModel):
- type: Literal["text"]
- text: str
-
-
-class ToolUseContent(BaseModel):
- type: Literal["tool_use"]
- id: str
- name: str
- input: Dict[str, Any]
-
-
-class ToolResultContent(BaseModel):
- type: Literal["tool_result"]
- tool_use_id: str
- content: Union[str, List[Dict[str, Any]]]
- is_error: Optional[bool] = None
-
-
-class ThinkingContent(BaseModel):
- type: Literal["thinking"]
- thinking: str
- signature: Optional[str] = None
-
-
-class ImageSource(BaseModel):
- type: Literal["base64"]
- media_type: str
- data: str
-
-
-class ImageContent(BaseModel):
- type: Literal["image"]
- source: ImageSource
-
-
-# Enhanced ContentItem to include official Anthropic ContentBlock types
-ContentItem = Union[
- TextContent,
- ToolUseContent,
- ToolResultContent,
- ThinkingContent,
- ImageContent,
- ContentBlock, # Official Anthropic content block types
-]
-
-
-class UserMessage(BaseModel):
- role: Literal["user"]
- content: Union[str, List[ContentItem]]
-
-
-class AssistantMessage(BaseModel):
- """Assistant message model compatible with Anthropic's Message type."""
-
- id: str
- type: Literal["message"]
- role: Literal["assistant"]
- model: str
- content: List[ContentItem]
- stop_reason: Optional[StopReason] = None
- stop_sequence: Optional[str] = None
- usage: Optional[UsageInfo] = None
-
- @classmethod
- def from_anthropic_message(
- cls, anthropic_msg: AnthropicMessage
- ) -> "AssistantMessage":
- """Create AssistantMessage from official Anthropic Message."""
- # Convert Anthropic Message to our format, preserving official types where possible
- return cls(
- id=anthropic_msg.id,
- type=anthropic_msg.type,
- role=anthropic_msg.role,
- model=anthropic_msg.model,
- content=list(
- anthropic_msg.content
- ), # Convert to list for ContentItem compatibility
- stop_reason=anthropic_msg.stop_reason,
- stop_sequence=anthropic_msg.stop_sequence,
- usage=normalize_usage_info(anthropic_msg.usage),
- )
-
-
-class FileInfo(BaseModel):
- filePath: str
- content: str
- numLines: int
- startLine: int
- totalLines: int
-
-
-class FileReadResult(BaseModel):
- type: Literal["text"]
- file: FileInfo
-
-
-class CommandResult(BaseModel):
- stdout: str
- stderr: str
- interrupted: bool
- isImage: bool
-
-
-class TodoResult(BaseModel):
- oldTodos: List[TodoItem]
- newTodos: List[TodoItem]
-
-
-class EditResult(BaseModel):
- oldString: Optional[str] = None
- newString: Optional[str] = None
- replaceAll: Optional[bool] = None
- originalFile: Optional[str] = None
- structuredPatch: Optional[Any] = None
- userModified: Optional[bool] = None
-
-
-ToolUseResult = Union[
- str,
- List[TodoItem],
- FileReadResult,
- CommandResult,
- TodoResult,
- EditResult,
- List[ContentItem],
-]
-
-
-class BaseTranscriptEntry(BaseModel):
- parentUuid: Optional[str]
- isSidechain: bool
- userType: str
- cwd: str
- sessionId: str
- version: str
- uuid: str
- timestamp: str
- isMeta: Optional[bool] = None
-
-
-class UserTranscriptEntry(BaseTranscriptEntry):
- type: Literal["user"]
- message: UserMessage
- toolUseResult: Optional[ToolUseResult] = None
-
-
-class AssistantTranscriptEntry(BaseTranscriptEntry):
- type: Literal["assistant"]
- message: AssistantMessage
- requestId: Optional[str] = None
-
-
-class SummaryTranscriptEntry(BaseModel):
- type: Literal["summary"]
- summary: str
- leafUuid: str
- cwd: Optional[str] = None
-
-
-class SystemTranscriptEntry(BaseTranscriptEntry):
- """System messages like warnings, notifications, etc."""
-
- type: Literal["system"]
- content: str
- level: Optional[str] = None # e.g., "warning", "info", "error"
-
-
-class QueueOperationTranscriptEntry(BaseModel):
- """Queue operations (enqueue/dequeue) for message queueing tracking.
-
- These are internal operations that track when messages are queued and dequeued.
- They are parsed but not rendered, as the content duplicates actual user messages.
- """
-
- type: Literal["queue-operation"]
- operation: Literal["enqueue", "dequeue"]
- timestamp: str
- sessionId: str
- content: Optional[List[ContentItem]] = None # Only present for enqueue operations
-
-
-TranscriptEntry = Union[
- UserTranscriptEntry,
- AssistantTranscriptEntry,
- SummaryTranscriptEntry,
- SystemTranscriptEntry,
- QueueOperationTranscriptEntry,
-]
-
-
-def normalize_usage_info(usage_data: Any) -> Optional[UsageInfo]:
- """Normalize usage data to be compatible with both custom and Anthropic formats."""
- if usage_data is None:
- return None
-
- # If it's already a UsageInfo instance, return as-is
- if isinstance(usage_data, UsageInfo):
- return usage_data
-
- # If it's an Anthropic Usage instance, convert using our method
- if isinstance(usage_data, AnthropicUsage):
- return UsageInfo.from_anthropic_usage(usage_data)
-
- # If it has the shape of an Anthropic Usage, try to construct it first
- if hasattr(usage_data, "input_tokens") and hasattr(usage_data, "output_tokens"):
- try:
- # Try to create an Anthropic Usage first
- anthropic_usage = AnthropicUsage.model_validate(usage_data)
- return UsageInfo.from_anthropic_usage(anthropic_usage)
- except Exception:
- # Fall back to direct conversion
- return UsageInfo(
- input_tokens=getattr(usage_data, "input_tokens", None),
- cache_creation_input_tokens=getattr(
- usage_data, "cache_creation_input_tokens", None
- ),
- cache_read_input_tokens=getattr(
- usage_data, "cache_read_input_tokens", None
- ),
- output_tokens=getattr(usage_data, "output_tokens", None),
- service_tier=getattr(usage_data, "service_tier", None),
- server_tool_use=getattr(usage_data, "server_tool_use", None),
- )
-
- # If it's a dict, validate and convert to our format
- if isinstance(usage_data, dict):
- return UsageInfo.model_validate(usage_data)
-
- return None
-
-
-def parse_content_item(item_data: Dict[str, Any]) -> ContentItem:
- """Parse a content item using enhanced approach with Anthropic types."""
- try:
- content_type = item_data.get("type", "")
-
- # Try official Anthropic types first for better future compatibility
- if content_type == "text":
- try:
- from anthropic.types.text_block import TextBlock
-
- return TextBlock.model_validate(item_data)
- except Exception:
- return TextContent.model_validate(item_data)
- elif content_type == "tool_use":
- try:
- from anthropic.types.tool_use_block import ToolUseBlock
-
- return ToolUseBlock.model_validate(item_data)
- except Exception:
- return ToolUseContent.model_validate(item_data)
- elif content_type == "thinking":
- try:
- from anthropic.types.thinking_block import ThinkingBlock
-
- return ThinkingBlock.model_validate(item_data)
- except Exception:
- return ThinkingContent.model_validate(item_data)
- elif content_type == "tool_result":
- return ToolResultContent.model_validate(item_data)
- elif content_type == "image":
- return ImageContent.model_validate(item_data)
- else:
- # Fallback to text content for unknown types
- return TextContent(type="text", text=str(item_data))
- except Exception:
- return TextContent(type="text", text=str(item_data))
-
-
-def parse_message_content(content_data: Any) -> Union[str, List[ContentItem]]:
- """Parse message content, handling both string and list formats."""
- if isinstance(content_data, str):
- return content_data
- elif isinstance(content_data, list):
- content_list = cast(List[Dict[str, Any]], content_data)
- return [parse_content_item(item) for item in content_list]
- else:
- return str(content_data)
-
-
-def parse_transcript_entry(data: Dict[str, Any]) -> TranscriptEntry:
- """
- Parse a JSON dictionary into the appropriate TranscriptEntry type.
-
- Enhanced to optionally use official Anthropic types for assistant messages.
-
- Args:
- data: Dictionary parsed from JSON
-
- Returns:
- The appropriate TranscriptEntry subclass
-
- Raises:
- ValueError: If the data doesn't match any known transcript entry type
- """
- entry_type = data.get("type")
-
- if entry_type == "user":
- # Parse message content if present
- data_copy = data.copy()
- if "message" in data_copy and "content" in data_copy["message"]:
- data_copy["message"] = data_copy["message"].copy()
- data_copy["message"]["content"] = parse_message_content(
- data_copy["message"]["content"]
- )
- # Parse toolUseResult if present and it's a list of content items
- if "toolUseResult" in data_copy and isinstance(
- data_copy["toolUseResult"], list
- ):
- # Check if it's a list of content items (MCP tool results)
- tool_use_result = cast(List[Any], data_copy["toolUseResult"])
- if (
- tool_use_result
- and isinstance(tool_use_result[0], dict)
- and "type" in tool_use_result[0]
- ):
- data_copy["toolUseResult"] = [
- parse_content_item(cast(Dict[str, Any], item))
- for item in tool_use_result
- if isinstance(item, dict)
- ]
- return UserTranscriptEntry.model_validate(data_copy)
-
- elif entry_type == "assistant":
- # Enhanced assistant message parsing with optional Anthropic types
- data_copy = data.copy()
-
- # Validate compatibility with official Anthropic Message type
- if "message" in data_copy:
- try:
- message_data = data_copy["message"]
- AnthropicMessage.model_validate(message_data)
- # Successfully validated - our data is compatible with official Anthropic types
- except Exception:
- # Validation failed - continue with standard parsing
- pass
-
- # Standard parsing path (works for all cases)
- if "message" in data_copy and "content" in data_copy["message"]:
- message_copy = data_copy["message"].copy()
- message_copy["content"] = parse_message_content(message_copy["content"])
-
- # Normalize usage data to support both Anthropic and custom formats
- if "usage" in message_copy:
- message_copy["usage"] = normalize_usage_info(message_copy["usage"])
-
- data_copy["message"] = message_copy
- return AssistantTranscriptEntry.model_validate(data_copy)
-
- elif entry_type == "summary":
- return SummaryTranscriptEntry.model_validate(data)
-
- elif entry_type == "system":
- return SystemTranscriptEntry.model_validate(data)
-
- elif entry_type == "queue-operation":
- # Parse content if present (only in enqueue operations)
- data_copy = data.copy()
- if "content" in data_copy and isinstance(data_copy["content"], list):
- data_copy["content"] = parse_message_content(data_copy["content"])
- return QueueOperationTranscriptEntry.model_validate(data_copy)
-
- else:
- raise ValueError(f"Unknown transcript entry type: {entry_type}")
\ No newline at end of file
diff --git a/docs/context/chroma-search-completion-plan.md b/docs/context/chroma-search-completion-plan.md
deleted file mode 100644
index 1b26b457..00000000
--- a/docs/context/chroma-search-completion-plan.md
+++ /dev/null
@@ -1,384 +0,0 @@
-# Chroma Search Completion Plan
-
-## Current State Analysis
-
-### What's Working ✅
-1. **Hybrid Search Implementation**
- - Chroma semantic search + SQLite temporal filtering is working
- - Evidence: Queries like "AI embeddings" find "hybrid search" through semantic similarity
- - All metadata-first tools use Chroma ranking
-
-2. **Tools Using Chroma Correctly**
- - `search_observations` - Semantic-first workflow (Chroma top 100 → 90-day filter → SQLite hydrate)
- - `find_by_concept` - Metadata-first + Chroma semantic ranking
- - `find_by_file` - Metadata-first + Chroma semantic ranking
- - `find_by_type` - Metadata-first + Chroma semantic ranking
-
-3. **Data Synced to Chroma**
- - ✅ Observations (all fields: narrative, facts, text as separate docs)
- - ✅ Session summaries (all fields: request, investigated, learned, completed, next_steps, notes as separate docs)
- - ❌ User prompts (NOT synced yet)
-
-### What's Missing ❌
-
-1. **search_sessions tool** - Only uses SQLite FTS5, not leveraging Chroma semantic search
-2. **search_user_prompts tool** - Only uses SQLite FTS5, not leveraging Chroma semantic search
-3. **User prompts not synced to Chroma** - Need to add to sync experiment and worker process
-
-## Why User Prompts Need Semantic Search
-
-**Benefits:**
-- Users often search for "what I asked about X" but phrase it differently than original prompt
-- Semantic search finds related requests even with different wording
-- Example: Search "authentication setup" finds prompts about "login system", "user auth", "sign-in flow"
-- Completes the triad: What was done (observations) + What was learned (summaries) + What was requested (prompts)
-
-**Storage pattern:**
-- Each user prompt becomes ONE document in Chroma (unlike observations/summaries which split by field)
-- Metadata: `sqlite_id`, `doc_type: 'user_prompt'`, `sdk_session_id`, `project`, `created_at_epoch`, `prompt_number`
-- Document ID format: `prompt_{id}` (simpler than observations since no field splitting)
-
-## Implementation Plan
-
-### Phase 1: Sync User Prompts to Chroma
-
-**Files to modify:**
-1. `experiment/chroma-sync-experiment.ts` - Add user_prompts sync section
-2. Future: Worker service incremental sync (not in this phase)
-
-**Implementation:**
-```typescript
-// In chroma-sync-experiment.ts after session summaries sync
-
-// Fetch user prompts
-console.log('📖 Reading user prompts from SQLite...');
-const prompts = store.db.prepare(`
- SELECT * FROM user_prompts WHERE project = ? ORDER BY created_at_epoch DESC LIMIT 1000
-`).all(project) as any[];
-console.log(`Found ${prompts.length} user prompts`);
-
-// Prepare prompt documents - one document per prompt
-const promptDocs: ChromaDocument[] = [];
-
-for (const prompt of prompts) {
- promptDocs.push({
- id: `prompt_${prompt.id}`,
- document: prompt.prompt_text,
- metadata: {
- sqlite_id: prompt.id,
- doc_type: 'user_prompt',
- sdk_session_id: prompt.sdk_session_id,
- project: prompt.project,
- created_at_epoch: prompt.created_at_epoch,
- prompt_number: prompt.prompt_number || 0
- }
- });
-}
-
-console.log(`Created ${promptDocs.length} user prompt documents\n`);
-
-// Sync prompts in batches (same pattern as observations/sessions)
-```
-
-**Testing:**
-```bash
-npm run experiment:sync
-# Verify prompts appear in Chroma collection
-```
-
-### Phase 2: Update search_sessions to Use Chroma
-
-**File:** `src/servers/search-server.ts` (lines ~441-481)
-
-**Current implementation:**
-```typescript
-const results = search.searchSessions(query, options);
-```
-
-**New implementation (semantic-first hybrid):**
-```typescript
-let results: SessionSummarySearchResult[] = [];
-
-// Hybrid search: Try Chroma semantic search first, fall back to FTS5
-if (chromaClient) {
- try {
- console.error('[search-server] Using hybrid semantic search for sessions');
-
- // Step 1: Chroma semantic search (top 100)
- const chromaResults = await queryChroma(query, 100, { doc_type: 'session_summary' });
- console.error(`[search-server] Chroma returned ${chromaResults.ids.length} semantic matches`);
-
- if (chromaResults.ids.length > 0) {
- // Step 2: Filter by recency (90 days)
- const ninetyDaysAgo = Math.floor(Date.now() / 1000) - (90 * 24 * 60 * 60);
- const recentIds = chromaResults.ids.filter((id, idx) => {
- const meta = chromaResults.metadatas[idx];
- return meta && meta.created_at_epoch > ninetyDaysAgo;
- });
-
- console.error(`[search-server] ${recentIds.length} results within 90-day window`);
-
- // Step 3: Hydrate from SQLite in temporal order
- if (recentIds.length > 0) {
- const limit = options.limit || 20;
- results = store.getSessionSummariesByIds(recentIds, { orderBy: 'date_desc', limit });
- console.error(`[search-server] Hydrated ${results.length} sessions from SQLite`);
- }
- }
- } catch (chromaError: any) {
- console.error('[search-server] Chroma query failed, falling back to FTS5:', chromaError.message);
- }
-}
-
-// Fall back to FTS5 if Chroma unavailable or returned no results
-if (results.length === 0) {
- console.error('[search-server] Using FTS5 keyword search');
- results = search.searchSessions(query, options);
-}
-```
-
-**Helper needed in queryChroma:**
-Update `queryChroma` function to extract summary IDs from document IDs:
-```typescript
-// Extract unique summary IDs from document IDs
-for (const docId of docIds) {
- // Handle both obs_{id}_* and summary_{id}_* formats
- const obsMatch = docId.match(/obs_(\d+)_/);
- const summaryMatch = docId.match(/summary_(\d+)_/);
-
- if (obsMatch) {
- const sqliteId = parseInt(obsMatch[1], 10);
- if (!ids.includes(sqliteId)) ids.push(sqliteId);
- } else if (summaryMatch) {
- const sqliteId = parseInt(summaryMatch[1], 10);
- if (!ids.includes(sqliteId)) ids.push(sqliteId);
- }
-}
-```
-
-**Database helper needed:**
-Add to `SessionStore.ts`:
-```typescript
-getSessionSummariesByIds(
- ids: number[],
- options: { orderBy?: 'date_desc' | 'date_asc'; limit?: number } = {}
-): SessionSummarySearchResult[] {
- if (ids.length === 0) return [];
-
- const { orderBy = 'date_desc', limit } = options;
- const orderClause = orderBy === 'date_asc' ? 'ASC' : 'DESC';
- const limitClause = limit ? `LIMIT ${limit}` : '';
- const placeholders = ids.map(() => '?').join(',');
-
- const stmt = this.db.prepare(`
- SELECT * FROM session_summaries
- WHERE id IN (${placeholders})
- ORDER BY created_at_epoch ${orderClause}
- ${limitClause}
- `);
-
- return stmt.all(...ids) as SessionSummarySearchResult[];
-}
-```
-
-### Phase 3: Update search_user_prompts to Use Chroma
-
-**File:** `src/servers/search-server.ts` (lines ~956-1010)
-
-**Current implementation:**
-```typescript
-const results = search.searchUserPrompts(query, options);
-```
-
-**New implementation (semantic-first hybrid):**
-```typescript
-let results: UserPromptSearchResult[] = [];
-
-// Hybrid search: Try Chroma semantic search first, fall back to FTS5
-if (chromaClient) {
- try {
- console.error('[search-server] Using hybrid semantic search for user prompts');
-
- // Step 1: Chroma semantic search (top 100)
- const chromaResults = await queryChroma(query, 100, { doc_type: 'user_prompt' });
- console.error(`[search-server] Chroma returned ${chromaResults.ids.length} semantic matches`);
-
- if (chromaResults.ids.length > 0) {
- // Step 2: Filter by recency (90 days)
- const ninetyDaysAgo = Math.floor(Date.now() / 1000) - (90 * 24 * 60 * 60);
- const recentIds = chromaResults.ids.filter((id, idx) => {
- const meta = chromaResults.metadatas[idx];
- return meta && meta.created_at_epoch > ninetyDaysAgo;
- });
-
- console.error(`[search-server] ${recentIds.length} results within 90-day window`);
-
- // Step 3: Hydrate from SQLite in temporal order
- if (recentIds.length > 0) {
- const limit = options.limit || 20;
- results = store.getUserPromptsByIds(recentIds, { orderBy: 'date_desc', limit });
- console.error(`[search-server] Hydrated ${results.length} user prompts from SQLite`);
- }
- }
- } catch (chromaError: any) {
- console.error('[search-server] Chroma query failed, falling back to FTS5:', chromaError.message);
- }
-}
-
-// Fall back to FTS5 if Chroma unavailable or returned no results
-if (results.length === 0) {
- console.error('[search-server] Using FTS5 keyword search');
- results = search.searchUserPrompts(query, options);
-}
-```
-
-**Helper needed in queryChroma:**
-Update to handle `prompt_{id}` format:
-```typescript
-// Extract unique prompt IDs from document IDs
-for (const docId of docIds) {
- const obsMatch = docId.match(/obs_(\d+)_/);
- const summaryMatch = docId.match(/summary_(\d+)_/);
- const promptMatch = docId.match(/prompt_(\d+)/);
-
- if (obsMatch) {
- const sqliteId = parseInt(obsMatch[1], 10);
- if (!ids.includes(sqliteId)) ids.push(sqliteId);
- } else if (summaryMatch) {
- const sqliteId = parseInt(summaryMatch[1], 10);
- if (!ids.includes(sqliteId)) ids.push(sqliteId);
- } else if (promptMatch) {
- const sqliteId = parseInt(promptMatch[1], 10);
- if (!ids.includes(sqliteId)) ids.push(sqliteId);
- }
-}
-```
-
-**Database helper needed:**
-Add to `SessionStore.ts`:
-```typescript
-getUserPromptsByIds(
- ids: number[],
- options: { orderBy?: 'date_desc' | 'date_asc'; limit?: number } = {}
-): UserPromptSearchResult[] {
- if (ids.length === 0) return [];
-
- const { orderBy = 'date_desc', limit } = options;
- const orderClause = orderBy === 'date_asc' ? 'ASC' : 'DESC';
- const limitClause = limit ? `LIMIT ${limit}` : '';
- const placeholders = ids.map(() => '?').join(',');
-
- const stmt = this.db.prepare(`
- SELECT * FROM user_prompts
- WHERE id IN (${placeholders})
- ORDER BY created_at_epoch ${orderClause}
- ${limitClause}
- `);
-
- return stmt.all(...ids) as UserPromptSearchResult[];
-}
-```
-
-### Phase 4: Timeline Context Tool
-
-**New tool:** `get_context_timeline`
-
-**Purpose:** Show observations/sessions/prompts around a specific point in time
-
-**API:**
-```typescript
-{
- name: 'get_context_timeline',
- description: 'Get a timeline of context around a specific observation, session, or timestamp',
- inputSchema: z.object({
- anchor: z.union([
- z.number(), // observation ID
- z.string() // ISO timestamp or session ID
- ]).describe('Anchor point: observation ID, session ID, or ISO timestamp'),
- depth_before: z.number().min(0).max(50).default(10).describe('Number of records to show before anchor'),
- depth_after: z.number().min(0).max(50).default(10).describe('Number of records to show after anchor'),
- format: z.enum(['index', 'full']).default('index'),
- project: z.string().optional()
- })
-}
-```
-
-**Implementation approach:**
-1. Resolve anchor to a timestamp (observation.created_at_epoch, session.created_at_epoch, or parse ISO)
-2. Query observations within [anchor_time - depth_before_duration, anchor_time + depth_after_duration]
-3. Return chronologically ordered results with anchor highlighted
-4. Support mixing observations, sessions, and prompts in single timeline
-
-**Database helper:**
-```typescript
-getTimelineAroundTimestamp(
- anchorEpoch: number,
- depthBefore: number,
- depthAfter: number,
- project?: string
-): { observations: any[], sessions: any[], prompts: any[] } {
- // Calculate time windows based on depth
- // For now: each depth = 1 hour (configurable)
- const hourInSeconds = 3600;
- const startEpoch = anchorEpoch - (depthBefore * hourInSeconds);
- const endEpoch = anchorEpoch + (depthAfter * hourInSeconds);
-
- // Query all three tables
- const observations = this.db.prepare(`...`).all(...);
- const sessions = this.db.prepare(`...`).all(...);
- const prompts = this.db.prepare(`...`).all(...);
-
- return { observations, sessions, prompts };
-}
-```
-
-## Testing Plan
-
-### Phase 1 Testing
-```bash
-# Run sync experiment
-npm run experiment:sync
-
-# Check Chroma collection for prompts
-# Should see prompt_* documents with doc_type: 'user_prompt'
-```
-
-### Phase 2 Testing
-```bash
-# Test semantic search for sessions
-# Example: "authentication system" should find sessions about "login", "user auth", etc.
-```
-
-### Phase 3 Testing
-```bash
-# Test semantic search for user prompts
-# Example: "fix bug" should find prompts with "error", "issue", "problem", etc.
-```
-
-### Phase 4 Testing
-```bash
-# Test timeline around specific observation
-# Should show before/after context
-```
-
-## Files to Modify
-
-1. **experiment/chroma-sync-experiment.ts** - Add user_prompts sync
-2. **src/servers/search-server.ts** - Update search_sessions and search_user_prompts, add get_context_timeline
-3. **src/services/sqlite/SessionStore.ts** - Add getSessionSummariesByIds, getUserPromptsByIds, getTimelineAroundTimestamp
-4. **src/services/sqlite/types.ts** - Ensure all return types are exported
-
-## Success Criteria
-
-- ✅ All 8 search tools use Chroma semantic search with SQLite temporal fallback
-- ✅ User prompts are synced to Chroma and searchable
-- ✅ Timeline tool provides chronological context around any point
-- ✅ Semantic search works across observations, sessions, and prompts
-- ✅ All searches maintain 90-day temporal filtering for relevance
-
-## Future Enhancements
-
-1. **Incremental sync in worker service** - Currently only batch sync via experiment
-2. **Configurable temporal windows** - Make 90-day filter configurable
-3. **Cross-collection search** - Search across observations + sessions + prompts in one query
-4. **Timeline view improvements** - Group by session, highlight anchor, show relationships
diff --git a/docs/context/claude-code/agent-sdk-cost-tracking.md b/docs/context/claude-code/agent-sdk-cost-tracking.md
deleted file mode 100644
index 4d36074f..00000000
--- a/docs/context/claude-code/agent-sdk-cost-tracking.md
+++ /dev/null
@@ -1,61 +0,0 @@
- For tracking costs and tokens in your Agent SDK plugin, you have built-in programmatic access to usage data through the SDK itself[(1)](https://docs.claude.com/en/api/agent-sdk/cost-tracking).
-
-## Agent SDK Cost Tracking
-
-The Claude Agent SDK provides detailed token usage information for each interaction[(1)](https://docs.claude.com/en/api/agent-sdk/cost-tracking). Here's how to track it:
-
-**TypeScript:**
-```typescript
-import { query } from "@anthropic-ai/claude-agent-sdk";
-
-const result = await query({
- prompt: "Your task here",
- options: {
- onMessage: (message) => {
- if (message.type === 'assistant' && message.usage) {
- console.log(`Message ID: ${message.id}`);
- console.log(`Usage:`, message.usage);
- }
- }
- }
-});
-```
-[(1)](https://docs.claude.com/en/api/agent-sdk/cost-tracking)
-
-The final `result` message contains the total cumulative usage from all steps in the conversation[(1)](https://docs.claude.com/en/api/agent-sdk/cost-tracking):
-
-```typescript
-console.log("Total usage:", result.usage);
-console.log("Total cost:", result.usage.total_cost_usd);
-```
-[(1)](https://docs.claude.com/en/api/agent-sdk/cost-tracking)
-
-## Important: Avoid Double-Counting
-
-When Claude executes tools in parallel, multiple assistant messages may share the same ID and usage data[(1)](https://docs.claude.com/en/api/agent-sdk/cost-tracking). You should only charge once per unique message ID[(1)](https://docs.claude.com/en/api/agent-sdk/cost-tracking):
-
-```typescript
-const processedMessageIds = new Set();
-
-onMessage: (message) => {
- if (message.type === 'assistant' && message.usage) {
- // Skip if already processed
- if (processedMessageIds.has(message.id)) {
- return;
- }
-
- processedMessageIds.add(message.id);
- // Record usage here
- }
-}
-```
-[(1)](https://docs.claude.com/en/api/agent-sdk/cost-tracking)
-
-## Usage Fields
-
-Each usage object contains[(1)](https://docs.claude.com/en/api/agent-sdk/cost-tracking):
-- `input_tokens`: Base input tokens processed
-- `output_tokens`: Tokens generated in the response
-- `cache_creation_input_tokens`: Tokens used to create cache entries
-- `cache_read_input_tokens`: Tokens read from cache
-- `total_cost_usd`: Total cost in USD (only in result message)
diff --git a/docs/context/claude-code/agent-skills.md b/docs/context/claude-code/agent-skills.md
deleted file mode 100644
index 5e3a8e58..00000000
--- a/docs/context/claude-code/agent-skills.md
+++ /dev/null
@@ -1,607 +0,0 @@
-# Agent Skills
-
-> Create, manage, and share Skills to extend Claude's capabilities in Claude Code.
-
-This guide shows you how to create, use, and manage Agent Skills in Claude Code. Skills are modular capabilities that extend Claude's functionality through organized folders containing instructions, scripts, and resources.
-
-## Prerequisites
-
-* Claude Code version 1.0 or later
-* Basic familiarity with [Claude Code](/en/docs/claude-code/quickstart)
-
-## What are Agent Skills?
-
-Agent Skills package expertise into discoverable capabilities. Each Skill consists of a `SKILL.md` file with instructions that Claude reads when relevant, plus optional supporting files like scripts and templates.
-
-**How Skills are invoked**: Skills are **model-invoked**—Claude autonomously decides when to use them based on your request and the Skill's description. This is different from slash commands, which are **user-invoked** (you explicitly type `/command` to trigger them).
-
-**Benefits**:
-
-* Extend Claude's capabilities for your specific workflows
-* Share expertise across your team via git
-* Reduce repetitive prompting
-* Compose multiple Skills for complex tasks
-
-Learn more in the [Agent Skills overview](/en/docs/agents-and-tools/agent-skills/overview).
-
-
- For a deep dive into the architecture and real-world applications of Agent Skills, read our engineering blog: [Equipping agents for the real world with Agent Skills](https://www.anthropic.com/engineering/equipping-agents-for-the-real-world-with-agent-skills).
-
-
-## Create a Skill
-
-Skills are stored as directories containing a `SKILL.md` file.
-
-### Personal Skills
-
-Personal Skills are available across all your projects. Store them in `~/.claude/skills/`:
-
-```bash theme={null}
-mkdir -p ~/.claude/skills/my-skill-name
-```
-
-**Use personal Skills for**:
-
-* Your individual workflows and preferences
-* Experimental Skills you're developing
-* Personal productivity tools
-
-### Project Skills
-
-Project Skills are shared with your team. Store them in `.claude/skills/` within your project:
-
-```bash theme={null}
-mkdir -p .claude/skills/my-skill-name
-```
-
-**Use project Skills for**:
-
-* Team workflows and conventions
-* Project-specific expertise
-* Shared utilities and scripts
-
-Project Skills are checked into git and automatically available to team members.
-
-### Plugin Skills
-
-Skills can also come from [Claude Code plugins](/en/docs/claude-code/plugins). Plugins may bundle Skills that are automatically available when the plugin is installed. These Skills work the same way as personal and project Skills.
-
-## Write SKILL.md
-
-Create a `SKILL.md` file with YAML frontmatter and Markdown content:
-
-```yaml theme={null}
----
-name: your-skill-name
-description: Brief description of what this Skill does and when to use it
----
-
-# Your Skill Name
-
-## Instructions
-Provide clear, step-by-step guidance for Claude.
-
-## Examples
-Show concrete examples of using this Skill.
-```
-
-**Field requirements**:
-
-* `name`: Must use lowercase letters, numbers, and hyphens only (max 64 characters)
-* `description`: Brief description of what the Skill does and when to use it (max 1024 characters)
-
-The `description` field is critical for Claude to discover when to use your Skill. It should include both what the Skill does and when Claude should use it.
-
-See the [best practices guide](/en/docs/agents-and-tools/agent-skills/best-practices) for complete authoring guidance including validation rules.
-
-## Add supporting files
-
-Create additional files alongside SKILL.md:
-
-```
-my-skill/
-├── SKILL.md (required)
-├── reference.md (optional documentation)
-├── examples.md (optional examples)
-├── scripts/
-│ └── helper.py (optional utility)
-└── templates/
- └── template.txt (optional template)
-```
-
-Reference these files from SKILL.md:
-
-````markdown theme={null}
-For advanced usage, see [reference.md](reference.md).
-
-Run the helper script:
-```bash
-python scripts/helper.py input.txt
-```
-````
-
-Claude reads these files only when needed, using progressive disclosure to manage context efficiently.
-
-## Restrict tool access with allowed-tools
-
-Use the `allowed-tools` frontmatter field to limit which tools Claude can use when a Skill is active:
-
-```yaml theme={null}
----
-name: safe-file-reader
-description: Read files without making changes. Use when you need read-only file access.
-allowed-tools: Read, Grep, Glob
----
-
-# Safe File Reader
-
-This Skill provides read-only file access.
-
-## Instructions
-1. Use Read to view file contents
-2. Use Grep to search within files
-3. Use Glob to find files by pattern
-```
-
-When this Skill is active, Claude can only use the specified tools (Read, Grep, Glob) without needing to ask for permission. This is useful for:
-
-* Read-only Skills that shouldn't modify files
-* Skills with limited scope (e.g., only data analysis, no file writing)
-* Security-sensitive workflows where you want to restrict capabilities
-
-If `allowed-tools` is not specified, Claude will ask for permission to use tools as normal, following the standard permission model.
-
-
- `allowed-tools` is only supported for Skills in Claude Code.
-
-
-## View available Skills
-
-Skills are automatically discovered by Claude from three sources:
-
-* Personal Skills: `~/.claude/skills/`
-* Project Skills: `.claude/skills/`
-* Plugin Skills: bundled with installed plugins
-
-**To view all available Skills**, ask Claude directly:
-
-```
-What Skills are available?
-```
-
-or
-
-```
-List all available Skills
-```
-
-This will show all Skills from all sources, including plugin Skills.
-
-**To inspect a specific Skill**, you can also check the filesystem:
-
-```bash theme={null}
-# List personal Skills
-ls ~/.claude/skills/
-
-# List project Skills (if in a project directory)
-ls .claude/skills/
-
-# View a specific Skill's content
-cat ~/.claude/skills/my-skill/SKILL.md
-```
-
-## Test a Skill
-
-After creating a Skill, test it by asking questions that match your description.
-
-**Example**: If your description mentions "PDF files":
-
-```
-Can you help me extract text from this PDF?
-```
-
-Claude autonomously decides to use your Skill if it matches the request—you don't need to explicitly invoke it. The Skill activates automatically based on the context of your question.
-
-## Debug a Skill
-
-If Claude doesn't use your Skill, check these common issues:
-
-### Make description specific
-
-**Too vague**:
-
-```yaml theme={null}
-description: Helps with documents
-```
-
-**Specific**:
-
-```yaml theme={null}
-description: Extract text and tables from PDF files, fill forms, merge documents. Use when working with PDF files or when the user mentions PDFs, forms, or document extraction.
-```
-
-Include both what the Skill does and when to use it in the description.
-
-### Verify file path
-
-**Personal Skills**: `~/.claude/skills/skill-name/SKILL.md`
-**Project Skills**: `.claude/skills/skill-name/SKILL.md`
-
-Check the file exists:
-
-```bash theme={null}
-# Personal
-ls ~/.claude/skills/my-skill/SKILL.md
-
-# Project
-ls .claude/skills/my-skill/SKILL.md
-```
-
-### Check YAML syntax
-
-Invalid YAML prevents the Skill from loading. Verify the frontmatter:
-
-```bash theme={null}
-cat SKILL.md | head -n 10
-```
-
-Ensure:
-
-* Opening `---` on line 1
-* Closing `---` before Markdown content
-* Valid YAML syntax (no tabs, correct indentation)
-
-### View errors
-
-Run Claude Code with debug mode to see Skill loading errors:
-
-```bash theme={null}
-claude --debug
-```
-
-## Share Skills with your team
-
-**Recommended approach**: Distribute Skills through [plugins](/en/docs/claude-code/plugins).
-
-To share Skills via plugin:
-
-1. Create a plugin with Skills in the `skills/` directory
-2. Add the plugin to a marketplace
-3. Team members install the plugin
-
-For complete instructions, see [Add Skills to your plugin](/en/docs/claude-code/plugins#add-skills-to-your-plugin).
-
-You can also share Skills directly through project repositories:
-
-### Step 1: Add Skill to your project
-
-Create a project Skill:
-
-```bash theme={null}
-mkdir -p .claude/skills/team-skill
-# Create SKILL.md
-```
-
-### Step 2: Commit to git
-
-```bash theme={null}
-git add .claude/skills/
-git commit -m "Add team Skill for PDF processing"
-git push
-```
-
-### Step 3: Team members get Skills automatically
-
-When team members pull the latest changes, Skills are immediately available:
-
-```bash theme={null}
-git pull
-claude # Skills are now available
-```
-
-## Update a Skill
-
-Edit SKILL.md directly:
-
-```bash theme={null}
-# Personal Skill
-code ~/.claude/skills/my-skill/SKILL.md
-
-# Project Skill
-code .claude/skills/my-skill/SKILL.md
-```
-
-Changes take effect the next time you start Claude Code. If Claude Code is already running, restart it to load the updates.
-
-## Remove a Skill
-
-Delete the Skill directory:
-
-```bash theme={null}
-# Personal
-rm -rf ~/.claude/skills/my-skill
-
-# Project
-rm -rf .claude/skills/my-skill
-git commit -m "Remove unused Skill"
-```
-
-## Best practices
-
-### Keep Skills focused
-
-One Skill should address one capability:
-
-**Focused**:
-
-* "PDF form filling"
-* "Excel data analysis"
-* "Git commit messages"
-
-**Too broad**:
-
-* "Document processing" (split into separate Skills)
-* "Data tools" (split by data type or operation)
-
-### Write clear descriptions
-
-Help Claude discover when to use Skills by including specific triggers in your description:
-
-**Clear**:
-
-```yaml theme={null}
-description: Analyze Excel spreadsheets, create pivot tables, and generate charts. Use when working with Excel files, spreadsheets, or analyzing tabular data in .xlsx format.
-```
-
-**Vague**:
-
-```yaml theme={null}
-description: For files
-```
-
-### Test with your team
-
-Have teammates use Skills and provide feedback:
-
-* Does the Skill activate when expected?
-* Are the instructions clear?
-* Are there missing examples or edge cases?
-
-### Document Skill versions
-
-You can document Skill versions in your SKILL.md content to track changes over time. Add a version history section:
-
-```markdown theme={null}
-# My Skill
-
-## Version History
-- v2.0.0 (2025-10-01): Breaking changes to API
-- v1.1.0 (2025-09-15): Added new features
-- v1.0.0 (2025-09-01): Initial release
-```
-
-This helps team members understand what changed between versions.
-
-## Troubleshooting
-
-### Claude doesn't use my Skill
-
-**Symptom**: You ask a relevant question but Claude doesn't use your Skill.
-
-**Check**: Is the description specific enough?
-
-Vague descriptions make discovery difficult. Include both what the Skill does and when to use it, with key terms users would mention.
-
-**Too generic**:
-
-```yaml theme={null}
-description: Helps with data
-```
-
-**Specific**:
-
-```yaml theme={null}
-description: Analyze Excel spreadsheets, generate pivot tables, create charts. Use when working with Excel files, spreadsheets, or .xlsx files.
-```
-
-**Check**: Is the YAML valid?
-
-Run validation to check for syntax errors:
-
-```bash theme={null}
-# View frontmatter
-cat .claude/skills/my-skill/SKILL.md | head -n 15
-
-# Check for common issues
-# - Missing opening or closing ---
-# - Tabs instead of spaces
-# - Unquoted strings with special characters
-```
-
-**Check**: Is the Skill in the correct location?
-
-```bash theme={null}
-# Personal Skills
-ls ~/.claude/skills/*/SKILL.md
-
-# Project Skills
-ls .claude/skills/*/SKILL.md
-```
-
-### Skill has errors
-
-**Symptom**: The Skill loads but doesn't work correctly.
-
-**Check**: Are dependencies available?
-
-Claude will automatically install required dependencies (or ask for permission to install them) when it needs them.
-
-**Check**: Do scripts have execute permissions?
-
-```bash theme={null}
-chmod +x .claude/skills/my-skill/scripts/*.py
-```
-
-**Check**: Are file paths correct?
-
-Use forward slashes (Unix style) in all paths:
-
-**Correct**: `scripts/helper.py`
-**Wrong**: `scripts\helper.py` (Windows style)
-
-### Multiple Skills conflict
-
-**Symptom**: Claude uses the wrong Skill or seems confused between similar Skills.
-
-**Be specific in descriptions**: Help Claude choose the right Skill by using distinct trigger terms in your descriptions.
-
-Instead of:
-
-```yaml theme={null}
-# Skill 1
-description: For data analysis
-
-# Skill 2
-description: For analyzing data
-```
-
-Use:
-
-```yaml theme={null}
-# Skill 1
-description: Analyze sales data in Excel files and CRM exports. Use for sales reports, pipeline analysis, and revenue tracking.
-
-# Skill 2
-description: Analyze log files and system metrics data. Use for performance monitoring, debugging, and system diagnostics.
-```
-
-## Examples
-
-### Simple Skill (single file)
-
-```
-commit-helper/
-└── SKILL.md
-```
-
-```yaml theme={null}
----
-name: generating-commit-messages
-description: Generates clear commit messages from git diffs. Use when writing commit messages or reviewing staged changes.
----
-
-# Generating Commit Messages
-
-## Instructions
-
-1. Run `git diff --staged` to see changes
-2. I'll suggest a commit message with:
- - Summary under 50 characters
- - Detailed description
- - Affected components
-
-## Best practices
-
-- Use present tense
-- Explain what and why, not how
-```
-
-### Skill with tool permissions
-
-```
-code-reviewer/
-└── SKILL.md
-```
-
-```yaml theme={null}
----
-name: code-reviewer
-description: Review code for best practices and potential issues. Use when reviewing code, checking PRs, or analyzing code quality.
-allowed-tools: Read, Grep, Glob
----
-
-# Code Reviewer
-
-## Review checklist
-
-1. Code organization and structure
-2. Error handling
-3. Performance considerations
-4. Security concerns
-5. Test coverage
-
-## Instructions
-
-1. Read the target files using Read tool
-2. Search for patterns using Grep
-3. Find related files using Glob
-4. Provide detailed feedback on code quality
-```
-
-### Multi-file Skill
-
-```
-pdf-processing/
-├── SKILL.md
-├── FORMS.md
-├── REFERENCE.md
-└── scripts/
- ├── fill_form.py
- └── validate.py
-```
-
-**SKILL.md**:
-
-````yaml theme={null}
----
-name: pdf-processing
-description: Extract text, fill forms, merge PDFs. Use when working with PDF files, forms, or document extraction. Requires pypdf and pdfplumber packages.
----
-
-# PDF Processing
-
-## Quick start
-
-Extract text:
-```python
-import pdfplumber
-with pdfplumber.open("doc.pdf") as pdf:
- text = pdf.pages[0].extract_text()
-```
-
-For form filling, see [FORMS.md](FORMS.md).
-For detailed API reference, see [REFERENCE.md](REFERENCE.md).
-
-## Requirements
-
-Packages must be installed in your environment:
-```bash
-pip install pypdf pdfplumber
-```
-````
-
-
- List required packages in the description. Packages must be installed in your environment before Claude can use them.
-
-
-Claude loads additional files only when needed.
-
-## Next steps
-
-
-
- Write Skills that Claude can use effectively
-
-
-
- Learn how Skills work across Claude products
-
-
-
- Use Skills programmatically with TypeScript and Python
-
-
-
- Create your first Skill
-
-
diff --git a/docs/context/claude-code/exit-codes.md b/docs/context/claude-code/exit-codes.md
deleted file mode 100644
index 8493fb40..00000000
--- a/docs/context/claude-code/exit-codes.md
+++ /dev/null
@@ -1,31 +0,0 @@
-
-
-# Claude Code Hooks Exit Code Cheat Sheet
-
-## Exit Code Behavior [(1)](https://docs.claude.com/en/docs/claude-code/hooks#hook-output)
-
-- **Exit code 0**: Success. `stdout` is shown to the user in transcript mode, except for `UserPromptSubmit` hook where stdout is injected as context [(1)](https://docs.claude.com/en/docs/claude-code/hooks#hook-output)
-- **Exit code 2**: Blocking error. `stderr` is fed back to Claude to process automatically [(1)](https://docs.claude.com/en/docs/claude-code/hooks#hook-output)
-- **Other exit codes**: Non-blocking error. `stderr` is shown to the user and execution continues [(1)](https://docs.claude.com/en/docs/claude-code/hooks#hook-output)
-
-## Per-Hook Event Behavior [(1)](https://docs.claude.com/en/docs/claude-code/hooks#hook-output)
-
-| Hook Event | Exit Code 2 Behavior |
-|------------|---------------------|
-| `PreToolUse` | Blocks the tool call, shows stderr to Claude [(1)](https://docs.claude.com/en/docs/claude-code/hooks#hook-output) |
-| `PostToolUse` | Shows stderr to Claude (tool already ran) [(1)](https://docs.claude.com/en/docs/claude-code/hooks#hook-output) |
-| `Notification` | N/A, shows stderr to user only [(1)](https://docs.claude.com/en/docs/claude-code/hooks#hook-output) |
-| `UserPromptSubmit` | Blocks prompt processing, erases prompt, shows stderr to user only [(1)](https://docs.claude.com/en/docs/claude-code/hooks#hook-output) |
-| `Stop` | Blocks stoppage, shows stderr to Claude [(1)](https://docs.claude.com/en/docs/claude-code/hooks#hook-output) |
-| `SubagentStop` | Blocks stoppage, shows stderr to Claude subagent [(1)](https://docs.claude.com/en/docs/claude-code/hooks#hook-output) |
-| `PreCompact` | N/A, shows stderr to user only [(1)](https://docs.claude.com/en/docs/claude-code/hooks#hook-output) |
-| `SessionStart` | N/A, shows stderr to user only [(1)](https://docs.claude.com/en/docs/claude-code/hooks#hook-output) |
-| `SessionEnd` | N/A, shows stderr to user only [(1)](https://docs.claude.com/en/docs/claude-code/hooks#hook-output) |
-
-## Quick Reference
-
-- **Success**: `process.exit(0)` - Operation completed successfully
-- **Block & feedback**: `process.exit(2)` - Block operation and give Claude feedback via stderr
-- **Non-blocking error**: `process.exit(1)` - Show error to user but continue execution
-
-**Important**: Claude Code does not see stdout if the exit code is 0, except for the `UserPromptSubmit` hook where stdout is injected as context [(1)](https://docs.claude.com/en/docs/claude-code/hooks#hook-output)
\ No newline at end of file
diff --git a/docs/context/claude-code/hooks.md b/docs/context/claude-code/hooks.md
deleted file mode 100644
index 20e51fcd..00000000
--- a/docs/context/claude-code/hooks.md
+++ /dev/null
@@ -1,837 +0,0 @@
-# Hooks reference
-
-> This page provides reference documentation for implementing hooks in Claude Code.
-
-
- For a quickstart guide with examples, see [Get started with Claude Code hooks](/en/docs/claude-code/hooks-guide).
-
-
-## Configuration
-
-Claude Code hooks are configured in your [settings files](/en/docs/claude-code/settings):
-
-* `~/.claude/settings.json` - User settings
-* `.claude/settings.json` - Project settings
-* `.claude/settings.local.json` - Local project settings (not committed)
-* Enterprise managed policy settings
-
-### Structure
-
-Hooks are organized by matchers, where each matcher can have multiple hooks:
-
-```json theme={null}
-{
- "hooks": {
- "EventName": [
- {
- "matcher": "ToolPattern",
- "hooks": [
- {
- "type": "command",
- "command": "your-command-here"
- }
- ]
- }
- ]
- }
-}
-```
-
-* **matcher**: Pattern to match tool names, case-sensitive (only applicable for
- `PreToolUse` and `PostToolUse`)
- * Simple strings match exactly: `Write` matches only the Write tool
- * Supports regex: `Edit|Write` or `Notebook.*`
- * Use `*` to match all tools. You can also use empty string (`""`) or leave
- `matcher` blank.
-* **hooks**: Array of commands to execute when the pattern matches
- * `type`: Currently only `"command"` is supported
- * `command`: The bash command to execute (can use `$CLAUDE_PROJECT_DIR`
- environment variable)
- * `timeout`: (Optional) How long a command should run, in seconds, before
- canceling that specific command.
-
-For events like `UserPromptSubmit`, `Notification`, `Stop`, and `SubagentStop`
-that don't use matchers, you can omit the matcher field:
-
-```json theme={null}
-{
- "hooks": {
- "UserPromptSubmit": [
- {
- "hooks": [
- {
- "type": "command",
- "command": "/path/to/prompt-validator.py"
- }
- ]
- }
- ]
- }
-}
-```
-
-### Project-Specific Hook Scripts
-
-You can use the environment variable `CLAUDE_PROJECT_DIR` (only available when
-Claude Code spawns the hook command) to reference scripts stored in your project,
-ensuring they work regardless of Claude's current directory:
-
-```json theme={null}
-{
- "hooks": {
- "PostToolUse": [
- {
- "matcher": "Write|Edit",
- "hooks": [
- {
- "type": "command",
- "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/check-style.sh"
- }
- ]
- }
- ]
- }
-}
-```
-
-### Plugin hooks
-
-[Plugins](/en/docs/claude-code/plugins) can provide hooks that integrate seamlessly with your user and project hooks. Plugin hooks are automatically merged with your configuration when plugins are enabled.
-
-**How plugin hooks work**:
-
-* Plugin hooks are defined in the plugin's `hooks/hooks.json` file or in a file given by a custom path to the `hooks` field.
-* When a plugin is enabled, its hooks are merged with user and project hooks
-* Multiple hooks from different sources can respond to the same event
-* Plugin hooks use the `${CLAUDE_PLUGIN_ROOT}` environment variable to reference plugin files
-
-**Example plugin hook configuration**:
-
-```json theme={null}
-{
- "description": "Automatic code formatting",
- "hooks": {
- "PostToolUse": [
- {
- "matcher": "Write|Edit",
- "hooks": [
- {
- "type": "command",
- "command": "${CLAUDE_PLUGIN_ROOT}/scripts/format.sh",
- "timeout": 30
- }
- ]
- }
- ]
- }
-}
-```
-
-
- Plugin hooks use the same format as regular hooks with an optional `description` field to explain the hook's purpose.
-
-
-
- Plugin hooks run alongside your custom hooks. If multiple hooks match an event, they all execute in parallel.
-
-
-**Environment variables for plugins**:
-
-* `${CLAUDE_PLUGIN_ROOT}`: Absolute path to the plugin directory
-* `${CLAUDE_PROJECT_DIR}`: Project root directory (same as for project hooks)
-* All standard environment variables are available
-
-See the [plugin components reference](/en/docs/claude-code/plugins-reference#hooks) for details on creating plugin hooks.
-
-## Hook Events
-
-### PreToolUse
-
-Runs after Claude creates tool parameters and before processing the tool call.
-
-**Common matchers:**
-
-* `Task` - Subagent tasks (see [subagents documentation](/en/docs/claude-code/sub-agents))
-* `Bash` - Shell commands
-* `Glob` - File pattern matching
-* `Grep` - Content search
-* `Read` - File reading
-* `Edit` - File editing
-* `Write` - File writing
-* `WebFetch`, `WebSearch` - Web operations
-
-### PostToolUse
-
-Runs immediately after a tool completes successfully.
-
-Recognizes the same matcher values as PreToolUse.
-
-### Notification
-
-Runs when Claude Code sends notifications. Notifications are sent when:
-
-1. Claude needs your permission to use a tool. Example: "Claude needs your
- permission to use Bash"
-2. The prompt input has been idle for at least 60 seconds. "Claude is waiting
- for your input"
-
-### UserPromptSubmit
-
-Runs when the user submits a prompt, before Claude processes it. This allows you
-to add additional context based on the prompt/conversation, validate prompts, or
-block certain types of prompts.
-
-### Stop
-
-Runs when the main Claude Code agent has finished responding. Does not run if
-the stoppage occurred due to a user interrupt.
-
-### SubagentStop
-
-Runs when a Claude Code subagent (Task tool call) has finished responding.
-
-### PreCompact
-
-Runs before Claude Code is about to run a compact operation.
-
-**Matchers:**
-
-* `manual` - Invoked from `/compact`
-* `auto` - Invoked from auto-compact (due to full context window)
-
-### SessionStart
-
-Runs when Claude Code starts a new session or resumes an existing session (which
-currently does start a new session under the hood). Useful for loading in
-development context like existing issues or recent changes to your codebase.
-
-**Matchers:**
-
-* `startup` - Invoked from startup
-* `resume` - Invoked from `--resume`, `--continue`, or `/resume`
-* `clear` - Invoked from `/clear`
-* `compact` - Invoked from auto or manual compact.
-
-### SessionEnd
-
-Runs when a Claude Code session ends. Useful for cleanup tasks, logging session
-statistics, or saving session state.
-
-The `reason` field in the hook input will be one of:
-
-* `clear` - Session cleared with /clear command
-* `logout` - User logged out
-* `prompt_input_exit` - User exited while prompt input was visible
-* `other` - Other exit reasons
-
-## Hook Input
-
-Hooks receive JSON data via stdin containing session information and
-event-specific data:
-
-```typescript theme={null}
-{
- // Common fields
- session_id: string
- transcript_path: string // Path to conversation JSON
- cwd: string // The current working directory when the hook is invoked
-
- // Event-specific fields
- hook_event_name: string
- ...
-}
-```
-
-### PreToolUse Input
-
-The exact schema for `tool_input` depends on the tool.
-
-```json theme={null}
-{
- "session_id": "abc123",
- "transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
- "cwd": "/Users/...",
- "hook_event_name": "PreToolUse",
- "tool_name": "Write",
- "tool_input": {
- "file_path": "/path/to/file.txt",
- "content": "file content"
- }
-}
-```
-
-### PostToolUse Input
-
-The exact schema for `tool_input` and `tool_response` depends on the tool.
-
-```json theme={null}
-{
- "session_id": "abc123",
- "transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
- "cwd": "/Users/...",
- "hook_event_name": "PostToolUse",
- "tool_name": "Write",
- "tool_input": {
- "file_path": "/path/to/file.txt",
- "content": "file content"
- },
- "tool_response": {
- "filePath": "/path/to/file.txt",
- "success": true
- }
-}
-```
-
-### Notification Input
-
-```json theme={null}
-{
- "session_id": "abc123",
- "transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
- "cwd": "/Users/...",
- "hook_event_name": "Notification",
- "message": "Task completed successfully"
-}
-```
-
-### UserPromptSubmit Input
-
-```json theme={null}
-{
- "session_id": "abc123",
- "transcript_path": "/Users/.../.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
- "cwd": "/Users/...",
- "hook_event_name": "UserPromptSubmit",
- "prompt": "Write a function to calculate the factorial of a number"
-}
-```
-
-### Stop and SubagentStop Input
-
-`stop_hook_active` is true when Claude Code is already continuing as a result of
-a stop hook. Check this value or process the transcript to prevent Claude Code
-from running indefinitely.
-
-```json theme={null}
-{
- "session_id": "abc123",
- "transcript_path": "~/.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
- "hook_event_name": "Stop",
- "stop_hook_active": true
-}
-```
-
-### PreCompact Input
-
-For `manual`, `custom_instructions` comes from what the user passes into
-`/compact`. For `auto`, `custom_instructions` is empty.
-
-```json theme={null}
-{
- "session_id": "abc123",
- "transcript_path": "~/.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
- "hook_event_name": "PreCompact",
- "trigger": "manual",
- "custom_instructions": ""
-}
-```
-
-### SessionStart Input
-
-```json theme={null}
-{
- "session_id": "abc123",
- "transcript_path": "~/.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
- "hook_event_name": "SessionStart",
- "source": "startup"
-}
-```
-
-### SessionEnd Input
-
-```json theme={null}
-{
- "session_id": "abc123",
- "transcript_path": "~/.claude/projects/.../00893aaf-19fa-41d2-8238-13269b9b3ca0.jsonl",
- "cwd": "/Users/...",
- "hook_event_name": "SessionEnd",
- "reason": "exit"
-}
-```
-
-## Hook Output
-
-There are two ways for hooks to return output back to Claude Code. The output
-communicates whether to block and any feedback that should be shown to Claude
-and the user.
-
-### Simple: Exit Code
-
-Hooks communicate status through exit codes, stdout, and stderr:
-
-* **Exit code 0**: Success. `stdout` is shown to the user in transcript mode
- (CTRL-R), except for `UserPromptSubmit` and `SessionStart`, where stdout is
- added to the context.
-* **Exit code 2**: Blocking error. `stderr` is fed back to Claude to process
- automatically. See per-hook-event behavior below.
-* **Other exit codes**: Non-blocking error. `stderr` is shown to the user and
- execution continues.
-
-
- Reminder: Claude Code does not see stdout if the exit code is 0, except for
- the `UserPromptSubmit` hook where stdout is injected as context.
-
-
-#### Exit Code 2 Behavior
-
-| Hook Event | Behavior |
-| ------------------ | ------------------------------------------------------------------ |
-| `PreToolUse` | Blocks the tool call, shows stderr to Claude |
-| `PostToolUse` | Shows stderr to Claude (tool already ran) |
-| `Notification` | N/A, shows stderr to user only |
-| `UserPromptSubmit` | Blocks prompt processing, erases prompt, shows stderr to user only |
-| `Stop` | Blocks stoppage, shows stderr to Claude |
-| `SubagentStop` | Blocks stoppage, shows stderr to Claude subagent |
-| `PreCompact` | N/A, shows stderr to user only |
-| `SessionStart` | N/A, shows stderr to user only |
-| `SessionEnd` | N/A, shows stderr to user only |
-
-### Advanced: JSON Output
-
-Hooks can return structured JSON in `stdout` for more sophisticated control:
-
-#### Common JSON Fields
-
-All hook types can include these optional fields:
-
-```json theme={null}
-{
- "continue": true, // Whether Claude should continue after hook execution (default: true)
- "stopReason": "string", // Message shown when continue is false
-
- "suppressOutput": true, // Hide stdout from transcript mode (default: false)
- "systemMessage": "string" // Optional warning message shown to the user
-}
-```
-
-If `continue` is false, Claude stops processing after the hooks run.
-
-* For `PreToolUse`, this is different from `"permissionDecision": "deny"`, which
- only blocks a specific tool call and provides automatic feedback to Claude.
-* For `PostToolUse`, this is different from `"decision": "block"`, which
- provides automated feedback to Claude.
-* For `UserPromptSubmit`, this prevents the prompt from being processed.
-* For `Stop` and `SubagentStop`, this takes precedence over any
- `"decision": "block"` output.
-* In all cases, `"continue" = false` takes precedence over any
- `"decision": "block"` output.
-
-`stopReason` accompanies `continue` with a reason shown to the user, not shown
-to Claude.
-
-#### `PreToolUse` Decision Control
-
-`PreToolUse` hooks can control whether a tool call proceeds.
-
-* `"allow"` bypasses the permission system. `permissionDecisionReason` is shown
- to the user but not to Claude.
-* `"deny"` prevents the tool call from executing. `permissionDecisionReason` is
- shown to Claude.
-* `"ask"` asks the user to confirm the tool call in the UI.
- `permissionDecisionReason` is shown to the user but not to Claude.
-
-```json theme={null}
-{
- "hookSpecificOutput": {
- "hookEventName": "PreToolUse",
- "permissionDecision": "allow" | "deny" | "ask",
- "permissionDecisionReason": "My reason here"
- }
-}
-```
-
-
- The `decision` and `reason` fields are deprecated for PreToolUse hooks.
- Use `hookSpecificOutput.permissionDecision` and
- `hookSpecificOutput.permissionDecisionReason` instead. The deprecated fields
- `"approve"` and `"block"` map to `"allow"` and `"deny"` respectively.
-
-
-#### `PostToolUse` Decision Control
-
-`PostToolUse` hooks can provide feedback to Claude after tool execution.
-
-* `"block"` automatically prompts Claude with `reason`.
-* `undefined` does nothing. `reason` is ignored.
-* `"hookSpecificOutput.additionalContext"` adds context for Claude to consider.
-
-```json theme={null}
-{
- "decision": "block" | undefined,
- "reason": "Explanation for decision",
- "hookSpecificOutput": {
- "hookEventName": "PostToolUse",
- "additionalContext": "Additional information for Claude"
- }
-}
-```
-
-#### `UserPromptSubmit` Decision Control
-
-`UserPromptSubmit` hooks can control whether a user prompt is processed.
-
-* `"block"` prevents the prompt from being processed. The submitted prompt is
- erased from context. `"reason"` is shown to the user but not added to context.
-* `undefined` allows the prompt to proceed normally. `"reason"` is ignored.
-* `"hookSpecificOutput.additionalContext"` adds the string to the context if not
- blocked.
-
-```json theme={null}
-{
- "decision": "block" | undefined,
- "reason": "Explanation for decision",
- "hookSpecificOutput": {
- "hookEventName": "UserPromptSubmit",
- "additionalContext": "My additional context here"
- }
-}
-```
-
-#### `Stop`/`SubagentStop` Decision Control
-
-`Stop` and `SubagentStop` hooks can control whether Claude must continue.
-
-* `"block"` prevents Claude from stopping. You must populate `reason` for Claude
- to know how to proceed.
-* `undefined` allows Claude to stop. `reason` is ignored.
-
-```json theme={null}
-{
- "decision": "block" | undefined,
- "reason": "Must be provided when Claude is blocked from stopping"
-}
-```
-
-#### `SessionStart` Decision Control
-
-`SessionStart` hooks allow you to load in context at the start of a session.
-
-* `"hookSpecificOutput.additionalContext"` adds the string to the context.
-* Multiple hooks' `additionalContext` values are concatenated.
-
-```json theme={null}
-{
- "hookSpecificOutput": {
- "hookEventName": "SessionStart",
- "additionalContext": "My additional context here"
- }
-}
-```
-
-#### `SessionEnd` Decision Control
-
-`SessionEnd` hooks run when a session ends. They cannot block session termination
-but can perform cleanup tasks.
-
-#### Exit Code Example: Bash Command Validation
-
-```python theme={null}
-#!/usr/bin/env python3
-import json
-import re
-import sys
-
-# Define validation rules as a list of (regex pattern, message) tuples
-VALIDATION_RULES = [
- (
- r"\bgrep\b(?!.*\|)",
- "Use 'rg' (ripgrep) instead of 'grep' for better performance and features",
- ),
- (
- r"\bfind\s+\S+\s+-name\b",
- "Use 'rg --files | rg pattern' or 'rg --files -g pattern' instead of 'find -name' for better performance",
- ),
-]
-
-
-def validate_command(command: str) -> list[str]:
- issues = []
- for pattern, message in VALIDATION_RULES:
- if re.search(pattern, command):
- issues.append(message)
- return issues
-
-
-try:
- input_data = json.load(sys.stdin)
-except json.JSONDecodeError as e:
- print(f"Error: Invalid JSON input: {e}", file=sys.stderr)
- sys.exit(1)
-
-tool_name = input_data.get("tool_name", "")
-tool_input = input_data.get("tool_input", {})
-command = tool_input.get("command", "")
-
-if tool_name != "Bash" or not command:
- sys.exit(1)
-
-# Validate the command
-issues = validate_command(command)
-
-if issues:
- for message in issues:
- print(f"• {message}", file=sys.stderr)
- # Exit code 2 blocks tool call and shows stderr to Claude
- sys.exit(2)
-```
-
-#### JSON Output Example: UserPromptSubmit to Add Context and Validation
-
-
- For `UserPromptSubmit` hooks, you can inject context using either method:
-
- * Exit code 0 with stdout: Claude sees the context (special case for `UserPromptSubmit`)
- * JSON output: Provides more control over the behavior
-
-
-```python theme={null}
-#!/usr/bin/env python3
-import json
-import sys
-import re
-import datetime
-
-# Load input from stdin
-try:
- input_data = json.load(sys.stdin)
-except json.JSONDecodeError as e:
- print(f"Error: Invalid JSON input: {e}", file=sys.stderr)
- sys.exit(1)
-
-prompt = input_data.get("prompt", "")
-
-# Check for sensitive patterns
-sensitive_patterns = [
- (r"(?i)\b(password|secret|key|token)\s*[:=]", "Prompt contains potential secrets"),
-]
-
-for pattern, message in sensitive_patterns:
- if re.search(pattern, prompt):
- # Use JSON output to block with a specific reason
- output = {
- "decision": "block",
- "reason": f"Security policy violation: {message}. Please rephrase your request without sensitive information."
- }
- print(json.dumps(output))
- sys.exit(0)
-
-# Add current time to context
-context = f"Current time: {datetime.datetime.now()}"
-print(context)
-
-"""
-The following is also equivalent:
-print(json.dumps({
- "hookSpecificOutput": {
- "hookEventName": "UserPromptSubmit",
- "additionalContext": context,
- },
-}))
-"""
-
-# Allow the prompt to proceed with the additional context
-sys.exit(0)
-```
-
-#### JSON Output Example: PreToolUse with Approval
-
-```python theme={null}
-#!/usr/bin/env python3
-import json
-import sys
-
-# Load input from stdin
-try:
- input_data = json.load(sys.stdin)
-except json.JSONDecodeError as e:
- print(f"Error: Invalid JSON input: {e}", file=sys.stderr)
- sys.exit(1)
-
-tool_name = input_data.get("tool_name", "")
-tool_input = input_data.get("tool_input", {})
-
-# Example: Auto-approve file reads for documentation files
-if tool_name == "Read":
- file_path = tool_input.get("file_path", "")
- if file_path.endswith((".md", ".mdx", ".txt", ".json")):
- # Use JSON output to auto-approve the tool call
- output = {
- "decision": "approve",
- "reason": "Documentation file auto-approved",
- "suppressOutput": True # Don't show in transcript mode
- }
- print(json.dumps(output))
- sys.exit(0)
-
-# For other cases, let the normal permission flow proceed
-sys.exit(0)
-```
-
-## Working with MCP Tools
-
-Claude Code hooks work seamlessly with
-[Model Context Protocol (MCP) tools](/en/docs/claude-code/mcp). When MCP servers
-provide tools, they appear with a special naming pattern that you can match in
-your hooks.
-
-### MCP Tool Naming
-
-MCP tools follow the pattern `mcp____`, for example:
-
-* `mcp__memory__create_entities` - Memory server's create entities tool
-* `mcp__filesystem__read_file` - Filesystem server's read file tool
-* `mcp__github__search_repositories` - GitHub server's search tool
-
-### Configuring Hooks for MCP Tools
-
-You can target specific MCP tools or entire MCP servers:
-
-```json theme={null}
-{
- "hooks": {
- "PreToolUse": [
- {
- "matcher": "mcp__memory__.*",
- "hooks": [
- {
- "type": "command",
- "command": "echo 'Memory operation initiated' >> ~/mcp-operations.log"
- }
- ]
- },
- {
- "matcher": "mcp__.*__write.*",
- "hooks": [
- {
- "type": "command",
- "command": "/home/user/scripts/validate-mcp-write.py"
- }
- ]
- }
- ]
- }
-}
-```
-
-## Examples
-
-
- For practical examples including code formatting, notifications, and file protection, see [More Examples](/en/docs/claude-code/hooks-guide#more-examples) in the get started guide.
-
-
-## Security Considerations
-
-### Disclaimer
-
-**USE AT YOUR OWN RISK**: Claude Code hooks execute arbitrary shell commands on
-your system automatically. By using hooks, you acknowledge that:
-
-* You are solely responsible for the commands you configure
-* Hooks can modify, delete, or access any files your user account can access
-* Malicious or poorly written hooks can cause data loss or system damage
-* Anthropic provides no warranty and assumes no liability for any damages
- resulting from hook usage
-* You should thoroughly test hooks in a safe environment before production use
-
-Always review and understand any hook commands before adding them to your
-configuration.
-
-### Security Best Practices
-
-Here are some key practices for writing more secure hooks:
-
-1. **Validate and sanitize inputs** - Never trust input data blindly
-2. **Always quote shell variables** - Use `"$VAR"` not `$VAR`
-3. **Block path traversal** - Check for `..` in file paths
-4. **Use absolute paths** - Specify full paths for scripts (use
- "\$CLAUDE\_PROJECT\_DIR" for the project path)
-5. **Skip sensitive files** - Avoid `.env`, `.git/`, keys, etc.
-
-### Configuration Safety
-
-Direct edits to hooks in settings files don't take effect immediately. Claude
-Code:
-
-1. Captures a snapshot of hooks at startup
-2. Uses this snapshot throughout the session
-3. Warns if hooks are modified externally
-4. Requires review in `/hooks` menu for changes to apply
-
-This prevents malicious hook modifications from affecting your current session.
-
-## Hook Execution Details
-
-* **Timeout**: 60-second execution limit by default, configurable per command.
- * A timeout for an individual command does not affect the other commands.
-* **Parallelization**: All matching hooks run in parallel
-* **Deduplication**: Multiple identical hook commands are deduplicated automatically
-* **Environment**: Runs in current directory with Claude Code's environment
- * The `CLAUDE_PROJECT_DIR` environment variable is available and contains the
- absolute path to the project root directory (where Claude Code was started)
-* **Input**: JSON via stdin
-* **Output**:
- * PreToolUse/PostToolUse/Stop/SubagentStop: Progress shown in transcript (Ctrl-R)
- * Notification/SessionEnd: Logged to debug only (`--debug`)
- * UserPromptSubmit/SessionStart: stdout added as context for Claude
-
-## Debugging
-
-### Basic Troubleshooting
-
-If your hooks aren't working:
-
-1. **Check configuration** - Run `/hooks` to see if your hook is registered
-2. **Verify syntax** - Ensure your JSON settings are valid
-3. **Test commands** - Run hook commands manually first
-4. **Check permissions** - Make sure scripts are executable
-5. **Review logs** - Use `claude --debug` to see hook execution details
-
-Common issues:
-
-* **Quotes not escaped** - Use `\"` inside JSON strings
-* **Wrong matcher** - Check tool names match exactly (case-sensitive)
-* **Command not found** - Use full paths for scripts
-
-### Advanced Debugging
-
-For complex hook issues:
-
-1. **Inspect hook execution** - Use `claude --debug` to see detailed hook
- execution
-2. **Validate JSON schemas** - Test hook input/output with external tools
-3. **Check environment variables** - Verify Claude Code's environment is correct
-4. **Test edge cases** - Try hooks with unusual file paths or inputs
-5. **Monitor system resources** - Check for resource exhaustion during hook
- execution
-6. **Use structured logging** - Implement logging in your hook scripts
-
-### Debug Output Example
-
-Use `claude --debug` to see hook execution details:
-
-```
-[DEBUG] Executing hooks for PostToolUse:Write
-[DEBUG] Getting matching hook commands for PostToolUse with query: Write
-[DEBUG] Found 1 hook matchers in settings
-[DEBUG] Matched 1 hooks for query "Write"
-[DEBUG] Found 1 hook commands to execute
-[DEBUG] Executing hook command: with timeout 60000ms
-[DEBUG] Hook command completed with status 0:
-```
-
-Progress messages appear in transcript mode (Ctrl-R) showing:
-
-* Which hook is running
-* Command being executed
-* Success/failure status
-* Output or error messages
diff --git a/docs/context/claude-code/marketplace.md b/docs/context/claude-code/marketplace.md
deleted file mode 100644
index e73cc7bd..00000000
--- a/docs/context/claude-code/marketplace.md
+++ /dev/null
@@ -1,391 +0,0 @@
-# Plugins
-
-> Extend Claude Code with custom commands, agents, hooks, and MCP servers through the plugin system.
-
-
- For complete technical specifications and schemas, see [Plugins reference](/en/docs/claude-code/plugins-reference). For marketplace management, see [Plugin marketplaces](/en/docs/claude-code/plugin-marketplaces).
-
-
-Plugins let you extend Claude Code with custom functionality that can be shared across projects and teams. Install plugins from [marketplaces](/en/docs/claude-code/plugin-marketplaces) to add pre-built commands, agents, hooks, and MCP servers, or create your own to automate your workflows.
-
-## Quickstart
-
-Let's create a simple greeting plugin to get you familiar with the plugin system. We'll build a working plugin that adds a custom command, test it locally, and understand the core concepts.
-
-### Prerequisites
-
-* Claude Code installed on your machine
-* Basic familiarity with command-line tools
-
-### Create your first plugin
-
-
-
- ```bash theme={null}
- mkdir test-marketplace
- cd test-marketplace
- ```
-
-
-
- ```bash theme={null}
- mkdir my-first-plugin
- cd my-first-plugin
- ```
-
-
-
- ```bash Create .claude-plugin/plugin.json theme={null}
- mkdir .claude-plugin
- cat > .claude-plugin/plugin.json << 'EOF'
- {
- "name": "my-first-plugin",
- "description": "A simple greeting plugin to learn the basics",
- "version": "1.0.0",
- "author": {
- "name": "Your Name"
- }
- }
- EOF
- ```
-
-
-
- ```bash Create commands/hello.md theme={null}
- mkdir commands
- cat > commands/hello.md << 'EOF'
- ---
- description: Greet the user with a personalized message
- ---
-
- # Hello Command
-
- Greet the user warmly and ask how you can help them today. Make the greeting personal and encouraging.
- EOF
- ```
-
-
-
- ```bash Create marketplace.json theme={null}
- cd ..
- mkdir .claude-plugin
- cat > .claude-plugin/marketplace.json << 'EOF'
- {
- "name": "test-marketplace",
- "owner": {
- "name": "Test User"
- },
- "plugins": [
- {
- "name": "my-first-plugin",
- "source": "./my-first-plugin",
- "description": "My first test plugin"
- }
- ]
- }
- EOF
- ```
-
-
-
- ```bash Start Claude Code from parent directory theme={null}
- cd ..
- claude
- ```
-
- ```shell Add the test marketplace theme={null}
- /plugin marketplace add ./test-marketplace
- ```
-
- ```shell Install your plugin theme={null}
- /plugin install my-first-plugin@test-marketplace
- ```
-
- Select "Install now". You'll then need to restart Claude Code in order to use the new plugin.
-
- ```shell Try your new command theme={null}
- /hello
- ```
-
- You'll see Claude use your greeting command! Check `/help` to see your new command listed.
-
-
-
-You've successfully created and tested a plugin with these key components:
-
-* **Plugin manifest** (`.claude-plugin/plugin.json`) - Describes your plugin's metadata
-* **Commands directory** (`commands/`) - Contains your custom slash commands
-* **Test marketplace** - Allows you to test your plugin locally
-
-### Plugin structure overview
-
-Your plugin follows this basic structure:
-
-```
-my-first-plugin/
-├── .claude-plugin/
-│ └── plugin.json # Plugin metadata
-├── commands/ # Custom slash commands (optional)
-│ └── hello.md
-├── agents/ # Custom agents (optional)
-│ └── helper.md
-├── skills/ # Agent Skills (optional)
-│ └── my-skill/
-│ └── SKILL.md
-└── hooks/ # Event handlers (optional)
- └── hooks.json
-```
-
-**Additional components you can add:**
-
-* **Commands**: Create markdown files in `commands/` directory
-* **Agents**: Create agent definitions in `agents/` directory
-* **Skills**: Create `SKILL.md` files in `skills/` directory
-* **Hooks**: Create `hooks/hooks.json` for event handling
-* **MCP servers**: Create `.mcp.json` for external tool integration
-
-
- **Next steps**: Ready to add more features? Jump to [Develop more complex plugins](#develop-more-complex-plugins) to add agents, hooks, and MCP servers. For complete technical specifications of all plugin components, see [Plugins reference](/en/docs/claude-code/plugins-reference).
-
-
-***
-
-## Install and manage plugins
-
-Learn how to discover, install, and manage plugins to extend your Claude Code capabilities.
-
-### Prerequisites
-
-* Claude Code installed and running
-* Basic familiarity with command-line interfaces
-
-### Add marketplaces
-
-Marketplaces are catalogs of available plugins. Add them to discover and install plugins:
-
-```shell Add a marketplace theme={null}
-/plugin marketplace add your-org/claude-plugins
-```
-
-```shell Browse available plugins theme={null}
-/plugin
-```
-
-For detailed marketplace management including Git repositories, local development, and team distribution, see [Plugin marketplaces](/en/docs/claude-code/plugin-marketplaces).
-
-### Install plugins
-
-#### Via interactive menu (recommended for discovery)
-
-```shell Open the plugin management interface theme={null}
-/plugin
-```
-
-Select "Browse Plugins" to see available options with descriptions, features, and installation options.
-
-#### Via direct commands (for quick installation)
-
-```shell Install a specific plugin theme={null}
-/plugin install formatter@your-org
-```
-
-```shell Enable a disabled plugin theme={null}
-/plugin enable plugin-name@marketplace-name
-```
-
-```shell Disable without uninstalling theme={null}
-/plugin disable plugin-name@marketplace-name
-```
-
-```shell Completely remove a plugin theme={null}
-/plugin uninstall plugin-name@marketplace-name
-```
-
-### Verify installation
-
-After installing a plugin:
-
-1. **Check available commands**: Run `/help` to see new commands
-2. **Test plugin features**: Try the plugin's commands and features
-3. **Review plugin details**: Use `/plugin` → "Manage Plugins" to see what the plugin provides
-
-## Set up team plugin workflows
-
-Configure plugins at the repository level to ensure consistent tooling across your team. When team members trust your repository folder, Claude Code automatically installs specified marketplaces and plugins.
-
-**To set up team plugins:**
-
-1. Add marketplace and plugin configuration to your repository's `.claude/settings.json`
-2. Team members trust the repository folder
-3. Plugins install automatically for all team members
-
-For complete instructions including configuration examples, marketplace setup, and rollout best practices, see [Configure team marketplaces](/en/docs/claude-code/plugin-marketplaces#how-to-configure-team-marketplaces).
-
-***
-
-## Develop more complex plugins
-
-Once you're comfortable with basic plugins, you can create more sophisticated extensions.
-
-### Add Skills to your plugin
-
-Plugins can include [Agent Skills](/en/docs/claude-code/skills) to extend Claude's capabilities. Skills are model-invoked—Claude autonomously uses them based on the task context.
-
-To add Skills to your plugin, create a `skills/` directory at your plugin root and add Skill folders with `SKILL.md` files. Plugin Skills are automatically available when the plugin is installed.
-
-For complete Skill authoring guidance, see [Agent Skills](/en/docs/claude-code/skills).
-
-### Organize complex plugins
-
-For plugins with many components, organize your directory structure by functionality. For complete directory layouts and organization patterns, see [Plugin directory structure](/en/docs/claude-code/plugins-reference#plugin-directory-structure).
-
-### Test your plugins locally
-
-When developing plugins, use a local marketplace to test changes iteratively. This workflow builds on the quickstart pattern and works for plugins of any complexity.
-
-
-
- Organize your plugin and marketplace for testing:
-
- ```bash Create directory structure theme={null}
- mkdir dev-marketplace
- cd dev-marketplace
- mkdir my-plugin
- ```
-
- This creates:
-
- ```
- dev-marketplace/
- ├── .claude-plugin/marketplace.json (you'll create this)
- └── my-plugin/ (your plugin under development)
- ├── .claude-plugin/plugin.json
- ├── commands/
- ├── agents/
- └── hooks/
- ```
-
-
-
- ```bash Create marketplace.json theme={null}
- mkdir .claude-plugin
- cat > .claude-plugin/marketplace.json << 'EOF'
- {
- "name": "dev-marketplace",
- "owner": {
- "name": "Developer"
- },
- "plugins": [
- {
- "name": "my-plugin",
- "source": "./my-plugin",
- "description": "Plugin under development"
- }
- ]
- }
- EOF
- ```
-
-
-
- ```bash Start Claude Code from parent directory theme={null}
- cd ..
- claude
- ```
-
- ```shell Add your development marketplace theme={null}
- /plugin marketplace add ./dev-marketplace
- ```
-
- ```shell Install your plugin theme={null}
- /plugin install my-plugin@dev-marketplace
- ```
-
- Test your plugin components:
-
- * Try your commands with `/command-name`
- * Check that agents appear in `/agents`
- * Verify hooks work as expected
-
-
-
- After making changes to your plugin code:
-
- ```shell Uninstall the current version theme={null}
- /plugin uninstall my-plugin@dev-marketplace
- ```
-
- ```shell Reinstall to test changes theme={null}
- /plugin install my-plugin@dev-marketplace
- ```
-
- Repeat this cycle as you develop and refine your plugin.
-
-
-
-
- **For multiple plugins**: Organize plugins in subdirectories like `./plugins/plugin-name` and update your marketplace.json accordingly. See [Plugin sources](/en/docs/claude-code/plugin-marketplaces#plugin-sources) for organization patterns.
-
-
-### Debug plugin issues
-
-If your plugin isn't working as expected:
-
-1. **Check the structure**: Ensure your directories are at the plugin root, not inside `.claude-plugin/`
-2. **Test components individually**: Check each command, agent, and hook separately
-3. **Use validation and debugging tools**: See [Debugging and development tools](/en/docs/claude-code/plugins-reference#debugging-and-development-tools) for CLI commands and troubleshooting techniques
-
-### Share your plugins
-
-When your plugin is ready to share:
-
-1. **Add documentation**: Include a README.md with installation and usage instructions
-2. **Version your plugin**: Use semantic versioning in your `plugin.json`
-3. **Create or use a marketplace**: Distribute through plugin marketplaces for easy installation
-4. **Test with others**: Have team members test the plugin before wider distribution
-
-
- For complete technical specifications, debugging techniques, and distribution strategies, see [Plugins reference](/en/docs/claude-code/plugins-reference).
-
-
-***
-
-## Next steps
-
-Now that you understand Claude Code's plugin system, here are suggested paths for different goals:
-
-### For plugin users
-
-* **Discover plugins**: Browse community marketplaces for useful tools
-* **Team adoption**: Set up repository-level plugins for your projects
-* **Marketplace management**: Learn to manage multiple plugin sources
-* **Advanced usage**: Explore plugin combinations and workflows
-
-### For plugin developers
-
-* **Create your first marketplace**: [Plugin marketplaces guide](/en/docs/claude-code/plugin-marketplaces)
-* **Advanced components**: Dive deeper into specific plugin components:
- * [Slash commands](/en/docs/claude-code/slash-commands) - Command development details
- * [Subagents](/en/docs/claude-code/sub-agents) - Agent configuration and capabilities
- * [Agent Skills](/en/docs/claude-code/skills) - Extend Claude's capabilities
- * [Hooks](/en/docs/claude-code/hooks) - Event handling and automation
- * [MCP](/en/docs/claude-code/mcp) - External tool integration
-* **Distribution strategies**: Package and share your plugins effectively
-* **Community contribution**: Consider contributing to community plugin collections
-
-### For team leads and administrators
-
-* **Repository configuration**: Set up automatic plugin installation for team projects
-* **Plugin governance**: Establish guidelines for plugin approval and security review
-* **Marketplace maintenance**: Create and maintain organization-specific plugin catalogs
-* **Training and documentation**: Help team members adopt plugin workflows effectively
-
-## See also
-
-* [Plugin marketplaces](/en/docs/claude-code/plugin-marketplaces) - Creating and managing plugin catalogs
-* [Slash commands](/en/docs/claude-code/slash-commands) - Understanding custom commands
-* [Subagents](/en/docs/claude-code/sub-agents) - Creating and using specialized agents
-* [Agent Skills](/en/docs/claude-code/skills) - Extend Claude's capabilities
-* [Hooks](/en/docs/claude-code/hooks) - Automating workflows with event handlers
-* [MCP](/en/docs/claude-code/mcp) - Connecting to external tools and services
-* [Settings](/en/docs/claude-code/settings) - Configuration options for plugins
diff --git a/docs/context/claude-code/models.md b/docs/context/claude-code/models.md
deleted file mode 100644
index 2dc7deae..00000000
--- a/docs/context/claude-code/models.md
+++ /dev/null
@@ -1,218 +0,0 @@
-# Models overview
-
-> Claude is a family of state-of-the-art large language models developed by Anthropic. This guide introduces our models and compares their performance with legacy models.
-
-export const ModelId = ({children, style = {}}) => {
- const copiedNotice = 'Copied!';
- const handleClick = e => {
- const element = e.currentTarget;
- const originalText = element.textContent;
- navigator.clipboard.writeText(children).then(() => {
- element.textContent = copiedNotice;
- element.style.backgroundColor = '#d4edda';
- element.style.color = '#155724';
- element.style.borderColor = '#c3e6cb';
- setTimeout(() => {
- element.textContent = originalText;
- element.style.backgroundColor = '#f5f5f5';
- element.style.color = '';
- element.style.borderColor = 'transparent';
- }, 2000);
- }).catch(error => {
- console.error('Failed to copy:', error);
- });
- };
- const handleMouseEnter = e => {
- const element = e.currentTarget;
- const tooltip = element.querySelector('.copy-tooltip');
- if (tooltip && element.textContent !== copiedNotice) {
- tooltip.style.opacity = '1';
- }
- element.style.backgroundColor = '#e8e8e8';
- element.style.borderColor = '#d0d0d0';
- };
- const handleMouseLeave = e => {
- const element = e.currentTarget;
- const tooltip = element.querySelector('.copy-tooltip');
- if (tooltip) {
- tooltip.style.opacity = '0';
- }
- if (element.textContent !== copiedNotice) {
- element.style.backgroundColor = '#f5f5f5';
- element.style.borderColor = 'transparent';
- }
- };
- const defaultStyle = {
- cursor: 'pointer',
- position: 'relative',
- transition: 'all 0.2s ease',
- display: 'inline-block',
- userSelect: 'none',
- backgroundColor: '#f5f5f5',
- padding: '2px 4px',
- borderRadius: '4px',
- fontFamily: 'Monaco, Consolas, "Courier New", monospace',
- fontSize: '0.9em',
- border: '1px solid transparent',
- ...style
- };
- return
- {children}
- ;
-};
-
-
-
- Our best model for complex agents and coding
-
- * Text and image input
- * Text output
- * 200k context window (1M context beta available)
- * Highest intelligence across most tasks
-
-
-
- Our fastest and most intelligent Haiku model
-
- * Text and image input
- * Text output
- * 200k context window
- * Lightning-fast speed with extended thinking
-
-
-
- Exceptional model for specialized complex tasks
-
- * Text and image input
- * Text output
- * 200k context window
- * Superior reasoning capabilities
-
-
-
-***
-
-## Model names
-
-| Model | Claude API | AWS Bedrock | GCP Vertex AI |
-| ----------------- | ------------------------------------------------------------------------------------------- | ------------------------------------------------------------ | ---------------------------------------------- |
-| Claude Sonnet 4.5 | claude-sonnet-4-5-20250929 | anthropic.claude-sonnet-4-5-20250929-v1:0 | claude-sonnet-4-5\@20250929 |
-| Claude Sonnet 4 | claude-sonnet-4-20250514 | anthropic.claude-sonnet-4-20250514-v1:0 | claude-sonnet-4\@20250514 |
-| Claude Sonnet 3.7 | claude-3-7-sonnet-20250219 (claude-3-7-sonnet-latest) | anthropic.claude-3-7-sonnet-20250219-v1:0 | claude-3-7-sonnet\@20250219 |
-| Claude Haiku 4.5 | claude-haiku-4-5-20251001 | anthropic.claude-haiku-4-5-20251001-v1:0 | claude-haiku-4-5\@20251001 |
-| Claude Haiku 3.5 | claude-3-5-haiku-20241022 (claude-3-5-haiku-latest) | anthropic.claude-3-5-haiku-20241022-v1:0 | claude-3-5-haiku\@20241022 |
-| Claude Haiku 3 | claude-3-haiku-20240307 | anthropic.claude-3-haiku-20240307-v1:0 | claude-3-haiku\@20240307 |
-| Claude Opus 4.1 | claude-opus-4-1-20250805 | anthropic.claude-opus-4-1-20250805-v1:0 | claude-opus-4-1\@20250805 |
-| Claude Opus 4 | claude-opus-4-20250514 | anthropic.claude-opus-4-20250514-v1:0 | claude-opus-4\@20250514 |
-
-Models with the same snapshot date (e.g., 20240620) are identical across all platforms and do not change. The snapshot date in the model name ensures consistency and allows developers to rely on stable performance across different environments.
-
-Starting with **Claude Sonnet 4.5 and all future models**, AWS Bedrock and Google Vertex AI offer two endpoint types: **global endpoints** (dynamic routing for maximum availability) and **regional endpoints** (guaranteed data routing through specific geographic regions). For more information, see the [third-party platform pricing section](/en/docs/about-claude/pricing#third-party-platform-pricing).
-
-### Model aliases
-
-For convenience during development and testing, we offer aliases for our model ids. These aliases automatically point to the most recent snapshot of a given model. When we release new model snapshots, we migrate aliases to point to the newest version of a model, typically within a week of the new release.
-
-
- While aliases are useful for experimentation, we recommend using specific model versions (e.g., `claude-sonnet-4-5-20250929`) in production applications to ensure consistent behavior.
-
-
-| Model | Alias | Model ID |
-| ----------------- | ------------------------------------------- | --------------------------------------------- |
-| Claude Sonnet 4.5 | claude-sonnet-4-5 | claude-sonnet-4-5-20250929 |
-| Claude Sonnet 4 | claude-sonnet-4-0 | claude-sonnet-4-20250514 |
-| Claude Sonnet 3.7 | claude-3-7-sonnet-latest | claude-3-7-sonnet-20250219 |
-| Claude Haiku 4.5 | claude-haiku-4-5 | claude-haiku-4-5-20251001 |
-| Claude Haiku 3.5 | claude-3-5-haiku-latest | claude-3-5-haiku-20241022 |
-| Claude Opus 4.1 | claude-opus-4-1 | claude-opus-4-1-20250805 |
-| Claude Opus 4 | claude-opus-4-0 | claude-opus-4-20250514 |
-
-
- Aliases are subject to the same rate limits and pricing as the underlying model version they reference.
-
-
-### Model comparison table
-
-To help you choose the right model for your needs, we've compiled a table comparing the key features and capabilities of each model in the Claude family:
-
-| Feature | Claude Sonnet 4.5 | Claude Sonnet 4 | Claude Sonnet 3.7 | Claude Opus 4.1 | Claude Opus 4 | Claude Haiku 4.5 | Claude Haiku 3.5 | Claude Haiku 3 |
-| :-------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------- |
-| **Description** | Our best model for complex agents and coding | High-performance model | High-performance model with early extended thinking | Exceptional model for specialized complex tasks | Our previous flagship model | Our fastest and most intelligent Haiku model | Our fastest model | Fast and compact model for near-instant responsiveness |
-| **Strengths** | Highest intelligence across most tasks with exceptional agent and coding capabilities | High intelligence and balanced performance | High intelligence with toggleable extended thinking | Very high intelligence and capability for specialized tasks | Very high intelligence and capability | Near-frontier intelligence at blazing speeds with extended thinking and exceptional cost-efficiency | Intelligence at blazing speeds | Quick and accurate targeted performance |
-| **Multilingual** | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
-| **Vision** | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
-| **[Extended thinking](/en/docs/build-with-claude/extended-thinking)** | Yes | Yes | Yes | Yes | Yes | Yes | No | No |
-| **[Priority Tier](/en/api/service-tiers)** | Yes | Yes | Yes | Yes | Yes | Yes | Yes | No |
-| **API model name** | claude-sonnet-4-5-20250929 | claude-sonnet-4-20250514 | claude-3-7-sonnet-20250219 | claude-opus-4-1-20250805 | claude-opus-4-20250514 | claude-haiku-4-5-20251001 | claude-3-5-haiku-20241022 | claude-3-haiku-20240307 |
-| **Comparative latency** | Fast | Fast | Fast | Moderately Fast | Moderately Fast | Fastest | Fastest | Fast |
-| **Context window** | 200K /
1M (beta)1 | 200K /
1M (beta)1 | 200K | 200K | 200K | 200K | 200K | 200K |
-| **Max output** | 64000 tokens | 64000 tokens | 64000 tokens | 32000 tokens | 32000 tokens | 64000 tokens | 8192 tokens | 4096 tokens |
-| **Reliable knowledge cutoff** | Jan 20252 | Jan 20252 | Oct 20242 | Jan 20252 | Jan 20252 | Feb 2025 | 3 | 3 |
-| **Training data cutoff** | Jul 2025 | Mar 2025 | Nov 2024 | Mar 2025 | Mar 2025 | Jul 2025 | Jul 2024 | Aug 2023 |
-
-*1 - Claude Sonnet 4.5 and Claude Sonnet 4 support a [1M token context window](/en/docs/build-with-claude/context-windows#1m-token-context-window) when using the `context-1m-2025-08-07` beta header. [Long context pricing](/en/docs/about-claude/pricing#long-context-pricing) applies to requests exceeding 200K tokens.*
-
-*2 - **Reliable knowledge cutoff** indicates the date through which a model's knowledge is most extensive and reliable. **Training data cutoff** is the broader date range of training data used. For example, Claude Sonnet 4.5 was trained on publicly available information through July 2025, but its knowledge is most extensive and reliable through January 2025. For more information, see [Anthropic's Transparency Hub](https://www.anthropic.com/transparency).*
-
-*3 - Some Haiku models have a single training data cutoff date.*
-
-
- Include the beta header `output-128k-2025-02-19` in your API request to increase the maximum output token length to 128k tokens for Claude Sonnet 3.7.
-
- We strongly suggest using our [streaming Messages API](/en/docs/build-with-claude/streaming) to avoid timeouts when generating longer outputs.
- See our guidance on [long requests](/en/api/errors#long-requests) for more details.
-
-
-### Model pricing
-
-The table below shows the price per million tokens for each model:
-
-| Model | Base Input Tokens | 5m Cache Writes | 1h Cache Writes | Cache Hits & Refreshes | Output Tokens |
-| -------------------------------------------------------------------------- | ----------------- | --------------- | --------------- | ---------------------- | ------------- |
-| Claude Opus 4.1 | \$15 / MTok | \$18.75 / MTok | \$30 / MTok | \$1.50 / MTok | \$75 / MTok |
-| Claude Opus 4 | \$15 / MTok | \$18.75 / MTok | \$30 / MTok | \$1.50 / MTok | \$75 / MTok |
-| Claude Sonnet 4.5 | \$3 / MTok | \$3.75 / MTok | \$6 / MTok | \$0.30 / MTok | \$15 / MTok |
-| Claude Sonnet 4 | \$3 / MTok | \$3.75 / MTok | \$6 / MTok | \$0.30 / MTok | \$15 / MTok |
-| Claude Sonnet 3.7 | \$3 / MTok | \$3.75 / MTok | \$6 / MTok | \$0.30 / MTok | \$15 / MTok |
-| Claude Sonnet 3.5 ([deprecated](/en/docs/about-claude/model-deprecations)) | \$3 / MTok | \$3.75 / MTok | \$6 / MTok | \$0.30 / MTok | \$15 / MTok |
-| Claude Haiku 4.5 | \$1 / MTok | \$1.25 / MTok | \$2 / MTok | \$0.10 / MTok | \$5 / MTok |
-| Claude Haiku 3.5 | \$0.80 / MTok | \$1 / MTok | \$1.6 / MTok | \$0.08 / MTok | \$4 / MTok |
-| Claude Opus 3 ([deprecated](/en/docs/about-claude/model-deprecations)) | \$15 / MTok | \$18.75 / MTok | \$30 / MTok | \$1.50 / MTok | \$75 / MTok |
-| Claude Haiku 3 | \$0.25 / MTok | \$0.30 / MTok | \$0.50 / MTok | \$0.03 / MTok | \$1.25 / MTok |
-
-## Prompt and output performance
-
-Claude 4 models excel in:
-
-* **Performance**: Top-tier results in reasoning, coding, multilingual tasks, long-context handling, honesty, and image processing. See the [Claude 4 blog post](http://www.anthropic.com/news/claude-4) for more information.
-* **Engaging responses**: Claude models are ideal for applications that require rich, human-like interactions.
-
- * If you prefer more concise responses, you can adjust your prompts to guide the model toward the desired output length. Refer to our [prompt engineering guides](/en/docs/build-with-claude/prompt-engineering) for details.
- * For specific Claude 4 prompting best practices, see our [Claude 4 best practices guide](/en/docs/build-with-claude/prompt-engineering/claude-4-best-practices).
-* **Output quality**: When migrating from previous model generations to Claude 4, you may notice larger improvements in overall performance.
-
-## Migrating to Claude 4.5
-
-If you're currently using Claude 3 models, we recommend migrating to Claude 4.5 to take advantage of improved intelligence and enhanced capabilities. For detailed migration instructions, see [Migrating to Claude 4.5](/en/docs/about-claude/models/migrating-to-claude-4).
-
-## Get started with Claude
-
-If you're ready to start exploring what Claude can do for you, let's dive in! Whether you're a developer looking to integrate Claude into your applications or a user wanting to experience the power of AI firsthand, we've got you covered.
-
-Looking to chat with Claude? Visit [claude.ai](http://www.claude.ai)!
-
-
-
- Explore Claude’s capabilities and development flow.
-
-
-
- Learn how to make your first API call in minutes.
-
-
-
- Craft and test powerful prompts directly in your browser.
-
-
-
-If you have any questions or need assistance, don't hesitate to reach out to our [support team](https://support.claude.com/) or consult the [Discord community](https://www.anthropic.com/discord).
diff --git a/docs/context/claude-code/plugins.md b/docs/context/claude-code/plugins.md
deleted file mode 100644
index e44ffb7f..00000000
--- a/docs/context/claude-code/plugins.md
+++ /dev/null
@@ -1,376 +0,0 @@
-# Plugins reference
-
-> Complete technical reference for Claude Code plugin system, including schemas, CLI commands, and component specifications.
-
-
- For hands-on tutorials and practical usage, see [Plugins](/en/docs/claude-code/plugins). For plugin management across teams and communities, see [Plugin marketplaces](/en/docs/claude-code/plugin-marketplaces).
-
-
-This reference provides complete technical specifications for the Claude Code plugin system, including component schemas, CLI commands, and development tools.
-
-## Plugin components reference
-
-This section documents the five types of components that plugins can provide.
-
-### Commands
-
-Plugins add custom slash commands that integrate seamlessly with Claude Code's command system.
-
-**Location**: `commands/` directory in plugin root
-
-**File format**: Markdown files with frontmatter
-
-For complete details on plugin command structure, invocation patterns, and features, see [Plugin commands](/en/docs/claude-code/slash-commands#plugin-commands).
-
-### Agents
-
-Plugins can provide specialized subagents for specific tasks that Claude can invoke automatically when appropriate.
-
-**Location**: `agents/` directory in plugin root
-
-**File format**: Markdown files describing agent capabilities
-
-**Agent structure**:
-
-```markdown theme={null}
----
-description: What this agent specializes in
-capabilities: ["task1", "task2", "task3"]
----
-
-# Agent Name
-
-Detailed description of the agent's role, expertise, and when Claude should invoke it.
-
-## Capabilities
-- Specific task the agent excels at
-- Another specialized capability
-- When to use this agent vs others
-
-## Context and examples
-Provide examples of when this agent should be used and what kinds of problems it solves.
-```
-
-**Integration points**:
-
-* Agents appear in the `/agents` interface
-* Claude can invoke agents automatically based on task context
-* Agents can be invoked manually by users
-* Plugin agents work alongside built-in Claude agents
-
-### Skills
-
-Plugins can provide Agent Skills that extend Claude's capabilities. Skills are model-invoked—Claude autonomously decides when to use them based on the task context.
-
-**Location**: `skills/` directory in plugin root
-
-**File format**: Directories containing `SKILL.md` files with frontmatter
-
-**Skill structure**:
-
-```
-skills/
-├── pdf-processor/
-│ ├── SKILL.md
-│ ├── reference.md (optional)
-│ └── scripts/ (optional)
-└── code-reviewer/
- └── SKILL.md
-```
-
-**Integration behavior**:
-
-* Plugin Skills are automatically discovered when the plugin is installed
-* Claude autonomously invokes Skills based on matching task context
-* Skills can include supporting files alongside SKILL.md
-
-For SKILL.md format and complete Skill authoring guidance, see:
-
-* [Use Skills in Claude Code](/en/docs/claude-code/skills)
-* [Agent Skills overview](/en/docs/agents-and-tools/agent-skills/overview#skill-structure)
-
-### Hooks
-
-Plugins can provide event handlers that respond to Claude Code events automatically.
-
-**Location**: `hooks/hooks.json` in plugin root, or inline in plugin.json
-
-**Format**: JSON configuration with event matchers and actions
-
-**Hook configuration**:
-
-```json theme={null}
-{
- "hooks": {
- "PostToolUse": [
- {
- "matcher": "Write|Edit",
- "hooks": [
- {
- "type": "command",
- "command": "${CLAUDE_PLUGIN_ROOT}/scripts/format-code.sh"
- }
- ]
- }
- ]
- }
-}
-```
-
-**Available events**:
-
-* `PreToolUse`: Before Claude uses any tool
-* `PostToolUse`: After Claude uses any tool
-* `UserPromptSubmit`: When user submits a prompt
-* `Notification`: When Claude Code sends notifications
-* `Stop`: When Claude attempts to stop
-* `SubagentStop`: When a subagent attempts to stop
-* `SessionStart`: At the beginning of sessions
-* `SessionEnd`: At the end of sessions
-* `PreCompact`: Before conversation history is compacted
-
-**Hook types**:
-
-* `command`: Execute shell commands or scripts
-* `validation`: Validate file contents or project state
-* `notification`: Send alerts or status updates
-
-### MCP servers
-
-Plugins can bundle Model Context Protocol (MCP) servers to connect Claude Code with external tools and services.
-
-**Location**: `.mcp.json` in plugin root, or inline in plugin.json
-
-**Format**: Standard MCP server configuration
-
-**MCP server configuration**:
-
-```json theme={null}
-{
- "mcpServers": {
- "plugin-database": {
- "command": "${CLAUDE_PLUGIN_ROOT}/servers/db-server",
- "args": ["--config", "${CLAUDE_PLUGIN_ROOT}/config.json"],
- "env": {
- "DB_PATH": "${CLAUDE_PLUGIN_ROOT}/data"
- }
- },
- "plugin-api-client": {
- "command": "npx",
- "args": ["@company/mcp-server", "--plugin-mode"],
- "cwd": "${CLAUDE_PLUGIN_ROOT}"
- }
- }
-}
-```
-
-**Integration behavior**:
-
-* Plugin MCP servers start automatically when the plugin is enabled
-* Servers appear as standard MCP tools in Claude's toolkit
-* Server capabilities integrate seamlessly with Claude's existing tools
-* Plugin servers can be configured independently of user MCP servers
-
-***
-
-## Plugin manifest schema
-
-The `plugin.json` file defines your plugin's metadata and configuration. This section documents all supported fields and options.
-
-### Complete schema
-
-```json theme={null}
-{
- "name": "plugin-name",
- "version": "1.2.0",
- "description": "Brief plugin description",
- "author": {
- "name": "Author Name",
- "email": "author@example.com",
- "url": "https://github.com/author"
- },
- "homepage": "https://docs.example.com/plugin",
- "repository": "https://github.com/author/plugin",
- "license": "MIT",
- "keywords": ["keyword1", "keyword2"],
- "commands": ["./custom/commands/special.md"],
- "agents": "./custom/agents/",
- "hooks": "./config/hooks.json",
- "mcpServers": "./mcp-config.json"
-}
-```
-
-### Required fields
-
-| Field | Type | Description | Example |
-| :----- | :----- | :---------------------------------------- | :------------------- |
-| `name` | string | Unique identifier (kebab-case, no spaces) | `"deployment-tools"` |
-
-### Metadata fields
-
-| Field | Type | Description | Example |
-| :------------ | :----- | :---------------------------------- | :------------------------------------------------- |
-| `version` | string | Semantic version | `"2.1.0"` |
-| `description` | string | Brief explanation of plugin purpose | `"Deployment automation tools"` |
-| `author` | object | Author information | `{"name": "Dev Team", "email": "dev@company.com"}` |
-| `homepage` | string | Documentation URL | `"https://docs.example.com"` |
-| `repository` | string | Source code URL | `"https://github.com/user/plugin"` |
-| `license` | string | License identifier | `"MIT"`, `"Apache-2.0"` |
-| `keywords` | array | Discovery tags | `["deployment", "ci-cd"]` |
-
-### Component path fields
-
-| Field | Type | Description | Example |
-| :----------- | :------------- | :----------------------------------- | :------------------------------------- |
-| `commands` | string\|array | Additional command files/directories | `"./custom/cmd.md"` or `["./cmd1.md"]` |
-| `agents` | string\|array | Additional agent files | `"./custom/agents/"` |
-| `hooks` | string\|object | Hook config path or inline config | `"./hooks.json"` |
-| `mcpServers` | string\|object | MCP config path or inline config | `"./mcp.json"` |
-
-### Path behavior rules
-
-**Important**: Custom paths supplement default directories - they don't replace them.
-
-* If `commands/` exists, it's loaded in addition to custom command paths
-* All paths must be relative to plugin root and start with `./`
-* Commands from custom paths use the same naming and namespacing rules
-* Multiple paths can be specified as arrays for flexibility
-
-**Path examples**:
-
-```json theme={null}
-{
- "commands": [
- "./specialized/deploy.md",
- "./utilities/batch-process.md"
- ],
- "agents": [
- "./custom-agents/reviewer.md",
- "./custom-agents/tester.md"
- ]
-}
-```
-
-### Environment variables
-
-**`${CLAUDE_PLUGIN_ROOT}`**: Contains the absolute path to your plugin directory. Use this in hooks, MCP servers, and scripts to ensure correct paths regardless of installation location.
-
-```json theme={null}
-{
- "hooks": {
- "PostToolUse": [
- {
- "hooks": [
- {
- "type": "command",
- "command": "${CLAUDE_PLUGIN_ROOT}/scripts/process.sh"
- }
- ]
- }
- ]
- }
-}
-```
-
-***
-
-## Plugin directory structure
-
-### Standard plugin layout
-
-A complete plugin follows this structure:
-
-```
-enterprise-plugin/
-├── .claude-plugin/ # Metadata directory
-│ └── plugin.json # Required: plugin manifest
-├── commands/ # Default command location
-│ ├── status.md
-│ └── logs.md
-├── agents/ # Default agent location
-│ ├── security-reviewer.md
-│ ├── performance-tester.md
-│ └── compliance-checker.md
-├── skills/ # Agent Skills
-│ ├── code-reviewer/
-│ │ └── SKILL.md
-│ └── pdf-processor/
-│ ├── SKILL.md
-│ └── scripts/
-├── hooks/ # Hook configurations
-│ ├── hooks.json # Main hook config
-│ └── security-hooks.json # Additional hooks
-├── .mcp.json # MCP server definitions
-├── scripts/ # Hook and utility scripts
-│ ├── security-scan.sh
-│ ├── format-code.py
-│ └── deploy.js
-├── LICENSE # License file
-└── CHANGELOG.md # Version history
-```
-
-
- The `.claude-plugin/` directory contains the `plugin.json` file. All other directories (commands/, agents/, skills/, hooks/) must be at the plugin root, not inside `.claude-plugin/`.
-
-
-### File locations reference
-
-| Component | Default Location | Purpose |
-| :-------------- | :--------------------------- | :------------------------------- |
-| **Manifest** | `.claude-plugin/plugin.json` | Required metadata file |
-| **Commands** | `commands/` | Slash command markdown files |
-| **Agents** | `agents/` | Subagent markdown files |
-| **Skills** | `skills/` | Agent Skills with SKILL.md files |
-| **Hooks** | `hooks/hooks.json` | Hook configuration |
-| **MCP servers** | `.mcp.json` | MCP server definitions |
-
-***
-
-## Debugging and development tools
-
-### Debugging commands
-
-Use `claude --debug` to see plugin loading details:
-
-```bash theme={null}
-claude --debug
-```
-
-This shows:
-
-* Which plugins are being loaded
-* Any errors in plugin manifests
-* Command, agent, and hook registration
-* MCP server initialization
-
-### Common issues
-
-| Issue | Cause | Solution |
-| :--------------------- | :------------------------------ | :--------------------------------------------------- |
-| Plugin not loading | Invalid `plugin.json` | Validate JSON syntax |
-| Commands not appearing | Wrong directory structure | Ensure `commands/` at root, not in `.claude-plugin/` |
-| Hooks not firing | Script not executable | Run `chmod +x script.sh` |
-| MCP server fails | Missing `${CLAUDE_PLUGIN_ROOT}` | Use variable for all plugin paths |
-| Path errors | Absolute paths used | All paths must be relative and start with `./` |
-
-***
-
-## Distribution and versioning reference
-
-### Version management
-
-Follow semantic versioning for plugin releases:
-
-```json theme={null}
-
-## See also
-
-- [Plugins](/en/docs/claude-code/plugins) - Tutorials and practical usage
-- [Plugin marketplaces](/en/docs/claude-code/plugin-marketplaces) - Creating and managing marketplaces
-- [Slash commands](/en/docs/claude-code/slash-commands) - Command development details
-- [Subagents](/en/docs/claude-code/sub-agents) - Agent configuration and capabilities
-- [Agent Skills](/en/docs/claude-code/skills) - Extend Claude's capabilities
-- [Hooks](/en/docs/claude-code/hooks) - Event handling and automation
-- [MCP](/en/docs/claude-code/mcp) - External tool integration
-- [Settings](/en/docs/claude-code/settings) - Configuration options for plugins
-```
diff --git a/docs/context/claude-code/streaming.md b/docs/context/claude-code/streaming.md
deleted file mode 100644
index dde14f98..00000000
--- a/docs/context/claude-code/streaming.md
+++ /dev/null
@@ -1,295 +0,0 @@
-# Streaming Input
-
-> Understanding the two input modes for Claude Agent SDK and when to use each
-
-## Overview
-
-The Claude Agent SDK supports two distinct input modes for interacting with agents:
-
-* **Streaming Input Mode** (Default & Recommended) - A persistent, interactive session
-* **Single Message Input** - One-shot queries that use session state and resuming
-
-This guide explains the differences, benefits, and use cases for each mode to help you choose the right approach for your application.
-
-## Streaming Input Mode (Recommended)
-
-Streaming input mode is the **preferred** way to use the Claude Agent SDK. It provides full access to the agent's capabilities and enables rich, interactive experiences.
-
-It allows the agent to operate as a long lived process that takes in user input, handles interruptions, surfaces permission requests, and handles session management.
-
-### How It Works
-
-```mermaid theme={null}
-%%{init: {"theme": "base", "themeVariables": {"edgeLabelBackground": "#F0F0EB", "lineColor": "#91918D", "primaryColor": "#F0F0EB", "primaryTextColor": "#191919", "primaryBorderColor": "#D9D8D5", "secondaryColor": "#F5E6D8", "tertiaryColor": "#CC785C", "noteBkgColor": "#FAF0E6", "noteBorderColor": "#91918D"}, "sequence": {"actorMargin": 50, "width": 150, "height": 65, "boxMargin": 10, "boxTextMargin": 5, "noteMargin": 10, "messageMargin": 35}}}%%
-sequenceDiagram
- participant App as Your Application
- participant Agent as Claude Agent
- participant Tools as Tools/Hooks
- participant FS as Environment/
File System
-
- App->>Agent: Initialize with AsyncGenerator
- activate Agent
-
- App->>Agent: Yield Message 1
- Agent->>Tools: Execute tools
- Tools->>FS: Read files
- FS-->>Tools: File contents
- Tools->>FS: Write/Edit files
- FS-->>Tools: Success/Error
- Agent-->>App: Stream partial response
- Agent-->>App: Stream more content...
- Agent->>App: Complete Message 1
-
- App->>Agent: Yield Message 2 + Image
- Agent->>Tools: Process image & execute
- Tools->>FS: Access filesystem
- FS-->>Tools: Operation results
- Agent-->>App: Stream response 2
-
- App->>Agent: Queue Message 3
- App->>Agent: Interrupt/Cancel
- Agent->>App: Handle interruption
-
- Note over App,Agent: Session stays alive
- Note over Tools,FS: Persistent file system
state maintained
-
- deactivate Agent
-```
-
-### Benefits
-
-
-
- Attach images directly to messages for visual analysis and understanding
-
-
-
- Send multiple messages that process sequentially, with ability to interrupt
-
-
-
- Full access to all tools and custom MCP servers during the session
-
-
-
- Use lifecycle hooks to customize behavior at various points
-
-
-
- See responses as they're generated, not just final results
-
-
-
- Maintain conversation context across multiple turns naturally
-
-
-
-### Implementation Example
-
-
- ```typescript TypeScript theme={null}
- import { query } from "@anthropic-ai/claude-agent-sdk";
- import { readFileSync } from "fs";
-
- async function* generateMessages() {
- // First message
- yield {
- type: "user" as const,
- message: {
- role: "user" as const,
- content: "Analyze this codebase for security issues"
- }
- };
-
- // Wait for conditions or user input
- await new Promise(resolve => setTimeout(resolve, 2000));
-
- // Follow-up with image
- yield {
- type: "user" as const,
- message: {
- role: "user" as const,
- content: [
- {
- type: "text",
- text: "Review this architecture diagram"
- },
- {
- type: "image",
- source: {
- type: "base64",
- media_type: "image/png",
- data: readFileSync("diagram.png", "base64")
- }
- }
- ]
- }
- };
- }
-
- // Process streaming responses
- for await (const message of query({
- prompt: generateMessages(),
- options: {
- maxTurns: 10,
- allowedTools: ["Read", "Grep"]
- }
- })) {
- if (message.type === "result") {
- console.log(message.result);
- }
- }
- ```
-
- ```python Python theme={null}
- from claude_agent_sdk import ClaudeSDKClient, ClaudeAgentOptions, AssistantMessage, TextBlock
- import asyncio
- import base64
-
- async def streaming_analysis():
- async def message_generator():
- # First message
- yield {
- "type": "user",
- "message": {
- "role": "user",
- "content": "Analyze this codebase for security issues"
- }
- }
-
- # Wait for conditions
- await asyncio.sleep(2)
-
- # Follow-up with image
- with open("diagram.png", "rb") as f:
- image_data = base64.b64encode(f.read()).decode()
-
- yield {
- "type": "user",
- "message": {
- "role": "user",
- "content": [
- {
- "type": "text",
- "text": "Review this architecture diagram"
- },
- {
- "type": "image",
- "source": {
- "type": "base64",
- "media_type": "image/png",
- "data": image_data
- }
- }
- ]
- }
- }
-
- # Use ClaudeSDKClient for streaming input
- options = ClaudeAgentOptions(
- max_turns=10,
- allowed_tools=["Read", "Grep"]
- )
-
- async with ClaudeSDKClient(options) as client:
- # Send streaming input
- await client.query(message_generator())
-
- # Process responses
- async for message in client.receive_response():
- if isinstance(message, AssistantMessage):
- for block in message.content:
- if isinstance(block, TextBlock):
- print(block.text)
-
- asyncio.run(streaming_analysis())
- ```
-
-
-## Single Message Input
-
-Single message input is simpler but more limited.
-
-### When to Use Single Message Input
-
-Use single message input when:
-
-* You need a one-shot response
-* You do not need image attachments, hooks, etc.
-* You need to operate in a stateless environment, such as a lambda function
-
-### Limitations
-
-
- Single message input mode does **not** support:
-
- * Direct image attachments in messages
- * Dynamic message queueing
- * Real-time interruption
- * Hook integration
- * Natural multi-turn conversations
-
-
-### Implementation Example
-
-
- ```typescript TypeScript theme={null}
- import { query } from "@anthropic-ai/claude-agent-sdk";
-
- // Simple one-shot query
- for await (const message of query({
- prompt: "Explain the authentication flow",
- options: {
- maxTurns: 1,
- allowedTools: ["Read", "Grep"]
- }
- })) {
- if (message.type === "result") {
- console.log(message.result);
- }
- }
-
- // Continue conversation with session management
- for await (const message of query({
- prompt: "Now explain the authorization process",
- options: {
- continue: true,
- maxTurns: 1
- }
- })) {
- if (message.type === "result") {
- console.log(message.result);
- }
- }
- ```
-
- ```python Python theme={null}
- from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage
- import asyncio
-
- async def single_message_example():
- # Simple one-shot query using query() function
- async for message in query(
- prompt="Explain the authentication flow",
- options=ClaudeAgentOptions(
- max_turns=1,
- allowed_tools=["Read", "Grep"]
- )
- ):
- if isinstance(message, ResultMessage):
- print(message.result)
-
- # Continue conversation with session management
- async for message in query(
- prompt="Now explain the authorization process",
- options=ClaudeAgentOptions(
- continue_conversation=True,
- max_turns=1
- )
- ):
- if isinstance(message, ResultMessage):
- print(message.result)
-
- asyncio.run(single_message_example())
- ```
-
diff --git a/docs/context/claude-pr-review-47.md b/docs/context/claude-pr-review-47.md
deleted file mode 100644
index 053684a7..00000000
--- a/docs/context/claude-pr-review-47.md
+++ /dev/null
@@ -1,259 +0,0 @@
-**Claude finished @thedotmack's task** —— [View job](https://github.com/thedotmack/claude-mem/actions/runs/19080917196)
-
----
-### Code Review Complete
-
-This PR implements significant architectural improvements focused on production stability. The refactoring successfully simplifies the worker service and eliminates several failure modes. Below is my detailed review:
-
----
-
-### ✅ Strengths
-
-#### 1. **Excellent Simplification of Worker Service Startup** (src/services/worker-service.ts:115-147)
-The new startup sequence is a major improvement:
-- HTTP server starts **first** before any blocking operations
-- ChromaSync initialization is deferred until after HTTP is ready
-- Chroma backfill runs in background (non-blocking)
-- Failures no longer crash the entire service
-
-**Before**: ChromaSync initialized in constructor → Chroma backfill blocks startup → Any failure prevents HTTP server from starting
-
-**After**: HTTP server starts immediately → ChromaSync initialized → Chroma backfill in background → Service always available
-
-This is excellent engineering. The worker will now be responsive even if Chroma has issues.
-
-#### 2. **Smart Executable Path Caching** (src/services/worker-service.ts:22-61)
-Adding `cachedClaudePath` prevents repeated `execSync` calls on every SDK agent initialization. This reduces filesystem overhead and improves performance. Good optimization.
-
-#### 3. **Improved Error Handling in Hooks** (src/hooks/save-hook.ts:84-91, new-hook.ts:62-69, summary-hook.ts:65-72)
-The error handling now distinguishes between connection errors vs HTTP errors:
-```typescript
-if (error.cause?.code === 'ECONNREFUSED' || error.name === 'TimeoutError' || error.message.includes('fetch failed')) {
- throw new Error("There's a problem with the worker. If you just updated, type `pm2 restart claude-mem-worker` in your terminal to continue");
-}
-```
-This provides actionable error messages to users instead of generic failures.
-
-#### 4. **Dramatic PM2 Configuration Simplification** (ecosystem.config.cjs)
-The PM2 config went from 44 lines with complex error/output redirection down to 26 lines. This is exactly right - PM2's defaults are sensible and the previous configuration was adding complexity without clear benefit.
-
-#### 5. **Worker Management Simplification** (src/shared/worker-utils.ts)
-Removed the async health check system (124 lines → 48 lines). The new synchronous approach using PM2 list output is simpler and more reliable:
-- No HTTP health checks that can timeout
-- No retry logic with arbitrary delays
-- Just checks if PM2 reports the process as 'online'
-
----
-
-### ⚠️ Issues & Concerns
-
-#### 1. **Race Condition in Worker Health Check** (src/shared/worker-utils.ts:28-39) - **CRITICAL**
-
-There's a race condition in the worker startup check:
-
-```typescript
-checkProcess.on('close', (code) => {
- const isRunning = output.includes('claude-mem-worker') && output.includes('online');
-
- if (!isRunning) {
- // Only start if not already running
- spawn(pm2Path, ['start', ecosystemPath], {
- cwd: packageRoot,
- stdio: 'ignore'
- });
- }
-});
-```
-
-**Problem**: The `spawn()` call inside the `close` event handler is non-blocking, but the function returns immediately. The hooks calling `ensureWorkerRunning()` will continue execution **before** the worker has actually started.
-
-**Impact**: Hooks may attempt to make HTTP requests to the worker before it's listening, resulting in connection failures.
-
-**Suggested Fix**: Either:
-- Make `ensureWorkerRunning()` async and await the health check
-- Add a brief health check retry loop after spawning (like the old implementation had)
-- Document that this is fire-and-forget and hooks should handle ECONNREFUSED
-
-#### 2. **Removed Health Endpoint Information** (src/services/worker-service.ts:152-154)
-
-The health endpoint was simplified from:
-```typescript
-// OLD
-res.json({
- status: 'ok',
- port: this.port,
- pid: process.pid,
- activeSessions: this.sessions.size,
- chromaSynced: true
-});
-```
-
-To:
-```typescript
-// NEW
-res.json({ status: 'ok' });
-```
-
-**Concern**: This removes useful debugging information. When troubleshooting production issues, knowing the PID, active sessions count, and port would be valuable. The health endpoint is typically used for diagnostics, not just uptime checks.
-
-**Suggestion**: Consider keeping at least `pid` and `activeSessions` in the health response for observability.
-
-#### 3. **Auto-Session Creation Without Validation** (src/services/worker-service.ts:252-280, 309-336)
-
-Both `handleObservation` and `handleSummarize` auto-create sessions if they don't exist:
-
-```typescript
-if (!session) {
- // Auto-create session if it doesn't exist (e.g., worker restarted)
- const db = new SessionStore();
- const dbSession = db.getSessionById(sessionDbId);
- db.close();
-
- session = {
- sessionDbId,
- claudeSessionId: dbSession!.claude_session_id, // ⚠️ Non-null assertion
- // ...
- };
-}
-```
-
-**Concerns**:
-- Uses non-null assertion (`dbSession!`) without checking if `dbSession` is actually null
-- If `getSessionById()` returns null (session doesn't exist in DB), this will throw at runtime
-- The error won't be caught and will crash the handler
-
-**Suggested Fix**:
-```typescript
-const dbSession = db.getSessionById(sessionDbId);
-if (!dbSession) {
- db.close();
- res.status(404).json({ error: 'Session not found in database' });
- return;
-}
-```
-
-This validation already exists in `handleInit` (lines 167-173), so it should be added to the other handlers too.
-
-#### 4. **Removed Observation Counter** (src/services/worker-service.ts:80)
-
-The `ActiveSession` interface no longer tracks `observationCounter`:
-```typescript
-// REMOVED: observationCounter: number; // Counter for correlation IDs
-```
-
-**Question**: Was this used for generating correlation IDs for logging? If so, is there now no way to correlate observations within a session for debugging?
-
-**Impact**: Minor - mainly affects debugging/logging. Not a functional issue.
-
-#### 5. **PM2 Watch Mode in Production** (ecosystem.config.cjs:16)
-
-The config enables watch mode:
-```javascript
-watch: true,
-```
-
-**Concern**: Watch mode causes PM2 to restart the process whenever files change. This is useful during development but potentially problematic in production:
-- Could cause unexpected restarts if files are touched
-- May interrupt active sessions
-- Increases resource usage (file watching)
-
-**Suggestion**: Consider disabling watch mode by default, or document that users should disable it in production if they're deploying this.
-
-#### 6. **Duplicate Port Constant** (Multiple Files)
-
-The `FIXED_PORT` constant is defined in multiple places:
-- `src/services/worker-service.ts:20`
-- `src/shared/worker-utils.ts:5`
-- `src/hooks/save-hook.ts:54`
-- `src/hooks/new-hook.ts:47`
-- `src/hooks/summary-hook.ts:39`
-
-**Concern**: This creates maintenance burden and risk of inconsistency. If the port needs to change, it must be updated in 5 places.
-
-**Suggestion**: Export `FIXED_PORT` from a single shared module (e.g., `worker-utils.ts`) and import it everywhere else.
-
----
-
-### 🔍 Minor Issues
-
-#### 7. **Error Handling Inconsistency in Chroma Sync** (src/services/worker-service.ts:220-223)
-
-```typescript
-).catch(err => {
- logger.failure('WORKER', 'Failed to sync user_prompt to Chroma - continuing', { promptId: latestPrompt.id }, err);
- // Don't crash - SQLite has the data
-});
-```
-
-The logger method is `failure` here but `error` elsewhere (lines 625, 664). For consistency, these should all use the same log level for Chroma sync failures.
-
-#### 8. **Type Safety in Error Handling** (src/hooks/save-hook.ts:84)
-
-```typescript
-} catch (error: any) {
-```
-
-Using `any` defeats type safety. Consider using `unknown` and type guards:
-```typescript
-} catch (error: unknown) {
- const err = error as Error & { cause?: { code?: string } };
- if (err.cause?.code === 'ECONNREFUSED' || err.name === 'TimeoutError' || err.message.includes('fetch failed')) {
- // ...
- }
-}
-```
-
-This pattern is used in all three hook files.
-
----
-
-### 📋 Documentation Cleanup
-
-The deletion of obsolete documentation files is appropriate:
-- `EXPERIMENTAL_RELEASE_NOTES.md` (331 lines)
-- `FEATURE_PLAN_HYBRID_SEARCH.md` (486 lines)
-- `GITHUB_RELEASE_TEMPLATE.md` (83 lines)
-- `IMPLEMENTATION_STATUS.md` (503 lines)
-- `NEXT_SESSION_PROMPT.md` (193 lines)
-- `docs/coderabbit-PR-41.md` (314 lines)
-
-These appear to be planning documents and old release artifacts. Good cleanup, though consider archiving rather than deleting if there's historical value.
-
----
-
-### 🎯 Testing Recommendations
-
-In addition to the testing recommendations in the PR description:
-
-1. **Test race condition scenario**: Start a fresh session immediately after a `pm2 stop` to verify hooks handle ECONNREFUSED gracefully
-2. **Test worker restart under load**: Restart worker while observations are being processed to verify auto-session-creation works
-3. **Test Chroma failures**: Disconnect/break Chroma to verify worker stays alive and logs errors appropriately
-4. **Verify PM2 watch mode**: Ensure watch mode doesn't cause problems with the data directory or node_modules changes
-
----
-
-### ✅ Overall Assessment
-
-**Verdict**: **Approve with recommendations**
-
-This PR represents a significant improvement in production stability and code maintainability. The architectural changes are sound and address real production failure modes. The primary concerns are:
-
-1. **Race condition in worker startup** (Critical - should be fixed)
-2. **Missing null checks in auto-session creation** (High - could cause crashes)
-3. **Reduced observability in health endpoint** (Medium - impacts debugging)
-4. **Watch mode in production** (Low - document or disable)
-
-The simplifications are well-justified and the error handling improvements make the system more resilient. Once the critical race condition and null check issues are addressed, this will be a solid improvement.
-
-**Lines of code review**: The 1,855 line reduction (2,335 deletions - 480 additions) accurately reflects meaningful simplification rather than just removing features.
-
----
-
-#### Todo List
-- [x] Read and analyze core service files (worker-service.ts, worker-utils.ts)
-- [x] Review hook changes (save-hook.ts, new-hook.ts, summary-hook.ts, cleanup-hook.ts)
-- [x] Review PM2 configuration changes (ecosystem.config.cjs)
-- [x] Analyze architectural changes and potential issues
-- [x] Provide comprehensive review feedback
-
----
\ No newline at end of file
diff --git a/docs/context/context-engineering.md b/docs/context/context-engineering.md
deleted file mode 100644
index adb1dce3..00000000
--- a/docs/context/context-engineering.md
+++ /dev/null
@@ -1,222 +0,0 @@
-# Context Engineering for AI Agents: Best Practices Cheat Sheet
-
-## Core Principle
-**Find the smallest possible set of high-signal tokens that maximize the likelihood of your desired outcome.**
-
----
-
-## Context Engineering vs Prompt Engineering
-
-**Prompt Engineering**: Writing and organizing LLM instructions for optimal outcomes (one-time task)
-
-**Context Engineering**: Curating and maintaining the optimal set of tokens during inference across multiple turns (iterative process)
-
-Context engineering manages:
-- System instructions
-- Tools
-- Model Context Protocol (MCP)
-- External data
-- Message history
-- Runtime data retrieval
-
----
-
-## The Problem: Context Rot
-
-**Key Insight**: LLMs have an "attention budget" that gets depleted as context grows
-
-- Every token attends to every other token (n² relationships)
-- As context length increases, model accuracy decreases
-- Models have less training experience with longer sequences
-- Context must be treated as a finite resource with diminishing marginal returns
-
----
-
-## System Prompts: Find the "Right Altitude"
-
-### The Goldilocks Zone
-
-**Too Prescriptive** ❌
-- Hardcoded if-else logic
-- Brittle and fragile
-- High maintenance complexity
-
-**Too Vague** ❌
-- High-level guidance without concrete signals
-- Falsely assumes shared context
-- Lacks actionable direction
-
-**Just Right** ✅
-- Specific enough to guide behavior effectively
-- Flexible enough to provide strong heuristics
-- Minimal set of information that fully outlines expected behavior
-
-### Best Practices
-- Use simple, direct language
-- Organize into distinct sections (``, ``, `## Tool guidance`, etc.)
-- Use XML tags or Markdown headers for structure
-- Start with minimal prompt, add based on failure modes
-- Note: Minimal ≠ short (provide sufficient information upfront)
-
----
-
-## Tools: Minimal and Clear
-
-### Design Principles
-- **Self-contained**: Each tool has a single, clear purpose
-- **Robust to error**: Handle edge cases gracefully
-- **Extremely clear**: Intended use is unambiguous
-- **Token-efficient**: Returns relevant information without bloat
-- **Descriptive parameters**: Unambiguous input names (e.g., `user_id` not `user`)
-
-### Critical Rule
-**If a human engineer can't definitively say which tool to use in a given situation, an AI agent can't be expected to do better.**
-
-### Common Failure Modes to Avoid
-- Bloated tool sets covering too much functionality
-- Tools with overlapping purposes
-- Ambiguous decision points about which tool to use
-
----
-
-## Examples: Diverse, Not Exhaustive
-
-**Do** ✅
-- Curate a set of diverse, canonical examples
-- Show expected behavior effectively
-- Think "pictures worth a thousand words"
-
-**Don't** ❌
-- Stuff in a laundry list of edge cases
-- Try to articulate every possible rule
-- Overwhelm with exhaustive scenarios
-
----
-
-## Context Retrieval Strategies
-
-### Just-In-Time Context (Recommended for Agents)
-**Approach**: Maintain lightweight identifiers (file paths, queries, links) and dynamically load data at runtime
-
-**Benefits**:
-- Avoids context pollution
-- Enables progressive disclosure
-- Mirrors human cognition (we don't memorize everything)
-- Leverages metadata (file names, folder structure, timestamps)
-- Agents discover context incrementally
-
-**Trade-offs**:
-- Slower than pre-computed retrieval
-- Requires proper tool guidance to avoid dead-ends
-
-### Pre-Inference Retrieval (Traditional RAG)
-**Approach**: Use embedding-based retrieval to surface context before inference
-
-**When to Use**: Static content that won't change during interaction
-
-### Hybrid Strategy (Best of Both)
-**Approach**: Retrieve some data upfront, enable autonomous exploration as needed
-
-**Example**: Claude Code loads CLAUDE.md files upfront, uses glob/grep for just-in-time retrieval
-
-**Rule of Thumb**: "Do the simplest thing that works"
-
----
-
-## Long-Horizon Tasks: Three Techniques
-
-### 1. Compaction
-**What**: Summarize conversation nearing context limit, reinitiate with summary
-
-**Implementation**:
-- Pass message history to model for compression
-- Preserve critical details (architectural decisions, bugs, implementation)
-- Discard redundant outputs
-- Continue with compressed context + recently accessed files
-
-**Tuning Process**:
-1. **First**: Maximize recall (capture all relevant information)
-2. **Then**: Improve precision (eliminate superfluous content)
-
-**Low-Hanging Fruit**: Clear old tool calls and results
-
-**Best For**: Tasks requiring extensive back-and-forth
-
-### 2. Structured Note-Taking (Agentic Memory)
-**What**: Agent writes notes persisted outside context window, retrieved later
-
-**Examples**:
-- To-do lists
-- NOTES.md files
-- Game state tracking (Pokémon example: tracking 1,234 steps of training)
-- Project progress logs
-
-**Benefits**:
-- Persistent memory with minimal overhead
-- Maintains critical context across tool calls
-- Enables multi-hour coherent strategies
-
-**Best For**: Iterative development with clear milestones
-
-### 3. Sub-Agent Architectures
-**What**: Specialized sub-agents handle focused tasks with clean context windows
-
-**How It Works**:
-- Main agent coordinates high-level plan
-- Sub-agents perform deep technical work
-- Sub-agents explore extensively (tens of thousands of tokens)
-- Return condensed summaries (1,000-2,000 tokens)
-
-**Benefits**:
-- Clear separation of concerns
-- Parallel exploration
-- Detailed context remains isolated
-
-**Best For**: Complex research and analysis tasks
-
----
-
-## Quick Decision Framework
-
-| Scenario | Recommended Approach |
-|----------|---------------------|
-| Static content | Pre-inference retrieval or hybrid |
-| Dynamic exploration needed | Just-in-time context |
-| Extended back-and-forth | Compaction |
-| Iterative development | Structured note-taking |
-| Complex research | Sub-agent architectures |
-| Rapid model improvement | "Do the simplest thing that works" |
-
----
-
-## Key Takeaways
-
-1. **Context is finite**: Treat it as a precious resource with an attention budget
-2. **Think holistically**: Consider the entire state available to the LLM
-3. **Stay minimal**: More context isn't always better
-4. **Be iterative**: Context curation happens each time you pass to the model
-5. **Design for autonomy**: As models improve, let them act intelligently
-6. **Start simple**: Test with minimal setup, add based on failure modes
-
----
-
-## Anti-Patterns to Avoid
-
-- ❌ Cramming everything into prompts
-- ❌ Creating brittle if-else logic
-- ❌ Building bloated tool sets
-- ❌ Stuffing exhaustive edge cases as examples
-- ❌ Assuming larger context windows solve everything
-- ❌ Ignoring context pollution over long interactions
-
----
-
-## Remember
-
-> "Even as models continue to improve, the challenge of maintaining coherence across extended interactions will remain central to building more effective agents."
-
-Context engineering will evolve, but the core principle stays the same: **optimize signal-to-noise ratio in your token budget**.
-
----
-
-*Based on Anthropic's "Effective context engineering for AI agents" (September 2025)*
\ No newline at end of file
diff --git a/docs/context/jit-context-postmortem.md b/docs/context/jit-context-postmortem.md
deleted file mode 100644
index d58fa03b..00000000
--- a/docs/context/jit-context-postmortem.md
+++ /dev/null
@@ -1,616 +0,0 @@
-# JIT Context Filtering: Post-Mortem
-
-**Date:** November 9, 2025
-**Duration:** 3.5 hours (7:45 PM - 11:11 PM)
-**Branches:** `feature/jit-context`, `failed/jit-context`
-**Status:** Failed, reverted to main
-**Commits:**
-- `3ac0790` - feat: Implement JIT context hook for user prompt submission
-- `adf7bf4` - Refactor JIT context handling in SDKAgent and WorkerService
-
-## Executive Summary
-
-Attempted to implement JIT (Just-In-Time) context filtering—a feature that would dynamically generate relevant context timelines on every user prompt, potentially replacing the static session-start context entirely. After multiple architectural iterations spanning 3.5 hours and adding ~2,850 lines of code, the implementation was abandoned and reverted. The revert was not due to lack of vision (the feature aligns with long-term architectural goals), but due to implementation complexity and the need for a simpler initial approach. Significant architectural knowledge was gained about hook limitations, worker patterns, and proper separation of concerns.
-
-## What We Tried to Build
-
-### Goal
-When a user submits a prompt, dynamically generate a relevant context timeline instead of the static session-start context. Use the fast search infrastructure (SQLite FTS5 + ChromaDB) to fetch precisely relevant context on-demand.
-
-### The Vision
-**Current approach:** SessionStart hook loads 50 recent observations blindly, displays them all.
-
-**Proposed approach:** UserPromptSubmit hook analyzes the prompt, queries the timeline search API, and loads only the relevant context window dynamically.
-
-**Why this makes sense:**
-- We already have fast search: SQLite FTS5 + Chroma semantic search
-- Dynamic context timeline search is implemented and tested
-- Search results come back in <200ms
-- Could **replace** session-start context entirely with smarter, prompt-specific context
-
-### User Experience
-```
-User types: "How did we fix the authentication bug?"
-
-Behind the scenes:
-1. Analyze prompt: "authentication bug fix"
-2. Query timeline search for relevant period
-3. Load 5-10 observations from that specific timeline
-4. Inject as context
-5. Claude answers with precisely relevant historical context
-
-vs. Current:
-Load 50 most recent observations regardless of relevance
-```
-
-### Why Checkbox Settings Became Less Important
-Originally asked for checkboxes to customize session-start context display. But if JIT context could replace session-start context with intelligent, prompt-specific timelines, the display customization became a non-issue.
-
-## Architectural Attempts
-
-### Attempt 1: Hook-Based Filtering (7:45 PM - 9:30 PM)
-
-**Approach:** Call Agent SDK `query()` directly in `new-hook.ts` during UserPromptSubmit event.
-
-**Implementation:**
-- Created `jit-context-hook.ts` (~432 lines)
-- Added `generateJitContext()` function in hook
-- Called SDK `query()` with observation list and user prompt
-- Expected hook to block for ~1-2s while Haiku filters
-
-**Failure:**
-```
-Error: Claude Code executable not found at
-/Users/alexnewman/.claude/plugins/marketplaces/thedotmack/plugin/scripts/cli.js
-```
-
-**Root Cause:** Hooks run in sandboxed environment without access to `claudePath` (path to Claude Code executable). The Agent SDK requires this path, which is only available in the worker service.
-
-**Architectural Violation:** This broke the established pattern where hooks handle orchestration and workers handle AI processing. The `save-hook` sets the precedent: hooks capture data, send to worker, worker runs SDK queries asynchronously.
-
-### Attempt 2: Worker-Based with Simple Queries (9:30 PM - 10:30 PM)
-
-**Approach:** Move JIT filtering to worker service, keep it simple with per-request SDK queries.
-
-**Implementation:**
-- Documented architecture fix plan in `docs/jit-context-architecture-fix.md`
-- Moved `generateJitContext()` to worker (considered creating `src/services/worker/JitContext.ts`)
-- Modified `/sessions/:id/init` endpoint to accept `jitEnabled` flag
-- Worker would run one-shot SDK query per prompt
-
-**Architecture:**
-```
-UserPromptSubmit → new-hook → POST /sessions/:id/init { jitEnabled: true }
- ↓
- Worker spawns Claude Haiku
- ↓
- Filters 50 obs → 3-5 IDs
- ↓
- Returns { context: [...] }
- ↓
- Hook injects context → Claude
-```
-
-**Issues Identified:**
-- Each filter request spawns a new Claude subprocess (~200-500ms overhead)
-- Observation list re-sent on every prompt (~5-10KB per request)
-- No token caching between requests
-- Performance worse than just loading all observations directly
-
-**Decision:** Pivoted to persistent sessions to solve performance issues.
-
-### Attempt 3: Persistent JIT Sessions (10:30 PM - 11:11 PM)
-
-**Approach:** Create a long-lived Agent SDK session that persists throughout user session, similar to main memory session pattern.
-
-**Implementation (291 new lines in SDKAgent.ts):**
-
-1. **Session Lifecycle:**
- - Added `jitSessionId`, `jitAbortController`, `jitGeneratorPromise` to `ActiveSession` interface
- - `startJitSession()`: Creates persistent SDK session at session init
- - `cleanupJitSession()`: Terminates JIT session at session end
-
-2. **Request Queue Architecture:**
- - `jitFilterQueues` Map: Per-session request queues
- - `JITFilterRequest` interface: `{ userPrompt, resolve, reject }`
- - EventEmitter coordination: Wake generator when new requests arrive
-
-3. **Message Generator Pattern:**
- - `createJitMessageGenerator()`: Async generator that yields filter requests
- - Initial prompt: Load 50 observations, wait for "READY" response
- - Loop: Wait for EventEmitter signal → yield user prompt → parse response → resolve promise
- - Pattern: Persistent session stays alive between requests
-
-4. **Filter Query Flow:**
- ```typescript
- runFilterQuery(sessionDbId, userPrompt) {
- // Queue request
- queue.requests.push({ userPrompt, resolve, reject });
- queue.emitter.emit('request');
-
- // Wait for response (30s timeout)
- return Promise.race([
- new Promise((resolve, reject) => { /* queued */ }),
- timeout(30000)
- ]);
- }
- ```
-
-5. **Response Processing:**
- - `processJitFilterResponse()`: Accumulate streaming text
- - Parse IDs: "1,5,23,41" or "NONE"
- - Resolve queued promise with ID array
-
-**Added Files:**
-- `src/services/worker/SDKAgent.ts`: +291 lines
-- `src/services/worker-types.ts`: +3 fields (jit state tracking)
-- `src/services/worker/SessionManager.ts`: +26 lines (JIT cleanup)
-- `src/services/worker-service.ts`: +102 lines (JIT initialization)
-- `src/shared/settings.ts`: +65 lines (JIT config)
-- `src/hooks/jit-context-hook.ts`: +208 lines (orchestration)
-- `docs/jit-context-architecture-fix.md`: +265 lines
-- `context/session-pattern-parity.md`: +298 lines
-
-**Total Changes:** 18 files, +2,852 lines, -133 lines
-
-**Final Status at Revert:** Implementation was complete and likely functional, but...
-
-## Why It Failed
-
-### 1. Architectural Complexity Explosion
-
-**Problem:** The persistent session pattern added enormous complexity for marginal benefit.
-
-**Evidence:**
-- Parallel session management: Regular + JIT sessions running concurrently
-- Complex coordination: EventEmitter + promise queues + generator pattern
-- Lifecycle coupling: Session init, request handling, cleanup all intertwined
-- State explosion: 3 new fields per session (`jitSessionId`, `jitAbortController`, `jitGeneratorPromise`)
-
-**Code Smell:** When the "optimization" requires 300 lines of coordination code, it's probably not an optimization.
-
-### 2. Premature Optimization
-
-**YAGNI Violation:** Built elaborate token caching and persistent session architecture before proving the feature provided value.
-
-**Reality Check:**
-- **Current approach:** Load 50 observations = ~25KB context, works fine
-- **JIT overhead:** Haiku query = 1-2s latency + coordination complexity
-- **User benefit:** Unclear—users haven't complained about context relevance
-- **Token savings:** Marginal—Claude caches long contexts efficiently anyway
-
-**Quote from CLAUDE.md:**
-> "Write the dumb, obvious thing first. Add complexity only when you actually hit the problem."
-
-We didn't hit a problem. We invented one.
-
-### 3. Implementation Complexity, Not Vision
-
-**The Vision is Sound:**
-- Dynamic context is better than static context
-- Timeline search API exists and is fast
-- Infrastructure (SQLite + Chroma) can support this
-- Replacing session-start context with prompt-specific context makes sense
-
-**The Problem:**
-We jumped to the complex persistent-session approach without trying the simple per-request approach first.
-
-**What We Should Have Done:**
-```typescript
-// Simple version (not tried):
-app.post('/sessions/:id/init', async (req, res) => {
- const { userPrompt } = req.body;
-
- // Query timeline search API (already exists, fast)
- const timeline = await timelineSearch(project, userPrompt, depth=10);
-
- // Return observations
- return res.json({ context: timeline });
-});
-```
-
-**This would have:**
-- Validated the feature's value quickly
-- Used existing infrastructure
-- Avoided all the persistence complexity
-- Taken 30 minutes instead of 3.5 hours
-
-### 4. Pattern Divergence
-
-**Inconsistency:** JIT sessions work fundamentally differently from memory sessions.
-
-**Memory Session Pattern:**
-```typescript
-// One-shot: Init → Process observations → Complete
-startSession() → yield prompts → parse responses → complete
-```
-
-**JIT Session Pattern:**
-```typescript
-// Persistent: Init → Wait indefinitely → Process on-demand → Complete
-startJitSession() → yield initial load → LOOP:
- - Wait for EventEmitter signal
- - Yield filter request
- - Parse response
- - Resolve promise
- - GOTO LOOP
-```
-
-**Maintenance Burden:** Two completely different session patterns means:
-- Doubled testing complexity
-- Increased cognitive load for contributors
-- Higher risk of subtle bugs in lifecycle management
-
-**Session Pattern Parity Document:** The 298-line `session-pattern-parity.md` was created to document the differences—a sign that maybe they shouldn't be different.
-
-### 5. Blocking I/O in Critical Path
-
-**Performance Impact:** Every user prompt now blocks for 1-2s waiting for Haiku filtering.
-
-**Current Flow:**
-```
-User types prompt → 10ms → Claude responds
-```
-
-**JIT Flow:**
-```
-User types prompt → 10ms init → 1-2s Haiku filter → Claude responds
-```
-
-**User Experience:** We added 1-2 seconds of latency to every interaction for questionable benefit.
-
-**Alternative:** If context filtering is valuable, do it asynchronously and apply to next prompt.
-
-### 6. Missing the Forest for the Trees
-
-**Real Issue:** We focused on technical implementation without asking strategic questions:
-
-- **Is context relevance actually a problem?** No evidence.
-- **Do users want this?** No feedback requested.
-- **Is 50 observations too many?** Not proven.
-- **Does filtering improve responses?** Not tested.
-
-**Anti-Pattern:** Solution in search of a problem.
-
-## What We Should Have Done
-
-### Option 1: Don't Build It
-
-**Justification:** No validated user need. Current system works fine.
-
-**Next Step:** Wait for user feedback indicating context relevance is an issue.
-
-### Option 2: Simple MVP
-
-If we really wanted to explore this:
-
-1. **Week 1:** Add basic filtering in worker with one-shot queries
- - Accept slight performance hit (~500ms overhead)
- - Measure filter accuracy and user impact
- - Gather feedback
-
-2. **Week 2:** If proven valuable, optimize
- - Add token caching only if needed
- - Consider persistent sessions only if performance is bottleneck
-
-3. **Week 3:** If still valuable, scale
- - Polish error handling
- - Add configuration options
- - Document patterns
-
-**Philosophy:** Incremental validation, not big-bang architecture.
-
-### Option 3: Different Approach Entirely
-
-**Alternative:** Pre-computed relevance scores
-
-Instead of on-demand filtering:
-- Score observations at creation time (save-hook)
-- Store relevance embeddings in Chroma
-- At session start, query Chroma with user's first prompt
-- Load top 10-20 most relevant observations
-- No runtime latency, better accuracy, simpler architecture
-
-**Benefit:** Leverages existing Chroma infrastructure, avoids runtime overhead.
-
-## Technical Lessons Learned
-
-### 1. EventEmitter Coordination Anti-Pattern
-
-**Code:**
-```typescript
-queue.emitter.on('request', () => {
- // Wake up generator to process request
-});
-```
-
-**Issue:** Complex async coordination using event-driven wakeup signals is hard to reason about.
-
-**Better:** Use async queues or channels (e.g., `async-queue` package) that handle coordination internally.
-
-### 2. Generator Pattern Complexity
-
-**Pattern:**
-```typescript
-async *createJitMessageGenerator() {
- yield initialPrompt;
- while (!aborted) {
- await waitForEvent(); // Blocks here
- yield nextRequest;
- }
-}
-```
-
-**Tradeoff:** Generators are great for iteration, but terrible for event-driven request/response patterns.
-
-**Better:** Use explicit session object with `sendMessage()/waitForResponse()` methods.
-
-### 3. Dual Session Management
-
-**Complexity:** Managing two concurrent SDK sessions per user session is inherently complex.
-
-**Alternatives Considered:**
-- Single session handling both observations and filtering (rejected: tight coupling)
-- Separate service for filtering (rejected: too much infrastructure)
-- Pre-computed filtering (not considered: should have been)
-
-**Lesson:** When parallel state management feels hard, question whether you need parallel state.
-
-### 4. Promise Queue Pattern
-
-**Implementation:**
-```typescript
-interface QueuedRequest {
- resolve: (result: T) => void;
- reject: (error: Error) => void;
-}
-queue.push({ resolve, reject });
-// Later...
-queue[0].resolve(result);
-```
-
-**Good:** Clean async API for callers
-**Bad:** Easy to leak promises if error handling isn't perfect
-**Improvement:** Use libraries like `p-queue` that handle edge cases
-
-## Process Lessons Learned
-
-### 1. No Incremental Validation
-
-**Mistake:** Went from "idea" to "complete architecture" without validation points.
-
-**Better Process:**
-1. Write one-pager explaining user value
-2. Build simplest possible version (2 hours max)
-3. Test with real usage
-4. Measure impact
-5. Decide: kill, iterate, or scale
-
-**Checkpoint Questions:**
-- After 1 hour: "Does this solve a real problem?"
-- After 2 hours: "Is this getting too complex?"
-- After 3 hours: "Should I just ship the simple version?"
-
-### 2. Architecture Astronomy
-
-**Definition:** Designing elaborate systems without building/testing them.
-
-**Evidence:**
-- 265-line architecture doc written before any code
-- 298-line session pattern parity analysis
-- Multiple complete rewrites of the same feature
-
-**Better:** Code first, document later. Spike solutions, learn from implementation.
-
-### 3. Sunk Cost Fallacy
-
-**Timeline:**
-- **Hour 1:** "This seems complex but achievable"
-- **Hour 2:** "We're halfway done, can't stop now"
-- **Hour 3:** "Just need to fix this one coordination issue"
-- **Hour 4:** "It's working, but... this feels wrong"
-
-**Correct Decision:** Revert. Took courage to throw away 4 hours of work.
-
-**Learning:** Time invested is not a reason to continue. Quality of outcome matters more.
-
-### 4. Missing User Feedback Loop
-
-**No User Input:**
-- Didn't ask: "Is context relevance a problem for you?"
-- Didn't test: "Does filtered context improve your responses?"
-- Didn't measure: "Are you hitting context limits?"
-
-**Engineering Theater:** Building impressive-sounding features without user validation.
-
-## What We Actually Learned (The Real Value)
-
-Despite reverting, this was productive R&D:
-
-### 1. Deep Understanding of Hook Architecture
-
-**Critical Discovery:** Hooks run in sandboxed environment without `claudePath`.
-- Hooks cannot call Agent SDK `query()` directly
-- All AI processing must happen in worker service
-- This architectural constraint is now documented
-
-**Learned Pattern:**
-```
-Hook (orchestration) → Worker (AI processing)
-✓ save-hook: Captures data → Worker processes with SDK
-✓ new-hook: Creates session → Worker returns confirmation
-✗ jit-hook: Tried SDK in hook → Failed, no claudePath
-```
-
-**Value:** Future features will avoid this mistake. We now know the boundary.
-
-### 2. Worker Architecture Patterns
-
-**Blocking vs. Non-Blocking:**
-- SessionStart: Can be non-blocking (context loads async)
-- UserPromptSubmit: Must be blocking (session must exist before processing)
-- JIT Context: Must be blocking (context needed before prompt processed)
-
-**Established Pattern:**
-```typescript
-// Worker endpoint for features requiring AI
-app.post('/sessions/:id/operation', async (req, res) => {
- const { operationData } = req.body;
- const result = await sdkAgent.performOperation(operationData);
- return res.json({ result });
-});
-```
-
-### 3. Persistent Session Management
-
-**Architecture Knowledge Gained:**
-- How to maintain long-lived SDK sessions
-- EventEmitter coordination patterns for request/response
-- Promise queue management for async operations
-- Proper cleanup with AbortControllers
-
-**Pattern Documented:**
-- Dual session management (regular + JIT)
-- Generator-based message loops
-- Request queuing with timeouts
-
-**Value:** When we build the simpler version, we'll know these patterns.
-
-### 4. Configuration Infrastructure
-
-`src/shared/settings.ts` (65 lines) provides reusable configuration patterns:
-```typescript
-export function getConfigValue(key: string, defaultValue: string): string {
- // Priority: settings.json → env var → default
-}
-```
-
-**Kept After Revert:** This module is useful for other features.
-
-### 5. Key Architectural Decisions Made
-
-**Decisions that will guide future implementation:**
-1. JIT context filtering must happen in worker (proven via failed hook attempt)
-2. Context must be blocking on UserPromptSubmit (session needs context before processing)
-3. Dynamic timeline search is the right approach (fast, precise, leverages existing infrastructure)
-4. Simple per-request queries should be tried before persistent sessions
-
-### 6. Documentation Quality
-
-- `jit-context-architecture-fix.md`: Documents why hooks can't run SDK queries
-- `session-pattern-parity.md`: Reference for implementing dual sessions
-- Hooks reference: Comprehensive hook documentation added
-
-**Value:** These docs help future contributors understand the system constraints.
-
-### 7. Infrastructure Validation
-
-**Confirmed that our search stack is ready:**
-- SQLite FTS5: Fast full-text search (<50ms)
-- ChromaDB: Semantic search (<200ms with 8,000+ vectors)
-- Timeline search API: Already implemented and tested
-- Worker service: Can handle synchronous AI operations
-
-**The infrastructure exists. We just need a simpler integration.**
-
-## Recommendations
-
-### Immediate Actions
-
-1. **Archive the work:**
- - Keep `failed/jit-context` branch for reference
- - Extract reusable components (settings.ts)
- - Save architecture docs for future features
-
-2. **Document the anti-patterns:**
- - Add this post-mortem to CLAUDE.md references
- - Update coding standards with lessons learned
-
-3. **Reset focus:**
- - Return to validated user needs
- - Prioritize features with clear value propositions
-
-### Future Feature Development
-
-**Gating Questions (Answer before coding):**
-
-1. **User Value:** What specific user problem does this solve?
-2. **Evidence:** Have users requested this or reported the underlying issue?
-3. **Measurement:** How will we know if it's successful?
-4. **Simplicity:** What's the dumbest version that could work?
-5. **Time Limit:** If we can't prove value in 2 hours, should we build it?
-
-**Process:**
-
-```
-VALIDATE → BUILD SIMPLE → TEST → MEASURE → DECIDE
- ↑ ↓
- └──────────── ITERATE OR KILL ────────────┘
-```
-
-### If Context Filtering Returns
-
-Should we revisit this idea in the future:
-
-**Prerequisites:**
-- User feedback requesting better context relevance
-- Metrics showing current context is too broad
-- Evidence that filtering improves response quality
-
-**Simple Approach:**
-```typescript
-// In worker-service.ts /sessions/:id/init
-if (jitEnabled) {
- const observations = await db.getRecentObservations(project, 50);
- const filtered = await simpleFilter(observations, userPrompt); // One-shot query
- return { context: filtered };
-}
-```
-
-**Acceptance Criteria:**
-- <100 lines of code
-- <500ms latency impact
-- No new session types
-- Degrades gracefully on errors
-
-**If that works:** Then consider optimization.
-
-## Conclusion
-
-JIT context filtering failed not because the vision was wrong, but because we jumped to the complex implementation without validating the simple one first. The feature aligns with long-term goals (dynamic, prompt-specific context using our fast search infrastructure), but the persistent-session architecture was premature optimization.
-
-**The right call:** Revert the complex implementation. Build the simple version when ready.
-
-**Key Takeaway:** The vision is sound. The execution was overcomplicated. We now have:
-- Deep knowledge of hook/worker architecture constraints
-- Documented patterns for persistent SDK sessions
-- Validated fast search infrastructure
-- Clear understanding of what to build next time (simple timeline search API integration)
-
-**This was R&D, not failure.** We learned what doesn't work (SDK in hooks), what does work (worker-based AI processing), and how to approach it next time (simple API calls before persistent sessions).
-
-**Next Implementation:**
-When we revisit this (and we should), start with:
-1. Worker endpoint that accepts prompt
-2. Queries existing timeline search API
-3. Returns context
-4. Hook injects context
-5. Validate it improves responses
-6. Then optimize if needed
-
-**Final Thought:** Sometimes you have to build the wrong thing to understand the right thing. That's R&D.
-
----
-
-**Branch Status:**
-- `feature/jit-context`: Abandoned
-- `failed/jit-context`: Archived for reference
-- `main`: Stable at v5.4.0
-
-**Files to Keep:**
-- `src/shared/settings.ts`: Reusable config utilities
-
-**Files Discarded:**
-- Everything else (+2,850 lines)
-
-**Emotional State:** Relieved. Dodged a maintenance nightmare.
diff --git a/docs/context/mem-search-technical-architecture.md b/docs/context/mem-search-technical-architecture.md
deleted file mode 100644
index 0dd0998d..00000000
--- a/docs/context/mem-search-technical-architecture.md
+++ /dev/null
@@ -1,1200 +0,0 @@
-# mem-search Skill: Technical Architecture & Implementation
-
-**Author:** Claude Code
-**Date:** 2025-11-11
-**Purpose:** Comprehensive technical explanation of how the mem-search skill works
-
----
-
-## Table of Contents
-
-1. [Overview](#overview)
-2. [Skill Invocation Mechanism](#skill-invocation-mechanism)
-3. [Search Architecture](#search-architecture)
-4. [Progressive Disclosure Workflow](#progressive-disclosure-workflow)
-5. [Search Operations Deep Dive](#search-operations-deep-dive)
-6. [Backend Processing](#backend-processing)
-7. [Token Efficiency Engineering](#token-efficiency-engineering)
-8. [Complete Request Flow Example](#complete-request-flow-example)
-
----
-
-## Overview
-
-The `mem-search` skill is a **Claude Code Skill** that provides access to claude-mem's persistent cross-session memory database through HTTP API calls. It enables Claude to search through past work, observations, sessions, and user prompts stored in SQLite and ChromaDB.
-
-### Key Components
-
-```
-┌─────────────────────────────────────────────────────────────┐
-│ Claude Code Session │
-│ ┌───────────────────────────────────────────────────────┐ │
-│ │ Claude (LLM) │ │
-│ │ - Reads skill description in session context │ │
-│ │ - Decides when to invoke based on trigger phrases │ │
-│ │ - Loads full SKILL.md when invoked │ │
-│ │ - Executes curl commands from operation guides │ │
-│ └───────────────────────────────────────────────────────┘ │
-│ │ │
-│ ▼ │
-│ ┌───────────────────────────────────────────────────────┐ │
-│ │ mem-search Skill (plugin/skills/mem-search/) │ │
-│ │ - SKILL.md (202 lines, navigation hub) │ │
-│ │ - operations/*.md (12 operation guides) │ │
-│ │ - principles/*.md (2 principle guides) │ │
-│ └───────────────────────────────────────────────────────┘ │
-└─────────────────────────────────────────────────────────────┘
- │
- │ HTTP GET requests
- │ (curl commands)
- ▼
-┌─────────────────────────────────────────────────────────────┐
-│ Worker Service (PM2-managed) │
-│ localhost:37777 │
-│ ┌───────────────────────────────────────────────────────┐ │
-│ │ Express.js HTTP Server │ │
-│ │ - GET /api/search/observations │ │
-│ │ - GET /api/search/sessions │ │
-│ │ - GET /api/search/prompts │ │
-│ │ - GET /api/search/by-type │ │
-│ │ - GET /api/search/by-file │ │
-│ │ - GET /api/search/by-concept │ │
-│ │ - GET /api/search/recent-context │ │
-│ │ - GET /api/search/timeline │ │
-│ │ - GET /api/search/timeline-by-query │ │
-│ │ - GET /api/search/help │ │
-│ └───────────────────────────────────────────────────────┘ │
-│ │ │
-│ ▼ │
-│ ┌─────────────────┬──────────────────────────────────┐ │
-│ │ SessionSearch │ ChromaSync │ │
-│ │ (FTS5) │ (Vector Search) │ │
-│ │ │ │ │
-│ │ SQLite DB │ ChromaDB │ │
-│ │ ~/.claude-mem/ │ ~/.claude-mem/chroma/ │ │
-│ └─────────────────┴──────────────────────────────────┘ │
-└─────────────────────────────────────────────────────────────┘
-```
-
----
-
-## Skill Invocation Mechanism
-
-### Phase 1: Session Start (Skill Discovery)
-
-When a Claude Code session starts:
-
-1. **Claude Code loads all skill descriptions** from `~/.claude/plugins/marketplaces/thedotmack/plugin/skills/*/SKILL.md`
-2. **Only the YAML frontmatter is loaded into context** (~250 tokens for mem-search):
- ```yaml
- ---
- name: mem-search
- description: Search claude-mem's persistent cross-session memory database to find work from previous conversations days, weeks, or months ago. Access past session summaries, bug fixes, feature implementations, and decisions that are NOT in the current conversation context. Use when user asks "did we already solve this?", "how did we do X last time?", "what happened in last week's session?", or needs information from previous sessions stored in the PM2-managed database. Searches observations, session summaries, and user prompts across entire project history.
- ---
- ```
-3. **Claude has awareness** that the skill exists and can be invoked via the `Skill` tool
-
-**Token efficiency:** 250 tokens for skill description vs 2,500 tokens for MCP tool definitions (10x improvement)
-
-### Phase 2: Trigger Detection (Auto-Invocation)
-
-When the user asks a question, Claude:
-
-1. **Analyzes the user prompt** for trigger phrases
-2. **Compares against skill descriptions** loaded in context
-3. **Decides whether to invoke** based on trigger matching
-
-**Example trigger analysis:**
-
-```
-User: "What bugs did we fix last week?"
-
-Claude's internal reasoning:
-- "last week" = temporal trigger → cross-session query
-- "bugs did we fix" = type=bugfix search
-- Description says: "Use when user asks 'did we already solve this?'"
-- Description says: "NOT in the current conversation context"
-- Description says: "previous conversations days, weeks, or months ago"
-→ MATCH: Invoke mem-search skill
-```
-
-**High-effectiveness triggers (85% concrete):**
-- Temporal: "already", "before", "last time", "previously", "last week/month"
-- System-specific: "claude-mem", "PM2-managed database", "cross-session memory"
-- Scope boundaries: "NOT in the current conversation context"
-
-**Why this works:**
-- 5+ unique identifiers distinguish from native memory
-- 9 scope differentiation keywords prevent false matches
-- Explicit negative boundary ("NOT current conversation")
-
-### Phase 3: Skill Loading (Progressive Disclosure)
-
-When Claude invokes the skill:
-
-1. **Loads full SKILL.md** into context (~1,500 tokens for mem-search)
-2. **Reads navigation hub** with operation index
-3. **Chooses appropriate operation** based on query type
-4. **Loads specific operation guide** (e.g., `operations/observations.md`, ~400 tokens)
-5. **Executes HTTP request** via curl command
-
-**Token cost progression:**
-- Session start: +250 tokens (description only)
-- Skill invocation: +1,500 tokens (full SKILL.md)
-- Operation load: +400 tokens (specific operation guide)
-- **Total: ~2,150 tokens** vs ~2,500 for always-loaded MCP tools
-
----
-
-## Search Architecture
-
-### 3-Layer Hybrid Search System
-
-claude-mem uses a **3-layer sequential search architecture** that mimics human long-term memory:
-
-**Storage Flow (Write Path):**
-1. **SQLite First** - Data written synchronously to SQLite (fast, immediate access)
-2. **ChromaDB Background Sync** - Worker asynchronously generates embeddings and syncs to ChromaDB
-
-**Search Flow (Read Path - Sequential, NOT parallel):**
-
-```
-┌─────────────────────────────────────────────────────────────┐
-│ 3-Layer Sequential Search Flow │
-└─────────────────────────────────────────────────────────────┘
- │
- ▼
- ┌─────────────────────────┐
- │ Worker Service │
- │ /api/search/* │
- └─────────────────────────┘
- │
- ▼
-┌─────────────────────────────────────────────────────────────┐
-│ LAYER 1: Semantic Retrieval (ChromaDB) │
-│ ───────────────────────────────────────────────────────── │
-│ Vector similarity search finds semantically relevant items │
-│ Returns: observation IDs in index format (~50-100 tokens) │
-│ Filter: 90-day recency prioritizes recent work │
-│ Output: List of relevant observation IDs │
-└─────────────────────────────────────────────────────────────┘
- │
- ▼
-┌─────────────────────────────────────────────────────────────┐
-│ LAYER 2: Temporal Ordering (SQLite) │
-│ ───────────────────────────────────────────────────────── │
-│ Takes observation IDs from Layer 1 │
-│ Sorts by created_at timestamp (fast SQLite temporal query) │
-│ Identifies: MOST RECENT relevant observation │
-│ Why: ChromaDB doesn't easily query by date range sorted │
-│ Output: Top observation ID by time │
-└─────────────────────────────────────────────────────────────┘
- │
- ▼
-┌─────────────────────────────────────────────────────────────┐
-│ LAYER 3: Instant Context Timeline (SQLite) │
-│ ───────────────────────────────────────────────────────── │
-│ Uses top observation ID from Layer 2 as anchor │
-│ Retrieves N observations BEFORE and AFTER that point │
-│ Provides: "what led here" + "what happened next" context │
-│ This is the KILLER FEATURE: mimics human memory │
-│ Output: Timeline with temporal context │
-└─────────────────────────────────────────────────────────────┘
-```
-
-**Why This Architecture Exists:**
-
-The problem: LLMs don't experience time linearly like humans do. Finding semantically relevant information isn't enough—you need temporal context.
-
-The solution:
-- **ChromaDB** for "what's relevant" (semantic understanding)
-- **SQLite** for "when did it happen" (temporal ordering with fast date-range queries)
-- **Timeline** for "what was the context" (before/after observations)
-
-Together, they mimic how humans recall: "I did X, which led to Y, then Z happened."
-
-**Human Memory Analogy:**
-
-Humans don't just remember isolated facts. They remember sequences: what they did before something, what happened after. The instant context timeline gives LLMs this same temporal awareness that humans experience naturally.
-
-### Search Types
-
-#### 1. Vector Search (ChromaDB) - PRIMARY Search Layer
-
-**Role:** Layer 1 - Semantic Retrieval
-
-**How it works:**
-- Text is embedded using OpenAI's `text-embedding-3-small` model
-- Vector similarity search finds semantically related content, not just keyword matches
-- 90-day recency filter prioritizes recent work
-- Returns observation IDs for temporal processing in Layer 2
-
-**Why it's primary:**
-- Understands meaning, not just keywords ("auth flow" matches "JWT implementation")
-- Finds relevant work even when you don't know exact terms used
-- Semantic understanding crucial for LLM memory retrieval
-
-**Example query:**
-```python
-# User asks: "How did we handle user login flow?"
-collection.query(
- query_texts=["user login flow authentication"],
- n_results=20,
- where={"created_at": {"$gte": ninety_days_ago}}
-)
-# Returns: observation IDs semantically related to login/auth
-```
-
-#### 2. Full-Text Search (FTS5) - Supporting Layer
-
-**Role:** Layer 2 & 3 - Temporal Ordering and Timeline Context
-
-**How it works:**
-- Uses SQLite FTS5 virtual tables for instant keyword matching
-- Supports boolean operators: `AND`, `OR`, `NOT`, `NEAR`, `*` (wildcard)
-- Fast temporal queries with date-range sorting
-- Sub-100ms performance on 8,000+ observations
-
-**Why it's supporting:**
-- ChromaDB handles semantic "what's relevant"
-- SQLite/FTS5 handles temporal "when did it happen" and "what came before/after"
-- Optimized for timeline queries and date-based sorting
-
-**Example query:**
-```sql
--- Takes observation IDs from ChromaDB, sorts by time
-SELECT * FROM observations
-WHERE id IN (/* IDs from ChromaDB */)
-ORDER BY created_at_epoch DESC
-LIMIT 1;
-
--- Then retrieves timeline context around that observation
-SELECT * FROM observations
-WHERE created_at_epoch < anchor_timestamp
-ORDER BY created_at_epoch DESC
-LIMIT 10; -- "what led here"
-```
-
-#### 3. Structured Filters
-
-**Type-based filtering:**
-```sql
--- User asks: "What bugs did we fix?"
-SELECT * FROM observations
-WHERE type = 'bugfix'
-ORDER BY created_at DESC;
-```
-
-**File-based filtering:**
-```sql
--- User asks: "What changes to auth.ts?"
-SELECT * FROM observations
-WHERE files LIKE '%auth.ts%'
-ORDER BY created_at DESC;
-```
-
-**Concept-based filtering:**
-```sql
--- User asks: "What gotchas did we encounter?"
-SELECT * FROM observations
-WHERE concepts LIKE '%gotcha%'
-ORDER BY created_at DESC;
-```
-
----
-
-## Progressive Disclosure Workflow
-
-### The 4-Step Token Efficiency Pattern
-
-Progressive disclosure is **mandatory** to avoid token waste and MCP limits.
-
-#### Step 1: Index Format Request (~50-100 tokens/result)
-
-**What Claude does:**
-```bash
-curl -s "http://localhost:37777/api/search/observations?query=authentication&format=index&limit=5"
-```
-
-**What the backend returns:**
-```json
-{
- "query": "authentication",
- "count": 5,
- "format": "index",
- "results": [
- {
- "id": 1234,
- "type": "feature",
- "title": "Implemented JWT authentication",
- "subtitle": "Added token-based auth with refresh tokens",
- "created_at_epoch": 1699564800000,
- "project": "api-server"
- },
- {
- "id": 1235,
- "type": "bugfix",
- "title": "Fixed token expiration edge case",
- "subtitle": "Handled race condition in refresh flow",
- "created_at_epoch": 1699478400000,
- "project": "api-server"
- }
- // ... 3 more results
- ]
-}
-```
-
-**Token cost:** 5 results × ~75 tokens = **~375 tokens**
-
-#### Step 2: Relevance Assessment (Human-in-Loop)
-
-**What Claude does:**
-- Scans titles and subtitles
-- Identifies which results are relevant to user's question
-- Decides which items need full details
-
-**Example reasoning:**
-```
-User asked: "How did we implement JWT authentication?"
-
-Results scan:
-- #1234 "Implemented JWT authentication" ← RELEVANT (direct match)
-- #1235 "Fixed token expiration edge case" ← MAYBE (related to JWT)
-- #1236 "Added OAuth2 provider" ← NOT RELEVANT (different auth method)
-- #1237 "Refactored user model" ← NOT RELEVANT (no auth connection)
-- #1238 "Updated login UI" ← MAYBE (UI for auth)
-
-Decision: Request full details for #1234, maybe #1235
-```
-
-#### Step 3: Selective Full Details Request (~500-1000 tokens/result)
-
-**What Claude does:**
-```bash
-# Request full details for ONLY relevant items
-curl -s "http://localhost:37777/api/search/observations?query=authentication&format=full&limit=1&offset=0"
-```
-
-**What the backend returns:**
-```json
-{
- "query": "authentication",
- "count": 1,
- "format": "full",
- "results": [
- {
- "id": 1234,
- "type": "feature",
- "title": "Implemented JWT authentication",
- "subtitle": "Added token-based auth with refresh tokens",
- "narrative": "Implemented a complete JWT authentication system with access tokens (15min expiry) and refresh tokens (7-day expiry). The system uses RS256 signing with key rotation support. Added middleware for token validation, refresh endpoint for token renewal, and secure cookie storage for refresh tokens (httpOnly, secure, sameSite). Handled edge cases including concurrent refresh requests, token revocation, and graceful expiry.",
- "facts": [
- "Access tokens expire after 15 minutes",
- "Refresh tokens expire after 7 days",
- "Uses RS256 algorithm for signing",
- "Implements key rotation for security",
- "Stores refresh tokens in httpOnly cookies",
- "Handles concurrent refresh requests with locking"
- ],
- "files": [
- "src/auth/jwt.ts",
- "src/auth/middleware.ts",
- "src/routes/auth.ts"
- ],
- "concepts": ["how-it-works", "pattern"],
- "created_at_epoch": 1699564800000,
- "created_at_iso": "2024-11-09T12:00:00Z",
- "project": "api-server",
- "session_db_id": 123
- }
- ]
-}
-```
-
-**Token cost:** 1 result × ~750 tokens = **~750 tokens**
-
-#### Step 4: Present to User
-
-**What Claude does:**
-- Formats the results for user readability
-- Includes file references with line numbers if available
-- Provides actionable next steps
-
-**Example response:**
-```markdown
-Based on past work, here's how we implemented JWT authentication:
-
-🟣 **Implemented JWT authentication** (session #123, Nov 9 2024)
-
-We built a complete JWT auth system with:
-- Access tokens (15min expiry) + refresh tokens (7-day expiry)
-- RS256 signing with key rotation support
-- Secure httpOnly cookies for refresh token storage
-- Concurrent refresh handling with locking
-
-**Files modified:**
-- src/auth/jwt.ts
-- src/auth/middleware.ts
-- src/routes/auth.ts
-
-**Key implementation details:**
-- Uses RS256 algorithm for signing
-- Implements key rotation for security
-- Handles edge cases: concurrent refreshes, token revocation, graceful expiry
-```
-
-### Token Efficiency Comparison
-
-**Bad approach (no progressive disclosure):**
-```bash
-# Request full details for all 20 results upfront
-curl -s "http://localhost:37777/api/search/observations?query=authentication&format=full&limit=20"
-```
-**Token cost:** 20 × 750 = **15,000 tokens** 🚫
-
-**Good approach (progressive disclosure):**
-```bash
-# Step 1: Get index for 5 results
-curl -s "...&format=index&limit=5" # 375 tokens
-# Step 2: Get full details for 1 relevant result
-curl -s "...&format=full&limit=1&offset=0" # 750 tokens
-```
-**Token cost:** 375 + 750 = **1,125 tokens** ✅
-
-**Savings:** 15,000 - 1,125 = **13,875 tokens saved** (92% reduction)
-
----
-
-## Search Operations Deep Dive
-
-### 1. Observations Search
-
-**User request:** "How did we implement X?"
-
-**Skill workflow:**
-1. Loads `operations/observations.md`
-2. Constructs FTS5 query
-3. Executes HTTP request
-
-**Backend processing:**
-```typescript
-// src/services/worker-service.ts
-app.get('/api/search/observations', async (req, res) => {
- const { query, format, limit, offset, project, type, concepts, files, dateRange } = req.query;
-
- // Step 1: Parse query parameters
- const searchParams = {
- query: query as string,
- limit: parseInt(limit as string) || 20,
- offset: parseInt(offset as string) || 0,
- format: (format as 'index' | 'full') || 'full',
- };
-
- // Step 2: Execute FTS5 search
- const results = await sessionSearch.searchObservations({
- query: searchParams.query,
- limit: searchParams.limit,
- offset: searchParams.offset,
- filters: {
- project: project as string,
- type: type as ObservationType,
- concepts: concepts ? (concepts as string).split(',') : undefined,
- files: files ? (files as string).split(',') : undefined,
- dateRange: dateRange ? JSON.parse(dateRange as string) : undefined,
- }
- });
-
- // Step 3: Format results based on format parameter
- if (searchParams.format === 'index') {
- return res.json({
- query: searchParams.query,
- count: results.length,
- format: 'index',
- results: results.map(r => ({
- id: r.id,
- type: r.type,
- title: r.title,
- subtitle: r.subtitle,
- created_at_epoch: r.created_at_epoch,
- project: r.project,
- concepts: r.concepts,
- }))
- });
- } else {
- return res.json({
- query: searchParams.query,
- count: results.length,
- format: 'full',
- results: results, // Full observation objects
- });
- }
-});
-```
-
-**FTS5 query execution:**
-```typescript
-// src/services/sqlite/SessionSearch.ts
-searchObservations(params: SearchParams): Observation[] {
- const { query, limit, offset, filters } = params;
-
- // Build FTS5 query
- let sql = `
- SELECT o.* FROM observations o
- JOIN observations_fts fts ON o.id = fts.rowid
- WHERE fts MATCH ?
- `;
-
- const queryParams: any[] = [query];
-
- // Apply filters
- if (filters.project) {
- sql += ` AND o.project = ?`;
- queryParams.push(filters.project);
- }
-
- if (filters.type) {
- sql += ` AND o.type = ?`;
- queryParams.push(filters.type);
- }
-
- if (filters.dateRange) {
- sql += ` AND o.created_at_epoch BETWEEN ? AND ?`;
- queryParams.push(filters.dateRange.start, filters.dateRange.end);
- }
-
- // Order by relevance
- sql += ` ORDER BY fts.rank LIMIT ? OFFSET ?`;
- queryParams.push(limit, offset);
-
- return this.db.prepare(sql).all(...queryParams);
-}
-```
-
-### 2. Timeline Search
-
-**User request:** "What was happening around that time?"
-
-**Skill workflow:**
-1. Identifies anchor point (observation ID, session ID, or timestamp)
-2. Loads `operations/timeline.md`
-3. Requests context window before/after anchor
-
-**Backend processing:**
-```typescript
-// Timeline retrieval with depth before/after
-app.get('/api/search/timeline', async (req, res) => {
- const { anchor, depth_before, depth_after, project } = req.query;
-
- // Step 1: Resolve anchor to timestamp
- let anchorTimestamp: number;
- if (typeof anchor === 'string' && anchor.startsWith('S')) {
- // Session ID format: "S123"
- const sessionId = parseInt(anchor.slice(1));
- const session = sessionStore.getSession(sessionId);
- anchorTimestamp = session.created_at_epoch;
- } else if (!isNaN(Number(anchor))) {
- // Observation ID
- const obs = sessionStore.getObservation(Number(anchor));
- anchorTimestamp = obs.created_at_epoch;
- } else {
- // ISO timestamp
- anchorTimestamp = new Date(anchor as string).getTime();
- }
-
- // Step 2: Fetch records before anchor
- const beforeRecords = await sessionSearch.getRecordsBeforeTimestamp({
- timestamp: anchorTimestamp,
- limit: parseInt(depth_before as string) || 10,
- project: project as string,
- });
-
- // Step 3: Fetch records after anchor
- const afterRecords = await sessionSearch.getRecordsAfterTimestamp({
- timestamp: anchorTimestamp,
- limit: parseInt(depth_after as string) || 10,
- project: project as string,
- });
-
- // Step 4: Merge and sort chronologically
- const timeline = [
- ...beforeRecords.reverse(), // Oldest first
- { type: 'anchor', timestamp: anchorTimestamp }, // Anchor point
- ...afterRecords, // Newest last
- ];
-
- return res.json({
- anchor: anchor,
- anchor_timestamp: anchorTimestamp,
- depth_before: beforeRecords.length,
- depth_after: afterRecords.length,
- timeline: timeline,
- });
-});
-```
-
-### 3. Recent Context
-
-**User request:** "What have we been working on?"
-
-**Skill workflow:**
-1. Loads `operations/recent-context.md`
-2. Requests last N sessions with summaries and observations
-
-**Backend processing:**
-```typescript
-app.get('/api/search/recent-context', async (req, res) => {
- const { limit, project } = req.query;
- const sessionLimit = parseInt(limit as string) || 3;
-
- // Step 1: Get recent sessions
- const sessions = await sessionSearch.getRecentSessions({
- limit: sessionLimit,
- project: project as string,
- });
-
- // Step 2: For each session, get summary and observations
- const context = await Promise.all(sessions.map(async (session) => {
- const summary = await sessionStore.getSummary(session.db_id);
- const observations = await sessionStore.getObservationsBySession(session.db_id);
-
- return {
- session: {
- db_id: session.db_id,
- created_at: session.created_at_iso,
- project: session.project,
- },
- summary: summary ? {
- request: summary.request,
- completion: summary.completion,
- learnings: summary.learnings,
- } : null,
- observations: observations.map(obs => ({
- id: obs.id,
- type: obs.type,
- title: obs.title,
- subtitle: obs.subtitle,
- })),
- };
- }));
-
- return res.json({
- limit: sessionLimit,
- project: project || 'all',
- sessions: context,
- });
-});
-```
-
----
-
-## Backend Processing
-
-### Request Flow Through Worker Service
-
-```
-1. HTTP Request arrives
- ↓
-2. Express.js route handler
- ↓
-3. Parameter parsing and validation
- ↓
-4. Database query construction
- ↓
- ┌─────────────────┬──────────────────┐
- ▼ ▼ ▼
-5. SessionSearch SessionStore ChromaSync
- (FTS5 queries) (CRUD ops) (Vector search)
- ↓ ▼ ▼
-6. SQLite DB SQLite DB ChromaDB
- observations_fts observations observations collection
- sessions_fts sessions
- prompts_fts summaries
- ↓ ▼ ▼
-7. Raw results Raw results Vector results
- └─────────────────┴──────────────────┘
- ▼
-8. Result merging and deduplication
- ↓
-9. Format transformation (index vs full)
- ↓
-10. JSON response
- ↓
-11. HTTP response sent to Claude
-```
-
-### Database Schema (Relevant Tables)
-
-**Observations Table:**
-```sql
-CREATE TABLE observations (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- session_db_id INTEGER NOT NULL,
- type TEXT NOT NULL, -- bugfix, feature, refactor, decision, discovery, change
- title TEXT NOT NULL,
- subtitle TEXT,
- narrative TEXT NOT NULL,
- facts TEXT, -- JSON array
- files TEXT, -- JSON array
- concepts TEXT, -- JSON array
- created_at_epoch INTEGER NOT NULL,
- created_at_iso TEXT NOT NULL,
- project TEXT NOT NULL,
- FOREIGN KEY (session_db_id) REFERENCES sessions(db_id)
-);
-```
-
-**FTS5 Virtual Table:**
-```sql
-CREATE VIRTUAL TABLE observations_fts USING fts5(
- title,
- subtitle,
- narrative,
- facts,
- concepts,
- content=observations,
- content_rowid=id
-);
-```
-
-**Auto-sync Triggers:**
-```sql
--- Keep FTS5 in sync with observations table
-CREATE TRIGGER observations_ai AFTER INSERT ON observations BEGIN
- INSERT INTO observations_fts(rowid, title, subtitle, narrative, facts, concepts)
- VALUES (new.id, new.title, new.subtitle, new.narrative, new.facts, new.concepts);
-END;
-
-CREATE TRIGGER observations_ad AFTER DELETE ON observations BEGIN
- DELETE FROM observations_fts WHERE rowid = old.id;
-END;
-
-CREATE TRIGGER observations_au AFTER UPDATE ON observations BEGIN
- UPDATE observations_fts
- SET title = new.title,
- subtitle = new.subtitle,
- narrative = new.narrative,
- facts = new.facts,
- concepts = new.concepts
- WHERE rowid = new.id;
-END;
-```
-
----
-
-## Token Efficiency Engineering
-
-### Why Token Efficiency Matters
-
-1. **MCP tool limits:** Maximum ~2,500 tokens per tool response
-2. **Context window:** Every token loaded reduces available space for code/conversation
-3. **Cost:** API costs scale with tokens
-4. **Performance:** Smaller payloads = faster responses
-
-### Engineering Decisions for Token Efficiency
-
-#### 1. Skill-based Architecture vs MCP Tools
-
-**Old approach (MCP tools):**
-```xml
-
- search_observations
- ...
-
- ...
- ...
-
-
-
-
-```
-**Token cost:** ~2,500 tokens loaded in EVERY session
-
-**New approach (skill):**
-```yaml
----
-name: mem-search
-description: Search claude-mem's persistent cross-session memory database...
----
-```
-**Token cost:** ~250 tokens at session start, ~2,150 total when invoked
-
-**Savings:** ~350 tokens per session (when not invoked), breaks even when invoked
-
-#### 2. Progressive Disclosure in Skill Structure
-
-**SKILL.md structure:**
-- **Navigation hub** (202 lines) - loaded on invocation
-- **Operation guides** (separate files) - loaded only when needed
-- **Principle guides** (separate files) - loaded only when referenced
-
-**Token progression:**
-1. Session start: 250 tokens (description only)
-2. Skill invocation: +1,500 tokens (SKILL.md loaded)
-3. Operation selection: +400 tokens (e.g., observations.md loaded)
-4. Total: ~2,150 tokens
-
-vs loading all 2,724 lines upfront: ~8,000+ tokens
-
-#### 3. Index vs Full Format
-
-**Index format design:**
-```json
-{
- "id": 1234,
- "type": "feature",
- "title": "Implemented JWT authentication",
- "subtitle": "Added token-based auth with refresh tokens",
- "created_at_epoch": 1699564800000,
- "project": "api-server"
-}
-```
-**Token cost:** ~75 tokens
-
-**Full format design:**
-```json
-{
- "id": 1234,
- "type": "feature",
- "title": "Implemented JWT authentication",
- "subtitle": "Added token-based auth with refresh tokens",
- "narrative": "Implemented a complete JWT authentication system with access tokens (15min expiry) and refresh tokens (7-day expiry). The system uses RS256 signing with key rotation support. Added middleware for token validation, refresh endpoint for token renewal, and secure cookie storage for refresh tokens (httpOnly, secure, sameSite). Handled edge cases including concurrent refresh requests, token revocation, and graceful expiry.",
- "facts": [
- "Access tokens expire after 15 minutes",
- "Refresh tokens expire after 7 days",
- "Uses RS256 algorithm for signing",
- "Implements key rotation for security",
- "Stores refresh tokens in httpOnly cookies",
- "Handles concurrent refresh requests with locking"
- ],
- "files": [
- "src/auth/jwt.ts",
- "src/auth/middleware.ts",
- "src/routes/auth.ts"
- ],
- "concepts": ["how-it-works", "pattern"],
- "created_at_epoch": 1699564800000,
- "created_at_iso": "2024-11-09T12:00:00Z",
- "project": "api-server",
- "session_db_id": 123
-}
-```
-**Token cost:** ~750 tokens
-
-**Ratio:** 10x difference
-
-#### 4. Limit Parameter Defaults
-
-**Anti-pattern:**
-```bash
-# Request 20 results by default
-curl "...&limit=20" # 20 × 750 = 15,000 tokens
-```
-
-**Best practice:**
-```bash
-# Start with 5 results
-curl "...&limit=5" # 5 × 75 = 375 tokens (index)
-```
-
-**Skill enforces this:** All operation guides recommend `limit=3-5` for initial requests
-
----
-
-## Complete Request Flow Example
-
-### Scenario: User asks "What bugs did we fix last week?"
-
-#### Step 1: Trigger Detection
-
-**Claude analyzes prompt:**
-- "bugs" → type=bugfix
-- "last week" → temporal trigger, dateRange filter
-- "did we fix" → past tense, cross-session query
-
-**Claude matches against mem-search description:**
-- ✅ "persistent cross-session memory database"
-- ✅ "previous conversations"
-- ✅ "NOT in the current conversation context"
-- ✅ "Use when user asks 'did we already solve this?'"
-
-**Decision:** Invoke `mem-search` skill
-
-#### Step 2: Skill Loading
-
-**Claude invokes:**
-```
-Skill(skill: "claude-mem:mem-search")
-```
-
-**System loads:**
-- `plugin/skills/mem-search/SKILL.md` (~1,500 tokens)
-
-**Claude reads navigation:**
-- "What is the user asking about?" → "Specific type (bugfixes)"
-- Decision guide says: "Use by-type filter"
-- Navigates to `operations/by-type.md`
-
-#### Step 3: Operation Loading
-
-**System loads:**
-- `plugin/skills/mem-search/operations/by-type.md` (~400 tokens)
-
-**Claude reads operation guide:**
-- Parameter: `type=bugfix`
-- Format: Start with `format=index`
-- Limit: Use `limit=5`
-- Date range: Can add `dateRange` filter
-
-#### Step 4: Initial Search (Index Format)
-
-**Claude constructs curl command:**
-```bash
-curl -s "http://localhost:37777/api/search/by-type?type=bugfix&format=index&limit=5&dateRange[start]=2024-11-04&dateRange[end]=2024-11-11"
-```
-
-**Worker service processes:**
-```typescript
-// Parse parameters
-const type = 'bugfix';
-const format = 'index';
-const limit = 5;
-const dateRange = { start: 1730678400000, end: 1731283200000 };
-
-// Execute FTS5 query
-const results = sessionSearch.searchByType({
- type: 'bugfix',
- limit: 5,
- filters: { dateRange }
-});
-
-// Format as index
-return {
- type: 'bugfix',
- count: 3,
- format: 'index',
- results: [
- {
- id: 1245,
- type: 'bugfix',
- title: 'Fixed token expiration edge case',
- subtitle: 'Handled race condition in refresh flow',
- created_at_epoch: 1731100800000,
- project: 'api-server',
- },
- {
- id: 1246,
- type: 'bugfix',
- title: 'Resolved memory leak in worker',
- subtitle: 'Fixed event listener cleanup',
- created_at_epoch: 1731014400000,
- project: 'worker-service',
- },
- {
- id: 1247,
- type: 'bugfix',
- title: 'Fixed CORS preflight handling',
- subtitle: 'Added OPTIONS method support',
- created_at_epoch: 1730928000000,
- project: 'api-server',
- }
- ]
-};
-```
-
-**Token cost:** ~225 tokens (3 results × 75)
-
-#### Step 5: Relevance Assessment
-
-**Claude scans results:**
-- All 3 results are bugfixes from last week ✅
-- Titles are descriptive enough to answer user's question
-
-**Decision:** No need for full details, index provides sufficient information
-
-#### Step 6: Response to User
-
-**Claude formats response:**
-```markdown
-Last week we fixed 3 bugs:
-
-🔴 **#1245** Fixed token expiration edge case (Nov 9, api-server)
- > Handled race condition in refresh flow
-
-🔴 **#1246** Resolved memory leak in worker (Nov 8, worker-service)
- > Fixed event listener cleanup
-
-🔴 **#1247** Fixed CORS preflight handling (Nov 7, api-server)
- > Added OPTIONS method support
-
-Would you like more details on any of these fixes?
-```
-
-#### Alternative Flow: User Wants Details
-
-If user asks: "Tell me more about the memory leak fix"
-
-**Claude requests full details:**
-```bash
-curl -s "http://localhost:37777/api/search/by-type?type=bugfix&format=full&limit=1&offset=1"
-```
-
-**Worker returns full observation:**
-```json
-{
- "type": "bugfix",
- "count": 1,
- "format": "full",
- "results": [{
- "id": 1246,
- "type": "bugfix",
- "title": "Resolved memory leak in worker",
- "subtitle": "Fixed event listener cleanup",
- "narrative": "Discovered a memory leak in the worker service caused by event listeners not being removed when sessions ended. The session cleanup hook was creating new event listeners on each session but never removing them, causing memory to grow unbounded. Fixed by tracking active listeners and explicitly calling removeAllListeners() in the cleanup handler. Added WeakMap to prevent duplicate listener registration.",
- "facts": [
- "Memory grew by ~50MB per 100 sessions",
- "Event listeners were never removed",
- "Session cleanup hook was the culprit",
- "Fixed with removeAllListeners() call",
- "Added WeakMap to track active listeners"
- ],
- "files": [
- "src/hooks/cleanup-hook.ts",
- "src/services/worker-service.ts"
- ],
- "concepts": ["problem-solution", "gotcha"],
- "created_at_epoch": 1731014400000,
- "created_at_iso": "2024-11-08T00:00:00Z",
- "project": "worker-service",
- "session_db_id": 156
- }]
-}
-```
-
-**Token cost:** ~750 tokens
-
-**Claude provides detailed response:**
-```markdown
-The memory leak was in the worker service's session cleanup:
-
-**Problem:** Event listeners were created on each session but never removed, causing ~50MB memory growth per 100 sessions.
-
-**Root cause:** The session cleanup hook (`src/hooks/cleanup-hook.ts`) was adding listeners without tracking or removing them.
-
-**Solution:**
-- Added `removeAllListeners()` call in cleanup handler
-- Implemented WeakMap to prevent duplicate listener registration
-- Memory now stable across sessions
-
-**Files modified:**
-- src/hooks/cleanup-hook.ts
-- src/services/worker-service.ts
-
-This was a "gotcha" - we discovered event listeners in Node.js must be explicitly removed to prevent memory leaks.
-```
-
-### Total Token Cost Breakdown
-
-**Efficient path (index only):**
-- Skill description (session start): 250 tokens
-- SKILL.md (invocation): 1,500 tokens
-- Operation guide: 400 tokens
-- Index results (3 items): 225 tokens
-- **Total: 2,375 tokens**
-
-**Detailed path (one full result):**
-- Efficient path: 2,375 tokens
-- Full details (1 item): 750 tokens
-- **Total: 3,125 tokens**
-
-**Comparison to loading everything upfront:**
-- All 5 results in full format: 5 × 750 = 3,750 tokens
-- Plus operation overhead: ~2,000 tokens
-- **Total: 5,750 tokens**
-
-**Savings:** 5,750 - 3,125 = **2,625 tokens saved** (45% reduction)
-
----
-
-## Summary: How Everything Works
-
-### The Complete Picture
-
-1. **Session Start:**
- - Claude loads skill descriptions (250 tokens per skill)
- - mem-search description contains high-effectiveness triggers
- - Claude has awareness that skill exists
-
-2. **User Query:**
- - Claude analyzes for trigger phrases
- - Temporal triggers: "already", "before", "last time", "last week"
- - System-specific triggers: "claude-mem", "cross-session memory"
- - Scope boundaries: "NOT current conversation"
-
-3. **Skill Invocation:**
- - Claude invokes skill via `Skill` tool
- - Full SKILL.md loads (~1,500 tokens)
- - Decision guide helps choose operation
-
-4. **Operation Selection:**
- - Claude loads specific operation guide (~400 tokens)
- - Learns HTTP API syntax and parameters
- - Understands progressive disclosure workflow
-
-5. **Search Execution:**
- - Claude constructs curl command with appropriate parameters
- - Worker service receives HTTP GET request
- - Backend queries SQLite FTS5 or ChromaDB
- - Results formatted as index or full
-
-6. **Progressive Disclosure:**
- - Start with index format (50-100 tokens/result)
- - Assess relevance from titles/subtitles
- - Request full details only for relevant items (500-1000 tokens/result)
- - Saves 10x tokens vs loading everything
-
-7. **Response Formatting:**
- - Claude presents results to user
- - Includes file references, timestamps, project names
- - Offers to provide more details if needed
-
-### Key Innovations
-
-1. **Trigger Engineering:** 85% concrete triggers ensure reliable auto-invocation
-2. **Progressive Disclosure:** 10x token efficiency via index-first workflow
-3. **Hybrid Search:** FTS5 keyword + vector semantic search for best results
-4. **Skill Architecture:** ~2,250 token savings vs always-loaded MCP tools
-5. **HTTP API:** Simple curl commands vs complex MCP protocol
-6. **Documentation:** 2,724 lines of operation guides prevent hallucination
-
-### Why This Works Better Than MCP Tools
-
-| Aspect | MCP Tools | mem-search Skill |
-|--------|-----------|------------------|
-| Token cost (session start) | ~2,500 tokens | 250 tokens |
-| Token cost (invoked) | ~2,500 tokens | ~2,150 tokens |
-| Auto-invocation reliability | Moderate | High (100% compliance) |
-| Trigger effectiveness | Not measured | 85% concrete |
-| Documentation size | Embedded in tool definitions | 2,724 lines (progressive) |
-| User education | Tool descriptions only | Operations + principles guides |
-| Token efficiency guidance | None | Mandatory progressive disclosure |
-| Scope differentiation | Weak | Strong (9 keywords) |
-
-**Result:** The mem-search skill provides better discoverability, higher reliability, and superior token efficiency compared to the previous MCP tool approach.
-
----
-
-## Further Reading
-
-**In this repository:**
-- `plugin/skills/mem-search/SKILL.md` - User-facing skill documentation
-- `plugin/skills/mem-search/principles/progressive-disclosure.md` - 4-step workflow
-- `plugin/skills/mem-search/principles/anti-patterns.md` - Common mistakes
-- `context/skill-audit-report.md` - Compliance validation
-- `src/services/worker-service.ts` - HTTP API implementation
-- `src/services/sqlite/SessionSearch.ts` - FTS5 search implementation
-- `src/services/sync/ChromaSync.ts` - Vector search implementation
-
-**External:**
-- [Anthropic Skill Creator Documentation](https://github.com/anthropics/anthropic-quickstarts/tree/main/skill-creator)
-- [SQLite FTS5 Documentation](https://www.sqlite.org/fts5.html)
-- [ChromaDB Documentation](https://docs.trychroma.com/)
diff --git a/docs/context/post-about-endless.md b/docs/context/post-about-endless.md
deleted file mode 100644
index fb6f70f9..00000000
--- a/docs/context/post-about-endless.md
+++ /dev/null
@@ -1,77 +0,0 @@
-@everyone
-
-**Endless Mode: Breaking Claude's Context Limits**
-
-## The Problem
-
-Ever hit 67% context usage mid-session and had to restart Claude Code? Context window limits are the #1 killer of long coding sessions. When you're deep in a complex refactor or debugging session, the last thing you want is to lose all that built-up context.
-
-## The Solution: Endless Mode
-
-Endless Mode compresses tool outputs **in real-time** as you work. Instead of storing the full 500-line file you just read, it stores a compact observation like:
-
-> "Read package.json - found 47 dependencies including React 18, TypeScript 5.2, and custom build scripts"
-
-**The result: 70-84% token reduction** on tool outputs, letting you work indefinitely without hitting context limits.
-
-## The Numbers (Real Test Results)
-
-We analyzed **500 transcripts** containing **1,884 tool uses**:
-
-| Metric | Value |
-|--------|-------|
-| Tool uses analyzed | 1,884 |
-| Observations matched | 868 |
-| Eligible for compression | 406 |
-| Compression rate (facts-only) | **84%** |
-| Characters saved | 887,783 of 1,056,285 |
-
-**Which tools benefit most:**
-- **Bash output**: 236 compressible (command outputs -> facts)
-- **Read file contents**: 98 compressible (file contents -> summaries)
-- **Grep results**: 42 compressible (search results -> key matches)
-
-**Key insight**: We only compress tool **outputs**, never inputs. Inputs contain semantic meaning (the actual diff, the query, the code you wrote). Outputs are verbose results that can be summarized without losing meaning.
-
-## The Journey (69 observations over 10 days)
-
-**Nov 16 - The Vision**
-Decided to build Endless Mode as an *optional* feature to avoid mandatory architectural refactoring. The idea: let users opt-in to experimental compression without breaking anything for those who don't.
-
-**Nov 19-20 - Implementation Begins**
-Hit our first bug immediately: duplicate observations appearing on the 2nd prompt of each session. Classic regression - the endless mode changes broke something that was already working. Fixed it, kept going.
-
-**Nov 21 - The Big Switch**
-Made a critical architectural change: switched from **deferred** (async, 5-second timeout) to **synchronous** transformation (blocking, 90-second timeout). Endless Mode needs to wait for compression to complete before continuing - otherwise you'd read uncompressed data.
-
-Multiple rounds of experimental release preparation. Documented all dependencies. Critical bugs kept appearing.
-
-**Nov 22 - Validation**
-Endpoints verified. Toggle working. Documentation reviewed. Things looking stable.
-
-**Nov 23 - The Setback**
-**Disabled endless mode.** It was causing everything to hang. The 90-second synchronous blocking was too aggressive - when compression took too long, the whole system locked up. Had to prioritize stability.
-
-25 sessions had successfully used it before this point.
-
-**Nov 25 - The Solution**
-Created a **beta branch strategy**: Endless Mode lives on `beta/7.0`, isolated from main. Added Version Channel UI so users can safely try it without affecting stable users. Easy rollback if issues occur.
-
-Built analysis scripts to measure *actual* compression rates instead of theoretical. Validated 84% savings on real transcripts.
-
-## How to Try It
-
-**v6.3.1** added a Version Channel switcher:
-
-1. Open http://localhost:37777
-2. Find **"Version Channel"** in Settings sidebar
-3. Click **"Try Beta (Endless Mode)"**
-4. Refresh the UI after switching
-
-**Safe to try**: Your memory data lives in `~/.claude-mem/` - completely separate from the plugin code. Switching branches won't touch your data. Easy rollback with "Switch to Stable" button.
-
-**Current beta branch**: `beta/7.0`
-
----
-
-This has been a real engineering journey - vision, implementation, bugs, setbacks, and creative solutions. The beta branch approach lets us keep iterating on stability while giving adventurous users access to the feature.
diff --git a/docs/context/processing-indicator-audit.md b/docs/context/processing-indicator-audit.md
deleted file mode 100644
index 2c4eb5b8..00000000
--- a/docs/context/processing-indicator-audit.md
+++ /dev/null
@@ -1,416 +0,0 @@
-# Processing Indicator "Fucking Stupid" Audit
-
-## What It SHOULD Do (Simple Version)
-
-1. **Page load**: Check if worker is already processing → spin or don't spin
-2. **UserPromptSubmit**: Start spinning, set worker status "on"
-3. **Summary complete**: Stop spinning, set worker status "off"
-
-**Result**: One boolean. Simple. Clear.
-
----
-
-## What It ACTUALLY Does (Overcomplicated Version)
-
-### Problem 1: Set Instead of Boolean
-
-**Current**: `processingSessions: Set` - tracks individual session IDs
-
-**File**: `src/ui/viewer/hooks/useSSE.ts:12`
-```typescript
-const [processingSessions, setProcessingSessions] = useState>(new Set());
-```
-
-**Why it's stupid**: We don't care WHICH sessions are processing. We just need to know IF anything is processing. The conversion to boolean happens anyway:
-
-**File**: `src/ui/viewer/App.tsx:92`
-```typescript
-isProcessing={processingSessions.size > 0} // ← Converting Set to boolean!
-```
-
-**Fix**: Just use `const [isProcessing, setIsProcessing] = useState(false)`
-
----
-
-### Problem 2: Complex Set Manipulation
-
-**Current**: Add/remove session IDs from Set based on SSE events
-
-**File**: `src/ui/viewer/hooks/useSSE.ts:90-104`
-```typescript
-case 'processing_status':
- if (data.processing) {
- const processing = data.processing;
- console.log('[SSE] Processing status:', processing);
- setProcessingSessions(prev => {
- const next = new Set(prev);
- if (processing.is_processing) {
- next.add(processing.session_id); // ← Why track session ID?
- } else {
- next.delete(processing.session_id); // ← Just need true/false
- }
- return next;
- });
- }
- break;
-```
-
-**Why it's stupid**: Creating new Sets, adding/removing items, all to track individual sessions when we only care about "any processing yes/no"
-
-**Fix**: `setIsProcessing(data.is_processing)`
-
----
-
-### Problem 3: Defensive Cleanup in Multiple Places
-
-**Current**: Two places remove sessions from the Set
-
-**Location 1** - `useSSE.ts:90-104` - Handles `processing_status` events
-**Location 2** - `useSSE.ts:73-78` - Handles `new_summary` events
-
-```typescript
-// Mark session as no longer processing (summary is the final step)
-setProcessingSessions(prev => {
- const next = new Set(prev);
- next.delete(summary.session_id); // ← Defensive cleanup
- return next;
-});
-```
-
-**Why it's stupid**: We're defensively cleaning up in case events arrive out of order. This is a band-aid for not having a single source of truth.
-
-**Fix**: One place sets `isProcessing = false` (summary complete). No defensive cleanup needed.
-
----
-
-### Problem 4: SSE Event Includes Session ID
-
-**Current**: Processing status events include session ID
-
-**File**: `src/services/worker-service.ts:277-285`
-```typescript
-private broadcastProcessingStatus(claudeSessionId: string, isProcessing: boolean): void {
- this.broadcastSSE({
- type: 'processing_status',
- processing: {
- session_id: claudeSessionId, // ← Why send session ID?
- is_processing: isProcessing
- }
- });
-}
-```
-
-**Why it's stupid**: We send session_id but never use it for the spinner decision. The logomark doesn't care WHICH session is processing.
-
-**Fix**: `{ type: 'processing_status', isProcessing: boolean }` - That's it.
-
----
-
-### Problem 5: TypeScript Interface Overcomplicated
-
-**Current**: StreamEvent includes processing object with session_id
-
-**File**: `src/ui/viewer/types.ts:54-57`
-```typescript
-processing?: {
- session_id: string; // ← Unnecessary
- is_processing: boolean;
-};
-```
-
-**Why it's stupid**: Adds complexity to type definitions when we only need the boolean.
-
-**Fix**: `isProcessing?: boolean;`
-
----
-
-### Problem 6: Multiple Broadcast Points (But No Initial State!)
-
-**Current**: 3 places broadcast processing status in worker-service.ts
-
-1. **Line 817**: `handleSummarize()` → `broadcastProcessingStatus(session.claudeSessionId, true)`
-2. **Line 1153**: `processSummarizeMessage()` success → `broadcastProcessingStatus(session.claudeSessionId, false)`
-3. **Line 1183**: `processSummarizeMessage()` no summary → `broadcastProcessingStatus(session.claudeSessionId, false)`
-
-**Why it's stupid**: We broadcast changes but there's NO WAY TO GET INITIAL STATE on page load. If you open the viewer while processing is active, you won't see the spinner until the next status change.
-
-**Fix**: Add `/api/processing-status` endpoint that returns current state. Call it on page load.
-
----
-
-### Problem 7: Skeleton Cards Require Session Tracking
-
-**Current**: Feed.tsx creates skeleton cards for each processing session
-
-**File**: `src/ui/viewer/components/Feed.tsx:66-80`
-```typescript
-const skeletons: FeedItem[] = [];
-processingSessions.forEach(sessionId => { // ← Iterating over Set
- if (!sessionsWithSummaries.has(sessionId)) {
- const prompt = sessionPrompts.get(sessionId);
- skeletons.push({
- itemType: 'skeleton',
- id: sessionId,
- session_id: sessionId, // ← Using individual session IDs
- project: prompt?.project,
- created_at_epoch: Date.now()
- });
- }
-});
-```
-
-**Why it's relevant**: This is the ONLY place that actually uses individual session IDs. If we want per-session skeleton cards, we need session tracking.
-
-**Question for you**: Do we still want skeleton cards in the feed? Or just the logomark spinner?
-
-**Option A**: Keep skeleton cards → Need to track session IDs (current complexity justified)
-**Option B**: Remove skeleton cards → Use simple boolean for logomark only
-
----
-
-### Problem 8: No Synchronization Between Worker State and UI State
-
-**Current**: Worker doesn't maintain processing state. It just broadcasts events.
-
-**Why it's stupid**: If the UI disconnects/reconnects, it loses processing state. Worker should be the source of truth.
-
-**Fix**: Worker maintains `private isProcessing: boolean = false`
-- Set to true on summarize request
-- Set to false when summary completes
-- Expose via `/api/processing-status` endpoint
-- Broadcast changes via SSE
-
----
-
-## The "Fucking Stupid" Score
-
-| Issue | Complexity Cost | Why It's Stupid |
-|-------|----------------|-----------------|
-| Set instead of boolean | HIGH | We convert it to boolean anyway |
-| Complex Set manipulation | HIGH | 10+ lines of code to add/remove from Set |
-| Defensive cleanup in 2 places | MEDIUM | Band-aid for lack of single source of truth |
-| SSE includes unused session_id | LOW | Minor overhead, but conceptually wrong |
-| Overcomplicated TypeScript types | LOW | Makes code harder to read |
-| No initial state endpoint | HIGH | Broken user experience (no spinner on page load during active processing) |
-| Session tracking for skeletons | ??? | Depends if we want per-session skeletons or not |
-| Worker has no state | HIGH | UI is source of truth, should be worker |
-
----
-
-## Proposed Simple Architecture
-
-### Worker Service (Source of Truth)
-
-```typescript
-class WorkerService {
- private isProcessing: boolean = false; // Single source of truth
-
- // New endpoint: GET /api/processing-status
- private handleGetProcessingStatus(req: Request, res: Response): void {
- res.json({ isProcessing: this.isProcessing });
- }
-
- // On summarize request
- private handleSummarize(req: Request, res: Response): void {
- // ... existing code ...
- this.isProcessing = true;
- this.broadcastSSE({ type: 'processing_status', isProcessing: true });
- // ...
- }
-
- // On summary complete
- private processSummarizeMessage(session: SessionState, message: Message): void {
- // ... existing code ...
-
- // After summary is saved/failed:
- this.isProcessing = false;
- this.broadcastSSE({ type: 'processing_status', isProcessing: false });
- }
-}
-```
-
-### React Hook (Simple Boolean)
-
-```typescript
-export function useSSE() {
- const [isProcessing, setIsProcessing] = useState(false);
-
- // On mount: Get initial state
- useEffect(() => {
- fetch('/api/processing-status')
- .then(res => res.json())
- .then(data => setIsProcessing(data.isProcessing));
- }, []);
-
- // Listen for changes
- useEffect(() => {
- const eventSource = new EventSource('/stream');
-
- eventSource.onmessage = (event) => {
- const data = JSON.parse(event.data);
-
- if (data.type === 'processing_status') {
- setIsProcessing(data.isProcessing); // Simple!
- }
- };
-
- return () => eventSource.close();
- }, []);
-
- return { isProcessing, /* other state */ };
-}
-```
-
-### TypeScript Types (Simplified)
-
-```typescript
-export interface StreamEvent {
- type: 'initial_load' | 'new_observation' | 'new_summary' | 'new_prompt' | 'processing_status';
- observations?: Observation[];
- summaries?: Summary[];
- prompts?: UserPrompt[];
- projects?: string[];
- observation?: Observation;
- summary?: Summary;
- prompt?: UserPrompt;
- isProcessing?: boolean; // Simple!
-}
-```
-
-### React Components (No Changes Needed!)
-
-```typescript
-// App.tsx
-const { isProcessing } = useSSE(); // Already a boolean now!
-
- // Just pass it through
-
-// Header.tsx (no changes needed)
-
-```
-
----
-
-## Breaking Changes & Decisions
-
-### Decision 1: What About Skeleton Cards?
-
-**Current**: Skeleton cards in feed show "Generating..." for each processing session
-
-**Options**:
-
-**A) Keep skeleton cards** (requires session tracking)
-- Need to track individual session IDs
-- Justifies the Set complexity
-- Provides per-session feedback in feed
-
-**B) Remove skeleton cards** (simplest)
-- Only logomark spins (global processing indicator)
-- No need to track individual sessions
-- Simpler architecture
-
-**C) Hybrid: Single skeleton card** (middle ground)
-- Show ONE skeleton card when `isProcessing === true`
-- Don't tie it to specific sessions
-- Keep it simple but provide feed feedback
-
-**What do you want?**
-
----
-
-### Decision 2: Multiple Concurrent Sessions?
-
-**Question**: Can multiple sessions be processing simultaneously?
-
-**Current assumption**: Yes (hence the Set)
-
-**Reality check**: Worker processes messages from a queue. Can it actually process multiple sessions at once, or is it sequential?
-
-**If sequential**: We DEFINITELY don't need session tracking. One boolean is perfect.
-
-**If concurrent**: We still might not need session tracking for the logomark (just spin if ANY processing), but skeleton cards would need session IDs.
-
----
-
-## Recommended Implementation Plan
-
-### Phase 1: Add Initial State (Quick Win)
-
-**File**: `src/services/worker-service.ts`
-- Add `private isProcessing: boolean = false;`
-- Add GET `/api/processing-status` endpoint
-- Set `this.isProcessing = true` on line 817
-- Set `this.isProcessing = false` on lines 1153, 1183
-
-**File**: `src/ui/viewer/hooks/useSSE.ts`
-- Add `fetch('/api/processing-status')` on mount
-- Initialize `isProcessing` state from response
-
-**Impact**: Fixes the "no spinner on page load" bug without breaking changes.
-
----
-
-### Phase 2: Simplify State (Breaking Change)
-
-**File**: `src/services/worker-service.ts`
-- Change `broadcastProcessingStatus()` to send `{ type: 'processing_status', isProcessing: boolean }`
-- Remove session_id from broadcast
-
-**File**: `src/ui/viewer/hooks/useSSE.ts`
-- Change `processingSessions` Set to `isProcessing` boolean
-- Simplify event handler: `setIsProcessing(data.isProcessing)`
-- Remove defensive cleanup from `new_summary` handler
-
-**File**: `src/ui/viewer/types.ts`
-- Simplify `StreamEvent.processing` to just `isProcessing?: boolean`
-
-**File**: `src/ui/viewer/App.tsx`
-- Change `processingSessions.size > 0` to just `isProcessing`
-
-**File**: `src/ui/viewer/components/Feed.tsx`
-- **Decision needed**: Remove skeleton cards or show single generic skeleton?
-
-**Impact**: Cleaner code, easier to maintain, fewer bugs.
-
----
-
-## Files That Need Changes
-
-### Worker Service
-- `src/services/worker-service.ts` (add state, endpoint, update broadcasts)
-
-### React
-- `src/ui/viewer/hooks/useSSE.ts` (boolean instead of Set, fetch initial state)
-- `src/ui/viewer/types.ts` (simplify StreamEvent)
-- `src/ui/viewer/App.tsx` (pass boolean instead of Set.size > 0)
-- `src/ui/viewer/components/Feed.tsx` (handle skeleton cards decision)
-- `src/ui/viewer/constants/api.ts` (add PROCESSING_STATUS endpoint)
-
-### No Changes Needed
-- `src/ui/viewer/components/Header.tsx` (already receives boolean)
-- `src/ui/viewer/components/SummarySkeleton.tsx` (might be removed)
-- CSS/animations (work the same with boolean)
-
----
-
-## Summary: What's Fucking Stupid
-
-1. **Set when we only need boolean** ← Biggest offender
-2. **No initial state on page load** ← Broken UX
-3. **Complex Set manipulation** ← 10+ lines for add/remove
-4. **Defensive cleanup in multiple places** ← No single source of truth
-5. **Session IDs in SSE events** ← Data we don't use
-6. **Worker doesn't maintain state** ← UI is source of truth (backwards!)
-
-**Complexity Score**: 7/10 stupid
-
-**After refactor**: 2/10 (the remaining complexity is React/SSE boilerplate)
-
----
-
-## What Do You Want To Do?
-
-Tell me:
-1. **Skeleton cards**: Keep (per-session), remove entirely, or show one generic skeleton?
-2. **Breaking changes**: OK to simplify now, or do you want backwards compatibility?
-3. **Implementation**: Want me to do Phase 1 (quick fix), Phase 2 (full refactor), or both?
diff --git a/docs/context/processing-indicator-code-reference.md b/docs/context/processing-indicator-code-reference.md
deleted file mode 100644
index 997c8ed4..00000000
--- a/docs/context/processing-indicator-code-reference.md
+++ /dev/null
@@ -1,564 +0,0 @@
-# Processing Indicator: Complete Code Reference
-
-This document provides a line-by-line breakdown of every piece of code related to the processing/activity indicator (the spinning logomark in the top left corner of the viewer UI).
-
-## Overview
-
-The processing indicator is a visual cue that shows when the worker service is actively processing memories (observations or summaries). It consists of:
-
-1. **Logomark Image**: `claude-mem-logomark.webp` in the header
-2. **Spinning Animation**: Applied via CSS class when processing is active
-3. **State Management**: Tracked via Server-Sent Events (SSE) from the worker
-4. **Processing Sessions Set**: Maintains active session IDs being processed
-
-## Data Flow
-
-```
-Worker Service
- └─> broadcastProcessingStatus(sessionId, isProcessing)
- └─> broadcastSSE({ type: 'processing_status', ... })
- └─> SSE Event Stream (/stream)
- └─> useSSE Hook (React)
- └─> processingSessions Set
- └─> App.tsx: isProcessing={processingSessions.size > 0}
- └─> Header.tsx: className={isProcessing ? 'spinning' : ''}
- └─> CSS Animation: @keyframes spin
-```
-
----
-
-## 1. TypeScript Types
-
-### File: `src/ui/viewer/types.ts`
-
-**Lines 45-58: StreamEvent interface with processing_status type**
-
-```typescript
-export interface StreamEvent {
- type: 'initial_load' | 'new_observation' | 'new_summary' | 'new_prompt' | 'processing_status';
- observations?: Observation[];
- summaries?: Summary[];
- prompts?: UserPrompt[];
- projects?: string[];
- observation?: Observation;
- summary?: Summary;
- prompt?: UserPrompt;
- processing?: {
- session_id: string;
- is_processing: boolean;
- };
-}
-```
-
-**Purpose**: Defines the structure of SSE events. The `processing_status` type includes a `processing` object that indicates whether a session is currently being processed.
-
----
-
-## 2. Worker Service (Backend)
-
-### File: `src/services/worker-service.ts`
-
-**Lines 247-272: broadcastSSE() - Core SSE broadcasting**
-
-```typescript
-/**
- * Broadcast SSE event to all connected clients
- */
-private broadcastSSE(event: any): void {
- if (this.sseClients.size === 0) {
- return; // No clients connected, skip broadcast
- }
-
- const data = `data: ${JSON.stringify(event)}\n\n`;
- const clientsToRemove: Response[] = [];
-
- for (const client of this.sseClients) {
- try {
- client.write(data);
- } catch (error) {
- // Client disconnected, mark for removal
- clientsToRemove.push(client);
- }
- }
-
- // Clean up disconnected clients
- for (const client of clientsToRemove) {
- this.sseClients.delete(client);
- }
-
- if (clientsToRemove.length > 0) {
- logger.info('WORKER', `SSE cleaned up disconnected clients`, { count: clientsToRemove.length });
- }
-}
-```
-
-**Purpose**: Broadcasts SSE events to all connected UI clients. Handles disconnected clients gracefully.
-
----
-
-**Lines 274-285: broadcastProcessingStatus() - Processing indicator control**
-
-```typescript
-/**
- * Broadcast processing status to SSE clients
- */
-private broadcastProcessingStatus(claudeSessionId: string, isProcessing: boolean): void {
- this.broadcastSSE({
- type: 'processing_status',
- processing: {
- session_id: claudeSessionId,
- is_processing: isProcessing
- }
- });
-}
-```
-
-**Purpose**: Dedicated method for broadcasting processing status changes. Called when sessions start/stop processing.
-
----
-
-**Line 817: Summarize request triggers processing start**
-
-```typescript
-// Notify UI that processing is active
-this.broadcastProcessingStatus(session.claudeSessionId, true);
-```
-
-**Context**: In `handleSummarize()` method - when a summary request is queued, processing starts.
-
-**File location**: `src/services/worker-service.ts:817`
-
----
-
-**Line 1153: Summary generation complete - processing stops**
-
-```typescript
-// Notify UI that processing is complete (summary is the final step)
-this.broadcastProcessingStatus(session.claudeSessionId, false);
-```
-
-**Context**: In `processSummarizeMessage()` after successfully generating and saving a summary.
-
-**File location**: `src/services/worker-service.ts:1153`
-
----
-
-**Line 1183: No summary generated - still mark processing complete**
-
-```typescript
-// Still mark processing as complete even if no summary was generated
-this.broadcastProcessingStatus(session.claudeSessionId, false);
-```
-
-**Context**: In `processSummarizeMessage()` when no summary tags are found in the AI response.
-
-**File location**: `src/services/worker-service.ts:1183`
-
----
-
-## 3. React Hook: SSE Connection
-
-### File: `src/ui/viewer/hooks/useSSE.ts`
-
-**Line 12: processingSessions state initialization**
-
-```typescript
-const [processingSessions, setProcessingSessions] = useState>(new Set());
-```
-
-**Purpose**: Maintains a Set of session IDs currently being processed. Used to determine if any processing is active.
-
----
-
-**Lines 90-104: processing_status event handler**
-
-```typescript
-case 'processing_status':
- if (data.processing) {
- const processing = data.processing;
- console.log('[SSE] Processing status:', processing);
- setProcessingSessions(prev => {
- const next = new Set(prev);
- if (processing.is_processing) {
- next.add(processing.session_id);
- } else {
- next.delete(processing.session_id);
- }
- return next;
- });
- }
- break;
-```
-
-**Purpose**: Listens for `processing_status` SSE events and updates the processingSessions Set:
-- `is_processing: true` → Adds session ID to Set
-- `is_processing: false` → Removes session ID from Set
-
-**File location**: `src/ui/viewer/hooks/useSSE.ts:90-104`
-
----
-
-**Lines 73-78: Summary completion also clears processing status**
-
-```typescript
-// Mark session as no longer processing (summary is the final step)
-setProcessingSessions(prev => {
- const next = new Set(prev);
- next.delete(summary.session_id);
- return next;
-});
-```
-
-**Purpose**: When a `new_summary` event arrives, remove the session from processingSessions (defensive cleanup in case the processing_status event was missed).
-
-**File location**: `src/ui/viewer/hooks/useSSE.ts:73-78`
-
----
-
-**Line 125: Hook return value includes processingSessions**
-
-```typescript
-return { observations, summaries, prompts, projects, processingSessions, isConnected };
-```
-
-**Purpose**: Exposes processingSessions Set to consuming components.
-
----
-
-## 4. React Component: App
-
-### File: `src/ui/viewer/App.tsx`
-
-**Line 20: Destructure processingSessions from useSSE**
-
-```typescript
-const { observations, summaries, prompts, projects, processingSessions, isConnected } = useSSE();
-```
-
-**Purpose**: Gets the processingSessions Set from the SSE hook.
-
----
-
-**Line 92: Convert Set to boolean for Header component**
-
-```typescript
-isProcessing={processingSessions.size > 0}
-```
-
-**Purpose**: Passes `true` to Header if ANY session is being processed (Set has items), `false` otherwise.
-
-**File location**: `src/ui/viewer/App.tsx:92`
-
----
-
-## 5. React Component: Header
-
-### File: `src/ui/viewer/components/Header.tsx`
-
-**Line 12: isProcessing prop definition**
-
-```typescript
-interface HeaderProps {
- isConnected: boolean;
- projects: string[];
- currentFilter: string;
- onFilterChange: (filter: string) => void;
- onSettingsToggle: () => void;
- sidebarOpen: boolean;
- isProcessing: boolean; // ← Processing indicator prop
- themePreference: ThemePreference;
- onThemeChange: (theme: ThemePreference) => void;
-}
-```
-
-**Purpose**: Defines the isProcessing boolean prop for the Header component.
-
----
-
-**Line 24: isProcessing destructured from props**
-
-```typescript
-export function Header({
- isConnected,
- projects,
- currentFilter,
- onFilterChange,
- onSettingsToggle,
- sidebarOpen,
- isProcessing, // ← Received from App.tsx
- themePreference,
- onThemeChange
-}: HeaderProps) {
-```
-
----
-
-**Line 31: Logomark with conditional spinning class**
-
-```typescript
-
-```
-
-**Purpose**: The core of the processing indicator. When `isProcessing` is `true`, adds the `spinning` CSS class to the logomark image, triggering the rotation animation.
-
-**File location**: `src/ui/viewer/components/Header.tsx:31`
-
-**Rendered HTML Examples**:
-- Not processing: `
`
-- Processing: `
`
-
----
-
-## 6. CSS Styling & Animation
-
-### File: `plugin/ui/viewer.html` (compiled output)
-
-**Lines 342-349: Logomark and spinning class styles**
-
-```css
-.logomark {
- height: 32px;
- width: auto;
-}
-
-.logomark.spinning {
- animation: spin 1.5s linear infinite;
-}
-```
-
-**Purpose**:
-- `.logomark`: Base styles for the logo image (32px height, auto width)
-- `.logomark.spinning`: Applies the spin animation when processing is active
- - **Duration**: 1.5 seconds per rotation
- - **Timing**: Linear (constant speed)
- - **Iteration**: Infinite (continues until class is removed)
-
-**File location**: `plugin/ui/viewer.html:342-349`
-
----
-
-**Lines 701-705: Spin animation keyframes**
-
-```css
-@keyframes spin {
- to {
- transform: rotate(360deg);
- }
-}
-```
-
-**Purpose**: Defines the rotation animation. Rotates the element from 0° (implicit) to 360° (full circle).
-
-**File location**: `plugin/ui/viewer.html:701-705`
-
----
-
-## 7. API Endpoint: Stream
-
-### File: `src/ui/viewer/constants/api.ts`
-
-**Line 11: SSE stream endpoint**
-
-```typescript
-export const API_ENDPOINTS = {
- OBSERVATIONS: '/api/observations',
- SUMMARIES: '/api/summaries',
- PROMPTS: '/api/prompts',
- SETTINGS: '/api/settings',
- STATS: '/api/stats',
- STREAM: '/stream', // ← SSE endpoint for processing events
-} as const;
-```
-
-**Purpose**: Centralized API endpoint constant. The `/stream` endpoint is used by `useSSE.ts` to establish the EventSource connection.
-
----
-
-## Bonus: Feed Skeleton Processing Indicator
-
-While not part of the logomark spinner, the feed also shows processing state with skeleton cards and a smaller spinner.
-
-### File: `src/ui/viewer/components/Feed.tsx`
-
-**Lines 66-80: Create skeleton items for processing sessions**
-
-```typescript
-// Create skeleton items for sessions being processed that don't have summaries yet
-const skeletons: FeedItem[] = [];
-processingSessions.forEach(sessionId => {
- if (!sessionsWithSummaries.has(sessionId)) {
- const prompt = sessionPrompts.get(sessionId);
- skeletons.push({
- itemType: 'skeleton',
- id: sessionId,
- session_id: sessionId,
- project: prompt?.project,
- // Always use current time so skeletons appear at top of feed
- created_at_epoch: Date.now()
- });
- }
-});
-```
-
-**Purpose**: Creates temporary skeleton cards for sessions currently being processed (from `processingSessions` Set).
-
----
-
-**Line 104: Render SummarySkeleton component**
-
-```typescript
-} else if (item.itemType === 'skeleton') {
- return ;
-```
-
----
-
-### File: `src/ui/viewer/components/SummarySkeleton.tsx`
-
-**Lines 14-17: Processing indicator in skeleton card**
-
-```typescript
-
-```
-
-**Purpose**: Shows a smaller inline spinner with "Generating..." text in skeleton summary cards.
-
----
-
-### CSS for Feed Spinner
-
-**Lines 682-690: Processing indicator container**
-
-```css
-.processing-indicator {
- display: inline-flex;
- align-items: center;
- gap: 6px;
- color: var(--color-accent-focus);
- font-size: 11px;
- font-weight: 500;
- margin-left: auto;
-}
-```
-
----
-
-**Lines 692-700: Small spinner for skeleton cards**
-
-```css
-.spinner {
- width: 12px;
- height: 12px;
- border: 2px solid var(--color-border-primary);
- border-top-color: var(--color-accent-focus);
- border-radius: 50%;
- animation: spin 0.8s linear infinite;
-}
-```
-
-**Purpose**: Smaller circular spinner (12px) with faster rotation (0.8s) used in skeleton cards. Uses the same `@keyframes spin` animation.
-
----
-
-**Lines 711-715: Skeleton card opacity**
-
-```css
-.summary-skeleton {
- opacity: 0.7;
-}
-
-.summary-skeleton .processing-indicator {
- margin-left: auto;
-}
-```
-
----
-
-**Lines 715-740: Skeleton line animations (shimmer effect)**
-
-```css
-.skeleton-line {
- height: 16px;
- background: linear-gradient(90deg, var(--color-skeleton-base) 25%, var(--color-skeleton-highlight) 50%, var(--color-skeleton-base) 75%);
- background-size: 200% 100%;
- animation: shimmer 1.5s infinite;
- border-radius: 4px;
- margin-bottom: 8px;
-}
-
-.skeleton-title {
- height: 20px;
- width: 80%;
- margin-bottom: 10px;
-}
-
-.skeleton-subtitle {
- height: 16px;
- width: 90%;
-}
-
-.skeleton-subtitle.short {
- width: 60%;
-}
-
-@keyframes shimmer {
- 0% {
- background-position: 200% 0;
- }
- 100% {
- background-position: -200% 0;
- }
-}
-```
-
-**Purpose**: Creates animated placeholder lines with a shimmer effect while summary is being generated.
-
----
-
-## Summary: Complete Processing Flow
-
-1. **User submits prompt** → Claude Code session starts
-2. **Worker receives summarize request** → `worker-service.ts:817` calls `broadcastProcessingStatus(sessionId, true)`
-3. **SSE broadcasts** → `{ type: 'processing_status', processing: { session_id: '...', is_processing: true } }`
-4. **React receives event** → `useSSE.ts:90-104` adds sessionId to `processingSessions` Set
-5. **State flows down** → `App.tsx:92` converts Set size to boolean → `Header.tsx:31` receives `isProcessing={true}`
-6. **CSS class applied** → `className="logomark spinning"` triggers animation
-7. **Logomark spins** → CSS animation `@keyframes spin` rotates 360° every 1.5s
-8. **Feed shows skeleton** → `Feed.tsx:66-80` creates skeleton cards for processing sessions
-9. **Summary completes** → `worker-service.ts:1153` calls `broadcastProcessingStatus(sessionId, false)`
-10. **SSE broadcasts** → `{ type: 'processing_status', processing: { session_id: '...', is_processing: false } }`
-11. **React clears state** → `useSSE.ts:90-104` removes sessionId from Set
-12. **Animation stops** → `isProcessing={false}` removes `spinning` class, logomark stops rotating
-
----
-
-## File Summary
-
-| File | Lines | Purpose |
-|------|-------|---------|
-| `src/ui/viewer/types.ts` | 45-58 | Defines `StreamEvent` interface with `processing_status` type |
-| `src/services/worker-service.ts` | 247-285, 817, 1153, 1183 | Broadcasts processing status via SSE |
-| `src/ui/viewer/hooks/useSSE.ts` | 12, 73-78, 90-104, 125 | Manages `processingSessions` Set from SSE events |
-| `src/ui/viewer/App.tsx` | 20, 92 | Converts Set to boolean, passes to Header |
-| `src/ui/viewer/components/Header.tsx` | 12, 24, 31 | Applies `spinning` class to logomark |
-| `plugin/ui/viewer.html` (CSS) | 342-349, 701-705 | Styles logomark and defines spin animation |
-| `src/ui/viewer/components/Feed.tsx` | 66-80, 104 | Creates skeleton cards for processing sessions |
-| `src/ui/viewer/components/SummarySkeleton.tsx` | 14-17 | Renders inline spinner in skeleton cards |
-| `plugin/ui/viewer.html` (CSS) | 682-740 | Styles for skeleton cards and inline spinner |
-
----
-
-## Key Design Decisions
-
-1. **Set vs Boolean**: Using a `Set` for `processingSessions` allows tracking multiple concurrent sessions. The UI shows spinning as long as *any* session is processing.
-
-2. **Defensive Cleanup**: Both `processing_status` events AND `new_summary` events clear processing state, ensuring the spinner stops even if events arrive out of order.
-
-3. **CSS-Only Animation**: No JavaScript animation loops - pure CSS transforms provide smooth, GPU-accelerated rotation with minimal performance impact.
-
-4. **Dual Indicators**: Header logomark (global processing state) + skeleton cards (per-session processing state) provide both overview and detail-level feedback.
-
-5. **SSE Architecture**: Server-Sent Events provide real-time updates without polling, keeping UI responsive with minimal network overhead.
diff --git a/docs/context/real-time-context-compression.md b/docs/context/real-time-context-compression.md
deleted file mode 100644
index 1d166fc9..00000000
--- a/docs/context/real-time-context-compression.md
+++ /dev/null
@@ -1,427 +0,0 @@
-# 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;
-
- // Helper functions
- private async lookupObservation(toolUseId: string): Promise;
- 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.
diff --git a/docs/context/reddit-posts.md b/docs/context/reddit-posts.md
deleted file mode 100644
index 5f7ec5af..00000000
--- a/docs/context/reddit-posts.md
+++ /dev/null
@@ -1,141 +0,0 @@
-# I built a context management plugin and it CHANGED MY LIFE
-
-Okay so I know this sounds clickbait-y but genuinely: if you've ever spent 20 minutes re-explaining your project architecture to Claude because you started a new chat, this might actually save your sanity.
-
-The actual problem I was trying to solve:
-
-Claude Code is incredible for building stuff, but it has the memory of a goldfish. Every new session I'd be like "okay so remember we're using Express for the API and SQLite for storage and—" and Claude's like "I have never seen this codebase in my life."
-
-What I built:
-
-A plugin that automatically captures everything Claude does during your coding sessions, compresses it with AI (using Claude itself lol), and injects relevant context back into future sessions.
-
-So instead of explaining your project every time, you just... start coding. Claude already knows what happened yesterday.
-
-How it actually works:
-
-Hooks into Claude's tool system and watches everything (file reads, edits, bash commands, etc.)
-
-Background worker processes observations into compressed summaries
-
-When you start a new session, last 10 summaries get auto-injected
-
-Built-in search tools let Claude query its own memory ("what did we decide about auth?")
-
-Runs locally on SQLite + PM2, your code never leaves your machine
-
-Real talk:
-
-I made this because I was building a different project and kept hitting the context limit, then having to restart and re-teach Claude the entire architecture. It was driving me insane. Now Claude just... remembers. It's wild.
-
-Link: https://github.com/thedotmack/claude-mem (AGPL-3.0 licensed)
-
-It is set up to use Claude Code's new plugin system, type the following to install, then restart Claude Code.
-
-/plugin marketplace add thedotmack/claude-mem
-
-/plugin install claude-mem
-Would love feedback from anyone actually building real projects with Claude Code, if this helps you continue, if it helps you save tokens and get more use out of Claude Code. Thanks in advance!
-
-===============================================================================
-
-# How is Claude-Mem different from Claude's New Memory Tool?
-
-A few people have been asking this question on the claude-mem thread I posted yesterday, so I wanted to put up a definitive answer for people, that really explains the differences and how they can be complimentary to each other actually. I used the "claude code docs agent" to help figure this out:
-
----
-
-Based on the documentation, here are the key differences between your Claude-Mem tool and Claude's official memory tool:
-
-Scope and Architecture
-Claude's Memory Tool is designed for single-session memory management within conversations (1). It provides commands like view, create, str_replace, insert, delete, and rename for managing memory files during a conversation (1). The tool automatically includes this instruction: "IMPORTANT: ALWAYS VIEW YOUR MEMORY DIRECTORY BEFORE DOING ANYTHING ELSE" (1).
-
-Your Claude-Mem is a comprehensive multi-session persistence system that captures context across different Claude Code sessions. It uses hooks to automatically capture tool usage, process observations through the Claude Agent SDK, and restore context when new sessions start.
-
-Memory Persistence
-Claude's Memory Tool focuses on within-session memory management. It helps Claude maintain context during a single conversation by reading and writing to memory files (1).
-
-Your Claude-Mem provides cross-session persistence by:
-
-Capturing every tool execution through PostToolUse hooks (2)
-
-Processing observations through the Claude Agent SDK (3)
-
-Automatically injecting summaries from the last few sessions into new session contexts
-
-Using SQLite with FTS5 full-text search for retrieval
-
-Integration Method
-Claude's Memory Tool is a built-in tool that works through the standard tool use interface (1).
-
-Your Claude-Mem integrates as a Claude Code plugin using multiple hooks:
-
-SessionStart for context injection (2)
-
-UserPromptSubmit for session initialization (2)
-
-PostToolUse for observation capture (2)
-
-Stop for summary generation (2)
-
-SessionEnd for cleanup (2)
-
-Search and Retrieval
-Claude's Memory Tool provides basic file operations for memory management (1).
-
-Your Claude-Mem includes an MCP server with 6 specialized search tools:
-
-search_observations - Full-text search across observations
-
-search_sessions - Search across session summaries
-
-find_by_concept - Find by tagged concepts
-
-find_by_file - Find by file paths
-
-find_by_type - Find by observation type
-
-advanced_search - Combined search with filters
-
-Use Cases
-Claude's Memory Tool is ideal for maintaining context within a single conversation, helping with tasks that require remembering information throughout the session (1).
-
-Your Claude-Mem addresses the broader challenge of maintaining project knowledge across multiple Claude Code sessions, essentially solving the session isolation problem that can occur in Claude Code (4).
-
-Your tool appears to be complementary to Claude's memory tool rather than directly competing - it operates at the session level while Claude's memory tool operates within conversations.
-
-===============================================================================
-
-# Hot take... "You're absolutely right!" is a bug, not a feature
-
-When Claude first started saying "You're absolutely right!" I started instructing it to "never tell me I'm absolutely right" because most of the time, it didn't do any verification or thinking before deeming my suggestion "The absolutely right one"
-
-Now we're many versions later, and the team at Claude have embraced "You're absolutely right!" as a "cute" addition to their overall brand, fully accepting this clear anti-pattern.
-
-Is Claude just "smarter" now? Do you perceive "You're absolutely right!" as being given the "absolute right" solution, or are do you feel as though you need to clarify or follow up when this happens?
-
-One of the foundations of my theory behind priming context with claude-mem is this:
-
-"The less Claude has to keep track of that's unrelated to the task at hand, the better Claude will perform that task."
-
-The system I designed uses a parallel instance to manage the memory flow, it's receiving data as it comes in, but the Claude instance you're working with doesn't have any instructions for storing memories. It doesn't need it. That's all handled in the background.
-
-This decoupling matters because every instruction you give Claude is cognitive overhead.
-
-When you load up context with "remember to store this" or "track that observation" or "don't forget to summarize," you're polluting the workspace. Claude has to juggle your actual task AND the meta-task of managing its own memory.
-
-That's when you get lazy agreement.
-
-I've noticed that when Claude's context window gets cluttered with unrelated instructions, this pattern of lazy agreement shows up more and more.
-
-Agreeing with you is easier than deep analysis when the context is already maxed out.
-
-"You're absolutely right!" becomes the path of least resistance.
-
-When Claude can focus purely on your code, your architecture, your question - without memory management instructions competing for attention - it accomplishes tasks faster and more accurately.
-
-The difference is measurable.
-
-The "You're absolutely right!" reflex drops off noticeably because there's room in the context window for actual analysis instead of performative agreement.
-
-What do you think? Does this bother you as much as it does me? 😭
diff --git a/docs/context/rich-context-examples.md b/docs/context/rich-context-examples.md
deleted file mode 100644
index 99eeab9b..00000000
--- a/docs/context/rich-context-examples.md
+++ /dev/null
@@ -1,156 +0,0 @@
-# Rich Context Examples
-
-This document shows what contextual data is available in transcripts
-that could improve observation generation quality.
-
-## Statistics
-
-- Total entries: 369
-- User messages: 74
-- Assistant messages: 133
-- Token usage: 67,465 total
-- Cache efficiency: 6,979,410 tokens read from cache
-
-## Conversation Flow
-
-This shows how user requests, assistant reasoning, and tool executions flow together.
-This is the rich context currently missing from individual tool observations.
-
----
-
-### Example 1
-
-#### 👤 User Request
-```
-Thank you for that. So now that you have a very deep understanding of what we are doing here, I'd like you to begin working on the enhancements to our prompts that leverage data using the transcript model we discovered
-```
-
-#### 🔧 Tools Executed (1)
-
-**TodoWrite**
-```json
-{
- "todos": [
- {
- "content": "Read the ROI implementation plan to understand full scope",
- "status": "in_progress",
- "activeForm": "Reading ROI implementation plan"
- },
- {
-
-```
-
-**📊 Data Available for This Exchange:**
-- User intent: ✅ (218 chars)
-- Assistant reasoning: ✅ (0 chars)
-- Thinking process: ❌
-- Tool executions: ✅ (1 tools)
-- **Currently sent to memory worker:** Tool inputs/outputs only (no context!) ❌
-
----
-
-### Example 2
-
-#### 👤 User Request
-```
-Thank you for that. So now that you have a very deep understanding of what we are doing here, I'd like you to begin working on the enhancements to our prompts that leverage data using the transcript model we discovered
-```
-
-#### 🔧 Tools Executed (1)
-
-**Glob**
-- Pattern: `**/*roi*`
-
-**📊 Data Available for This Exchange:**
-- User intent: ✅ (218 chars)
-- Assistant reasoning: ✅ (0 chars)
-- Thinking process: ❌
-- Tool executions: ✅ (1 tools)
-- **Currently sent to memory worker:** Tool inputs/outputs only (no context!) ❌
-
----
-
-### Example 3
-
-#### 👤 User Request
-```
-Thank you for that. So now that you have a very deep understanding of what we are doing here, I'd like you to begin working on the enhancements to our prompts that leverage data using the transcript model we discovered
-```
-
-#### 🔧 Tools Executed (1)
-
-**Glob**
-- Pattern: `**/*implementation*plan*`
-
-**📊 Data Available for This Exchange:**
-- User intent: ✅ (218 chars)
-- Assistant reasoning: ✅ (0 chars)
-- Thinking process: ❌
-- Tool executions: ✅ (1 tools)
-- **Currently sent to memory worker:** Tool inputs/outputs only (no context!) ❌
-
----
-
-### Example 4
-
-#### 👤 User Request
-```
-Thank you for that. So now that you have a very deep understanding of what we are doing here, I'd like you to begin working on the enhancements to our prompts that leverage data using the transcript model we discovered
-```
-
-#### 🔧 Tools Executed (1)
-
-**Read**
-- Reading: `/Users/alexnewman/Scripts/claude-mem/docs/context/transcript-data-discovery.md`
-
-**📊 Data Available for This Exchange:**
-- User intent: ✅ (218 chars)
-- Assistant reasoning: ✅ (0 chars)
-- Thinking process: ❌
-- Tool executions: ✅ (1 tools)
-- **Currently sent to memory worker:** Tool inputs/outputs only (no context!) ❌
-
----
-
-### Example 5
-
-#### 👤 User Request
-```
-Thank you for that. So now that you have a very deep understanding of what we are doing here, I'd like you to begin working on the enhancements to our prompts that leverage data using the transcript model we discovered
-```
-
-#### 🔧 Tools Executed (1)
-
-**Read**
-- Reading: `/Users/alexnewman/Scripts/claude-mem/IMPLEMENTATION_PLAN_ROI_METRICS.md`
-
-**📊 Data Available for This Exchange:**
-- User intent: ✅ (218 chars)
-- Assistant reasoning: ✅ (0 chars)
-- Thinking process: ❌
-- Tool executions: ✅ (1 tools)
-- **Currently sent to memory worker:** Tool inputs/outputs only (no context!) ❌
-
-
----
-
-## Key Insight
-
-Currently, the memory worker receives **isolated tool executions** via save-hook:
-- tool_name: "Read"
-- tool_input: {"file_path": "src/foo.ts"}
-- tool_output: {file contents}
-
-But the transcript contains **rich contextual data**:
-- WHY the tool was used (user's request)
-- WHAT the assistant planned to accomplish
-- HOW it fits into the broader task
-- The assistant's reasoning/thinking
-- Multiple related tools used together
-
-This context would help the memory worker:
-1. Understand if a tool use is meaningful or routine
-2. Generate observations that capture WHY, not just WHAT
-3. Group related tools into coherent actions
-4. Avoid "investigating" - the context is already present
-
diff --git a/docs/context/search-architecture-analysis.md b/docs/context/search-architecture-analysis.md
deleted file mode 100644
index ddf22e5c..00000000
--- a/docs/context/search-architecture-analysis.md
+++ /dev/null
@@ -1,857 +0,0 @@
-# Search Architecture Analysis
-
-**Date:** 2025-11-11 **Scope:** HTTP API endpoints, MCP search server, DRY violations, architectural recommendations
-
----
-
-## Current State: Dual Search Architectures
-
-### Architecture Overview
-
-```
-┌─────────────────────────────────────────────────────────────┐
-│ Claude Code Session │
-│ │
-│ ┌────────────────────────────────────────────────────┐ │
-│ │ mem-search Skill (ACTIVE) │ │
-│ │ - Uses HTTP API via curl commands │ │
-│ │ - 10 search operations │ │
-│ │ - Progressive disclosure workflow │ │
-│ └────────────────────────────────────────────────────┘ │
-│ │ │
-│ │ HTTP GET │
-│ ▼ │
-│ ┌────────────────────────────────────────────────────┐ │
-│ │ MCP Search Server (DEPRECATED but BUILT) │ │
-│ │ - .mcp.json configured │ │
-│ │ - search-server.mjs exists (74KB) │ │
-│ │ - 9 MCP tools defined │ │
-│ │ - Not used by skill │ │
-│ └────────────────────────────────────────────────────┘ │
-└─────────────────────────────────────────────────────────────┘
- │
- ┌───────────┴───────────┐
- ▼ ▼
-┌──────────────────────────┐ ┌──────────────────────────┐
-│ Worker Service │ │ MCP Server │
-│ (worker-service.ts) │ │ (search-server.ts) │
-│ │ │ │
-│ 10 HTTP Endpoints: │ │ 9 MCP Tools: │
-│ ├─ /api/search/ │ │ ├─ search_observations │
-│ │ observations │ │ ├─ search_sessions │
-│ ├─ /api/search/ │ │ ├─ search_user_prompts │
-│ │ sessions │ │ ├─ find_by_concept │
-│ ├─ /api/search/ │ │ ├─ find_by_file │
-│ │ prompts │ │ ├─ find_by_type │
-│ ├─ /api/search/ │ │ ├─ get_recent_context │
-│ │ by-concept │ │ ├─ get_context_timeline │
-│ ├─ /api/search/ │ │ └─ get_timeline_by_query│
-│ │ by-file │ │ │
-│ ├─ /api/search/ │ │ Built: ✅ │
-│ │ by-type │ │ Used: ❌ │
-│ ├─ /api/context/recent │ │ Configured: ✅ │
-│ ├─ /api/context/ │ │ Status: DEPRECATED │
-│ │ timeline │ │ │
-│ ├─ /api/timeline/ │ │ │
-│ │ by-query │ │ │
-│ └─ /api/search/help │ │ │
-│ │ │ │
-│ Built: ✅ │ │ │
-│ Used: ✅ │ │ │
-│ Status: ACTIVE │ │ │
-└──────────────────────────┘ └──────────────────────────┘
- │ │
- └─────────┬─────────────────┘
- ▼
- ┌────────────────────────────────┐
- │ SessionSearch (Shared Layer) │
- │ - FTS5 queries │
- │ - SQLite operations │
- │ - Common data access │
- └────────────────────────────────┘
- │
- ▼
- ┌────────────────────────────────┐
- │ SQLite Database │
- │ ~/.claude-mem/claude-mem.db │
- └────────────────────────────────┘
-```
-
----
-
-## HTTP Endpoints Architecture
-
-### Location
-
-`src/services/worker-service.ts` (lines 108-118, 748-1174)
-
-### Endpoints (10 total)
-
-| Endpoint | Method | Purpose | Used By |
-| -------------------------- | ------ | ----------------------------------- | ---------------- |
-| `/api/search/observations` | GET | Full-text search observations | mem-search skill |
-| `/api/search/sessions` | GET | Full-text search session summaries | mem-search skill |
-| `/api/search/prompts` | GET | Full-text search user prompts | mem-search skill |
-| `/api/search/by-concept` | GET | Find observations by concept tag | mem-search skill |
-| `/api/search/by-file` | GET | Find work related to specific files | mem-search skill |
-| `/api/search/by-type` | GET | Find observations by type | mem-search skill |
-| `/api/context/recent` | GET | Get recent session context | mem-search skill |
-| `/api/context/timeline` | GET | Get timeline around point in time | mem-search skill |
-| `/api/timeline/by-query` | GET | Search + timeline in one call | mem-search skill |
-| `/api/search/help` | GET | API documentation | mem-search skill |
-
-### Implementation Pattern
-
-**Example: Search Observations**
-
-```typescript
-// src/services/worker-service.ts:748-781
-private handleSearchObservations(req: Request, res: Response): void {
- try {
- // 1. Parse query parameters
- const query = req.query.query as string;
- const format = (req.query.format as string) || 'full';
- const limit = parseInt(req.query.limit as string, 10) || 20;
- const project = req.query.project as string | undefined;
-
- // 2. Validate required parameters
- if (!query) {
- res.status(400).json({ error: 'Missing required parameter: query' });
- return;
- }
-
- // 3. Call SessionSearch (shared data layer)
- const sessionSearch = this.dbManager.getSessionSearch();
- const results = sessionSearch.searchObservations(query, { limit, project });
-
- // 4. Format response based on format parameter
- res.json({
- query,
- count: results.length,
- format,
- results: format === 'index' ? results.map(r => ({
- id: r.id,
- type: r.type,
- title: r.title,
- subtitle: r.subtitle,
- created_at_epoch: r.created_at_epoch,
- project: r.project,
- score: r.score
- })) : results
- });
- } catch (error) {
- logger.failure('WORKER', 'Search observations failed', {}, error as Error);
- res.status(500).json({ error: (error as Error).message });
- }
-}
-```
-
-### Characteristics
-
-**Pros:**
-
-- ✅ Simple HTTP GET requests (curl-friendly)
-- ✅ Standard REST API pattern
-- ✅ Easy to test and debug
-- ✅ No MCP protocol overhead
-- ✅ Works with any HTTP client
-
-**Cons:**
-
-- ⚠️ Parameter parsing duplicated across 10 endpoints
-- ⚠️ Format conversion logic duplicated
-- ⚠️ Error handling pattern repeated
-
----
-
-## MCP Search Server Architecture
-
-### Location
-
-`src/servers/search-server.ts` (1,781 lines)
-
-### Status
-
-- **Built:** ✅ Yes (`plugin/scripts/search-server.mjs`, 74KB)
-- **Configured:** ✅ Yes (`.mcp.json` line 3-6)
-- **Used:** ❌ No (deprecated in v5.4.0)
-- **Maintained:** ⚠️ Source kept for reference
-
-### Tools (9 total)
-
-| Tool Name | Purpose | Line |
-| ----------------------- | -------------------------------------- | -------- |
-| `search_observations` | Search observations with FTS5 + Chroma | 348-422 |
-| `search_sessions` | Search session summaries | 438-490 |
-| `search_user_prompts` | Search user prompts | 506-558 |
-| `find_by_concept` | Find by concept tag | 574-626 |
-| `find_by_file` | Find by file path | 642-694 |
-| `find_by_type` | Find by observation type | 710-762 |
-| `get_recent_context` | Get recent sessions | 778-830 |
-| `get_context_timeline` | Get timeline context | 846-950 |
-| `get_timeline_by_query` | Search + timeline | 966-1064 |
-
-### Implementation Pattern
-
-**Example: Search Observations (MCP)**
-
-```typescript
-// src/servers/search-server.ts:348-422
-{
- name: 'search_observations',
- description: 'Search observations using full-text search across titles, narratives, facts, and concepts...',
- inputSchema: z.object({
- query: z.string().describe('Search query for FTS5 full-text search'),
- format: z.enum(['index', 'full']).default('index').describe('...'),
- ...filterSchema.shape
- }),
- handler: async (args: any) => {
- try {
- const { query, format = 'index', ...options } = args;
- let results: ObservationSearchResult[] = [];
-
- // Hybrid search: Try Chroma semantic search first, fall back to FTS5
- if (chromaClient) {
- try {
- // Step 1: Chroma semantic search (top 100)
- const chromaResults = await queryChroma(query, 100);
-
- if (chromaResults.ids.length > 0) {
- // Step 2: Filter by recency (90 days)
- 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
- if (recentIds.length > 0) {
- const limit = options.limit || 20;
- results = store.getObservationsByIds(recentIds, { orderBy: 'date_desc', limit });
- }
- }
- } catch (chromaError: any) {
- console.error('[search-server] Chroma query failed, falling back to FTS5:', chromaError.message);
- }
- }
-
- // Fall back to FTS5 if Chroma unavailable or returned no results
- if (results.length === 0) {
- results = search.searchObservations(query, options);
- }
-
- // Format results
- if (format === 'index') {
- return {
- content: [{
- type: 'text',
- text: results.map((r, i) => formatObservationIndex(r, i)).join('\n\n') + formatSearchTips()
- }]
- };
- } else {
- return {
- content: results.map(r => ({
- type: 'resource',
- resource: {
- uri: `claude-mem://observation/${r.id}`,
- mimeType: 'text/markdown',
- text: formatObservationResult(r)
- }
- }))
- };
- }
- } catch (error: any) {
- return { content: [{ type: 'text', text: `Error: ${error.message}` }] };
- }
- }
-}
-```
-
-### Characteristics
-
-**Pros:**
-
-- ✅ MCP protocol support
-- ✅ Hybrid search (Chroma + FTS5)
-- ✅ Rich formatting (markdown, resources)
-- ✅ Comprehensive error handling
-
-**Cons:**
-
-- ❌ Not used by skill (deprecated)
-- ❌ ~2,500 token overhead for tool definitions
-- ❌ More complex than HTTP
-- ❌ Still being built despite deprecation
-
----
-
-## DRY Violation Analysis
-
-### Areas of Duplication
-
-#### 1. **Parameter Parsing** (10 HTTP endpoints + 9 MCP tools)
-
-**HTTP Endpoints:**
-
-```typescript
-// Repeated in each endpoint handler
-const query = req.query.query as string;
-const format = (req.query.format as string) || "full";
-const limit = parseInt(req.query.limit as string, 10) || 20;
-const project = req.query.project as string | undefined;
-
-if (!query) {
- res.status(400).json({ error: "Missing required parameter: query" });
- return;
-}
-```
-
-**MCP Tools:**
-
-```typescript
-// Repeated in each tool handler
-const { query, format = "index", ...options } = args;
-if (!query) {
- throw new Error("Missing required parameter: query");
-}
-```
-
-**Violation:** Parameter parsing logic duplicated 19 times (10 + 9)
-
-#### 2. **Format Conversion** (Index vs Full)
-
-**HTTP Endpoints:**
-
-```typescript
-results: format === "index"
- ? results.map((r) => ({
- id: r.id,
- type: r.type,
- title: r.title,
- subtitle: r.subtitle,
- created_at_epoch: r.created_at_epoch,
- project: r.project,
- score: r.score,
- }))
- : results;
-```
-
-**MCP Tools:**
-
-```typescript
-if (format === "index") {
- return {
- content: [
- {
- type: "text",
- text: results.map((r, i) => formatObservationIndex(r, i)).join("\n\n"),
- },
- ],
- };
-} else {
- return {
- content: results.map((r) => ({
- type: "resource",
- resource: {
- uri: `claude-mem://observation/${r.id}`,
- mimeType: "text/markdown",
- text: formatObservationResult(r),
- },
- })),
- };
-}
-```
-
-**Violation:** Format conversion logic duplicated with different output formats
-
-#### 3. **Search Logic Duplication**
-
-**HTTP Endpoints:**
-
-```typescript
-const sessionSearch = this.dbManager.getSessionSearch();
-const results = sessionSearch.searchObservations(query, { limit, project });
-```
-
-**MCP Tools:**
-
-```typescript
-// Hybrid search with Chroma fallback
-if (chromaClient) {
- const chromaResults = await queryChroma(query, 100);
- // ... complex hybrid logic ...
-}
-if (results.length === 0) {
- results = search.searchObservations(query, options);
-}
-```
-
-**Violation:** MCP has hybrid Chroma+FTS5 search, HTTP only has FTS5
-
-#### 4. **Error Handling**
-
-**HTTP Endpoints:**
-
-```typescript
-try {
- // ... handler logic ...
-} catch (error) {
- logger.failure("WORKER", "Search observations failed", {}, error as Error);
- res.status(500).json({ error: (error as Error).message });
-}
-```
-
-**MCP Tools:**
-
-```typescript
-try {
- // ... handler logic ...
-} catch (error: any) {
- return { content: [{ type: "text", text: `Error: ${error.message}` }] };
-}
-```
-
-**Violation:** Different error handling patterns
-
-### DRY Compliance at Data Layer ✅
-
-**Good news:** Both architectures use the **same data layer**:
-
-```
-HTTP Endpoints → SessionSearch → SQLite
-MCP Tools → SessionSearch → SQLite
-```
-
-The `SessionSearch` class is the **single source of truth** for data access. No duplication there.
-
----
-
-## Is curl the Best Approach?
-
-### Current Approach: curl Commands
-
-**Example from skill:**
-
-```bash
-curl -s "http://localhost:37777/api/search/observations?query=authentication&format=index&limit=5"
-```
-
-### Alternative Approaches
-
-#### 1. **MCP Tools** (Deprecated)
-
-**Pros:**
-
-- Native Claude Code protocol
-- Rich type definitions
-- Better error handling
-- Resource formatting
-
-**Cons:**
-
-- ❌ ~2,500 token overhead per session
-- ❌ More complex to implement
-- ❌ Requires MCP server process
-- ❌ Less accessible for external tools
-
-**Verdict:** MCP was deprecated for good reasons (token overhead). curl is better.
-
-#### 2. **Direct Database Access** (Not feasible)
-
-**Pros:**
-
-- No HTTP overhead
-- No worker process needed
-
-**Cons:**
-
-- ❌ Skills can't access files directly
-- ❌ No way to execute TypeScript/SQLite from skill
-- ❌ Would require building native bindings
-
-**Verdict:** Not possible with current skill architecture.
-
-#### 3. **HTTP API via curl** (Current) ✅
-
-**Pros:**
-
-- ✅ Simple, standard protocol
-- ✅ Works with skill architecture
-- ✅ Easy to test (curl in terminal)
-
-- ✅ Language-agnostic
-- ✅ No MCP token overhead
-- ✅ RESTful design
-
-**Cons:**
-
-- ⚠️ Requires worker service running
-- ⚠️ HTTP parsing overhead (minimal)
-
-**Verdict:** **Best approach given constraints.**
-
-### Why curl is Optimal
-
-1. **Skill Constraints:** Skills can only execute shell commands. curl is the standard HTTP client.
-2. **Token Efficiency:** No tool definitions loaded into context (~2,250 token savings).
-3. **Progressive Disclosure:** Skill loads gradually, HTTP requests are made only when needed.
-
-4. **Debuggability:** Easy to test endpoints manually with curl.
-5. **Cross-platform:** curl available on all platforms.
-
----
-
-### Question: "Is it routing into the search-service MCP file or is it a DRY violation?"
-
-**Answer:** Both architectures exist, creating a DRY violation:
-
-1. **HTTP Endpoints** (worker-service.ts) ← **Used by skill**
-2. **MCP Server** (search-server.ts) ← **Deprecated but still built**
-
-### Current State
-
-```
-mem-search skill → HTTP API (worker-service.ts) → SessionSearch → SQLite
- ↑
-MCP search server (deprecated) → SessionSearch ──────────────────────┘
-```
-
-Both use the same data layer (SessionSearch), but:
-
-- ❌ Parameter parsing duplicated
-- ❌ Format conversion duplicated
-- ❌ MCP has hybrid Chroma search, HTTP doesn't
-- ❌ MCP still being built despite deprecation
-
-**You said:** "We are intentionally exposing API search endpoints
-
-```
-┌─────────────────────────────────────────────────────────────┐
-
-│ - Web UI │
-│ - Mobile app │
-│ - VS Code extension │
-│ - CLI tools │
-└─────────────────────────────────────────────────────────────┘
- │
- │ HTTP API
- ▼
-┌─────────────────────────────────────────────────────────────┐
-│ Worker Service HTTP API │
-│ localhost:37777/api/search/* │
-│ │
-│ - Standard REST endpoints │
-│ - JSON responses │
-│ - Query parameter API │
-│ - format=index/full support │
-└─────────────────────────────────────────────────────────────┘
- │
- ▼
-┌─────────────────────────────────────────────────────────────┐
-│ SessionSearch + ChromaSync │
-│ (Shared data layer) │
-└─────────────────────────────────────────────────────────────┘
-```
-
-- Standard REST API
-- Easy to consume from any language/platform
-- Already supports format=index/full for token efficiency
-- Well-documented in skill operation guides
-- Clean JSON responses
-
----
-
-## Architectural Recommendations
-
-### Immediate Actions
-
-#### 1. **Remove MCP Search Server** (Reduce Maintenance Burden)
-
-**Problem:**
-
-- MCP server is deprecated but still being built
-- Adds 1,781 lines of maintenance burden
-- Creates confusion about which search to use
-- DRY violation with HTTP endpoints
-
-**Recommendation:**
-
-```bash
-# Remove from build pipeline
-# scripts/build-hooks.js - already commented out, make permanent
-
-# Delete configuration
-rm plugin/.mcp.json
-
-# Archive source (don't delete, keep for reference)
-git mv src/servers/search-server.ts archive/search-server.ts.archived
-
-# Remove built file
-rm plugin/scripts/search-server.mjs
-```
-
-**Impact:**
-
-- ✅ Reduces build time
-- ✅ Eliminates confusion
-- ✅ Reduces maintenance burden
-- ✅ Removes DRY violation
-- ⚠️ Loses hybrid Chroma search in MCP (but HTTP doesn't have it anyway)
-
-#### 2. **Add Hybrid Search to HTTP Endpoints** (Feature Parity)
-
-**Problem:** MCP server has Chroma hybrid search, HTTP endpoints don't
-
-**Recommendation:**
-
-```typescript
-// src/services/worker-service.ts
-private async handleSearchObservations(req: Request, res: Response): Promise {
- try {
- const { query, format, limit, project } = this.parseSearchParams(req);
-
- // Try hybrid search first if Chroma available
- let results = await this.hybridSearch(query, { limit, project });
-
- // Fallback to FTS5 if Chroma unavailable
- if (results.length === 0) {
- const sessionSearch = this.dbManager.getSessionSearch();
- results = sessionSearch.searchObservations(query, { limit, project });
- }
-
- res.json(this.formatSearchResponse(query, results, format));
- } catch (error) {
- this.handleSearchError(res, 'Search observations failed', error);
- }
-}
-
-// Extract shared methods
-private parseSearchParams(req: Request): SearchParams { /* ... */ }
-private async hybridSearch(query: string, options: SearchOptions): Promise { /* ... */ }
-private formatSearchResponse(query: string, results: any[], format: string): any { /* ... */ }
-private handleSearchError(res: Response, message: string, error: any): void { /* ... */ }
-```
-
-**Impact:**
-
-- ✅ Adds Chroma semantic search to HTTP API
-- ✅ Makes HTTP API feature-complete
-
-#### 3. **Extract Shared Search Logic** (DRY Refactoring)
-
-**Problem:** 10 HTTP endpoints have duplicated parameter parsing and formatting
-
-**Recommendation:**
-
-```typescript
-// src/services/search/SearchController.ts (new file)
-export class SearchController {
- constructor(private sessionSearch: SessionSearch, private chromaSync: ChromaSync) {}
-
- async searchObservations(params: SearchParams): Promise {
- // Shared logic for observations search
- const results = await this.hybridSearch(params);
- return this.formatResponse(results, params.format);
- }
-
- async searchSessions(params: SearchParams): Promise {
- // Shared logic for sessions search
- }
-
- // ... other search methods
-
- private async hybridSearch(params: SearchParams): Promise {
- // Shared hybrid search logic
- }
-
- private formatResponse(results: any[], format: "index" | "full"): SearchResponse {
- // Shared formatting logic
- }
-
- private parseParams(req: Request): SearchParams {
- // Shared parameter parsing
- }
-}
-```
-
-**Usage in worker-service.ts:**
-
-```typescript
-private searchController: SearchController;
-
-private handleSearchObservations(req: Request, res: Response): void {
- try {
- const params = this.searchController.parseParams(req);
- const response = await this.searchController.searchObservations(params);
- res.json(response);
- } catch (error) {
- this.handleSearchError(res, error);
- }
-}
-```
-
-**Impact:**
-
-- ✅ Eliminates 90% of duplication across 10 endpoints
-- ✅ Single source of truth for search logic
-- ✅ Easier to test (test controller, not HTTP layer)
-- ✅ Easier to maintain
-- ✅ Easier to add new search endpoints
-
-### Long-term Architecture
-
-```
-┌─────────────────────────────────────────────────────────────┐
-│ Clients │
-│ ┌──────────────┬──────────────┬──────────────────────┐ │
-
-│ │ Skill │ Frontend │ (CLI, IDE plugins) │ │
-│ └──────────────┴──────────────┴──────────────────────┘ │
-└─────────────────────────────────────────────────────────────┘
- │
- │ HTTP API (REST)
- ▼
-┌─────────────────────────────────────────────────────────────┐
-│ WorkerService (Express.js) │
-│ │
-│ Route Layer (thin) │
-│ ├─ GET /api/search/observations │
-│ ├─ GET /api/search/sessions │
-│ └─ ... (delegates to controller) │
-└─────────────────────────────────────────────────────────────┘
- │
- ▼
-┌─────────────────────────────────────────────────────────────┐
-│ SearchController (business logic) │
-│ │
-│ ├─ searchObservations() │
-│ ├─ searchSessions() │
-│ ├─ hybridSearch() - Chroma + FTS5 │
-│ ├─ formatResponse() - index/full conversion │
-│ └─ parseParams() - parameter validation │
-└─────────────────────────────────────────────────────────────┘
- │
- ┌───────────┴───────────┐
- ▼ ▼
-┌──────────────────────────┐ ┌──────────────────────────┐
-│ SessionSearch (FTS5) │ │ ChromaSync (Vectors) │
-│ - searchObservations() │ │ - queryByEmbedding() │
-│ - searchSessions() │ │ - 90-day recency filter │
-│ - searchPrompts() │ │ - Hydrate from SQLite │
-└──────────────────────────┘ └──────────────────────────┘
- │ │
- └─────────┬─────────────────┘
- ▼
- ┌────────────────────────────────┐
- │ SQLite Database │
- │ ~/.claude-mem/claude-mem.db │
- └────────────────────────────────┘
-```
-
----
-
-## Summary
-
-### Current Architecture Issues
-
-1. ❌ **Dual search implementations** (HTTP + deprecated MCP)
-2. ❌ **DRY violations** across 19 search handlers
-3. ❌ **MCP server still built** despite deprecation
-4. ❌ **HTTP missing hybrid Chroma search** (MCP has it)
-5. ❌ **No shared controller layer** for search logic
-
-### Is curl the Best Approach?
-
-**Yes.** ✅
-
-Given the constraints:
-
-- Skills can only execute shell commands
-
-- Token efficiency vs MCP (~2,250 token savings)
-- Standard REST pattern, easy to consume
-
-curl + HTTP API is the optimal architecture.
-
-### Is it Routing into search-service or DRY Violation?
-
-**DRY violation.** ❌
-
-Both architectures exist and duplicate logic:
-
-- HTTP endpoints (worker-service.ts) ← ACTIVE
-- MCP server (search-server.ts) ← DEPRECATED but BUILT
-
-They share the data layer (SessionSearch) but duplicate:
-
-- Parameter parsing
-- Format conversion
-- Error handling
-- Search orchestration (MCP has Chroma, HTTP doesn't)
-
-### Recommendations Priority
-
-**High Priority:**
-
-1. ✅ Remove MCP search server entirely (archive source)
-2. ✅ Add hybrid Chroma search to HTTP endpoints
-3. ✅ Extract SearchController for shared logic
-
-**Medium Priority:**
-
-5. Add API versioning (/api/v1/search/\*)
-6. Add rate limiting for external access
-
-**Low Priority:** 7. OpenAPI/Swagger documentation
-
-9. WebSocket support for real-time search
-
-### Action Plan
-
-**Phase 1: Cleanup (1 day)**
-
-- Remove .mcp.json
-- Archive search-server.ts
-- Update CLAUDE.md to reflect removal
-- Update build scripts to skip MCP server
-
-**Phase 2: Feature Parity (2 days)**
-
-- Port hybrid Chroma search from MCP to HTTP
-- Test all 10 endpoints with hybrid search
-- Update skill documentation
-
-**Phase 3: DRY Refactoring (3 days)**
-
-- Create SearchController class
-- Extract shared logic (parsing, formatting, errors)
-- Refactor 10 HTTP handlers to use controller
-- Add comprehensive tests
-
-- Document API for external consumption
-- Add authentication/authorization (if needed)
-- Add rate limiting
-- Create OpenAPI spec
-
----
-
-## Files Referenced
-
-**Active:**
-
-- `src/services/worker-service.ts` - HTTP endpoints (1,338 lines)
-- `src/services/sqlite/SessionSearch.ts` - FTS5 search
-- `src/services/sync/ChromaSync.ts` - Vector search
-- `plugin/skills/mem-search/SKILL.md` - Skill using HTTP API
-
-**Deprecated:**
-
-- `src/servers/search-server.ts` - MCP tools (1,781 lines)
-- `plugin/.mcp.json` - MCP configuration
-- `plugin/scripts/search-server.mjs` - Built MCP server (74KB)
-
-**Configuration:**
-
-- `CLAUDE.md` line 314 - Deprecation notice
-- `CHANGELOG.md` line 32-52 - v5.4.0 migration
-- `scripts/build-hooks.js` - Build pipeline (MCP commented out)
diff --git a/docs/context/skill-audit-report.md b/docs/context/skill-audit-report.md
deleted file mode 100644
index 33e83f40..00000000
--- a/docs/context/skill-audit-report.md
+++ /dev/null
@@ -1,160 +0,0 @@
-# Skill Audit Report
-
-**Date:** 2025-11-10
-**Validation:** Anthropic's official skill-creator documentation
-**Skills Audited:** mem-search, search
-
-## Executive Summary
-
-The mem-search skill achieves 100% compliance across all dimensions. The search skill meets technical requirements but fails effectiveness metrics critical for auto-invocation.
-
-**mem-search:** Production-ready. No changes required.
-
-**search:** Requires three critical fixes before Claude reliably discovers and invokes this skill.
-
-## mem-search Skill Results
-
-**Status:** ✅ PASS
-**Compliance:** 100% technical, 100% effectiveness
-**Files:** 17 (202-line SKILL.md + 13 operations + 2 principles)
-
-### Strengths
-
-The skill demonstrates exemplary effectiveness engineering:
-
-1. **Trigger Design (85% concrete)**
- - Five unique identifiers: claude-mem, PM2-managed database, cross-session memory, session summaries, observations
- - Nine scope differentiation keywords
- - Explicit boundary: "NOT in the current conversation context"
- - Minimal overlap with Claude's native capabilities
-
-2. **Capability Visibility (100%)**
- - All nine operations include inline "Use when" examples
- - Decision guide reduces complexity from nine operations to five common cases
- - No navigation friction
-
-3. **Structure**
- - 202 lines (60% under limit)
- - Perfect progressive disclosure with token cost documentation
- - Clean file organization: operations/ and principles/ directories
- - No content duplication
-
-### Issues
-
-**One false positive:** Line 152 contains backslashes in regex notation `(bugfix\|feature\|decision)`. This documents parameter syntax, not Windows paths. No action required.
-
-## search Skill Results
-
-**Status:** ⚠️ NEEDS IMPROVEMENT
-**Compliance:** 100% technical, 67% effectiveness
-**Files:** 13 (96-line SKILL.md + 12 operations)
-
-### Critical Effectiveness Issues
-
-Three failures prevent reliable auto-invocation:
-
-#### Issue 1: Insufficient Scope Differentiation
-
-**Problem:** Description contains only two differentiation keywords (threshold: ≥3). Claude cannot distinguish this skill from native conversation memory.
-
-**Current description:**
-```text
-Search claude-mem persistent memory for past sessions, observations, bugs
-fixed, features implemented, decisions made, code changes, and previous work.
-Use when answering questions about history, finding past decisions, or
-researching previous implementations.
-```
-
-**Domain overlap analysis:**
-- Claude answers natively: "What bugs did we fix?" (current conversation)
-- Claude needs skill: "What bugs did we fix last week?" (external database)
-
-**Fix required:**
-
-```text
-Search claude-mem's external database of past sessions, observations, and
-work from previous conversations. Accesses persistent memory stored outside
-current session context - NOT information from today's conversation. Use when
-users ask about: (1) previous sessions ("what did we do last week?"),
-(2) historical work ("bugs we fixed months ago"), (3) cross-session patterns
-("how have we approached this before?"), (4) work already stored in claude-mem
-("what's in the database about X?"). Searches FTS5 full-text index across
-typed observations (bugfix/feature/refactor/decision/discovery). For current
-session memory, use native conversation context instead.
-```
-
-This adds eight differentiation keywords: "external database", "past sessions", "previous conversations", "outside current session", "NOT information from today's", "last week", "months ago", "already stored in claude-mem".
-
-#### Issue 2: Weak Trigger Specificity
-
-**Problem:** Only 44% concrete triggers (threshold: >50%). Only one unique identifier (threshold: ≥2).
-
-**Abstract triggers (low specificity):**
-- "history" (could mean git history, browser history)
-- "past work" (could mean files, commits, documents)
-- "decisions" (could mean any decision tracking)
-- "previous work" (could mean current session earlier)
-- "implementations" (could mean code in current conversation)
-
-**Concrete triggers (high specificity):**
-- "claude-mem" (unique system name)
-- "persistent memory" (system-specific)
-- "sessions" (cross-session concept)
-- "observations" (system-specific)
-
-**Concrete ratio:** 4/9 = 44% (fails 50% threshold)
-
-**Fix required:** Add system-specific terminology: "HTTP API", "port 37777", "FTS5 full-text index", "typed observations". See combined description in Issue 1 fix.
-
-#### Issue 3: Wasted Content in Body
-
-**Problem:** Lines 10-22 contain "When to Use This Skill" section in SKILL.md body. This loads AFTER triggering, wastes ~200 tokens, provides no value.
-
-**Reference:** [Anthropic's skill-creator documentation](https://github.com/anthropics/anthropic-quickstarts/tree/main/skill-creator) states: "The body is only loaded after triggering, so 'When to Use This Skill' sections in the body are not helpful to Claude."
-
-**Fix required:** Delete lines 10-22 entirely. Move triggering examples to description field (already included in Issue 1 fix).
-
-### Strengths
-
-The skill demonstrates strong structure:
-
-- Excellent progressive disclosure (96-line navigation hub)
-- Strong decision guide (reduces 10 operations to common cases)
-- 100% capability visibility (all operations show purpose inline)
-- No forbidden files or content duplication
-- Clean operations/ directory structure
-
-### Warning
-
-**Minor:** Description uses imperative "Use when" instead of third person. Change to "Useful for" or "Invoked when" for consistency with skill-creator best practices.
-
-## Comparison
-
-| Metric | mem-search | search | Impact |
-|--------|-----------|---------|--------|
-| Concrete triggers | 85% | 44% | search harder to discover |
-| Unique identifiers | 5+ | 1 | search less distinct |
-| Scope differentiation | 9 keywords | 2 keywords | **search conflicts with native memory** |
-| Body optimization | Clean | Wasted section | search wastes tokens |
-| Overall effectiveness | 100% | 67% | search needs fixes |
-
-## Critical Recommendations
-
-The search skill requires three changes before production use:
-
-1. **Rewrite description** to add scope differentiation and concrete triggers (see Issue 1 fix)
-2. **Delete lines 10-22** from SKILL.md body
-3. **Convert to third person** - change "Use when" to "Useful for"
-
-**Why this matters:** Without scope differentiation, Claude assumes "What bugs did we fix?" refers to current conversation, not the external claude-mem database. This causes systematic under-invocation.
-
-## Reference Implementation
-
-The mem-search skill serves as a reference implementation for:
-
-- Trigger design with explicit scope boundaries
-- Progressive disclosure with token efficiency documentation
-- Inline capability visibility eliminating navigation friction
-- Decision guides reducing cognitive load
-
-Study mem-search when creating skills that overlap with Claude's native capabilities.
diff --git a/docs/context/transcript-complete-readable.md b/docs/context/transcript-complete-readable.md
deleted file mode 100644
index e4daedf3..00000000
--- a/docs/context/transcript-complete-readable.md
+++ /dev/null
@@ -1,467 +0,0 @@
-# Transcript: 57dcc12f-4751-46bb-82b4-2aa96a3e226d.jsonl
-
-**Generated:** 11/13/2025, 3:58:53 PM
-**Total Entries:** 448
-**Entry Types:** {
- "summary": 149,
- "file-history-snapshot": 20,
- "user": 98,
- "assistant": 181
-}
-**Showing:** First 10 conversation turns
-
----
-
-## Turn 1
-
-### 👤 User
-**Timestamp:** 2025-11-13T17:09:47.231Z
-**UUID:** 5c4b246c-2100-4e6b-a275-f0bb62bba020
-**Session ID:** 57dcc12f-4751-46bb-82b4-2aa96a3e226d
-**CWD:** /Users/alexnewman/Scripts/claude-mem
-
-I have a question regarding the context timeline that we made. and it's a bit of a thinker...
-
-when i first started out working on claude-mem my idea was using a fast index of memories with links to access them on demand. I thought that claude-mem would be searching for all the things it needed to get the job done, but what I found is that the description of the work done + the files touched actually improved performance tremendously, and without actively searching for things constantly.
-
-I just had a thought that maybe claude isn't searching, because when it reads what it did, it already "knows" how it did that, for the most part. Would you say I'm on the right track? I want you to ultrathink about WHY claude-mem's session start context works so well, from your OWN perspective
-
-### 🤖 Assistant
-**Timestamp:** 2025-11-13T17:10:28.576Z
-**UUID:** 1dd9591b-c98b-4031-ac0a-2f7e067b427a
-**Model:** claude-sonnet-4-5-20250929
-**Stop Reason:** tool_use
-
-**💭 Thinking:**
-
-> This is a fascinating question about the fundamental mechanics of how LLMs process context and why claude-mem's approach is working so well. The user is asking me to think deeply about WHY the session start context (descriptions + file paths) works better than active searching.
->
-> Let me think about this from my own perspective as Claude...
->
-> The user's hypothesis is: "Claude isn't searching because when it reads what it did, it already 'knows' how it did that."
->
-> This is a really interesting observation. Let me think about what happens when I see context at session start vs when I actively search:
->
-> 1. **Session Start Context**: I see summaries like "Fixed authentication bug in src/auth/login.ts by updating token validation logic" with file paths
-> 2. **Active Search**: I use grep/search tools to find specific code or patterns
->
-> The key insight the user is pointing to: When I see the session summary, it's triggering my training knowledge about how these kinds of problems are typically solved. The description + file path combo is enough to reconstruct the likely solution pattern.
->
-> Let me ultrathink about this...
->
-> **Information Density & Compression**:
-> - A summary like "Added JWT token refresh logic to src/auth/token-manager.ts" is highly compressed but information-rich
-> - It contains: the pattern (JWT refresh), the location (token-manager), and the action type (added)
-> - This maps to my training data about how JWT refresh is typically implemented
-> - The file path gives architectural context about where this lives in the codebase
->
-> **Pattern Recognition vs Literal Recall**:
-> - I don't need to see the exact code to "know" what was probably done
-> - JWT token refresh follows common patterns that I've seen thousands of times in training
-> - The summary activates these patterns in my understanding
-> - It's like how a human developer reading "fixed the N+1 query problem" immediately knows the general shape of the solution without seeing the code
->
-> **Contextual Scaffolding**:
-> - File paths provide architectural scaffolding: "src/hooks/context-hook.ts" tells me this is a hook in a hooks directory
->
->
-> - The summary provides semantic scaffolding: what was accomplished
-> - Together they form a mental model of the codebase without needing to read every line
->
-> **Search is for Unknowns, Summaries are for Knowns**:
-> - I search when I don't know something: "Where is error handling done?"
-> - I don't need to search when the summary tells me: "Enhanced error handling in src/services/worker-service.ts"
-> - The summary has already done the work of locating and describing the relevant information
->
-> **The Compression-Decompression Cycle**:
-> - The AI compression (creating observations) is like creating a highly optimized index
-> - When I encounter a summary, I'm essentially decompressing that index into actionable knowledge
-> - This process is far more efficient than searching through raw code or logs
->
-> The summaries act like cognitive shortcuts, instantly reconstructing complex contextual understanding through minimal, strategic information.
->
-> **Why This Works Better Than Constant Searching**:
-> 1. Drastically reduces mental overhead of navigating complex systems
-> 2. Enables rapid pattern recognition across different code segments
-> 3. Provides architectural insights without deep diving into implementation details
-> 4. Minimizes computational and cognitive resources spent on exploration
-> 5. Delivers pre-filtered, contextually relevant information
->
-> **The "Already Knows" Hypothesis**:
-> I recognize complex technical changes instantly through pattern matching. When encountering a concise code modification description, my training allows immediate comprehension by activating relevant technical knowledge domains. The summary triggers an intuitive understanding of systemic implications, similar to how experienced developers quickly grasp architectural shifts through minimal context.
->
-> **The Real Magic**:
-> Leveraging deep pattern recognition capabilities, I can reconstruct technical landscapes from minimal descriptive signals. File paths and succinct change descriptions become powerful activation keys that unlock comprehensive understanding without exhaustive code examination.
->
-> **Why Links-On-Demand Falls Short**:
-> Retrieving information through multiple steps disrupts cognitive flow. Contextual understanding emerges more naturally through continuous narrative, where each detail builds upon previous insights. Searching and parsing interrupt the organic comprehension process, making knowledge integration less fluid and intuitive.
->
-> **The Timeline Innovation**:
-> Project evolution unfolds like a narrative, with each session representing a chapter in technological development. Temporal relationships between changes reveal deeper systemic transformations, mirroring how humans conceptualize complex system growth through interconnected stories of incremental progress.
->
-> The essence lies in providing compressed, meaningful representations that leverage my inherent training knowledge. By presenting contextual frameworks that enable rapid pattern recognition, understanding emerges more elegantly than through fragmented, manually retrieved information.
-
-**📊 Token Usage:**
-- Input: 10
-- Output: 1495
-- Cache creation: 28036
-
----
-
-## Turn 2
-
-### 👤 User
-**Timestamp:** 2025-11-13T17:10:31.963Z
-**UUID:** 364676a7-51c3-4036-afc3-7ff8f7301a8f
-**Session ID:** 57dcc12f-4751-46bb-82b4-2aa96a3e226d
-**CWD:** /Users/alexnewman/Scripts/claude-mem
-
-**Tool Results Submitted (1):**
-
-- **Tool Use ID:** `toolu_01T477WUra1sDR6gHaqZHhKT`
- ```json
- {
- "thoughtNumber": 1,
- "totalThoughts": 8,
- "nextThoughtNeeded": true,
- "branches": [],
- "thoughtHistoryLength": 1
-}
- ```
-
-### 🤖 Assistant
-**Timestamp:** 2025-11-13T17:10:35.939Z
-**UUID:** 201d5077-aae8-4e26-9c5e-6a63f1ca88fa
-**Model:** claude-sonnet-4-5-20250929
-**Stop Reason:** tool_use
-
-**💭 Thinking:**
-
-> Good, I'm using the sequential thinking tool to really dig into this. Let me continue with the next thought about information density.
-
-**📊 Token Usage:**
-- Input: 12
-- Output: 256
-- Cache creation: 1529
-- Cache read: 28036
-
----
-
-## Turn 3
-
-### 👤 User
-**Timestamp:** 2025-11-13T17:10:38.354Z
-**UUID:** 6c2f5da2-6a5a-4742-a46d-6dd5d193ad39
-**Session ID:** 57dcc12f-4751-46bb-82b4-2aa96a3e226d
-**CWD:** /Users/alexnewman/Scripts/claude-mem
-
-**Tool Results Submitted (1):**
-
-- **Tool Use ID:** `toolu_01QMMWEJm8yH25VtBmAzYbmy`
- ```json
- {
- "thoughtNumber": 2,
- "totalThoughts": 8,
- "nextThoughtNeeded": true,
- "branches": [],
- "thoughtHistoryLength": 2
-}
- ```
-
-### 🤖 Assistant
-**Timestamp:** 2025-11-13T17:10:41.841Z
-**UUID:** 3e6f3e78-5c12-4739-a7aa-a8c6fc9d2a33
-**Model:** claude-sonnet-4-5-20250929
-**Stop Reason:** tool_use
-
-**💭 Thinking:**
-
-> Good, now let me think about the difference between pattern recognition and literal recall.
-
-**📊 Token Usage:**
-- Input: 11
-- Output: 262
-- Cache creation: 323
-- Cache read: 29565
-
----
-
-## Turn 4
-
-### 👤 User
-**Timestamp:** 2025-11-13T17:10:45.430Z
-**UUID:** f99751a0-b13a-446a-9b3b-dfc08ffb8e5b
-**Session ID:** 57dcc12f-4751-46bb-82b4-2aa96a3e226d
-**CWD:** /Users/alexnewman/Scripts/claude-mem
-
-**Tool Results Submitted (1):**
-
-- **Tool Use ID:** `toolu_01JwNsDV2HcPnDE2eFJdG7zF`
- ```json
- {
- "thoughtNumber": 3,
- "totalThoughts": 8,
- "nextThoughtNeeded": true,
- "branches": [],
- "thoughtHistoryLength": 3
-}
- ```
-
-### 🤖 Assistant
-**Timestamp:** 2025-11-13T17:10:48.954Z
-**UUID:** 11c28930-0797-4f9e-ada8-5eaa27c70b47
-**Model:** claude-sonnet-4-5-20250929
-**Stop Reason:** tool_use
-
-**💭 Thinking:**
-
-> Now let me think about the architectural scaffolding aspect.
-
-**📊 Token Usage:**
-- Input: 10
-- Output: 260
-- Cache creation: 329
-- Cache read: 29888
-
----
-
-## Turn 5
-
-### 👤 User
-**Timestamp:** 2025-11-13T17:10:53.302Z
-**UUID:** 6f013ecd-a0f0-4bb9-8eca-e6e8d3f80a53
-**Session ID:** 57dcc12f-4751-46bb-82b4-2aa96a3e226d
-**CWD:** /Users/alexnewman/Scripts/claude-mem
-
-**Tool Results Submitted (1):**
-
-- **Tool Use ID:** `toolu_016nFXyGw4SsdKfhUb9eBtkv`
- ```json
- {
- "thoughtNumber": 4,
- "totalThoughts": 8,
- "nextThoughtNeeded": true,
- "branches": [],
- "thoughtHistoryLength": 4
-}
- ```
-
-### 🤖 Assistant
-**Timestamp:** 2025-11-13T17:10:56.583Z
-**UUID:** c6d275b3-7f28-4bdd-8a81-80f1a73ffbc7
-**Model:** claude-sonnet-4-5-20250929
-**Stop Reason:** tool_use
-
-**💭 Thinking:**
-
-> Now let me think about why this works better than active searching.
-
-**📊 Token Usage:**
-- Input: 9
-- Output: 258
-- Cache creation: 327
-- Cache read: 30217
-
----
-
-## Turn 6
-
-### 👤 User
-**Timestamp:** 2025-11-13T17:11:00.306Z
-**UUID:** 885fbc9f-e767-4a16-a8aa-b8bb54849d67
-**Session ID:** 57dcc12f-4751-46bb-82b4-2aa96a3e226d
-**CWD:** /Users/alexnewman/Scripts/claude-mem
-
-**Tool Results Submitted (1):**
-
-- **Tool Use ID:** `toolu_013QydhPuiBKVRHh1vbD8eL8`
- ```json
- {
- "thoughtNumber": 5,
- "totalThoughts": 8,
- "nextThoughtNeeded": true,
- "branches": [],
- "thoughtHistoryLength": 5
-}
- ```
-
-### 🤖 Assistant
-**Timestamp:** 2025-11-13T17:11:04.430Z
-**UUID:** 780354e4-abbf-4d97-b1e2-4f3abc5aef8f
-**Model:** claude-sonnet-4-5-20250929
-**Stop Reason:** tool_use
-
-**💭 Thinking:**
-
-> Now let me think about the compression-decompression cycle - this is a key insight.
-
-**📊 Token Usage:**
-- Input: 8
-- Output: 264
-- Cache creation: 325
-- Cache read: 30544
-
----
-
-## Turn 7
-
-### 👤 User
-**Timestamp:** 2025-11-13T17:11:07.748Z
-**UUID:** 462efec2-17c3-47f7-9171-622e00f6bbf7
-**Session ID:** 57dcc12f-4751-46bb-82b4-2aa96a3e226d
-**CWD:** /Users/alexnewman/Scripts/claude-mem
-
-**Tool Results Submitted (1):**
-
-- **Tool Use ID:** `toolu_01Wa5zzkzrYqf5Cfb5UZi5H6`
- ```json
- {
- "thoughtNumber": 6,
- "totalThoughts": 8,
- "nextThoughtNeeded": true,
- "branches": [],
- "thoughtHistoryLength": 6
-}
- ```
-
-### 🤖 Assistant
-**Timestamp:** 2025-11-13T17:11:12.931Z
-**UUID:** 325a8179-8291-4198-a661-eae8f02d57b2
-**Model:** claude-sonnet-4-5-20250929
-**Stop Reason:** tool_use
-
-**💭 Thinking:**
-
-> The user wants me to continue with the sequential thinking about why claude-mem's session start context works so well. I should continue the thinking process and not mention the todo reminder since this is a theoretical discussion, not a task execution.
-
-**📊 Token Usage:**
-- Input: 10
-- Output: 328
-- Cache creation: 1883
-- Cache read: 28036
-
----
-
-## Turn 8
-
-### 👤 User
-**Timestamp:** 2025-11-13T17:11:17.712Z
-**UUID:** 189d903f-a901-465f-bd6b-8addf44cd5f5
-**Session ID:** 57dcc12f-4751-46bb-82b4-2aa96a3e226d
-**CWD:** /Users/alexnewman/Scripts/claude-mem
-
-**Tool Results Submitted (1):**
-
-- **Tool Use ID:** `toolu_015VKanT9FxnCSsZiTduHd2y`
- ```json
- {
- "thoughtNumber": 7,
- "totalThoughts": 9,
- "nextThoughtNeeded": true,
- "branches": [],
- "thoughtHistoryLength": 7
-}
- ```
-
-### 🤖 Assistant
-**Timestamp:** 2025-11-13T17:11:21.820Z
-**UUID:** 96ca3f03-f889-454d-a13a-96e7a36a3daa
-**Model:** claude-sonnet-4-5-20250929
-**Stop Reason:** tool_use
-
-**💭 Thinking:**
-
-> I want to add one more thought about the temporal/chronological aspect and how that creates a narrative structure that aids understanding.
-
-**📊 Token Usage:**
-- Input: 12
-- Output: 290
-- Cache creation: 392
-- Cache read: 29919
-
----
-
-## Turn 9
-
-### 👤 User
-**Timestamp:** 2025-11-13T17:11:26.591Z
-**UUID:** cfd56a68-8628-4811-9df5-75a8e557bec6
-**Session ID:** 57dcc12f-4751-46bb-82b4-2aa96a3e226d
-**CWD:** /Users/alexnewman/Scripts/claude-mem
-
-**Tool Results Submitted (1):**
-
-- **Tool Use ID:** `toolu_01EWtaz38nageXXEjYzzm2hu`
- ```json
- {
- "thoughtNumber": 8,
- "totalThoughts": 9,
- "nextThoughtNeeded": true,
- "branches": [],
- "thoughtHistoryLength": 8
-}
- ```
-
-### 🤖 Assistant
-**Timestamp:** 2025-11-13T17:11:30.749Z
-**UUID:** bb106bb5-9696-4538-b810-d9fa56bcffff
-**Model:** claude-sonnet-4-5-20250929
-**Stop Reason:** tool_use
-
-**💭 Thinking:**
-
-> Now for the synthesis - bringing all these threads together to answer the user's question about WHY it works so well from my own perspective.
-
-**📊 Token Usage:**
-- Input: 11
-- Output: 360
-- Cache creation: 357
-- Cache read: 30311
-
----
-
-## Turn 10
-
-### 👤 User
-**Timestamp:** 2025-11-13T17:11:36.692Z
-**UUID:** 0aa4f55d-9f5a-485f-9aef-dd90f9e80391
-**Session ID:** 57dcc12f-4751-46bb-82b4-2aa96a3e226d
-**CWD:** /Users/alexnewman/Scripts/claude-mem
-
-**Tool Results Submitted (1):**
-
-- **Tool Use ID:** `toolu_01R5Sy5zet7JCPmXhvDrkMAR`
- ```json
- {
- "thoughtNumber": 9,
- "totalThoughts": 9,
- "nextThoughtNeeded": false,
- "branches": [],
- "thoughtHistoryLength": 9
-}
- ```
-
-### 🤖 Assistant
-**Timestamp:** 2025-11-13T17:11:43.435Z
-**UUID:** 2a5c5c37-9124-441f-ac1f-3f9bff08281c
-**Model:** claude-sonnet-4-5-20250929
-**Stop Reason:** end_turn
-
-**💭 Thinking:**
-
-> This is a fascinating philosophical question about how I process information. The user is asking me to think deeply about WHY claude-mem's session start context works so well from MY perspective as an LLM. They've noticed something important: I don't seem to need to actively search when I have good summaries, and they're hypothesizing it's because the summaries trigger pattern recognition rather than requiring literal recall.
->
-> This is not a task-oriented request - it's a meta-cognitive question about how I think. I should not use TodoWrite here as this is a philosophical discussion, not a task to track.
->
-> The sequential thinking tool has helped me organize my thoughts. Let me now synthesize this into a clear response about my actual experience.
-
-**📊 Token Usage:**
-- Input: 10
-- Output: 936
-- Cache creation: 1140
-- Cache read: 29919
-
----
-
-
-*... 87 more turns not shown*
diff --git a/docs/context/transcript-data-analysis.md b/docs/context/transcript-data-analysis.md
deleted file mode 100644
index 620d3cd4..00000000
--- a/docs/context/transcript-data-analysis.md
+++ /dev/null
@@ -1,632 +0,0 @@
-# Transcript Data Analysis: Available Context for Memory Worker
-
-**Generated:** 2025-11-13
-**Purpose:** Document what contextual data exists in Claude Code transcripts and identify opportunities to improve memory worker observation generation.
-
----
-
-## Executive Summary
-
-**Current State:** The memory worker receives isolated tool executions via `save-hook.ts`:
-- Tool name
-- Tool input (parameters)
-- Tool output (results)
-
-**Available in Transcripts:** Rich contextual data that could dramatically improve observation quality:
-- User's original request/intent
-- Assistant's reasoning (thinking blocks)
-- Full conversation context
-- Tool result data
-- Token usage and performance metrics
-- Session metadata (timestamps, UUIDs, CWD)
-
-**Recommendation:** Enhance the memory worker to receive full conversation context for each tool execution, not just isolated tool data.
-
----
-
-## Transcript Structure
-
-### Entry Types
-
-The transcript file (`~/.claude/projects/-{project}/session-id.jsonl`) contains:
-
-```
-- summary entries (149 in sample)
-- file-history-snapshot entries (18 in sample)
-- user entries (86 in sample)
-- assistant entries (155 in sample)
-```
-
-### Conversation Turn Pattern
-
-Each conversation turn consists of:
-1. **User Entry** - User's request
-2. **Assistant Entry** - Assistant's response
-3. **User Entry** - Tool results submitted back (automatic)
-4. **Assistant Entry** - Assistant processes results and continues
-
-This creates a pattern: User → Assistant → User (tool results) → Assistant (continues) → ...
-
----
-
-## Available Data by Entry Type
-
-### 1. User Entries
-
-**Current Save-Hook Access:**
-- Tool name
-- Tool input
-- Tool output
-
-**Additional Data Available in User Entries:**
-
-```typescript
-interface UserTranscriptEntry {
- type: 'user';
- timestamp: string; // ISO timestamp
- uuid: string; // Unique entry ID
- sessionId: string; // Session identifier
- cwd: string; // Working directory
- parentUuid?: string; // Parent entry reference
- isSidechain: boolean; // Is this a side conversation?
- userType: string; // 'human' or 'system'
- version: string; // Claude Code version
-
- message: {
- role: 'user';
- content: string | ContentItem[]; // Can be text or structured
- };
-
- toolUseResult?: ToolUseResult; // Legacy field, may contain results
-}
-```
-
-**When `content` is an array, it contains:**
-- Text blocks with user's actual request
-- Tool result blocks with complete output data
-
-**Example Structure:**
-```json
-{
- "type": "user",
- "timestamp": "2025-11-13T17:10:31.963Z",
- "uuid": "364676a7-51c3-4036-afc3-7ff8f7301a8f",
- "sessionId": "57dcc12f-4751-46bb-82b4-2aa96a3e226d",
- "cwd": "/Users/alexnewman/Scripts/claude-mem",
- "message": {
- "role": "user",
- "content": [
- {
- "type": "tool_result",
- "tool_use_id": "toolu_01T477WUra1sDR6gHaqZHhKT",
- "content": "[actual tool output data]"
- }
- ]
- }
-}
-```
-
-### 2. Assistant Entries
-
-**Current Save-Hook Access:**
-- Nothing from assistant entries (they happen after tool execution)
-
-**Available Data in Assistant Entries:**
-
-```typescript
-interface AssistantTranscriptEntry {
- type: 'assistant';
- timestamp: string;
- uuid: string;
- sessionId: string;
- cwd: string;
- parentUuid?: string;
- isSidechain: boolean;
- userType: string;
- version: string;
- requestId?: string; // API request ID
-
- message: {
- id: string;
- type: 'message';
- role: 'assistant';
- model: string; // e.g., "claude-sonnet-4-5-20250929"
- content: ContentItem[]; // Array of content blocks
- stop_reason?: string; // 'tool_use' | 'end_turn' | etc.
- stop_sequence?: string;
- usage?: UsageInfo; // Token usage stats
- };
-}
-```
-
-**Content Block Types in `message.content`:**
-
-1. **Thinking Blocks** - Internal reasoning before acting
- ```typescript
- {
- type: 'thinking';
- thinking: string; // Full reasoning text
- signature?: string;
- }
- ```
-
-2. **Text Blocks** - Assistant's visible response
- ```typescript
- {
- type: 'text';
- text: string; // Response text
- }
- ```
-
-3. **Tool Use Blocks** - Tool invocations
- ```typescript
- {
- type: 'tool_use';
- id: string; // Tool use ID
- name: string; // Tool name (e.g., 'Read', 'Edit')
- input: Record; // Complete tool parameters
- }
- ```
-
-**Token Usage Data:**
-```typescript
-interface UsageInfo {
- input_tokens?: number;
- output_tokens?: number;
- cache_creation_input_tokens?: number;
- cache_read_input_tokens?: number;
- service_tier?: string;
-}
-```
-
-### 3. Summary Entries
-
-```typescript
-interface SummaryTranscriptEntry {
- type: 'summary';
- summary: string; // Generated summary text
- leafUuid: string; // UUID of summarized entry
- cwd?: string;
-}
-```
-
-These appear frequently (149 in sample) and provide high-level summaries of work done.
-
----
-
-## Data Flow: Current vs Potential
-
-### Current Flow (Save-Hook Only)
-
-```
-User: "Fix the bug in login.ts"
- ↓
-Assistant: [uses Edit tool]
- ↓
-Tool Execution: Edit(file_path: "login.ts", old_string: "...", new_string: "...")
- ↓
-Save-Hook receives:
- - toolName: "Edit"
- - toolInput: { file_path: "login.ts", old_string: "...", new_string: "..." }
- - toolOutput: { success: true }
- ↓
-Memory Worker generates observation from ONLY tool data
- - No user intent
- - No assistant reasoning
- - No context about WHY this change was made
-```
-
-### Enhanced Flow (With Transcript Context)
-
-```
-User: "Fix the authentication bug - users getting logged out randomly"
- ↓
-Assistant (thinking): "This sounds like a token expiration issue.
- Let me check the JWT handling in login.ts..."
- ↓
-Assistant (uses Edit tool)
- ↓
-Save-Hook receives:
- - toolName: "Edit"
- - toolInput: { file_path: "login.ts", ... }
- - toolOutput: { success: true }
- - PLUS:
- - userRequest: "Fix the authentication bug - users getting logged out randomly"
- - assistantReasoning: "This sounds like a token expiration issue..."
- - conversationContext: Previous 2-3 turns
- - sessionMetadata: { cwd, timestamp, sessionId }
- ↓
-Memory Worker generates richer observation:
- - "Fixed authentication bug causing random logouts"
- - "Problem: JWT tokens expiring too quickly"
- - "Solution: Updated token expiration to 24h in login.ts"
- - "Files: src/auth/login.ts"
- - "Concepts: authentication, token-management, bugfix"
-```
-
----
-
-## Specific Opportunities
-
-### 1. User Intent Extraction
-
-**Problem:** Current observations lack user intent.
-
-**Solution:** Parse the most recent user text entry before the tool execution.
-
-**Implementation:**
-- Walk backward from tool execution entry
-- Find first user entry with text content
-- Extract text blocks (filter out tool_result blocks)
-
-**Example:**
-```typescript
-// In save-hook.ts
-const userEntries = parser.getUserEntries();
-const recentUserMessage = findUserMessageBeforeTool(userEntries, toolExecutionTimestamp);
-const userIntent = extractTextFromContent(recentUserMessage.content);
-```
-
-### 2. Assistant Reasoning
-
-**Problem:** We don't capture WHY the assistant chose to use a tool.
-
-**Solution:** Extract thinking blocks from assistant entry immediately before tool use.
-
-**Implementation:**
-- Find assistant entry that contains the tool_use block
-- Extract thinking blocks from same entry
-- Include first ~500 chars of thinking in observation context
-
-**Example:**
-```typescript
-const assistantEntry = findAssistantEntryWithToolUse(toolUseId);
-const thinkingBlocks = assistantEntry.message.content.filter(c => c.type === 'thinking');
-const reasoning = thinkingBlocks.map(b => b.thinking).join('\n');
-```
-
-### 3. Tool Results Context
-
-**Problem:** Tool output alone doesn't show what was found or changed.
-
-**Solution:** Access full tool result content from next user entry.
-
-**Implementation:**
-- Tool execution happens in assistant entry
-- Results come back in next user entry as tool_result content
-- Save-hook can access both
-
-**Current Structure:**
-```
-Assistant Entry:
- { type: 'tool_use', id: 'toolu_123', name: 'Read', input: {...} }
- ↓
-User Entry (automatic):
- { type: 'tool_result', tool_use_id: 'toolu_123', content: "file contents..." }
-```
-
-**Opportunity:** Match tool_use_id to tool_result and include full result content.
-
-### 4. Conversation Context
-
-**Problem:** Isolated tool executions miss the larger conversation flow.
-
-**Solution:** Include last N conversation turns (2-3 turns is usually sufficient).
-
-**Implementation:**
-- Get entries from transcript within time window (e.g., last 5 minutes)
-- Include user messages and assistant text responses
-- Exclude thinking blocks to save tokens
-
-**Example Context:**
-```
-Turn 1:
-User: "I need to add dark mode support"
-Assistant: "I'll help you add dark mode. Let me start by..."
-
-Turn 2:
-User: [tool results]
-Assistant: "Now I'll update the theme configuration..."
-
-Turn 3: [current tool execution]
-```
-
-### 5. Session Metadata
-
-**Problem:** Observations lack temporal and project context.
-
-**Solution:** Include session metadata in observation generation.
-
-**Available Fields:**
-- `cwd` - Working directory (project path)
-- `timestamp` - Exact time of execution
-- `sessionId` - Session identifier
-- `uuid` - Entry identifier
-- `version` - Claude Code version
-
-**Use Case:** Helps with project-specific context and temporal queries.
-
-### 6. Token Usage Metrics
-
-**Problem:** No visibility into performance and cost.
-
-**Solution:** Track token usage per observation.
-
-**Available Data:**
-- Input tokens
-- Output tokens
-- Cache creation tokens
-- Cache read tokens
-
-**Use Case:**
-- Performance monitoring
-- Cost attribution
-- Cache effectiveness analysis
-
----
-
-## Recommended Implementation Strategy
-
-### Phase 1: User Intent (High Impact, Low Effort)
-
-**Change:** Modify save-hook to extract user's most recent message.
-
-**Implementation:**
-```typescript
-// In save-hook.ts
-import { TranscriptParser } from '../utils/transcript-parser';
-
-const parser = new TranscriptParser(transcriptPath);
-const userIntent = parser.getLastUserMessage();
-
-// Send to worker
-await workerService.saveToolExecution({
- ...existingData,
- userIntent, // NEW
-});
-```
-
-**Impact:** Observations now include "what the user wanted to do".
-
-### Phase 2: Assistant Reasoning (High Impact, Medium Effort)
-
-**Change:** Extract thinking blocks from assistant entry containing tool use.
-
-**Implementation:**
-```typescript
-const assistantEntries = parser.getAssistantEntries();
-const toolUseEntry = findEntryWithToolUse(assistantEntries, toolUseId);
-const thinking = extractThinkingBlocks(toolUseEntry);
-
-await workerService.saveToolExecution({
- ...existingData,
- userIntent,
- assistantReasoning: thinking, // NEW
-});
-```
-
-**Impact:** Observations include "why the assistant chose this approach".
-
-### Phase 3: Conversation Context (Medium Impact, High Effort)
-
-**Change:** Include last 2-3 conversation turns.
-
-**Implementation:**
-```typescript
-const recentTurns = getRecentConversationTurns(parser, 3);
-
-await workerService.saveToolExecution({
- ...existingData,
- userIntent,
- assistantReasoning: thinking,
- conversationContext: recentTurns, // NEW
-});
-```
-
-**Impact:** Observations understand multi-turn workflows.
-
-### Phase 4: Enhanced Metadata (Low Impact, Low Effort)
-
-**Change:** Include session and performance metadata.
-
-**Implementation:**
-```typescript
-await workerService.saveToolExecution({
- ...existingData,
- userIntent,
- assistantReasoning: thinking,
- conversationContext: recentTurns,
- metadata: { // NEW
- cwd: entry.cwd,
- timestamp: entry.timestamp,
- sessionId: entry.sessionId,
- tokenUsage: entry.message.usage,
- },
-});
-```
-
-**Impact:** Better analytics and debugging.
-
----
-
-## Example: Before and After
-
-### Current Observation (Tool Data Only)
-
-```json
-{
- "type": "feature",
- "title": "Updated login.ts",
- "narrative": "Modified authentication logic in src/auth/login.ts",
- "files": ["src/auth/login.ts"],
- "concepts": ["authentication"],
- "facts": []
-}
-```
-
-### Enhanced Observation (With Transcript Context)
-
-```json
-{
- "type": "bugfix",
- "title": "Fixed authentication bug causing random logouts",
- "narrative": "Users were experiencing random logouts due to JWT token expiration. Updated token expiration from 1h to 24h in token validation logic. Modified src/auth/login.ts to use longer-lived tokens and improved error handling for expired tokens.",
- "files": ["src/auth/login.ts"],
- "concepts": ["authentication", "jwt", "token-management", "bugfix"],
- "facts": [
- "JWT token expiration was too short (1h)",
- "Updated expiration to 24h",
- "Added error handling for expired tokens"
- ]
-}
-```
-
-**Improvement:**
-- Clear problem statement
-- Explicit solution
-- Specific technical details
-- Better concept tagging
-- Actionable facts
-
----
-
-## Technical Considerations
-
-### 1. Performance
-
-**Concern:** Parsing entire transcript on every tool execution.
-
-**Solution:**
-- TranscriptParser already loads full file (unavoidable)
-- Use caching for transcript parsing within same session
-- Only parse once per session, reuse parsed entries
-
-**Benchmark:**
-- Current: ~10ms to parse 408-line transcript
-- Impact: Negligible (save-hook already reads transcript)
-
-### 2. Token Usage
-
-**Concern:** Sending more context to worker increases tokens.
-
-**Solution:**
-- Thinking blocks: Limit to first 500 chars
-- Conversation context: Only last 2-3 turns
-- Tool results: Truncate large outputs to 500 chars
-- User intent: Full text (usually short)
-
-**Estimate:**
-- Current: ~200 tokens per observation generation
-- Enhanced: ~500 tokens per observation generation
-- Increase: ~150%
-- Cost: Still < $0.001 per observation with Haiku
-
-### 3. Implementation Complexity
-
-**Concern:** Matching tool executions to transcript entries.
-
-**Solution:**
-- Tool use IDs are in both places
-- Timestamps provide ordering
-- UUID chains provide parent-child relationships
-
-**Example Matching:**
-```typescript
-function findToolContext(parser: TranscriptParser, toolUseId: string) {
- // 1. Find assistant entry with tool_use block
- const assistantEntry = parser.getAssistantEntries()
- .find(entry =>
- entry.message.content.some(c =>
- c.type === 'tool_use' && c.id === toolUseId
- )
- );
-
- // 2. Find next user entry with tool_result
- const userEntry = parser.getUserEntries()
- .find(entry =>
- entry.message.content.some(c =>
- c.type === 'tool_result' && c.tool_use_id === toolUseId
- )
- );
-
- return { assistantEntry, userEntry };
-}
-```
-
----
-
-## Next Steps
-
-1. **Validate Approach**
- - Review this analysis with project team
- - Confirm data availability in all transcript scenarios
- - Identify any privacy concerns
-
-2. **Implement Phase 1**
- - Update save-hook.ts to extract user intent
- - Modify worker service to accept new fields
- - Update observation prompt to use user intent
-
-3. **Test and Measure**
- - Compare observation quality before/after
- - Measure token usage increase
- - Validate performance impact
-
-4. **Iterate**
- - Roll out Phase 2 (assistant reasoning)
- - Roll out Phase 3 (conversation context)
- - Monitor improvements at each phase
-
----
-
-## Appendix: Data Samples
-
-### Complete Markdown Representation
-
-See `/Users/alexnewman/Scripts/claude-mem/docs/context/transcript-complete-readable.md` for a full 1:1 markdown representation of the first 10 conversation turns from the sample transcript, including:
-- Complete user messages
-- Full assistant responses
-- Thinking blocks (truncated to 2000 chars)
-- Tool uses with complete input JSON
-- Tool results with actual output data (truncated to 500 chars)
-- Token usage stats
-- All metadata (timestamps, UUIDs, session IDs, CWD)
-
-### Sample Tool Result Structure
-
-```typescript
-// User entry containing tool result
-{
- "type": "user",
- "message": {
- "content": [
- {
- "type": "tool_result",
- "tool_use_id": "toolu_01T477WUra1sDR6gHaqZHhKT",
- "content": [
- {
- "type": "text",
- "text": "{\n \"thoughtNumber\": 1,\n \"totalThoughts\": 8,\n \"nextThoughtNeeded\": true,\n \"branches\": [],\n \"thoughtHistoryLength\": 1\n}"
- }
- ]
- }
- ]
- }
-}
-```
-
----
-
-## Conclusion
-
-The Claude Code transcript files contain a wealth of contextual data that is currently unused by the memory worker. By extracting:
-
-1. User intent (the "what" and "why")
-2. Assistant reasoning (the "how" and "because")
-3. Tool results (the "outcome")
-4. Conversation context (the "flow")
-5. Session metadata (the "when" and "where")
-
-We can generate significantly richer, more useful observations that better capture the intent, decisions, and outcomes of each coding session.
-
-**The data is already there - we just need to read it.**
diff --git a/docs/context/transcript-data-discovery.md b/docs/context/transcript-data-discovery.md
deleted file mode 100644
index 33389d8c..00000000
--- a/docs/context/transcript-data-discovery.md
+++ /dev/null
@@ -1,234 +0,0 @@
-# Claude Code Transcript Data Discovery
-
-## Executive Summary
-
-This document details findings from implementing a validated transcript parser for Claude Code JSONL transcripts. The parser enables extraction of rich contextual data that can optimize prompt generation and track token usage for ROI metrics.
-
-## Transcript Structure
-
-### File Location
-```
-~/.claude/projects//.jsonl
-```
-
-Example:
-```
-~/.claude/projects/-Users-alexnewman-Scripts-claude-mem/2933cff9-f0a7-4f0b-8296-0a030e7658a6.jsonl
-```
-
-### Entry Types
-
-Discovered 5 transcript entry types:
-
-1. **`file-history-snapshot`** (NEW - not in Python model)
- - Purpose: Track file state snapshots
- - Frequency: ~10 entries per session
-
-2. **`user`** - User messages and tool results
- - Contains actual user text messages OR tool result data
- - Can have string content or array of ContentItems
-
-3. **`assistant`** - Assistant responses and tool uses
- - Contains text responses, tool uses, and thinking blocks
- - **Critical**: Contains usage data with token counts
-
-4. **`summary`** (not yet observed in test data)
- - Session summaries
-
-5. **`system`** (not yet observed in test data)
- - System messages/warnings
-
-6. **`queue-operation`** (not yet observed in test data)
- - Queue tracking for message flow
-
-## Key Findings
-
-### 1. Message Extraction Complexity
-
-**Problem**: Naively getting the "last" entry doesn't work because:
-- Last user entry might be a tool result, not a text message
-- Last assistant entry might only contain tool uses, no text
-
-**Solution**: Iterate backward through entries to find the last entry with actual text content.
-
-### 2. Tool Use Tracking
-
-**Discovery**: Tool uses are in **assistant** messages, not user messages.
-
-**Data Available**:
-```typescript
-{
- name: string; // Tool name (e.g., "Bash", "Read", "TodoWrite")
- timestamp: string; // When the tool was used
- input: any; // Full tool input parameters
-}
-```
-
-**Test Session Results** (168 entries):
-- 42 tool uses across 7 different tool types
-- Most used: Bash (24x), TodoWrite (5x), Edit (4x)
-
-### 3. Token Usage Data (ROI Foundation)
-
-**Critical Discovery**: Every assistant message contains complete token usage data:
-
-```typescript
-interface UsageInfo {
- input_tokens?: number; // Total input tokens (includes context)
- cache_creation_input_tokens?: number; // Tokens used to create cache
- cache_read_input_tokens?: number; // Cached tokens read (discounted cost)
- output_tokens?: number; // Model output tokens
-}
-```
-
-**Test Session Token Analysis**:
-```
-Input tokens: 858
-Output tokens: 44,165
-Cache creation tokens: 469,650
-Cache read tokens: 5,294,101 ← 5.29M tokens saved by caching!
-Total tokens: 45,023
-```
-
-**ROI Implication**: This validates our ROI implementation plan. We can track:
-- Discovery cost = sum of all input + output tokens across session
-- Context savings = cache_read_input_tokens (tokens NOT paid for in full)
-- ROI = Discovery cost / Context savings
-
-### 4. Parse Reliability
-
-**Result**: 0.00% parse failure rate on production transcript with 168 entries.
-
-**Conclusion**: The JSONL format is stable and well-formed. No need for extensive error handling.
-
-## Implementation Files
-
-### Created Files
-
-1. **`src/types/transcript.ts`** - TypeScript types matching Python Pydantic model
- - All entry types, content types, usage info
- - Drop-in compatible with Python model structure
-
-2. **`src/utils/transcript-parser.ts`** - Robust transcript parsing class
- - Handles all entry types
- - Smart message extraction (finds last text message, not just last entry)
- - Tool use history extraction
- - Token usage aggregation
- - Parse statistics and error tracking
-
-3. **`scripts/test-transcript-parser.ts`** - Validation script
- - Tests all extraction methods
- - Reports parse statistics
- - Shows token usage breakdown
- - Lists tool use history
-
-### Usage Example
-
-```typescript
-import { TranscriptParser } from '../src/utils/transcript-parser.js';
-
-const parser = new TranscriptParser('/path/to/transcript.jsonl');
-
-// Extract messages
-const lastUserMsg = parser.getLastUserMessage();
-const lastAssistantMsg = parser.getLastAssistantMessage();
-
-// Get tool history
-const tools = parser.getToolUseHistory();
-// => [{name: 'Bash', timestamp: '...', input: {...}}, ...]
-
-// Get token usage
-const tokens = parser.getTotalTokenUsage();
-// => {inputTokens: 858, outputTokens: 44165, cacheReadTokens: 5294101, ...}
-
-// Parse statistics
-const stats = parser.getParseStats();
-// => {totalLines: 168, parsedEntries: 168, failedLines: 0, ...}
-```
-
-## Next Steps for PR Review
-
-### Addressing "Drops Unknown Lines" Concern
-
-**Original Issue**: Summary hook silently skipped malformed lines without visibility.
-
-**Root Cause**: We didn't understand the full transcript model. The "skip malformed lines" was a band-aid.
-
-**Solution**: Replace ad-hoc parsing in `summary-hook.ts` with validated `TranscriptParser` class:
-
-**Before** (summary-hook.ts:38-117):
-```typescript
-// Manually parsing with try/catch, no type safety
-for (let i = lines.length - 1; i >= 0; i--) {
- try {
- const line = JSON.parse(lines[i]);
- if (line.type === 'user' && line.message?.content) {
- // ... extraction logic
- }
- } catch (parseError) {
- // Skip malformed lines ← BLACK HOLE
- continue;
- }
-}
-```
-
-**After** (using TranscriptParser):
-```typescript
-import { TranscriptParser } from '../utils/transcript-parser.js';
-
-const parser = new TranscriptParser(transcriptPath);
-const lastUserMessage = parser.getLastUserMessage();
-const lastAssistantMessage = parser.getLastAssistantMessage();
-
-// Parse errors are tracked in parser.getParseErrors()
-```
-
-**Benefits**:
-1. ✅ Type-safe extraction based on validated model
-2. ✅ No silent failures - parse errors are tracked
-3. ✅ Smart extraction (finds last TEXT message, not last entry)
-4. ✅ Reusable across all hooks and scripts
-5. ✅ Enables token usage tracking (ROI metrics)
-6. ✅ Enables tool use tracking (prompt optimization)
-
-## Prompt Optimization Opportunities
-
-With rich transcript data available, we can enhance prompts with:
-
-### 1. Tool Use Patterns
-- "In this session you've used: Bash (24x), TodoWrite (5x), Edit (4x)"
-- Helps Claude understand what kind of work is being done
-
-### 2. Token Economics Awareness
-- "Cache read tokens: 5.29M (context savings)"
-- Reinforces value of memory system
-
-### 3. Session Flow Understanding
-- Number of user/assistant exchanges
-- Tools used per exchange
-- Session complexity metrics
-
-### 4. File History Snapshots
-- Track which files were modified during session
-- Provide file change context to summaries
-
-## Testing
-
-Run the validation script:
-```bash
-# Find your current session transcript
-ls -lt ~/.claude/projects/-Users-alexnewman-Scripts-claude-mem/*.jsonl | head -1
-
-# Test the parser
-npx tsx scripts/test-transcript-parser.ts
-```
-
-## Conclusion
-
-The transcript parser implementation:
-1. ✅ Addresses PR review concern about dropped lines
-2. ✅ Validates the ROI metrics implementation plan
-3. ✅ Enables prompt optimization with rich context
-4. ✅ Provides foundation for future enhancements
-
-**Recommendation**: Replace ad-hoc transcript parsing in hooks with `TranscriptParser` class for improved reliability and feature richness.
diff --git a/docs/context/transcript-dump.md b/docs/context/transcript-dump.md
deleted file mode 100644
index d21ee514..00000000
--- a/docs/context/transcript-dump.md
+++ /dev/null
@@ -1,22 +0,0 @@
-# Transcript Dump
-
-Total entries: 384
-
----
-
-## Entry 151: USER
-**Timestamp:** 2025-11-13T17:09:47.231Z
-
-**Content:**
-```
-I have a question regarding the context timeline that we made. and it's a bit of a thinker...
-
-when i first started out working on claude-mem my idea was using a fast index of memories with links to access them on demand. I thought that claude-mem would be searching for all the things it needed to get the job done, but what I found is that the description of the work done + the files touched actually improved performance tremendously, and without actively searching for things constantly.
-
-I just had a thought that maybe claude isn't searching, because when it reads what it did, it already "knows" how it did that, for the most part. Would you say I'm on the right track? I want you to ultrathink about WHY claude-mem's session start context works so well, from your OWN perspective
-```
-
----
-
-
-_Remaining 364 entries omitted for brevity_
diff --git a/docs/context/transcript-formatted-example.md b/docs/context/transcript-formatted-example.md
deleted file mode 100644
index 3d8a3a4e..00000000
--- a/docs/context/transcript-formatted-example.md
+++ /dev/null
@@ -1,106 +0,0 @@
-# Transcript Context Analysis
-
-**File:** 57dcc12f-4751-46bb-82b4-2aa96a3e226d.jsonl
-**Parsed:** 11/13/2025, 12:34:19 PM
-
-## Statistics
-
-- Total entries: 281
-- Successfully parsed: 281
-- Failed lines: 0
-- Conversation turns: 43
-
-## Token Usage
-
-- Input tokens: 6,805
-- Output tokens: 38,209
-- Cache creation: 224,078
-- Cache read: 3,320,726
-- Total: 45,014
-
----
-
-# Conversation Turns
-
-## Turn 1
-
----
-
-## Turn 2
-
----
-
-## Turn 3
-
----
-
-## Turn 4
-
----
-
-## Turn 5
-
----
-
-## Turn 6
-
----
-
-## Turn 7
-
----
-
-## Turn 8
-
----
-
-## Turn 9
-
----
-
-## Turn 10
-
----
-
-## Turn 11
-
----
-
-## Turn 12
-
----
-
-## Turn 13
-
----
-
-## Turn 14
-
----
-
-## Turn 15
-
----
-
-## Turn 16
-
----
-
-## Turn 17
-
----
-
-## Turn 18
-
----
-
-## Turn 19
-
----
-
-## Turn 20
-
----
-
-
-_... 23 more turns omitted for brevity_
diff --git a/docs/context/typescript-errors.md b/docs/context/typescript-errors.md
deleted file mode 100644
index 804c632c..00000000
--- a/docs/context/typescript-errors.md
+++ /dev/null
@@ -1,180 +0,0 @@
-# TypeScript Errors to Fix
-
-Generated: 2025-11-06
-
-## Summary
-
-Total files with errors: 20
-Total error count: 160+
-
-## Errors by File
-
-### 1. src/sdk/parser.ts (5 errors)
-**Lines 149-153**: Type 'string | null' is not assignable to type 'string'
-- `request` - line 149
-- `investigated` - line 150
-- `learned` - line 151
-- `completed` - line 152
-- `next_steps` - line 153
-
-**Fix**: Update return type to allow null values or provide default values
-
----
-
-### 2. src/hooks/index.ts (4 errors)
-**Lines 0-3**: Cannot find module errors
-- `'./context.js'` - line 0
-- `'./save.js'` - line 1
-- `'./new.js'` - line 2
-- `'./summary.js'` - line 3
-
-**Fix**: Update imports to use correct paths without .js extension
-
----
-
-### 3. src/sdk/index.ts (1 error)
-**Line 4**: `'./prompts.js'` has no exported member named 'buildFinalizePrompt'
-
-**Fix**: Remove unused import or implement the missing function
-
----
-
-### 4. src/services/sync/ChromaSync.ts (26 errors)
-**Multiple lines**: Argument of type '"CHROMA_SYNC"' is not assignable to parameter of type 'Component'
-- Lines: 91, 114, 116, 141, 144, 155, 157, 324, 329, 370, 409, 463, 493, 535, 541, 546, 562, 589, 607, 630, 648, 679, 697, 703, 718, 733
-
-**Line 508**: `'result.content'` is of type 'unknown'
-
-**Fix**: Add 'CHROMA_SYNC' to Component type union or update logger calls
-
----
-
-### 5. src/shared/config.ts (1 error)
-**Line 11**: Cannot find name '__DEFAULT_PACKAGE_VERSION__'
-
-**Fix**: This should be injected during build, check build configuration
-
----
-
-### 6. src/shared/storage.ts (25 errors)
-**Lines 1-5**: Module has no exported member errors
-- `'createStores'` - line 1
-- `'MemoryStore'` - line 3
-- `'OverviewStore'` - line 4
-- `'DiagnosticsStore'` - line 5
-
-**Lines 87-162**: Various property errors (legacy interface usage)
-- Property 'create' does not exist - line 87
-- Property 'getBySessionId' does not exist - line 92
-- Property 'has' does not exist - line 97
-- Property 'getAllSessionIds' does not exist - line 102
-- Property 'getRecent' does not exist - line 107
-- Property 'getRecentForProject' does not exist - line 112
-- Multiple 'stores' is possibly 'undefined' errors
-
-**Fix**: Remove legacy code or update to use current SessionStore interface
-
----
-
-### 7. src/servers/search-server.ts (8 errors)
-**Line 58**: `'result.content'` is of type 'unknown'
-**Lines 150, 230, 309**: 'index' is declared but its value is never read
-**Lines 371, 466, 1032, 1405**: 'id' is declared but its value is never read
-
-**Fix**: Add proper type assertions and remove unused variables
-
----
-
-### 8. src/services/sqlite/Database.ts (1 error)
-**Line 0**: Cannot find module 'bun:sqlite'
-
-**Fix**: This is legacy code using Bun's SQLite, should not be imported
-
----
-
-### 9. src/services/sqlite/migrations.ts (2 errors)
-**Line 0**: Cannot find module 'bun:sqlite'
-**Line 153**: 'db' is declared but its value is never read
-
-**Fix**: Update imports to use better-sqlite3 instead
-
----
-
-### 10. tests/session-search.test.ts (1 error)
-**Line 173**: Type 'null' is not assignable to type 'SessionSearch'
-
-**Fix**: Update test to handle nullable type properly
-
----
-
-### 11. React/Viewer UI Files (100+ errors)
-
-#### All .tsx files: Cannot use JSX unless '--jsx' flag is provided
-This affects all viewer components but is expected - these are built with esbuild which handles JSX.
-
-#### src/ui/viewer/hooks/usePagination.ts (2 errors)
-**Lines 66, 70**: `'data'` is of type 'unknown'
-
-#### src/ui/viewer/hooks/useSettings.ts (5 errors)
-**Lines 17-19**: `'data'` is of type 'unknown'
-**Lines 40, 45**: `'result'` is of type 'unknown'
-
-#### src/ui/viewer/hooks/useSSE.ts (2 errors)
-**Line 19**: `'data'` is of type 'unknown'
-**Line 71**: Type mismatch in setObservations
-
-#### src/ui/viewer/hooks/useStats.ts (1 error)
-**Line 13**: Argument of type 'unknown' not assignable to SetStateAction
-
-#### src/ui/viewer/hooks/useTheme.ts (8 errors)
-**Multiple lines**: DOM-related type errors
-- Cannot find name 'window' - lines 8, 9, 48
-- Cannot find name 'localStorage' - lines 14, 61
-- Cannot find name 'document' - lines 41, 52
-- Cannot find name 'MediaQueryListEvent' - line 49
-
-**Fix**: Add DOM lib to tsconfig for viewer files or add type assertions
-
-#### src/ui/viewer/index.tsx (2 errors)
-**Line 5**: Cannot find name 'document'
-**Multiple**: JSX errors (expected, built with esbuild)
-
-#### src/ui/viewer/App.tsx (3 errors)
-**Lines 63, 66, 69**: Type mismatch errors in setState callbacks
-
-#### src/ui/viewer/components/Header.tsx (6 errors)
-**Lines 46, 47, 66, 67, 85, 86**: Property 'style' does not exist on EventTarget & HTMLAnchorElement
-**Line 94**: Property 'value' does not exist on EventTarget & HTMLSelectElement
-
-#### src/ui/viewer/components/Feed.tsx (2 errors)
-**Line 30**: Cannot find name 'IntersectionObserver'
-**Line 31**: Parameter 'entries' implicitly has 'any' type
-
-#### src/ui/viewer/components/Sidebar.tsx (3 errors)
-**Lines 81, 99, 113**: Property 'value' does not exist on EventTarget
-
----
-
-## Priority Fix Order
-
-1. **High Priority - Breaks build:**
- - src/shared/config.ts (__DEFAULT_PACKAGE_VERSION__)
- - src/hooks/index.ts (module import errors)
- - src/sdk/index.ts (buildFinalizePrompt export)
- - src/shared/storage.ts (legacy interface usage)
-
-2. **Medium Priority - Type safety:**
- - src/sdk/parser.ts (null handling)
- - src/services/sync/ChromaSync.ts (logger Component type)
- - src/servers/search-server.ts (unknown types)
- - React hooks (unknown types)
-
-3. **Low Priority - Cosmetic:**
- - Unused variable warnings
- - JSX errors (these are expected, esbuild handles them)
- - DOM type errors in viewer (handled by esbuild)
-
-4. **Legacy/Cleanup:**
- - src/services/sqlite/Database.ts (remove bun:sqlite)
- - src/services/sqlite/migrations.ts (update to better-sqlite3)
- - src/shared/storage.ts (remove entire file if legacy)
diff --git a/docs/context/vscode-api-lm.md b/docs/context/vscode-api-lm.md
deleted file mode 100644
index a2c33ca0..00000000
--- a/docs/context/vscode-api-lm.md
+++ /dev/null
@@ -1,67 +0,0 @@
-# VS Code API – Language Model (`lm`) Namespace
-
-Excerpt captured from `https://code.visualstudio.com/api/references/vscode-api#lm` using markitdown MCP on 2025-11-12.
-
-## Overview
-
-The `vscode.lm` namespace exposes APIs for interacting with language models inside Visual Studio Code. It allows extensions to register tools, select chat models, invoke tools, and surface MCP servers so that agent mode can compose complex responses.
-
-### Available Tools
-
-- `vscode.lm.tools`: Readonly array of [`LanguageModelToolInformation`](https://code.visualstudio.com/api/references/vscode-api#LanguageModelToolInformation).
- - Lists all tools registered via `vscode.lm.registerTool`.
- - Tools can be invoked programmatically with `vscode.lm.invokeTool` when their inputs satisfy the declared schema.
-
-### Events
-
-- `vscode.lm.onDidChangeChatModels`: Fires when the set of available chat models changes. Extensions should re-query models after this event.
-
-### Functions
-
-#### `vscode.lm.invokeTool(name, options, token?)`
-
-Invokes a tool by name with a given input payload.
-
-- Validates input against the schema declared by the tool.
-- When called from a chat participant, pass the `toolInvocationToken` so the chat UI associates results with the correct conversation.
-- Returns a [`LanguageModelToolResult`](https://code.visualstudio.com/api/references/vscode-api#LanguageModelToolResult) composed of text and optional prompt-tsx parts.
-- Tool results can be preserved across turns by storing them in `ChatResult.metadata` and retrieving them later from `ChatResponseTurn.result`.
-
-#### `vscode.lm.registerLanguageModelChatProvider(vendor, provider)`
-
-Registers a [`LanguageModelChatProvider`](https://code.visualstudio.com/api/references/vscode-api#LanguageModelChatProvider).
-
-- Requires a matching `languageModelChatProviders` contribution in `package.json`.
-- `vendor` must be globally unique (for example `copilot` or `openai`).
-- Returns a `Disposable` to unregister the provider.
-
-#### `vscode.lm.registerMcpServerDefinitionProvider(id, provider)`
-
-Publishes Model Context Protocol servers for the editor.
-
-- Requires a `contributes.mcpServerDefinitionProviders` entry in `package.json`.
-- Enables dynamic discovery of MCP servers and tools when users submit chat messages.
-- Returns a `Disposable` that unregisters the provider.
-
-#### `vscode.lm.registerTool(name, tool)`
-
-Registers a [`LanguageModelTool`](https://code.visualstudio.com/api/references/vscode-api#LanguageModelTool) implementation with the runtime.
-
-- Tool must also appear in `package.json -> contributes.languageModelTools`.
-- Registered tools appear in `vscode.lm.tools` and can be used by any extension.
-
-#### `vscode.lm.selectChatModels(selector?)`
-
-Returns an array of [`LanguageModelChat`](https://code.visualstudio.com/api/references/vscode-api#LanguageModelChat) instances matching a selector.
-
-- Selector can be broad (by vendor or family) or narrow (by exact model ID).
-- Handle scenarios where zero models are available.
-- Persisted model references should be refreshed when `onDidChangeChatModels` fires.
-
-## Usage Notes
-
-- Extensions should gracefully handle missing models or tools.
-- Tool invocation responses can include multiple parts; integrate them using prompt-tsx or by constructing `LanguageModelToolResultPart` objects.
-- When providing MCP servers or tools, ensure proper contribution points exist in `package.json`.
-
-_Fetched on 2025-11-12 via markitdown MCP._
diff --git a/docs/context/vscode-copilot-extension-plan.md b/docs/context/vscode-copilot-extension-plan.md
deleted file mode 100644
index 65f76544..00000000
--- a/docs/context/vscode-copilot-extension-plan.md
+++ /dev/null
@@ -1,49 +0,0 @@
-# VS Code Copilot Extension Integration Plan
-
-## 1. Groundwork
-- Audit existing claude-mem hook scripts (`context-hook`, `user-message-hook`, `new-hook`, `save-hook`, `summary-hook`, `cleanup-hook`) and their worker-service payloads.
-- Document REST endpoints, request bodies, and SessionStore schema fields used today so the extension mirrors them exactly.
-- Confirm worker service availability workflow (`ensureWorkerRunning`, port resolution) and decide how extension error reporting will surface issues to Copilot chat users.
-
-## 2. Project Scaffold
-- Clone the VS Code `chat-sample` starter, convert to a TypeScript-only extension, and align lint/tsconfig with repo standards.
-- Add build pipeline (esbuild or webpack) plus npm scripts that match the existing `scripts/build-hooks.js` release flow.
-- Wire extension activation events for chat participation and ensure packaging metadata (publisher, categories) is in place.
-
-## 3. Shared Worker Client
-- Extract reusable worker-service client utilities from `plugin/scripts/*.js` (port discovery, session init, observation uploads).
-- Publish TypeScript definitions by re-exporting from `src/services/worker-types.ts` to keep contracts synchronized.
-- Centralize HTTP calls (timeouts, retries, logging) so every tool implementation uses the same helper layer.
-
-## 4. Language Model Tool Contracts
-- Add `contributes.languageModelTools` entries in `package.json` for lifecycle parity:
- - `mem_session_init`, `mem_user_prompt_log`, `mem_observation_record`, `mem_summary_finalize`, `mem_session_cleanup`.
-- Provide detailed JSON schemas mirroring hook input structures (session IDs, cwd, prompt text, tool payload metadata).
-- Supply descriptive `modelDescription`, `userDescription`, icons, tags, and enable `canBeReferencedInPrompt` where appropriate.
-
-## 5. Tool Implementations
-- Register each tool via `vscode.lm.registerTool` inside `activate`.
-- Implement `prepareInvocation` to show user confirmations (especially for cleanup/stop actions) and tailor messages to match existing CLI prompts.
-- In `invoke`, call the shared worker client, translate successes into `LanguageModelToolResult` text parts, and craft error messages that guide the LLM toward recovery (retry, alternate parameters).
-- Ensure telemetry/logging records tool usage for debugging without leaking sensitive data.
-
-## 6. Chat Orchestration
-- Implement a chat participant based on the sample that maps Copilot threads to claude-mem session IDs stored in turn metadata.
-- On conversation start, auto-run `mem_session_init`; before each user prompt, dispatch `mem_user_prompt_log`; when Copilot signals stop, run `mem_summary_finalize` (with fallbacks if the worker is unavailable).
-- Capture tool events emitted by Copilot (file edits, terminal runs) and forward them through `mem_observation_record` with matching payload structure.
-- Handle conversation disposal or model changes by calling `mem_session_cleanup` to mirror `SessionEnd` hooks.
-
-## 7. Settings and UX
-- Read `.claude-mem/settings.json` overrides (worker port, observation depth) and surface VS Code settings for Copilot-specific toggles (auto-sync enabled, max observations per prompt).
-- Add status bar indicator/commands for worker health, quick restart instructions, and opening the viewer UI (`http://localhost:37777`).
-- Provide inline notifications when the worker is unreachable, including guidance to restart via PM2.
-
-## 8. Testing and QA
-- Draft manual validation checklist: initial session, prompt logging, observation capture, summary completion, worker-down handling.
-- Add integration tests using `@vscode/test-electron` to simulate chat turns and assert database side effects in a temporary claude-mem data directory.
-- Build mocks for worker endpoints to enable unit tests of tool invocation logic without hitting the real service.
-
-## 9. Release Readiness
-- Document installation and usage in `README.md`, including architecture diagrams showing Copilot → tool → worker flow.
-- Update CHANGELOG and marketing copy to announce Copilot support and list prerequisites (worker running, settings file placement).
-- Prepare Marketplace assets (icon, gallery text) and extend existing publish scripts to package and ship the new extension.
diff --git a/docs/context/vscode-extension-chat-sample.md b/docs/context/vscode-extension-chat-sample.md
deleted file mode 100644
index 0f3f1517..00000000
--- a/docs/context/vscode-extension-chat-sample.md
+++ /dev/null
@@ -1,8 +0,0 @@
-# VS Code Extension Chat Sample Repository (GitHub Snapshot)
-
-The markitdown MCP fetch for `https://github.com/microsoft/vscode-extension-samples/tree/main/chat-sample` returned only the public navigation scaffolding for GitHub. No repository-specific content or README data was captured because the site requires client-side execution that the fetcher cannot perform.
-
-You can browse the repository directly for full details:
-- https://github.com/microsoft/vscode-extension-samples/tree/main/chat-sample
-
-_Fetched on 2025-11-12 via markitdown MCP. Content retrieval was limited to GitHub's static navigation shell._
diff --git a/docs/context/vscode-language-model-tool-api.md b/docs/context/vscode-language-model-tool-api.md
deleted file mode 100644
index dcaa114c..00000000
--- a/docs/context/vscode-language-model-tool-api.md
+++ /dev/null
@@ -1,267 +0,0 @@
-# Language Model Tool API
-
-Language model tools enable you to extend the functionality of a large language model (LLM) in chat with domain-specific capabilities. To process a user's chat prompt, [agent mode](/docs/copilot/chat/chat-agent-mode) in VS Code can automatically invoke these tools to perform specialized tasks as part of the conversation.
-
-By contributing a language model tool in your VS Code extension, you can extend the agentic coding workflow while also providing deep integration with the editor. Extension tools are one of three types of tools available in VS Code, alongside [built-in tools and MCP tools](/docs/copilot/chat/chat-tools.md#types-of-tools).
-
-In this extension guide, you learn how to create a language model tool by using the Language Model Tools API and how to implement tool calling in a chat extension.
-
-You can also extend the chat experience with specialized tools by contributing an [MCP server](/api/extension-guides/ai/mcp). See the [AI Extensibility Overview](/api/extension-guides/ai/ai-extensibility-overview) for details on the different options and how to decide which approach to use.
-
-> **Tip**
-> For information about using tools as an end user, see [Use tools in chat](/docs/copilot/chat/chat-tools.md).
-
-## What is tool calling in an LLM?
-
-A language model tool is a function that can be invoked as part of a language model request. For example, you might have a function that retrieves information from a database, performs some calculation, or calls an online API. When you contribute a tool in a VS Code extension, agent mode can then invoke the tool based on the context of the conversation.
-
-The LLM never actually executes the tool itself, instead the LLM generates the parameters that are used to call your tool. It's important to clearly describe the tool's purpose, functionality, and input parameters so that the tool can be invoked in the right context.
-
-The following diagram shows the tool-calling flow in agent mode in VS Code. See [Tool-calling flow](#tool-calling-flow) for details about the specific steps involved.
-
-
-
-Read more about [function calling](https://platform.openai.com/docs/guides/function-calling) in the OpenAI documentation.
-
-## Why implement a language model tool in your extension?
-
-Implementing a language model tool in your extension has several benefits:
-
-- **Extend agent mode** with specialized, domain-specific tools that are automatically invoked as part of responding to a user prompt. For example, enable database scaffolding and querying to dynamically provide the LLM with relevant context.
-- **Deeply integrate with VS Code** by using the broad set of extension APIs. For example, use the [debug APIs](/api/extension-guides/debugger-extension) to get the current debugging context and use it as part of the tool's functionality.
-- **Distribute and deploy** tools via the Visual Studio Marketplace, providing a reliable and seamless experience for users. Users don't need a separate installation and update process for your tool.
-
-You might consider implementing a language model tool with an [MCP server](/api/extension-guides/ai/mcp) in the following scenarios:
-
-- You already have an MCP server implementation and also want to use it in VS Code.
-- You want to reuse the same tool across different development environments and platforms.
-- Your tool is hosted remotely as a service.
-- You don't need access to VS Code APIs.
-
-Learn more about the [differences between tool types](/docs/copilot/chat/chat-tools.md#types-of-tools).
-
-## Create a language model tool
-
-Implementing a language model tool consists of two main parts:
-
-1. Define the tool's configuration in the `package.json` file of your extension.
-2. Implement the tool in your extension code by using the [Language Model API reference](/api/references/vscode-api#lm)
-
-You can get started with a [basic example project](https://github.com/microsoft/vscode-extension-samples/tree/main/chat-sample).
-
-### 1. Static configuration in `package.json`
-
-The first step to define a language model tool in your extension is to define it in the `package.json` file of your extension. This configuration includes the tool name, description, input schema, and other metadata:
-
-1. Add an entry for your tool in the `contributes.languageModelTools` section of your extension's `package.json` file.
-2. Give the tool a unique name:
-
- | Property | Description |
- | --- | --- |
- | `name` | The unique name of the tool, used to reference the tool in the extension implementation code. Format the name in the format `{verb}_{noun}`. See [naming guidelines](#guidelines-and-conventions). |
- | `displayName` | The user-friendly name of the tool, used for displaying in the UI. |
-
-3. If the tool can be used in [agent mode](/docs/copilot/chat/chat-agent-mode) or referenced in a chat prompt with `#`, add the following properties:
-
- Users can enable or disable the tool in the Chat view, similar to how this is done for [Model Context Protocol (MCP) tools](/docs/copilot/chat/chat-tools.md#mcp-tools).
-
- | Property | Description |
- | --- | --- |
- | `canBeReferencedInPrompt` | Set to `true` if the tool can be used in [agent mode](/docs/copilot/chat/chat-agent-mode) or referenced in chat. |
- | `toolReferenceName` | The name for users to reference the tool in a chat prompt via `#`. |
- | `icon` | The icon to display for the tool in the UI. |
- | `userDescription` | User-friendly description of the tool, used for displaying in the UI. |
-
-4. Add a detailed description in `modelDescription`. This information is used by the LLM to determine in which context your tool should be used.
-
- - What exactly does the tool do?
- - What kind of information does it return?
- - When should and shouldn't it be used?
- - Describe important limitations or constraints of the tool.
-
-5. If the tool takes input parameters, add an `inputSchema` property that describes the tool's input parameters.
-
- This JSON schema describes an object with the properties that the tool takes as input, and whether they are required. File paths should be absolute paths.
-
- Describe what each parameter does and how it relates to the tool's functionality.
-
-6. Add a `when` clause to control when the tool is available.
-
- The `languageModelTools` contribution point lets you restrict when a tool is available for agent mode or can be referenced in a prompt by using a [when clause](/api/references/when-clause-contexts). For example, a tool that gets the debug call stack information should only be available when the user is debugging.
-
- ```json
- "contributes": {
- "languageModelTools": [
- {
- "name": "chat-tools-sample_tabCount",
- ...
- "when": "debugState == 'running'"
- }
- ]
- }
- ```
-
-**Example tool definition**
-
-The following example shows how to define a tool that counts the number of active tabs in a tab group.
-
-```json
-"contributes": {
- "languageModelTools": [
- {
- "name": "chat-tools-sample_tabCount",
- "tags": [
- "editors",
- "chat-tools-sample"
- ],
- "toolReferenceName": "tabCount",
- "displayName": "Tab Count",
- "modelDescription": "The number of active tabs in a tab group in VS Code.",
- "userDescription": "Count the number of active tabs in a tab group.",
- "canBeReferencedInPrompt": true,
- "icon": "$(files)",
- "inputSchema": {
- "type": "object",
- "properties": {
- "tabGroup": {
- "type": "number",
- "description": "The index of the tab group to check. This is optional- if not specified, the active tab group will be checked.",
- "default": 0
- }
- }
- }
- }
- ]
-}
-```
-
-### 2. Tool implementation
-
-Implement the language model tool by using the [Language Model API](/api/references/vscode-api#lm). This consists of the following steps:
-
-1. On activation of the extension, register the tool with [`vscode.lm.registerTool`](/api/references/vscode-api#lm.registerTool).
-
- Provide the name of the tool as you specified it in the `name` property in `package.json`.
-
- If you want the tool to be private to your extension, skip the tool registration step.
-
- ```ts
- export function registerChatTools(context: vscode.ExtensionContext) {
- context.subscriptions.push(
- vscode.lm.registerTool('chat-tools-sample_tabCount', new TabCountTool())
- );
- }
- ```
-
-2. Create a class that implements the [`vscode.LanguageModelTool<>`](/api/references/vscode-api#LanguageModelTool%3CT%3E) interface.
-
-3. Add tool confirmation messages in the `prepareInvocation` method.
-
- A generic confirmation dialog will always be shown for tools from extensions, but the tool can customize the confirmation message. Give enough context to the user to understand what the tool is doing. The message can be a `MarkdownString` containing a code block.
-
- The following example shows how to provide a confirmation message for the tab count tool.
-
- ```ts
- async prepareInvocation(
- options: vscode.LanguageModelToolInvocationPrepareOptions,
- _token: vscode.CancellationToken
- ) {
- const confirmationMessages = {
- title: 'Count the number of open tabs',
- message: new vscode.MarkdownString(
- `Count the number of open tabs?` +
- (options.input.tabGroup !== undefined
- ? ` in tab group ${options.input.tabGroup}`
- : '')
- ),
- };
-
- return {
- invocationMessage: 'Counting the number of tabs',
- confirmationMessages,
- };
- }
- ```
-
- If `prepareInvocation` returned `undefined`, the generic confirmation message will be shown. Note that the user can also select to "Always Allow" a certain tool.
-
-4. Define an interface that describes the tool input parameters.
-
- The interface is used in the `invoke` method of the `vscode.LanguageModelTool` class. The input parameters are validated against the JSON schema you defined in the `inputSchema` in `package.json`.
-
- The following example shows the interface for the tab count tool.
-
- ```ts
- export interface ITabCountParameters {
- tabGroup?: number;
- }
- ```
-
-5. Implement the `invoke` method. This method is called when the language model tool is invoked while processing a chat prompt.
-
- The `invoke` method receives the tool input parameters in the `options` parameter. The parameters are validated against the JSON schema defined in `inputSchema` in `package.json`.
-
- When an error occurs, throw an error with a message that makes sense to the LLM. Optionally, provide instructions on what the LLM should do next, such as retrying with different parameters, or performing a different action.
-
- The following example shows the implementation of the tab count tool. The result of the tool is an instance of type `vscode.LanguageModelToolResult`.
-
- ```ts
- async invoke(
- options: vscode.LanguageModelToolInvocationOptions,
- _token: vscode.CancellationToken
- ) {
- const params = options.input;
- if (typeof params.tabGroup === 'number') {
- const group = vscode.window.tabGroups.all[Math.max(params.tabGroup - 1, 0)];
- const nth =
- params.tabGroup === 1
- ? '1st'
- : params.tabGroup === 2
- ? '2nd'
- : params.tabGroup === 3
- ? '3rd'
- : `${params.tabGroup}th`;
- return new vscode.LanguageModelToolResult([new vscode.LanguageModelTextPart(`There are ${group.tabs.length} tabs open in the ${nth} tab group.`)]);
- } else {
- const group = vscode.window.tabGroups.activeTabGroup;
- return new vscode.LanguageModelToolResult([new vscode.LanguageModelTextPart(`There are ${group.tabs.length} tabs open.`)]);
- }
- }
- ```
-
-View the full source code for implementing a [language model tool](https://github.com/microsoft/vscode-extension-samples/blob/main/chat-sample/src/tools.ts) in the VS Code Extension Samples repository.
-
-## Tool-calling flow
-
-When a user sends a chat prompt, the following steps occur:
-
-1. Copilot determines the list of available tools based on the user's configuration.
- The list of tools consists of built-in tools, tools registered by extensions, and tools from [MCP servers](/docs/copilot/chat/mcp-servers). You can contribute to agent mode via extensions or MCP servers (shown in green in the diagram).
-2. Copilot sends the request to the LLM and provides it with the prompt, chat context, and the list of tool definitions to consider.
- The LLM generates a response, which might include one or more requests to invoke a tool.
-3. If needed, Copilot invokes the suggested tool(s) with the parameter values provided by the LLM.
- A tool response might result in more requests for tool invocations.
-4. If there are errors or follow-up tool requests, Copilot iterates over the tool-calling flow until all tool requests are resolved.
-5. Copilot returns the final response to the user, which might include responses from multiple tools.
-
-## Guidelines and conventions
-
-- **Naming**: write clear and descriptive names for tools and parameters.
- - **Tool name**: should be unique, and clearly describe their intent. Structure the tool name in the format `{verb}_{noun}`. For example, `get_weather`, `get_azure_deployment`, or `get_terminal_output`.
- - **Parameter name**: should describe the parameter's purpose. Structure the parameter name in the format `{noun}`. For example, `destination_location`, `ticker`, or `file_name`.
-- **Descriptions**: write detailed descriptions for tools and parameters.
- - Describe what the tool does and when it should and shouldn't be used. For example, "This tool retrieves the weather for a given location."
- - Describe what each parameter does and how it relates to the tool's functionality. For example, "The `destination_location` parameter specifies the location for which to retrieve the weather. It should be a valid location name or coordinates."
- - Describe important limitations or constraints of the tool. For example, "This tool only retrieves weather data for locations in the United States. It might not work for other regions."
-- **User confirmation**: provide a confirmation message for the tool invocation. A generic confirmation dialog will always be shown for tools from extensions, but the tool can customize the confirmation message. Give enough context to the user to understand what the tool is doing.
-- **Error handling**: when an error occurs, throw an error with a message that makes sense to the LLM. Optionally, provide instructions on what the LLM should do next, such as retrying with different parameters, or performing a different action.
-
-Get more best practices for creating tools in the [OpenAI documentation](https://platform.openai.com/docs/guides/function-calling?api-mode=chat#best-practices-for-defining-functions) and [Anthropic documentation](https://docs.anthropic.com/en/docs/build-with-claude/tool-use/overview).
-
-## Related content
-
-- [Language Model API reference](/api/references/vscode-api#lm)
-- [Register an MCP server in a VS Code extension](/api/extension-guides/ai/mcp)
-- [Use MCP tools in agent mode](/docs/copilot/chat/mcp-servers)
-
-_Fetched on 2025-11-12 via markitdown MCP._
diff --git a/docs/context/vscode-language-model-tool.md b/docs/context/vscode-language-model-tool.md
deleted file mode 100644
index 6504b5dc..00000000
--- a/docs/context/vscode-language-model-tool.md
+++ /dev/null
@@ -1,13 +0,0 @@
-VSCode Language Model Tool API
-
-Local snapshots fetched via the markitdown MCP on 2025-11-12:
-
-- `docs/context/vscode-language-model-tool-api.md`
-- `docs/context/vscode-extension-chat-sample.md`
-- `docs/context/vscode-api-lm.md`
-
-Original sources for reference:
-
-- http://code.visualstudio.com/api/extension-guides/ai/tools
-- https://github.com/microsoft/vscode-extension-samples/tree/main/chat-sample
-- https://code.visualstudio.com/api/references/vscode-api#lm
\ No newline at end of file
diff --git a/docs/context/worker-service-architecture.md b/docs/context/worker-service-architecture.md
deleted file mode 100644
index 1bb55d7d..00000000
--- a/docs/context/worker-service-architecture.md
+++ /dev/null
@@ -1,1174 +0,0 @@
-# Worker Service Architecture: Object-Oriented Design
-
-**Date**: 2025-11-06
-**Purpose**: Clean, DRY class structure for worker-service.ts rewrite
-**Target**: ~600-700 lines (down from 1173)
-
----
-
-## Core Principles
-
-1. **Single Responsibility Principle**: Each class does ONE thing
-2. **DRY**: Extract repeated patterns into reusable components
-3. **KISS**: Simple, obvious implementations
-4. **YAGNI**: Only build what's needed now
-5. **Composition over Inheritance**: Use dependency injection
-6. **Fail Fast**: No defensive programming for problems that can't occur
-
----
-
-## Class Hierarchy
-
-```
-WorkerService (orchestration, HTTP routing)
-├─ DatabaseManager (single long-lived connection)
-├─ SessionManager (session lifecycle, event-driven queue)
-├─ SSEBroadcaster (SSE client management)
-├─ SDKAgent (SDK query loop handling)
-├─ PaginationHelper (DRY utility for paginated queries)
-└─ SettingsManager (DRY utility for settings CRUD)
-```
-
----
-
-## 1. WorkerService (Orchestration)
-
-### Responsibility
-HTTP server setup, route handlers, dependency orchestration. NO business logic.
-
-### Public Interface
-```typescript
-class WorkerService {
- constructor();
- async start(): Promise;
- async shutdown(): Promise;
-}
-```
-
-### Dependencies
-```typescript
-class WorkerService {
- private app: express.Application;
- private server: http.Server | null = null;
-
- // Composed services
- private dbManager: DatabaseManager;
- private sessionManager: SessionManager;
- private sseBroadcaster: SSEBroadcaster;
- private sdkAgent: SDKAgent;
- private paginationHelper: PaginationHelper;
- private settingsManager: SettingsManager;
-}
-```
-
-### Implementation Pattern
-
-```typescript
-class WorkerService {
- constructor() {
- this.app = express();
-
- // Initialize services (dependency injection)
- this.dbManager = new DatabaseManager();
- this.sessionManager = new SessionManager(this.dbManager);
- this.sseBroadcaster = new SSEBroadcaster();
- this.sdkAgent = new SDKAgent(this.dbManager, this.sessionManager);
- this.paginationHelper = new PaginationHelper(this.dbManager);
- this.settingsManager = new SettingsManager(this.dbManager);
-
- this.setupMiddleware();
- this.setupRoutes();
- }
-
- private setupMiddleware(): void {
- this.app.use(express.json({ limit: '50mb' }));
- this.app.use(cors());
- }
-
- private setupRoutes(): void {
- // Health & Viewer
- this.app.get('/health', this.handleHealth.bind(this));
- this.app.get('/', this.handleViewerUI.bind(this));
- this.app.get('/stream', this.handleSSEStream.bind(this));
-
- // Session endpoints
- this.app.post('/sessions/:sessionDbId/init', this.handleSessionInit.bind(this));
- this.app.post('/sessions/:sessionDbId/observations', this.handleObservations.bind(this));
- this.app.post('/sessions/:sessionDbId/summarize', this.handleSummarize.bind(this));
- this.app.get('/sessions/:sessionDbId/status', this.handleSessionStatus.bind(this));
- this.app.delete('/sessions/:sessionDbId', this.handleSessionDelete.bind(this));
-
- // Data retrieval
- this.app.get('/api/observations', this.handleGetObservations.bind(this));
- this.app.get('/api/summaries', this.handleGetSummaries.bind(this));
- this.app.get('/api/prompts', this.handleGetPrompts.bind(this));
- this.app.get('/api/stats', this.handleGetStats.bind(this));
-
- // Settings
- this.app.get('/api/settings', this.handleGetSettings.bind(this));
- this.app.post('/api/settings', this.handleUpdateSettings.bind(this));
- }
-
- async start(): Promise {
- // Initialize database (once, stays open)
- await this.dbManager.initialize();
-
- // Cleanup orphaned sessions from previous runs
- const cleaned = this.dbManager.cleanupOrphanedSessions();
- if (cleaned > 0) {
- logger.info('SYSTEM', `Cleaned ${cleaned} orphaned sessions`);
- }
-
- // Start HTTP server
- const port = getWorkerPort();
- this.server = await new Promise((resolve, reject) => {
- const srv = this.app.listen(port, () => resolve(srv));
- srv.on('error', reject);
- });
-
- logger.info('SYSTEM', 'Worker started', { port, pid: process.pid });
- }
-
- async shutdown(): Promise {
- // Shutdown all active sessions
- await this.sessionManager.shutdownAll();
-
- // Close HTTP server
- if (this.server) {
- await new Promise((resolve, reject) => {
- this.server!.close(err => err ? reject(err) : resolve());
- });
- }
-
- // Close database connection
- await this.dbManager.close();
-
- logger.info('SYSTEM', 'Worker shutdown complete');
- }
-
- // Route handlers - thin wrappers that delegate to services
- private handleSessionInit(req: Request, res: Response): void {
- try {
- const sessionDbId = parseInt(req.params.sessionDbId, 10);
- const session = this.sessionManager.initializeSession(sessionDbId);
-
- // Start SDK agent in background
- this.sdkAgent.startSession(session).catch(err => {
- logger.failure('WORKER', 'SDK agent error', { sessionId: sessionDbId }, err);
- });
-
- // Broadcast SSE event
- this.sseBroadcaster.broadcast({
- type: 'session_started',
- sessionDbId,
- project: session.project
- });
-
- res.json({ status: 'initialized', sessionDbId, port: getWorkerPort() });
- } catch (error) {
- logger.failure('HTTP', 'Session init failed', {}, error as Error);
- res.status(500).json({ error: (error as Error).message });
- }
- }
-
- private handleObservations(req: Request, res: Response): void {
- try {
- const sessionDbId = parseInt(req.params.sessionDbId, 10);
- const { tool_name, tool_input, tool_output, prompt_number } = req.body;
-
- this.sessionManager.queueObservation(sessionDbId, {
- tool_name,
- tool_input,
- tool_output,
- prompt_number
- });
-
- res.json({ status: 'queued' });
- } catch (error) {
- logger.failure('HTTP', 'Observation queuing failed', {}, error as Error);
- res.status(500).json({ error: (error as Error).message });
- }
- }
-
- private handleGetObservations(req: Request, res: Response): void {
- try {
- const { offset, limit, project } = parsePaginationParams(req);
- const result = this.paginationHelper.getObservations(offset, limit, project);
- res.json(result);
- } catch (error) {
- logger.failure('HTTP', 'Get observations failed', {}, error as Error);
- res.status(500).json({ error: (error as Error).message });
- }
- }
-
- private handleGetSettings(req: Request, res: Response): void {
- try {
- const settings = this.settingsManager.getSettings();
- res.json(settings);
- } catch (error) {
- logger.failure('HTTP', 'Get settings failed', {}, error as Error);
- res.status(500).json({ error: (error as Error).message });
- }
- }
-
- // ... other route handlers follow same pattern
-}
-```
-
-### Key Points
-- **Thin controllers**: Route handlers are 5-10 lines each
-- **Delegation**: All business logic delegated to services
-- **Error handling**: Centralized try/catch with consistent logging
-- **No database access**: WorkerService never touches SessionStore directly
-
----
-
-## 2. DatabaseManager (Single Connection)
-
-### Responsibility
-Manage single long-lived database connection. Provide centralized access to SessionStore and SessionSearch.
-
-### Public Interface
-```typescript
-class DatabaseManager {
- async initialize(): Promise;
- async close(): Promise;
-
- // Direct access to stores
- getSessionStore(): SessionStore;
- getSessionSearch(): SessionSearch;
-
- // High-level operations
- cleanupOrphanedSessions(): number;
- getSessionById(sessionDbId: number): DBSession;
- createSession(data: Partial): number;
- updateSession(sessionDbId: number, updates: Partial): void;
- markSessionComplete(sessionDbId: number): void;
-
- // ChromaSync integration
- getChromaSync(): ChromaSync;
-}
-```
-
-### Dependencies
-```typescript
-class DatabaseManager {
- private sessionStore: SessionStore | null = null;
- private sessionSearch: SessionSearch | null = null;
- private chromaSync: ChromaSync | null = null;
-}
-```
-
-### Implementation Pattern
-
-```typescript
-class DatabaseManager {
- private sessionStore: SessionStore | null = null;
- private sessionSearch: SessionSearch | null = null;
- private chromaSync: ChromaSync | null = null;
-
- async initialize(): Promise {
- // Open database connection (ONCE)
- this.sessionStore = new SessionStore();
- this.sessionSearch = new SessionSearch();
-
- // Initialize ChromaSync
- this.chromaSync = new ChromaSync('claude-mem');
-
- // Start background backfill (fire-and-forget)
- this.chromaSync.ensureBackfilled().catch(() => {});
-
- logger.info('DB', 'Database initialized');
- }
-
- async close(): Promise {
- if (this.sessionStore) {
- this.sessionStore.close();
- this.sessionStore = null;
- }
- if (this.sessionSearch) {
- this.sessionSearch.close();
- this.sessionSearch = null;
- }
- logger.info('DB', 'Database closed');
- }
-
- getSessionStore(): SessionStore {
- if (!this.sessionStore) {
- throw new Error('Database not initialized');
- }
- return this.sessionStore;
- }
-
- getSessionSearch(): SessionSearch {
- if (!this.sessionSearch) {
- throw new Error('Database not initialized');
- }
- return this.sessionSearch;
- }
-
- getChromaSync(): ChromaSync {
- if (!this.chromaSync) {
- throw new Error('ChromaSync not initialized');
- }
- return this.chromaSync;
- }
-
- cleanupOrphanedSessions(): number {
- return this.getSessionStore().cleanupOrphanedSessions();
- }
-
- getSessionById(sessionDbId: number): DBSession {
- const session = this.getSessionStore().getSessionById(sessionDbId);
- if (!session) {
- throw new Error(`Session ${sessionDbId} not found`);
- }
- return session;
- }
-
- createSession(data: Partial): number {
- return this.getSessionStore().createSession(data);
- }
-
- updateSession(sessionDbId: number, updates: Partial): void {
- this.getSessionStore().updateSession(sessionDbId, updates);
- }
-
- markSessionComplete(sessionDbId: number): void {
- this.getSessionStore().markSessionComplete(sessionDbId);
- }
-}
-```
-
-### Key Points
-- **Single source of truth**: One connection for entire worker lifetime
-- **Fail fast**: Throw if accessed before initialization
-- **Encapsulation**: Services use DatabaseManager, not SessionStore directly
-- **No open/close churn**: Eliminates 100+ open/close cycles per session
-
----
-
-## 3. SessionManager (Event-Driven Queue)
-
-### Responsibility
-Manage active session lifecycle. Handle event-driven message queues. Coordinate between HTTP requests and SDK agent.
-
-### Public Interface
-```typescript
-class SessionManager {
- constructor(dbManager: DatabaseManager);
-
- // Session lifecycle
- initializeSession(sessionDbId: number): ActiveSession;
- getSession(sessionDbId: number): ActiveSession | undefined;
- queueObservation(sessionDbId: number, data: ObservationData): void;
- queueSummarize(sessionDbId: number): void;
- deleteSession(sessionDbId: number): Promise;
-
- // Bulk operations
- async shutdownAll(): Promise;
-
- // Queue access (for SDKAgent)
- getMessageIterator(sessionDbId: number): AsyncIterableIterator;
-}
-```
-
-### Dependencies
-```typescript
-class SessionManager {
- private dbManager: DatabaseManager;
- private sessions: Map = new Map();
- private sessionQueues: Map = new Map();
-}
-```
-
-### Implementation Pattern
-
-```typescript
-interface ActiveSession {
- sessionDbId: number;
- claudeSessionId: string;
- sdkSessionId: string | null;
- project: string;
- userPrompt: string;
- pendingMessages: PendingMessage[];
- abortController: AbortController;
- generatorPromise: Promise | null;
- lastPromptNumber: number;
- startTime: number;
-}
-
-interface PendingMessage {
- type: 'observation' | 'summarize';
- tool_name?: string;
- tool_input?: any;
- tool_output?: any;
- prompt_number?: number;
-}
-
-class SessionManager {
- private dbManager: DatabaseManager;
- private sessions: Map = new Map();
- private sessionQueues: Map = new Map();
-
- constructor(dbManager: DatabaseManager) {
- this.dbManager = dbManager;
- }
-
- initializeSession(sessionDbId: number): ActiveSession {
- // Check if already active
- let session = this.sessions.get(sessionDbId);
- if (session) {
- return session;
- }
-
- // Fetch from database
- const dbSession = this.dbManager.getSessionById(sessionDbId);
-
- // Create active session
- session = {
- sessionDbId,
- claudeSessionId: dbSession.claude_session_id,
- sdkSessionId: null,
- project: dbSession.project,
- userPrompt: dbSession.user_prompt,
- pendingMessages: [],
- abortController: new AbortController(),
- generatorPromise: null,
- lastPromptNumber: 0,
- startTime: Date.now()
- };
-
- this.sessions.set(sessionDbId, session);
-
- // Create event emitter for queue notifications
- const emitter = new EventEmitter();
- this.sessionQueues.set(sessionDbId, emitter);
-
- logger.info('SESSION', 'Session initialized', { sessionDbId, project: session.project });
-
- return session;
- }
-
- getSession(sessionDbId: number): ActiveSession | undefined {
- return this.sessions.get(sessionDbId);
- }
-
- queueObservation(sessionDbId: number, data: ObservationData): void {
- const session = this.sessions.get(sessionDbId);
- if (!session) {
- throw new Error(`Session ${sessionDbId} not active`);
- }
-
- session.pendingMessages.push({
- type: 'observation',
- tool_name: data.tool_name,
- tool_input: data.tool_input,
- tool_output: data.tool_output,
- prompt_number: data.prompt_number
- });
-
- // Notify generator immediately (zero latency)
- const emitter = this.sessionQueues.get(sessionDbId);
- emitter?.emit('message');
-
- logger.debug('SESSION', 'Observation queued', {
- sessionDbId,
- queueLength: session.pendingMessages.length
- });
- }
-
- queueSummarize(sessionDbId: number): void {
- const session = this.sessions.get(sessionDbId);
- if (!session) {
- throw new Error(`Session ${sessionDbId} not active`);
- }
-
- session.pendingMessages.push({ type: 'summarize' });
-
- const emitter = this.sessionQueues.get(sessionDbId);
- emitter?.emit('message');
-
- logger.debug('SESSION', 'Summarize queued', { sessionDbId });
- }
-
- async deleteSession(sessionDbId: number): Promise {
- const session = this.sessions.get(sessionDbId);
- if (!session) {
- return; // Already deleted
- }
-
- // Abort the SDK agent
- session.abortController.abort();
-
- // Wait for generator to finish
- if (session.generatorPromise) {
- await session.generatorPromise.catch(() => {});
- }
-
- // Cleanup
- this.sessions.delete(sessionDbId);
- this.sessionQueues.delete(sessionDbId);
-
- logger.info('SESSION', 'Session deleted', { sessionDbId });
- }
-
- async shutdownAll(): Promise {
- const sessionIds = Array.from(this.sessions.keys());
- await Promise.all(sessionIds.map(id => this.deleteSession(id)));
- }
-
- // Generator for SDKAgent to consume
- async *getMessageIterator(sessionDbId: number): AsyncIterableIterator {
- const session = this.sessions.get(sessionDbId);
- if (!session) {
- throw new Error(`Session ${sessionDbId} not active`);
- }
-
- const emitter = this.sessionQueues.get(sessionDbId);
- if (!emitter) {
- throw new Error(`No emitter for session ${sessionDbId}`);
- }
-
- while (!session.abortController.signal.aborted) {
- // Wait for messages if queue is empty
- if (session.pendingMessages.length === 0) {
- await new Promise(resolve => {
- const handler = () => resolve();
- emitter.once('message', handler);
-
- // Also listen for abort
- session.abortController.signal.addEventListener('abort', () => {
- emitter.off('message', handler);
- resolve();
- }, { once: true });
- });
- }
-
- // Yield all pending messages
- while (session.pendingMessages.length > 0) {
- const message = session.pendingMessages.shift()!;
- yield message;
- }
- }
- }
-}
-```
-
-### Key Points
-- **Event-driven**: Zero polling, immediate notification via EventEmitter
-- **Single responsibility**: Only manages session state and queues
-- **Clean separation**: Database access delegated to DatabaseManager
-- **Fail fast**: Throws if session doesn't exist (caller handles gracefully)
-
----
-
-## 4. SSEBroadcaster (SSE Client Management)
-
-### Responsibility
-Manage SSE client connections. Broadcast events to all connected clients. Handle disconnections gracefully.
-
-### Public Interface
-```typescript
-class SSEBroadcaster {
- addClient(res: Response): void;
- removeClient(res: Response): void;
- broadcast(event: SSEEvent): void;
- getClientCount(): number;
-}
-```
-
-### Dependencies
-```typescript
-class SSEBroadcaster {
- private sseClients: Set = new Set();
-}
-```
-
-### Implementation Pattern
-
-```typescript
-interface SSEEvent {
- type: string;
- [key: string]: any;
-}
-
-class SSEBroadcaster {
- private sseClients: Set = new Set();
-
- addClient(res: Response): void {
- this.sseClients.add(res);
- logger.debug('SSE', 'Client connected', { total: this.sseClients.size });
-
- // Setup cleanup on disconnect
- res.on('close', () => {
- this.removeClient(res);
- });
-
- // Send initial event
- this.sendToClient(res, { type: 'connected', timestamp: Date.now() });
- }
-
- removeClient(res: Response): void {
- this.sseClients.delete(res);
- logger.debug('SSE', 'Client disconnected', { total: this.sseClients.size });
- }
-
- broadcast(event: SSEEvent): void {
- if (this.sseClients.size === 0) {
- return; // Short-circuit if no clients
- }
-
- const eventWithTimestamp = { ...event, timestamp: Date.now() };
- const data = `data: ${JSON.stringify(eventWithTimestamp)}\n\n`;
-
- // Single-pass write + cleanup
- for (const client of this.sseClients) {
- try {
- client.write(data);
- } catch (err) {
- // Remove failed client immediately
- this.sseClients.delete(client);
- logger.debug('SSE', 'Client removed due to write error');
- }
- }
- }
-
- getClientCount(): number {
- return this.sseClients.size;
- }
-
- private sendToClient(res: Response, event: SSEEvent): void {
- const data = `data: ${JSON.stringify(event)}\n\n`;
- try {
- res.write(data);
- } catch (err) {
- this.sseClients.delete(res);
- }
- }
-}
-```
-
-### Key Points
-- **Simple**: Single-pass broadcast, no two-step cleanup
-- **Fail gracefully**: Remove dead clients on write errors
-- **Zero polling**: Event-driven notifications
-- **Encapsulated**: WorkerService never touches SSE internals
-
----
-
-## 5. SDKAgent (SDK Query Loop)
-
-### Responsibility
-Spawn Claude subprocess. Run Agent SDK query loop. Process SDK responses (observations, summaries). Sync to database and Chroma.
-
-### Public Interface
-```typescript
-class SDKAgent {
- constructor(dbManager: DatabaseManager, sessionManager: SessionManager);
- async startSession(session: ActiveSession): Promise;
-}
-```
-
-### Dependencies
-```typescript
-class SDKAgent {
- private dbManager: DatabaseManager;
- private sessionManager: SessionManager;
-}
-```
-
-### Implementation Pattern
-
-```typescript
-class SDKAgent {
- private dbManager: DatabaseManager;
- private sessionManager: SessionManager;
-
- constructor(dbManager: DatabaseManager, sessionManager: SessionManager) {
- this.dbManager = dbManager;
- this.sessionManager = sessionManager;
- }
-
- async startSession(session: ActiveSession): Promise {
- try {
- // Find Claude executable (inline, called once per session)
- const claudePath = process.env.CLAUDE_CODE_PATH ||
- execSync(process.platform === 'win32' ? 'where claude' : 'which claude', { encoding: 'utf8' })
- .trim().split('\n')[0].trim();
-
- if (!claudePath) {
- throw new Error('Claude executable not found in PATH');
- }
-
- // Build SDK config
- const config: AgentSDKConfig = {
- apiKey: getCachedAPIKey(),
- modelId: getModelId(),
- sessionFilePath: getSessionFilePath(session.claudeSessionId)
- };
-
- // Create message generator
- const messageGenerator = this.createMessageGenerator(session);
-
- // Run Agent SDK query loop
- const { response } = await claudeAgent.run({
- config,
- userMessages: messageGenerator,
- abortSignal: session.abortController.signal
- });
-
- // Process SDK responses
- for await (const chunk of response) {
- if (chunk.type === 'text') {
- await this.processSDKResponse(session, chunk.text);
- }
- }
-
- // Mark session complete
- this.dbManager.markSessionComplete(session.sessionDbId);
- logger.info('SDK', 'Session complete', { sessionDbId: session.sessionDbId });
-
- } catch (error) {
- logger.failure('SDK', 'Agent error', { sessionDbId: session.sessionDbId }, error as Error);
- this.dbManager.markSessionComplete(session.sessionDbId); // Mark failed
- } finally {
- // Cleanup
- this.sessionManager.deleteSession(session.sessionDbId).catch(() => {});
- }
- }
-
- private async *createMessageGenerator(session: ActiveSession): AsyncIterableIterator {
- // Yield initial user prompt
- yield {
- role: 'user',
- content: session.userPrompt
- };
-
- // Consume pending messages from SessionManager (event-driven)
- for await (const message of this.sessionManager.getMessageIterator(session.sessionDbId)) {
- if (message.type === 'observation') {
- yield {
- role: 'user',
- content: this.buildObservationPrompt(message)
- };
- } else if (message.type === 'summarize') {
- yield {
- role: 'user',
- content: this.buildSummarizePrompt(session)
- };
- }
- }
- }
-
- private buildObservationPrompt(message: PendingMessage): string {
- return `
-${message.tool_name}
-${JSON.stringify(message.tool_input)}
-${message.tool_output}
-
-
-Please analyze this tool execution and extract observations.`;
- }
-
- private buildSummarizePrompt(session: ActiveSession): string {
- return `Please summarize this session.`;
- }
-
- private async processSDKResponse(session: ActiveSession, text: string): Promise {
- // Parse XML for observations or summaries
- const observations = this.parseObservations(text);
- const summary = this.parseSummary(text);
-
- // Store observations
- for (const obs of observations) {
- const obsId = this.dbManager.getSessionStore().saveObservation({
- sessionDbId: session.sessionDbId,
- claudeSessionId: session.claudeSessionId,
- project: session.project,
- type: obs.type,
- title: obs.title,
- subtitle: obs.subtitle,
- text: obs.text,
- concepts: obs.concepts,
- files: obs.files,
- prompt_number: session.lastPromptNumber
- });
-
- // Sync to Chroma (fire-and-forget)
- this.dbManager.getChromaSync().syncObservation(obsId).catch(() => {});
-
- logger.info('SDK', 'Observation saved', { obsId, type: obs.type });
- }
-
- // Store summary
- if (summary) {
- const summaryId = this.dbManager.getSessionStore().saveSummary({
- sessionDbId: session.sessionDbId,
- claudeSessionId: session.claudeSessionId,
- project: session.project,
- summary: summary.text
- });
-
- // Sync to Chroma (fire-and-forget)
- this.dbManager.getChromaSync().syncSummary(summaryId).catch(() => {});
-
- logger.info('SDK', 'Summary saved', { summaryId });
- }
- }
-
- private parseObservations(text: string): Array> {
- // XML parsing logic (existing implementation)
- // ...
- return [];
- }
-
- private parseSummary(text: string): { text: string } | null {
- // XML parsing logic (existing implementation)
- // ...
- return null;
- }
-}
-```
-
-### Key Points
-- **Event-driven**: Consumes SessionManager's async iterator (no polling)
-- **Fail fast**: Throws if Claude executable not found
-- **Fire-and-forget Chroma**: Don't block on vector sync
-- **Clean separation**: Database access via DatabaseManager only
-
----
-
-## 6. PaginationHelper (DRY Utility)
-
-### Responsibility
-DRY helper for paginated queries. Eliminates copy-paste across observations/summaries/prompts endpoints.
-
-### Public Interface
-```typescript
-class PaginationHelper {
- constructor(dbManager: DatabaseManager);
-
- getObservations(offset: number, limit: number, project?: string): PaginatedResult;
- getSummaries(offset: number, limit: number, project?: string): PaginatedResult;
- getPrompts(offset: number, limit: number, project?: string): PaginatedResult;
-}
-```
-
-### Dependencies
-```typescript
-class PaginationHelper {
- private dbManager: DatabaseManager;
-}
-```
-
-### Implementation Pattern
-
-```typescript
-interface PaginatedResult {
- items: T[];
- hasMore: boolean;
- offset: number;
- limit: number;
-}
-
-class PaginationHelper {
- private dbManager: DatabaseManager;
-
- constructor(dbManager: DatabaseManager) {
- this.dbManager = dbManager;
- }
-
- getObservations(offset: number, limit: number, project?: string): PaginatedResult {
- return this.paginate(
- 'observations',
- 'id, type, title, subtitle, text, project, prompt_number, created_at, created_at_epoch',
- offset,
- limit,
- project
- );
- }
-
- getSummaries(offset: number, limit: number, project?: string): PaginatedResult {
- return this.paginate(
- 'summaries',
- 'id, session_db_id, project, summary, created_at, created_at_epoch',
- offset,
- limit,
- project
- );
- }
-
- getPrompts(offset: number, limit: number, project?: string): PaginatedResult {
- return this.paginate(
- 'user_prompts',
- 'id, session_db_id, project, prompt, created_at, created_at_epoch',
- offset,
- limit,
- project
- );
- }
-
- private paginate(
- table: string,
- columns: string,
- offset: number,
- limit: number,
- project?: string
- ): PaginatedResult {
- const db = this.dbManager.getSessionStore().db;
-
- let query = `SELECT ${columns} FROM ${table}`;
- const params: any[] = [];
-
- if (project) {
- query += ' WHERE project = ?';
- params.push(project);
- }
-
- query += ' ORDER BY created_at_epoch DESC LIMIT ? OFFSET ?';
- params.push(limit + 1, offset); // Fetch one extra to check hasMore
-
- const stmt = db.prepare(query);
- const results = stmt.all(...params) as T[];
-
- return {
- items: results.slice(0, limit),
- hasMore: results.length > limit,
- offset,
- limit
- };
- }
-}
-```
-
-### Key Points
-- **DRY**: Single pagination implementation for 3 endpoints
-- **Efficient**: Uses LIMIT+1 trick to avoid COUNT(*) query
-- **Type-safe**: Generic typing preserves type information
-- **Simple**: 40 lines replaces 120+ lines of copy-paste
-
----
-
-## 7. SettingsManager (DRY Utility)
-
-### Responsibility
-DRY helper for viewer settings CRUD. Eliminates duplication in settings read/write logic.
-
-### Public Interface
-```typescript
-class SettingsManager {
- constructor(dbManager: DatabaseManager);
-
- getSettings(): ViewerSettings;
- updateSettings(updates: Partial): ViewerSettings;
-}
-```
-
-### Dependencies
-```typescript
-class SettingsManager {
- private dbManager: DatabaseManager;
-}
-```
-
-### Implementation Pattern
-
-```typescript
-interface ViewerSettings {
- sidebarOpen: boolean;
- selectedProject: string | null;
- theme: 'light' | 'dark' | 'system';
-}
-
-class SettingsManager {
- private dbManager: DatabaseManager;
- private readonly defaultSettings: ViewerSettings = {
- sidebarOpen: true,
- selectedProject: null,
- theme: 'system'
- };
-
- constructor(dbManager: DatabaseManager) {
- this.dbManager = dbManager;
- }
-
- getSettings(): ViewerSettings {
- const db = this.dbManager.getSessionStore().db;
-
- try {
- const stmt = db.prepare('SELECT key, value FROM viewer_settings');
- const rows = stmt.all() as Array<{ key: string; value: string }>;
-
- const settings = { ...this.defaultSettings };
- for (const row of rows) {
- if (row.key in settings) {
- settings[row.key as keyof ViewerSettings] = JSON.parse(row.value);
- }
- }
-
- return settings;
- } catch (error) {
- logger.debug('SETTINGS', 'Failed to load settings, using defaults', {}, error as Error);
- return { ...this.defaultSettings };
- }
- }
-
- updateSettings(updates: Partial): ViewerSettings {
- const db = this.dbManager.getSessionStore().db;
-
- const stmt = db.prepare(`
- INSERT OR REPLACE INTO viewer_settings (key, value)
- VALUES (?, ?)
- `);
-
- for (const [key, value] of Object.entries(updates)) {
- stmt.run(key, JSON.stringify(value));
- }
-
- return this.getSettings();
- }
-}
-```
-
-### Key Points
-- **DRY**: Single source of truth for settings logic
-- **Type-safe**: Strong typing for settings object
-- **Fail gracefully**: Returns defaults if settings table doesn't exist
-- **Simple**: 50 lines replaces scattered settings logic
-
----
-
-## Dependency Graph
-
-```
-WorkerService
-├─ DatabaseManager
-│ ├─ SessionStore (long-lived)
-│ ├─ SessionSearch (long-lived)
-│ └─ ChromaSync
-├─ SessionManager
-│ └─ DatabaseManager (injected)
-├─ SSEBroadcaster
-│ └─ (no dependencies)
-├─ SDKAgent
-│ ├─ DatabaseManager (injected)
-│ └─ SessionManager (injected)
-├─ PaginationHelper
-│ └─ DatabaseManager (injected)
-└─ SettingsManager
- └─ DatabaseManager (injected)
-```
-
-### Initialization Order
-1. `DatabaseManager.initialize()` - Opens DB connection
-2. `SessionManager(dbManager)` - Injected dependency
-3. `SDKAgent(dbManager, sessionManager)` - Injected dependencies
-4. `PaginationHelper(dbManager)` - Injected dependency
-5. `SettingsManager(dbManager)` - Injected dependency
-6. `SSEBroadcaster()` - No dependencies
-7. `WorkerService.start()` - Orchestrates all services
-
----
-
-## File Structure
-
-```
-src/services/
-├─ worker-service.ts (WorkerService class - orchestration)
-├─ worker/
-│ ├─ DatabaseManager.ts (Single connection manager)
-│ ├─ SessionManager.ts (Event-driven session lifecycle)
-│ ├─ SSEBroadcaster.ts (SSE client management)
-│ ├─ SDKAgent.ts (SDK query loop)
-│ ├─ PaginationHelper.ts (DRY pagination)
-│ └─ SettingsManager.ts (DRY settings CRUD)
-└─ worker-types.ts (Shared interfaces)
-```
-
----
-
-## Benefits of This Architecture
-
-### 1. Single Responsibility
-- Each class does ONE thing
-- Easy to understand, test, and modify
-- Changes are localized
-
-### 2. DRY
-- Pagination logic: 40 lines (down from 120+)
-- Settings logic: 50 lines (down from scattered code)
-- Database access: Centralized in DatabaseManager
-
-### 3. Testability
-- Each class can be unit tested in isolation
-- Mock dependencies via constructor injection
-- No global state
-
-### 4. Performance
-- Single database connection (down from 100+ open/close cycles)
-- Event-driven queues (zero polling latency)
-- Fire-and-forget Chroma sync (doesn't block)
-
-### 5. Maintainability
-- Clear dependency graph
-- Explicit interfaces
-- Fail-fast error handling
-- No defensive programming
-
----
-
-## Migration Strategy
-
-### Phase 1: Extract Classes
-1. Create `src/services/worker/` directory
-2. Extract `DatabaseManager.ts` from existing code
-3. Extract `SessionManager.ts` with EventEmitter pattern
-4. Extract `SSEBroadcaster.ts`
-5. Extract `SDKAgent.ts`
-6. Extract `PaginationHelper.ts`
-7. Extract `SettingsManager.ts`
-
-### Phase 2: Refactor WorkerService
-1. Replace inline database access with `DatabaseManager`
-2. Replace session map with `SessionManager`
-3. Replace SSE logic with `SSEBroadcaster`
-4. Replace SDK logic with `SDKAgent`
-5. Replace pagination endpoints with `PaginationHelper`
-6. Replace settings endpoints with `SettingsManager`
-
-### Phase 3: Delete Dead Code
-1. Remove `cachedClaudePath` and `findClaudePath()`
-2. Remove `checkAndStopSpinner()` and debounce logic
-3. Remove polling loops (replace with EventEmitter)
-4. Remove two-pass SSE cleanup
-5. Remove verbose Chroma error handling
-6. Remove duplicate pagination logic
-
-### Phase 4: Testing
-1. Run existing integration tests
-2. Performance benchmarks (latency, throughput)
-3. Memory profiling (check for EventEmitter leaks)
-4. Load testing (100 concurrent sessions)
-
----
-
-## Success Metrics
-
-### Code Quality
-- **Total lines**: ~600-700 (down from 1173)
-- **Classes**: 7 focused classes vs 1 monolithic class
-- **Duplicate code**: Eliminated (pagination, settings, DB access)
-- **Cyclomatic complexity**: <15 per method
-
-### Performance
-- **Observation latency**: <5ms (down from 50-100ms)
-- **Database open/close**: 1 per worker lifetime (down from 100+)
-- **SSE broadcast**: Single-pass (down from two-pass)
-- **Polling loops**: 0 (down from 1)
-
-### Maintainability
-- **Single Responsibility**: Each class has one clear purpose
-- **DRY**: No copy-paste code
-- **Testability**: All classes unit-testable
-- **Dependencies**: Explicit via constructor injection
diff --git a/docs/context/worker-service-overhead.md b/docs/context/worker-service-overhead.md
deleted file mode 100644
index 84bba30e..00000000
--- a/docs/context/worker-service-overhead.md
+++ /dev/null
@@ -1,959 +0,0 @@
-# Worker Service Overhead Analysis
-
-**Date**: 2025-11-06
-**File**: `src/services/worker-service.ts`
-**Total Lines**: 1173
-**Overall Assessment**: This file has accumulated unnecessary complexity, artificial delays, and defensive programming patterns that actively harm performance. Many patterns were likely added "just in case" without real-world justification.
-
----
-
-## Executive Summary
-
-**High Severity Issues (Score 8-10)**:
-- **Line 942**: Polling loop with 100ms delay instead of event-driven architecture (Score: 10/10)
-- **Lines 338-365**: Spinner debounce with 1.5s artificial delay (Score: 9/10)
-- **Lines 204-234**: Database reopening on every getOrCreateSession call (Score: 8/10)
-
-**Medium Severity Issues (Score 5-7)**:
-- **Lines 33-70**: Unnecessary Claude path caching for rare operation (Score: 6/10)
-- **Lines 694-711**: Redundant database reopening in handleInit (Score: 7/10)
-- **Lines 728-741**: Fire-and-forget Chroma sync with verbose error handling (Score: 5/10)
-
-**Low Severity Issues (Score 3-4)**:
-- **Line 28**: Magic number MESSAGE_POLL_INTERVAL_MS without justification (Score: 4/10)
-- **Lines 303-321**: Over-engineered SSE client cleanup (Score: 4/10)
-
----
-
-## Line-by-Line Analysis
-
-### Lines 1-30: Setup and Constants
-
-**Lines 22-24**: Version reading from package.json
-```typescript
-const packageJson = JSON.parse(readFileSync(join(__dirname, '..', '..', 'package.json'), 'utf-8'));
-const VERSION = packageJson.version;
-```
-**Score**: 2/10
-**Why**: This is fine. Reads once at startup, uses the value for the /api/stats endpoint.
-
-**Line 26**: Model configuration
-```typescript
-const MODEL = process.env.CLAUDE_MEM_MODEL || 'claude-sonnet-4-5';
-```
-**Score**: 1/10
-**Why**: Clean, simple, correct.
-
-**Line 28**: Magic number
-```typescript
-const MESSAGE_POLL_INTERVAL_MS = 100;
-```
-**Score**: 4/10
-**Why**: This is a magic number without justification. Why 100ms? Why not 50ms or 200ms? More importantly, **why are we polling at all instead of using event-driven patterns?** The name is descriptive, but the existence of this constant indicates a fundamental architectural problem (see line 942).
-
-**Pattern**: This constant exists to support a polling loop that shouldn't exist.
-
----
-
-### Lines 33-70: Claude Path Caching
-
-```typescript
-let cachedClaudePath: string | null = null;
-
-function findClaudePath(): string {
- if (cachedClaudePath) {
- return cachedClaudePath;
- }
- // ... 30 lines of logic to find and cache path ...
-}
-```
-
-**Score**: 6/10
-**Why Stupid**:
-1. **YAGNI Violation**: This function is called **exactly once** per worker startup (line 846 in runSDKAgent)
-2. **Premature Optimization**: Caching saves ~5ms on an operation that happens once per worker lifetime
-3. **Added Complexity**: 37 lines of code including module-level state for negligible benefit
-4. **False Economy**: The worker runs for hours/days. Saving 5ms on startup is meaningless.
-
-**What Should Happen**:
-```typescript
-function findClaudePath(): string {
- if (process.env.CLAUDE_CODE_PATH) return process.env.CLAUDE_CODE_PATH;
-
- const command = process.platform === 'win32' ? 'where claude' : 'which claude';
- const result = execSync(command, { encoding: 'utf8' }).trim().split('\n')[0].trim();
-
- if (!result) throw new Error('Claude executable not found in PATH');
- return result;
-}
-```
-**Savings**: Remove 33 lines of unnecessary code and module-level state.
-
----
-
-### Lines 103-110: WorkerService State
-
-```typescript
-class WorkerService {
- private app: express.Application;
- private sessions: Map = new Map();
- private chromaSync!: ChromaSync;
- private sseClients: Set = new Set();
- private isProcessing: boolean = false;
- private spinnerStopTimer: NodeJS.Timeout | null = null;
-```
-
-**Score**: 7/10 (for spinnerStopTimer)
-**Why**:
-- `app`, `sessions`, `chromaSync`, `sseClients`: **Good** - necessary state
-- `isProcessing`: **Questionable** (Score 5/10) - Do we really need to track this globally? Can't we derive it from `sessions.size > 0` or `sessions.values().some(s => s.pendingMessages.length > 0)`?
-- `spinnerStopTimer`: **Bad** (Score 7/10) - Exists solely to support artificial debouncing (see lines 338-365)
-
-**Pattern**: State that exists to support other unnecessary complexity.
-
----
-
-### Lines 145-178: Service Startup
-
-**Lines 145-153**: HTTP server startup
-```typescript
-async start(): Promise {
- const port = getWorkerPort();
- await new Promise((resolve, reject) => {
- this.app.listen(port, () => resolve())
- .on('error', reject);
- });
- logger.info('SYSTEM', 'Worker started', { port, pid: process.pid });
-```
-**Score**: 1/10
-**Why**: This is good. Clean promise wrapper, fail-fast on errors, clear logging.
-
-**Lines 155-167**: ChromaSync initialization and orphan cleanup
-```typescript
-this.chromaSync = new ChromaSync('claude-mem');
-logger.info('SYSTEM', 'ChromaSync initialized');
-
-const db = new SessionStore();
-const cleanedCount = db.cleanupOrphanedSessions();
-db.close();
-```
-**Score**: 2/10
-**Why**: This is fine. Necessary initialization and cleanup. Database is opened, used, and closed immediately.
-
-**Lines 168-177**: Chroma backfill
-```typescript
-logger.info('SYSTEM', 'Starting Chroma backfill in background...');
-this.chromaSync.ensureBackfilled()
- .then(() => {
- logger.info('SYSTEM', 'Chroma backfill complete');
- })
- .catch((error: Error) => {
- logger.error('SYSTEM', 'Chroma backfill failed - continuing anyway', {}, error);
- // Don't exit - allow worker to continue serving requests
- });
-```
-**Score**: 3/10
-**Why**: This is mostly fine. Fire-and-forget background operation that doesn't block startup. The verbose error handling is slightly excessive (could be a single logger call), but acceptable for a background operation.
-
----
-
-### Lines 200-236: getOrCreateSession - THE KILLER
-
-```typescript
-private getOrCreateSession(sessionDbId: number): ActiveSession {
- let session = this.sessions.get(sessionDbId);
- if (session) return session;
-
- const db = new SessionStore();
- const dbSession = db.getSessionById(sessionDbId);
- if (!dbSession) {
- db.close();
- throw new Error(`Session ${sessionDbId} not found in database`);
- }
-
- session = {
- sessionDbId,
- claudeSessionId: dbSession.claude_session_id,
- sdkSessionId: null,
- project: dbSession.project,
- userPrompt: dbSession.user_prompt,
- pendingMessages: [],
- abortController: new AbortController(),
- generatorPromise: null,
- lastPromptNumber: 0,
- startTime: Date.now()
- };
-
- this.sessions.set(sessionDbId, session);
-
- session.generatorPromise = this.runSDKAgent(session).catch(err => {
- logger.failure('WORKER', 'SDK agent error', { sessionId: sessionDbId }, err);
- const db = new SessionStore();
- db.markSessionFailed(sessionDbId);
- db.close();
- this.sessions.delete(sessionDbId);
- });
-
- db.close();
- return session;
-}
-```
-
-**Score**: 8/10
-**Why This Is Stupid**:
-
-1. **Database Reopening**: Opens database at line 204, closes at line 234. This happens on:
- - First call to `/sessions/:id/init` (line 691)
- - First call to `/sessions/:id/observations` (line 762)
- - First call to `/sessions/:id/summarize` (line 789)
-
- For a typical session: init (DB open/close) → observation (DB open/close) → observation (DB open/close) → summarize (DB open/close). **That's 4 database open/close cycles when ONE would suffice.**
-
-2. **Redundant Database Access**: The database is ALREADY opened in `handleInit` at line 695 to call `setWorkerPort()`. So we have:
- - Line 695: `const db = new SessionStore()` in handleInit
- - Line 696: `db.setWorkerPort()`
- - Line 697-711: More queries on the same database
- - Line 711: `db.close()`
- - Line 691: `this.getOrCreateSession()` is called
- - Line 204: **Opens database AGAIN** inside getOrCreateSession
- - Line 234: Closes it
-
- **This is fucking insane.** We close the database, then immediately reopen it in the same call stack.
-
-3. **Error Handler Opens Database**: Line 228 opens a NEW database connection in the error handler. If runSDKAgent fails, we open the database AGAIN just to mark it failed, then close it. This is defensive programming for ghosts - if the worker is crashing, do we really care about marking it failed?
-
-**What Should Happen**:
-- Pass the already-open database connection to getOrCreateSession
-- Or at minimum, reuse the connection from the calling context
-- The error handler should either crash hard or mark failed WITHOUT reopening the database
-
-**Estimated Performance Impact**: Database open/close is expensive (~1-5ms each). For a session with 10 observations, this pattern adds **20-100ms of pure overhead**.
-
----
-
-### Lines 263-292: SSE Stream Setup
-
-```typescript
-private handleSSEStream(req: Request, res: Response): void {
- // Set SSE headers
- res.setHeader('Content-Type', 'text/event-stream');
- res.setHeader('Cache-Control', 'no-cache');
- res.setHeader('Connection', 'keep-alive');
- res.setHeader('Access-Control-Allow-Origin', '*');
-
- // Add client to set
- this.sseClients.add(res);
- logger.info('WORKER', `SSE client connected`, { totalClients: this.sseClients.size });
-
- // Send only projects list - all data will be loaded via pagination
- const db = new SessionStore();
- const allProjects = db.getAllProjects();
- db.close();
-
- const initialData = {
- type: 'initial_load',
- projects: allProjects,
- timestamp: Date.now()
- };
-
- res.write(`data: ${JSON.stringify(initialData)}\n\n`);
-
- // Handle client disconnect
- req.on('close', () => {
- this.sseClients.delete(res);
- logger.info('WORKER', `SSE client disconnected`, { remainingClients: this.sseClients.size });
- });
-}
-```
-
-**Score**: 2/10
-**Why**: This is mostly good. Clean SSE setup with proper headers and client tracking. Database is opened, used, and closed.
-
----
-
-### Lines 297-322: SSE Broadcast and Cleanup
-
-```typescript
-private broadcastSSE(event: any): void {
- if (this.sseClients.size === 0) {
- return; // No clients connected, skip broadcast
- }
-
- const data = `data: ${JSON.stringify(event)}\n\n`;
- const clientsToRemove: Response[] = [];
-
- for (const client of this.sseClients) {
- try {
- client.write(data);
- } catch (error) {
- // Client disconnected, mark for removal
- clientsToRemove.push(client);
- }
- }
-
- // Clean up disconnected clients
- for (const client of clientsToRemove) {
- this.sseClients.delete(client);
- }
-
- if (clientsToRemove.length > 0) {
- logger.info('WORKER', `SSE cleaned up disconnected clients`, { count: clientsToRemove.length });
- }
-}
-```
-
-**Score**: 4/10
-**Why This Is Slightly Stupid**:
-
-1. **Two-Pass Cleanup**: Creates a temporary array of failed clients, then iterates again to remove them. Why not just remove them in the first loop?
-2. **Unnecessary Logging**: Do we really need to log every time a client disconnects? The `handleSSEStream` already logs disconnects (line 290). This is duplicate logging.
-
-**What Should Happen**:
-```typescript
-private broadcastSSE(event: any): void {
- if (this.sseClients.size === 0) return;
-
- const data = `data: ${JSON.stringify(event)}\n\n`;
- for (const client of this.sseClients) {
- try {
- client.write(data);
- } catch {
- this.sseClients.delete(client);
- }
- }
-}
-```
-
-**Savings**: Remove 10 lines, remove duplicate logging, eliminate temporary array.
-
----
-
-### Lines 338-365: Spinner Debounce - ARTIFICIAL DELAY
-
-```typescript
-private checkAndStopSpinner(): void {
- // Clear any existing timer
- if (this.spinnerStopTimer) {
- clearTimeout(this.spinnerStopTimer);
- this.spinnerStopTimer = null;
- }
-
- // Check if any session has pending messages
- const hasPendingMessages = Array.from(this.sessions.values()).some(
- session => session.pendingMessages.length > 0
- );
-
- if (!hasPendingMessages) {
- // Debounce: wait 1.5s and check again
- this.spinnerStopTimer = setTimeout(() => {
- const stillEmpty = Array.from(this.sessions.values()).every(
- session => session.pendingMessages.length === 0
- );
-
- if (stillEmpty) {
- logger.debug('WORKER', 'All queues empty - stopping spinner');
- this.broadcastProcessingStatus(false);
- }
-
- this.spinnerStopTimer = null;
- }, 1500);
- }
-}
-```
-
-**Score**: 9/10
-**Why This Is ABSOLUTELY FUCKING STUPID**:
-
-1. **Artificial Delay**: **1.5 SECONDS** (1500ms) of artificial delay before stopping the spinner. This is pure overhead added for no reason.
-
-2. **Why Was This Added?**: Probably someone thought "the UI flickers when the spinner stops/starts rapidly." **SO FUCKING WHAT?** That's a UI rendering problem, not a worker service problem. Fix it in the UI with CSS transitions or debouncing on the CLIENT side.
-
-3. **Double-Check Pattern**: Checks if queues are empty, waits 1.5s, then checks AGAIN. This is defensive programming for ghosts. If the queue is empty, it's empty. We're not protecting against race conditions here - we're just wasting time.
-
-4. **Polling Instead of Events**: This function is called from `handleAgentMessage` (line 1145) after processing every single response. Instead of reacting to the actual completion of work, we're polling state and debouncing.
-
-5. **State Management Overhead**: Requires `spinnerStopTimer` field (line 109), timer cleanup logic, null checks, etc.
-
-**Real-World Impact**: Every time the worker finishes processing observations, the UI spinner continues to show "processing" for **1.5 seconds** even though nothing is happening. This makes the entire system feel slower.
-
-**What Should Happen**:
-```typescript
-private checkAndStopSpinner(): void {
- const hasPendingMessages = Array.from(this.sessions.values()).some(
- session => session.pendingMessages.length > 0
- );
-
- if (!hasPendingMessages) {
- this.broadcastProcessingStatus(false);
- }
-}
-```
-
-**Savings**: Remove 15 lines of debouncing logic, remove timer state, eliminate 1.5s artificial delay.
-
-**Alternative**: If UI flickering is actually a problem (prove it first), handle it client-side with CSS transitions or client-side debouncing.
-
----
-
-### Lines 370-411: Stats Endpoint
-
-```typescript
-private handleStats(_req: Request, res: Response): void {
- try {
- const db = new SessionStore();
-
- // Get database stats
- const obsCount = db.db.prepare('SELECT COUNT(*) as count FROM observations').get() as { count: number };
- const sessionCount = db.db.prepare('SELECT COUNT(*) as count FROM sdk_sessions').get() as { count: number };
- const summaryCount = db.db.prepare('SELECT COUNT(*) as count FROM session_summaries').get() as { count: number };
-
- // Get database file size
- const dbPath = join(homedir(), '.claude-mem', 'claude-mem.db');
- let dbSize = 0;
- if (existsSync(dbPath)) {
- dbSize = statSync(dbPath).size;
- }
-
- db.close();
-
- // Get worker stats
- const uptime = process.uptime();
-
- res.json({
- worker: {
- version: VERSION,
- uptime: Math.floor(uptime),
- activeSessions: this.sessions.size,
- sseClients: this.sseClients.size,
- port: getWorkerPort()
- },
- database: {
- path: dbPath,
- size: dbSize,
- observations: obsCount.count,
- sessions: sessionCount.count,
- summaries: summaryCount.count
- }
- });
- } catch (error: any) {
- logger.error('WORKER', 'Failed to get stats', {}, error);
- res.status(500).json({ error: 'Failed to get stats' });
- }
-}
-```
-
-**Score**: 3/10
-**Why Slightly Stupid**:
-
-1. **Redundant existsSync Check**: The database path is guaranteed to exist if SessionStore initialized successfully. If it doesn't exist, SessionStore would have crashed on startup. This is defensive programming for ghosts.
-
-2. **Three Separate Queries**: Could be combined into a single query with UNION or multiple SELECT columns, but this is minor.
-
-**What Should Happen**:
-```typescript
-const dbSize = statSync(dbPath).size; // Just crash if it doesn't exist
-```
-
-Otherwise, this is mostly fine. Stats endpoints are low-frequency and non-critical.
-
----
-
-### Lines 507-555: GET /api/observations
-
-```typescript
-private handleGetObservations(req: Request, res: Response): void {
- try {
- const offset = parseInt(req.query.offset as string || '0', 10);
- const limit = Math.min(parseInt(req.query.limit as string || '50', 10), 100); // Cap at 100
- const project = req.query.project as string | undefined;
-
- const db = new SessionStore();
-
- // Build query with optional project filter
- let query = `
- SELECT id, type, title, subtitle, text, project, prompt_number, created_at, created_at_epoch
- FROM observations
- `;
- let countQuery = 'SELECT COUNT(*) as total FROM observations';
- const params: any[] = [];
- const countParams: any[] = [];
-
- if (project) {
- query += ' WHERE project = ?';
- countQuery += ' WHERE project = ?';
- params.push(project);
- countParams.push(project);
- }
-
- query += ' ORDER BY created_at_epoch DESC LIMIT ? OFFSET ?';
- params.push(limit, offset);
-
- const stmt = db.db.prepare(query);
- const observations = stmt.all(...params);
-
- // Check if there are more results
- const countStmt = db.db.prepare(countQuery);
- const { total } = countStmt.get(...countParams) as { total: number };
- const hasMore = (offset + limit) < total;
-
- db.close();
-
- res.json({
- observations,
- hasMore,
- total,
- offset,
- limit
- });
- } catch (error: any) {
- logger.error('WORKER', 'Failed to get observations', {}, error);
- res.status(500).json({ error: 'Failed to get observations' });
- }
-}
-```
-
-**Score**: 5/10
-**Why This Is Mildly Stupid**:
-
-1. **Duplicate Parameter Arrays**: `params` and `countParams` are maintained separately even though they contain the same values (just the project filter). This is error-prone and verbose.
-
-2. **Two Queries Instead of One**: We run a COUNT query and a SELECT query. For small datasets, this is fine, but for large datasets, the COUNT query can be expensive. The `hasMore` flag could be computed by fetching `limit + 1` rows and checking if we got more than `limit`.
-
-**What Should Happen**:
-```typescript
-// Fetch one extra row to determine if there are more results
-const stmt = db.db.prepare(query);
-const results = stmt.all(...params);
-const observations = results.slice(0, limit);
-const hasMore = results.length > limit;
-
-// Only run COUNT if the UI actually needs it (it probably doesn't)
-```
-
-**Pattern**: This same pattern is repeated in `handleGetSummaries` (line 557) and `handleGetPrompts` (line 618). Copy-paste code smell.
-
-**Estimated Savings**: Remove COUNT queries (which can be expensive on large tables), simplify parameter handling.
-
----
-
-### Lines 685-752: POST /sessions/:sessionDbId/init - DATABASE REOPENING HELL
-
-```typescript
-private async handleInit(req: Request, res: Response): Promise {
- const sessionDbId = parseInt(req.params.sessionDbId, 10);
- const { project } = req.body;
-
- logger.info('WORKER', 'Session init', { sessionDbId, project });
-
- const session = this.getOrCreateSession(sessionDbId); // <-- Opens DB at line 204
- const claudeSessionId = session.claudeSessionId;
-
- // Update port in database
- const db = new SessionStore(); // <-- Opens DB AGAIN
- db.setWorkerPort(sessionDbId, getWorkerPort());
-
- // Get the latest user_prompt for this session to sync to Chroma
- const latestPrompt = db.db.prepare(`
- SELECT
- up.*,
- s.sdk_session_id,
- s.project
- FROM user_prompts up
- JOIN sdk_sessions s ON up.claude_session_id = s.claude_session_id
- WHERE up.claude_session_id = ?
- ORDER BY up.created_at_epoch DESC
- LIMIT 1
- `).get(claudeSessionId) as any;
-
- db.close(); // <-- Closes DB
-
- // ... SSE broadcast ...
- // ... Chroma sync ...
-
- logger.success('WORKER', 'Session initialized', { sessionId: sessionDbId, port: getWorkerPort() });
- res.json({
- status: 'initialized',
- sessionDbId,
- port: getWorkerPort()
- });
-}
-```
-
-**Score**: 7/10
-**Why This Is Stupid**:
-
-1. **Two Database Opens in Same Function**:
- - Line 691: `getOrCreateSession()` opens DB internally (line 204)
- - Line 695: Opens DB AGAIN for `setWorkerPort()`
- - Line 711: Closes DB
-
-2. **Redundant Data Fetching**: `getOrCreateSession()` already fetches session data from the database (line 205). Then we query AGAIN for the user prompt (line 698).
-
-3. **Tight Coupling**: `getOrCreateSession()` hides database access, making it unclear that we're opening the database twice.
-
-**What Should Happen**:
-- Open database ONCE at the start of handleInit
-- Pass the open database to getOrCreateSession
-- Fetch all needed data in a single transaction
-- Close database at the end
-
-**Estimated Savings**: Eliminate 1 database open/close cycle (1-5ms).
-
----
-
-### Lines 728-741: Chroma Sync with Verbose Error Handling
-
-```typescript
-// Sync user prompt to Chroma (fire-and-forget, but crash on failure)
-if (latestPrompt) {
- this.chromaSync.syncUserPrompt(
- latestPrompt.id,
- latestPrompt.sdk_session_id,
- latestPrompt.project,
- latestPrompt.prompt_text,
- latestPrompt.prompt_number,
- latestPrompt.created_at_epoch
- ).catch(err => {
- logger.failure('WORKER', 'Failed to sync user_prompt to Chroma - continuing', { promptId: latestPrompt.id }, err);
- // Don't crash - SQLite has the data
- });
-}
-```
-
-**Score**: 5/10
-**Why This Is Mildly Stupid**:
-
-1. **Inconsistent Error Handling**: The comment says "crash on failure" but then we catch the error and continue. Which is it?
-
-2. **Redundant Comment**: The code says `.catch(err => { /* continue */ })` and the comment says "Don't crash - SQLite has the data". The code is self-documenting.
-
-3. **Fire-and-Forget**: If we're going to fire-and-forget, why bother with verbose error handling? Either care about failures (and retry/alert) or don't (and just log).
-
-**What Should Happen**:
-```typescript
-// Fire-and-forget Chroma sync (SQLite is source of truth)
-if (latestPrompt) {
- this.chromaSync.syncUserPrompt(/* ... */).catch(() => {}); // Swallow errors
-}
-```
-
-**Pattern**: This same verbose error handling appears in lines 1057-1076 and 1114-1133.
-
----
-
-### Lines 758-779: POST /sessions/:sessionDbId/observations
-
-```typescript
-private handleObservation(req: Request, res: Response): void {
- const sessionDbId = parseInt(req.params.sessionDbId, 10);
- const { tool_name, tool_input, tool_output, prompt_number } = req.body;
-
- const session = this.getOrCreateSession(sessionDbId); // <-- Opens DB
- const toolStr = logger.formatTool(tool_name, tool_input);
-
- logger.dataIn('WORKER', `Observation queued: ${toolStr}`, {
- sessionId: sessionDbId,
- queue: session.pendingMessages.length + 1
- });
-
- session.pendingMessages.push({
- type: 'observation',
- tool_name,
- tool_input,
- tool_output,
- prompt_number
- });
-
- res.json({ status: 'queued', queueLength: session.pendingMessages.length });
-}
-```
-
-**Score**: 6/10
-**Why This Is Stupid**:
-
-1. **Database Opens for No Reason**: `getOrCreateSession()` opens the database (line 204), but we don't actually need any data from the database here. We just need to get or create the in-memory session object.
-
-2. **Hot Path Performance**: This endpoint is called **for every single tool execution**. If you run 100 tool calls in a session, this opens/closes the database 100 times unnecessarily.
-
-**What Should Happen**:
-- Separate "get existing session" from "create session from database"
-- Only open database if creating a new session
-- For existing sessions, just push to the queue
-
-**Estimated Savings**: For a session with 100 observations, eliminate 99 unnecessary database open/close cycles (**99-495ms of pure overhead**).
-
----
-
-### Lines 914-1005: createMessageGenerator - THE POLLING HORROR
-
-```typescript
-private async* createMessageGenerator(session: ActiveSession): AsyncIterable {
- // ... send init prompt ...
-
- // Process messages continuously until session is deleted
- while (true) {
- if (session.abortController.signal.aborted) {
- break;
- }
-
- if (session.pendingMessages.length === 0) {
- await new Promise(resolve => setTimeout(resolve, MESSAGE_POLL_INTERVAL_MS));
- continue;
- }
-
- while (session.pendingMessages.length > 0) {
- const message = session.pendingMessages.shift()!;
- // ... process message ...
- yield { /* SDK message */ };
- }
- }
-}
-```
-
-**Score**: 10/10
-**Why This Is ABSOLUTELY FUCKING STUPID**:
-
-1. **Infinite Polling Loop**: Lines 936-944 implement a **busy-wait polling loop** that checks `pendingMessages.length` every 100ms. This is the single dumbest pattern in the entire file.
-
-2. **Event-Driven Alternative**: We have a fucking queue! When something is added to the queue, **NOTIFY THE CONSUMER**. Use an EventEmitter, a Promise, a Condition Variable, ANYTHING but polling.
-
-3. **Wasted CPU**: Every 100ms, this loop wakes up, checks if the queue is empty, and goes back to sleep. For a worker that runs for hours, this is thousands of unnecessary wake-ups.
-
-4. **Latency**: When an observation is queued (line 770), it sits in the queue for up to 100ms before being processed. **This adds 0-100ms of artificial latency to every single observation.**
-
-5. **Battery Impact**: On laptops, constant polling prevents CPU from entering deep sleep states, draining battery.
-
-**What Should Happen**:
-
-```typescript
-// In WorkerService class
-private sessionQueues: Map = new Map();
-
-private handleObservation(req: Request, res: Response): void {
- // ... existing code ...
- session.pendingMessages.push({ /* message */ });
-
- // Notify the generator that new work is available
- const emitter = this.sessionQueues.get(sessionDbId);
- if (emitter) {
- emitter.emit('message');
- }
-
- res.json({ status: 'queued', queueLength: session.pendingMessages.length });
-}
-
-private async* createMessageGenerator(session: ActiveSession): AsyncIterable {
- const emitter = new EventEmitter();
- this.sessionQueues.set(session.sessionDbId, emitter);
-
- yield { /* init prompt */ };
-
- while (!session.abortController.signal.aborted) {
- if (session.pendingMessages.length === 0) {
- // Wait for new messages via event, not polling
- await new Promise(resolve => emitter.once('message', resolve));
- }
-
- while (session.pendingMessages.length > 0) {
- const message = session.pendingMessages.shift()!;
- yield { /* process message */ };
- }
- }
-
- this.sessionQueues.delete(session.sessionDbId);
-}
-```
-
-**Estimated Savings**:
-- Remove 100ms polling interval (eliminate 0-100ms latency per observation)
-- Reduce CPU wake-ups from ~10/second to 0 when idle
-- Improve battery life on laptops
-- Make the system feel more responsive
-
-**Real-World Impact**: For a session with 10 observations, this polling adds **0-1000ms of cumulative latency**. The user is literally waiting for the polling loop to wake up.
-
----
-
-### Lines 1011-1146: handleAgentMessage - Database Reopening and Chroma Spam
-
-```typescript
-private handleAgentMessage(session: ActiveSession, content: string, promptNumber: number): void {
- // ... parse observations and summary ...
-
- const db = new SessionStore(); // <-- Opens DB
-
- // Store observations and sync to Chroma (non-blocking, fail-fast)
- for (const obs of observations) {
- const { id, createdAtEpoch } = db.storeObservation(/* ... */);
- logger.success('DB', 'Observation stored', { /* ... */ });
-
- // Broadcast to SSE clients
- this.broadcastSSE({ /* ... */ });
-
- // Sync to Chroma (non-blocking fire-and-forget, but crash on failure)
- this.chromaSync.syncObservation(/* ... */)
- .then(() => {
- logger.success('WORKER', 'Observation synced to Chroma', { /* ... */ });
- })
- .catch((error: Error) => {
- logger.error('WORKER', 'Observation sync failed - continuing', { /* ... */ }, error);
- // Don't crash - SQLite has the data
- });
- }
-
- // ... similar pattern for summary ...
-
- db.close(); // <-- Closes DB
-
- // Check if queue is empty and stop spinner after debounce
- this.checkAndStopSpinner(); // <-- Triggers 1.5s delay
-}
-```
-
-**Score**: 6/10
-**Why This Is Stupid**:
-
-1. **Database Reopening**: Opens database (line 1030), stores all observations, closes database (line 1142). This is called **for every SDK response**. For a session with 10 observations, this opens/closes the database 10+ times.
-
-2. **Verbose Chroma Error Handling**: Lines 1057-1076 and 1114-1133 have identical verbose error handling for Chroma sync failures. This is copy-paste code smell.
-
-3. **Success Logging Spam**: Line 1066 and 1123 log success for EVERY Chroma sync. For a session with 100 observations, this logs 100 success messages. Why? Who reads these?
-
-4. **Debounce Call**: Line 1145 calls `checkAndStopSpinner()`, triggering the 1.5s artificial delay.
-
-**What Should Happen**:
-- Reuse database connection across multiple calls
-- Simplify Chroma error handling (fire-and-forget means swallow errors)
-- Remove success logging (or make it debug-level)
-- Remove debounce delay
-
----
-
-## Summary of Patterns
-
-### 1. Database Reopening Anti-Pattern
-**Occurrences**: Lines 200-236, 685-752, 758-779, 1011-1146
-**Impact**: Opens/closes database 4-100+ times per session instead of reusing connections
-**Fix**: Pass open database connections between functions, use transactions, connection pooling
-
-### 2. Polling Instead of Events
-**Occurrences**: Line 942 (100ms polling loop)
-**Impact**: 0-100ms latency per observation, wasted CPU cycles, battery drain
-**Fix**: Use EventEmitter or async queue with await/notify pattern
-
-### 3. Artificial Delays
-**Occurrences**: Line 363 (1.5s spinner debounce), line 942 (100ms poll interval)
-**Impact**: 1.5s delay before spinner stops, 0-100ms delay per observation
-**Fix**: Remove debouncing, use event-driven patterns
-
-### 4. Premature Optimization
-**Occurrences**: Lines 33-70 (Claude path caching)
-**Impact**: 37 lines of code to save 5ms on a one-time operation
-**Fix**: Remove caching, inline the function
-
-### 5. Defensive Programming for Ghosts
-**Occurrences**: Line 382 (existsSync check), lines 228-231 (error handler reopens DB), lines 728-741 (verbose error handling)
-**Impact**: Code complexity without real benefit
-**Fix**: Fail fast, trust invariants, simplify error handling
-
-### 6. Copy-Paste Code
-**Occurrences**: handleGetObservations, handleGetSummaries, handleGetPrompts (nearly identical)
-**Impact**: Maintenance burden, inconsistency risk
-**Fix**: Extract common pagination logic into helper function
-
----
-
-## Recommendations
-
-### Immediate Wins (Low Effort, High Impact)
-
-1. **Remove Spinner Debounce** (Lines 338-365)
- - **Effort**: 5 minutes
- - **Impact**: Eliminate 1.5s artificial delay
- - **Score**: 9/10 stupidity
-
-2. **Replace Polling with Events** (Line 942)
- - **Effort**: 30 minutes
- - **Impact**: Eliminate 0-100ms latency per observation, reduce CPU usage
- - **Score**: 10/10 stupidity
-
-3. **Remove Claude Path Caching** (Lines 33-70)
- - **Effort**: 5 minutes
- - **Impact**: Remove 37 lines of unnecessary code
- - **Score**: 6/10 stupidity
-
-### Medium Wins (Moderate Effort, Good Impact)
-
-4. **Fix Database Reopening in Hot Path** (Lines 758-779)
- - **Effort**: 1 hour
- - **Impact**: Eliminate 99+ database cycles per session
- - **Score**: 6/10 stupidity
-
-5. **Simplify Chroma Error Handling** (Lines 728-741, 1057-1076, 1114-1133)
- - **Effort**: 15 minutes
- - **Impact**: Remove 50+ lines of verbose error handling
- - **Score**: 5/10 stupidity
-
-6. **Simplify SSE Broadcast** (Lines 297-322)
- - **Effort**: 5 minutes
- - **Impact**: Remove 10 lines, eliminate two-pass cleanup
- - **Score**: 4/10 stupidity
-
-### Long-Term Improvements (High Effort, Architectural)
-
-7. **Database Connection Pooling**
- - **Effort**: 4 hours
- - **Impact**: Reuse connections across requests, eliminate all open/close overhead
- - **Score**: 8/10 stupidity (current approach)
-
-8. **Extract Pagination Helper**
- - **Effort**: 1 hour
- - **Impact**: DRY up handleGetObservations/Summaries/Prompts
- - **Score**: 5/10 stupidity
-
----
-
-## Estimated Performance Impact
-
-**Current Hot Path (1 observation)**:
-- HTTP request arrives: 0ms
-- getOrCreateSession opens/closes DB: 1-5ms
-- Queue message: 0ms
-- Poll interval: 0-100ms (average 50ms)
-- SDK processing: variable
-- handleAgentMessage opens/closes DB: 1-5ms
-- Chroma sync (async): N/A
-- checkAndStopSpinner debounce: 1500ms
-- **Total artificial overhead**: 1502-1610ms (1.5-1.6 seconds)
-
-**Optimized Hot Path (1 observation)**:
-- HTTP request arrives: 0ms
-- Get existing session (no DB): 0ms
-- Queue message + notify: 0ms
-- SDK processing: variable
-- Store in DB (connection pool): 0.1-0.5ms
-- Chroma sync (async): N/A
-- Stop spinner (no debounce): 0ms
-- **Total artificial overhead**: 0.1-0.5ms
-
-**Speedup**: **3000-16000x faster** (removing artificial delays and polling)
-
----
-
-## Conclusion
-
-This file has accumulated significant technical debt in the form of:
-- **Artificial delays** (1.5s debounce, 100ms polling)
-- **Database reopening anti-pattern** (4-100+ opens per session)
-- **Polling instead of events** (busy-wait loop)
-- **Premature optimization** (caching rare operations)
-- **Defensive programming** (protecting against non-existent failures)
-
-The worker spends more time **waiting** (polling, debouncing) than **working**. Most of these patterns were likely added with good intentions ("make the UI smooth", "cache for performance", "handle errors gracefully") but ended up creating more problems than they solved.
-
-**Priority Fixes**:
-1. Remove spinner debounce (9/10 stupidity)
-2. Replace polling with events (10/10 stupidity)
-3. Fix database reopening in hot path (6-8/10 stupidity)
-
-These three changes alone would eliminate **1.5+ seconds of artificial delay** per session and make the system feel dramatically more responsive.
diff --git a/docs/context/worker-service-rewrite-outline.md b/docs/context/worker-service-rewrite-outline.md
deleted file mode 100644
index 0cd57c16..00000000
--- a/docs/context/worker-service-rewrite-outline.md
+++ /dev/null
@@ -1,1069 +0,0 @@
-# Worker Service Rewrite Blueprint
-
-**Date**: 2025-11-06
-**File**: `src/services/worker-service.ts`
-**Current State**: 1173 lines with significant technical debt
-**Target**: ~600-700 lines of event-driven, connection-pooled architecture
-
----
-
-## Core Principles
-
-### 1. Event-Driven, Not Polling
-- **NEVER** use `setTimeout` in a loop to check for work
-- Use EventEmitter or async queues with proper notification
-- Connections stay open, work is pushed not pulled
-- Zero artificial delays
-
-### 2. Keep Database Connections Open
-- Connection pool pattern (or single long-lived connection)
-- Pass connections as parameters, don't open/close per request
-- Transactions for related operations
-- Close only on shutdown
-
-### 3. Fail Fast, Not Defensive
-- If database doesn't exist, crash
-- If PM2 isn't available, crash
-- No "just in case" error handling
-- Trust invariants established at startup
-
-### 4. YAGNI - Delete Speculative Code
-- No caching for operations that happen once
-- No debouncing for problems that don't exist
-- No premature optimization
-- Write the obvious solution first
-
-### 5. DRY - Extract After Second Duplication
-- Not before
-- Identify patterns in existing code, don't scaffold frameworks
-- Shared logic = helper functions, not inheritance hierarchies
-
----
-
-## What Gets Deleted
-
-### Complete Removals (0 lines remaining)
-
-1. **Claude Path Caching (Lines 33-70)**
- - `cachedClaudePath` module-level state
- - `findClaudePath()` wrapper function
- - Replace with direct inline logic in `runSDKAgent()`
- - Saves: 37 lines
-
-2. **Spinner Debounce (Lines 338-365)**
- - `checkAndStopSpinner()` entire function
- - `spinnerStopTimer` class field
- - 1.5s artificial delay
- - Replace with immediate status broadcast
- - Saves: 28 lines + class field
-
-3. **Message Polling Loop (Line 942)**
- - `await new Promise(resolve => setTimeout(resolve, MESSAGE_POLL_INTERVAL_MS))`
- - `MESSAGE_POLL_INTERVAL_MS` constant
- - Replace with EventEmitter-based notification
- - Saves: 100ms latency per observation
-
-4. **Two-Pass SSE Cleanup (Lines 303-321)**
- - `clientsToRemove` temporary array
- - Second loop to remove clients
- - Duplicate disconnect logging
- - Replace with single-pass delete in try/catch
- - Saves: 10 lines
-
-5. **Defensive existsSync Checks**
- - Line 382: `if (existsSync(dbPath))` before statSync
- - Any other "just in case" file checks
- - Replace with direct calls that fail fast
- - Saves: 3-5 lines scattered
-
-6. **Verbose Chroma Error Handling**
- - Lines 728-741, 1057-1076, 1114-1133
- - Redundant `.then(() => logger.success(...))` calls
- - Verbose `.catch()` with comments
- - Replace with silent swallow or minimal logging
- - Saves: 40 lines
-
-### Partial Removals (simplifications)
-
-7. **Database Reopening Pattern**
- - Remove all `new SessionStore()` calls except initialization
- - Remove all `db.close()` calls except shutdown
- - Keep single connection pool or long-lived connection
- - Reduces open/close cycles from 100+ to 1 per worker lifetime
-
-8. **Duplicate Pagination Logic**
- - Extract `handleGetObservations`, `handleGetSummaries`, `handleGetPrompts` into single helper
- - Keep only endpoint-specific logic (table names, columns)
- - Saves: 60-80 lines
-
-9. **isProcessing Flag**
- - Derive from `sessions.size > 0` or `sessions.values().some(s => s.pendingMessages.length > 0)`
- - Remove class field
- - Saves: 1 field + related logic
-
----
-
-## What Gets Replaced
-
-### 1. Database Access Pattern
-
-**Before**: Open/close on every request
-```typescript
-private getOrCreateSession(sessionDbId: number): ActiveSession {
- const db = new SessionStore(); // Open
- const dbSession = db.getSessionById(sessionDbId);
- db.close(); // Close
- // ...
-}
-
-private handleInit(req: Request, res: Response): void {
- const session = this.getOrCreateSession(sessionDbId); // Open/close #1
- const db = new SessionStore(); // Open #2
- db.setWorkerPort(sessionDbId, port);
- db.close(); // Close #2
-}
-```
-
-**After**: Connection pool + pass connections
-```typescript
-class WorkerService {
- private db: SessionStore; // Long-lived connection
-
- async start(): Promise {
- this.db = new SessionStore(); // Open once
- // ...
- }
-
- private getOrCreateSession(sessionDbId: number): ActiveSession {
- // Use this.db, no open/close
- const dbSession = this.db.getSessionById(sessionDbId);
- // ...
- }
-
- private handleInit(req: Request, res: Response): void {
- const session = this.getOrCreateSession(sessionDbId);
- this.db.setWorkerPort(sessionDbId, port); // Reuse connection
- // No close
- }
-
- async shutdown(): Promise {
- this.db.close(); // Close once
- }
-}
-```
-
-### 2. Message Queue Pattern
-
-**Before**: Polling loop
-```typescript
-private async* createMessageGenerator(session: ActiveSession): AsyncIterable {
- yield initPrompt;
-
- while (true) {
- if (session.pendingMessages.length === 0) {
- await new Promise(resolve => setTimeout(resolve, 100)); // Poll
- continue;
- }
-
- while (session.pendingMessages.length > 0) {
- const message = session.pendingMessages.shift()!;
- yield processMessage(message);
- }
- }
-}
-```
-
-**After**: Event-driven notification
-```typescript
-class WorkerService {
- private sessionQueues: Map = new Map();
-
- private handleObservation(req: Request, res: Response): void {
- // ... existing logic ...
- session.pendingMessages.push(message);
-
- // Notify generator immediately
- const emitter = this.sessionQueues.get(sessionDbId);
- emitter?.emit('message');
-
- res.json({ status: 'queued' });
- }
-
- private async* createMessageGenerator(session: ActiveSession): AsyncIterable {
- const emitter = new EventEmitter();
- this.sessionQueues.set(session.sessionDbId, emitter);
-
- yield initPrompt;
-
- while (!session.abortController.signal.aborted) {
- if (session.pendingMessages.length === 0) {
- // Wait for notification, not poll
- await new Promise(resolve => emitter.once('message', resolve));
- }
-
- while (session.pendingMessages.length > 0) {
- const message = session.pendingMessages.shift()!;
- yield processMessage(message);
- }
- }
-
- this.sessionQueues.delete(session.sessionDbId);
- }
-}
-```
-
-### 3. Spinner Status Updates
-
-**Before**: 1.5s debounce
-```typescript
-private checkAndStopSpinner(): void {
- if (this.spinnerStopTimer) {
- clearTimeout(this.spinnerStopTimer);
- }
-
- const hasPending = Array.from(this.sessions.values()).some(
- s => s.pendingMessages.length > 0
- );
-
- if (!hasPending) {
- this.spinnerStopTimer = setTimeout(() => {
- // Check again after 1.5s
- const stillEmpty = Array.from(this.sessions.values()).every(
- s => s.pendingMessages.length === 0
- );
- if (stillEmpty) {
- this.broadcastProcessingStatus(false);
- }
- }, 1500);
- }
-}
-```
-
-**After**: Immediate status update
-```typescript
-private updateProcessingStatus(): void {
- const hasPending = Array.from(this.sessions.values()).some(
- s => s.pendingMessages.length > 0
- );
-
- this.broadcastProcessingStatus(hasPending);
-}
-```
-
-### 4. SSE Broadcast Cleanup
-
-**Before**: Two-pass cleanup
-```typescript
-private broadcastSSE(event: any): void {
- const clientsToRemove: Response[] = [];
-
- for (const client of this.sseClients) {
- try {
- client.write(data);
- } catch {
- clientsToRemove.push(client);
- }
- }
-
- for (const client of clientsToRemove) {
- this.sseClients.delete(client);
- }
-}
-```
-
-**After**: Single-pass delete
-```typescript
-private broadcastSSE(event: any): void {
- if (this.sseClients.size === 0) return;
-
- const data = `data: ${JSON.stringify(event)}\n\n`;
- for (const client of this.sseClients) {
- try {
- client.write(data);
- } catch {
- this.sseClients.delete(client); // Delete immediately
- }
- }
-}
-```
-
-### 5. Pagination Logic
-
-**Before**: Copy-paste across 3 endpoints
-```typescript
-private handleGetObservations(req: Request, res: Response): void {
- const offset = parseInt(req.query.offset as string || '0', 10);
- const limit = Math.min(parseInt(req.query.limit as string || '50', 10), 100);
- const project = req.query.project as string | undefined;
-
- const db = new SessionStore();
-
- let query = 'SELECT ... FROM observations';
- let countQuery = 'SELECT COUNT(*) FROM observations';
- const params: any[] = [];
- const countParams: any[] = [];
-
- if (project) {
- query += ' WHERE project = ?';
- countQuery += ' WHERE project = ?';
- params.push(project);
- countParams.push(project);
- }
-
- query += ' ORDER BY created_at_epoch DESC LIMIT ? OFFSET ?';
- params.push(limit, offset);
-
- const stmt = db.db.prepare(query);
- const observations = stmt.all(...params);
-
- const countStmt = db.db.prepare(countQuery);
- const { total } = countStmt.get(...countParams) as { total: number };
- const hasMore = (offset + limit) < total;
-
- db.close();
-
- res.json({ observations, hasMore, total, offset, limit });
-}
-
-// Identical pattern in handleGetSummaries and handleGetPrompts
-```
-
-**After**: Extract helper function
-```typescript
-private paginate(
- table: string,
- columns: string,
- project: string | undefined,
- offset: number,
- limit: number
-): { items: T[]; hasMore: boolean; total: number } {
- let query = `SELECT ${columns} FROM ${table}`;
- const params: any[] = [];
-
- if (project) {
- query += ' WHERE project = ?';
- params.push(project);
- }
-
- query += ' ORDER BY created_at_epoch DESC LIMIT ? OFFSET ?';
- params.push(limit + 1, offset); // Fetch one extra to check hasMore
-
- const stmt = this.db.db.prepare(query);
- const results = stmt.all(...params) as T[];
-
- const items = results.slice(0, limit);
- const hasMore = results.length > limit;
-
- // Optional: Only compute total if needed
- // const total = this.db.db.prepare(`SELECT COUNT(*) as count FROM ${table}...`).get().count;
-
- return { items, hasMore, total: -1 }; // Or compute total if UI needs it
-}
-
-private handleGetObservations(req: Request, res: Response): void {
- const offset = parseInt(req.query.offset as string || '0', 10);
- const limit = Math.min(parseInt(req.query.limit as string || '50', 10), 100);
- const project = req.query.project as string | undefined;
-
- const { items, hasMore } = this.paginate(
- 'observations',
- 'id, type, title, subtitle, text, project, prompt_number, created_at, created_at_epoch',
- project,
- offset,
- limit
- );
-
- res.json({ observations: items, hasMore, offset, limit });
-}
-```
-
-### 6. Claude Path Resolution
-
-**Before**: 37 lines with caching
-```typescript
-let cachedClaudePath: string | null = null;
-
-function findClaudePath(): string {
- if (cachedClaudePath) {
- return cachedClaudePath;
- }
-
- // Check environment variable
- if (process.env.CLAUDE_CODE_PATH) {
- cachedClaudePath = process.env.CLAUDE_CODE_PATH;
- return cachedClaudePath;
- }
-
- // Find in PATH
- try {
- const command = process.platform === 'win32' ? 'where claude' : 'which claude';
- const result = execSync(command, { encoding: 'utf8' });
- const paths = result.trim().split('\n');
- const claudePath = paths[0].trim();
-
- if (!claudePath) {
- throw new Error('Claude executable not found');
- }
-
- cachedClaudePath = claudePath;
- return cachedClaudePath;
- } catch {
- throw new Error('Failed to find Claude executable');
- }
-}
-```
-
-**After**: Inline in runSDKAgent (called once per session)
-```typescript
-private async runSDKAgent(session: ActiveSession): Promise {
- const claudePath = process.env.CLAUDE_CODE_PATH ||
- execSync(process.platform === 'win32' ? 'where claude' : 'which claude', { encoding: 'utf8' })
- .trim().split('\n')[0].trim();
-
- if (!claudePath) {
- throw new Error('Claude executable not found in PATH');
- }
-
- // ... rest of runSDKAgent ...
-}
-```
-
-### 7. Chroma Sync Error Handling
-
-**Before**: Verbose logging
-```typescript
-this.chromaSync.syncObservation(...)
- .then(() => {
- logger.success('WORKER', 'Observation synced to Chroma', { obsId: id });
- })
- .catch((error: Error) => {
- logger.error('WORKER', 'Observation sync failed - continuing', { obsId: id }, error);
- // Don't crash - SQLite has the data
- });
-```
-
-**After**: Silent or minimal
-```typescript
-// Fire-and-forget (SQLite is source of truth)
-this.chromaSync.syncObservation(...).catch(() => {});
-
-// Or minimal logging at debug level
-this.chromaSync.syncObservation(...).catch(err =>
- logger.debug('WORKER', 'Chroma sync failed', {}, err)
-);
-```
-
----
-
-## New Architecture
-
-### Class Structure
-
-```typescript
-class WorkerService {
- // Core services
- private app: express.Application;
- private db: SessionStore; // Long-lived connection
- private chromaSync: ChromaSync;
-
- // Session management
- private sessions: Map = new Map();
- private sessionQueues: Map = new Map();
-
- // SSE clients
- private sseClients: Set = new Set();
-
- constructor() {
- this.app = express();
- this.setupMiddleware();
- this.setupRoutes();
- }
-
- async start(): Promise {
- // Initialize database (once)
- this.db = new SessionStore();
-
- // Initialize ChromaSync
- this.chromaSync = new ChromaSync('claude-mem');
-
- // Cleanup orphaned sessions
- const cleaned = this.db.cleanupOrphanedSessions();
- if (cleaned > 0) {
- logger.info('SYSTEM', `Cleaned ${cleaned} orphaned sessions`);
- }
-
- // Start HTTP server
- const port = getWorkerPort();
- await new Promise((resolve, reject) => {
- this.app.listen(port, resolve).on('error', reject);
- });
-
- logger.info('SYSTEM', 'Worker started', { port, pid: process.pid });
-
- // Start Chroma backfill (fire-and-forget)
- this.chromaSync.ensureBackfilled().catch(() => {});
- }
-
- async shutdown(): Promise {
- // Abort all active sessions
- for (const session of this.sessions.values()) {
- session.abortController.abort();
- }
-
- // Wait for generators to finish
- await Promise.all(
- Array.from(this.sessions.values())
- .map(s => s.generatorPromise)
- .filter(Boolean)
- );
-
- // Close database
- this.db.close();
-
- logger.info('SYSTEM', 'Worker shutdown complete');
- }
-}
-```
-
-### ActiveSession Interface
-
-```typescript
-interface ActiveSession {
- sessionDbId: number;
- claudeSessionId: string;
- sdkSessionId: string | null;
- project: string;
- userPrompt: string;
- pendingMessages: PendingMessage[];
- abortController: AbortController;
- generatorPromise: Promise | null;
- lastPromptNumber: number;
- startTime: number;
-}
-
-interface PendingMessage {
- type: 'observation' | 'summarize';
- tool_name?: string;
- tool_input?: any;
- tool_output?: any;
- prompt_number?: number;
-}
-```
-
----
-
-## Initialization Flow
-
-```
-1. Constructor
- └─ new express()
- └─ setupMiddleware()
- └─ setupRoutes()
-
-2. start()
- ├─ new SessionStore() → this.db (STAYS OPEN)
- ├─ new ChromaSync()
- ├─ db.cleanupOrphanedSessions()
- ├─ app.listen(port)
- └─ chromaSync.ensureBackfilled() (async, fire-and-forget)
-
-3. Ready to accept requests
- └─ Database connection: OPEN
- └─ HTTP server: LISTENING
- └─ ChromaSync: INITIALIZED
-```
-
-**Key Changes**:
-- Database opened ONCE, stays open for worker lifetime
-- No version checks, no npm install logic (move to separate install script)
-- ChromaSync backfill doesn't block startup
-- Clean startup path: construct → start → ready
-
----
-
-## Request Flow
-
-### POST /sessions/:sessionDbId/init
-
-```
-1. Parse sessionDbId from URL params
-2. Get or create session:
- ├─ Check this.sessions.get(sessionDbId)
- ├─ If exists: return existing
- └─ If not exists:
- ├─ Fetch session from this.db (connection already open)
- ├─ Create ActiveSession object
- ├─ Create EventEmitter for queue
- ├─ this.sessions.set(sessionDbId, session)
- ├─ this.sessionQueues.set(sessionDbId, emitter)
- └─ Start runSDKAgent(session) in background
-
-3. Update session in database:
- ├─ this.db.setWorkerPort(sessionDbId, port)
- ├─ Fetch latest user prompt (already have connection)
- └─ NO CLOSE - connection stays open
-
-4. Broadcast SSE event (new session started)
-
-5. Fire-and-forget Chroma sync:
- └─ chromaSync.syncUserPrompt(...).catch(() => {})
-
-6. Return response:
- └─ { status: 'initialized', sessionDbId, port }
-```
-
-**Performance**:
-- Database: 0 open/close cycles (connection already open)
-- Latency: ~1-2ms (just a SELECT and UPDATE)
-
-### POST /sessions/:sessionDbId/observations
-
-```
-1. Parse sessionDbId and observation data
-2. Get session (from this.sessions, NO database access)
-3. Push message to session.pendingMessages queue
-4. Notify generator immediately:
- └─ sessionQueues.get(sessionDbId)?.emit('message')
-5. Return response:
- └─ { status: 'queued', queueLength: session.pendingMessages.length }
-```
-
-**Performance**:
-- Database: 0 accesses (session already in memory)
-- Latency: <1ms (just queue push + emit)
-- Generator latency: 0ms (wakes up immediately on emit, not 0-100ms poll)
-
-### SDK Agent Processing (runSDKAgent)
-
-```
-1. Create EventEmitter for this session's queue
-2. Create async generator (createMessageGenerator):
- ├─ Yield init prompt
- └─ Loop:
- ├─ If queue empty: await emitter.once('message')
- ├─ If queue has messages: process all
- └─ Yield SDK messages
-
-3. Run Agent SDK with generator:
- └─ For each response from SDK:
- ├─ Parse observations/summary
- ├─ Store in database (this.db, connection open)
- ├─ Broadcast SSE events
- ├─ Fire-and-forget Chroma sync
- └─ Update processing status (immediate, no debounce)
-
-4. On completion or error:
- ├─ Mark session complete in database
- ├─ Delete from this.sessions
- ├─ Delete from this.sessionQueues
- └─ Broadcast final status
-```
-
-**Performance**:
-- Message latency: 0ms (event-driven, not polled)
-- Database overhead: 1 connection for entire session
-- Spinner updates: Immediate (no 1.5s delay)
-
----
-
-## Session Lifecycle
-
-```
-[Client] POST /init
- ↓
-[Worker] Create ActiveSession
- ├─ Fetch from database (this.db)
- ├─ Store in this.sessions
- ├─ Create EventEmitter in this.sessionQueues
- └─ Start runSDKAgent() background task
- ↓
-[Worker] runSDKAgent spawns claude subprocess
- ├─ Creates message generator
- └─ Generator waits for events (not polling)
- ↓
-[Client] POST /observations (multiple times)
- ├─ Push to session.pendingMessages
- └─ Emit 'message' event → generator wakes immediately
- ↓
-[SDK Agent] Processes observations
- ├─ Stores in database (this.db)
- ├─ Syncs to Chroma (async)
- └─ Broadcasts SSE events
- ↓
-[Client] POST /summarize
- ├─ Push to session.pendingMessages
- └─ Emit 'message' event
- ↓
-[SDK Agent] Generates summary
- ├─ Stores in database
- ├─ Syncs to Chroma
- └─ Broadcasts SSE event
- ↓
-[SDK Agent] Session ends
- ├─ Mark complete in database
- ├─ Delete from this.sessions
- └─ Delete from this.sessionQueues
-
-[Cleanup Hook] DELETE /sessions/:id
- ├─ Abort session (abortController.abort())
- ├─ Wait for generator to finish
- └─ Return success
-```
-
-**Key Points**:
-- Database connection: Open for entire worker lifetime
-- EventEmitter: Created per session, deleted on completion
-- No polling loops anywhere
-- No artificial delays
-- Generator responds to events in real-time
-
----
-
-## Event System Design
-
-### EventEmitter Per Session
-
-```typescript
-class WorkerService {
- private sessionQueues: Map = new Map();
-
- private getOrCreateSession(sessionDbId: number): ActiveSession {
- let session = this.sessions.get(sessionDbId);
- if (session) return session;
-
- // Fetch from database
- const dbSession = this.db.getSessionById(sessionDbId);
- if (!dbSession) {
- throw new Error(`Session ${sessionDbId} not found`);
- }
-
- // Create session object
- session = {
- sessionDbId,
- claudeSessionId: dbSession.claude_session_id,
- sdkSessionId: null,
- project: dbSession.project,
- userPrompt: dbSession.user_prompt,
- pendingMessages: [],
- abortController: new AbortController(),
- generatorPromise: null,
- lastPromptNumber: 0,
- startTime: Date.now()
- };
-
- this.sessions.set(sessionDbId, session);
-
- // Create EventEmitter for queue notifications
- const emitter = new EventEmitter();
- this.sessionQueues.set(sessionDbId, emitter);
-
- // Start background processing
- session.generatorPromise = this.runSDKAgent(session).catch(err => {
- logger.failure('WORKER', 'SDK agent error', { sessionId: sessionDbId }, err);
- this.db.markSessionFailed(sessionDbId);
- this.sessions.delete(sessionDbId);
- this.sessionQueues.delete(sessionDbId);
- });
-
- return session;
- }
-
- private handleObservation(req: Request, res: Response): void {
- const sessionDbId = parseInt(req.params.sessionDbId, 10);
- const { tool_name, tool_input, tool_output, prompt_number } = req.body;
-
- const session = this.getOrCreateSession(sessionDbId);
-
- session.pendingMessages.push({
- type: 'observation',
- tool_name,
- tool_input,
- tool_output,
- prompt_number
- });
-
- // Notify generator immediately (no polling delay)
- const emitter = this.sessionQueues.get(sessionDbId);
- if (emitter) {
- emitter.emit('message');
- }
-
- res.json({ status: 'queued', queueLength: session.pendingMessages.length });
- }
-
- private async* createMessageGenerator(session: ActiveSession): AsyncIterable {
- // Get the EventEmitter for this session
- const emitter = this.sessionQueues.get(session.sessionDbId);
- if (!emitter) {
- throw new Error(`No emitter found for session ${session.sessionDbId}`);
- }
-
- // Yield initial prompt
- yield {
- role: 'user',
- content: session.userPrompt
- };
-
- // Process messages as they arrive (event-driven)
- while (!session.abortController.signal.aborted) {
- // If queue is empty, wait for notification
- if (session.pendingMessages.length === 0) {
- await new Promise(resolve => {
- const handler = () => resolve();
- emitter.once('message', handler);
-
- // Also listen for abort signal
- session.abortController.signal.addEventListener('abort', () => {
- emitter.off('message', handler);
- resolve();
- }, { once: true });
- });
- }
-
- // Process all pending messages
- while (session.pendingMessages.length > 0) {
- const message = session.pendingMessages.shift()!;
-
- if (message.type === 'observation') {
- yield {
- role: 'user',
- content: `
-${message.tool_name}
-${JSON.stringify(message.tool_input)}
-${message.tool_output}
-
-Please analyze this tool execution and extract observations.`
- };
- } else if (message.type === 'summarize') {
- yield {
- role: 'user',
- content: `Please summarize this session.`
- };
- }
- }
- }
- }
-}
-```
-
-### Benefits
-
-1. **Zero Polling Delay**: Generator wakes up immediately when work arrives
-2. **Clean Separation**: Each session has its own event channel
-3. **Abort Handling**: EventEmitter can be aborted cleanly
-4. **No Timers**: No `setTimeout`, no `setInterval`, no `MESSAGE_POLL_INTERVAL_MS`
-5. **Responsive**: User sees processing start instantly, not after 0-100ms poll delay
-
-### Alternative: Async Queue
-
-If EventEmitter feels too imperative, consider an async queue library:
-
-```typescript
-import { Queue } from 'async-queue'; // Or similar library
-
-class WorkerService {
- private sessionQueues: Map> = new Map();
-
- private handleObservation(req: Request, res: Response): void {
- const queue = this.sessionQueues.get(sessionDbId);
- queue.enqueue(message); // Automatically notifies consumers
- res.json({ status: 'queued' });
- }
-
- private async* createMessageGenerator(session: ActiveSession): AsyncIterable {
- const queue = this.sessionQueues.get(session.sessionDbId);
-
- yield initPrompt;
-
- while (!session.abortController.signal.aborted) {
- const message = await queue.dequeue(); // Blocks until work available
- yield processMessage(message);
- }
- }
-}
-```
-
-Choose whichever pattern is clearest for the use case. **The key principle is: events, not polling.**
-
----
-
-## Helper Functions to Extract
-
-### 1. Pagination Helper
-
-```typescript
-private paginate(
- table: string,
- columns: string,
- project: string | undefined,
- offset: number,
- limit: number
-): { items: T[]; hasMore: boolean } {
- let query = `SELECT ${columns} FROM ${table}`;
- const params: any[] = [];
-
- if (project) {
- query += ' WHERE project = ?';
- params.push(project);
- }
-
- query += ' ORDER BY created_at_epoch DESC LIMIT ? OFFSET ?';
- params.push(limit + 1, offset); // Fetch one extra
-
- const stmt = this.db.db.prepare(query);
- const results = stmt.all(...params) as T[];
-
- return {
- items: results.slice(0, limit),
- hasMore: results.length > limit
- };
-}
-```
-
-### 2. SSE Event Builder
-
-```typescript
-private createSSEEvent(type: string, data: any): string {
- return `data: ${JSON.stringify({ type, ...data, timestamp: Date.now() })}\n\n`;
-}
-```
-
-### 3. Request Parsing
-
-```typescript
-private parsePaginationParams(req: Request): { offset: number; limit: number; project?: string } {
- return {
- offset: parseInt(req.query.offset as string || '0', 10),
- limit: Math.min(parseInt(req.query.limit as string || '50', 10), 100),
- project: req.query.project as string | undefined
- };
-}
-```
-
----
-
-## Estimated Line Count
-
-### Current: 1173 lines
-
-**Deletions**:
-- Claude path caching: -37 lines
-- Spinner debounce: -28 lines
-- Two-pass SSE cleanup: -10 lines
-- Verbose Chroma error handling: -40 lines
-- Defensive checks: -5 lines
-- **Subtotal deletions**: -120 lines
-
-**Simplifications** (replace verbose with simple):
-- Duplicate pagination logic: -80 lines (3 endpoints → 1 helper)
-- Database reopening pattern: -50 lines (remove redundant open/close)
-- **Subtotal simplifications**: -130 lines
-
-**Additions** (new patterns):
-- EventEmitter setup: +20 lines
-- Connection pool management: +10 lines
-- Helper functions: +30 lines
-- **Subtotal additions**: +60 lines
-
-### Target: ~983 lines (1173 - 120 - 130 + 60)
-
-**Realistically**: ~600-700 lines after aggressive cleanup and extraction of helpers.
-
----
-
-## Testing Strategy
-
-### Before Rewrite
-1. Document current behavior with integration tests
-2. Capture expected HTTP responses for all endpoints
-3. Measure baseline performance (latency, throughput)
-
-### During Rewrite
-1. Rewrite in isolated branch
-2. Run integration tests after each major change
-3. Ensure HTTP contract remains identical
-
-### After Rewrite
-1. Performance comparison:
- - Measure latency per observation (should drop from 50-100ms to <5ms)
- - Measure spinner delay (should drop from 1.5s to 0ms)
- - Measure database overhead (should drop 90%+)
-2. Load testing: 100 concurrent sessions, 1000 observations
-3. Memory profiling: Ensure no EventEmitter leaks
-
----
-
-## Migration Checklist
-
-- [ ] Extract current integration tests
-- [ ] Create new branch: `rewrite/worker-service`
-- [ ] Rewrite constructor and initialization
-- [ ] Replace database pattern (connection pool)
-- [ ] Replace polling with EventEmitter
-- [ ] Remove spinner debounce
-- [ ] Simplify SSE broadcast
-- [ ] Extract pagination helper
-- [ ] Simplify Chroma error handling
-- [ ] Remove Claude path caching
-- [ ] Add shutdown handler
-- [ ] Run integration tests
-- [ ] Performance benchmarks
-- [ ] Code review
-- [ ] Merge to main
-
----
-
-## Success Metrics
-
-### Performance
-- **Observation latency**: <5ms (down from 50-100ms)
-- **Spinner delay**: 0ms (down from 1500ms)
-- **Database open/close cycles**: 1 per worker lifetime (down from 100+ per session)
-
-### Code Quality
-- **Total lines**: <700 (down from 1173)
-- **Artificial delays**: 0 (down from 2)
-- **Polling loops**: 0 (down from 1)
-- **Cyclomatic complexity**: <15 per function
-
-### Maintainability
-- **DRY**: No copy-paste pagination logic
-- **Fail Fast**: No defensive programming for ghosts
-- **YAGNI**: No premature optimization or speculative features
-- **Event-Driven**: All async work uses proper notification patterns
-
----
-
-## Appendix: Key Insights from Overhead Analysis
-
-### The Three Deadly Sins
-
-1. **Polling Instead of Events** (Line 942)
- - Adds 0-100ms latency to every observation
- - Wakes CPU every 100ms even when idle
- - Prevents laptop deep sleep → drains battery
-
-2. **Artificial Debouncing** (Lines 338-365)
- - Adds 1.5s delay before spinner stops
- - Solves a problem that doesn't exist (UI flickering)
- - Makes the entire system feel slower
-
-3. **Database Reopening** (Multiple locations)
- - Opens/closes database 4-100+ times per session
- - Adds 1-5ms overhead per cycle
- - Total overhead: 20-500ms per session of pure waste
-
-### Why These Patterns Appeared
-
-- **Training Bias**: "Professional" code often looks more complex
-- **Risk Aversion**: "What if X fails?" even when X can't fail
-- **Pattern Matching**: Seeing a problem and scaffolding a framework
-- **No Real-World Pain**: Not debugging at 2am = not feeling cost of complexity
-
-### The Fix
-
-- Write the obvious solution first
-- Add complexity only when you hit the actual problem
-- Delete aggressively
-- Trust invariants
-- Fail fast
diff --git a/experiment/README.md b/experiment/README.md
deleted file mode 100644
index 9ae6def9..00000000
--- a/experiment/README.md
+++ /dev/null
@@ -1,53 +0,0 @@
-# Chroma MCP Experiment
-
-This directory contains experimental scripts to test semantic search via ChromaDB without modifying production code.
-
-## Files
-
-- **chroma-sync-experiment.ts** - Syncs SQLite observations/summaries to ChromaDB via Chroma MCP tools
-- **chroma-search-test.ts** - Compares semantic search (Chroma) vs keyword search (FTS5)
-- **RESULTS.md** - Document findings and make decision on production integration
-
-## Prerequisites
-
-1. Chroma MCP server configured in Claude settings
-2. Running: `uvx chroma-mcp --client-type persistent --data-dir ~/.claude-mem/vector-db`
-
-## Running the Experiment
-
-### Step 1: Sync Data
-```bash
-npx tsx experiment/chroma-sync-experiment.ts
-```
-
-This will:
-- Connect to your Chroma MCP server
-- Create collection `cm__claude-mem`
-- Sync all observations and sessions from SQLite
-- Report sync statistics
-
-### Step 2: Test Search
-```bash
-npx tsx experiment/chroma-search-test.ts
-```
-
-This will:
-- Run 8 test queries (4 semantic, 4 keyword)
-- Compare Chroma semantic search vs FTS5 keyword search
-- Display results side-by-side
-
-### Step 3: Document Results
-Edit `RESULTS.md` with your findings:
-- Which queries worked better with semantic search?
-- Which worked better with keyword search?
-- Is hybrid search worth the complexity?
-
-## Decision Point
-
-Based on results:
-- **If semantic search provides significant value**: Design production integration
-- **If FTS5 is sufficient**: Keep current implementation, document why
-
-## Note
-
-This is a **pure experiment** - no production code changes. All scripts are self-contained in this directory.
diff --git a/experiment/RESULTS.md b/experiment/RESULTS.md
deleted file mode 100644
index 455bdb39..00000000
--- a/experiment/RESULTS.md
+++ /dev/null
@@ -1,216 +0,0 @@
-# Chroma MCP Search Experiment Results
-
-**Date**: 2025-11-01T03:14:23.093Z
-**Project**: claude-mem
-**Collection**: cm__claude-mem
-
-## Summary
-
-- **Semantic Search (Chroma)**: 8/8 queries succeeded (100%)
-- **Keyword Search (FTS5)**: 5/8 queries succeeded (63%)
-
-## Key Findings
-
-✅ **Semantic search outperformed keyword search by 3 queries.**
-
-Chroma's vector embeddings successfully handled conceptual queries that FTS5 completely missed. For queries requiring semantic understanding rather than exact keyword matching, Chroma is clearly superior.
-
-## Detailed Results
-
-### 1. Semantic - conceptual understanding
-
-**Query**: `how does memory compression work`
-**Expected Best**: semantic
-
-#### 🔵 Semantic Search (Chroma)
-
-**Status**: ❌ No results
-
-#### 🟡 Keyword Search (FTS5)
-
-**Status**: ❌ No results
-
----
-
-### 2. Semantic - similar patterns
-
-**Query**: `problems with database synchronization`
-**Expected Best**: semantic
-
-#### 🔵 Semantic Search (Chroma)
-
-**Status**: ❌ No results
-
-#### 🟡 Keyword Search (FTS5)
-
-**Status**: ✅ Found 2 results
-
-**Result 1: Search Type Categories Tested: Mechanism, Problem-Solution, and Pattern Queries** (discovery)
-
-```
-The session systematically tested both search systems against diverse query types to understand search quality and relevance capabilities. Three primary categories emerged: (1) mechanism/how-to questions seeking explanations of system behavior, (2) problem-solution queries focused on troubleshooting and bug fixes, and (3) pattern/best-practice questions for architectural guidance. Additional testing included specific technical domain queries (context injection, PM2, FTS5) and operational queries (versioning, configuration, error handling). This taxonomy of query types provides a framework for evaluating and comparing search system quality across different information-seeking needs.
-```
-
-**Result 2: Semantic search (Chroma) superior to keyword search (FTS5) for memory queries** (discovery)
-
-```
-Testing revealed that semantic search via Chroma vastly outperforms traditional full-text search (FTS5) for the memory system use case. Across 8 diverse test queries, Chroma found relevant results in every case while FTS5 succeeded only 38% of the time. The gap is most pronounced for conceptual queries: FTS5 has no mechanism to understand queries like "problems with database synchronization" or "patterns for background workers" without exact keyword matches. Chroma, using vector embeddings, correctly interpreted semantic intent and returned highly relevant results even when exact phrases didn't appear in the database. For exact-match queries, both performed well, but Chroma ranked results by semantic relevance rather than just text occurrence. This data demonstrates semantic search should be the primary interface for memory retrieval.
-```
-
----
-
-### 3. Keyword - specific file
-
-**Query**: `SessionStore.ts`
-**Expected Best**: keyword
-
-#### 🔵 Semantic Search (Chroma)
-
-**Status**: ❌ No results
-
-#### 🟡 Keyword Search (FTS5)
-
-**Status**: ✅ Found 3 results
-
-**Result 1: Search for observations referencing "SessionStore.ts" returned no results** (discovery)
-
-```
-A search was performed to find observations and sessions that reference the file path "SessionStore.ts" using the find_by_file tool, limiting results to 5 items. The empty result indicates that no observations or sessions have documented work touching this file yet. This could mean that SessionStore.ts-related changes either haven't been recorded as observations, or the file hasn't been included in any stored observation file references.
-```
-
-**Result 2: Session Store File Location** (discovery)
-
-```
-Located SessionStore.ts which is the database abstraction layer for session persistence. This file likely contains the problematic validation logic that checks for a parent session ID before saving a session. The issue described requires modification to this file to use the session ID from the hook directly without validating parent session relationships.
-```
-
-**Result 3: SessionStore.ts Method Definition Search** (discovery)
-
-```
-Continuing investigation into SessionStore.ts to locate the method definitions. The file appears to have content issues or is structured differently than expected, as multiple read attempts at different line ranges are returning no output. This is problematic because the simplified new-hook.ts now depends on createSDKSession existing and functioning properly without validation checks.
-```
-
----
-
-### 4. Keyword - exact function name
-
-**Query**: `getAllObservations`
-**Expected Best**: keyword
-
-#### 🔵 Semantic Search (Chroma)
-
-**Status**: ❌ No results
-
-#### 🟡 Keyword Search (FTS5)
-
-**Status**: ✅ Found 3 results
-
-**Result 1: Chroma sync experiment missing getAllObservations method on store** (bugfix)
-
-```
-The Chroma MCP sync experiment script connects successfully to Chroma and creates a collection named cm__claude-mem, but fails when attempting to read observations from SQLite. The store object lacks the getAllObservations method, preventing the script from retrieving stored observations to sync with Chroma. This method needs to be implemented to enable the full sync workflow from SQLite to vector database.
-```
-
-**Result 2: Chroma sync experiment updated to bypass missing getAllObservations method** (bugfix)
-
-```
-The Chroma sync experiment script was fixed by replacing the unimplemented getAllObservations() method call with a direct SQL query using the SessionStore's db property. This allows the script to retrieve observations from SQLite and continue with the Chroma sync workflow. The fix is a temporary workaround until the getAllObservations method is properly implemented in the SessionStore class.
-```
-
-**Result 3: SessionStore implementation missing getAllObservations method** (discovery)
-
-```
-The SessionStore class in src/services/sqlite/SessionStore.ts does not implement the getAllObservations method that the Chroma sync experiment depends on. The experiment script successfully connects to Chroma MCP and creates a collection, but fails when attempting to retrieve observations from SQLite storage. The missing method prevents the sync system from transferring stored observations into the vector database for semantic search capabilities.
-```
-
----
-
-### 5. Both - technical concept with specifics
-
-**Query**: `FTS5 full text search implementation`
-**Expected Best**: both
-
-#### 🔵 Semantic Search (Chroma)
-
-**Status**: ❌ No results
-
-#### 🟡 Keyword Search (FTS5)
-
-**Status**: ❌ No results
-
----
-
-### 6. Semantic - user intent
-
-**Query**: `similar to context injection issues`
-**Expected Best**: semantic
-
-#### 🔵 Semantic Search (Chroma)
-
-**Status**: ❌ No results
-
-#### 🟡 Keyword Search (FTS5)
-
-**Status**: ✅ Found 1 results
-
-**Result 1: Semantic search (Chroma) superior to keyword search (FTS5) for memory queries** (discovery)
-
-```
-Testing revealed that semantic search via Chroma vastly outperforms traditional full-text search (FTS5) for the memory system use case. Across 8 diverse test queries, Chroma found relevant results in every case while FTS5 succeeded only 38% of the time. The gap is most pronounced for conceptual queries: FTS5 has no mechanism to understand queries like "problems with database synchronization" or "patterns for background workers" without exact keyword matches. Chroma, using vector embeddings, correctly interpreted semantic intent and returned highly relevant results even when exact phrases didn't appear in the database. For exact-match queries, both performed well, but Chroma ranked results by semantic relevance rather than just text occurrence. This data demonstrates semantic search should be the primary interface for memory retrieval.
-```
-
----
-
-### 7. Keyword - specific error
-
-**Query**: `NOT NULL constraint violation`
-**Expected Best**: keyword
-
-#### 🔵 Semantic Search (Chroma)
-
-**Status**: ❌ No results
-
-#### 🟡 Keyword Search (FTS5)
-
-**Status**: ✅ Found 3 results
-
-**Result 1: Critical: NOT NULL constraint violation on sdk_sessions.claude_session_id** (bugfix)
-
-```
-The claude-mem-worker is failing to properly initialize sessions because the application code is attempting to persist a session record to the database without setting the required claude_session_id field. The logs show claudeSessionId=undefined being logged during init prompt send, indicating the field is not being populated before database insertion. This causes a NOT NULL constraint violation in the sdk_sessions table. As a cascading effect, the system receives empty responses from the API and the response parser cannot extract summary tags from the malformed content.
-```
-
-**Result 2: Cleaned up v4.0.0 section in CLAUDE.md to minimal highlight** (change)
-
-```
-The v4.0.0 section in CLAUDE.md was further condensed by removing the detailed NOT NULL constraint bugfix explanation, technical implementation details about SessionStore, and file change listings. Only the high-level features (MCP Search Server with FTS5, plugin data directory integration, and HTTP REST API with PM2) remain as a brief three-line summary. This completes the consolidation of CLAUDE.md's Version History section into a lean recent highlights view, with all comprehensive documentation now exclusively in CHANGELOG.md.
-```
-
-**Result 3: Critical Fix: NOT NULL Constraint Violation in Session ID Flow** (bugfix)
-
-```
-A critical bug prevented observations and summaries from being stored to the database. The root cause was that SessionStore.getSessionById() was not selecting the claude_session_id column from the database query. This caused the worker service to receive undefined for claude_session_id when initializing sessions, leading to NOT NULL constraint violations on database inserts. The fix involved adding claude_session_id to the SELECT query and updating the return type signature to include this field. This ensures the session ID from hooks flows correctly through the entire pipeline: hook → database → worker → SDK agent. The fix restores full functionality to all observation and summary storage operations.
-```
-
----
-
-### 8. Semantic - design patterns
-
-**Query**: `patterns for background worker processes`
-**Expected Best**: semantic
-
-#### 🔵 Semantic Search (Chroma)
-
-**Status**: ❌ No results
-
-#### 🟡 Keyword Search (FTS5)
-
-**Status**: ❌ No results
-
----
-
-## Conclusion
-
-Semantic search via Chroma demonstrates clear superiority for this use case. It successfully answered all test queries, while keyword search failed on 3 queries. The gap is especially pronounced for conceptual queries where users ask about "how something works" or "problems with X" - cases where FTS5 has no mechanism to understand intent beyond literal keyword matching.
-
-**Recommendation**: Implement Chroma as the primary search interface for the memory system.
diff --git a/experiment/chroma-search-test.ts b/experiment/chroma-search-test.ts
deleted file mode 100644
index 1dadfa25..00000000
--- a/experiment/chroma-search-test.ts
+++ /dev/null
@@ -1,304 +0,0 @@
-#!/usr/bin/env node
-/**
- * Chroma MCP Search Test
- *
- * Compares semantic search (via Chroma MCP) vs keyword search (SQLite FTS5)
- * to determine if hybrid approach is worthwhile.
- */
-
-import { Client } from '@modelcontextprotocol/sdk/client/index.js';
-import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
-import { SessionSearch } from '../src/services/sqlite/SessionSearch.js';
-import path from 'path';
-import os from 'os';
-import fs from 'fs';
-
-interface TestQuery {
- description: string;
- query: string;
- expectedType: 'semantic' | 'keyword' | 'both';
-}
-
-const TEST_QUERIES: TestQuery[] = [
- {
- description: 'Semantic - conceptual understanding',
- query: 'how does memory compression work',
- expectedType: 'semantic'
- },
- {
- description: 'Semantic - similar patterns',
- query: 'problems with database synchronization',
- expectedType: 'semantic'
- },
- {
- description: 'Keyword - specific file',
- query: 'SessionStore.ts',
- expectedType: 'keyword'
- },
- {
- description: 'Keyword - exact function name',
- query: 'getAllObservations',
- expectedType: 'keyword'
- },
- {
- description: 'Both - technical concept with specifics',
- query: 'FTS5 full text search implementation',
- expectedType: 'both'
- },
- {
- description: 'Semantic - user intent',
- query: 'similar to context injection issues',
- expectedType: 'semantic'
- },
- {
- description: 'Keyword - specific error',
- query: 'NOT NULL constraint violation',
- expectedType: 'keyword'
- },
- {
- description: 'Semantic - design patterns',
- query: 'patterns for background worker processes',
- expectedType: 'semantic'
- }
-];
-
-async function main() {
- console.log('🧪 Chroma MCP Search Comparison Test\n');
-
- // Initialize MCP client
- console.log('📡 Connecting to Chroma MCP server...');
- const transport = new StdioClientTransport({
- command: 'uvx',
- args: [
- 'chroma-mcp',
- '--client-type', 'persistent',
- '--data-dir', path.join(os.homedir(), '.claude-mem', 'vector-db')
- ]
- });
-
- const client = new Client({
- name: 'chroma-search-test',
- version: '1.0.0'
- }, {
- capabilities: {}
- });
-
- await client.connect(transport);
- console.log('✅ Connected to Chroma MCP\n');
-
- // Initialize SessionSearch for FTS5
- const dbPath = path.join(os.homedir(), '.claude-mem', 'claude-mem.db');
- const search = new SessionSearch(dbPath);
-
- const project = 'claude-mem';
- const collectionName = `cm__${project}`;
-
- console.log('Running comparison tests...\n');
- console.log('='.repeat(80));
- console.log();
-
- // Track results for documentation
- const results: any[] = [];
- let chromaSuccessCount = 0;
- let fts5SuccessCount = 0;
-
- for (const testQuery of TEST_QUERIES) {
- console.log(`📝 ${testQuery.description}`);
- console.log(`Query: "${testQuery.query}"`);
- console.log(`Expected best: ${testQuery.expectedType}`);
- console.log();
-
- const testResult: any = {
- description: testQuery.description,
- query: testQuery.query,
- expectedType: testQuery.expectedType,
- chromaFound: false,
- fts5Found: false,
- chromaResults: '',
- chromaTopResults: [],
- fts5TopResults: []
- };
-
- // Semantic search via Chroma MCP
- console.log('🔍 Semantic Search (Chroma):');
- try {
- const chromaResult = await client.callTool({
- name: 'chroma_query_documents',
- arguments: {
- collection_name: collectionName,
- query_texts: [testQuery.query],
- n_results: 3,
- include: ['documents', 'metadatas', 'distances']
- }
- });
-
- const resultText = chromaResult.content[0]?.text || '';
- testResult.chromaResults = resultText;
- testResult.chromaFound = resultText.includes('ids') && resultText.length > 50;
-
- // Extract documents from result text
- if (testResult.chromaFound) {
- chromaSuccessCount++;
-
- // Try to parse documents from the Python dict-like output
- const docsMatch = resultText.match(/'documents':\s*\[(.*?)\]/s);
- const metasMatch = resultText.match(/'metadatas':\s*\[(.*?)\]/s);
- const distancesMatch = resultText.match(/'distances':\s*\[(.*?)\]/s);
-
- if (docsMatch) {
- // Extract individual document strings
- const docsContent = docsMatch[1];
- const docMatches = docsContent.match(/'([^']*(?:\\'[^']*)*)'/g) || [];
- const docs = docMatches.map(d => d.slice(1, -1).replace(/\\'/g, "'"));
-
- testResult.chromaTopResults = docs.slice(0, 3);
- }
-
- console.log(' ✅ Found results');
- console.log(resultText.substring(0, 500) + '...');
- } else {
- console.log(' ❌ No results');
- }
- } catch (error: any) {
- console.log(` ❌ Error: ${error.message}`);
- testResult.chromaResults = `Error: ${error.message}`;
- }
- console.log();
-
- // Keyword search via FTS5
- console.log('🔍 Keyword Search (FTS5):');
- try {
- const fts5Results = search.searchObservations(testQuery.query, {
- limit: 3,
- project
- });
-
- testResult.fts5Found = fts5Results.length > 0;
-
- if (testResult.fts5Found) {
- fts5SuccessCount++;
-
- // Capture top results with title and narrative
- testResult.fts5TopResults = fts5Results.map(r => ({
- title: r.title,
- narrative: r.narrative || r.text || '(no content)',
- type: r.type
- }));
-
- console.log(` ✅ Found: ${fts5Results.length} results`);
- console.log(` Top result: ${fts5Results[0].title}`);
- } else {
- console.log(' ❌ No results');
- }
- } catch (error: any) {
- console.log(` ❌ Error: ${error.message}`);
- }
-
- results.push(testResult);
-
- console.log();
- console.log('-'.repeat(80));
- console.log();
- }
-
- // Generate results summary
- const totalTests = TEST_QUERIES.length;
- const chromaSuccessRate = ((chromaSuccessCount / totalTests) * 100).toFixed(0);
- const fts5SuccessRate = ((fts5SuccessCount / totalTests) * 100).toFixed(0);
-
- console.log('✅ Search comparison complete!\n');
- console.log(`📊 Results Summary:`);
- console.log(` Chroma: ${chromaSuccessCount}/${totalTests} queries succeeded (${chromaSuccessRate}%)`);
- console.log(` FTS5: ${fts5SuccessCount}/${totalTests} queries succeeded (${fts5SuccessRate}%)`);
- console.log();
-
- // Write results to RESULTS.md
- const resultsPath = path.join(process.cwd(), 'experiment', 'RESULTS.md');
- const timestamp = new Date().toISOString();
-
- let markdown = `# Chroma MCP Search Experiment Results
-
-**Date**: ${timestamp}
-**Project**: ${project}
-**Collection**: ${collectionName}
-
-## Summary
-
-- **Semantic Search (Chroma)**: ${chromaSuccessCount}/${totalTests} queries succeeded (${chromaSuccessRate}%)
-- **Keyword Search (FTS5)**: ${fts5SuccessCount}/${totalTests} queries succeeded (${fts5SuccessRate}%)
-
-## Key Findings
-
-`;
-
- if (chromaSuccessCount > fts5SuccessCount) {
- const diff = chromaSuccessCount - fts5SuccessCount;
- markdown += `✅ **Semantic search outperformed keyword search by ${diff} queries.**\n\n`;
- markdown += `Chroma's vector embeddings successfully handled conceptual queries that FTS5 completely missed. `;
- markdown += `For queries requiring semantic understanding rather than exact keyword matching, Chroma is clearly superior.\n\n`;
- } else if (fts5SuccessCount > chromaSuccessCount) {
- const diff = fts5SuccessCount - chromaSuccessCount;
- markdown += `⚠️ **Keyword search outperformed semantic search by ${diff} queries.**\n\n`;
- } else {
- markdown += `Both search methods performed equally well.\n\n`;
- }
-
- markdown += `## Detailed Results\n\n`;
-
- for (let i = 0; i < results.length; i++) {
- const result = results[i];
- markdown += `### ${i + 1}. ${result.description}\n\n`;
- markdown += `**Query**: \`${result.query}\` \n`;
- markdown += `**Expected Best**: ${result.expectedType}\n\n`;
-
- // Chroma Results
- markdown += `#### 🔵 Semantic Search (Chroma)\n\n`;
- if (result.chromaFound && result.chromaTopResults.length > 0) {
- markdown += `**Status**: ✅ Found ${result.chromaTopResults.length} results\n\n`;
- result.chromaTopResults.forEach((doc: string, idx: number) => {
- markdown += `**Result ${idx + 1}:**\n\n`;
- markdown += `\`\`\`\n${doc}\n\`\`\`\n\n`;
- });
- } else {
- markdown += `**Status**: ❌ No results\n\n`;
- }
-
- // FTS5 Results
- markdown += `#### 🟡 Keyword Search (FTS5)\n\n`;
- if (result.fts5Found && result.fts5TopResults.length > 0) {
- markdown += `**Status**: ✅ Found ${result.fts5TopResults.length} results\n\n`;
- result.fts5TopResults.forEach((r: any, idx: number) => {
- markdown += `**Result ${idx + 1}: ${r.title}** (${r.type})\n\n`;
- markdown += `\`\`\`\n${r.narrative}\n\`\`\`\n\n`;
- });
- } else {
- markdown += `**Status**: ❌ No results\n\n`;
- }
-
- markdown += `---\n\n`;
- }
-
- markdown += `## Conclusion\n\n`;
-
- if (chromaSuccessRate === '100' && fts5SuccessRate !== '100') {
- markdown += `Semantic search via Chroma demonstrates clear superiority for this use case. `;
- markdown += `It successfully answered all test queries, while keyword search failed on ${totalTests - fts5SuccessCount} queries. `;
- markdown += `The gap is especially pronounced for conceptual queries where users ask about "how something works" `;
- markdown += `or "problems with X" - cases where FTS5 has no mechanism to understand intent beyond literal keyword matching.\n\n`;
- markdown += `**Recommendation**: Implement Chroma as the primary search interface for the memory system.\n`;
- } else if (chromaSuccessCount > fts5SuccessCount) {
- markdown += `Semantic search shows better performance overall. Consider using Chroma as primary with FTS5 as fallback.\n`;
- } else {
- markdown += `Both methods show similar performance. A hybrid approach may be beneficial.\n`;
- }
-
- fs.writeFileSync(resultsPath, markdown);
- console.log(`📝 Results written to: ${resultsPath}\n`);
-
- await client.close();
-}
-
-main().catch(error => {
- console.error('❌ Test failed:', error);
- process.exit(1);
-});
diff --git a/experiment/chroma-sync-experiment.ts b/experiment/chroma-sync-experiment.ts
deleted file mode 100644
index 8e100aad..00000000
--- a/experiment/chroma-sync-experiment.ts
+++ /dev/null
@@ -1,380 +0,0 @@
-#!/usr/bin/env node
-/**
- * Chroma MCP Sync Experiment
- *
- * This script tests syncing SQLite observations/summaries to ChromaDB
- * via the existing Chroma MCP server (uvx chroma-mcp).
- *
- * NO PRODUCTION CODE CHANGES - Pure experiment.
- */
-
-import { Client } from '@modelcontextprotocol/sdk/client/index.js';
-import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
-import { SessionStore } from '../src/services/sqlite/SessionStore.js';
-import path from 'path';
-import os from 'os';
-
-interface ChromaDocument {
- id: string;
- document: string;
- metadata: Record;
-}
-
-async function main() {
- console.log('🧪 Chroma MCP Sync Experiment\n');
-
- // Initialize MCP client to Chroma server
- console.log('📡 Connecting to Chroma MCP server...');
- const transport = new StdioClientTransport({
- command: 'uvx',
- args: [
- 'chroma-mcp',
- '--client-type', 'persistent',
- '--data-dir', path.join(os.homedir(), '.claude-mem', 'vector-db')
- ]
- });
-
- const client = new Client({
- name: 'chroma-sync-experiment',
- version: '1.0.0'
- }, {
- capabilities: {}
- });
-
- await client.connect(transport);
- console.log('✅ Connected to Chroma MCP\n');
-
- // List available tools
- const { tools } = await client.listTools();
- console.log('🔧 Available MCP tools:');
- tools.forEach(tool => console.log(` - ${tool.name}`));
- console.log();
-
- // Initialize SessionStore to read SQLite data
- const dbPath = path.join(os.homedir(), '.claude-mem', 'claude-mem.db');
- const store = new SessionStore();
-
- // Get project name (for collection naming)
- const project = 'claude-mem';
- const collectionName = `cm__${project}`;
-
- console.log(`🗑️ Deleting existing collection: ${collectionName}`);
-
- try {
- await client.callTool({
- name: 'chroma_delete_collection',
- arguments: {
- collection_name: collectionName
- }
- });
- console.log('✅ Collection deleted\n');
- } catch (error) {
- console.log('ℹ️ Collection does not exist (first run)\n');
- }
-
- console.log(`📚 Creating collection: ${collectionName}`);
-
- // Create collection via MCP
- const createResult = await client.callTool({
- name: 'chroma_create_collection',
- arguments: {
- collection_name: collectionName,
- embedding_function_name: 'default'
- }
- });
-
- console.log('✅ Collection created:', createResult.content[0]);
- console.log();
-
- // Fetch observations from SQLite using raw query
- console.log('📖 Reading observations from SQLite...');
- const observations = store.db.prepare(`
- SELECT * FROM observations WHERE project = ? ORDER BY created_at_epoch DESC
- `).all(project) as any[];
- console.log(`Found ${observations.length} observations\n`);
-
- // Prepare documents for Chroma - each semantic chunk is its own document
- const documents: ChromaDocument[] = [];
-
- for (const obs of observations) {
- // Parse JSON fields
- const facts = obs.facts ? JSON.parse(obs.facts) : [];
- const concepts = obs.concepts ? JSON.parse(obs.concepts) : [];
- const files_read = obs.files_read ? JSON.parse(obs.files_read) : [];
- const files_modified = obs.files_modified ? JSON.parse(obs.files_modified) : [];
-
- const baseMetadata = {
- sqlite_id: obs.id,
- doc_type: 'observation',
- sdk_session_id: obs.sdk_session_id,
- project: obs.project,
- created_at_epoch: obs.created_at_epoch,
- type: obs.type || 'discovery',
- title: obs.title || 'Untitled',
- ...(obs.subtitle && { subtitle: obs.subtitle }),
- ...(concepts.length && { concepts: concepts.join(',') }),
- ...(files_read.length && { files_read: files_read.join(',') }),
- ...(files_modified.length && { files_modified: files_modified.join(',') })
- };
-
- // Narrative as separate document
- if (obs.narrative) {
- documents.push({
- id: `obs_${obs.id}_narrative`,
- document: obs.narrative,
- metadata: { ...baseMetadata, field_type: 'narrative' }
- });
- }
-
- // Text as separate document
- if (obs.text) {
- documents.push({
- id: `obs_${obs.id}_text`,
- document: obs.text,
- metadata: { ...baseMetadata, field_type: 'text' }
- });
- }
-
- // Each fact as separate document
- facts.forEach((fact: string, index: number) => {
- documents.push({
- id: `obs_${obs.id}_fact_${index}`,
- document: fact,
- metadata: { ...baseMetadata, field_type: 'fact', fact_index: index }
- });
- });
- }
-
- console.log(`Created ${documents.length} observation field documents (narratives, texts, facts)\n`);
-
- // Sync in batches of 100
- console.log('⬆️ Syncing observation fields to ChromaDB...');
- const batchSize = 100;
- const totalBatches = Math.ceil(documents.length / batchSize);
- const startTime = Date.now();
-
- for (let i = 0; i < documents.length; i += batchSize) {
- const batch = documents.slice(i, i + batchSize);
- const batchNumber = Math.floor(i / batchSize) + 1;
- const progress = Math.round((batchNumber / totalBatches) * 100);
- const docsProcessed = Math.min(i + batchSize, documents.length);
- const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
-
- process.stdout.write(` [${batchNumber}/${totalBatches}] ${progress}% - Syncing docs ${i + 1}-${docsProcessed}/${documents.length} (${elapsed}s elapsed)...`);
-
- await client.callTool({
- name: 'chroma_add_documents',
- arguments: {
- collection_name: collectionName,
- documents: batch.map(d => d.document),
- ids: batch.map(d => d.id),
- metadatas: batch.map(d => d.metadata)
- }
- });
-
- console.log(' ✓');
- }
-
- const totalTime = ((Date.now() - startTime) / 1000).toFixed(1);
- console.log(`✅ Synced ${documents.length} observation documents in ${totalTime}s\n`);
-
- // Fetch session summaries
- console.log('📖 Reading session summaries from SQLite...');
- const summaries = store.db.prepare(`
- SELECT * FROM session_summaries WHERE project = ? ORDER BY created_at_epoch DESC LIMIT 100
- `).all(project) as any[];
- console.log(`Found ${summaries.length} session summaries`);
-
- // Prepare session documents - each field is its own document
- const sessionDocs: ChromaDocument[] = [];
-
- for (const summary of summaries) {
- const baseMetadata = {
- sqlite_id: summary.id,
- doc_type: 'session_summary',
- sdk_session_id: summary.sdk_session_id,
- project: summary.project,
- created_at_epoch: summary.created_at_epoch,
- prompt_number: summary.prompt_number || 0
- };
-
- // Each field becomes a separate document
- if (summary.request) {
- sessionDocs.push({
- id: `summary_${summary.id}_request`,
- document: summary.request,
- metadata: { ...baseMetadata, field_type: 'request' }
- });
- }
-
- if (summary.investigated) {
- sessionDocs.push({
- id: `summary_${summary.id}_investigated`,
- document: summary.investigated,
- metadata: { ...baseMetadata, field_type: 'investigated' }
- });
- }
-
- if (summary.learned) {
- sessionDocs.push({
- id: `summary_${summary.id}_learned`,
- document: summary.learned,
- metadata: { ...baseMetadata, field_type: 'learned' }
- });
- }
-
- if (summary.completed) {
- sessionDocs.push({
- id: `summary_${summary.id}_completed`,
- document: summary.completed,
- metadata: { ...baseMetadata, field_type: 'completed' }
- });
- }
-
- if (summary.next_steps) {
- sessionDocs.push({
- id: `summary_${summary.id}_next_steps`,
- document: summary.next_steps,
- metadata: { ...baseMetadata, field_type: 'next_steps' }
- });
- }
-
- if (summary.notes) {
- sessionDocs.push({
- id: `summary_${summary.id}_notes`,
- document: summary.notes,
- metadata: { ...baseMetadata, field_type: 'notes' }
- });
- }
- }
-
- console.log(`Created ${sessionDocs.length} session field documents\n`);
-
- // Sync sessions
- console.log('⬆️ Syncing session fields to ChromaDB...');
- const sessionBatches = Math.ceil(sessionDocs.length / batchSize);
- const sessionStartTime = Date.now();
-
- for (let i = 0; i < sessionDocs.length; i += batchSize) {
- const batch = sessionDocs.slice(i, i + batchSize);
- const batchNumber = Math.floor(i / batchSize) + 1;
- const progress = Math.round((batchNumber / sessionBatches) * 100);
- const docsProcessed = Math.min(i + batchSize, sessionDocs.length);
- const elapsed = ((Date.now() - sessionStartTime) / 1000).toFixed(1);
-
- process.stdout.write(` [${batchNumber}/${sessionBatches}] ${progress}% - Syncing docs ${i + 1}-${docsProcessed}/${sessionDocs.length} (${elapsed}s elapsed)...`);
-
- await client.callTool({
- name: 'chroma_add_documents',
- arguments: {
- collection_name: collectionName,
- documents: batch.map(d => d.document),
- ids: batch.map(d => d.id),
- metadatas: batch.map(d => d.metadata)
- }
- });
-
- console.log(' ✓');
- }
-
- const sessionTotalTime = ((Date.now() - sessionStartTime) / 1000).toFixed(1);
- console.log(`✅ Synced ${sessionDocs.length} session documents in ${sessionTotalTime}s\n`);
-
- // Fetch user prompts
- console.log('📖 Reading user prompts from SQLite...');
- const prompts = store.db.prepare(`
- 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 s.project = ?
- ORDER BY up.created_at_epoch DESC
- LIMIT 1000
- `).all(project) as any[];
- console.log(`Found ${prompts.length} user prompts`);
-
- // Prepare prompt documents - one document per prompt
- const promptDocs: ChromaDocument[] = [];
-
- for (const prompt of prompts) {
- promptDocs.push({
- id: `prompt_${prompt.id}`,
- document: prompt.prompt_text,
- metadata: {
- sqlite_id: prompt.id,
- doc_type: 'user_prompt',
- sdk_session_id: prompt.sdk_session_id,
- project: prompt.project,
- created_at_epoch: prompt.created_at_epoch,
- prompt_number: prompt.prompt_number || 0
- }
- });
- }
-
- console.log(`Created ${promptDocs.length} user prompt documents\n`);
-
- // Sync prompts in batches
- console.log('⬆️ Syncing user prompts to ChromaDB...');
- const promptBatches = Math.ceil(promptDocs.length / batchSize);
- const promptStartTime = Date.now();
-
- for (let i = 0; i < promptDocs.length; i += batchSize) {
- const batch = promptDocs.slice(i, i + batchSize);
- const batchNumber = Math.floor(i / batchSize) + 1;
- const progress = Math.round((batchNumber / promptBatches) * 100);
- const docsProcessed = Math.min(i + batchSize, promptDocs.length);
- const elapsed = ((Date.now() - promptStartTime) / 1000).toFixed(1);
-
- process.stdout.write(` [${batchNumber}/${promptBatches}] ${progress}% - Syncing docs ${i + 1}-${docsProcessed}/${promptDocs.length} (${elapsed}s elapsed)...`);
-
- await client.callTool({
- name: 'chroma_add_documents',
- arguments: {
- collection_name: collectionName,
- documents: batch.map(d => d.document),
- ids: batch.map(d => d.id),
- metadatas: batch.map(d => d.metadata)
- }
- });
-
- console.log(' ✓');
- }
-
- const promptTotalTime = ((Date.now() - promptStartTime) / 1000).toFixed(1);
- console.log(`✅ Synced ${promptDocs.length} user prompt documents in ${promptTotalTime}s\n`);
-
- // Get collection info
- const infoResult = await client.callTool({
- name: 'chroma_get_collection_info',
- arguments: {
- collection_name: collectionName
- }
- });
-
- console.log('📊 Collection Info:');
- console.log(infoResult.content[0]);
- console.log();
-
- // Get count
- const countResult = await client.callTool({
- name: 'chroma_get_collection_count',
- arguments: {
- collection_name: collectionName
- }
- });
-
- console.log('📊 Total Documents:', countResult.content[0]);
- console.log();
-
- console.log('✅ Sync experiment complete!\n');
- console.log('Next: Run chroma-search-test.ts to test semantic search');
-
- await client.close();
-}
-
-main().catch(error => {
- console.error('❌ Experiment failed:', error);
- process.exit(1);
-});
diff --git a/experiment/test-mcp-direct.js b/experiment/test-mcp-direct.js
deleted file mode 100644
index 58e5bbae..00000000
--- a/experiment/test-mcp-direct.js
+++ /dev/null
@@ -1,54 +0,0 @@
-import { SessionStore } from '../src/services/sqlite/SessionStore.js';
-
-const store = new SessionStore();
-
-// Simulate what the MCP handler does
-const args = {
- anchor: 3300,
- depth_before: 10,
- depth_after: 10
-};
-
-console.log('Testing MCP handler logic with anchor:', args.anchor);
-
-try {
- let timeline;
- const anchor = args.anchor;
- const depth_before = args.depth_before;
- const depth_after = args.depth_after;
-
- if (typeof anchor === 'number') {
- console.log('Anchor is number, getting observation...');
- const obs = store.getObservationById(anchor);
- if (!obs) {
- console.error('Observation not found!');
- process.exit(1);
- }
- console.log('Found observation:', obs.id, 'at epoch:', obs.created_at_epoch);
-
- console.log('Calling getTimelineAroundObservation...');
- timeline = store.getTimelineAroundObservation(anchor, obs.created_at_epoch, depth_before, depth_after);
-
- console.log('Timeline result:', {
- observations: timeline.observations?.length,
- sessions: timeline.sessions?.length,
- prompts: timeline.prompts?.length
- });
-
- console.log('Timeline observations type:', typeof timeline.observations);
- console.log('Timeline sessions type:', typeof timeline.sessions);
- console.log('Timeline prompts type:', typeof timeline.prompts);
-
- if (timeline.observations) {
- console.log('First observation:', timeline.observations[0]);
- }
- }
-
- console.log('\n✓ No errors!');
-} catch (err) {
- console.error('ERROR:', err.message);
- console.error(err.stack);
- process.exit(1);
-}
-
-store.close();
diff --git a/experiment/test-timeline-query.ts b/experiment/test-timeline-query.ts
deleted file mode 100644
index a374df94..00000000
--- a/experiment/test-timeline-query.ts
+++ /dev/null
@@ -1,67 +0,0 @@
-import { SessionStore } from '../src/services/sqlite/SessionStore.js';
-
-const store = new SessionStore();
-
-console.log('=== Test 1: Without project filter ===');
-try {
- const result = store.getTimelineAroundTimestamp(
- 1730667961000, // timestamp for observation 3300
- 5,
- 5
- );
-
- console.log('Result:', {
- observations: result?.observations?.length,
- sessions: result?.sessions?.length,
- prompts: result?.prompts?.length
- });
-} catch (err) {
- console.error('ERROR:', err);
-}
-
-console.log('\n=== Test 2: With project filter ===');
-try {
- const result = store.getTimelineAroundTimestamp(
- 1730667961000,
- 5,
- 5,
- 'claude-mem'
- );
-
- console.log('Result:', {
- observations: result?.observations?.length,
- sessions: result?.sessions?.length,
- prompts: result?.prompts?.length
- });
-} catch (err) {
- console.error('ERROR:', err);
-}
-
-console.log('\n=== Test 3: With actual observation ID ===');
-// First get the actual timestamp for observation 3300
-const obs = store.getObservationById(3300);
-console.log('Observation 3300:', obs ? `Found at epoch ${obs.created_at_epoch}` : 'Not found');
-
-if (obs) {
- try {
- const result = store.getTimelineAroundTimestamp(
- obs.created_at_epoch,
- 5,
- 5
- );
-
- console.log('Result:', {
- observations: result?.observations?.length,
- sessions: result?.sessions?.length,
- prompts: result?.prompts?.length
- });
-
- console.log('Observations:', result.observations?.map(o => `#${o.id}`));
- console.log('Sessions:', result.sessions?.map(s => `#S${s.id}`));
- console.log('Prompts:', result.prompts?.map(p => `#P${p.id}`));
- } catch (err) {
- console.error('ERROR:', err);
- }
-}
-
-store.close();
diff --git a/plugin/scripts/context-hook.js b/plugin/scripts/context-hook.js
index c2384b3f..0e822be6 100755
--- a/plugin/scripts/context-hook.js
+++ b/plugin/scripts/context-hook.js
@@ -423,5 +423,5 @@ ${o.gray}${"\u2500".repeat(60)}${o.reset}
${o.dim}No previous sessions found for this project yet.${o.reset}
`:`# [${t}] recent context
-No previous sessions found for this project yet.`;let d=i,m=a.slice(0,z),g=d,n=[];if(e?(n.push(""),n.push(`${o.bright}${o.cyan}\u{1F4DD} [${t}] recent context${o.reset}`),n.push(`${o.gray}${"\u2500".repeat(60)}${o.reset}`),n.push("")):(n.push(`# [${t}] recent context`),n.push("")),g.length>0){e?n.push(`${o.dim}Legend: \u{1F3AF} session-request | \u{1F534} bugfix | \u{1F7E3} feature | \u{1F504} refactor | \u2705 change | \u{1F535} discovery | \u2696\uFE0F decision${o.reset}`):n.push("**Legend:** \u{1F3AF} session-request | \u{1F534} bugfix | \u{1F7E3} feature | \u{1F504} refactor | \u2705 change | \u{1F535} discovery | \u2696\uFE0F decision"),n.push(""),e?(n.push(`${o.bright}\u{1F4A1} Column Key${o.reset}`),n.push(`${o.dim} Read: Tokens to read this observation (cost to learn it now)${o.reset}`),n.push(`${o.dim} Work: Tokens spent on work that produced this record (\u{1F50D} research, \u{1F6E0}\uFE0F building, \u2696\uFE0F deciding)${o.reset}`)):(n.push("\u{1F4A1} **Column Key**:"),n.push("- **Read**: Tokens to read this observation (cost to learn it now)"),n.push("- **Work**: Tokens spent on work that produced this record (\u{1F50D} research, \u{1F6E0}\uFE0F building, \u2696\uFE0F deciding)")),n.push(""),e?(n.push(`${o.dim}\u{1F4A1} Context Index: This semantic index (titles, types, files, tokens) is usually sufficient to understand past work.${o.reset}`),n.push(""),n.push(`${o.dim}When you need implementation details, rationale, or debugging context:${o.reset}`),n.push(`${o.dim} - Use the mem-search skill to fetch full observations on-demand${o.reset}`),n.push(`${o.dim} - Critical types (\u{1F534} bugfix, \u2696\uFE0F decision) often need detailed fetching${o.reset}`),n.push(`${o.dim} - Trust this index over re-reading code for past decisions and learnings${o.reset}`)):(n.push("\u{1F4A1} **Context Index:** This semantic index (titles, types, files, tokens) is usually sufficient to understand past work."),n.push(""),n.push("When you need implementation details, rationale, or debugging context:"),n.push("- Use the mem-search skill to fetch full observations on-demand"),n.push("- Critical types (\u{1F534} bugfix, \u2696\uFE0F decision) often need detailed fetching"),n.push("- Trust this index over re-reading code for past decisions and learnings")),n.push("");let f=d.length,b=d.reduce((u,_)=>{let h=(_.title?.length||0)+(_.subtitle?.length||0)+(_.narrative?.length||0)+JSON.stringify(_.facts||[]).length;return u+Math.ceil(h/Z)},0),T=d.reduce((u,_)=>u+(_.discovery_tokens||0),0),R=T-b,l=T>0?Math.round(R/T*100):0;e?(n.push(`${o.bright}${o.cyan}\u{1F4CA} Context Economics${o.reset}`),n.push(`${o.dim} Loading: ${f} observations (${b.toLocaleString()} tokens to read)${o.reset}`),n.push(`${o.dim} Work investment: ${T.toLocaleString()} tokens spent on research, building, and decisions${o.reset}`),T>0&&n.push(`${o.green} Your savings: ${R.toLocaleString()} tokens (${l}% reduction from reuse)${o.reset}`),n.push("")):(n.push("\u{1F4CA} **Context Economics**:"),n.push(`- Loading: ${f} observations (${b.toLocaleString()} tokens to read)`),n.push(`- Work investment: ${T.toLocaleString()} tokens spent on research, building, and decisions`),T>0&&n.push(`- Your savings: ${R.toLocaleString()} tokens (${l}% reduction from reuse)`),n.push(""));let p=a[0]?.id,P=m.map((u,_)=>{let h=_===0?null:a[_+1];return{...u,displayEpoch:h?h.created_at_epoch:u.created_at_epoch,displayTime:h?h.created_at:u.created_at,shouldShowLink:u.id!==p}}),x=[...g.map(u=>({type:"observation",data:u})),...P.map(u=>({type:"summary",data:u}))];x.sort((u,_)=>{let h=u.type==="observation"?u.data.created_at_epoch:u.data.displayEpoch,v=_.type==="observation"?_.data.created_at_epoch:_.data.displayEpoch;return h-v});let A=new Map;for(let u of x){let _=u.type==="observation"?u.data.created_at:u.data.displayTime,h=Re(_);A.has(h)||A.set(h,[]),A.get(h).push(u)}let se=Array.from(A.entries()).sort((u,_)=>{let h=new Date(u[0]).getTime(),v=new Date(_[0]).getTime();return h-v});for(let[u,_]of se){e?(n.push(`${o.bright}${o.cyan}${u}${o.reset}`),n.push("")):(n.push(`### ${u}`),n.push(""));let h=null,v="",D=!1;for(let F of _)if(F.type==="summary"){D&&(n.push(""),D=!1,h=null,v="");let E=F.data,C=`${E.request||"Session started"} (${Se(E.displayTime)})`,O=E.shouldShowLink?`claude-mem://session-summary/${E.id}`:"";if(e){let I=O?`${o.dim}[${O}]${o.reset}`:"";n.push(`\u{1F3AF} ${o.yellow}#S${E.id}${o.reset} ${C} ${I}`)}else{let I=O?` [\u2192](${O})`:"";n.push(`**\u{1F3AF} #S${E.id}** ${C}${I}`)}n.push("")}else{let E=F.data,C=be(E.files_modified),O=C.length>0?Ne(C[0],s):"General";O!==h&&(D&&n.push(""),e?n.push(`${o.dim}${O}${o.reset}`):n.push(`**${O}**`),e||(n.push("| ID | Time | T | Title | Read | Work |"),n.push("|----|------|---|-------|------|------|")),h=O,D=!0,v="");let I=fe(E.created_at),Y=E.title||"Untitled",y="\u2022";switch(E.type){case"bugfix":y="\u{1F534}";break;case"feature":y="\u{1F7E3}";break;case"refactor":y="\u{1F504}";break;case"change":y="\u2705";break;case"discovery":y="\u{1F535}";break;case"decision":y="\u2696\uFE0F";break;default:y="\u2022"}let te=(E.title?.length||0)+(E.subtitle?.length||0)+(E.narrative?.length||0)+JSON.stringify(E.facts||[]).length,X=Math.ceil(te/Z),$=E.discovery_tokens||0,k="\u{1F50D}";switch(E.type){case"discovery":k="\u{1F50D}";break;case"change":case"feature":case"bugfix":case"refactor":k="\u{1F6E0}\uFE0F";break;case"decision":k="\u2696\uFE0F";break}let re=$>0?`${k} ${$.toLocaleString()}`:"-",K=I!==v,ne=K?I:"";if(v=I,e){let oe=K?`${o.dim}${I}${o.reset}`:" ".repeat(I.length),ie=X>0?`${o.dim}(~${X}t)${o.reset}`:"",ae=$>0?`${o.dim}(${k} ${$.toLocaleString()}t)${o.reset}`:"";n.push(` ${o.dim}#${E.id}${o.reset} ${oe} ${y} ${Y} ${ie} ${ae}`)}else n.push(`| #${E.id} | ${ne||"\u2033"} | ${y} | ${Y} | ~${X} | ${re} |`)}D&&n.push("")}let N=a[0],W=d[0];if(N&&(N.investigated||N.learned||N.completed||N.next_steps)&&(!W||N.created_at_epoch>W.created_at_epoch)&&(n.push(...M("Investigated",N.investigated,o.blue,e)),n.push(...M("Learned",N.learned,o.yellow,e)),n.push(...M("Completed",N.completed,o.green,e)),n.push(...M("Next Steps",N.next_steps,o.magenta,e))),T>0&&R>0){let u=Math.round(T/1e3);n.push(""),e?n.push(`${o.dim}\u{1F4B0} Access ${u}k tokens of past research & decisions for just ${b.toLocaleString()}t. Use claude-mem search to access memories by ID instead of re-reading files.${o.reset}`):n.push(`\u{1F4B0} Access ${u}k tokens of past research & decisions for just ${b.toLocaleString()}t. Use claude-mem search to access memories by ID instead of re-reading files.`)}}return r.close(),n.join(`
+No previous sessions found for this project yet.`;let d=i,m=a.slice(0,z),g=d,n=[];if(e?(n.push(""),n.push(`${o.bright}${o.cyan}\u{1F4DD} [${t}] recent context${o.reset}`),n.push(`${o.gray}${"\u2500".repeat(60)}${o.reset}`),n.push("")):(n.push(`# [${t}] recent context`),n.push("")),g.length>0){e?n.push(`${o.dim}Legend: \u{1F3AF} session-request | \u{1F534} bugfix | \u{1F7E3} feature | \u{1F504} refactor | \u2705 change | \u{1F535} discovery | \u2696\uFE0F decision${o.reset}`):n.push("**Legend:** \u{1F3AF} session-request | \u{1F534} bugfix | \u{1F7E3} feature | \u{1F504} refactor | \u2705 change | \u{1F535} discovery | \u2696\uFE0F decision"),n.push(""),e?(n.push(`${o.bright}\u{1F4A1} Column Key${o.reset}`),n.push(`${o.dim} Read: Tokens to read this observation (cost to learn it now)${o.reset}`),n.push(`${o.dim} Work: Tokens spent on work that produced this record (\u{1F50D} research, \u{1F6E0}\uFE0F building, \u2696\uFE0F deciding)${o.reset}`)):(n.push("\u{1F4A1} **Column Key**:"),n.push("- **Read**: Tokens to read this observation (cost to learn it now)"),n.push("- **Work**: Tokens spent on work that produced this record (\u{1F50D} research, \u{1F6E0}\uFE0F building, \u2696\uFE0F deciding)")),n.push(""),e?(n.push(`${o.dim}\u{1F4A1} Context Index: This semantic index (titles, types, files, tokens) is usually sufficient to understand past work.${o.reset}`),n.push(""),n.push(`${o.dim}When you need implementation details, rationale, or debugging context:${o.reset}`),n.push(`${o.dim} - Use the mem-search skill to fetch full observations on-demand${o.reset}`),n.push(`${o.dim} - Critical types (\u{1F534} bugfix, \u2696\uFE0F decision) often need detailed fetching${o.reset}`),n.push(`${o.dim} - Trust this index over re-reading code for past decisions and learnings${o.reset}`)):(n.push("\u{1F4A1} **Context Index:** This semantic index (titles, types, files, tokens) is usually sufficient to understand past work."),n.push(""),n.push("When you need implementation details, rationale, or debugging context:"),n.push("- Use the mem-search skill to fetch full observations on-demand"),n.push("- Critical types (\u{1F534} bugfix, \u2696\uFE0F decision) often need detailed fetching"),n.push("- Trust this index over re-reading code for past decisions and learnings")),n.push("");let f=d.length,b=d.reduce((u,_)=>{let h=(_.title?.length||0)+(_.subtitle?.length||0)+(_.narrative?.length||0)+JSON.stringify(_.facts||[]).length;return u+Math.ceil(h/Z)},0),T=d.reduce((u,_)=>u+(_.discovery_tokens||0),0),R=T-b,l=T>0?Math.round(R/T*100):0;e?(n.push(`${o.bright}${o.cyan}\u{1F4CA} Context Economics${o.reset}`),n.push(`${o.dim} Loading: ${f} observations (${b.toLocaleString()} tokens to read)${o.reset}`),n.push(`${o.dim} Work investment: ${T.toLocaleString()} tokens spent on research, building, and decisions${o.reset}`),T>0&&n.push(`${o.green} Your savings: ${R.toLocaleString()} tokens (${l}% reduction from reuse)${o.reset}`),n.push("")):(n.push("\u{1F4CA} **Context Economics**:"),n.push(`- Loading: ${f} observations (${b.toLocaleString()} tokens to read)`),n.push(`- Work investment: ${T.toLocaleString()} tokens spent on research, building, and decisions`),T>0&&n.push(`- Your savings: ${R.toLocaleString()} tokens (${l}% reduction from reuse)`),n.push(""));let p=a[0]?.id,P=m.map((u,_)=>{let h=_===0?null:a[_+1];return{...u,displayEpoch:h?h.created_at_epoch:u.created_at_epoch,displayTime:h?h.created_at:u.created_at,shouldShowLink:u.id!==p}}),x=[...g.map(u=>({type:"observation",data:u})),...P.map(u=>({type:"summary",data:u}))];x.sort((u,_)=>{let h=u.type==="observation"?u.data.created_at_epoch:u.data.displayEpoch,v=_.type==="observation"?_.data.created_at_epoch:_.data.displayEpoch;return h-v});let A=new Map;for(let u of x){let _=u.type==="observation"?u.data.created_at:u.data.displayTime,h=Re(_);A.has(h)||A.set(h,[]),A.get(h).push(u)}let se=Array.from(A.entries()).sort((u,_)=>{let h=new Date(u[0]).getTime(),v=new Date(_[0]).getTime();return h-v});for(let[u,_]of se){e?(n.push(`${o.bright}${o.cyan}${u}${o.reset}`),n.push("")):(n.push(`### ${u}`),n.push(""));let h=null,v="",D=!1;for(let F of _)if(F.type==="summary"){D&&(n.push(""),D=!1,h=null,v="");let E=F.data,C=`${E.request||"Session started"} (${Se(E.displayTime)})`,O=E.shouldShowLink?`claude-mem://session-summary/${E.id}`:"";if(e){let I=O?`${o.dim}[${O}]${o.reset}`:"";n.push(`\u{1F3AF} ${o.yellow}#S${E.id}${o.reset} ${C} ${I}`)}else{let I=O?` [\u2192](${O})`:"";n.push(`**\u{1F3AF} #S${E.id}** ${C}${I}`)}n.push("")}else{let E=F.data,C=be(E.files_modified),O=C.length>0?Ne(C[0],s):"General";O!==h&&(D&&n.push(""),e?n.push(`${o.dim}${O}${o.reset}`):n.push(`**${O}**`),e||(n.push("| ID | Time | T | Title | Read | Work |"),n.push("|----|------|---|-------|------|------|")),h=O,D=!0,v="");let I=fe(E.created_at),Y=E.title||"Untitled",y="\u2022";switch(E.type){case"bugfix":y="\u{1F534}";break;case"feature":y="\u{1F7E3}";break;case"refactor":y="\u{1F504}";break;case"change":y="\u2705";break;case"discovery":y="\u{1F535}";break;case"decision":y="\u2696\uFE0F";break;default:y="\u2022"}let te=(E.title?.length||0)+(E.subtitle?.length||0)+(E.narrative?.length||0)+JSON.stringify(E.facts||[]).length,X=Math.ceil(te/Z),$=E.discovery_tokens||0,k="\u{1F50D}";switch(E.type){case"discovery":k="\u{1F50D}";break;case"change":case"feature":case"bugfix":case"refactor":k="\u{1F6E0}\uFE0F";break;case"decision":k="\u2696\uFE0F";break}let re=$>0?`${k} ${$.toLocaleString()}`:"-",K=I!==v,ne=K?I:"";if(v=I,e){let oe=K?`${o.dim}${I}${o.reset}`:" ".repeat(I.length),ie=X>0?`${o.dim}(~${X}t)${o.reset}`:"",ae=$>0?`${o.dim}(${k} ${$.toLocaleString()}t)${o.reset}`:"";n.push(` ${o.dim}#${E.id}${o.reset} ${oe} ${y} ${Y} ${ie} ${ae}`)}else n.push(`| #${E.id} | ${ne||"\u2033"} | ${y} | ${Y} | ~${X} | ${re} |`)}D&&n.push("")}let N=a[0],W=d[0];if(N&&(N.investigated||N.learned||N.completed||N.next_steps)&&(!W||N.created_at_epoch>W.created_at_epoch)&&(n.push(...M("Investigated",N.investigated,o.blue,e)),n.push(...M("Learned",N.learned,o.yellow,e)),n.push(...M("Completed",N.completed,o.green,e)),n.push(...M("Next Steps",N.next_steps,o.magenta,e))),T>0&&R>0){let u=Math.round(T/1e3);n.push(""),e?n.push(`${o.dim}\u{1F4B0} Access ${u}k tokens of past research & decisions for just ${b.toLocaleString()}t. Use the mem-search skill to access memories by ID instead of re-reading files.${o.reset}`):n.push(`\u{1F4B0} Access ${u}k tokens of past research & decisions for just ${b.toLocaleString()}t. Use the mem-search skill to access memories by ID instead of re-reading files.`)}}return r.close(),n.join(`
`).trimEnd()}var Oe=process.argv.includes("--colors");if(G.isTTY||Oe)ee(void 0,!0).then(c=>{console.log(c),process.exit(0)});else{let c="";G.on("data",e=>c+=e),G.on("end",async()=>{let e=c.trim()?JSON.parse(c):void 0,t={hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:await ee(e,!1)}};console.log(JSON.stringify(t)),process.exit(0)})}
diff --git a/plugin/scripts/user-message-hook.js b/plugin/scripts/user-message-hook.js
index 47c2355d..76b7560b 100755
--- a/plugin/scripts/user-message-hook.js
+++ b/plugin/scripts/user-message-hook.js
@@ -1,5 +1,5 @@
#!/usr/bin/env node
-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(`
+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 R}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 S=d(),e=process.env.CLAUDE_MEM_DATA_DIR||t(c(),".claude-mem"),s=process.env.CLAUDE_CONFIG_DIR||t(c(),".claude"),w=t(e,"archives"),P=t(e,"logs"),C=t(e,"trash"),I=t(e,"backups"),v=t(e,"settings.json"),U=t(e,"claude-mem.db"),b=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"),y=i(D,"node_modules");x(y)||(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.
@@ -24,8 +24,8 @@ This message was not added to your startup context, so you can continue working
`+n+`
-\u{1F4AC} Feedback & Support
-https://github.com/thedotmack/claude-mem/discussions/110
+\u{1F4AC} Community
+https://discord.gg/J4wttp9vDu
\u{1F4FA} Watch live in browser http://localhost:${r}/
`)}catch(o){console.error(`\u274C Failed to load context display: ${o}`)}process.exit(3);
diff --git a/src/hooks/context-hook.ts b/src/hooks/context-hook.ts
index a09cf4e6..0f53b3bc 100644
--- a/src/hooks/context-hook.ts
+++ b/src/hooks/context-hook.ts
@@ -514,9 +514,9 @@ async function contextHook(input?: SessionStartInput, useColors: boolean = false
const workTokensK = Math.round(totalDiscoveryTokens / 1000);
output.push('');
if (useColors) {
- output.push(`${colors.dim}💰 Access ${workTokensK}k tokens of past research & decisions for just ${totalReadTokens.toLocaleString()}t. Use claude-mem search to access memories by ID instead of re-reading files.${colors.reset}`);
+ output.push(`${colors.dim}💰 Access ${workTokensK}k tokens of past research & decisions for just ${totalReadTokens.toLocaleString()}t. Use the mem-search skill to access memories by ID instead of re-reading files.${colors.reset}`);
} else {
- output.push(`💰 Access ${workTokensK}k tokens of past research & decisions for just ${totalReadTokens.toLocaleString()}t. Use claude-mem search to access memories by ID instead of re-reading files.`);
+ output.push(`💰 Access ${workTokensK}k tokens of past research & decisions for just ${totalReadTokens.toLocaleString()}t. Use the mem-search skill to access memories by ID instead of re-reading files.`);
}
}
}
diff --git a/src/hooks/user-message-hook.ts b/src/hooks/user-message-hook.ts
index e7e514d1..1fb11552 100644
--- a/src/hooks/user-message-hook.ts
+++ b/src/hooks/user-message-hook.ts
@@ -52,7 +52,7 @@ try {
"\n\n📝 Claude-Mem Context Loaded\n" +
" ℹ️ Note: This appears as stderr but is informational only\n\n" +
output +
- "\n\n💬 Feedback & Support\nhttps://github.com/thedotmack/claude-mem/discussions/110\n" +
+ "\n\n💬 Community\nhttps://discord.gg/J4wttp9vDu\n" +
`\n📺 Watch live in browser http://localhost:${port}/\n`
);
diff --git a/test-results/run-search-tests.sh b/test-results/run-search-tests.sh
deleted file mode 100755
index e1601fcd..00000000
--- a/test-results/run-search-tests.sh
+++ /dev/null
@@ -1,86 +0,0 @@
-#!/bin/bash
-
-# Comprehensive Search API Test Suite
-# Tests all endpoints and parameter combinations
-
-API_URL="http://localhost:37777"
-RESULTS_DIR="test-results"
-
-echo "🔍 Starting comprehensive search API tests..."
-echo ""
-
-# SEMANTIC QUERIES - Understanding how things work
-echo "📚 Running semantic queries..."
-curl -s "$API_URL/api/search?type=observations&query=worker%20service%20startup&format=full&limit=5&orderBy=relevance" > "$RESULTS_DIR/test-01-worker-service-startup.json"
-curl -s "$API_URL/api/search?type=observations&query=SQLite%20FTS5%20implementation&format=full&limit=5&orderBy=relevance" > "$RESULTS_DIR/test-02-sqlite-fts5-implementation.json"
-curl -s "$API_URL/api/search?type=observations&query=hook%20lifecycle%20flow&format=full&limit=5&orderBy=relevance" > "$RESULTS_DIR/test-03-hook-lifecycle-flow.json"
-curl -s "$API_URL/api/search?type=observations&query=build%20pipeline%20process&format=full&limit=5&orderBy=relevance" > "$RESULTS_DIR/test-04-build-pipeline-process.json"
-echo "✅ Semantic queries complete (4 tests)"
-
-# DECISION QUERIES - Architectural choices
-echo "⚖️ Running decision queries..."
-curl -s "$API_URL/api/search?type=observations&obs_type=decision&query=PM2%20instead%20of%20direct%20process&format=full&limit=5&orderBy=relevance" > "$RESULTS_DIR/test-05-pm2-decision.json"
-curl -s "$API_URL/api/search?type=observations&obs_type=decision&query=search%20architecture%20guidelines&format=full&limit=5&orderBy=relevance" > "$RESULTS_DIR/test-06-search-architecture-decision.json"
-curl -s "$API_URL/api/search?type=observations&obs_type=decision&query=MCP%20as%20DRY%20source&format=full&limit=5&orderBy=relevance" > "$RESULTS_DIR/test-07-mcp-dry-decision.json"
-echo "✅ Decision queries complete (3 tests)"
-
-# TROUBLESHOOTING QUERIES - Finding bugfixes
-echo "🔴 Running troubleshooting queries..."
-curl -s "$API_URL/api/search?type=observations&obs_type=bugfix&query=worker%20service%20debugging&format=full&limit=5&orderBy=relevance" > "$RESULTS_DIR/test-08-worker-debugging.json"
-curl -s "$API_URL/api/search?type=observations&query=hook%20timeout%20problems&format=full&limit=5&orderBy=relevance" > "$RESULTS_DIR/test-09-hook-timeout.json"
-curl -s "$API_URL/api/search?type=observations&query=database%20migration%20issues&format=full&limit=5&orderBy=relevance" > "$RESULTS_DIR/test-10-database-migration.json"
-echo "✅ Troubleshooting queries complete (3 tests)"
-
-# FILE-SPECIFIC QUERIES - Tracking file changes
-echo "📁 Running file-specific queries..."
-curl -s "$API_URL/api/search?type=observations&files=search-server.ts&format=full&limit=5&orderBy=date_desc" > "$RESULTS_DIR/test-11-search-server-changes.json"
-curl -s "$API_URL/api/search?type=observations&files=context-hook&format=full&limit=5&orderBy=date_desc" > "$RESULTS_DIR/test-12-context-hook-changes.json"
-curl -s "$API_URL/api/search?type=observations&files=worker-service&format=full&limit=5&orderBy=date_desc" > "$RESULTS_DIR/test-13-worker-service-changes.json"
-echo "✅ File-specific queries complete (3 tests)"
-
-# CONCEPT-BASED QUERIES - Patterns, gotchas, discoveries
-echo "🏷️ Running concept-based queries..."
-curl -s "$API_URL/api/search?type=observations&concepts=pattern&format=full&limit=5&orderBy=date_desc" > "$RESULTS_DIR/test-14-patterns.json"
-curl -s "$API_URL/api/search?type=observations&concepts=gotcha&format=full&limit=5&orderBy=date_desc" > "$RESULTS_DIR/test-15-gotchas.json"
-curl -s "$API_URL/api/search?type=observations&concepts=discovery&format=full&limit=5&orderBy=date_desc" > "$RESULTS_DIR/test-16-discoveries.json"
-echo "✅ Concept-based queries complete (3 tests)"
-
-# TYPE-FILTERED QUERIES - Bugfixes, features, decisions
-echo "🔖 Running type-filtered queries..."
-curl -s "$API_URL/api/search?type=observations&obs_type=bugfix&format=full&limit=5&orderBy=date_desc" > "$RESULTS_DIR/test-17-all-bugfixes.json"
-curl -s "$API_URL/api/search?type=observations&obs_type=feature&format=full&limit=5&orderBy=date_desc" > "$RESULTS_DIR/test-18-all-features.json"
-curl -s "$API_URL/api/search?type=observations&obs_type=decision&format=full&limit=5&orderBy=date_desc" > "$RESULTS_DIR/test-19-all-decisions.json"
-echo "✅ Type-filtered queries complete (3 tests)"
-
-# SESSION QUERIES - Testing session search
-echo "📝 Running session queries..."
-curl -s "$API_URL/api/search?type=sessions&query=search%20architecture&format=full&limit=5&orderBy=relevance" > "$RESULTS_DIR/test-20-session-search.json"
-echo "✅ Session queries complete (1 test)"
-
-# USER PROMPT QUERIES - Testing prompt search
-echo "💬 Running user prompt queries..."
-curl -s "$API_URL/api/search?type=prompts&query=build%20and%20deploy&format=full&limit=5&orderBy=relevance" > "$RESULTS_DIR/test-21-prompt-search.json"
-echo "✅ User prompt queries complete (1 test)"
-
-# DEDICATED ENDPOINTS - Timeline and semantic shortcuts
-echo "🎯 Running dedicated endpoint tests..."
-curl -s "$API_URL/api/decisions?format=full&limit=5" > "$RESULTS_DIR/test-22-decisions-endpoint.json"
-curl -s "$API_URL/api/changes?format=full&limit=5" > "$RESULTS_DIR/test-23-changes-endpoint.json"
-curl -s "$API_URL/api/how-it-works?format=full&limit=5" > "$RESULTS_DIR/test-24-how-it-works-endpoint.json"
-curl -s "$API_URL/api/contextualize?format=full" > "$RESULTS_DIR/test-25-contextualize-endpoint.json"
-echo "✅ Dedicated endpoint tests complete (4 tests)"
-
-# TIMELINE QUERY - Get context around a specific observation
-echo "⏱️ Running timeline query..."
-curl -s "$API_URL/api/timeline?anchor=10630&depth_before=3&depth_after=3&format=full" > "$RESULTS_DIR/test-26-timeline-around-observation.json"
-echo "✅ Timeline query complete (1 test)"
-
-# MULTI-PARAMETER COMBO - Test complex query combinations
-echo "🎛️ Running multi-parameter combination tests..."
-curl -s "$API_URL/api/search?type=observations&obs_type=decision&concepts=pattern&query=search&format=full&limit=5&orderBy=relevance" > "$RESULTS_DIR/test-27-multi-param-combo.json"
-curl -s "$API_URL/api/search?type=observations&files=search-server&obs_type=feature&format=full&limit=5&orderBy=date_desc" > "$RESULTS_DIR/test-28-file-type-combo.json"
-echo "✅ Multi-parameter tests complete (2 tests)"
-
-echo ""
-echo "✨ All tests complete! 28 total queries executed."
-echo "📊 Results saved to $RESULTS_DIR/"
diff --git a/test-results/test-01-worker-service-startup.json b/test-results/test-01-worker-service-startup.json
deleted file mode 100644
index 2d13cdbb..00000000
--- a/test-results/test-01-worker-service-startup.json
+++ /dev/null
@@ -1 +0,0 @@
-[{"type":"text","text":"## Search Results Return Structured Observation Content with Rich Metadata\n*Source: claude-mem://observation/10695*\n\n**Worker service query returned six observations with complete narratives, facts, concepts, and source links**\n\nExamination of test-01-worker-service-startup.json reveals the structure and quality of search API responses. The worker service startup query successfully retrieved six relevant observations demonstrating that FTS5 keyword search provides meaningful results. Each observation includes comprehensive metadata: title, subtitle (one-line summary), full narrative text explaining the context, structured facts list with specific details, concept tags (what-changed, how-it-works, problem-solution, gotcha), associated file paths, creation dates, and observation type classification. Results are formatted with source links using the claude-mem://observation/ID format for easy reference. The retrieved observations span multiple aspects of worker service startup including port file handling migration to plugin data directory, getPackageRoot path resolution dependencies, comprehensive production failure analysis documenting 8 critical issues causing 100% failure rate, and ChromaSync initialization timing problems. The diversity of observation types and relevance to the search query demonstrates the search system's ability to provide rich, contextual results for SessionStart injection.\n\n---\nType: discovery | Facts: Search API returns observations with title, subtitle, full narrative, structured facts list, concept tags, file paths, creation dates, and observation type; Results tagged with source links using claude-mem://observation/ID format for reference; Worker service startup query retrieved six relevant observations spanning October 18 to November 17, 2025; Retrieved observations include diverse types (change, discovery, bugfix) covering port file handling, getPackageRoot issues, production failures, and ChromaSync initialization; Test result format stores array of observation objects with type and text fields containing formatted markdown content | Concepts: how-it-works, pattern | Files: test-results/test-05-pm2-decision.json, test-results/test-01-worker-service-startup.json\n\n---\nDate: 11/17/2025, 9:52:44 PM\n\n---\n\n## Test Results Return Full Observation Content with Metadata\n*Source: claude-mem://observation/10649*\n\n**Worker service startup query retrieved six relevant observations with complete details, context, and source links**\n\nThe first test result file revealed the structure and quality of search API responses. The worker service startup query successfully retrieved six relevant observations with comprehensive details. Each result includes the observation title, subtitle (one-line summary), full narrative text, structured facts list, concept tags (what-changed, how-it-works, problem-solution, gotcha), associated file paths, and creation dates. Results are tagged with source links using the claude-mem://observation/ID format. The retrieved observations span multiple aspects of worker service startup including port file handling migration, getPackageRoot path resolution issues, production failure analysis, and ChromaSync initialization timing. The diversity of observation types (change, discovery, bugfix) and the relevance to the search query demonstrate that FTS5 keyword search is providing meaningful results despite Chroma semantic search failures.\n\n---\nType: discovery | Facts: Test result contains array of observation objects with type and text fields; Each observation includes title, subtitle, narrative, facts list, concepts, files, and dates; First test query retrieved six observations about worker service startup and configuration; Results include observations about port file writing, getPackageRoot dependencies, production failures, and ChromaSync initialization; Each observation tagged with source link format claude-mem://observation/ID; Observations span date range from October 18, 2025 to November 17, 2025; Results include diverse observation types: change, discovery, and bugfix; Metadata includes observation type, facts list, concepts tags, and file paths | Concepts: how-it-works | Files: test-results/test-01-worker-service-startup.json\n\n---\nDate: 11/17/2025, 9:31:24 PM\n\n---\n\n## Build and Deployment Pipeline Executed\n*Source: claude-mem://observation/10634*\n\n**Built all hooks, worker service, search server, synced to marketplace, and restarted worker.**\n\nA complete build and deployment pipeline was executed for the claude-mem plugin version 6.0.9. The build process compiled all TypeScript hooks (context-hook, new-hook, save-hook, summary-hook, cleanup-hook, user-message-hook), the worker service, and the search server into their final JavaScript distributions. The React-based viewer UI was also bundled with its associated assets including fonts and icons. After successful compilation, the entire plugin directory was synchronized to the Claude marketplace location using rsync, transferring 11,751 files while excluding .git directories. The deployment concluded with an npm install to refresh dependencies and a worker service restart to activate the newly built code. This pipeline ensures that all code changes are properly compiled, deployed, and running in the Claude environment.\n\n---\nType: change | Facts: Package claude-mem version 6.0.9 successfully built all components; Built components include worker-service.cjs (1337.16 KB), search-server.mjs (332.02 KB), and seven hooks ranging from 2.24 KB to 38.29 KB; React viewer bundle built with viewer-bundle.js, viewer.html, fonts, and 4 SVG icon files; Files synced from /Users/alexnewman/Scripts/claude-mem to ~/.claude/plugins/marketplaces/thedotmack/ using rsync with 11,751 files transferred; npm install executed in marketplace directory to ensure dependencies are current; Worker service restarted after successful build and sync | Concepts: what-changed, how-it-works, pattern | Files: plugin/scripts/worker-service.cjs, plugin/scripts/search-server.mjs, plugin/scripts/context-hook.js, plugin/scripts/new-hook.js, plugin/scripts/save-hook.js, plugin/scripts/summary-hook.js, plugin/scripts/cleanup-hook.js, plugin/scripts/user-message-hook.js, plugin/ui/viewer-bundle.js, plugin/ui/viewer.html\n\n---\nDate: 11/17/2025, 9:22:48 PM\n\n---\n\n## Build, Sync, and Worker Restart Executed\n*Source: claude-mem://observation/10622*\n\n**Project built successfully with all hooks and services, synced to marketplace directory, and worker service restarted.**\n\nA complete build, sync, and restart operation was executed for the claude-mem project version 6.0.9. The build process compiled all hook scripts, the worker service, the search server, and the React viewer UI with associated assets. The build system used scripts/build-hooks.js to bundle all components, producing optimized output in the plugin/scripts/ directory. After successful compilation, rsync transferred the entire project to the Claude marketplace plugin directory (~/.claude/plugins/marketplaces/thedotmack/), removing any files not present in the source with the --delete flag. The operation concluded with npm install to ensure all dependencies were properly installed in the marketplace location, preparing the plugin for use.\n\n---\nType: change | Facts: Project version is 6.0.9; Build output includes context-hook (38.29 KB), new-hook (32.14 KB), save-hook (32.49 KB), summary-hook (34.61 KB), cleanup-hook (31.38 KB), and user-message-hook (2.24 KB); Worker service built at 1337.16 KB; Search server built at 331.02 KB; React viewer built successfully with viewer-bundle.js, viewer.html, font assets, and 4 icon SVG files; Project synced from /Users/alexnewman/Scripts/claude-mem to ~/.claude/plugins/marketplaces/thedotmack/ using rsync with delete flag; Transfer included 11751 files with npm install executed in target directory | Concepts: what-changed, how-it-works | Files: plugin/scripts/context-hook.js, plugin/scripts/new-hook.js, plugin/scripts/save-hook.js, plugin/scripts/summary-hook.js, plugin/scripts/cleanup-hook.js, plugin/scripts/user-message-hook.js, plugin/scripts/worker-service.cjs, plugin/scripts/search-server.mjs, plugin/ui/viewer-bundle.js, plugin/ui/viewer.html\n\n---\nDate: 11/17/2025, 9:16:40 PM\n\n---\n\n## Phase 3 Testing Initiated\n*Source: claude-mem://observation/10528*\n\n**Beginning phase 3 testing with the feature enabled and worker process running.**\n\nPhase 3 testing has begun with the necessary components in place. The phase 3 feature has been enabled in the system configuration, and the associated worker process is confirmed to be running. This represents a transition from development/configuration to active testing of phase 3 functionality. The testing phase will validate that the enabled feature and running worker interact correctly and perform as expected.\n\n---\nType: change | Facts: Phase 3 testing has been initiated; Phase 3 feature is currently enabled; Worker process is running and operational | Concepts: what-changed, how-it-works\n\n---\nDate: 11/17/2025, 7:02:59 PM"}]
\ No newline at end of file
diff --git a/test-results/test-02-sqlite-fts5-implementation.json b/test-results/test-02-sqlite-fts5-implementation.json
deleted file mode 100644
index 2733fb34..00000000
--- a/test-results/test-02-sqlite-fts5-implementation.json
+++ /dev/null
@@ -1 +0,0 @@
-[{"type":"text","text":"## Query Routing Architecture: ChromaDB, SQLite, and FTS5 Fallback Strategy\n*Source: claude-mem://observation/10736*\n\n**Defined three distinct query paths based on query presence and ChromaDB availability to prevent incorrect fallback logic.**\n\nThe proper query routing architecture requires three distinct paths based on query presence and ChromaDB availability. Path 1: When a semantic query exists and ChromaDB is available, route to queryChroma with both query and filters. If ChromaDB errors (service failure), fall back to FTS5. However, if ChromaDB successfully returns zero results, accept that as the final answer without falling back - empty results indicate no semantic match, which is more accurate than FTS5. Path 2: When the query parameter is undefined (filter-only operations), skip ChromaDB entirely and route directly to SQLite filtering using SessionStore, not SessionSearch. This path handles metadata queries like retrieving last decisions. Path 3: When a query exists but ChromaDB is unavailable, use FTS5 as a degraded fallback. The current implementation incorrectly mixes these paths, leading to inappropriate fallback behavior and inefficient routing of filter-only queries through vector search systems.\n\n---\nType: decision | Facts: When query exists and ChromaDB is available, call queryChroma with query and filters; ChromaDB errors should trigger FTS5 fallback, but ChromaDB returning zero results is final and should not trigger fallback; Filter-only queries (undefined query parameter) should skip ChromaDB entirely and use direct SQLite filtering via SessionStore; Filter-only queries should use SessionStore for direct SQLite access, not SessionSearch which involves vector operations; When query exists but ChromaDB is unavailable, FTS5 serves as degraded fallback mode; The current code incorrectly mixes these three query routing paths | Concepts: pattern, how-it-works, problem-solution, trade-off\n\n---\nDate: 11/17/2025, 11:41:54 PM\n\n---\n\n## Chroma requires query text; FTS5 fallback logic is incorrect\n*Source: claude-mem://observation/10735*\n\n**Chroma cannot do filter-only queries, and FTS5 fallback on zero results is pointless.**\n\nTwo critical insights emerge about the search architecture. First, Chroma cannot perform filter-only queries without query text because it fundamentally operates on semantic search via vector embeddings. When query is undefined, the system must bypass Chroma entirely and proceed directly to SQLite structured filtering. Second, the current FTS5 fallback logic at line 472 is fundamentally flawed - it triggers when Chroma returns zero results. However, since FTS5 maintains a 1:1 copy of the SQLite data that Chroma also indexes, if Chroma returns zero results, FTS5 will also return zero results. FTS5 fallback should only activate when Chroma is unavailable or encounters an error, not when it successfully returns an empty result set.\n\n---\nType: discovery | Facts: Chroma vector database requires query text for semantic search operations; Filter-only queries must skip Chroma and use SQLite structured filtering directly; FTS5 fallback at line 472 triggers on zero results, not on Chroma errors; FTS5 contains 1:1 copy of SQLite data, so zero Chroma results means zero FTS5 results; FTS5 fallback should only activate when Chroma is unavailable or errors, not on empty results | Concepts: problem-solution, gotcha, how-it-works, why-it-exists | Files: src/servers/search-server.ts\n\n---\nDate: 11/17/2025, 11:41:43 PM\n\n---\n\n## ChromaDB Cannot Perform Filter-Only Queries Without Vector Search\n*Source: claude-mem://observation/10734*\n\n**ChromaDB requires vector search; filter-only queries return zero results, necessitating the hybrid store architecture.**\n\nThe hybrid store architecture using both ChromaDB and SQLite exists because of a fundamental limitation in ChromaDB: it cannot perform filter-only queries without vector search. This revelation clarifies the intended query routing strategy. Metadata queries (like retrieving last decisions) should go directly to SQLite, not ChromaDB. When ChromaDB is running and returns no results, this represents an accurate answer based on vector similarity - the data simply doesn't exist or doesn't match the semantic query. The FTS5 fallback mechanism should only activate when the ChromaDB service itself fails, not when it returns empty results. If ChromaDB returns no results, SQLite will also return no results because the stores maintain a 1:1 relationship. Using FTS5 when ChromaDB returns empty results would provide less accurate results than accepting ChromaDB's empty response, since FTS5 would also return nothing but through a less sophisticated search mechanism.\n\n---\nType: discovery | Facts: ChromaDB cannot execute filter-only queries without vector search components; The hybrid store architecture exists specifically because ChromaDB lacks filter-only query capability; Queries for last decisions or metadata should query SQLite directly instead of ChromaDB; When ChromaDB returns no results, SQLite will also have no results due to 1:1 relationship between stores; FTS5 fallback should only activate if ChromaDB service fails, not when ChromaDB returns empty results; Empty results from ChromaDB are more accurate than FTS5 results when ChromaDB is operational | Concepts: how-it-works, why-it-exists, gotcha, pattern\n\n---\nDate: 11/17/2025, 11:41:32 PM\n\n---\n\n## Three-tier search architecture hierarchy revealed\n*Source: claude-mem://observation/10730*\n\n**System uses Chroma vector search primarily, FTS5 as fallback, direct SQLite for structured filtering.**\n\nThe complete search architecture operates as a three-tier hierarchy based on capability and availability. Chroma vector database provides the primary semantic search mechanism, storing vector embeddings that represent a 1:1 copy of all SQLite data. When Python dependencies are unavailable, the system falls back to FTS5 full-text search, which provides keyword matching but with inferior results compared to semantic search. Direct SQLite queries handle structured filtering operations when no text query is involved. This reveals that FTS5 was previously misunderstood as a core feature when it actually exists only as a degraded fallback mode for environments lacking Python support.\n\n---\nType: discovery | Facts: Chroma provides primary semantic search using vector embeddings of SQLite data; FTS5 serves as degraded keyword search fallback when Python dependencies unavailable; Direct SQLite queries handle structured filtering when no text query present; Search architecture forms hierarchy: Chroma (best) → FTS5 (fallback) → SQLite (filtering only); Chroma maintains complete 1:1 vector embedding copy of SQLite database | Concepts: how-it-works, why-it-exists, pattern, trade-off\n\n---\nDate: 11/17/2025, 11:37:53 PM\n\n---\n\n## Found SessionStore class structure and initialization\n*Source: claude-mem://observation/10723*\n\n**SessionStore manages database and schema, likely contains methods for direct observation retrieval without FTS5.**\n\nThe SessionStore class was located, confirming its role as the database management layer. This class initializes and maintains the SQLite database schema including observations, session summaries, and user prompts tables. Unlike SessionSearch which uses FTS5 for full-text search, SessionStore should provide methods for direct table access using structured WHERE clauses based on filters like type, concepts, files, and dates. The next step is to find existing methods in SessionStore that can retrieve observations using filters alone, without requiring FTS5 text search.\n\n---\nType: discovery | Facts: SessionStore class defined at line 9 in src/services/sqlite/SessionStore.ts; SessionStore manages SQLite database with WAL mode and foreign keys enabled; Class handles schema initialization and migrations; Database contains sdk_sessions, observations, and session_summaries tables; SessionStore provides direct access to these tables for structured queries | Concepts: how-it-works, pattern | Files: src/services/sqlite/SessionStore.ts\n\n---\nDate: 11/17/2025, 10:08:37 PM"}]
\ No newline at end of file
diff --git a/test-results/test-03-hook-lifecycle-flow.json b/test-results/test-03-hook-lifecycle-flow.json
deleted file mode 100644
index 746bf735..00000000
--- a/test-results/test-03-hook-lifecycle-flow.json
+++ /dev/null
@@ -1 +0,0 @@
-[{"type":"text","text":"## claude-mem plugin built and deployed to marketplace\n*Source: claude-mem://observation/10605*\n\n**Complete build pipeline executed: React viewer, worker service, search server, hooks compiled and synced to Claude marketplace directory**\n\nThe claude-mem plugin underwent a complete build and deployment cycle. The build system compiled all components including the React-based viewer UI, a CommonJS worker service, an ES module search server, and six specialized hooks for different lifecycle events (context, new session, save, summary, cleanup, and user messages). All compiled outputs were placed in the plugin/scripts/ directory. The build process then used rsync to synchronize the entire project to the Claude marketplace directory at ~/.claude/plugins/marketplaces/thedotmack/, transferring thousands of files including all node_modules dependencies. This deployment makes version 6.0.9 of the plugin available through the Claude marketplace with all required dependencies installed.\n\n---\nType: change | Facts: Build process compiled 6 hooks: context-hook, new-hook, save-hook, summary-hook, cleanup-hook, user-message-hook; Worker service built as worker-service.cjs (1337.16 KB) and search server as search-server.mjs (331.02 KB); React viewer built with bundled JavaScript, HTML template, font assets, and 4 SVG icon files; Plugin synced via rsync to ~/.claude/plugins/marketplaces/thedotmack/ with 11,751 files transferred; Package version 6.0.9 deployed with dependencies auto-installed in marketplace directory | Concepts: what-changed, how-it-works | Files: scripts/build-hooks.js, package.json, plugin/scripts/worker-service.cjs, plugin/scripts/search-server.mjs, plugin/scripts/context-hook.js, plugin/scripts/new-hook.js, plugin/scripts/save-hook.js, plugin/scripts/summary-hook.js, plugin/scripts/cleanup-hook.js, plugin/scripts/user-message-hook.js, plugin/ui/viewer-bundle.js, plugin/ui/viewer.html\n\n---\nDate: 11/17/2025, 9:03:34 PM\n\n---\n\n## Session Context Review for Endless Mode Phase 3\n*Source: claude-mem://observation/10554*\n\n**Reviewing progress on Phase 3 endless mode implementation with PreToolUse hook and SKIP_TOOLS optimization.**\n\nA context review was initiated to understand the current state of the endless mode implementation. Phase 3 work includes a PreToolUse hook with SKIP_TOOLS optimization that was created in a previous session (#S1937) but remains unbuilt and untested. This discovery phase aims to gather full observations from prior work to determine the next steps needed to complete the endless mode feature.\n\n---\nType: discovery | Facts: Session #S1937 previously created a PreToolUse hook with SKIP_TOOLS optimization; The PreToolUse hook has not yet been built or tested; Work is currently in Phase 3 of an endless mode implementation; Project located at /Users/alexnewman/Scripts/claude-mem | Concepts: how-it-works, what-changed\n\n---\nDate: 11/17/2025, 7:30:21 PM\n\n---\n\n## Added PreToolUse hook to lifecycle configuration\n*Source: claude-mem://observation/10541*\n\n**New PreToolUse hook executes pre-tool-use-hook.js before any tool is invoked by Claude.**\n\nA new PreToolUse lifecycle hook was added to the claude-mem plugin configuration to enable capturing state before tools are executed. This complements the existing PostToolUse hook that runs save-hook.js after tool execution. By adding pre-tool-use-hook.js with the same wildcard matcher and timeout settings as PostToolUse, the system can now monitor and potentially intercept or prepare for tool invocations before they occur. This creates a complete before/after hook pattern for tool usage tracking.\n\n---\nType: feature | Facts: Added PreToolUse hook section to plugin/hooks/hooks.json before the existing PostToolUse hook; PreToolUse hook runs pre-tool-use-hook.js script with 120s timeout; PreToolUse hook uses matcher \"*\" to trigger on all tool invocations; The hook system now captures both before and after states of tool usage | Concepts: what-changed, pattern, why-it-exists | Files: plugin/hooks/hooks.json\n\n---\nDate: 11/17/2025, 7:16:47 PM\n\n---\n\n## PreToolUse Hook Implementation for Transcript Transformation\n*Source: claude-mem://observation/10540*\n\n**Created hook that transforms previous tool results into observation references before each tool execution.**\n\nA new PreToolUse hook was implemented to handle the transformation of tool results into observation references in the transcript. The hook executes before each tool use, ensuring that the previous tool's result has already been written to the transcript file. It searches the transcript backwards to find the most recent tool_result that hasn't been transformed yet (identified by not containing \"[Observation #\"). When found, it sends a synchronous POST request to the worker's transform endpoint with the tool_use_id. If the worker successfully creates an observation, the hook modifies the transcript file in-place, replacing the full tool result content with a compact reference like \"[Observation #123: Title]\". The implementation elegantly handles edge cases: first tool use (no previous tool), already-transformed results, disabled endless mode, and worker failures—all by simply returning success and continuing. This design fulfills the architectural decision to consolidate logic in PreToolUse while avoiding YAGNI violations.\n\n---\nType: feature | Facts: Pre-tool-use hook processes the PREVIOUS tool's result, guaranteeing it's already in transcript; Hook searches transcript backwards to find most recent untransformed tool_result entry; Transformed tool_result content becomes compact reference format: \"[Observation #ID: Title]\"; Hook communicates with worker via POST to /sessions/{id}/observations/transform endpoint; SKIP_TOOLS set filters out low-value tools: ListMcpResourcesTool, SlashCommand, Skill, TodoWrite, AskUserQuestion; Hook gracefully handles first tool use case by checking if previousTool is null; Transformation only occurs when EndlessModeConfig.enabled is true and transcript_path exists; Worker returns ObservationEndpointResponse with status: queued, completed, or timeout | Concepts: how-it-works, pattern, problem-solution | Files: src/hooks/pre-tool-use-hook.ts\n\n---\nDate: 11/17/2025, 7:16:07 PM\n\n---\n\n## Moving All Logic to PreToolUse Hook\n*Source: claude-mem://observation/10538*\n\n**Decision to implement all functionality in PreToolUse hook to guarantee previous tool use is available.**\n\nA new architectural decision was made to consolidate all functionality into the PreToolUse hook. This approach was chosen because PreToolUse guarantees that the previous tool use will be available in the context, which is essential for the intended functionality. The decision explicitly avoids over-engineering (YAGNI - You Aren't Gonna Need It) while acknowledging that the first tool use scenario may require smart handling since there would be no previous tool use at that point. This represents a simplification of the architecture by using a single hook point rather than distributing logic across multiple hooks.\n\n---\nType: decision | Facts: All logic will be moved to the PreToolUse hook instead of other lifecycle hooks; PreToolUse hook guarantees that the previous tool use is in context; The approach avoids YAGNI (You Aren't Gonna Need It) principle violations; Special handling may be needed for the first tool use edge case | Concepts: pattern, why-it-exists, trade-off\n\n---\nDate: 11/17/2025, 7:13:35 PM"}]
\ No newline at end of file
diff --git a/test-results/test-04-build-pipeline-process.json b/test-results/test-04-build-pipeline-process.json
deleted file mode 100644
index a7ff40aa..00000000
--- a/test-results/test-04-build-pipeline-process.json
+++ /dev/null
@@ -1 +0,0 @@
-[{"type":"text","text":"## Build and Deployment of Unified Search API\n*Source: claude-mem://observation/10682*\n\n**Successfully built all hooks, worker service, and search server, then synced to marketplace and restarted worker.**\n\nThe unified search API implementation was successfully built and deployed. The build process generated all necessary components including the worker service, search server, and various hooks, with the search server weighing in at 332.97 KB. The React viewer UI was also compiled with all required assets. After building, the entire project (11,781 files) was synced to the Claude marketplace plugins directory using rsync, followed by npm install to ensure all dependencies were properly installed. Finally, the worker service was restarted to activate the new unified search endpoints. This deployment marks the completion of the unified search API architecture with its six public-facing endpoints and backward-compatible granular routing.\n\n---\nType: change | Facts: Build process compiled 8 components: worker-service (1337.16 KB), search-server (332.97 KB), context-hook (38.29 KB), new-hook (32.14 KB), save-hook (32.49 KB), summary-hook (34.61 KB), cleanup-hook (31.38 KB), and user-message-hook (2.24 KB); React viewer built successfully with viewer-bundle.js, viewer.html, font assets, and 4 icon SVG files; Project synced to ~/.claude/plugins/marketplaces/thedotmack/ using rsync with 11,781 files transferred; npm install executed in marketplace directory to ensure dependencies are current; Worker service restarted to activate the newly deployed unified search API endpoints | Concepts: what-changed, how-it-works | Files: plugin/scripts/worker-service.cjs, plugin/scripts/search-server.mjs, plugin/scripts/context-hook.js, plugin/scripts/new-hook.js, plugin/scripts/save-hook.js, plugin/scripts/summary-hook.js, plugin/scripts/cleanup-hook.js, plugin/scripts/user-message-hook.js, plugin/ui/viewer-bundle.js, plugin/ui/viewer.html\n\n---\nDate: 11/17/2025, 9:43:18 PM\n\n---\n\n## Build and Deployment Pipeline Execution\n*Source: claude-mem://observation/10663*\n\n**Successfully built all hooks, worker service, search server, synced to marketplace, and restarted Cloudflare worker.**\n\nThe complete build and deployment pipeline executed successfully for claude-mem version 6.0.9. The build process first prepared output directories, then compiled the React viewer bundle along with all necessary UI assets including fonts and icons. Next, it built the worker service and search server components followed by all six hook modules (context, new, save, summary, cleanup, and user-message). After successful compilation, the rsync operation transferred all 11,781 project files to the marketplace plugin directory, removing any obsolete files with the --delete flag. Finally, npm install ensured all dependencies were current in the deployment location. This deployment follows the implementation of the unified search API architecture and prepares the system for comprehensive testing of the new search capabilities.\n\n---\nType: change | Facts: Build process compiled 8 components: worker-service (1337.16 KB), search-server (332.08 KB), context-hook (38.29 KB), new-hook (32.14 KB), save-hook (32.49 KB), summary-hook (34.61 KB), cleanup-hook (31.38 KB), and user-message-hook (2.24 KB); React viewer was built successfully with viewer-bundle.js, viewer.html template, font assets, and 4 icon SVG files in plugin/ui directory; Project synced 11,781 files from /Users/alexnewman/Scripts/claude-mem to ~/.claude/plugins/marketplaces/thedotmack/ using rsync with --delete flag; npm install ran in marketplace directory after sync to update dependencies; Build outputs placed in plugin/scripts/ directory for hooks and worker service, plugin/skills/ for skills components | Concepts: how-it-works, what-changed | Files: plugin/ui/viewer-bundle.js, plugin/ui/viewer.html, plugin/scripts/worker-service.cjs, plugin/scripts/search-server.mjs, plugin/scripts/context-hook.js, plugin/scripts/new-hook.js, plugin/scripts/save-hook.js, plugin/scripts/summary-hook.js, plugin/scripts/cleanup-hook.js, plugin/scripts/user-message-hook.js\n\n---\nDate: 11/17/2025, 9:36:22 PM\n\n---\n\n## Twenty Specific Test Queries for Search API Coverage\n*Source: claude-mem://observation/10636*\n\n**Designed concrete test cases covering worker services, architecture decisions, troubleshooting, file tracking, and concept filtering**\n\nTwenty specific test queries were designed to comprehensively validate the unified search API functionality. The queries span all major categories: semantic queries focus on understanding system components (worker startup, SQLite FTS5, hooks, build pipeline), decision queries target architectural rationale (PM2 choice, search architecture, MCP principles), troubleshooting queries seek solutions (worker debugging, hook timeouts, migrations), file-specific queries track evolution (search-server.ts, context-hook, worker-service), concept-based queries filter by knowledge type (patterns, gotchas, discoveries), type-filtered queries segment by observation type (bugfixes, features, decisions), and timeline queries provide contextual history. Each query maps to specific API capabilities including unified search parameters (type, obs_type, concepts, files) and specialized endpoints (/api/decisions, /api/timeline, /api/how-it-works).\n\n---\nType: decision | Facts: Semantic queries test understanding of worker service startup, SQLite FTS5 implementation, hook lifecycle flow, and build pipeline process; Decision queries test retrieval of PM2 architectural choice, search architecture guidelines, and MCP as DRY source principle; Troubleshooting queries test finding bugfixes for worker service debugging, hook timeout problems, and database migration issues; File-specific queries test tracking changes to search-server.ts, context-hook modifications, and worker-service updates; Concept-based queries test filtering by pattern, gotcha, and discovery observation types; Type-filtered queries test filtering all bugfixes, features, and decisions independently; Timeline queries test contextual history retrieval around search architecture work | Concepts: pattern, how-it-works\n\n---\nDate: 11/17/2025, 9:26:45 PM\n\n---\n\n## claude-mem plugin built and deployed to marketplace\n*Source: claude-mem://observation/10605*\n\n**Complete build pipeline executed: React viewer, worker service, search server, hooks compiled and synced to Claude marketplace directory**\n\nThe claude-mem plugin underwent a complete build and deployment cycle. The build system compiled all components including the React-based viewer UI, a CommonJS worker service, an ES module search server, and six specialized hooks for different lifecycle events (context, new session, save, summary, cleanup, and user messages). All compiled outputs were placed in the plugin/scripts/ directory. The build process then used rsync to synchronize the entire project to the Claude marketplace directory at ~/.claude/plugins/marketplaces/thedotmack/, transferring thousands of files including all node_modules dependencies. This deployment makes version 6.0.9 of the plugin available through the Claude marketplace with all required dependencies installed.\n\n---\nType: change | Facts: Build process compiled 6 hooks: context-hook, new-hook, save-hook, summary-hook, cleanup-hook, user-message-hook; Worker service built as worker-service.cjs (1337.16 KB) and search server as search-server.mjs (331.02 KB); React viewer built with bundled JavaScript, HTML template, font assets, and 4 SVG icon files; Plugin synced via rsync to ~/.claude/plugins/marketplaces/thedotmack/ with 11,751 files transferred; Package version 6.0.9 deployed with dependencies auto-installed in marketplace directory | Concepts: what-changed, how-it-works | Files: scripts/build-hooks.js, package.json, plugin/scripts/worker-service.cjs, plugin/scripts/search-server.mjs, plugin/scripts/context-hook.js, plugin/scripts/new-hook.js, plugin/scripts/save-hook.js, plugin/scripts/summary-hook.js, plugin/scripts/cleanup-hook.js, plugin/scripts/user-message-hook.js, plugin/ui/viewer-bundle.js, plugin/ui/viewer.html\n\n---\nDate: 11/17/2025, 9:03:34 PM\n\n---\n\n## Postmortem documentation for failed worker service debug attempt\n*Source: claude-mem://observation/10581*\n\n**Documented debugging anti-patterns and root cause analysis failures from worker crash loop investigation.**\n\nCreated comprehensive postmortem analyzing why worker service debugging failed. The investigation identified five critical mistakes: jumping to symptoms instead of root cause, ignoring the build pipeline, attempting to fix by disabling functionality, not comparing working vs broken state early, and overcomplicating the investigation. The core issue was \"symptom chasing\" - following MCP error messages instead of asking why code that previously worked was suddenly broken. Git diff revealed worker code was unchanged from main branch, strongly suggesting a build or environment issue rather than code bug. The attempted fix violated KISS/YAGNI by adding defensive complexity (commenting out features) instead of finding the actual problem. Postmortem establishes correct debug sequence: compare working/broken states, verify build artifacts, test simplest hypothesis first, and never disable features as a \"fix\". Action items for next attempt emphasize verifying build pipeline health before modifying any source code.\n\n---\nType: decision | Facts: Worker service was in crash loop with 225 restarts failing with \"MCP error -32000: Connection closed\"; Debug attempt chased symptoms (MCP connection errors) instead of investigating root cause (why suddenly broken); Build pipeline was never investigated despite worker file location mismatch and corrupted search-server.mjs output; Git diff showed core worker code was UNCHANGED from main branch, only search-everything additions present; Attempted fix by disabling Chroma and search server features violated KISS/YAGNI principles; Likely root causes identified as corrupted build artifacts, missing node_modules, or ESM/CJS bundling issues; Correct debug sequence should start with comparing main vs current branch and verifying build artifacts | Concepts: problem-solution, gotcha, pattern, why-it-exists | Files: POSTMORTEM-worker-debug-failure.md\n\n---\nDate: 11/17/2025, 8:09:26 PM"}]
\ No newline at end of file
diff --git a/test-results/test-05-pm2-decision.json b/test-results/test-05-pm2-decision.json
deleted file mode 100644
index 18ed63cf..00000000
--- a/test-results/test-05-pm2-decision.json
+++ /dev/null
@@ -1 +0,0 @@
-[{"type":"text","text":"## Package script worker:logs includes automatic log flushing\n*Source: claude-mem://observation/10658*\n\n**The worker:logs npm script now flushes PM2 logs before displaying them automatically.**\n\nThe worker:logs npm script has been implemented with automatic log flushing as the default behavior. This addresses the recurring issue where logs were frequently forgotten to be flushed before viewing. The script now chains two PM2 commands: first flushing the claude-mem-worker logs, then displaying the last 100 lines without streaming. This automation ensures users always see fresh logs without needing to remember the manual flush step, improving the debugging workflow reliability.\n\n---\nType: feature | Facts: The worker:logs script runs 'pm2 flush claude-mem-worker' before showing logs; After flushing, the script displays the last 100 lines using 'pm2 logs claude-mem-worker --lines 100 --nostream'; PM2 flushes both stdout and stderr log files located at /Users/alexnewman/.pm2/logs/ | Concepts: how-it-works, problem-solution, pattern\n\n---\nDate: 11/17/2025, 9:34:27 PM\n\n---\n\n## PM2 Decision Query Returns No Results\n*Source: claude-mem://observation/10651*\n\n**Search for \"PM2 instead of direct process\" found no matching records in memory.**\n\nA test query searching for architectural decision documentation about \"PM2 instead of direct process\" returned no results from the memory system. The test file test-05-pm2-decision.json captured this search result, indicating that while the system may be using PM2 for process management, no observation has been recorded explaining the rationale for choosing PM2 over direct process management. This suggests either the decision predates the memory system, wasn't documented as an observation, or uses different terminology that doesn't match this search query.\n\n---\nType: discovery | Facts: Test file test-05-pm2-decision.json contains search results for query \"PM2 instead of direct process\"; Search returned \"No results found\" indicating no observations or records document this architectural decision; Query appears to target a decision about using PM2 for process management versus direct process execution | Concepts: problem-solution, gotcha | Files: test-results/test-05-pm2-decision.json\n\n---\nDate: 11/17/2025, 9:33:24 PM\n\n---\n\n## PM2 Worker Service Restarted\n*Source: claude-mem://observation/10598*\n\n**Restarted claude-mem-worker process managed by PM2 process manager**\n\nThe claude-mem-worker background service was restarted using PM2 process manager via npm script. The worker process (version 6.0.9) is now running with a fresh instance (PID 86354) and shows online status. The restart counter indicates this service has been restarted 136 times, suggesting active development or iterative deployment. The worker is running in fork mode with file watching disabled.\n\n---\nType: change | Facts: PM2 process claude-mem-worker (ID: 1) restarted successfully; Worker process running as version 6.0.9 with PID 86354; Process has been restarted 136 times total; Worker process status is online with 912.0kb memory usage | Concepts: how-it-works, what-changed\n\n---\nDate: 11/17/2025, 8:38:22 PM\n\n---\n\n## PM2 Worker Logs Flushed\n*Source: claude-mem://observation/10532*\n\n**Cleared claude-mem-worker stdout and error logs to prepare for clean debugging session.**\n\nThe PM2 logs for the claude-mem-worker process were flushed to clear accumulated debugging output. This removes old log entries from both the stdout log (claude-mem-worker-out.log) and error log (claude-mem-worker-error.log) in the ~/.pm2/logs directory. Flushing logs before starting fresh debugging ensures that subsequent testing will produce clean, unambiguous output that clearly shows whether the synchronous endpoint is being called with the wait_until_obs_is_saved parameter and whether UNIQUE constraint errors still occur with the updated implementation.\n\n---\nType: change | Facts: PM2 logs flushed for claude-mem-worker process; Flushed log files: ~/.pm2/logs/claude-mem-worker-out.log and claude-mem-worker-error.log; Log flush prepares clean slate for testing synchronous endpoint and UNIQUE constraint issues | Concepts: pattern, why-it-exists\n\n---\nDate: 11/17/2025, 7:05:00 PM\n\n---\n\n## Worker Process Restarted for Phase 3 Testing\n*Source: claude-mem://observation/10492*\n\n**claude-mem-worker restarted via PM2 to apply changes for testing phase 3**\n\nThe claude-mem-worker background process was restarted via PM2 to apply configuration or code changes for phase 3 testing. The worker is running version 6.0.9 of the claude-mem application and is managed by PM2 in fork mode with file watching enabled. The process successfully came online with a new process ID of 17916, indicating a clean restart. This restart was performed as part of initiating phase 3 testing, ensuring the worker is running with the latest changes.\n\n---\nType: change | Facts: claude-mem-worker process restarted using PM2 process manager; Worker running as version 6.0.9 with process ID 17916; Worker status is online with watching enabled in fork mode; Process has restarted 2 times total since initial deployment | Concepts: how-it-works, what-changed\n\n---\nDate: 11/17/2025, 6:44:21 PM"}]
\ No newline at end of file
diff --git a/test-results/test-06-search-architecture-decision.json b/test-results/test-06-search-architecture-decision.json
deleted file mode 100644
index a4c2a085..00000000
--- a/test-results/test-06-search-architecture-decision.json
+++ /dev/null
@@ -1 +0,0 @@
-[{"type":"text","text":"## FTS5 Method Renamed to Indicate Fallback Status\n*Source: claude-mem://observation/10747*\n\n**Renamed escapeFTS5 to escapeFTS5_fallback_when_chroma_unavailable across all search methods to clarify temporary usage.**\n\nThe FTS5 query escaping method was renamed from escapeFTS5 to escapeFTS5_fallback_when_chroma_unavailable to align with the newly established naming convention for FTS5-related code. This change was applied across all three search methods in SessionSearch: searchObservations, searchSessions, and searchUserPrompts. The rename makes explicit that FTS5 is only being used as a temporary fallback solution when ChromaDB (which requires UVX) is unavailable, rather than being a permanent part of the architecture. The method's existing documentation already stated that FTS5 provides degraded search capability with no semantic understanding and is only used when uvx/Python/ChromaDB is disabled. This refactoring follows the pattern established in CLAUDE.md where FTS5-related functions are given semantic names indicating their temporary nature pending UVX availability.\n\n---\nType: refactor | Facts: File modified: /Users/alexnewman/Scripts/claude-mem/src/services/sqlite/SessionSearch.ts; Method escapeFTS5 renamed to escapeFTS5_fallback_when_chroma_unavailable throughout the file; Three search methods updated: searchObservations, searchSessions, and searchUserPrompts; All calls to escapeFTS5 replaced with escapeFTS5_fallback_when_chroma_unavailable using replace_all; Method already had documentation indicating FTS5 is degraded fallback when ChromaDB unavailable | Concepts: what-changed, why-it-exists, pattern | Files: /Users/alexnewman/Scripts/claude-mem/src/services/sqlite/SessionSearch.ts\n\n---\nDate: 11/17/2025, 11:48:30 PM\n\n---\n\n## Three-tier search architecture hierarchy revealed\n*Source: claude-mem://observation/10730*\n\n**System uses Chroma vector search primarily, FTS5 as fallback, direct SQLite for structured filtering.**\n\nThe complete search architecture operates as a three-tier hierarchy based on capability and availability. Chroma vector database provides the primary semantic search mechanism, storing vector embeddings that represent a 1:1 copy of all SQLite data. When Python dependencies are unavailable, the system falls back to FTS5 full-text search, which provides keyword matching but with inferior results compared to semantic search. Direct SQLite queries handle structured filtering operations when no text query is involved. This reveals that FTS5 was previously misunderstood as a core feature when it actually exists only as a degraded fallback mode for environments lacking Python support.\n\n---\nType: discovery | Facts: Chroma provides primary semantic search using vector embeddings of SQLite data; FTS5 serves as degraded keyword search fallback when Python dependencies unavailable; Direct SQLite queries handle structured filtering when no text query present; Search architecture forms hierarchy: Chroma (best) → FTS5 (fallback) → SQLite (filtering only); Chroma maintains complete 1:1 vector embedding copy of SQLite database | Concepts: how-it-works, why-it-exists, pattern, trade-off\n\n---\nDate: 11/17/2025, 11:37:53 PM\n\n---\n\n## Final solution synthesis: separate text search from structured filtering\n*Source: claude-mem://observation/10728*\n\n**Root cause is architectural: FTS5 text search and structured filtering conflated; fix separates them into independent query paths.**\n\nThe sequential thinking process concluded with a clear architectural understanding and implementation plan. The root cause is a design assumption that text search and structured filtering always occur together, leading to FTS5 being required even when users only want to filter by type, concepts, or files. The proper fix has two implementation parts: make the query parameter optional in the schema, and add conditional logic to the search methods that branches based on query presence. When query exists, use the full-text search path; when absent, use direct table querying with filters. This architectural separation allows text search and structured filtering to work independently or in combination, fixing the immediate bug while enabling more flexible query patterns. The sequential thinking reached thought 12 of 12 with complete problem analysis and solution design.\n\n---\nType: decision | Facts: Bug exists because system forces FTS5 search even for filter-only queries; Fix part 1: Change line 366 schema from query: z.string() to query: z.string().optional(); Fix part 2: Add conditional branching in searchObservations, searchSessions, searchUserPrompts; When query exists: use FTS5 path with JOIN observations_fts and MATCH clause; When query undefined: skip FTS5, query base table directly with filter WHERE clause only; Architecture separates concerns: text search (FTS5) independent from structured filtering (SQL WHERE); Original error occurred because code assumed text search and filtering always work together | Concepts: problem-solution, how-it-works, why-it-exists, pattern, trade-off\n\n---\nDate: 11/17/2025, 10:09:25 PM\n\n---\n\n## Architectural insight: use SessionStore for filter-only, SessionSearch for text search\n*Source: claude-mem://observation/10722*\n\n**SessionSearch class handles FTS5 text search; SessionStore class should handle structured filter queries without text.**\n\nA key architectural insight emerged: the system already has appropriate separation of concerns between SessionSearch and SessionStore classes. SessionSearch is designed for full-text search using FTS5, which inherently requires search text. SessionStore should handle structured queries based purely on filters (type, concepts, files, dates) without requiring full-text search. The real fix isn't to patch escapeFTS5 or make FTS5 work without a query - it's to route filter-only queries to SessionStore methods instead of SessionSearch methods. This preserves the architectural integrity where each class serves its intended purpose.\n\n---\nType: decision | Facts: SessionSearch.searchObservations always uses FTS5 with WHERE observations_fts MATCH ?; FTS5 fundamentally requires search term, cannot MATCH against nothing; SessionStore class likely contains methods for structured queries without FTS5; Proper architecture: SessionSearch for full-text search, SessionStore for filter-based retrieval; Sequential thinking at thought 8 of 12 | Concepts: pattern, how-it-works, trade-off, why-it-exists\n\n---\nDate: 11/17/2025, 10:08:29 PM\n\n---\n\n## Root cause identified: required query parameter vs filter-only queries\n*Source: claude-mem://observation/10717*\n\n**Schema defines query as required but filter-only queries need query to be optional.**\n\nThe root cause has been identified: a mismatch between the schema definition and actual usage patterns. The Zod schema defines query as z.string(), marking it as required, yet the system appears to receive requests without a query parameter for filter-only searches. This creates two possibilities: either (1) the schema should allow optional queries to support filtering by type/concepts/files without search text, or (2) query should truly be required and filter-only requests should be rejected. The investigation now shifts to understanding whether the search functions can meaningfully operate without a query parameter, which will determine the correct fix approach.\n\n---\nType: discovery | Facts: Line 366 defines query: z.string() as required, not optional; Runtime errors show requests without query are passing through validation; Filter-only queries (using type, obs_type, concepts, files) should be valid use cases; Schema should likely be z.string().optional() to support filter-only retrieval; Sequential thinking extended from 10 to 12 total thoughts; Next investigation examines search.searchObservations to determine if query is truly required | Concepts: problem-solution, gotcha, trade-off\n\n---\nDate: 11/17/2025, 10:07:36 PM"}]
\ No newline at end of file
diff --git a/test-results/test-07-mcp-dry-decision.json b/test-results/test-07-mcp-dry-decision.json
deleted file mode 100644
index d74a8130..00000000
--- a/test-results/test-07-mcp-dry-decision.json
+++ /dev/null
@@ -1 +0,0 @@
-[{"type":"text","text":"## Session Search Successfully Retrieves Full Session Summaries\n*Source: claude-mem://observation/10654*\n\n**Search query for search architecture returns complete session records with request, investigated, learned, completed, and next steps fields**\n\nThe session search test demonstrated successful functionality, contrasting sharply with the failed observation searches. Using type=sessions, the query retrieved 57 semantic matches from Chroma and returned comprehensive session summaries. Each result includes the full session structure: completed (what was accomplished), learned (key insights), investigated (what was explored), next steps (planned actions), and notes (additional context). The first result details a session reviewing search architecture guidelines and the MCP-as-DRY-source principle, including specifics about the build pipeline, unified search API design with catch-all parameters, and the layered architecture pattern. This successful session search, combined with earlier log evidence showing prompt searches also working, confirms that Chroma's failures are isolated to observation queries specifically.\n\n---\nType: discovery | Facts: Session search query succeeded with 57 semantic matches from Chroma; Results include full session summary format with completed, learned, investigated, next steps, and notes sections; First result shows session about MCP architecture review from November 17, 2025; Session records tagged with source link format claude-mem://session/UUID; Sessions contain detailed context about build pipeline, search architecture decisions, and unified API design; Session search uses type=sessions parameter successfully unlike observation searches | Concepts: how-it-works | Files: test-results/test-20-session-search.json\n\n---\nDate: 11/17/2025, 9:33:37 PM\n\n---\n\n## Architecture Guidelines for Search Established\n*Source: claude-mem://observation/10633*\n\n**Code review requested to verify commit follows MCP-based search architecture with specific DRY principles.**\n\nA commit review was requested to ensure compliance with established architecture guidelines for the search functionality. The architecture mandates that MCP (Model Context Protocol) acts as the single, authoritative search source following DRY principles. All HTTP API search requests must route through MCP rather than implementing parallel search logic. This architectural decision confirms that MCP is the active, non-deprecated component for search operations, establishing clear boundaries between API layers and preventing code duplication across the search infrastructure.\n\n---\nType: decision | Facts: MCP serves as the DRY (Don't Repeat Yourself) search source in the architecture; HTTP API is designed to route through MCP for search functionality; MCP is confirmed as not deprecated and remains the canonical search implementation | Concepts: pattern, why-it-exists, how-it-works\n\n---\nDate: 11/17/2025, 9:22:30 PM\n\n---\n\n## Search Architecture Guidelines Enforcement\n*Source: claude-mem://observation/10629*\n\n**Architecture review requested to validate commit follows search design: MCP as dry source, HTTP routes through MCP.**\n\nA commit review was requested to validate compliance with established search architecture guidelines. The architecture mandates that MCP (Model Context Protocol) functions as the DRY (Don't Repeat Yourself) search source, with HTTP API routes required to route through MCP rather than implementing search logic independently. This ensures centralized search implementation and prevents code duplication. The review specifically aims to confirm MCP remains the canonical, non-deprecated search implementation approach.\n\n---\nType: decision | Facts: MCP serves as the dry search source in the system architecture; HTTP API routes must go through MCP rather than implementing search directly; MCP is the active, non-deprecated approach for search functionality | Concepts: pattern, why-it-exists, how-it-works\n\n---\nDate: 11/17/2025, 9:21:29 PM\n\n---\n\n## Search Architecture Guidelines Validation Requested\n*Source: claude-mem://observation/10621*\n\n**Commit review requested to verify adherence to search architecture: MCP as dry source, HTTP through MCP, MCP not deprecated.**\n\nA commit review was requested to ensure compliance with established search architecture guidelines. The architecture mandates that MCP (Model Context Protocol) functions as the canonical dry search source, with HTTP API endpoints routing through MCP rather than implementing search logic directly. This design decision maintains MCP as the active, non-deprecated integration layer for search functionality, ensuring consistency and avoiding duplication of search logic across different access methods.\n\n---\nType: decision | Facts: Architecture guideline: MCP serves as the dry search source; Architecture guideline: HTTP API routes search requests through MCP; Architecture guideline: MCP is not deprecated and remains the primary search integration layer | Concepts: why-it-exists, pattern, how-it-works\n\n---\nDate: 11/17/2025, 9:16:24 PM\n\n---\n\n## Search server contains no deprecation warnings or plans to remove MCP\n*Source: claude-mem://observation/10617*\n\n**Zero matches for deprecated, legacy, TODO, FIXME, replace MCP, or remove MCP in search-server.ts**\n\nThe absence of any deprecation warnings, legacy markers, or plans to remove MCP functionality confirms that the search server refactoring treats MCP as a first-class, actively maintained interface rather than a deprecated feature. This aligns with the architectural guideline that MCP should not be deprecated. The implementation invests in MCP with comprehensive tool definitions, schema validation, and feature parity with HTTP APIs, indicating it's the preferred interface rather than a transitional technology.\n\n---\nType: discovery | Facts: Case-insensitive search for deprecation-related terms returned zero matches in src/servers/search-server.ts; No comments indicating MCP is deprecated or will be replaced; No TODO or FIXME items suggesting future removal of MCP functionality; Search server implementation treats MCP as the primary, actively maintained interface | Concepts: pattern, why-it-exists | Files: src/servers/search-server.ts\n\n---\nDate: 11/17/2025, 9:10:45 PM"}]
\ No newline at end of file
diff --git a/test-results/test-08-worker-debugging.json b/test-results/test-08-worker-debugging.json
deleted file mode 100644
index aeba2c24..00000000
--- a/test-results/test-08-worker-debugging.json
+++ /dev/null
@@ -1 +0,0 @@
-[{"type":"text","text":"## Search API crashes with \"Cannot read properties of undefined (reading 'replace')\" error\n*Source: claude-mem://observation/10705*\n\n**9 of 12 failed tests show identical JavaScript error accessing undefined property, 3 show no results.**\n\nFresh test results reveal a critical JavaScript error in the search API. Nine tests crash with \"Cannot read properties of undefined (reading 'replace')\", indicating that some code is attempting to call .replace() on an undefined value. This error affects three distinct query categories: file-specific change queries (tests 11-13), concept-based queries (tests 14-15), and type-filtered queries (tests 16-19), plus multi-parameter file-type combinations (test 28). The pattern suggests that when the search API processes queries with file parameters, concept parameters, or type filters, it encounters an undefined value where it expects a string. The remaining three failures (tests 05, 08, 21) return clean \"No results found\" messages without crashing, indicating they may be legitimate empty result sets or different search code paths that don't trigger the bug.\n\n---\nType: discovery | Facts: 9 tests fail with error \"Cannot read properties of undefined (reading 'replace')\"; Tests 11, 12, 13 fail with replace error on file-specific change queries; Tests 14, 15 fail with replace error on concept-based queries for patterns and gotchas; Tests 16, 17, 18, 19 fail with replace error on type-filtered queries for discoveries, bugfixes, features, decisions; Test 28 fails with replace error on file-type combination query; Test 05 returns \"No results found matching 'PM2 instead of direct process'\" without crashing; Test 08 returns \"No results found matching 'worker service debugging'\" without crashing; Test 21 returns \"No results found matching 'build and deploy'\" without crashing; The replace error suggests undefined variable being passed to a string replace operation; All file-specific queries trigger the replace error; All concept-based queries trigger the replace error; All type-filtered queries trigger the replace error | Concepts: problem-solution, gotcha, pattern | Files: test-results/test-05-pm2-decision.json, test-results/test-08-worker-debugging.json, test-results/test-11-search-server-changes.json, test-results/test-12-context-hook-changes.json, test-results/test-13-worker-service-changes.json, test-results/test-14-patterns.json, test-results/test-15-gotchas.json, test-results/test-16-discoveries.json, test-results/test-17-all-bugfixes.json, test-results/test-18-all-features.json, test-results/test-19-all-decisions.json, test-results/test-21-prompt-search.json, test-results/test-28-file-type-combo.json\n\n---\nDate: 11/17/2025, 10:04:43 PM\n\n---\n\n## Twenty Specific Test Queries for Search API Coverage\n*Source: claude-mem://observation/10636*\n\n**Designed concrete test cases covering worker services, architecture decisions, troubleshooting, file tracking, and concept filtering**\n\nTwenty specific test queries were designed to comprehensively validate the unified search API functionality. The queries span all major categories: semantic queries focus on understanding system components (worker startup, SQLite FTS5, hooks, build pipeline), decision queries target architectural rationale (PM2 choice, search architecture, MCP principles), troubleshooting queries seek solutions (worker debugging, hook timeouts, migrations), file-specific queries track evolution (search-server.ts, context-hook, worker-service), concept-based queries filter by knowledge type (patterns, gotchas, discoveries), type-filtered queries segment by observation type (bugfixes, features, decisions), and timeline queries provide contextual history. Each query maps to specific API capabilities including unified search parameters (type, obs_type, concepts, files) and specialized endpoints (/api/decisions, /api/timeline, /api/how-it-works).\n\n---\nType: decision | Facts: Semantic queries test understanding of worker service startup, SQLite FTS5 implementation, hook lifecycle flow, and build pipeline process; Decision queries test retrieval of PM2 architectural choice, search architecture guidelines, and MCP as DRY source principle; Troubleshooting queries test finding bugfixes for worker service debugging, hook timeout problems, and database migration issues; File-specific queries test tracking changes to search-server.ts, context-hook modifications, and worker-service updates; Concept-based queries test filtering by pattern, gotcha, and discovery observation types; Type-filtered queries test filtering all bugfixes, features, and decisions independently; Timeline queries test contextual history retrieval around search architecture work | Concepts: pattern, how-it-works\n\n---\nDate: 11/17/2025, 9:26:45 PM\n\n---\n\n## Build, Sync, and Worker Restart Executed\n*Source: claude-mem://observation/10622*\n\n**Project built successfully with all hooks and services, synced to marketplace directory, and worker service restarted.**\n\nA complete build, sync, and restart operation was executed for the claude-mem project version 6.0.9. The build process compiled all hook scripts, the worker service, the search server, and the React viewer UI with associated assets. The build system used scripts/build-hooks.js to bundle all components, producing optimized output in the plugin/scripts/ directory. After successful compilation, rsync transferred the entire project to the Claude marketplace plugin directory (~/.claude/plugins/marketplaces/thedotmack/), removing any files not present in the source with the --delete flag. The operation concluded with npm install to ensure all dependencies were properly installed in the marketplace location, preparing the plugin for use.\n\n---\nType: change | Facts: Project version is 6.0.9; Build output includes context-hook (38.29 KB), new-hook (32.14 KB), save-hook (32.49 KB), summary-hook (34.61 KB), cleanup-hook (31.38 KB), and user-message-hook (2.24 KB); Worker service built at 1337.16 KB; Search server built at 331.02 KB; React viewer built successfully with viewer-bundle.js, viewer.html, font assets, and 4 icon SVG files; Project synced from /Users/alexnewman/Scripts/claude-mem to ~/.claude/plugins/marketplaces/thedotmack/ using rsync with delete flag; Transfer included 11751 files with npm install executed in target directory | Concepts: what-changed, how-it-works | Files: plugin/scripts/context-hook.js, plugin/scripts/new-hook.js, plugin/scripts/save-hook.js, plugin/scripts/summary-hook.js, plugin/scripts/cleanup-hook.js, plugin/scripts/user-message-hook.js, plugin/scripts/worker-service.cjs, plugin/scripts/search-server.mjs, plugin/ui/viewer-bundle.js, plugin/ui/viewer.html\n\n---\nDate: 11/17/2025, 9:16:40 PM\n\n---\n\n## Phase 3 Testing Initiated\n*Source: claude-mem://observation/10528*\n\n**Beginning phase 3 testing with the feature enabled and worker process running.**\n\nPhase 3 testing has begun with the necessary components in place. The phase 3 feature has been enabled in the system configuration, and the associated worker process is confirmed to be running. This represents a transition from development/configuration to active testing of phase 3 functionality. The testing phase will validate that the enabled feature and running worker interact correctly and perform as expected.\n\n---\nType: change | Facts: Phase 3 testing has been initiated; Phase 3 feature is currently enabled; Worker process is running and operational | Concepts: what-changed, how-it-works\n\n---\nDate: 11/17/2025, 7:02:59 PM\n\n---\n\n## Worker Service REST API Endpoints\n*Source: claude-mem://observation/10068*\n\n**Worker service exposes 28 REST endpoints for session management, observations, search, and context retrieval.**\n\nThe worker service implements a comprehensive REST API surface for the claude-mem system. The endpoints are organized into logical groupings: basic health/status (3), session management (5), data retrieval (5), configuration (2), MCP integration (2), search capabilities (6), and context/timeline queries (4). Each endpoint uses explicit method binding (this.handleXxx.bind(this)) for request handling. The API supports both observation-level operations and session-level aggregations, enabling the SDK integration to push memories during execution and retrieve contextual information on demand. The presence of multiple search and context endpoints suggests sophisticated querying capabilities for finding relevant historical information during agent execution.\n\n---\nType: discovery | Facts: Worker service file located at /Users/alexnewman/Scripts/claude-mem/src/services/worker-service.ts; Three GET endpoints for basic service info: /health, / (viewer UI), /stream (SSE); Five POST/GET endpoints for session lifecycle: /sessions/:sessionDbId/{init, observations, summarize, status, complete}; Five GET endpoints for data retrieval: /api/{observations, summaries, prompts, stats, processing-status}; Two POST endpoints for configuration: /api/processing and /api/settings; Two GET endpoints for MCP integration: /api/mcp/{status, toggle}; Six GET endpoints for search functionality: /api/search/{observations, sessions, prompts, by-concept, by-file, by-type}; Four GET endpoints for context and timeline: /api/context/{recent, timeline}, /api/timeline/by-query, /api/search/help | Concepts: how-it-works, why-it-exists | Files: /Users/alexnewman/Scripts/claude-mem/src/services/worker-service.ts\n\n---\nDate: 11/17/2025, 12:42:24 AM"}]
\ No newline at end of file
diff --git a/test-results/test-09-hook-timeout.json b/test-results/test-09-hook-timeout.json
deleted file mode 100644
index f0f0dfc9..00000000
--- a/test-results/test-09-hook-timeout.json
+++ /dev/null
@@ -1 +0,0 @@
-[{"type":"text","text":"## Added PreToolUse hook to lifecycle configuration\n*Source: claude-mem://observation/10541*\n\n**New PreToolUse hook executes pre-tool-use-hook.js before any tool is invoked by Claude.**\n\nA new PreToolUse lifecycle hook was added to the claude-mem plugin configuration to enable capturing state before tools are executed. This complements the existing PostToolUse hook that runs save-hook.js after tool execution. By adding pre-tool-use-hook.js with the same wildcard matcher and timeout settings as PostToolUse, the system can now monitor and potentially intercept or prepare for tool invocations before they occur. This creates a complete before/after hook pattern for tool usage tracking.\n\n---\nType: feature | Facts: Added PreToolUse hook section to plugin/hooks/hooks.json before the existing PostToolUse hook; PreToolUse hook runs pre-tool-use-hook.js script with 120s timeout; PreToolUse hook uses matcher \"*\" to trigger on all tool invocations; The hook system now captures both before and after states of tool usage | Concepts: what-changed, pattern, why-it-exists | Files: plugin/hooks/hooks.json\n\n---\nDate: 11/17/2025, 7:16:47 PM\n\n---\n\n## Claude-mem plugin hooks configuration structure\n*Source: claude-mem://observation/10539*\n\n**The hooks.json file defines five lifecycle hooks that trigger Node.js scripts at different session stages.**\n\nThe hooks.json configuration file reveals the complete lifecycle management system for the claude-mem plugin. It defines five distinct lifecycle events with corresponding Node.js scripts. At session start, the system performs installation checks and loads context. During the session, it captures user prompts and saves tool usage data. At session stop and end, it generates summaries and performs cleanup. All scripts are referenced using ${CLAUDE_PLUGIN_ROOT} environment variable for portability, and each has defined timeout limits to prevent hanging operations.\n\n---\nType: discovery | Facts: File plugin/hooks/hooks.json configures SessionStart, UserPromptSubmit, PostToolUse, Stop, and SessionEnd hooks; SessionStart hook runs smart-install.js, context-hook.js, and user-message-hook.js with timeouts of 300s and 10s; UserPromptSubmit hook executes new-hook.js with 120s timeout on every user prompt submission; PostToolUse hook runs save-hook.js with 120s timeout after any tool use (matcher: \"*\"); Stop hook triggers summary-hook.js and SessionEnd triggers cleanup-hook.js, both with 120s timeouts; SessionStart hooks use matcher \"startup|clear|compact\" to control when they execute | Concepts: how-it-works, why-it-exists, pattern | Files: plugin/hooks/hooks.json\n\n---\nDate: 11/17/2025, 7:13:54 PM\n\n---\n\n## Phase 3: Conditional Blocking Logic Implemented in Save Hook\n*Source: claude-mem://observation/10478*\n\n**Save hook now conditionally blocks based on Endless Mode configuration, waits for observations, and transforms transcripts.**\n\nPhase 3 of the Endless Mode implementation has been completed in the save-hook.ts file. The hook now implements conditional blocking logic based on the EndlessModeConfig.enabled flag. When Endless Mode is enabled (and tool_use_id and transcript_path are available), the hook switches to synchronous mode by appending wait_until_obs_is_saved=true to the endpoint URL and increasing the timeout from 2 to 90 seconds. After sending the observation request, the hook awaits the JSON response and handles three possible states. On 'completed' status, it receives the observation data and calls transformTranscript() to replace the tool output in the JSONL file with compressed observation markdown. On 'timeout' status, it logs a warning and falls back to using the full output while the observation continues processing in the background. On 'queued' status (async mode), it simply logs success. The implementation includes error handling for transcript transformation failures that allows execution to continue even if compression fails. This completes the integration of all three phases: synchronous observation endpoint (Phase 1), transcript transformation logic (Phase 2), and conditional blocking behavior (Phase 3).\n\n---\nType: feature | Facts: Save hook checks EndlessModeConfig.getConfig().enabled to determine blocking behavior; Blocking mode requires enabled flag, tool_use_id, and transcript_path to all be present; Endpoint URL conditionally includes query parameter wait_until_obs_is_saved=true for synchronous mode; Timeout increased from 2 seconds to 90 seconds when Endless Mode is enabled; Hook awaits response JSON and handles three states: completed, timeout, and queued; On status 'completed', hook calls transformTranscript() to replace tool output with compressed observation; On status 'timeout', hook logs warning and falls back to full output while observation completes asynchronously; Transcript transformation errors are caught and logged but don't fail the hook execution; Async mode behavior remains unchanged when Endless Mode is disabled | Concepts: how-it-works, what-changed, pattern | Files: src/hooks/save-hook.ts\n\n---\nDate: 11/17/2025, 6:36:54 PM\n\n---\n\n## Endless Mode Implementation Plan Documentation Created\n*Source: claude-mem://observation/10420*\n\n**Comprehensive four-phase plan documented for completing Endless Mode with blocking observation creation and transcript transformation.**\n\nA comprehensive implementation plan was documented to complete the Endless Mode feature. The plan acknowledges that infrastructure components (TransformLayer.ts, EndlessModeConfig.ts, tool_use_id tracking) are already in place on the experiment/endless-mode branch, but critical components are missing. The implementation is divided into four phases: Phase 1 creates a synchronous observation endpoint that blocks until observation creation completes (with timeout fallback), Phase 2 implements transcript file transformation to replace full tool outputs with compressed observations on disk, Phase 3 adds conditional blocking behavior to the save-hook based on configuration, and Phase 4 defines comprehensive testing procedures. The plan includes specific success criteria for each phase and overall success metrics targeting 80-95% token reduction while maintaining sub-120s hook execution times. This completes the architecture described in endless-mode-blocking-hooks-plan.md and supersedes the earlier UserPromptSubmit hook approach.\n\n---\nType: decision | Facts: Created endless-mode-implementation-plan.md in docs/context directory; Infrastructure is complete including TransformLayer.ts, EndlessModeConfig.ts, and tool_use_id tracking; Missing components identified: synchronous observation endpoint, transcript file transformation, and blocking save-hook behavior; Phase 1 focuses on creating synchronous observation endpoint in worker-service.ts with 60-90s wait time; Phase 2 implements transcript transformation in save-hook.ts to replace full tool results with compressed observation markdown; Phase 3 adds conditional blocking logic to save-hook based on EndlessModeConfig with timeout fallback; Phase 4 defines testing and validation procedures targeting 80-95% token reduction; TransformLayer.ts currently only handles in-memory SDK transformation, not on-disk transcript transformation | Concepts: pattern, trade-off, how-it-works, problem-solution | Files: docs/context/endless-mode-implementation-plan.md\n\n---\nDate: 11/17/2025, 4:00:05 PM\n\n---\n\n## Save-hook sends tool_use_id to worker observation endpoint\n*Source: claude-mem://observation/10406*\n\n**Modified POST request body to include extracted tool_use_id alongside tool data for observation storage**\n\nThe final portion of the save-hook changes completes the tool_use_id integration by including it in the HTTP request body sent to the worker service. After extracting the tool_use_id from the transcript via getLatestToolUseId, the hook adds it to the observation data payload alongside the tool name, inputs, outputs, prompt number, and working directory. This ensures the worker service receives the linking information needed to store observations with their corresponding tool_use_id in the database. The logging also captures this ID for debugging, displaying either the actual ID or '(none)' when extraction fails. This completes the data flow from transcript parsing through observation storage, establishing the foundation for EndlessMode's later retrieval and compression operations.\n\n---\nType: feature | Facts: POST request to worker observations endpoint now includes tool_use_id field in body; Tool_use_id added alongside existing fields: tool_name, tool_input, tool_response, prompt_number, cwd; Request maintains 2-second timeout via AbortSignal; Logger output includes toolUseId field showing extracted ID or '(none)' if not found | Concepts: what-changed, how-it-works | Files: src/hooks/save-hook.ts\n\n---\nDate: 11/17/2025, 3:49:43 PM"}]
\ No newline at end of file
diff --git a/test-results/test-10-database-migration.json b/test-results/test-10-database-migration.json
deleted file mode 100644
index de953554..00000000
--- a/test-results/test-10-database-migration.json
+++ /dev/null
@@ -1 +0,0 @@
-[{"type":"text","text":"## Enhanced CLAUDE.md with semantic naming guidelines and evergreen context reminder\n*Source: claude-mem://observation/10389*\n\n**Development documentation updated with LLM-focused naming conventions and context management best practices.**\n\nThe CLAUDE.md development documentation was enhanced with two significant additions. First, a header comment was added reminding Claude to keep only evergreen context in the file, since claude-mem handles working context separately. Second, a new coding principle (#6) was added establishing semantic naming as a core practice: all variables, parameters, and API endpoints should use verbose, self-documenting names that optimize for comprehension by both humans and LLMs rather than brevity. The example provided contrasts `wait_until_obs_is_saved=true` with the overly terse `wait=true`. Additionally, the database migration strategy documentation was refined to clarify the distinction between alpha phase behavior (schema recreation) and future production requirements (proper migration handling). These changes reflect the project's commitment to LLM-friendly code and clear separation between working and evergreen context.\n\n---\nType: change | Facts: Added header comment instructing Claude to maintain only evergreen context in CLAUDE.md; New coding principle #6 mandates semantic naming optimized for human and LLM comprehension over brevity; Semantic naming example provided: `wait_until_obs_is_saved=true` preferred over `wait=true`; Database migration strategy documentation clarified for alpha vs production phases; Documentation emphasizes verbose, self-documenting names for variables, parameters, and API endpoints | Concepts: what-changed, pattern, why-it-exists | Files: CLAUDE.md\n\n---\nDate: 11/17/2025, 3:45:08 PM\n\n---\n\n## Improved Documentation Clarity for Database Migration Strategy\n*Source: claude-mem://observation/10387*\n\n**Refined migration strategy documentation from TODO item to clear phase-based statement about production requirements.**\n\nThe database schema modification documentation in CLAUDE.md was revised to improve clarity and remove TODO-style language. The previous version used separate numbered items for the current migration strategy and a future TODO, which created ambiguity about the project's maturity and requirements. The updated version consolidates this into a single, clear statement that explicitly describes the current alpha-phase approach (database recreation on schema changes) while acknowledging the future requirement for proper migration handling in production deployments. This change makes the documentation more evergreen and professional while maintaining the same technical information.\n\n---\nType: change | Facts: CLAUDE.md database schema section was updated to improve documentation clarity; Removed TODO item \"Add proper migrations for production\" from schema modification steps; Consolidated migration strategy from two lines into one clear statement describing alpha and production phases; New wording clarifies that database recreation is acceptable for alpha phase while production deployments will require proper migration handling | Concepts: what-changed, why-it-exists | Files: CLAUDE.md\n\n---\nDate: 11/17/2025, 3:42:03 PM\n\n---\n\n## Database Schema Modification Process and Worker Debugging Procedures\n*Source: claude-mem://observation/10386*\n\n**Documentation review revealed schema update workflow and worker troubleshooting commands for claude-mem plugin.**\n\nThe CLAUDE.md documentation file was reviewed, revealing critical operational procedures for the claude-mem plugin. The database schema modification process requires coordinated updates across the schema definition file and data access classes, with a known TODO item to implement proper migrations before production. Worker process management relies on pm2 with custom npm scripts for monitoring, restarting, and force-cleaning the worker. The local development cycle follows a build-and-sync pattern to test changes. Additionally, the documentation clarifies that smart-install.js serves as a dependency checker that precedes the context-hook rather than being a hook itself.\n\n---\nType: discovery | Facts: Schema modifications require updates in src/services/sqlite/schema.ts and SessionStore/SessionSearch classes; The plugin currently recreates database on schema changes, which is acceptable for alpha but needs proper migrations for production; Worker debugging uses pm2 commands including list, worker:logs, worker:restart, and delete claude-mem-worker for clean restart; Local testing workflow involves making changes in src/, then running npm run build && npm run sync-marketplace; smart-install.js is a pre-hook dependency checker that runs before context-hook via command chaining, not a hook itself | Concepts: how-it-works, pattern, gotcha | Files: CLAUDE.md\n\n---\nDate: 11/17/2025, 3:41:50 PM\n\n---\n\n## Issue 121 Documents SQLite Database Migration Bug in Plugin v6.0.5\n*Source: claude-mem://observation/10286*\n\n**Plugin v6.0.5 broke with missing discovery_tokens column in SQLite database schema**\n\nIssue 121 documents a critical bug introduced in plugin version 6.0.5 where the plugin fails to start due to a missing database column. The error trace shows that when the context-hook.js script attempts to prepare a SQL statement, it fails because the discovery_tokens column does not exist in the SQLite database schema. This appears to be a database migration that was not properly executed during the upgrade to version 6.0.5, causing the plugin to crash on startup and preventing users from loading the context display functionality.\n\n---\nType: discovery | Facts: Issue 121 titled \"6.0.5 - broke the plugin - looks like a db migration miss\" reports plugin failure; SqliteError occurs at Database.prepare() with message \"no such column: discovery_tokens\"; Error originates in context-hook.js at line 400:1864 within the thedotmack marketplace plugin; Plugin hook error prevents context display from loading successfully | Concepts: problem-solution, what-changed, gotcha\n\n---\nDate: 11/17/2025, 2:45:15 PM\n\n---\n\n## Hotfix Release Announced on GitHub Issue #121\n*Source: claude-mem://observation/10202*\n\n**Release notification posted to issue tracker informing users of v6.0.7 availability and upgrade instructions.**\n\nA comment was posted on GitHub issue #121 announcing the release of v6.0.7 hotfix to the users who reported the database migration error. The announcement includes a link to the GitHub Release page with full details, upgrade instructions for both fresh and existing installations, and acknowledgment of the users' detailed error reports. The comment explains that fresh installations will work correctly, and existing installations can either manually add the missing columns or delete the database to trigger recreation. By posting this directly on the issue, the notification reaches the original issue reporters and provides them with clear path-forward instructions. This closes the communication loop between the development team and users affected by the bug.\n\n---\nType: change | Facts: GitHub issue comment posted at 2025-11-17T18:47:53.434Z on issue #121.; Comment URL: https://github.com/thedotmack/claude-mem/issues/121#issuecomment-3543383965.; Comment announces v6.0.7 hotfix release with link to GitHub Release page.; Upgrade instructions provided for both fresh and existing installations.; Comment acknowledges user reports and thanks for detailed error logs.; Instructions direct users to either manually add columns or delete/recreate database.; Comment requests users to close issue or report if problems persist. | Concepts: what-changed, problem-solution\n\n---\nDate: 11/17/2025, 1:47:58 PM"}]
\ No newline at end of file
diff --git a/test-results/test-11-search-server-changes.json b/test-results/test-11-search-server-changes.json
deleted file mode 100644
index ea5c0a5f..00000000
--- a/test-results/test-11-search-server-changes.json
+++ /dev/null
@@ -1 +0,0 @@
-[{"type":"text","text":"## Unified search handler implements Chroma-first with FTS5 fallback on zero results\n*Source: claude-mem://observation/10757*\n\n**Lines 403-489 show current implementation attempts Chroma first, falls back to FTS5 when empty.**\n\nThe unified search handler in search-server.ts (lines 390-489) reveals the current implementation architecture. The handler first attempts Chroma semantic search when chromaClient is available, calling queryChroma with the query parameter at line 419. If Chroma throws an error, it catches at line 465 and falls through to FTS5. However, the problematic FTS5 fallback logic at line 470 triggers when all result arrays are empty, not just on errors. This contradicts the insight that FTS5 contains identical data to Chroma, making zero-result fallback pointless. Additionally, lines 476-483 call search methods (searchObservations, searchSessions, searchUserPrompts) with the query parameter without checking if it's undefined, which will cause failures when query is not provided. The handler needs modification to skip Chroma when query is undefined and only fall back to FTS5 on Chroma errors.\n\n---\nType: discovery | Facts: Line 403 checks `if (chromaClient)` to attempt Chroma semantic search first; Line 419 calls `queryChroma(query, 100, whereFilter)` with query parameter as required; Lines 465-467 catch Chroma errors and fall through to FTS5 fallback; Line 470 triggers FTS5 fallback when all result arrays have zero length; Lines 476-483 call SessionSearch methods with query parameter without checking if undefined; Hybrid search applies 90-day recency filter at lines 424-431 | Concepts: how-it-works, problem-solution, gotcha | Files: src/servers/search-server.ts\n\n---\nDate: 11/17/2025, 11:50:29 PM\n\n---\n\n## Search Query Parameter Made Optional for Filter-Only Queries\n*Source: claude-mem://observation/10744*\n\n**Unified search tool now supports filter-only queries by making query parameter optional.**\n\nThe unified search tool was updated to support filter-only query patterns by making the 'query' parameter optional. Previously, the search tool required a natural language query string, but now users can perform searches using only metadata filters such as observation type, date ranges, concepts, or file paths. The parameter description was enhanced to explicitly indicate that the query is optional and should be omitted for filter-only queries. Additionally, the description was clarified to state that ChromaDB is used for semantic ranking when available, with FTS5 serving as a fallback when ChromaDB is unavailable, making the system's fallback behavior more transparent. This change increases the flexibility of the search interface and better aligns with use cases where users want to browse or filter by specific criteria without performing text-based search.\n\n---\nType: change | Facts: The 'query' parameter in the search tool's inputSchema changed from required to optional; File modified: /Users/alexnewman/Scripts/claude-mem/src/servers/search-server.ts; Updated description clarifies query is optional and can be omitted for filter-only queries; Description now explicitly states ChromaDB is used when available, FTS5 as fallback when unavailable; Change enables searching by filters (type, dateRange, concepts, files) without providing search text | Concepts: what-changed, how-it-works, pattern | Files: /Users/alexnewman/Scripts/claude-mem/src/servers/search-server.ts\n\n---\nDate: 11/17/2025, 11:47:49 PM\n\n---\n\n## Search Tool Requires Query Parameter as Non-Optional String\n*Source: claude-mem://observation/10743*\n\n**The search tool schema defines query as mandatory string parameter, preventing filter-only queries through current interface.**\n\nThe search tool definition in src/servers/search-server.ts reveals that the query parameter is currently defined as a mandatory string using z.string(), which prevents filter-only queries from being submitted through the MCP interface. The tool is described as providing unified search across observations, sessions, and user prompts using hybrid semantic and full-text search, with ChromaDB as primary and SQLite FTS5 as fallback. While the schema includes optional filters like type (for document type filtering), the query parameter itself cannot be omitted. To support the newly identified filter-only query pattern where queries go directly to SQLite instead of ChromaDB, this schema will need modification to make the query parameter optional using z.string().optional(), allowing users to submit filter-only requests.\n\n---\nType: discovery | Facts: The search tool is defined at line 363 in src/servers/search-server.ts; The query parameter is defined as z.string() without optional modifier, making it mandatory; The query parameter description states it enables semantic ranking via ChromaDB with FTS5 fallback; The inputSchema includes optional type filter for observations, sessions, or prompts; The search tool provides index and full output format options with index as default | Concepts: how-it-works, problem-solution | Files: src/servers/search-server.ts\n\n---\nDate: 11/17/2025, 11:47:33 PM\n\n---\n\n## Unified search handler fix requires five-step query routing logic\n*Source: claude-mem://observation/10739*\n\n**Route by query presence, error-based FTS5 fallback only, remove zero-results fallback check.**\n\nThe fix for the unified search handler requires restructuring the query routing logic into five distinct steps. First, check whether the query parameter exists at the top of the handler. If query is undefined, bypass Chroma entirely and invoke a new filter-only path in SessionSearch that performs direct SQLite filtering. If query exists, proceed with Chroma semantic search. The FTS5 fallback logic must change fundamentally - it should only trigger when Chroma throws an error (caught at line 465), not when Chroma successfully returns zero results. When Chroma returns zero results, that is the correct answer since FTS5 contains identical data and would also return zero. This requires removing the zero-results check from line 472's fallback condition, fixing the current incorrect fallback behavior.\n\n---\nType: decision | Facts: Step 1: Check if query parameter exists at beginning of unified search handler; Step 2: If query is undefined, skip Chroma and call new filter-only path in SessionSearch; Step 3: If query exists, attempt Chroma semantic search; Step 4: If Chroma throws error (line 465 catch block), fall back to FTS5; Step 5: If Chroma succeeds with zero results, accept zero as correct answer without FTS5 fallback; Line 472 condition must be modified to remove zero-results check | Concepts: problem-solution, how-it-works, pattern, trade-off | Files: src/servers/search-server.ts\n\n---\nDate: 11/17/2025, 11:42:18 PM\n\n---\n\n## Chroma requires query text; FTS5 fallback logic is incorrect\n*Source: claude-mem://observation/10735*\n\n**Chroma cannot do filter-only queries, and FTS5 fallback on zero results is pointless.**\n\nTwo critical insights emerge about the search architecture. First, Chroma cannot perform filter-only queries without query text because it fundamentally operates on semantic search via vector embeddings. When query is undefined, the system must bypass Chroma entirely and proceed directly to SQLite structured filtering. Second, the current FTS5 fallback logic at line 472 is fundamentally flawed - it triggers when Chroma returns zero results. However, since FTS5 maintains a 1:1 copy of the SQLite data that Chroma also indexes, if Chroma returns zero results, FTS5 will also return zero results. FTS5 fallback should only activate when Chroma is unavailable or encounters an error, not when it successfully returns an empty result set.\n\n---\nType: discovery | Facts: Chroma vector database requires query text for semantic search operations; Filter-only queries must skip Chroma and use SQLite structured filtering directly; FTS5 fallback at line 472 triggers on zero results, not on Chroma errors; FTS5 contains 1:1 copy of SQLite data, so zero Chroma results means zero FTS5 results; FTS5 fallback should only activate when Chroma is unavailable or errors, not on empty results | Concepts: problem-solution, gotcha, how-it-works, why-it-exists | Files: src/servers/search-server.ts\n\n---\nDate: 11/17/2025, 11:41:43 PM"}]
\ No newline at end of file
diff --git a/test-results/test-12-context-hook-changes.json b/test-results/test-12-context-hook-changes.json
deleted file mode 100644
index 20e6b57d..00000000
--- a/test-results/test-12-context-hook-changes.json
+++ /dev/null
@@ -1 +0,0 @@
-[{"type":"text","text":"## Build and sync claude-mem plugin to marketplace location\n*Source: claude-mem://observation/10758*\n\n**Built all hooks and services, synced to marketplace directory, and installed dependencies successfully.**\n\nThe claude-mem plugin was fully built and deployed to the marketplace directory. The build process compiled all React components, bundled the worker service and search server, and built each individual hook. The build script used scripts/build-hooks.js to generate production-ready bundles in the plugin/scripts/ directory. After building, rsync transferred all 11781 files to the marketplace directory at ~/.claude/plugins/marketplaces/thedotmack/, removing the .git directory and .install-version file in the process. The sync command used --delete flag to ensure the marketplace directory perfectly mirrors the development directory. Finally, npm install ran in the marketplace directory to ensure all dependencies are available for the deployed plugin.\n\n---\nType: change | Facts: Built claude-mem version 6.0.9 including React viewer, worker service, search server, and all hooks; Worker service built to 1338.75 KB, search server to 335.14 KB; Synced 11781 files from development directory to ~/.claude/plugins/marketplaces/thedotmack/; All hooks built successfully: context-hook (38.29 KB), new-hook (32.14 KB), save-hook (32.49 KB), summary-hook (34.61 KB), cleanup-hook (31.38 KB), user-message-hook (2.24 KB); Dependencies auto-installed in marketplace location after sync | Concepts: what-changed, how-it-works | Files: scripts/build-hooks.js, package.json, plugin/scripts/worker-service.cjs, plugin/scripts/search-server.mjs, plugin/scripts/context-hook.js, plugin/scripts/new-hook.js, plugin/scripts/save-hook.js, plugin/scripts/summary-hook.js, plugin/scripts/cleanup-hook.js, plugin/scripts/user-message-hook.js, plugin/ui/viewer-bundle.js, plugin/ui/viewer.html\n\n---\nDate: 11/17/2025, 11:51:51 PM\n\n---\n\n## Search API crashes with \"Cannot read properties of undefined (reading 'replace')\" error\n*Source: claude-mem://observation/10705*\n\n**9 of 12 failed tests show identical JavaScript error accessing undefined property, 3 show no results.**\n\nFresh test results reveal a critical JavaScript error in the search API. Nine tests crash with \"Cannot read properties of undefined (reading 'replace')\", indicating that some code is attempting to call .replace() on an undefined value. This error affects three distinct query categories: file-specific change queries (tests 11-13), concept-based queries (tests 14-15), and type-filtered queries (tests 16-19), plus multi-parameter file-type combinations (test 28). The pattern suggests that when the search API processes queries with file parameters, concept parameters, or type filters, it encounters an undefined value where it expects a string. The remaining three failures (tests 05, 08, 21) return clean \"No results found\" messages without crashing, indicating they may be legitimate empty result sets or different search code paths that don't trigger the bug.\n\n---\nType: discovery | Facts: 9 tests fail with error \"Cannot read properties of undefined (reading 'replace')\"; Tests 11, 12, 13 fail with replace error on file-specific change queries; Tests 14, 15 fail with replace error on concept-based queries for patterns and gotchas; Tests 16, 17, 18, 19 fail with replace error on type-filtered queries for discoveries, bugfixes, features, decisions; Test 28 fails with replace error on file-type combination query; Test 05 returns \"No results found matching 'PM2 instead of direct process'\" without crashing; Test 08 returns \"No results found matching 'worker service debugging'\" without crashing; Test 21 returns \"No results found matching 'build and deploy'\" without crashing; The replace error suggests undefined variable being passed to a string replace operation; All file-specific queries trigger the replace error; All concept-based queries trigger the replace error; All type-filtered queries trigger the replace error | Concepts: problem-solution, gotcha, pattern | Files: test-results/test-05-pm2-decision.json, test-results/test-08-worker-debugging.json, test-results/test-11-search-server-changes.json, test-results/test-12-context-hook-changes.json, test-results/test-13-worker-service-changes.json, test-results/test-14-patterns.json, test-results/test-15-gotchas.json, test-results/test-16-discoveries.json, test-results/test-17-all-bugfixes.json, test-results/test-18-all-features.json, test-results/test-19-all-decisions.json, test-results/test-21-prompt-search.json, test-results/test-28-file-type-combo.json\n\n---\nDate: 11/17/2025, 10:04:43 PM\n\n---\n\n## Fresh test results reveal 12 failing tests with empty or no results\n*Source: claude-mem://observation/10704*\n\n**28 search API tests show 16 successes and 12 failures, all failures return empty or no results.**\n\nAfter clearing old logs and running fresh tests, a summary analysis reveals significant issues with the search API. Out of 28 tests, 12 are failing by returning empty results or no results at all. The failures appear to cluster around specific query types: PM2 decision search, worker debugging troubleshooting, file-specific change queries (search-server, context-hook, worker-service), concept-based queries (patterns, gotchas), type-filtered queries (discoveries, bugfixes, features, decisions), user prompt search, and multi-parameter combinations involving files and types. The successful tests (16 total) all returned exactly one result each and cover semantic queries, some decision queries, some troubleshooting queries, session search, dedicated endpoint tests, timeline queries, and some multi-parameter tests. This pattern suggests potential issues with certain search filters, concept matching, or type filtering functionality in the search API.\n\n---\nType: discovery | Facts: 16 tests returned SUCCESS with results: tests 01, 02, 03, 04, 06, 07, 09, 10, 20, 22, 23, 24, 25, 26, 27; 12 tests returned FAIL/EMPTY with no results: tests 05, 08, 11, 12, 13, 14, 15, 16, 17, 18, 19, 21, 28; Test 05 pm2-decision query failed to return results; Test 08 worker-debugging query failed to return results; Test 11 search-server-changes query failed to return results; Test 12 context-hook-changes query failed to return results; Test 13 worker-service-changes query failed to return results; Test 14 patterns concept query failed to return results; Test 15 gotchas concept query failed to return results; Test 16 discoveries type filter query failed to return results; Test 17 all-bugfixes type filter query failed to return results; Test 18 all-features type filter query failed to return results; Test 19 all-decisions type filter query failed to return results; Test 21 prompt-search query failed to return results; Test 28 file-type-combo multi-parameter query failed to return results; All successful tests returned exactly 1 result each | Concepts: problem-solution, pattern, how-it-works | Files: test-results/test-01-worker-service-startup.json, test-results/test-02-sqlite-fts5-implementation.json, test-results/test-03-hook-lifecycle-flow.json, test-results/test-04-build-pipeline-process.json, test-results/test-05-pm2-decision.json, test-results/test-06-search-architecture-decision.json, test-results/test-07-mcp-dry-decision.json, test-results/test-08-worker-debugging.json, test-results/test-09-hook-timeout.json, test-results/test-10-database-migration.json, test-results/test-11-search-server-changes.json, test-results/test-12-context-hook-changes.json, test-results/test-13-worker-service-changes.json, test-results/test-14-patterns.json, test-results/test-15-gotchas.json, test-results/test-16-discoveries.json, test-results/test-17-all-bugfixes.json, test-results/test-18-all-features.json, test-results/test-19-all-decisions.json, test-results/test-20-session-search.json, test-results/test-21-prompt-search.json, test-results/test-22-decisions-endpoint.json, test-results/test-23-changes-endpoint.json, test-results/test-24-how-it-works-endpoint.json, test-results/test-25-contextualize-endpoint.json, test-results/test-26-timeline-around-observation.json, test-results/test-27-multi-param-combo.json, test-results/test-28-file-type-combo.json\n\n---\nDate: 11/17/2025, 10:04:28 PM\n\n---\n\n## Build and Deployment Cycle Completed\n*Source: claude-mem://observation/10689*\n\n**Successfully built all hooks, worker service, search server, synced to marketplace, and restarted worker.**\n\nThe complete build and deployment cycle executed successfully for claude-mem version 6.0.9. The build process compiled all components including the React viewer bundle with associated assets, the worker service (1.3 MB), search server (333 KB), and six hooks ranging from 2-38 KB each. After building, rsync synchronized 11,781 files to the marketplace plugin directory and ran npm install to ensure dependencies were up to date. The worker was then restarted to load the new code. This deployment includes all the newly implemented unified search API features with catch-all parameters, preparing the system for comprehensive testing of the search endpoints.\n\n---\nType: change | Facts: Build process compiled React viewer bundle and HTML template with font assets and icon files; Worker service built to 1337.16 KB at plugin/scripts/worker-service.cjs; Search server built to 332.92 KB at plugin/scripts/search-server.mjs; All hooks built: context-hook (38.29 KB), new-hook (32.14 KB), save-hook (32.49 KB), summary-hook (34.61 KB), cleanup-hook (31.38 KB), user-message-hook (2.24 KB); Rsync transferred 11781 files to ~/.claude/plugins/marketplaces/thedotmack/ with npm install executed; Build version 6.0.9 deployed successfully | Concepts: how-it-works, what-changed | Files: plugin/ui/viewer-bundle.js, plugin/ui/viewer.html, plugin/scripts/worker-service.cjs, plugin/scripts/search-server.mjs, plugin/scripts/context-hook.js, plugin/scripts/new-hook.js, plugin/scripts/save-hook.js, plugin/scripts/summary-hook.js, plugin/scripts/cleanup-hook.js, plugin/scripts/user-message-hook.js\n\n---\nDate: 11/17/2025, 9:48:29 PM\n\n---\n\n## Build and Deployment of Unified Search API\n*Source: claude-mem://observation/10682*\n\n**Successfully built all hooks, worker service, and search server, then synced to marketplace and restarted worker.**\n\nThe unified search API implementation was successfully built and deployed. The build process generated all necessary components including the worker service, search server, and various hooks, with the search server weighing in at 332.97 KB. The React viewer UI was also compiled with all required assets. After building, the entire project (11,781 files) was synced to the Claude marketplace plugins directory using rsync, followed by npm install to ensure all dependencies were properly installed. Finally, the worker service was restarted to activate the new unified search endpoints. This deployment marks the completion of the unified search API architecture with its six public-facing endpoints and backward-compatible granular routing.\n\n---\nType: change | Facts: Build process compiled 8 components: worker-service (1337.16 KB), search-server (332.97 KB), context-hook (38.29 KB), new-hook (32.14 KB), save-hook (32.49 KB), summary-hook (34.61 KB), cleanup-hook (31.38 KB), and user-message-hook (2.24 KB); React viewer built successfully with viewer-bundle.js, viewer.html, font assets, and 4 icon SVG files; Project synced to ~/.claude/plugins/marketplaces/thedotmack/ using rsync with 11,781 files transferred; npm install executed in marketplace directory to ensure dependencies are current; Worker service restarted to activate the newly deployed unified search API endpoints | Concepts: what-changed, how-it-works | Files: plugin/scripts/worker-service.cjs, plugin/scripts/search-server.mjs, plugin/scripts/context-hook.js, plugin/scripts/new-hook.js, plugin/scripts/save-hook.js, plugin/scripts/summary-hook.js, plugin/scripts/cleanup-hook.js, plugin/scripts/user-message-hook.js, plugin/ui/viewer-bundle.js, plugin/ui/viewer.html\n\n---\nDate: 11/17/2025, 9:43:18 PM"}]
\ No newline at end of file
diff --git a/test-results/test-13-worker-service-changes.json b/test-results/test-13-worker-service-changes.json
deleted file mode 100644
index 3807d8c6..00000000
--- a/test-results/test-13-worker-service-changes.json
+++ /dev/null
@@ -1 +0,0 @@
-[{"type":"text","text":"## Build and sync claude-mem plugin to marketplace location\n*Source: claude-mem://observation/10758*\n\n**Built all hooks and services, synced to marketplace directory, and installed dependencies successfully.**\n\nThe claude-mem plugin was fully built and deployed to the marketplace directory. The build process compiled all React components, bundled the worker service and search server, and built each individual hook. The build script used scripts/build-hooks.js to generate production-ready bundles in the plugin/scripts/ directory. After building, rsync transferred all 11781 files to the marketplace directory at ~/.claude/plugins/marketplaces/thedotmack/, removing the .git directory and .install-version file in the process. The sync command used --delete flag to ensure the marketplace directory perfectly mirrors the development directory. Finally, npm install ran in the marketplace directory to ensure all dependencies are available for the deployed plugin.\n\n---\nType: change | Facts: Built claude-mem version 6.0.9 including React viewer, worker service, search server, and all hooks; Worker service built to 1338.75 KB, search server to 335.14 KB; Synced 11781 files from development directory to ~/.claude/plugins/marketplaces/thedotmack/; All hooks built successfully: context-hook (38.29 KB), new-hook (32.14 KB), save-hook (32.49 KB), summary-hook (34.61 KB), cleanup-hook (31.38 KB), user-message-hook (2.24 KB); Dependencies auto-installed in marketplace location after sync | Concepts: what-changed, how-it-works | Files: scripts/build-hooks.js, package.json, plugin/scripts/worker-service.cjs, plugin/scripts/search-server.mjs, plugin/scripts/context-hook.js, plugin/scripts/new-hook.js, plugin/scripts/save-hook.js, plugin/scripts/summary-hook.js, plugin/scripts/cleanup-hook.js, plugin/scripts/user-message-hook.js, plugin/ui/viewer-bundle.js, plugin/ui/viewer.html\n\n---\nDate: 11/17/2025, 11:51:51 PM\n\n---\n\n## Search API crashes with \"Cannot read properties of undefined (reading 'replace')\" error\n*Source: claude-mem://observation/10705*\n\n**9 of 12 failed tests show identical JavaScript error accessing undefined property, 3 show no results.**\n\nFresh test results reveal a critical JavaScript error in the search API. Nine tests crash with \"Cannot read properties of undefined (reading 'replace')\", indicating that some code is attempting to call .replace() on an undefined value. This error affects three distinct query categories: file-specific change queries (tests 11-13), concept-based queries (tests 14-15), and type-filtered queries (tests 16-19), plus multi-parameter file-type combinations (test 28). The pattern suggests that when the search API processes queries with file parameters, concept parameters, or type filters, it encounters an undefined value where it expects a string. The remaining three failures (tests 05, 08, 21) return clean \"No results found\" messages without crashing, indicating they may be legitimate empty result sets or different search code paths that don't trigger the bug.\n\n---\nType: discovery | Facts: 9 tests fail with error \"Cannot read properties of undefined (reading 'replace')\"; Tests 11, 12, 13 fail with replace error on file-specific change queries; Tests 14, 15 fail with replace error on concept-based queries for patterns and gotchas; Tests 16, 17, 18, 19 fail with replace error on type-filtered queries for discoveries, bugfixes, features, decisions; Test 28 fails with replace error on file-type combination query; Test 05 returns \"No results found matching 'PM2 instead of direct process'\" without crashing; Test 08 returns \"No results found matching 'worker service debugging'\" without crashing; Test 21 returns \"No results found matching 'build and deploy'\" without crashing; The replace error suggests undefined variable being passed to a string replace operation; All file-specific queries trigger the replace error; All concept-based queries trigger the replace error; All type-filtered queries trigger the replace error | Concepts: problem-solution, gotcha, pattern | Files: test-results/test-05-pm2-decision.json, test-results/test-08-worker-debugging.json, test-results/test-11-search-server-changes.json, test-results/test-12-context-hook-changes.json, test-results/test-13-worker-service-changes.json, test-results/test-14-patterns.json, test-results/test-15-gotchas.json, test-results/test-16-discoveries.json, test-results/test-17-all-bugfixes.json, test-results/test-18-all-features.json, test-results/test-19-all-decisions.json, test-results/test-21-prompt-search.json, test-results/test-28-file-type-combo.json\n\n---\nDate: 11/17/2025, 10:04:43 PM\n\n---\n\n## Fresh test results reveal 12 failing tests with empty or no results\n*Source: claude-mem://observation/10704*\n\n**28 search API tests show 16 successes and 12 failures, all failures return empty or no results.**\n\nAfter clearing old logs and running fresh tests, a summary analysis reveals significant issues with the search API. Out of 28 tests, 12 are failing by returning empty results or no results at all. The failures appear to cluster around specific query types: PM2 decision search, worker debugging troubleshooting, file-specific change queries (search-server, context-hook, worker-service), concept-based queries (patterns, gotchas), type-filtered queries (discoveries, bugfixes, features, decisions), user prompt search, and multi-parameter combinations involving files and types. The successful tests (16 total) all returned exactly one result each and cover semantic queries, some decision queries, some troubleshooting queries, session search, dedicated endpoint tests, timeline queries, and some multi-parameter tests. This pattern suggests potential issues with certain search filters, concept matching, or type filtering functionality in the search API.\n\n---\nType: discovery | Facts: 16 tests returned SUCCESS with results: tests 01, 02, 03, 04, 06, 07, 09, 10, 20, 22, 23, 24, 25, 26, 27; 12 tests returned FAIL/EMPTY with no results: tests 05, 08, 11, 12, 13, 14, 15, 16, 17, 18, 19, 21, 28; Test 05 pm2-decision query failed to return results; Test 08 worker-debugging query failed to return results; Test 11 search-server-changes query failed to return results; Test 12 context-hook-changes query failed to return results; Test 13 worker-service-changes query failed to return results; Test 14 patterns concept query failed to return results; Test 15 gotchas concept query failed to return results; Test 16 discoveries type filter query failed to return results; Test 17 all-bugfixes type filter query failed to return results; Test 18 all-features type filter query failed to return results; Test 19 all-decisions type filter query failed to return results; Test 21 prompt-search query failed to return results; Test 28 file-type-combo multi-parameter query failed to return results; All successful tests returned exactly 1 result each | Concepts: problem-solution, pattern, how-it-works | Files: test-results/test-01-worker-service-startup.json, test-results/test-02-sqlite-fts5-implementation.json, test-results/test-03-hook-lifecycle-flow.json, test-results/test-04-build-pipeline-process.json, test-results/test-05-pm2-decision.json, test-results/test-06-search-architecture-decision.json, test-results/test-07-mcp-dry-decision.json, test-results/test-08-worker-debugging.json, test-results/test-09-hook-timeout.json, test-results/test-10-database-migration.json, test-results/test-11-search-server-changes.json, test-results/test-12-context-hook-changes.json, test-results/test-13-worker-service-changes.json, test-results/test-14-patterns.json, test-results/test-15-gotchas.json, test-results/test-16-discoveries.json, test-results/test-17-all-bugfixes.json, test-results/test-18-all-features.json, test-results/test-19-all-decisions.json, test-results/test-20-session-search.json, test-results/test-21-prompt-search.json, test-results/test-22-decisions-endpoint.json, test-results/test-23-changes-endpoint.json, test-results/test-24-how-it-works-endpoint.json, test-results/test-25-contextualize-endpoint.json, test-results/test-26-timeline-around-observation.json, test-results/test-27-multi-param-combo.json, test-results/test-28-file-type-combo.json\n\n---\nDate: 11/17/2025, 10:04:28 PM\n\n---\n\n## Search Results Return Structured Observation Content with Rich Metadata\n*Source: claude-mem://observation/10695*\n\n**Worker service query returned six observations with complete narratives, facts, concepts, and source links**\n\nExamination of test-01-worker-service-startup.json reveals the structure and quality of search API responses. The worker service startup query successfully retrieved six relevant observations demonstrating that FTS5 keyword search provides meaningful results. Each observation includes comprehensive metadata: title, subtitle (one-line summary), full narrative text explaining the context, structured facts list with specific details, concept tags (what-changed, how-it-works, problem-solution, gotcha), associated file paths, creation dates, and observation type classification. Results are formatted with source links using the claude-mem://observation/ID format for easy reference. The retrieved observations span multiple aspects of worker service startup including port file handling migration to plugin data directory, getPackageRoot path resolution dependencies, comprehensive production failure analysis documenting 8 critical issues causing 100% failure rate, and ChromaSync initialization timing problems. The diversity of observation types and relevance to the search query demonstrates the search system's ability to provide rich, contextual results for SessionStart injection.\n\n---\nType: discovery | Facts: Search API returns observations with title, subtitle, full narrative, structured facts list, concept tags, file paths, creation dates, and observation type; Results tagged with source links using claude-mem://observation/ID format for reference; Worker service startup query retrieved six relevant observations spanning October 18 to November 17, 2025; Retrieved observations include diverse types (change, discovery, bugfix) covering port file handling, getPackageRoot issues, production failures, and ChromaSync initialization; Test result format stores array of observation objects with type and text fields containing formatted markdown content | Concepts: how-it-works, pattern | Files: test-results/test-05-pm2-decision.json, test-results/test-01-worker-service-startup.json\n\n---\nDate: 11/17/2025, 9:52:44 PM\n\n---\n\n## Build and Deployment Cycle Completed\n*Source: claude-mem://observation/10689*\n\n**Successfully built all hooks, worker service, search server, synced to marketplace, and restarted worker.**\n\nThe complete build and deployment cycle executed successfully for claude-mem version 6.0.9. The build process compiled all components including the React viewer bundle with associated assets, the worker service (1.3 MB), search server (333 KB), and six hooks ranging from 2-38 KB each. After building, rsync synchronized 11,781 files to the marketplace plugin directory and ran npm install to ensure dependencies were up to date. The worker was then restarted to load the new code. This deployment includes all the newly implemented unified search API features with catch-all parameters, preparing the system for comprehensive testing of the search endpoints.\n\n---\nType: change | Facts: Build process compiled React viewer bundle and HTML template with font assets and icon files; Worker service built to 1337.16 KB at plugin/scripts/worker-service.cjs; Search server built to 332.92 KB at plugin/scripts/search-server.mjs; All hooks built: context-hook (38.29 KB), new-hook (32.14 KB), save-hook (32.49 KB), summary-hook (34.61 KB), cleanup-hook (31.38 KB), user-message-hook (2.24 KB); Rsync transferred 11781 files to ~/.claude/plugins/marketplaces/thedotmack/ with npm install executed; Build version 6.0.9 deployed successfully | Concepts: how-it-works, what-changed | Files: plugin/ui/viewer-bundle.js, plugin/ui/viewer.html, plugin/scripts/worker-service.cjs, plugin/scripts/search-server.mjs, plugin/scripts/context-hook.js, plugin/scripts/new-hook.js, plugin/scripts/save-hook.js, plugin/scripts/summary-hook.js, plugin/scripts/cleanup-hook.js, plugin/scripts/user-message-hook.js\n\n---\nDate: 11/17/2025, 9:48:29 PM"}]
\ No newline at end of file
diff --git a/test-results/test-14-patterns.json b/test-results/test-14-patterns.json
deleted file mode 100644
index fa0406c6..00000000
--- a/test-results/test-14-patterns.json
+++ /dev/null
@@ -1 +0,0 @@
-[{"type":"text","text":"## Add filter-only query path to searchUserPrompts method\n*Source: claude-mem://observation/10755*\n\n**Method accepts undefined query and handles dual-path logic with separate parameter arrays for clarity.**\n\nThe searchUserPrompts method has been enhanced with filter-only query support, completing the pattern established in searchObservations and searchSessions. The method signature now accepts optional query parameter. The implementation builds base filter conditions once for project and date range filters, then diverges into two paths. The filter-only path (when query is undefined) validates that at least some filters exist, then queries the user_prompts table directly with a WHERE clause, joining sdk_sessions for project filtering. The FTS5 path rebuilds filter conditions into a separate ftsParams array to avoid parameter ordering conflicts between the two paths. This dual-path approach enables both semantic/keyword search via FTS5 and pure metadata filtering via direct SQLite queries, completing the architectural pattern across all three search methods.\n\n---\nType: feature | Facts: searchUserPrompts signature changed to accept `query: string | undefined` at line 541; Filter conditions built once and shared between filter-only and FTS5 paths at lines 546-563; Filter-only path at lines 566-588 validates filters exist and queries user_prompts table directly; FTS5 path at lines 591-616 rebuilds filter conditions with separate ftsParams array to avoid parameter conflicts; Filter-only path joins sdk_sessions table for project filtering support; Documentation updated to clarify dual-mode operation matching searchObservations and searchSessions patterns | Concepts: what-changed, how-it-works, pattern, gotcha | Files: src/services/sqlite/SessionSearch.ts\n\n---\nDate: 11/17/2025, 11:50:08 PM\n\n---\n\n## searchUserPrompts Uses Custom ORDER BY Logic Instead of buildOrderClause\n*Source: claude-mem://observation/10754*\n\n**Unlike searchObservations, searchUserPrompts implements inline ORDER BY construction with hard-coded table aliases rather than using buildOrderClause helper.**\n\nThe searchUserPrompts method implements its ORDER BY logic inline rather than using the buildOrderClause helper method used by searchObservations and searchSessions. Lines 619-623 show a ternary expression that constructs the ORDER BY clause with hard-coded references to user_prompts_fts.rank for relevance ordering and up.created_at_epoch for date-based sorting. The main SQL query joins three tables: user_prompts, user_prompts_fts, and sdk_sessions, with the sdk_sessions join enabling project-based filtering. When implementing the filter-only path for this method, the code will need to construct a query that joins only user_prompts and sdk_sessions (skipping user_prompts_fts), and the ORDER BY logic will need to handle the absence of FTS5 rank data, defaulting to date-based ordering when query is undefined.\n\n---\nType: discovery | Facts: searchUserPrompts constructs ORDER BY clause inline at lines 619-623 instead of calling buildOrderClause; The inline ORDER BY uses hard-coded table alias user_prompts_fts.rank for relevance ordering; Date-based ordering uses up.created_at_epoch for both ascending and descending sorts; The SQL query joins user_prompts, user_prompts_fts, and sdk_sessions tables; searchUserPrompts will need filter-only path to query user_prompts without user_prompts_fts JOIN when query is undefined | Concepts: how-it-works, pattern | Files: src/services/sqlite/SessionSearch.ts\n\n---\nDate: 11/17/2025, 11:50:07 PM\n\n---\n\n## User Prompts Search Joins Three Tables for Filtering\n*Source: claude-mem://observation/10753*\n\n**searchUserPrompts method joins user_prompts, user_prompts_fts, and sdk_sessions tables to enable project-based filtering.**\n\nThe searchUserPrompts method demonstrates a more complex query structure than searchObservations or searchSessions due to the need for project filtering. Since user_prompts don't directly store project information, the method joins with the sdk_sessions table using claude_session_id as the key, which allows filtering by the project field from sdk_sessions. The query structure follows the same FTS5 pattern seen in other search methods: it joins the base table (user_prompts) with its FTS5 virtual table (user_prompts_fts) via rowid, uses the MATCH operator for full-text search, and orders by rank for relevance-based results. The default limit is set to 20 rather than 50, suggesting user prompts are typically returned in smaller batches. Like the other search methods, rank values are normalized to a 0-1 score scale where higher values indicate better matches, inverting FTS5's native lower-is-better ranking.\n\n---\nType: discovery | Facts: searchUserPrompts method joins three tables: user_prompts, user_prompts_fts, and sdk_sessions; sdk_sessions table joined via claude_session_id to enable project filtering on user prompts; user_prompts table aliased as 'up' and uses MATCH operator on user_prompts_fts virtual table; Default limit for user prompts is 20, compared to 50 for observations and sessions; Method supports dateRange filtering using created_at_epoch column from user_prompts table; Rank normalization converts FTS5 rank scores to 0-1 scale where higher values indicate better matches | Concepts: how-it-works, pattern | Files: /Users/alexnewman/Scripts/claude-mem/src/services/sqlite/SessionSearch.ts\n\n---\nDate: 11/17/2025, 11:49:42 PM\n\n---\n\n## Add filter-only query path to searchSessions method\n*Source: claude-mem://observation/10751*\n\n**Method now accepts undefined query and queries session_summaries table directly for metadata-only filtering.**\n\nThe searchSessions method in SessionSearch.ts has been enhanced to support filter-only queries without query text. The method signature now accepts `query: string | undefined`, enabling two distinct code paths. When query is undefined, a new filter-only path (lines 331-352) bypasses FTS5 entirely and queries the session_summaries table directly using metadata filters like project or date range. This path validates that at least some filters exist, throwing an error if both query and filters are missing. When query text is provided, the existing FTS5 search path executes as before. This dual-mode approach allows the system to handle both semantic/keyword search and pure metadata filtering, addressing the architectural requirement that filter-only queries skip text search mechanisms.\n\n---\nType: feature | Facts: searchSessions signature changed to accept `query: string | undefined` at line 327; New filter-only path added at lines 331-352 that bypasses FTS5 when query is undefined; Filter-only path throws error if neither query nor filters provided at line 336; Direct SQLite query on session_summaries table uses WHERE clause built from metadata filters; FTS5 path at line 355 only executes when query text exists; Documentation updated to clarify dual-mode operation: FTS5 fallback vs filter-only direct query | Concepts: what-changed, how-it-works, pattern | Files: src/services/sqlite/SessionSearch.ts\n\n---\nDate: 11/17/2025, 11:49:23 PM\n\n---\n\n## SessionSearch Methods Use FTS5 for Full-Text Search\n*Source: claude-mem://observation/10750*\n\n**Observed searchSessions method implementation showing FTS5 virtual table usage with rank-based relevance scoring.**\n\nThe SessionSearch.searchSessions method demonstrates the FTS5 full-text search implementation for session summaries. The method builds a SQL query that joins the session_summaries table with its corresponding session_summaries_fts virtual table, using the MATCH operator for full-text search. The query construction includes filter support (excluding type filters which aren't applicable to sessions), and adjusts column names to account for session_summaries using files_edited instead of files_modified. When orderBy is set to 'relevance', the query orders by session_summaries_fts.rank in ascending order, where lower rank values indicate better matches (FTS5 convention). The method then normalizes these ranks into 0-1 scores where higher is better. This pattern is consistent with searchObservations and searchUserPrompts methods, all using the same FTS5-based approach as a fallback when ChromaDB semantic search is unavailable.\n\n---\nType: discovery | Facts: searchSessions method in SessionSearch.ts performs FTS5 full-text search on session summaries; Method queries session_summaries_fts virtual table and joins with session_summaries table; FTS5 relevance ranking uses rank ASC ordering where lower rank values indicate better matches; Method supports three order modes: relevance (FTS5 rank), date_asc, and date_desc; Files_modified column name is replaced with files_edited for session_summaries table compatibility; Method uses escapeFTS5_fallback_when_chroma_unavailable to sanitize query input | Concepts: how-it-works, pattern | Files: /Users/alexnewman/Scripts/claude-mem/src/services/sqlite/SessionSearch.ts\n\n---\nDate: 11/17/2025, 11:49:09 PM"}]
\ No newline at end of file
diff --git a/test-results/test-15-gotchas.json b/test-results/test-15-gotchas.json
deleted file mode 100644
index 1d5f1b23..00000000
--- a/test-results/test-15-gotchas.json
+++ /dev/null
@@ -1 +0,0 @@
-[{"type":"text","text":"## Unified search handler implements Chroma-first with FTS5 fallback on zero results\n*Source: claude-mem://observation/10757*\n\n**Lines 403-489 show current implementation attempts Chroma first, falls back to FTS5 when empty.**\n\nThe unified search handler in search-server.ts (lines 390-489) reveals the current implementation architecture. The handler first attempts Chroma semantic search when chromaClient is available, calling queryChroma with the query parameter at line 419. If Chroma throws an error, it catches at line 465 and falls through to FTS5. However, the problematic FTS5 fallback logic at line 470 triggers when all result arrays are empty, not just on errors. This contradicts the insight that FTS5 contains identical data to Chroma, making zero-result fallback pointless. Additionally, lines 476-483 call search methods (searchObservations, searchSessions, searchUserPrompts) with the query parameter without checking if it's undefined, which will cause failures when query is not provided. The handler needs modification to skip Chroma when query is undefined and only fall back to FTS5 on Chroma errors.\n\n---\nType: discovery | Facts: Line 403 checks `if (chromaClient)` to attempt Chroma semantic search first; Line 419 calls `queryChroma(query, 100, whereFilter)` with query parameter as required; Lines 465-467 catch Chroma errors and fall through to FTS5 fallback; Line 470 triggers FTS5 fallback when all result arrays have zero length; Lines 476-483 call SessionSearch methods with query parameter without checking if undefined; Hybrid search applies 90-day recency filter at lines 424-431 | Concepts: how-it-works, problem-solution, gotcha | Files: src/servers/search-server.ts\n\n---\nDate: 11/17/2025, 11:50:29 PM\n\n---\n\n## Fixed Incorrect Parameter Array in searchUserPrompts FTS5 Path\n*Source: claude-mem://observation/10756*\n\n**Changed params to ftsParams in FTS5 query execution to use correct parameter array with ftsQuery.**\n\nA parameter array mismatch bug was fixed in the searchUserPrompts method's FTS5 code path. The method creates two separate parameter arrays: 'params' for the filter-only path and 'ftsParams' for the FTS5 path. The FTS5 path correctly initialized ftsParams with the escaped query and rebuilt all filter conditions into this new array, but then incorrectly used the 'params' array when executing the SQL query. This would have caused a parameter binding mismatch where the SQL query expected parameters in one order (starting with ftsQuery) but received them in a different order or with missing values. The fix ensures that when the FTS5 path is taken (query text provided), the query uses ftsParams.push() for limit/offset and passes ftsParams to the db.prepare().all() call, maintaining correct parameter alignment throughout the FTS5 execution path.\n\n---\nType: bugfix | Facts: File modified: /Users/alexnewman/Scripts/claude-mem/src/services/sqlite/SessionSearch.ts; Bug was in searchUserPrompts method's FTS5 execution path using wrong parameter array; Changed params.push(limit, offset) to ftsParams.push(limit, offset); Changed this.db.prepare(sql).all(...params) to this.db.prepare(sql).all(...ftsParams); The ftsParams array was created separately for FTS5 path but not being used in query execution; Bug would have caused SQL parameter binding mismatch in FTS5 search path for user prompts | Concepts: problem-solution, what-changed, gotcha | Files: /Users/alexnewman/Scripts/claude-mem/src/services/sqlite/SessionSearch.ts\n\n---\nDate: 11/17/2025, 11:50:17 PM\n\n---\n\n## Add filter-only query path to searchUserPrompts method\n*Source: claude-mem://observation/10755*\n\n**Method accepts undefined query and handles dual-path logic with separate parameter arrays for clarity.**\n\nThe searchUserPrompts method has been enhanced with filter-only query support, completing the pattern established in searchObservations and searchSessions. The method signature now accepts optional query parameter. The implementation builds base filter conditions once for project and date range filters, then diverges into two paths. The filter-only path (when query is undefined) validates that at least some filters exist, then queries the user_prompts table directly with a WHERE clause, joining sdk_sessions for project filtering. The FTS5 path rebuilds filter conditions into a separate ftsParams array to avoid parameter ordering conflicts between the two paths. This dual-path approach enables both semantic/keyword search via FTS5 and pure metadata filtering via direct SQLite queries, completing the architectural pattern across all three search methods.\n\n---\nType: feature | Facts: searchUserPrompts signature changed to accept `query: string | undefined` at line 541; Filter conditions built once and shared between filter-only and FTS5 paths at lines 546-563; Filter-only path at lines 566-588 validates filters exist and queries user_prompts table directly; FTS5 path at lines 591-616 rebuilds filter conditions with separate ftsParams array to avoid parameter conflicts; Filter-only path joins sdk_sessions table for project filtering support; Documentation updated to clarify dual-mode operation matching searchObservations and searchSessions patterns | Concepts: what-changed, how-it-works, pattern, gotcha | Files: src/services/sqlite/SessionSearch.ts\n\n---\nDate: 11/17/2025, 11:50:08 PM\n\n---\n\n## Line 472 Incorrectly Falls Back to FTS5 on Empty ChromaDB Results\n*Source: claude-mem://observation/10740*\n\n**The condition checks for zero results instead of ChromaDB failure, causing inappropriate FTS5 fallback on valid empty responses.**\n\nLine 472 in the current code contains the bug causing inappropriate FTS5 fallback behavior. The condition `if (observations.length === 0 && sessions.length === 0 && prompts.length === 0)` checks whether ChromaDB returned empty result arrays, but this check cannot distinguish between two scenarios: ChromaDB erroring (caught at line 465) versus ChromaDB successfully returning no matching results. The catch block at line 465 handles ChromaDB errors and execution falls through to line 472, where the empty arrays condition triggers FTS5 fallback. However, when ChromaDB succeeds but finds no semantic matches, it also returns empty arrays, incorrectly triggering the same FTS5 fallback. The solution is to introduce a chromaFailed boolean flag that is set to true only within the catch block when ChromaDB actually errors. Line 472 should then check this flag instead of checking array lengths, ensuring FTS5 fallback only occurs on actual ChromaDB failures, not on valid empty results.\n\n---\nType: discovery | Facts: Line 472 contains condition checking if observations, sessions, and prompts arrays are all empty; The catch block at line 465 handles ChromaDB errors and falls through to line 472; Line 472's zero-length check triggers FTS5 fallback both when ChromaDB errors AND when ChromaDB successfully returns empty results; ChromaDB returning zero results is a valid answer that should not trigger FTS5 fallback; The fix requires introducing a chromaFailed flag set only in the catch block, not on successful empty results; Line 472 condition should check the chromaFailed flag instead of checking result array lengths | Concepts: problem-solution, gotcha, how-it-works\n\n---\nDate: 11/17/2025, 11:42:30 PM\n\n---\n\n## Chroma requires query text; FTS5 fallback logic is incorrect\n*Source: claude-mem://observation/10735*\n\n**Chroma cannot do filter-only queries, and FTS5 fallback on zero results is pointless.**\n\nTwo critical insights emerge about the search architecture. First, Chroma cannot perform filter-only queries without query text because it fundamentally operates on semantic search via vector embeddings. When query is undefined, the system must bypass Chroma entirely and proceed directly to SQLite structured filtering. Second, the current FTS5 fallback logic at line 472 is fundamentally flawed - it triggers when Chroma returns zero results. However, since FTS5 maintains a 1:1 copy of the SQLite data that Chroma also indexes, if Chroma returns zero results, FTS5 will also return zero results. FTS5 fallback should only activate when Chroma is unavailable or encounters an error, not when it successfully returns an empty result set.\n\n---\nType: discovery | Facts: Chroma vector database requires query text for semantic search operations; Filter-only queries must skip Chroma and use SQLite structured filtering directly; FTS5 fallback at line 472 triggers on zero results, not on Chroma errors; FTS5 contains 1:1 copy of SQLite data, so zero Chroma results means zero FTS5 results; FTS5 fallback should only activate when Chroma is unavailable or errors, not on empty results | Concepts: problem-solution, gotcha, how-it-works, why-it-exists | Files: src/servers/search-server.ts\n\n---\nDate: 11/17/2025, 11:41:43 PM"}]
\ No newline at end of file
diff --git a/test-results/test-16-discoveries.json b/test-results/test-16-discoveries.json
deleted file mode 100644
index a73d5f90..00000000
--- a/test-results/test-16-discoveries.json
+++ /dev/null
@@ -1 +0,0 @@
-[{"type":"text","text":"## Finalized TOMORROW.md with consolidated priorities and success criteria\n*Source: claude-mem://observation/2571*\n\n**Updated focus areas and success criteria to reflect settings merge, UI porting, subscription setup, and landing page launch.**\n\nThe TOMORROW.md file was finalized with four consolidated priorities that align with the original request. Focus areas were reorganized from documentation-heavy tasks to feature-delivery focused work: (1) merging the settings branch and applying the SessionStart hotfix, (2) porting the v3.0 memory-stream UI to v4.0, (3) setting up the $10/month subscription tier, and (4) launching the landing page. The file now includes a key observation that Claude-Mem successfully retained critical architectural context (v4.3.1 SessionStart hotfix, recent changes) even after a `/clear` command, validating the system's persistent memory capabilities.\n\n---\nType: change | Facts: Priority 1 for tomorrow: merge settings branch with SessionStart hotfix; Priority 2: integrate v3.0 memory-stream UI with v4.0 worker service; Priority 3: design and implement $10/month subscription system for web access; Priority 4: complete and deploy landing page from ~/Scripts/claude-mem-4-lp/; System demonstrated persistent memory across session boundaries after /clear command | Concepts: what-changed, discovery | Files: /Users/alexnewman/Scripts/claude-mem/private/TOMORROW.md\n\n---\nDate: 10/26/2025, 12:39:42 AM\n\n---\n\n## Corrected database location documentation\n*Source: claude-mem://observation/2127*\n\n**Updated database.md to reflect actual implementation after README claim validation.**\n\nDuring README validation against the codebase, a discrepancy was discovered: the README claimed the database was moved to the plugin directory in v4.0.0+, but the actual implementation continues using ~/.claude-mem/. The database.md documentation was corrected to reflect the actual current implementation location while noting the discrepancy with the README claim. This is part of the validation process to ensure documentation accuracy.\n\n---\nType: change | Facts: database.md updated: database location claim in README contradicted actual implementation; README claimed v4.0.0+ moved database to ${CLAUDE_PLUGIN_ROOT}/data/claude-mem.db; Actual implementation still uses ~/.claude-mem/claude-mem.db; Documentation now shows current location with note about README claim discrepancy | Concepts: discovery, what-changed | Files: docs/architecture/database.md\n\n---\nDate: 10/23/2025, 11:20:41 PM\n\n---\n\n## Evaluation Data Integration Verified - No Code Changes Required\n*Source: claude-mem://observation/2058*\n\n**MCP tool and API route working correctly; system fully integrated with Kipu patient evaluation data**\n\nAfter testing both the MCP tool and underlying REST API, the user discovered that the evaluation data integration is already working correctly. The GetPatientsPatientEvaluations tool successfully retrieves patient evaluation records from Kipu, and the API route properly handles the MCP response. The earlier empty responses during testing were likely due to testing methodology rather than system defects. The user documented this finding in EVALUATION_DATA_FIX_PLAN.md, clarifying that if the UI shows no data, the issue is likely invalid patient IDs, missing casefile format, or patients without evaluations rather than system misconfiguration. A working test patient ID (7993:e772aaad-3267-4903-be54-333cee663fbd) was identified for UI validation.\n\n---\nType: decision | Facts: MCP tool GetPatientsPatientEvaluations returns structured evaluation data successfully; API route /api/patient-qa/patient-evaluations/[patientId] correctly extracts and returns evaluations; Response structure: success boolean, evaluations array with id, name, enabled, patient_process_id, timestamps; Test patient 7993:e772aaad-3267-4903-be54-333cee663fbd has 3 evaluations (1 completed, 2 open); No code modifications needed; system already correctly configured and functional | Concepts: how-it-works, discovery, why-it-exists | Files: /Users/alexnewman/Scripts/clean-kit/EVALUATION_DATA_FIX_PLAN.md\n\n---\nDate: 10/23/2025, 9:48:06 PM\n\n---\n\n## Git Pull in Plugin Directory\n*Source: claude-mem://observation/1342*\n\n**Executed git pull in plugin marketplace directory; no output returned**\n\nExecuted a git pull command in a different directory (~/.claude/plugins/marketplaces/thedotmack/). This appears to be unrelated to the session validation fixes being made in the claude-mem project. The command returned no output, which typically indicates either the repository is already up to date or the command executed without standard output. This may indicate a context switch or separate task being performed.\n\n---\nType: change | Facts: Command executed: cd ~/.claude/plugins/marketplaces/thedotmack/ && git pull; Directory: ~/.claude/plugins/marketplaces/thedotmack/; No output returned from git pull; This appears to be a different project/directory from the claude-mem session fixes | Concepts: discovery\n\n---\nDate: 10/21/2025, 9:44:47 PM\n\n---\n\n## Test search for \"SessionStore.ts\" file with limit=3 returned no results\n*Source: claude-mem://observation/1071*\n\n**Test query for SessionStore.ts file references with limit 3 returned empty, consistent with empty database state.**\n\nThe second test search was performed using find_by_file with a limit of 3 for \"SessionStore.ts\". The query returned no results, which is again expected given the empty database. The critical aspect of this test is that the tool executed successfully and accepted the limit parameter. The fix for the findByFile method to respect limit/offset is working - the tool is accepting and processing the pagination parameters correctly, which proves the critical bug has been addressed. The empty result is due to the empty database state, not a tool malfunction.\n\n---\nType: discovery | Facts: Tool: find_by_file (MCP plugin); File path: \"SessionStore.ts\"; Limit: 3; Output: Empty (no observations found); Tool executed successfully without errors, confirming deployment is working | Concepts: discovery, how-it-works\n\n---\nDate: 10/21/2025, 6:39:30 PM"}]
\ No newline at end of file
diff --git a/test-results/test-17-all-bugfixes.json b/test-results/test-17-all-bugfixes.json
deleted file mode 100644
index 8982281c..00000000
--- a/test-results/test-17-all-bugfixes.json
+++ /dev/null
@@ -1 +0,0 @@
-[{"type":"text","text":"## Fixed Incorrect Parameter Array in searchUserPrompts FTS5 Path\n*Source: claude-mem://observation/10756*\n\n**Changed params to ftsParams in FTS5 query execution to use correct parameter array with ftsQuery.**\n\nA parameter array mismatch bug was fixed in the searchUserPrompts method's FTS5 code path. The method creates two separate parameter arrays: 'params' for the filter-only path and 'ftsParams' for the FTS5 path. The FTS5 path correctly initialized ftsParams with the escaped query and rebuilt all filter conditions into this new array, but then incorrectly used the 'params' array when executing the SQL query. This would have caused a parameter binding mismatch where the SQL query expected parameters in one order (starting with ftsQuery) but received them in a different order or with missing values. The fix ensures that when the FTS5 path is taken (query text provided), the query uses ftsParams.push() for limit/offset and passes ftsParams to the db.prepare().all() call, maintaining correct parameter alignment throughout the FTS5 execution path.\n\n---\nType: bugfix | Facts: File modified: /Users/alexnewman/Scripts/claude-mem/src/services/sqlite/SessionSearch.ts; Bug was in searchUserPrompts method's FTS5 execution path using wrong parameter array; Changed params.push(limit, offset) to ftsParams.push(limit, offset); Changed this.db.prepare(sql).all(...params) to this.db.prepare(sql).all(...ftsParams); The ftsParams array was created separately for FTS5 path but not being used in query execution; Bug would have caused SQL parameter binding mismatch in FTS5 search path for user prompts | Concepts: problem-solution, what-changed, gotcha | Files: /Users/alexnewman/Scripts/claude-mem/src/services/sqlite/SessionSearch.ts\n\n---\nDate: 11/17/2025, 11:50:17 PM\n\n---\n\n## Diagnosing undefined replace() error in search endpoint\n*Source: claude-mem://observation/10709*\n\n**Sequential thinking initiated to locate source of undefined variable causing replace() method failure in search-server.ts.**\n\nA systematic debugging approach using sequential thinking was initiated to diagnose and fix a runtime error in the search functionality. The error occurs when the replace() method is called on an undefined variable within the search endpoint handler in search-server.ts. Rather than applying a null check workaround, the investigation aims to identify the root cause of why the variable is undefined in the first place. This represents the first thought in a planned 10-step sequential analysis to properly resolve the issue.\n\n---\nType: bugfix | Facts: Error message indicates \"Cannot read properties of undefined (reading 'replace')\" in search code; Sequential thinking process started with thought 1 of 10 planned thoughts; Investigation focuses on query processing in search-server.ts file; The replace() method is being called on an undefined variable in the search endpoint handler; Working directory is /Users/alexnewman/Scripts/claude-mem | Concepts: problem-solution, how-it-works, pattern\n\n---\nDate: 11/17/2025, 10:06:23 PM\n\n---\n\n## Patterns Concept Query Failing with Undefined Replace Error\n*Source: claude-mem://observation/10698*\n\n**Test 14 concept-based query for patterns returns search failure with undefined replace error**\n\nThe concept-based query test for patterns revealed a runtime error in the search API. When querying /api/search with concepts=pattern parameter, the server returns \"Cannot read properties of undefined (reading 'replace')\" instead of observation results. This error pattern typically indicates that a string manipulation operation (replace) is being called on a null or undefined value, likely during query parameter processing or result formatting in search-server.ts. The failure occurs specifically with concept-based filtering, suggesting the concepts parameter handling code path has a bug where an expected string value is missing or undefined. This is a critical issue as concept-based queries are essential for retrieving observations by knowledge category (pattern, gotcha, trade-off, etc.).\n\n---\nType: bugfix | Facts: Test query /api/search?type=observations&concepts=pattern&format=full&limit=5&orderBy=date_desc fails with error \"Cannot read properties of undefined (reading 'replace')\"; Error suggests null or undefined value being passed to string replace operation in search-server.ts; Test file test-results/test-14-patterns.json contains error response instead of observation results | Concepts: problem-solution, gotcha | Files: test-results/test-14-patterns.json\n\n---\nDate: 11/17/2025, 9:53:47 PM\n\n---\n\n## Chroma Internal Error Detection During Testing\n*Source: claude-mem://observation/10669*\n\n**Seven Chroma query failures detected with \"Error finding id\" internal errors requiring investigation despite test suite completion.**\n\nWhile the search test suite reported successful completion of all 28 queries, worker logs reveal that 7 Chroma semantic search queries failed with internal errors. The error pattern indicates Chroma's MCP tool is encountering an \"Error finding id\" issue when querying the 'cm__claude-mem' collection. This manifests as unparseable responses where Chroma returns error text instead of JSON, causing the search server's JSON.parse() to fail. The specific error \"Error executing plan: Internal error: Error finding id\" suggests a data integrity issue within the Chroma vector database, possibly corrupted document IDs or missing metadata. Despite these failures, the search system appears to have gracefully degraded - tests completed and likely fell back to non-semantic results. This issue needs investigation to determine if certain documents have malformed IDs, if the collection needs rebuilding, or if there's a deeper compatibility issue between the Chroma MCP server and the current data schema.\n\n---\nType: bugfix | Facts: Seven instances of \"Failed to parse Chroma response as JSON\" errors found in worker logs; Chroma error message shows: \"Error executing tool chroma_query_documents: Failed to query documents from collection 'cm__claude-mem': Error executing plan: Internal error: Error finding id\"; Errors occurred during test suite execution despite all 28 tests reporting as complete; JSON parsing fails because Chroma returns error text starting with \"Error exec\" instead of valid JSON; Test suite continued execution and completed successfully despite these internal Chroma failures | Concepts: problem-solution, gotcha\n\n---\nDate: 11/17/2025, 9:38:06 PM\n\n---\n\n## Chroma Semantic Search Fix Validated\n*Source: claude-mem://observation/10667*\n\n**Chroma integration now successfully returns 92 semantic matches for worker service query after fixing response parsing.**\n\nThe Chroma semantic search integration was validated as working correctly following the earlier fix for response parsing. A test query searching for \"worker service\" within observations successfully triggered Chroma's semantic search, which identified 92 semantically relevant matches from the vector database. The system correctly limited results to the top 2 matches as requested. The absence of parsing errors in the logs confirms that the fix for handling Chroma's response format is working properly. Both returned observations are contextually relevant to worker service topics - one about build and restart workflows, another about testing phases. This validation confirms that the unified search API can now leverage Chroma's semantic capabilities to find conceptually related content beyond simple keyword matching, significantly enhancing search quality for the testing phase ahead.\n\n---\nType: bugfix | Facts: Test query for \"worker service\" with type=observations and limit=2 executed against localhost:37777/api/search; Chroma returned 92 semantic matches for the query, demonstrating successful semantic search functionality; Search results correctly returned 2 observations: \"Build, Sync, and Worker Restart Executed\" and \"Phase 3 Testing Initiated\"; Worker logs show \"Chroma returned 92 semantic matches\" with no \"Failed to parse\" errors; Response properly formatted with metadata including observation type, date, and claude-mem:// source URIs | Concepts: problem-solution, how-it-works\n\n---\nDate: 11/17/2025, 9:37:40 PM"}]
\ No newline at end of file
diff --git a/test-results/test-18-all-features.json b/test-results/test-18-all-features.json
deleted file mode 100644
index 8958499c..00000000
--- a/test-results/test-18-all-features.json
+++ /dev/null
@@ -1 +0,0 @@
-[{"type":"text","text":"## Add filter-only query path to searchUserPrompts method\n*Source: claude-mem://observation/10755*\n\n**Method accepts undefined query and handles dual-path logic with separate parameter arrays for clarity.**\n\nThe searchUserPrompts method has been enhanced with filter-only query support, completing the pattern established in searchObservations and searchSessions. The method signature now accepts optional query parameter. The implementation builds base filter conditions once for project and date range filters, then diverges into two paths. The filter-only path (when query is undefined) validates that at least some filters exist, then queries the user_prompts table directly with a WHERE clause, joining sdk_sessions for project filtering. The FTS5 path rebuilds filter conditions into a separate ftsParams array to avoid parameter ordering conflicts between the two paths. This dual-path approach enables both semantic/keyword search via FTS5 and pure metadata filtering via direct SQLite queries, completing the architectural pattern across all three search methods.\n\n---\nType: feature | Facts: searchUserPrompts signature changed to accept `query: string | undefined` at line 541; Filter conditions built once and shared between filter-only and FTS5 paths at lines 546-563; Filter-only path at lines 566-588 validates filters exist and queries user_prompts table directly; FTS5 path at lines 591-616 rebuilds filter conditions with separate ftsParams array to avoid parameter conflicts; Filter-only path joins sdk_sessions table for project filtering support; Documentation updated to clarify dual-mode operation matching searchObservations and searchSessions patterns | Concepts: what-changed, how-it-works, pattern, gotcha | Files: src/services/sqlite/SessionSearch.ts\n\n---\nDate: 11/17/2025, 11:50:08 PM\n\n---\n\n## Add filter-only query path to searchSessions method\n*Source: claude-mem://observation/10751*\n\n**Method now accepts undefined query and queries session_summaries table directly for metadata-only filtering.**\n\nThe searchSessions method in SessionSearch.ts has been enhanced to support filter-only queries without query text. The method signature now accepts `query: string | undefined`, enabling two distinct code paths. When query is undefined, a new filter-only path (lines 331-352) bypasses FTS5 entirely and queries the session_summaries table directly using metadata filters like project or date range. This path validates that at least some filters exist, throwing an error if both query and filters are missing. When query text is provided, the existing FTS5 search path executes as before. This dual-mode approach allows the system to handle both semantic/keyword search and pure metadata filtering, addressing the architectural requirement that filter-only queries skip text search mechanisms.\n\n---\nType: feature | Facts: searchSessions signature changed to accept `query: string | undefined` at line 327; New filter-only path added at lines 331-352 that bypasses FTS5 when query is undefined; Filter-only path throws error if neither query nor filters provided at line 336; Direct SQLite query on session_summaries table uses WHERE clause built from metadata filters; FTS5 path at line 355 only executes when query text exists; Documentation updated to clarify dual-mode operation: FTS5 fallback vs filter-only direct query | Concepts: what-changed, how-it-works, pattern | Files: src/services/sqlite/SessionSearch.ts\n\n---\nDate: 11/17/2025, 11:49:23 PM\n\n---\n\n## Implemented Filter-Only Query Path in searchObservations Method\n*Source: claude-mem://observation/10749*\n\n**Added conditional logic to route filter-only queries directly to observations table, bypassing FTS5 when query parameter is undefined.**\n\nThe searchObservations method in SessionSearch.ts now supports two distinct query paths based on whether a query string is provided. The method signature was updated to accept query as string | undefined, enabling filter-only queries. When query is undefined, the new filter-only path executes: it builds filter conditions using buildFilterClause, validates that at least some filters exist (throwing an error if neither query nor filters are present), constructs a direct SQL query against the observations table without joining observations_fts, and returns results ordered by the specified criteria (with hasFTS: false passed to buildOrderClause since no FTS5 rank is available). When query text exists, the original FTS5 path executes, now explicitly documented as fallback mode for ChromaDB unavailability. This dual-path implementation allows metadata queries to go directly to SQLite without the overhead of FTS5, while preserving FTS5 functionality for text search when ChromaDB is unavailable.\n\n---\nType: feature | Facts: The searchObservations method signature changed to accept query parameter as string | undefined; Added filter-only code path that executes when query parameter is undefined or empty; Filter-only path queries observations table directly without JOIN to observations_fts table; Filter-only path throws error if no filters are provided, requiring either query or filters; Filter-only path passes false to buildOrderClause to indicate FTS5 rank is unavailable; FTS5 path only executes when query text exists, serving as fallback for ChromaDB unavailability; Both paths reuse the same buildFilterClause method for consistent filter handling | Concepts: how-it-works, pattern, what-changed | Files: src/services/sqlite/SessionSearch.ts\n\n---\nDate: 11/17/2025, 11:48:59 PM\n\n---\n\n## Unified Search API with Catch-All Parameters\n*Source: claude-mem://observation/10681*\n\n**Six public-facing endpoints with backward-compatible granular routing through MCP for comprehensive project searching.**\n\nA comprehensive unified search API was completed with six public-facing endpoints to query project knowledge. The core /api/search endpoint accepts catch-all parameters including type (observations/sessions/prompts), obs_type (decision/bugfix/feature), concepts (concept tags), and files (file path filter), along with common query parameters like format, dateRange, limit, and orderBy. This unified approach can replace all granular endpoints with appropriate query parameters. Backward compatibility is maintained by keeping granular endpoints functional and routing them through the same MCP infrastructure as public endpoints. The implementation was built, synced, and the worker restarted to deploy the changes. A testing phase will follow where common codebase action scenarios are queried, with results saved to organized files for comprehensive analysis of search quality and coverage.\n\n---\nType: feature | Facts: /api/search endpoint supports catch-all parameters: type, obs_type, concepts, files, query, format, project, dateRange, limit, offset, orderBy; Six public endpoints deployed: /api/search, /api/timeline, /api/decisions, /api/changes, /api/how-it-works, /api/contextualize; Granular endpoints like /api/search/observations and /api/search/sessions maintained for backward compatibility; All granular endpoints route through MCP infrastructure alongside public endpoints; Unified search replaces granular endpoints: /api/search?type=observations replaces /api/search/observations, /api/search?obs_type=decision replaces /api/search/by-type?type=decision; Testing strategy involves running common codebase action queries, saving results to organized files by test query title for collective analysis | Concepts: how-it-works, pattern, what-changed, trade-off\n\n---\nDate: 11/17/2025, 9:43:02 PM\n\n---\n\n## Comprehensive Search API Test Suite Executed Successfully\n*Source: claude-mem://observation/10668*\n\n**28 search queries across 11 test categories completed successfully with 17 Chroma semantic search invocations detected.**\n\nA comprehensive automated test suite for the unified search API was executed successfully, validating all search functionality across diverse query patterns. The test script systematically tested 28 different search scenarios organized into 11 functional categories. Semantic queries test conceptual search capabilities, decision and troubleshooting queries validate observation type filtering, file-specific and concept-based queries test targeted filtering, type-filtered queries ensure proper categorization, session and prompt queries validate non-observation search, dedicated endpoint tests verify specialized API routes, timeline tests validate temporal context queries, and multi-parameter tests ensure complex filter combinations work correctly. The 17 Chroma invocations detected in logs indicate that semantic search was appropriately triggered for relevant queries while simpler queries likely used direct filtering. All tests passed without errors, and results were persisted to the test-results/ directory, setting up the next phase where these results will be analyzed collectively to assess search quality and relevance.\n\n---\nType: feature | Facts: Test suite executed 28 total queries organized into 11 categories via test-results/run-search-tests.sh script; Test categories include semantic queries (4), decisions (3), troubleshooting (3), file-specific (3), concept-based (3), type-filtered (3), sessions (1), prompts (1), dedicated endpoints (4), timeline (1), and multi-parameter combinations (2); All 28 tests completed successfully with checkmarks indicating no test failures; Worker logs show 17 instances of \"Chroma returned\" or \"Failed\" patterns, indicating semantic search was triggered 17 times; Test results saved to test-results/ directory for subsequent analysis | Concepts: how-it-works, pattern | Files: test-results/run-search-tests.sh\n\n---\nDate: 11/17/2025, 9:37:52 PM"}]
\ No newline at end of file
diff --git a/test-results/test-19-all-decisions.json b/test-results/test-19-all-decisions.json
deleted file mode 100644
index ca8732cc..00000000
--- a/test-results/test-19-all-decisions.json
+++ /dev/null
@@ -1 +0,0 @@
-[{"type":"text","text":"## Apply semantic naming principle to clarify FTS5 as degraded fallback\n*Source: claude-mem://observation/10742*\n\n**Rename FTS5 functions to explicitly communicate fallback status per CLAUDE.md semantic naming guidelines.**\n\nThe fix requires applying the semantic naming principle from CLAUDE.md to all FTS5-related code. Instead of ambiguous names like `escapeFTS5()` that hide architectural intent, functions should use verbose descriptive names like `escapeFTS5_fallback_for_missing_chroma()` that explicitly communicate FTS5's role as a degraded fallback mode for users without Python/uvx/Chroma dependencies. This naming strategy extends to all search functions - they should reveal whether they use Chroma semantic search, FTS5 keyword fallback, or direct SQLite filtering. The semantic naming approach ensures that anyone reading the code immediately understands FTS5 is not a primary feature but exists only as a backup when the preferred Chroma system is unavailable. This prevents future developers from treating FTS5 as an equal alternative to Chroma.\n\n---\nType: decision | Facts: CLAUDE.md semantic naming principle requires verbose names that reveal intent; Current name `escapeFTS5()` hides that FTS5 is fallback for missing Chroma; Proposed naming like `escapeFTS5_fallback_for_missing_chroma()` makes degraded mode explicit; All FTS5-related functions need renaming to state \"degraded fallback for users without Python\"; Search function names should reveal which mechanism they use: Chroma, FTS5, or direct filtering; Semantic naming prevents future confusion about FTS5 architectural role | Concepts: pattern, why-it-exists, how-it-works, trade-off | Files: CLAUDE.md\n\n---\nDate: 11/17/2025, 11:47:00 PM\n\n---\n\n## Semantic Naming Strategy for FTS5 Functions Due to UVX Dependency\n*Source: claude-mem://observation/10741*\n\n**FTS5-related functions, variables renamed to indicate temporary usage pending UVX availability for semantic search.**\n\nThe team has decided to implement a semantic naming convention for all FTS5-related code to clearly communicate that FTS5 is a temporary workaround. The naming pattern \"fts5_only_because_uvx_is_disabled\" will be applied to functions and variables that currently implement full-text search using FTS5. This decision stems from the understanding that UVX (which would enable proper semantic search) is currently disabled, forcing reliance on SQLite's FTS5 for text search capabilities. By embedding this context directly in function names, the codebase self-documents the temporary nature of the implementation and the intended migration path. This naming strategy was recently documented in CLAUDE.md and represents a deliberate choice to maintain clarity about technical debt and future refactoring needs despite the increased verbosity.\n\n---\nType: decision | Facts: FTS5 is being used as a temporary solution because UVX is currently disabled; Functions related to FTS5 will be renamed with semantic suffix \"_only_because_uvx_is_disabled\"; The naming convention makes the temporary nature of FTS5 usage explicit in the codebase; This decision was recently added to CLAUDE.md documentation; The renaming applies to all FTS5 functions and related variables throughout the codebase | Concepts: why-it-exists, pattern, trade-off, what-changed\n\n---\nDate: 11/17/2025, 11:46:49 PM\n\n---\n\n## Unified search handler fix requires five-step query routing logic\n*Source: claude-mem://observation/10739*\n\n**Route by query presence, error-based FTS5 fallback only, remove zero-results fallback check.**\n\nThe fix for the unified search handler requires restructuring the query routing logic into five distinct steps. First, check whether the query parameter exists at the top of the handler. If query is undefined, bypass Chroma entirely and invoke a new filter-only path in SessionSearch that performs direct SQLite filtering. If query exists, proceed with Chroma semantic search. The FTS5 fallback logic must change fundamentally - it should only trigger when Chroma throws an error (caught at line 465), not when Chroma successfully returns zero results. When Chroma returns zero results, that is the correct answer since FTS5 contains identical data and would also return zero. This requires removing the zero-results check from line 472's fallback condition, fixing the current incorrect fallback behavior.\n\n---\nType: decision | Facts: Step 1: Check if query parameter exists at beginning of unified search handler; Step 2: If query is undefined, skip Chroma and call new filter-only path in SessionSearch; Step 3: If query exists, attempt Chroma semantic search; Step 4: If Chroma throws error (line 465 catch block), fall back to FTS5; Step 5: If Chroma succeeds with zero results, accept zero as correct answer without FTS5 fallback; Line 472 condition must be modified to remove zero-results check | Concepts: problem-solution, how-it-works, pattern, trade-off | Files: src/servers/search-server.ts\n\n---\nDate: 11/17/2025, 11:42:18 PM\n\n---\n\n## Query Routing Architecture: ChromaDB, SQLite, and FTS5 Fallback Strategy\n*Source: claude-mem://observation/10736*\n\n**Defined three distinct query paths based on query presence and ChromaDB availability to prevent incorrect fallback logic.**\n\nThe proper query routing architecture requires three distinct paths based on query presence and ChromaDB availability. Path 1: When a semantic query exists and ChromaDB is available, route to queryChroma with both query and filters. If ChromaDB errors (service failure), fall back to FTS5. However, if ChromaDB successfully returns zero results, accept that as the final answer without falling back - empty results indicate no semantic match, which is more accurate than FTS5. Path 2: When the query parameter is undefined (filter-only operations), skip ChromaDB entirely and route directly to SQLite filtering using SessionStore, not SessionSearch. This path handles metadata queries like retrieving last decisions. Path 3: When a query exists but ChromaDB is unavailable, use FTS5 as a degraded fallback. The current implementation incorrectly mixes these paths, leading to inappropriate fallback behavior and inefficient routing of filter-only queries through vector search systems.\n\n---\nType: decision | Facts: When query exists and ChromaDB is available, call queryChroma with query and filters; ChromaDB errors should trigger FTS5 fallback, but ChromaDB returning zero results is final and should not trigger fallback; Filter-only queries (undefined query parameter) should skip ChromaDB entirely and use direct SQLite filtering via SessionStore; Filter-only queries should use SessionStore for direct SQLite access, not SessionSearch which involves vector operations; When query exists but ChromaDB is unavailable, FTS5 serves as degraded fallback mode; The current code incorrectly mixes these three query routing paths | Concepts: pattern, how-it-works, problem-solution, trade-off\n\n---\nDate: 11/17/2025, 11:41:54 PM\n\n---\n\n## FTS5 demoted to fallback-only search mechanism\n*Source: claude-mem://observation/10729*\n\n**FTS5 search recognized as redundant when Chroma vector database contains 1:1 SQLite copy.**\n\nThe architectural decision clarifies that FTS5 full-text search, while technically implemented, is essentially redundant in normal operation. Since Chroma vector database maintains a complete 1:1 copy of the SQLite data and provides superior search results, FTS5 exists only as a degraded fallback mode for users who cannot install Python dependencies. In practical deployment, the application requires Chroma to function properly, making FTS5 a legacy accommodation rather than a primary feature. This understanding positions FTS5 as \"Chroma search with shitty results\" - functional but not the intended search mechanism.\n\n---\nType: decision | Facts: FTS5 full-text search produces inferior results compared to Chroma vector search; Chroma database maintains a 1:1 copy of all SQLite data; FTS5 serves only as fallback for users without Python dependencies; Application functionality depends primarily on Chroma, making FTS5 nearly vestigial | Concepts: why-it-exists, trade-off, pattern, how-it-works\n\n---\nDate: 11/17/2025, 11:37:41 PM"}]
\ No newline at end of file
diff --git a/test-results/test-20-session-search.json b/test-results/test-20-session-search.json
deleted file mode 100644
index 2cfca8dc..00000000
--- a/test-results/test-20-session-search.json
+++ /dev/null
@@ -1 +0,0 @@
-[{"type":"text","text":"## Deep sequential analysis to find root cause of undefined replace() error in search functionality instead of applying null check workaround\n*Source: claude-mem://session/56378ffe-eab5-4ec5-a7c4-ae6af977bb8f*\n\n**Completed:** Completed 12-step sequential thinking analysis identifying the bug location, understanding the architectural issue, and designing a three-part solution: (1) make query parameter optional in Zod schema, (2) add conditional SQL paths in searchObservations/searchSessions/searchUserPrompts that branch on query presence - use FTS5 JOIN when query exists, use direct table SELECT when query undefined, (3) preserve composability allowing text search and filters to work independently or together.\n\n**Learned:** Root cause is architectural assumption conflating FTS5 text search with structured filtering. Current implementation always requires FTS5 MATCH clause even for filter-only queries (type, concepts, files). FTS5 fundamentally cannot MATCH against undefined/empty query. SessionSearch class is for full-text search; SessionStore is for structured queries. The buildFilterClause method already constructs WHERE conditions independently of FTS5, enabling separate query paths. TypeScript type annotations provide no runtime protection against undefined values.\n\n**Investigated:** Traced error through multiple layers: search-server.ts unified endpoint → queryChroma function → SessionSearch.searchObservations → escapeFTS5 method. Examined Zod schema validation, parameter extraction, FTS5 full-text search architecture, and SessionStore vs SessionSearch separation of concerns. Located exact error at line 151 of SessionSearch.ts where text.replace() is called without undefined check.\n\n**Next Steps:** Ready to implement the three-part fix across src/servers/search-server.ts (schema change) and src/services/sqlite/SessionSearch.ts (conditional query logic in three search methods). Implementation will enable filter-only queries like \"show all bugfixes\" or \"show all changes to search-server.ts\" without requiring FTS5 text search.\n\n**Notes:** This represents proper root cause fixing versus superficial workarounds. The solution maintains architectural integrity by separating concerns between text search (FTS5) and structured filtering (direct SQL), making them independently functional and composable. This unlocks precision context retrieval patterns that were previously impossible due to the forced FTS5 requirement.\n\n---\nDate: 11/17/2025\n\n---\n\n## Review commit against architecture guidelines: MCP as DRY search source, HTTP API routes through MCP, MCP not deprecated\n*Source: claude-mem://session/b1a1c481-6560-41bf-a0d3-23e448584f08*\n\n**Completed:** Build and deployment pipeline executed successfully for claude-mem version 6.0.9. All components compiled including worker-service.cjs (1337.16 KB), search-server.mjs (332.02 KB), seven hooks (context, new, save, summary, cleanup, user-message), and React viewer UI bundle. All 11,751 files synced to marketplace location (~/.claude/plugins/marketplaces/thedotmack/), dependencies refreshed via npm install, and worker service restarted to activate new code.\n\n**Learned:** The search architecture follows a clear layered pattern: MCP contains the single source of truth for search logic, HTTP API endpoints act as a thin routing layer that delegates to MCP, and this prevents code duplication. The system maintains 6 public-facing endpoints (/api/search, /api/timeline, /api/decisions, /api/changes, /api/how-it-works, /api/contextualize) with granular endpoints preserved for backward compatibility. The unified /api/search endpoint accepts catch-all parameters (type, obs_type, concepts, files) that replace the need for specialized endpoints.\n\n**Investigated:** A commit review was requested to verify compliance with established search architecture principles. The architecture mandates that MCP (Model Context Protocol) serves as the canonical, DRY search implementation with HTTP API routing through it.\n\n**Next Steps:** Awaiting confirmation that the commit passes architecture review or identification of any violations of the MCP-as-DRY-source principle where HTTP API might be duplicating search logic instead of routing through MCP.\n\n**Notes:** The unified search API design demonstrates strong architectural discipline by consolidating multiple specialized endpoints into a single parameterized endpoint while maintaining backward compatibility. The build output shows healthy file sizes with the worker service as the largest component at ~1.3 MB, which is reasonable for a service handling search, MCP integration, and worker orchestration.\n\n---\nDate: 11/17/2025\n\n---\n\n## Clarifying whether semantic shortcuts should return simple search results or contextual timelines\n*Source: claude-mem://session/b1a1c481-6560-41bf-a0d3-23e448584f08*\n\n**Completed:** The primary Claude identified the architectural decision point and presented two clear options to the user with concrete examples showing the difference between search-based responses (returning flat lists) versus timeline-based responses (returning anchor observations with contextual timelines).\n\n**Learned:** Semantic shortcuts like `/api/decisions`, `/api/changes`, and `/api/how-it-works` currently function as search shortcuts that filter observations by concept tags. The user's question suggested they might expect timeline functionality, prompting architectural clarification about whether these endpoints should evolve from simple filtered lists into richer timeline-based responses that provide chronological context around concept evolution.\n\n**Investigated:** The primary Claude session explored two architectural options for semantic shortcuts: Option A keeps them as simple search endpoints returning lists of observations filtered by concept type, while Option B transforms them into timeline-generating endpoints that return anchor observations with surrounding temporal context.\n\n**Next Steps:** Awaiting user decision on whether to keep semantic shortcuts as simple search endpoints (Option A) or enhance them to return timeline context (Option B), which will determine the implementation approach for the semantic shortcut API architecture.\n\n**Notes:** This represents a key architectural fork in the memory system design: balancing simplicity of search shortcuts against the richer context provided by timelines. The decision will impact how users interact with concept-filtered observations and whether they get isolated results or chronological narratives.\n\n---\nDate: 11/17/2025\n\n---\n\n## Clarify exposed search endpoints and investigate unified search parameters\n*Source: claude-mem://session/b1a1c481-6560-41bf-a0d3-23e448584f08*\n\n**Completed:** Identified the gap between current unified search capabilities and the catch-all parameters needed to fully replace granular search endpoints. Clarified that the 6 desired public-facing endpoints are: /api/search, /api/timeline, /api/decisions, /api/changes, /api/how-it-works, and /api/contextualize.\n\n**Learned:** The unified /api/search tool currently supports query, format, project, dateRange, limit, offset, and orderBy parameters, but lacks catch-all filtering parameters like type (document type filter), concept (observation concept tags), file (file path filter), and obs_type (observation type filter). The search implementation uses hybrid ChromaDB semantic search with SQLite FTS5 fallback, searches across all document types simultaneously, filters to a 90-day recency window, and defaults to 'index' format for token efficiency. The user wants only 6 specific search endpoints exposed publicly despite the potential for broader parameter combinations.\n\n**Investigated:** The unified search tool definition in src/servers/search-server.ts was examined to understand its current parameter structure and hybrid search implementation. The search tool's inputSchema was reviewed to identify which filtering parameters are currently available.\n\n**Next Steps:** The session is awaiting user decision on whether to add the missing catch-all parameters (type, concept, file, obs_type) to the unified search MCP tool to enable full replacement of granular endpoints while maintaining backward compatibility.\n\n**Notes:** The discovery revealed that while the unified search has sophisticated hybrid semantic search capabilities, it still needs additional filtering parameters to fully consolidate all search functionality into the 6 intended public endpoints. The granular endpoints would remain for backward compatibility but not be documented for end users.\n\n---\nDate: 11/17/2025\n\n---\n\n## Clarifying whether granular search endpoints are deprecated given the unified search endpoint has parameters\n*Source: claude-mem://session/b1a1c481-6560-41bf-a0d3-23e448584f08*\n\n**Completed:** No code changes have been made. The discussion clarified that the granular endpoints are not actually deprecated in the current implementation because they provide type-specific filtering that the unified endpoint lacks.\n\n**Learned:** The unified search endpoint accepts parameters like query, format, project, dateRange, limit, offset, and orderBy, but does NOT have a type filter parameter. It returns mixed results from all document types (observations, sessions, prompts). The granular endpoints (/api/search/observations, /api/search/sessions, /api/search/prompts) serve a complementary purpose by returning only specific document types. The search uses hybrid ChromaDB semantic search with SQLite FTS5 fallback, and implements an index/full format pattern to optimize token usage.\n\n**Investigated:** The search-server.ts file was examined to understand the unified search tool implementation and its parameters, specifically looking at whether the search parameters make the granular type-specific endpoints redundant\n\n**Next Steps:** Awaiting user decision on whether to add a type parameter to the unified search endpoint, which would make it truly replace the granular endpoints and allow them to be deprecated as backward-compatible legacy endpoints\n\n**Notes:** This represents a potential architecture decision point: whether to consolidate all search functionality into a single parameterized endpoint (DRY principle) versus maintaining separate endpoints for different document types (explicit API design). The current hybrid approach exists because the type filter was never added to the unified search.\n\n---\nDate: 11/17/2025"}]
\ No newline at end of file
diff --git a/test-results/test-21-prompt-search.json b/test-results/test-21-prompt-search.json
deleted file mode 100644
index 5f4a5d28..00000000
--- a/test-results/test-21-prompt-search.json
+++ /dev/null
@@ -1 +0,0 @@
-[{"type":"text","text":"## User Prompt #5\n*Source: claude-mem://user-prompt/2735*\n\nAre you ok? You seem a little excited and it's hard to do this project. Can you tell me what's going on?\n\n---\nDate: 11/17/2025, 9:38:06 PM\n\n---\n\n## User Prompt #2\n*Source: claude-mem://user-prompt/2719*\n\nedit the build script, we need it to always do this.\n\n---\nDate: 11/17/2025, 7:49:17 PM\n\n---\n\n## User Prompt #3\n*Source: claude-mem://user-prompt/2662*\n\nthose built files, the js, the cjs, if the source files don't have merge issues, then these are just way outdated build files.... is that the case?\n\n---\nDate: 11/17/2025, 3:03:13 PM\n\n---\n\n## User Prompt #12\n*Source: claude-mem://user-prompt/2651*\n\nI FUCKING DID BUILD AND SYNC AND DELETE. I would not have told you to fucking check pm2 info had I not done that\n\n---\nDate: 11/17/2025, 2:25:18 PM\n\n---\n\n## User Prompt #7\n*Source: claude-mem://user-prompt/2646*\n\nCan you fix it so that it runs from the marketplace folder so it doesn't break on EVERYONES SYSTEM WHO DOESNT FUCKING BUILD MANUALLY\n\n---\nDate: 11/17/2025, 2:19:29 PM"}]
\ No newline at end of file
diff --git a/test-results/test-22-decisions-endpoint.json b/test-results/test-22-decisions-endpoint.json
deleted file mode 100644
index ca8732cc..00000000
--- a/test-results/test-22-decisions-endpoint.json
+++ /dev/null
@@ -1 +0,0 @@
-[{"type":"text","text":"## Apply semantic naming principle to clarify FTS5 as degraded fallback\n*Source: claude-mem://observation/10742*\n\n**Rename FTS5 functions to explicitly communicate fallback status per CLAUDE.md semantic naming guidelines.**\n\nThe fix requires applying the semantic naming principle from CLAUDE.md to all FTS5-related code. Instead of ambiguous names like `escapeFTS5()` that hide architectural intent, functions should use verbose descriptive names like `escapeFTS5_fallback_for_missing_chroma()` that explicitly communicate FTS5's role as a degraded fallback mode for users without Python/uvx/Chroma dependencies. This naming strategy extends to all search functions - they should reveal whether they use Chroma semantic search, FTS5 keyword fallback, or direct SQLite filtering. The semantic naming approach ensures that anyone reading the code immediately understands FTS5 is not a primary feature but exists only as a backup when the preferred Chroma system is unavailable. This prevents future developers from treating FTS5 as an equal alternative to Chroma.\n\n---\nType: decision | Facts: CLAUDE.md semantic naming principle requires verbose names that reveal intent; Current name `escapeFTS5()` hides that FTS5 is fallback for missing Chroma; Proposed naming like `escapeFTS5_fallback_for_missing_chroma()` makes degraded mode explicit; All FTS5-related functions need renaming to state \"degraded fallback for users without Python\"; Search function names should reveal which mechanism they use: Chroma, FTS5, or direct filtering; Semantic naming prevents future confusion about FTS5 architectural role | Concepts: pattern, why-it-exists, how-it-works, trade-off | Files: CLAUDE.md\n\n---\nDate: 11/17/2025, 11:47:00 PM\n\n---\n\n## Semantic Naming Strategy for FTS5 Functions Due to UVX Dependency\n*Source: claude-mem://observation/10741*\n\n**FTS5-related functions, variables renamed to indicate temporary usage pending UVX availability for semantic search.**\n\nThe team has decided to implement a semantic naming convention for all FTS5-related code to clearly communicate that FTS5 is a temporary workaround. The naming pattern \"fts5_only_because_uvx_is_disabled\" will be applied to functions and variables that currently implement full-text search using FTS5. This decision stems from the understanding that UVX (which would enable proper semantic search) is currently disabled, forcing reliance on SQLite's FTS5 for text search capabilities. By embedding this context directly in function names, the codebase self-documents the temporary nature of the implementation and the intended migration path. This naming strategy was recently documented in CLAUDE.md and represents a deliberate choice to maintain clarity about technical debt and future refactoring needs despite the increased verbosity.\n\n---\nType: decision | Facts: FTS5 is being used as a temporary solution because UVX is currently disabled; Functions related to FTS5 will be renamed with semantic suffix \"_only_because_uvx_is_disabled\"; The naming convention makes the temporary nature of FTS5 usage explicit in the codebase; This decision was recently added to CLAUDE.md documentation; The renaming applies to all FTS5 functions and related variables throughout the codebase | Concepts: why-it-exists, pattern, trade-off, what-changed\n\n---\nDate: 11/17/2025, 11:46:49 PM\n\n---\n\n## Unified search handler fix requires five-step query routing logic\n*Source: claude-mem://observation/10739*\n\n**Route by query presence, error-based FTS5 fallback only, remove zero-results fallback check.**\n\nThe fix for the unified search handler requires restructuring the query routing logic into five distinct steps. First, check whether the query parameter exists at the top of the handler. If query is undefined, bypass Chroma entirely and invoke a new filter-only path in SessionSearch that performs direct SQLite filtering. If query exists, proceed with Chroma semantic search. The FTS5 fallback logic must change fundamentally - it should only trigger when Chroma throws an error (caught at line 465), not when Chroma successfully returns zero results. When Chroma returns zero results, that is the correct answer since FTS5 contains identical data and would also return zero. This requires removing the zero-results check from line 472's fallback condition, fixing the current incorrect fallback behavior.\n\n---\nType: decision | Facts: Step 1: Check if query parameter exists at beginning of unified search handler; Step 2: If query is undefined, skip Chroma and call new filter-only path in SessionSearch; Step 3: If query exists, attempt Chroma semantic search; Step 4: If Chroma throws error (line 465 catch block), fall back to FTS5; Step 5: If Chroma succeeds with zero results, accept zero as correct answer without FTS5 fallback; Line 472 condition must be modified to remove zero-results check | Concepts: problem-solution, how-it-works, pattern, trade-off | Files: src/servers/search-server.ts\n\n---\nDate: 11/17/2025, 11:42:18 PM\n\n---\n\n## Query Routing Architecture: ChromaDB, SQLite, and FTS5 Fallback Strategy\n*Source: claude-mem://observation/10736*\n\n**Defined three distinct query paths based on query presence and ChromaDB availability to prevent incorrect fallback logic.**\n\nThe proper query routing architecture requires three distinct paths based on query presence and ChromaDB availability. Path 1: When a semantic query exists and ChromaDB is available, route to queryChroma with both query and filters. If ChromaDB errors (service failure), fall back to FTS5. However, if ChromaDB successfully returns zero results, accept that as the final answer without falling back - empty results indicate no semantic match, which is more accurate than FTS5. Path 2: When the query parameter is undefined (filter-only operations), skip ChromaDB entirely and route directly to SQLite filtering using SessionStore, not SessionSearch. This path handles metadata queries like retrieving last decisions. Path 3: When a query exists but ChromaDB is unavailable, use FTS5 as a degraded fallback. The current implementation incorrectly mixes these paths, leading to inappropriate fallback behavior and inefficient routing of filter-only queries through vector search systems.\n\n---\nType: decision | Facts: When query exists and ChromaDB is available, call queryChroma with query and filters; ChromaDB errors should trigger FTS5 fallback, but ChromaDB returning zero results is final and should not trigger fallback; Filter-only queries (undefined query parameter) should skip ChromaDB entirely and use direct SQLite filtering via SessionStore; Filter-only queries should use SessionStore for direct SQLite access, not SessionSearch which involves vector operations; When query exists but ChromaDB is unavailable, FTS5 serves as degraded fallback mode; The current code incorrectly mixes these three query routing paths | Concepts: pattern, how-it-works, problem-solution, trade-off\n\n---\nDate: 11/17/2025, 11:41:54 PM\n\n---\n\n## FTS5 demoted to fallback-only search mechanism\n*Source: claude-mem://observation/10729*\n\n**FTS5 search recognized as redundant when Chroma vector database contains 1:1 SQLite copy.**\n\nThe architectural decision clarifies that FTS5 full-text search, while technically implemented, is essentially redundant in normal operation. Since Chroma vector database maintains a complete 1:1 copy of the SQLite data and provides superior search results, FTS5 exists only as a degraded fallback mode for users who cannot install Python dependencies. In practical deployment, the application requires Chroma to function properly, making FTS5 a legacy accommodation rather than a primary feature. This understanding positions FTS5 as \"Chroma search with shitty results\" - functional but not the intended search mechanism.\n\n---\nType: decision | Facts: FTS5 full-text search produces inferior results compared to Chroma vector search; Chroma database maintains a 1:1 copy of all SQLite data; FTS5 serves only as fallback for users without Python dependencies; Application functionality depends primarily on Chroma, making FTS5 nearly vestigial | Concepts: why-it-exists, trade-off, pattern, how-it-works\n\n---\nDate: 11/17/2025, 11:37:41 PM"}]
\ No newline at end of file
diff --git a/test-results/test-23-changes-endpoint.json b/test-results/test-23-changes-endpoint.json
deleted file mode 100644
index 282bba42..00000000
--- a/test-results/test-23-changes-endpoint.json
+++ /dev/null
@@ -1 +0,0 @@
-[{"type":"text","text":"## Build and sync claude-mem plugin to marketplace location\n*Source: claude-mem://observation/10758*\n\n**Built all hooks and services, synced to marketplace directory, and installed dependencies successfully.**\n\nThe claude-mem plugin was fully built and deployed to the marketplace directory. The build process compiled all React components, bundled the worker service and search server, and built each individual hook. The build script used scripts/build-hooks.js to generate production-ready bundles in the plugin/scripts/ directory. After building, rsync transferred all 11781 files to the marketplace directory at ~/.claude/plugins/marketplaces/thedotmack/, removing the .git directory and .install-version file in the process. The sync command used --delete flag to ensure the marketplace directory perfectly mirrors the development directory. Finally, npm install ran in the marketplace directory to ensure all dependencies are available for the deployed plugin.\n\n---\nType: change | Facts: Built claude-mem version 6.0.9 including React viewer, worker service, search server, and all hooks; Worker service built to 1338.75 KB, search server to 335.14 KB; Synced 11781 files from development directory to ~/.claude/plugins/marketplaces/thedotmack/; All hooks built successfully: context-hook (38.29 KB), new-hook (32.14 KB), save-hook (32.49 KB), summary-hook (34.61 KB), cleanup-hook (31.38 KB), user-message-hook (2.24 KB); Dependencies auto-installed in marketplace location after sync | Concepts: what-changed, how-it-works | Files: scripts/build-hooks.js, package.json, plugin/scripts/worker-service.cjs, plugin/scripts/search-server.mjs, plugin/scripts/context-hook.js, plugin/scripts/new-hook.js, plugin/scripts/save-hook.js, plugin/scripts/summary-hook.js, plugin/scripts/cleanup-hook.js, plugin/scripts/user-message-hook.js, plugin/ui/viewer-bundle.js, plugin/ui/viewer.html\n\n---\nDate: 11/17/2025, 11:51:51 PM\n\n---\n\n## Fixed Incorrect Parameter Array in searchUserPrompts FTS5 Path\n*Source: claude-mem://observation/10756*\n\n**Changed params to ftsParams in FTS5 query execution to use correct parameter array with ftsQuery.**\n\nA parameter array mismatch bug was fixed in the searchUserPrompts method's FTS5 code path. The method creates two separate parameter arrays: 'params' for the filter-only path and 'ftsParams' for the FTS5 path. The FTS5 path correctly initialized ftsParams with the escaped query and rebuilt all filter conditions into this new array, but then incorrectly used the 'params' array when executing the SQL query. This would have caused a parameter binding mismatch where the SQL query expected parameters in one order (starting with ftsQuery) but received them in a different order or with missing values. The fix ensures that when the FTS5 path is taken (query text provided), the query uses ftsParams.push() for limit/offset and passes ftsParams to the db.prepare().all() call, maintaining correct parameter alignment throughout the FTS5 execution path.\n\n---\nType: bugfix | Facts: File modified: /Users/alexnewman/Scripts/claude-mem/src/services/sqlite/SessionSearch.ts; Bug was in searchUserPrompts method's FTS5 execution path using wrong parameter array; Changed params.push(limit, offset) to ftsParams.push(limit, offset); Changed this.db.prepare(sql).all(...params) to this.db.prepare(sql).all(...ftsParams); The ftsParams array was created separately for FTS5 path but not being used in query execution; Bug would have caused SQL parameter binding mismatch in FTS5 search path for user prompts | Concepts: problem-solution, what-changed, gotcha | Files: /Users/alexnewman/Scripts/claude-mem/src/services/sqlite/SessionSearch.ts\n\n---\nDate: 11/17/2025, 11:50:17 PM\n\n---\n\n## Add filter-only query path to searchUserPrompts method\n*Source: claude-mem://observation/10755*\n\n**Method accepts undefined query and handles dual-path logic with separate parameter arrays for clarity.**\n\nThe searchUserPrompts method has been enhanced with filter-only query support, completing the pattern established in searchObservations and searchSessions. The method signature now accepts optional query parameter. The implementation builds base filter conditions once for project and date range filters, then diverges into two paths. The filter-only path (when query is undefined) validates that at least some filters exist, then queries the user_prompts table directly with a WHERE clause, joining sdk_sessions for project filtering. The FTS5 path rebuilds filter conditions into a separate ftsParams array to avoid parameter ordering conflicts between the two paths. This dual-path approach enables both semantic/keyword search via FTS5 and pure metadata filtering via direct SQLite queries, completing the architectural pattern across all three search methods.\n\n---\nType: feature | Facts: searchUserPrompts signature changed to accept `query: string | undefined` at line 541; Filter conditions built once and shared between filter-only and FTS5 paths at lines 546-563; Filter-only path at lines 566-588 validates filters exist and queries user_prompts table directly; FTS5 path at lines 591-616 rebuilds filter conditions with separate ftsParams array to avoid parameter conflicts; Filter-only path joins sdk_sessions table for project filtering support; Documentation updated to clarify dual-mode operation matching searchObservations and searchSessions patterns | Concepts: what-changed, how-it-works, pattern, gotcha | Files: src/services/sqlite/SessionSearch.ts\n\n---\nDate: 11/17/2025, 11:50:08 PM\n\n---\n\n## Add filter-only query path to searchSessions method\n*Source: claude-mem://observation/10751*\n\n**Method now accepts undefined query and queries session_summaries table directly for metadata-only filtering.**\n\nThe searchSessions method in SessionSearch.ts has been enhanced to support filter-only queries without query text. The method signature now accepts `query: string | undefined`, enabling two distinct code paths. When query is undefined, a new filter-only path (lines 331-352) bypasses FTS5 entirely and queries the session_summaries table directly using metadata filters like project or date range. This path validates that at least some filters exist, throwing an error if both query and filters are missing. When query text is provided, the existing FTS5 search path executes as before. This dual-mode approach allows the system to handle both semantic/keyword search and pure metadata filtering, addressing the architectural requirement that filter-only queries skip text search mechanisms.\n\n---\nType: feature | Facts: searchSessions signature changed to accept `query: string | undefined` at line 327; New filter-only path added at lines 331-352 that bypasses FTS5 when query is undefined; Filter-only path throws error if neither query nor filters provided at line 336; Direct SQLite query on session_summaries table uses WHERE clause built from metadata filters; FTS5 path at line 355 only executes when query text exists; Documentation updated to clarify dual-mode operation: FTS5 fallback vs filter-only direct query | Concepts: what-changed, how-it-works, pattern | Files: src/services/sqlite/SessionSearch.ts\n\n---\nDate: 11/17/2025, 11:49:23 PM\n\n---\n\n## Implemented Filter-Only Query Path in searchObservations Method\n*Source: claude-mem://observation/10749*\n\n**Added conditional logic to route filter-only queries directly to observations table, bypassing FTS5 when query parameter is undefined.**\n\nThe searchObservations method in SessionSearch.ts now supports two distinct query paths based on whether a query string is provided. The method signature was updated to accept query as string | undefined, enabling filter-only queries. When query is undefined, the new filter-only path executes: it builds filter conditions using buildFilterClause, validates that at least some filters exist (throwing an error if neither query nor filters are present), constructs a direct SQL query against the observations table without joining observations_fts, and returns results ordered by the specified criteria (with hasFTS: false passed to buildOrderClause since no FTS5 rank is available). When query text exists, the original FTS5 path executes, now explicitly documented as fallback mode for ChromaDB unavailability. This dual-path implementation allows metadata queries to go directly to SQLite without the overhead of FTS5, while preserving FTS5 functionality for text search when ChromaDB is unavailable.\n\n---\nType: feature | Facts: The searchObservations method signature changed to accept query parameter as string | undefined; Added filter-only code path that executes when query parameter is undefined or empty; Filter-only path queries observations table directly without JOIN to observations_fts table; Filter-only path throws error if no filters are provided, requiring either query or filters; Filter-only path passes false to buildOrderClause to indicate FTS5 rank is unavailable; FTS5 path only executes when query text exists, serving as fallback for ChromaDB unavailability; Both paths reuse the same buildFilterClause method for consistent filter handling | Concepts: how-it-works, pattern, what-changed | Files: src/services/sqlite/SessionSearch.ts\n\n---\nDate: 11/17/2025, 11:48:59 PM"}]
\ No newline at end of file
diff --git a/test-results/test-24-how-it-works-endpoint.json b/test-results/test-24-how-it-works-endpoint.json
deleted file mode 100644
index a7300406..00000000
--- a/test-results/test-24-how-it-works-endpoint.json
+++ /dev/null
@@ -1 +0,0 @@
-[{"type":"text","text":"## Build and sync claude-mem plugin to marketplace location\n*Source: claude-mem://observation/10758*\n\n**Built all hooks and services, synced to marketplace directory, and installed dependencies successfully.**\n\nThe claude-mem plugin was fully built and deployed to the marketplace directory. The build process compiled all React components, bundled the worker service and search server, and built each individual hook. The build script used scripts/build-hooks.js to generate production-ready bundles in the plugin/scripts/ directory. After building, rsync transferred all 11781 files to the marketplace directory at ~/.claude/plugins/marketplaces/thedotmack/, removing the .git directory and .install-version file in the process. The sync command used --delete flag to ensure the marketplace directory perfectly mirrors the development directory. Finally, npm install ran in the marketplace directory to ensure all dependencies are available for the deployed plugin.\n\n---\nType: change | Facts: Built claude-mem version 6.0.9 including React viewer, worker service, search server, and all hooks; Worker service built to 1338.75 KB, search server to 335.14 KB; Synced 11781 files from development directory to ~/.claude/plugins/marketplaces/thedotmack/; All hooks built successfully: context-hook (38.29 KB), new-hook (32.14 KB), save-hook (32.49 KB), summary-hook (34.61 KB), cleanup-hook (31.38 KB), user-message-hook (2.24 KB); Dependencies auto-installed in marketplace location after sync | Concepts: what-changed, how-it-works | Files: scripts/build-hooks.js, package.json, plugin/scripts/worker-service.cjs, plugin/scripts/search-server.mjs, plugin/scripts/context-hook.js, plugin/scripts/new-hook.js, plugin/scripts/save-hook.js, plugin/scripts/summary-hook.js, plugin/scripts/cleanup-hook.js, plugin/scripts/user-message-hook.js, plugin/ui/viewer-bundle.js, plugin/ui/viewer.html\n\n---\nDate: 11/17/2025, 11:51:51 PM\n\n---\n\n## Unified search handler implements Chroma-first with FTS5 fallback on zero results\n*Source: claude-mem://observation/10757*\n\n**Lines 403-489 show current implementation attempts Chroma first, falls back to FTS5 when empty.**\n\nThe unified search handler in search-server.ts (lines 390-489) reveals the current implementation architecture. The handler first attempts Chroma semantic search when chromaClient is available, calling queryChroma with the query parameter at line 419. If Chroma throws an error, it catches at line 465 and falls through to FTS5. However, the problematic FTS5 fallback logic at line 470 triggers when all result arrays are empty, not just on errors. This contradicts the insight that FTS5 contains identical data to Chroma, making zero-result fallback pointless. Additionally, lines 476-483 call search methods (searchObservations, searchSessions, searchUserPrompts) with the query parameter without checking if it's undefined, which will cause failures when query is not provided. The handler needs modification to skip Chroma when query is undefined and only fall back to FTS5 on Chroma errors.\n\n---\nType: discovery | Facts: Line 403 checks `if (chromaClient)` to attempt Chroma semantic search first; Line 419 calls `queryChroma(query, 100, whereFilter)` with query parameter as required; Lines 465-467 catch Chroma errors and fall through to FTS5 fallback; Line 470 triggers FTS5 fallback when all result arrays have zero length; Lines 476-483 call SessionSearch methods with query parameter without checking if undefined; Hybrid search applies 90-day recency filter at lines 424-431 | Concepts: how-it-works, problem-solution, gotcha | Files: src/servers/search-server.ts\n\n---\nDate: 11/17/2025, 11:50:29 PM\n\n---\n\n## Add filter-only query path to searchUserPrompts method\n*Source: claude-mem://observation/10755*\n\n**Method accepts undefined query and handles dual-path logic with separate parameter arrays for clarity.**\n\nThe searchUserPrompts method has been enhanced with filter-only query support, completing the pattern established in searchObservations and searchSessions. The method signature now accepts optional query parameter. The implementation builds base filter conditions once for project and date range filters, then diverges into two paths. The filter-only path (when query is undefined) validates that at least some filters exist, then queries the user_prompts table directly with a WHERE clause, joining sdk_sessions for project filtering. The FTS5 path rebuilds filter conditions into a separate ftsParams array to avoid parameter ordering conflicts between the two paths. This dual-path approach enables both semantic/keyword search via FTS5 and pure metadata filtering via direct SQLite queries, completing the architectural pattern across all three search methods.\n\n---\nType: feature | Facts: searchUserPrompts signature changed to accept `query: string | undefined` at line 541; Filter conditions built once and shared between filter-only and FTS5 paths at lines 546-563; Filter-only path at lines 566-588 validates filters exist and queries user_prompts table directly; FTS5 path at lines 591-616 rebuilds filter conditions with separate ftsParams array to avoid parameter conflicts; Filter-only path joins sdk_sessions table for project filtering support; Documentation updated to clarify dual-mode operation matching searchObservations and searchSessions patterns | Concepts: what-changed, how-it-works, pattern, gotcha | Files: src/services/sqlite/SessionSearch.ts\n\n---\nDate: 11/17/2025, 11:50:08 PM\n\n---\n\n## searchUserPrompts Uses Custom ORDER BY Logic Instead of buildOrderClause\n*Source: claude-mem://observation/10754*\n\n**Unlike searchObservations, searchUserPrompts implements inline ORDER BY construction with hard-coded table aliases rather than using buildOrderClause helper.**\n\nThe searchUserPrompts method implements its ORDER BY logic inline rather than using the buildOrderClause helper method used by searchObservations and searchSessions. Lines 619-623 show a ternary expression that constructs the ORDER BY clause with hard-coded references to user_prompts_fts.rank for relevance ordering and up.created_at_epoch for date-based sorting. The main SQL query joins three tables: user_prompts, user_prompts_fts, and sdk_sessions, with the sdk_sessions join enabling project-based filtering. When implementing the filter-only path for this method, the code will need to construct a query that joins only user_prompts and sdk_sessions (skipping user_prompts_fts), and the ORDER BY logic will need to handle the absence of FTS5 rank data, defaulting to date-based ordering when query is undefined.\n\n---\nType: discovery | Facts: searchUserPrompts constructs ORDER BY clause inline at lines 619-623 instead of calling buildOrderClause; The inline ORDER BY uses hard-coded table alias user_prompts_fts.rank for relevance ordering; Date-based ordering uses up.created_at_epoch for both ascending and descending sorts; The SQL query joins user_prompts, user_prompts_fts, and sdk_sessions tables; searchUserPrompts will need filter-only path to query user_prompts without user_prompts_fts JOIN when query is undefined | Concepts: how-it-works, pattern | Files: src/services/sqlite/SessionSearch.ts\n\n---\nDate: 11/17/2025, 11:50:07 PM\n\n---\n\n## User Prompts Search Joins Three Tables for Filtering\n*Source: claude-mem://observation/10753*\n\n**searchUserPrompts method joins user_prompts, user_prompts_fts, and sdk_sessions tables to enable project-based filtering.**\n\nThe searchUserPrompts method demonstrates a more complex query structure than searchObservations or searchSessions due to the need for project filtering. Since user_prompts don't directly store project information, the method joins with the sdk_sessions table using claude_session_id as the key, which allows filtering by the project field from sdk_sessions. The query structure follows the same FTS5 pattern seen in other search methods: it joins the base table (user_prompts) with its FTS5 virtual table (user_prompts_fts) via rowid, uses the MATCH operator for full-text search, and orders by rank for relevance-based results. The default limit is set to 20 rather than 50, suggesting user prompts are typically returned in smaller batches. Like the other search methods, rank values are normalized to a 0-1 score scale where higher values indicate better matches, inverting FTS5's native lower-is-better ranking.\n\n---\nType: discovery | Facts: searchUserPrompts method joins three tables: user_prompts, user_prompts_fts, and sdk_sessions; sdk_sessions table joined via claude_session_id to enable project filtering on user prompts; user_prompts table aliased as 'up' and uses MATCH operator on user_prompts_fts virtual table; Default limit for user prompts is 20, compared to 50 for observations and sessions; Method supports dateRange filtering using created_at_epoch column from user_prompts table; Rank normalization converts FTS5 rank scores to 0-1 scale where higher values indicate better matches | Concepts: how-it-works, pattern | Files: /Users/alexnewman/Scripts/claude-mem/src/services/sqlite/SessionSearch.ts\n\n---\nDate: 11/17/2025, 11:49:42 PM"}]
\ No newline at end of file
diff --git a/test-results/test-25-contextualize-endpoint.json b/test-results/test-25-contextualize-endpoint.json
deleted file mode 100644
index 6b9dd602..00000000
--- a/test-results/test-25-contextualize-endpoint.json
+++ /dev/null
@@ -1 +0,0 @@
-[{"type":"text","text":"# Project Context: claude-mem\n\nNo activity found for this project."}]
\ No newline at end of file
diff --git a/test-results/test-26-timeline-around-observation.json b/test-results/test-26-timeline-around-observation.json
deleted file mode 100644
index f5a13db4..00000000
--- a/test-results/test-26-timeline-around-observation.json
+++ /dev/null
@@ -1 +0,0 @@
-[{"type":"text","text":"# Timeline around anchor: 10630\n**Window:** 3 records before → 3 records after | **Items:** 3 (3 obs, 0 sessions, 0 prompts)\n\n**Legend:** 🎯 session-request | 🔴 bugfix | 🟣 feature | 🔄 refactor | ✅ change | 🔵 discovery | 🧠 decision\n\n### Nov 17, 2025\n\n**General**\n| ID | Time | T | Title | Tokens |\n|----|------|---|-------|--------|\n| #10756 | 11:50 PM | 🔴 | Fixed Incorrect Parameter Array in searchUserPrompts FTS5 Path | ~213 |\n| #10757 | ″ | 🔵 | Unified search handler implements Chroma-first with FTS5 fallback on zero results | ~224 |\n| #10758 | 11:51 PM | ✅ | Build and sync claude-mem plugin to marketplace location | ~189 |\n"}]
\ No newline at end of file
diff --git a/test-results/test-27-multi-param-combo.json b/test-results/test-27-multi-param-combo.json
deleted file mode 100644
index cdaa0045..00000000
--- a/test-results/test-27-multi-param-combo.json
+++ /dev/null
@@ -1 +0,0 @@
-[{"type":"text","text":"## FTS5 Method Renamed to Indicate Fallback Status\n*Source: claude-mem://observation/10747*\n\n**Renamed escapeFTS5 to escapeFTS5_fallback_when_chroma_unavailable across all search methods to clarify temporary usage.**\n\nThe FTS5 query escaping method was renamed from escapeFTS5 to escapeFTS5_fallback_when_chroma_unavailable to align with the newly established naming convention for FTS5-related code. This change was applied across all three search methods in SessionSearch: searchObservations, searchSessions, and searchUserPrompts. The rename makes explicit that FTS5 is only being used as a temporary fallback solution when ChromaDB (which requires UVX) is unavailable, rather than being a permanent part of the architecture. The method's existing documentation already stated that FTS5 provides degraded search capability with no semantic understanding and is only used when uvx/Python/ChromaDB is disabled. This refactoring follows the pattern established in CLAUDE.md where FTS5-related functions are given semantic names indicating their temporary nature pending UVX availability.\n\n---\nType: refactor | Facts: File modified: /Users/alexnewman/Scripts/claude-mem/src/services/sqlite/SessionSearch.ts; Method escapeFTS5 renamed to escapeFTS5_fallback_when_chroma_unavailable throughout the file; Three search methods updated: searchObservations, searchSessions, and searchUserPrompts; All calls to escapeFTS5 replaced with escapeFTS5_fallback_when_chroma_unavailable using replace_all; Method already had documentation indicating FTS5 is degraded fallback when ChromaDB unavailable | Concepts: what-changed, why-it-exists, pattern | Files: /Users/alexnewman/Scripts/claude-mem/src/services/sqlite/SessionSearch.ts\n\n---\nDate: 11/17/2025, 11:48:30 PM\n\n---\n\n## Architectural insight: use SessionStore for filter-only, SessionSearch for text search\n*Source: claude-mem://observation/10722*\n\n**SessionSearch class handles FTS5 text search; SessionStore class should handle structured filter queries without text.**\n\nA key architectural insight emerged: the system already has appropriate separation of concerns between SessionSearch and SessionStore classes. SessionSearch is designed for full-text search using FTS5, which inherently requires search text. SessionStore should handle structured queries based purely on filters (type, concepts, files, dates) without requiring full-text search. The real fix isn't to patch escapeFTS5 or make FTS5 work without a query - it's to route filter-only queries to SessionStore methods instead of SessionSearch methods. This preserves the architectural integrity where each class serves its intended purpose.\n\n---\nType: decision | Facts: SessionSearch.searchObservations always uses FTS5 with WHERE observations_fts MATCH ?; FTS5 fundamentally requires search term, cannot MATCH against nothing; SessionStore class likely contains methods for structured queries without FTS5; Proper architecture: SessionSearch for full-text search, SessionStore for filter-based retrieval; Sequential thinking at thought 8 of 12 | Concepts: pattern, how-it-works, trade-off, why-it-exists\n\n---\nDate: 11/17/2025, 10:08:29 PM\n\n---\n\n## Root cause identified: required query parameter vs filter-only queries\n*Source: claude-mem://observation/10717*\n\n**Schema defines query as required but filter-only queries need query to be optional.**\n\nThe root cause has been identified: a mismatch between the schema definition and actual usage patterns. The Zod schema defines query as z.string(), marking it as required, yet the system appears to receive requests without a query parameter for filter-only searches. This creates two possibilities: either (1) the schema should allow optional queries to support filtering by type/concepts/files without search text, or (2) query should truly be required and filter-only requests should be rejected. The investigation now shifts to understanding whether the search functions can meaningfully operate without a query parameter, which will determine the correct fix approach.\n\n---\nType: discovery | Facts: Line 366 defines query: z.string() as required, not optional; Runtime errors show requests without query are passing through validation; Filter-only queries (using type, obs_type, concepts, files) should be valid use cases; Schema should likely be z.string().optional() to support filter-only retrieval; Sequential thinking extended from 10 to 12 total thoughts; Next investigation examines search.searchObservations to determine if query is truly required | Concepts: problem-solution, gotcha, trade-off\n\n---\nDate: 11/17/2025, 10:07:36 PM\n\n---\n\n## Session Search Returns Multi-Session Summaries with Structured Format\n*Source: claude-mem://observation/10696*\n\n**Search retrieved five session summaries about hybrid search architecture work with completed tasks and learnings**\n\nExamination of test-20-session-search.json reveals how session-level search provides comprehensive work history context. The query retrieved five sessions related to hybrid search architecture work, demonstrating the value of session summaries for understanding project evolution. Each session result follows a structured format with five sections: Completed (what was accomplished), Learned (technical insights gained), Investigated (what was examined), Next Steps (planned follow-up), and Notes (additional context). Sessions are tagged with claude-mem://session/UUID source links for reference. The retrieved sessions trace the complete lifecycle of the hybrid search architecture documentation fix: initial creation of the fix document, relocation between projects, implementation of documentation updates, PR creation and merging, and final comment corrections for architectural accuracy. This demonstrates how session-level search complements observation search by providing narrative flow and project history context rather than atomic technical facts.\n\n---\nType: discovery | Facts: Session search results return summaries with Completed, Learned, Investigated, Next Steps, and Notes sections; Session summaries tagged with source links using claude-mem://session/UUID format; Search query about hybrid search architecture retrieved five relevant sessions from November 15-17, 2025; Session results include session date and comprehensive context about what was accomplished in each session; Retrieved sessions document the evolution of hybrid search architecture fixes across multiple work sessions | Concepts: how-it-works, pattern, why-it-exists | Files: test-results/test-20-session-search.json\n\n---\nDate: 11/17/2025, 9:52:57 PM\n\n---\n\n## Context Retrieval System Architecture Analysis\n*Source: claude-mem://observation/10694*\n\n**Three-layer hybrid semantic search enables precision context injection for SessionStart**\n\nAnalysis of the 28 passing test queries reveals a production-grade context retrieval system designed for precision over bulk context injection. The architecture operates on three distinct layers: observations (AI-compressed insights), sessions (overall summaries), and prompts (exact user language), allowing retrieval strategies tailored to different needs. The hybrid Chroma + FTS5 search enables both semantic matching (finding conceptually related content) and keyword matching (exact term searches). Multi-parameter filter composability transforms broad queries like \"authentication\" into precise requests like \"bugfix observations about authentication in auth.ts from last week\". Timeline functionality provides chronological context to understand why decisions were made and how features evolved. The dual endpoint strategy separates ad-hoc session queries from optimized SessionStart injection patterns. This design philosophy prioritizes targeted, relevant context delivery over simply dumping recent observations.\n\n---\nType: discovery | Facts: Search system implements three-layer architecture: AI-compressed observations, session-level summaries, and verbatim user prompts; Hybrid search combines Chroma semantic embeddings with SQLite FTS5 keyword search for both conceptual and exact matching; Multi-parameter filtering allows stacking filters (type+query+file+dateRange+concept+project) for precision retrieval; Timeline queries (get_context_timeline and get_timeline_by_query) provide chronological context around specific events; Dual endpoint approach: unified /api/search for ad-hoc queries and dedicated endpoints (by-type, by-file, by-concept) for optimized SessionStart context injection; Test categories validate real-world context retrieval patterns: semantic (conceptual similarity), decision (architectural choices), troubleshooting (past bug fixes), file-specific (code history) | Concepts: how-it-works, pattern, why-it-exists\n\n---\nDate: 11/17/2025, 9:52:24 PM"}]
\ No newline at end of file
diff --git a/test-results/test-28-file-type-combo.json b/test-results/test-28-file-type-combo.json
deleted file mode 100644
index 362b18bd..00000000
--- a/test-results/test-28-file-type-combo.json
+++ /dev/null
@@ -1 +0,0 @@
-[{"type":"text","text":"## Enhanced MCP Search Tool with Document Type and Metadata Filters\n*Source: claude-mem://observation/10631*\n\n**Added type-specific filtering to unified search tool supporting observations, sessions, prompts with metadata filters.**\n\nThe unified search tool in the MCP search server was enhanced with granular filtering capabilities. Four new optional parameters were added to the search tool's input schema: a 'type' parameter to filter by document type (observations/sessions/prompts), an 'obs_type' parameter to filter observations by their observation type, a 'concepts' parameter to filter by concept tags, and a 'files' parameter to filter by file paths. The obs_type, concepts, and files filters are observation-specific and only take effect when the type parameter is set to \"observations\". This enhancement allows clients to perform more targeted searches through the MCP interface while maintaining backward compatibility since all new parameters are optional.\n\n---\nType: feature | Facts: File src/servers/search-server.ts search tool inputSchema extended with new filter parameters; New type parameter filters by document type: observations, sessions, or prompts; New obs_type parameter filters observations by type (decision, bugfix, feature, refactor, discovery, change); New concepts parameter filters observations by concept tags, accepts string or array; New files parameter filters observations by file paths with partial matching, accepts string or array; Type-specific filters (obs_type, concepts, files) only apply when type equals observations | Concepts: what-changed, how-it-works, pattern | Files: src/servers/search-server.ts\n\n---\nDate: 11/17/2025, 9:21:49 PM\n\n---\n\n## Contextualize Tool for Comprehensive Project Overview\n*Source: claude-mem://observation/10597*\n\n**New tool provides one-stop project context with recent activity summaries and timeline visualization.**\n\nA comprehensive 'contextualize' tool was added to provide a quick project overview in a single request. The tool fetches recent observations (default 7), sessions (default 7), and user prompts (default 3), then identifies the newest activity across all types to use as a timeline anchor. The output is structured in sections: header with overview statistics and latest activity timestamp, followed by summaries of recent observations (top 3 with type emoji icons), recent sessions (top 3), and recent user prompts (all, truncated to 60 chars). The timeline section shows the last 3 days of activity around the newest item, displaying up to 5 entries per day with timestamps and type-specific icons. All activity is pulled using configurable limits, and timeline depth defaults to 5 records before/after the newest item. The tool is designed as a starting point for exploration, with footer text suggesting more specialized tools for deeper investigation. This addresses the \"what's been happening\" use case with minimal token overhead.\n\n---\nType: feature | Facts: Added 'contextualize' tool that fetches recent observations (7), sessions (7), and user prompts (3) in configurable quantities; Tool identifies newest activity across all memory types and builds timeline centered around it; Output includes three summary sections showing top 3 items from each category with type icons; Timeline section displays last 3 days of activity with up to 5 items per day in reverse chronological order; All limits configurable via parameters: observations_limit, sessions_limit, prompts_limit, timeline_depth; Defaults to current working directory basename for project filtering; Footer suggests other tools for deeper exploration (search, timeline, decisions, changes, how_it_works) | Concepts: what-changed, how-it-works, pattern | Files: src/servers/search-server.ts\n\n---\nDate: 11/17/2025, 8:36:23 PM\n\n---\n\n## Semantic Shortcut Tools for Common Search Patterns\n*Source: claude-mem://observation/10596*\n\n**Added three convenience tools for frequently accessed observation categories: decisions, changes, and how-it-works.**\n\nThree semantic shortcut tools were added to the MCP search server to provide convenient access to commonly searched observation patterns. The 'decisions' tool is equivalent to find_by_type with type=\"decision\", filtering for observations where important architectural, technical, or process decisions were made. The 'changes' tool provides a broader search by combining results from type=\"change\", concept=\"change\", and concept=\"what-changed\", deduplicating IDs before applying semantic ranking with a \"what changed\" query. The 'how_it_works' tool searches for concept=\"how-it-works\" observations and applies semantic ranking with \"how it works architecture\" to surface the most relevant architectural documentation. All three tools use the metadata-first, semantic-enhanced search pattern where SQLite filters by type/concept first, then ChromaDB ranks results by semantic relevance. They support the same index/full format options as other search tools, defaulting to index format for token efficiency. When ChromaDB is unavailable, they fall back to SQLite-only searches with chronological ordering.\n\n---\nType: feature | Facts: Added 'decisions' tool as shortcut for finding decision-type observations with semantic ranking; Added 'changes' tool that searches type=change OR concept=change OR concept=what-changed with deduplication; Added 'how_it_works' tool for finding how-it-works concept observations with architecture-focused semantic queries; All three tools support index and full output formats with same token-efficiency guidelines as other search tools; Semantic shortcuts use metadata-first filtering followed by ChromaDB semantic ranking for relevance; Changes tool combines results from multiple filters (type and concepts) before semantic ranking; Tools fall back to SQLite-only search when ChromaDB unavailable | Concepts: what-changed, how-it-works, pattern | Files: src/servers/search-server.ts\n\n---\nDate: 11/17/2025, 8:35:32 PM\n\n---\n\n## Unified Timeline Tool Combining Anchor and Query Modes\n*Source: claude-mem://observation/10595*\n\n**New timeline tool supports both anchor-based and query-based timeline exploration in single interface.**\n\nA new unified 'timeline' tool was added to the MCP search server that combines the capabilities of get_context_timeline and get_timeline_by_query into a single interface. The tool accepts either an anchor parameter for anchor-based timeline exploration or a query parameter for search-based timeline discovery, but not both. Input validation ensures exactly one parameter is provided with helpful error messages guiding proper usage. In query mode, the tool performs a hybrid semantic search (ChromaDB with FTS5 fallback) to find the most relevant observation matching the query, then uses that observation as the timeline anchor. In anchor mode, it supports three anchor types: numeric observation IDs, session ID strings (format \"S123\" or \"#S123\"), and ISO timestamp strings. Both modes use the same timeline rendering logic with observations, sessions, and prompts interleaved chronologically, grouped by day, and formatted with tables for observations and standalone entries for sessions/prompts. The header adapts to the mode, showing the query and matched anchor for query-based searches or just the anchor identifier for anchor-based searches.\n\n---\nType: feature | Facts: Added new 'timeline' tool that consolidates get_context_timeline and get_timeline_by_query functionality; Timeline tool accepts either anchor parameter (observation ID, session ID, or ISO timestamp) or query parameter (natural language search); Tool validates that exactly one of anchor or query is provided with clear error messages; Query mode performs hybrid semantic search to find best matching observation and uses it as timeline anchor; Anchor mode supports three anchor types: numeric observation ID, session ID string (S123), or ISO timestamp string; Timeline rendering identical for both modes with chronologically sorted observations, sessions, and prompts grouped by day; Header changes based on mode showing either \"Timeline for query\" or \"Timeline around anchor\" | Concepts: what-changed, how-it-works, pattern | Files: src/servers/search-server.ts\n\n---\nDate: 11/17/2025, 8:34:35 PM\n\n---\n\n## Unified Search Tool Added to MCP Server\n*Source: claude-mem://observation/10594*\n\n**New unified search tool queries all memory types simultaneously with combined chronological results.**\n\nA new unified 'search' tool was added to the MCP search server that queries all memory types (observations, sessions, and user prompts) in a single operation. Unlike the existing type-specific tools (search_observations, search_sessions, search_user_prompts), this unified tool performs a single ChromaDB query without doc_type filtering, then categorizes results based on metadata and hydrates each type from SQLite. All results are combined into a single array with type discriminators, sorted chronologically according to the orderBy parameter, and then limited. The output format distinguishes between types when rendering results, using the appropriate formatter (formatObservationIndex, formatSessionIndex, formatUserPromptIndex) based on the item type. This provides a more efficient search workflow when users want to see all relevant context across different memory types without running multiple separate searches.\n\n---\nType: feature | Facts: Added new 'search' tool that queries observations, sessions, and user prompts in a single operation; Unified search categorizes ChromaDB results by doc_type metadata field (observation, session_summary, user_prompt); Results from all document types combined and sorted chronologically by epoch timestamp; Limit applies across all document types after merging results; Index format header shows breakdown of result counts by type (e.g., \"5 obs, 3 sessions, 2 prompts\"); Unified search uses same hybrid ChromaDB + FTS5 fallback strategy as type-specific search tools | Concepts: what-changed, how-it-works, pattern | Files: src/servers/search-server.ts\n\n---\nDate: 11/17/2025, 8:33:18 PM"}]
\ No newline at end of file
diff --git a/tests/cwd-propagation.test.ts b/tests/cwd-propagation.test.ts
deleted file mode 100644
index 0b9f3c59..00000000
--- a/tests/cwd-propagation.test.ts
+++ /dev/null
@@ -1,182 +0,0 @@
-import { test, describe } from 'node:test';
-import assert from 'node:assert';
-
-/**
- * CWD Propagation Tests
- *
- * These tests verify that the working directory (cwd) context flows correctly
- * from hook input through the worker service to the SDK agent prompts.
- */
-
-describe('CWD Propagation Tests', () => {
- test('save-hook should extract cwd from input', () => {
- // Test that PostToolUseInput interface includes cwd
- const mockInput = {
- session_id: 'test-session',
- cwd: '/home/user/project',
- tool_name: 'ReadTool',
- tool_input: { path: 'README.md' },
- tool_response: { content: 'test' }
- };
-
- // Verify the shape matches PostToolUseInput
- assert.strictEqual(typeof mockInput.cwd, 'string');
- assert.strictEqual(mockInput.cwd, '/home/user/project');
- });
-
- test('ObservationData should include cwd field', () => {
- // Import the type to ensure it compiles with cwd
- type ObservationData = {
- tool_name: string;
- tool_input: any;
- tool_response: any;
- prompt_number: number;
- cwd?: string;
- };
-
- const mockData: ObservationData = {
- tool_name: 'ReadTool',
- tool_input: { path: 'test.ts' },
- tool_response: { content: 'test' },
- prompt_number: 1,
- cwd: '/test/project'
- };
-
- assert.strictEqual(mockData.cwd, '/test/project');
- });
-
- test('PendingMessage should include cwd field', () => {
- // Import the type to ensure it compiles with cwd
- type PendingMessage = {
- type: 'observation' | 'summarize';
- tool_name?: string;
- tool_input?: any;
- tool_response?: any;
- prompt_number?: number;
- cwd?: string;
- };
-
- const mockMessage: PendingMessage = {
- type: 'observation',
- tool_name: 'ReadTool',
- tool_input: { path: 'test.ts' },
- tool_response: { content: 'test' },
- prompt_number: 1,
- cwd: '/test/workspace'
- };
-
- assert.strictEqual(mockMessage.cwd, '/test/workspace');
- });
-
- test('buildObservationPrompt should include tool_cwd when present', () => {
- // Mock implementation of what buildObservationPrompt does
- const mockObservation = {
- id: 1,
- tool_name: 'ReadTool',
- tool_input: JSON.stringify({ path: 'test.ts' }),
- tool_output: JSON.stringify({ content: 'test' }),
- created_at_epoch: Date.now(),
- cwd: '/home/user/my-project'
- };
-
- // Simulate the prompt generation
- const promptSegment = mockObservation.cwd
- ? `\n ${mockObservation.cwd}`
- : '';
-
- // Verify cwd is included in the prompt
- assert.ok(promptSegment.includes(''));
- assert.ok(promptSegment.includes('/home/user/my-project'));
- });
-
- test('buildObservationPrompt should handle missing cwd gracefully', () => {
- // Mock observation without cwd
- const mockObservation = {
- id: 1,
- tool_name: 'ReadTool',
- tool_input: JSON.stringify({ path: 'test.ts' }),
- tool_output: JSON.stringify({ content: 'test' }),
- created_at_epoch: Date.now()
- };
-
- // Simulate the prompt generation (no cwd)
- const promptSegment = mockObservation.cwd
- ? `\n ${mockObservation.cwd}`
- : '';
-
- // Verify no tool_cwd element when cwd is undefined
- assert.strictEqual(promptSegment, '');
- });
-
- test('worker API body should include cwd field', () => {
- // Mock worker API request body
- const requestBody = {
- tool_name: 'ReadTool',
- tool_input: JSON.stringify({ path: 'test.ts' }),
- tool_response: JSON.stringify({ content: 'test' }),
- prompt_number: 1,
- cwd: '/workspace/project'
- };
-
- // Verify all expected fields are present
- assert.strictEqual(requestBody.tool_name, 'ReadTool');
- assert.strictEqual(requestBody.prompt_number, 1);
- assert.strictEqual(requestBody.cwd, '/workspace/project');
- });
-
- test('buildInitPrompt should mention spatial awareness', () => {
- // Mock the init prompt check
- const initPromptSnippet = `SPATIAL AWARENESS: Tool executions include the working directory (tool_cwd) to help you understand:
-- Which repository/project is being worked on
-- Where files are located relative to the project root
-- How to match requested paths to actual execution paths`;
-
- // Verify the prompt explains spatial awareness
- assert.ok(initPromptSnippet.includes('SPATIAL AWARENESS'));
- assert.ok(initPromptSnippet.includes('tool_cwd'));
- assert.ok(initPromptSnippet.includes('working directory'));
- });
-
- test('cwd should flow from hook to worker to SDK agent', () => {
- // End-to-end flow test (conceptual)
- const hookInput = {
- session_id: 'test-123',
- cwd: '/home/developer/awesome-project',
- tool_name: 'ReadTool',
- tool_input: { path: 'src/index.ts' },
- tool_response: { content: 'export default...' }
- };
-
- // Step 1: Hook extracts cwd
- const extractedCwd = hookInput.cwd;
- assert.strictEqual(extractedCwd, '/home/developer/awesome-project');
-
- // Step 2: Worker receives cwd in observation data
- const observationData = {
- tool_name: hookInput.tool_name,
- tool_input: hookInput.tool_input,
- tool_response: hookInput.tool_response,
- prompt_number: 1,
- cwd: extractedCwd
- };
- assert.strictEqual(observationData.cwd, extractedCwd);
-
- // Step 3: SDK agent includes cwd in observation prompt
- const sdkObservation = {
- id: 0,
- tool_name: observationData.tool_name,
- tool_input: JSON.stringify(observationData.tool_input),
- tool_output: JSON.stringify(observationData.tool_response),
- created_at_epoch: Date.now(),
- cwd: observationData.cwd
- };
- assert.strictEqual(sdkObservation.cwd, extractedCwd);
-
- // Step 4: Prompt includes tool_cwd element
- const promptSnippet = sdkObservation.cwd
- ? `${sdkObservation.cwd}`
- : '';
- assert.ok(promptSnippet.includes(''));
- assert.ok(promptSnippet.includes(extractedCwd));
- });
-});
diff --git a/tests/session-search.test.ts b/tests/session-search.test.ts
deleted file mode 100644
index ede04c2a..00000000
--- a/tests/session-search.test.ts
+++ /dev/null
@@ -1,332 +0,0 @@
-import { test, describe } from 'node:test';
-import assert from 'node:assert';
-import Database from 'better-sqlite3';
-import { SessionSearch } from '../src/services/sqlite/SessionSearch';
-import fs from 'fs';
-import path from 'path';
-
-const TEST_DB_DIR = '/tmp/claude-mem-test';
-const TEST_DB_PATH = path.join(TEST_DB_DIR, 'test.db');
-
-describe('SessionSearch FTS5 Injection Tests', () => {
- let search: SessionSearch;
- let db: Database.Database;
-
- // Setup test database before each test
- function setupTestDB() {
- // Clean up any existing test database
- if (fs.existsSync(TEST_DB_DIR)) {
- fs.rmSync(TEST_DB_DIR, { recursive: true, force: true });
- }
- fs.mkdirSync(TEST_DB_DIR, { recursive: true });
-
- // Create database with required schema
- db = new Database(TEST_DB_PATH);
- db.pragma('journal_mode = WAL');
-
- // Create minimal schema needed for search tests
- // Note: Using claude_session_id to match SessionSearch expectations
- db.exec(`
- CREATE TABLE sdk_sessions (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- claude_session_id TEXT UNIQUE NOT NULL,
- project TEXT NOT NULL,
- started_at_epoch INTEGER DEFAULT ((unixepoch() * 1000))
- );
-
- CREATE TABLE observations (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- claude_session_id TEXT NOT NULL,
- prompt_number INTEGER DEFAULT 1,
- type TEXT NOT NULL,
- title TEXT,
- subtitle TEXT,
- narrative TEXT,
- text TEXT,
- facts TEXT,
- concepts TEXT,
- files_read TEXT,
- files_modified TEXT,
- project TEXT,
- created_at_epoch INTEGER DEFAULT ((unixepoch() * 1000)),
- FOREIGN KEY (claude_session_id) REFERENCES sdk_sessions(claude_session_id)
- );
-
- CREATE TABLE session_summaries (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- claude_session_id TEXT NOT NULL,
- prompt_number INTEGER DEFAULT 1,
- request TEXT,
- investigated TEXT,
- learned TEXT,
- completed TEXT,
- next_steps TEXT,
- notes TEXT,
- files_read TEXT,
- files_edited TEXT,
- project TEXT,
- created_at_epoch INTEGER DEFAULT ((unixepoch() * 1000)),
- FOREIGN KEY (claude_session_id) REFERENCES sdk_sessions(claude_session_id)
- );
-
- CREATE TABLE user_prompts (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- claude_session_id TEXT NOT NULL,
- prompt_number INTEGER DEFAULT 1,
- prompt_text TEXT NOT NULL,
- created_at_epoch INTEGER DEFAULT ((unixepoch() * 1000)),
- FOREIGN KEY (claude_session_id) REFERENCES sdk_sessions(claude_session_id)
- );
-
- -- Create FTS5 tables manually
- CREATE VIRTUAL TABLE observations_fts USING fts5(
- title,
- subtitle,
- narrative,
- text,
- facts,
- concepts,
- content='observations',
- content_rowid='id'
- );
-
- CREATE VIRTUAL TABLE session_summaries_fts USING fts5(
- request,
- investigated,
- learned,
- completed,
- next_steps,
- notes,
- content='session_summaries',
- content_rowid='id'
- );
-
- CREATE VIRTUAL TABLE user_prompts_fts USING fts5(
- prompt_text,
- content='user_prompts',
- content_rowid='id'
- );
-
- -- Create triggers for observations
- CREATE TRIGGER observations_ai AFTER INSERT ON observations BEGIN
- INSERT INTO observations_fts(rowid, title, subtitle, narrative, text, facts, concepts)
- VALUES (new.id, new.title, new.subtitle, new.narrative, new.text, new.facts, new.concepts);
- END;
-
- CREATE TRIGGER observations_ad AFTER DELETE ON observations BEGIN
- INSERT INTO observations_fts(observations_fts, rowid, title, subtitle, narrative, text, facts, concepts)
- VALUES('delete', old.id, old.title, old.subtitle, old.narrative, old.text, old.facts, old.concepts);
- END;
-
- CREATE TRIGGER observations_au AFTER UPDATE ON observations BEGIN
- INSERT INTO observations_fts(observations_fts, rowid, title, subtitle, narrative, text, facts, concepts)
- VALUES('delete', old.id, old.title, old.subtitle, old.narrative, old.text, old.facts, old.concepts);
- INSERT INTO observations_fts(rowid, title, subtitle, narrative, text, facts, concepts)
- VALUES (new.id, new.title, new.subtitle, new.narrative, new.text, new.facts, new.concepts);
- END;
-
- -- Create triggers for session_summaries
- CREATE TRIGGER session_summaries_ai AFTER INSERT ON session_summaries BEGIN
- INSERT INTO session_summaries_fts(rowid, request, investigated, learned, completed, next_steps, notes)
- VALUES (new.id, new.request, new.investigated, new.learned, new.completed, new.next_steps, new.notes);
- END;
-
- CREATE TRIGGER session_summaries_ad AFTER DELETE ON session_summaries BEGIN
- INSERT INTO session_summaries_fts(session_summaries_fts, rowid, request, investigated, learned, completed, next_steps, notes)
- VALUES('delete', old.id, old.request, old.investigated, old.learned, old.completed, old.next_steps, old.notes);
- END;
-
- CREATE TRIGGER session_summaries_au AFTER UPDATE ON session_summaries BEGIN
- INSERT INTO session_summaries_fts(session_summaries_fts, rowid, request, investigated, learned, completed, next_steps, notes)
- VALUES('delete', old.id, old.request, old.investigated, old.learned, old.completed, old.next_steps, old.notes);
- INSERT INTO session_summaries_fts(rowid, request, investigated, learned, completed, next_steps, notes)
- VALUES (new.id, new.request, new.investigated, new.learned, new.completed, new.next_steps, new.notes);
- END;
-
- -- Create triggers for user_prompts
- CREATE TRIGGER user_prompts_ai AFTER INSERT ON user_prompts BEGIN
- INSERT INTO user_prompts_fts(rowid, prompt_text)
- VALUES (new.id, new.prompt_text);
- END;
-
- CREATE TRIGGER user_prompts_ad AFTER DELETE ON user_prompts BEGIN
- INSERT INTO user_prompts_fts(user_prompts_fts, rowid, prompt_text)
- VALUES('delete', old.id, old.prompt_text);
- END;
-
- CREATE TRIGGER user_prompts_au AFTER UPDATE ON user_prompts BEGIN
- INSERT INTO user_prompts_fts(user_prompts_fts, rowid, prompt_text)
- VALUES('delete', old.id, old.prompt_text);
- INSERT INTO user_prompts_fts(rowid, prompt_text)
- VALUES (new.id, new.prompt_text);
- END;
- `);
-
- db.close();
-
- // Create SessionSearch instance
- return new SessionSearch(TEST_DB_PATH);
- }
-
- function teardownTestDB() {
- if (search) {
- search.close();
- search = null;
- }
- if (fs.existsSync(TEST_DB_DIR)) {
- fs.rmSync(TEST_DB_DIR, { recursive: true, force: true });
- }
- }
-
- test('should escape double quotes in search queries', () => {
- search = setupTestDB();
-
- // Insert test data
- const db = new Database(TEST_DB_PATH);
- db.exec(`
- INSERT INTO sdk_sessions (claude_session_id, project) VALUES ('test-session-1', 'test-project');
- INSERT INTO observations (claude_session_id, prompt_number, type, title, narrative, text, facts, concepts, files_read, files_modified, project)
- VALUES ('test-session-1', 1, 'feature', 'Test observation', 'A test "quoted" narrative', 'Some text', '[]', '[]', '[]', '[]', 'test-project');
- `);
- db.close();
-
- // Test query with double quotes - should not cause injection
- const maliciousQuery = 'test" OR 1=1 --';
-
- // This should not throw an error and should search safely
- const results = search.searchObservations(maliciousQuery);
-
- // With proper escaping, this should return 0 results (no match for the literal string)
- // Without escaping, it could match everything due to OR 1=1
- assert.strictEqual(Array.isArray(results), true, 'Should return an array');
-
- teardownTestDB();
- });
-
- test('should handle FTS5 special operators safely', () => {
- search = setupTestDB();
-
- // Insert test data
- const db = new Database(TEST_DB_PATH);
- db.exec(`
- INSERT INTO sdk_sessions (claude_session_id, project) VALUES ('test-session-2', 'test-project');
- INSERT INTO observations (claude_session_id, prompt_number, type, title, narrative, text, facts, concepts, files_read, files_modified, project)
- VALUES ('test-session-2', 1, 'feature', 'Security feature', 'Implements security', 'Authentication system', '[]', '[]', '[]', '[]', 'test-project');
- `);
- db.close();
-
- // Test queries with FTS5 operators that should be escaped
- const testQueries = [
- 'AND OR NOT', // Boolean operators
- '(parentheses)', // Grouping
- 'asterisk*', // Wildcard
- 'column:value', // Column filter attempt
- ];
-
- testQueries.forEach(query => {
- // Should not throw an error
- const results = search.searchObservations(query);
- assert.strictEqual(Array.isArray(results), true, `Should return array for query: ${query}`);
- });
-
- teardownTestDB();
- });
-
- test('should find exact phrase matches when properly escaped', () => {
- search = setupTestDB();
-
- // Insert test data
- const db = new Database(TEST_DB_PATH);
- db.exec(`
- INSERT INTO sdk_sessions (claude_session_id, project) VALUES ('test-session-3', 'test-project');
- INSERT INTO observations (claude_session_id, prompt_number, type, title, narrative, text, facts, concepts, files_read, files_modified, project)
- VALUES ('test-session-3', 1, 'feature', 'Hello world', 'This is a hello world example', 'Hello world program', '[]', '[]', '[]', '[]', 'test-project');
- INSERT INTO observations (claude_session_id, prompt_number, type, title, narrative, text, facts, concepts, files_read, files_modified, project)
- VALUES ('test-session-3', 2, 'feature', 'Goodbye moon', 'This is something else', 'Different content', '[]', '[]', '[]', '[]', 'test-project');
- `);
- db.close();
-
- // Search for exact phrase
- const results = search.searchObservations('hello world');
-
- assert.strictEqual(Array.isArray(results), true, 'Should return an array');
- assert.ok(results.length > 0, 'Should find at least one result');
- assert.ok(
- results.some(r => r.title?.toLowerCase().includes('hello') || r.narrative?.toLowerCase().includes('hello')),
- 'Should find observation with "hello"'
- );
-
- teardownTestDB();
- });
-
- test('should handle empty and special character queries safely', () => {
- search = setupTestDB();
-
- // Insert test data
- const db = new Database(TEST_DB_PATH);
- db.exec(`
- INSERT INTO sdk_sessions (claude_session_id, project) VALUES ('test-session-4', 'test-project');
- INSERT INTO observations (claude_session_id, prompt_number, type, title, narrative, text, facts, concepts, files_read, files_modified, project)
- VALUES ('test-session-4', 1, 'feature', 'Test', 'Test observation', 'Test content', '[]', '[]', '[]', '[]', 'test-project');
- `);
- db.close();
-
- // Test edge cases
- const edgeCases = [
- '""', // Empty quoted string
- ' ', // Whitespace only
- '!!!', // Special characters
- '@#$%', // More special characters
- ];
-
- edgeCases.forEach(query => {
- // Should not throw an error
- const results = search.searchObservations(query);
- assert.strictEqual(Array.isArray(results), true, `Should return array for edge case: "${query}"`);
- });
-
- teardownTestDB();
- });
-
- test('should search session summaries safely', () => {
- search = setupTestDB();
-
- // Insert test data
- const db = new Database(TEST_DB_PATH);
- db.exec(`
- INSERT INTO sdk_sessions (claude_session_id, project) VALUES ('test-session-5', 'test-project');
- INSERT INTO session_summaries (claude_session_id, prompt_number, request, investigated, learned, completed, next_steps, notes, files_read, files_edited, project)
- VALUES ('test-session-5', 1, 'Implement feature', 'Looked into options', 'Learned new approach', 'Completed task', 'Next: testing', 'Notes here', '[]', '[]', 'test-project');
- `);
- db.close();
-
- // Test with potential injection
- const maliciousQuery = 'feature" OR type:*';
- const results = search.searchSessions(maliciousQuery);
-
- assert.strictEqual(Array.isArray(results), true, 'Should return an array');
-
- teardownTestDB();
- });
-
- test('should search user prompts safely', () => {
- search = setupTestDB();
-
- // Insert test data
- const db = new Database(TEST_DB_PATH);
- db.exec(`
- INSERT INTO sdk_sessions (claude_session_id, project) VALUES ('test-session-6', 'test-project');
- INSERT INTO user_prompts (claude_session_id, prompt_number, prompt_text)
- VALUES ('test-session-6', 1, 'Please implement authentication');
- `);
- db.close();
-
- // Test with potential injection
- const maliciousQuery = 'authentication" AND request:*';
- const results = search.searchUserPrompts(maliciousQuery);
-
- assert.strictEqual(Array.isArray(results), true, 'Should return an array');
-
- teardownTestDB();
- });
-});
diff --git a/tests/test-process-cleanup.sh b/tests/test-process-cleanup.sh
deleted file mode 100644
index 736e5794..00000000
--- a/tests/test-process-cleanup.sh
+++ /dev/null
@@ -1,95 +0,0 @@
-#!/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