Files
claude-mem/docs/public/architecture/hooks.mdx
T
Alex Newman 61488042d8 Mem-search enhancements: table output, simplified API, Sonnet default, and removed fake URIs (#317)
* feat: Add batch fetching for observations and update documentation

- Implemented a new endpoint for fetching multiple observations by IDs in a single request.
- Updated the DataRoutes to include a POST /api/observations/batch endpoint.
- Enhanced SKILL.md documentation to reflect changes in the search process and batch fetching capabilities.
- Increased the default limit for search results from 5 to 40 for better usability.

* feat!: Fix timeline parameter passing with SearchManager alignment

BREAKING CHANGE: Timeline MCP tools now use standardized parameter names
- anchor_id → anchor
- before → depth_before
- after → depth_after
- obs_type → type (timeline tool only)

Fixes timeline endpoint failures caused by parameter name mismatch between
MCP layer and SearchManager. Adds new SessionStore methods for fetching
prompts and session summaries by ID.

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

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

* docs: reframe timeline parameter fix as bug fix, not breaking change

The timeline tools were completely broken due to parameter name mismatch.
There's nothing to migrate from since the old parameters never worked.

Co-authored-by: Alex Newman <thedotmack@users.noreply.github.com>

* Refactor mem-search documentation and optimize API tool definitions

- Updated SKILL.md to emphasize batch fetching for observations, clarifying usage and efficiency.
- Removed deprecated tools from mcp-server.ts and streamlined tool definitions for clarity.
- Enhanced formatting in FormattingService.ts for better output readability.
- Adjusted SearchManager.ts to improve result headers and removed unnecessary search tips from combined text.

* Refactor FormattingService and SearchManager for table-based output

- Updated FormattingService to format search results as tables, including methods for formatting observations, sessions, and user prompts.
- Removed JSON format handling from SearchManager and streamlined result formatting to consistently use table format.
- Enhanced readability and consistency in search tips and formatting logic.
- Introduced token estimation for observations and improved time formatting.

* refactor: update documentation and API references for version bump and search functionalities

* Refactor code structure for improved readability and maintainability

* chore: change default model from haiku to sonnet

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

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

* feat: unify timeline formatting across search and context services

Extract shared timeline formatting utilities into reusable module to align
MCP search output format with context-generator's date/file-grouped format.

Changes:
- Create src/shared/timeline-formatting.ts with reusable utilities
  (parseJsonArray, formatDateTime, formatTime, formatDate, toRelativePath,
  extractFirstFile, groupByDate)
- Refactor context-generator.ts to use shared utilities
- Update SearchManager.search() to use date/file grouping
- Add search-specific row formatters to FormattingService
- Fix timeline methods to extract actual file paths from metadata
  instead of hardcoding 'General'
- Remove Work column from search output (kept in context output)

Result: Consistent date/file-grouped markdown formatting across both
systems while maintaining their different column requirements.

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

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

* refactor: remove redundant legend from search output

Remove legend from search/timeline results since it's already shown
in SessionStart context. Saves ~30 tokens per search result.

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

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

* Refactor session summary rendering to remove links

- Removed link generation for session summaries in context generation and search manager.
- Updated output formatting to exclude links while maintaining the session summary structure.
- Adjusted related components in TimelineService to ensure consistency across the application.

* fix: move skillPath declaration outside try block to fix scoping bug

The skillPath variable was declared inside the try block but referenced
in the catch block for error logging. Since const is block-scoped, this
would cause a ReferenceError when the error handler executes.

Moved skillPath declaration before the try block so it's accessible in
both try and catch scopes.

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

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

* fix: address PR #317 code review feedback

**Critical Fixes:**
- Replace happy_path_error__with_fallback debug calls with proper logger methods in mcp-server.ts
- All HTTP API calls now use logger.debug/error for consistent logging

**Code Quality Improvements:**
- Extract 90-day recency window magic numbers to named constants
- Added RECENCY_WINDOW_DAYS and RECENCY_WINDOW_MS constants in SearchManager

**Documentation:**
- Document model cost implications of Haiku → Sonnet upgrade in CHANGELOG
- Provide clear migration path for users who want to revert to Haiku

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

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

* refactor: simplify CHANGELOG - remove cost documentation

Removed model cost comparison documentation per user feedback.
Kept only the technical code quality improvements.

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

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

---------

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Alex Newman <thedotmack@users.noreply.github.com>
2025-12-14 21:58:11 -05:00

960 lines
29 KiB
Plaintext

---
title: "Hook Lifecycle"
description: "Complete guide to the 5-stage memory agent lifecycle for platform implementers"
---
# Hook Lifecycle
Claude-Mem implements a **5-stage hook system** that captures development work across Claude Code sessions. This document provides a complete technical reference for developers implementing this pattern on other platforms.
## Architecture Overview
### System Architecture
This two-process architecture works in both Claude Code and VS Code:
```mermaid
graph TB
subgraph EXT["Extension Process (runs in IDE)"]
direction TB
ACT[Extension Activation]
HOOKS[Hook Event Handlers]
ACT --> HOOKS
subgraph HOOK_HANDLERS["5 Lifecycle Hooks"]
H1[SessionStart<br/>activate function]
H2[UserPromptSubmit<br/>command handler]
H3[PostToolUse<br/>middleware]
H4[Stop<br/>idle timeout]
H5[SessionEnd<br/>deactivate function]
end
HOOKS --> HOOK_HANDLERS
end
HOOK_HANDLERS -->|"HTTP<br/>(fire-and-forget<br/>2s timeout)"| HTTP[Worker HTTP API<br/>Port 37777]
subgraph WORKER["Worker Process (separate Node.js)"]
direction TB
HTTP --> API[Express Server]
API --> SESS[Session Manager]
API --> AGENT[SDK Agent]
API --> DB[Database Manager]
AGENT -->|Event-Driven| CLAUDE[Claude Agent SDK]
CLAUDE --> SQLITE[(SQLite + FTS5)]
CLAUDE --> CHROMA[(Chroma Vectors)]
end
style EXT fill:#e1f5ff
style WORKER fill:#fff4e1
style HOOK_HANDLERS fill:#f0f0f0
```
**Key Principles:**
- Extension process never blocks (fire-and-forget HTTP)
- Worker processes observations asynchronously
- Session state persists across IDE restarts
### VS Code Extension API Integration Points
For developers porting to VS Code, here's where to hook into the VS Code Extension API:
```mermaid
graph LR
subgraph VSCODE["VS Code Extension API"]
direction TB
A["activate(context)"]
B["commands.registerCommand()"]
C["chat.createChatParticipant()"]
D["workspace.onDidSaveTextDocument()"]
E["window.onDidChangeActiveTextEditor()"]
F["deactivate()"]
end
subgraph HOOKS["Hook Equivalents"]
direction TB
G[SessionStart]
H[UserPromptSubmit]
I[PostToolUse]
J[Stop/Summary]
K[SessionEnd]
end
subgraph WORKER_API["Worker HTTP Endpoints"]
direction TB
L[GET /api/context/inject]
M[POST /sessions/init]
N[POST /sessions/observations]
O[POST /sessions/summarize]
P[POST /sessions/complete]
end
A --> G
B --> H
C --> H
D --> I
E --> I
F --> K
G --> L
H --> M
I --> N
J --> O
K --> P
style VSCODE fill:#007acc,color:#fff
style HOOKS fill:#f0f0f0
style WORKER_API fill:#4caf50,color:#fff
```
**Implementation Examples:**
```typescript
// VS Code Extension - SessionStart Hook
export async function activate(context: vscode.ExtensionContext) {
const sessionId = generateSessionId()
const project = workspace.name || 'default'
// Fetch context from worker
const response = await fetch(`http://localhost:37777/api/context/inject?project=${project}`)
const context = await response.text()
// Inject into chat or UI panel
injectContextToChat(context)
}
// VS Code Extension - UserPromptSubmit Hook
const command = vscode.commands.registerCommand('extension.command', async (prompt) => {
await fetch('http://localhost:37777/sessions/init', {
method: 'POST',
body: JSON.stringify({ sessionId, project, userPrompt: prompt })
})
})
// VS Code Extension - PostToolUse Hook (middleware pattern)
workspace.onDidSaveTextDocument(async (document) => {
await fetch('http://localhost:37777/api/sessions/observations', {
method: 'POST',
body: JSON.stringify({
claudeSessionId: sessionId,
tool_name: 'FileSave',
tool_input: { path: document.uri.path },
tool_response: 'File saved successfully'
})
})
})
```
### Async Processing Pipeline
How observations flow from extension to database without blocking the IDE:
```mermaid
graph TB
A["Extension: Tool Use Event"] --> B{"Skip List?<br/>(TodoWrite, AskUserQuestion, etc.)"}
B -->|"Skip"| X["Discard"]
B -->|"Keep"| C["Strip Privacy Tags<br/>&lt;private&gt;...&lt;/private&gt;"]
C --> D["HTTP POST to Worker<br/>Port 37777"]
D --> E["2s timeout<br/>fire-and-forget"]
E --> F["Extension continues<br/>(non-blocking)"]
D -.Async Path.-> G["Worker: Queue Observation"]
G --> H["SDK Agent picks up<br/>(event-driven)"]
H --> I["Call Claude API<br/>(compress observation)"]
I --> J["Parse XML response"]
J --> K["Save to SQLite<br/>(sdk_sessions table)"]
K --> L["Sync to Chroma<br/>(vector embeddings)"]
style F fill:#90EE90,stroke:#2d6b2d,stroke-width:3px
style L fill:#87CEEB,stroke:#2d5f8d,stroke-width:3px
style E fill:#ffeb3b,stroke:#c6a700,stroke-width:2px
```
**Critical Pattern:** The extension's HTTP call has a 2-second timeout and doesn't wait for AI processing. The worker handles compression asynchronously using an event-driven queue.
## The 5 Lifecycle Stages
| Stage | Hook | Trigger | Purpose |
|-------|------|---------|---------|
| **1. SessionStart** | `context-hook.js` + `user-message-hook.js` | User opens Claude Code | Inject prior context, show UI messages |
| **2. UserPromptSubmit** | `new-hook.js` | User submits a prompt | Create/get session, save prompt, init worker |
| **3. PostToolUse** | `save-hook.js` | Claude uses any tool | Queue observation for AI compression |
| **4. Stop** | `summary-hook.js` | User stops asking questions | Generate session summary |
| **5. SessionEnd** | `cleanup-hook.js` | Session closes | Mark session completed |
## Hook Configuration
Hooks are configured in `plugin/hooks/hooks.json`:
```json
{
"hooks": {
"SessionStart": [{
"matcher": "startup|clear|compact",
"hooks": [{
"type": "command",
"command": "node ${CLAUDE_PLUGIN_ROOT}/scripts/context-hook.js",
"timeout": 300
}, {
"type": "command",
"command": "node ${CLAUDE_PLUGIN_ROOT}/scripts/user-message-hook.js",
"timeout": 10
}]
}],
"UserPromptSubmit": [{
"hooks": [{
"type": "command",
"command": "node ${CLAUDE_PLUGIN_ROOT}/scripts/new-hook.js",
"timeout": 120
}]
}],
"PostToolUse": [{
"matcher": "*",
"hooks": [{
"type": "command",
"command": "node ${CLAUDE_PLUGIN_ROOT}/scripts/save-hook.js",
"timeout": 120
}]
}],
"Stop": [{
"hooks": [{
"type": "command",
"command": "node ${CLAUDE_PLUGIN_ROOT}/scripts/summary-hook.js",
"timeout": 120
}]
}],
"SessionEnd": [{
"hooks": [{
"type": "command",
"command": "node ${CLAUDE_PLUGIN_ROOT}/scripts/cleanup-hook.js",
"timeout": 120
}]
}]
}
}
```
---
## Stage 1: SessionStart
**Timing**: When user opens Claude Code or resumes session
**Hooks Triggered** (in order):
1. `context-hook.js` - Fetches and injects prior session context
2. `user-message-hook.js` - Displays context info to user via stderr
### Sequence Diagram
```mermaid
sequenceDiagram
participant User
participant IDE as IDE/Extension
participant ContextHook as context-hook.js
participant Worker as Worker Service
participant DB as SQLite Database
User->>IDE: Opens workspace / resumes session
IDE->>ContextHook: Trigger SessionStart hook
ContextHook->>ContextHook: Generate/reuse session_id
ContextHook->>Worker: Health check (max 10s retry)
alt Worker Ready
ContextHook->>Worker: GET /api/context/inject?project=X
Worker->>DB: SELECT * FROM observations<br/>WHERE project=X<br/>ORDER BY created_at DESC<br/>LIMIT 50
DB-->>Worker: Last 50 observations
Worker-->>ContextHook: Context markdown
ContextHook-->>IDE: hookSpecificOutput.additionalContext
IDE->>IDE: Inject context to Claude's prompt
IDE-->>User: Session ready with context
else Worker Not Ready
ContextHook-->>IDE: Empty context (graceful degradation)
IDE-->>User: Session ready without context
end
Note over User,DB: Total time: <300ms (with health check)
```
### Context Hook (`context-hook.js`)
**Purpose**: Inject context from previous sessions into Claude's initial context.
**Input** (via stdin):
```json
{
"session_id": "claude-session-123",
"cwd": "/path/to/project",
"source": "startup"
}
```
**Processing**:
1. Wait for worker to be available (health check, max 10 seconds)
2. Call: `GET http://127.0.0.1:37777/api/context/inject?project={project}`
3. Return formatted context as `additionalContext` in `hookSpecificOutput`
**Output** (via stdout):
```json
{
"hookSpecificOutput": {
"hookEventName": "SessionStart",
"additionalContext": "<<formatted context markdown>>"
}
}
```
**Implementation**: `src/hooks/context-hook.ts`
### User Message Hook (`user-message-hook.js`)
**Purpose**: Display helpful user messages during first-time setup or when viewing context.
**Behavior**:
- Shows first-time setup message when `node_modules` is missing
- Displays formatted context information with colors
- Provides tips for using claude-mem effectively
- Shows link to viewer UI (`http://localhost:37777`)
- Uses stderr as communication channel (only output available in Claude Code UI)
**Implementation**: `src/hooks/user-message-hook.ts`
---
## Stage 2: UserPromptSubmit
**Timing**: When user submits any prompt in a session
**Hook**: `new-hook.js`
### Sequence Diagram
```mermaid
sequenceDiagram
participant User
participant IDE as IDE/Extension
participant NewHook as new-hook.js
participant DB as Direct SQLite Access
participant Worker as Worker Service
User->>IDE: Submits prompt: "Add login feature"
IDE->>NewHook: Trigger UserPromptSubmit<br/>{ session_id, cwd, prompt }
NewHook->>NewHook: Extract project = basename(cwd)
NewHook->>NewHook: Strip privacy tags<br/>&lt;private&gt;...&lt;/private&gt;
alt Prompt fully private (empty after stripping)
NewHook-->>IDE: Skip (don't save)
else Prompt has content
NewHook->>DB: INSERT OR IGNORE INTO sdk_sessions<br/>(claude_session_id, project, first_user_prompt)
DB-->>NewHook: sessionDbId (new or existing)
NewHook->>DB: UPDATE sdk_sessions<br/>SET prompt_counter = prompt_counter + 1<br/>WHERE id = sessionDbId
DB-->>NewHook: promptNumber (e.g., 1 for first, 2 for continuation)
NewHook->>DB: INSERT INTO user_prompts<br/>(session_id, prompt_number, prompt)
NewHook->>Worker: POST /sessions/{sessionDbId}/init<br/>{ project, userPrompt, promptNumber }<br/>(fire-and-forget, 2s timeout)
Worker-->>NewHook: 200 OK (or timeout)
NewHook-->>IDE: { continue: true, suppressOutput: true }
IDE-->>User: Prompt accepted
end
Note over NewHook,DB: Idempotent: Same session_id → same sessionDbId
```
**Key Pattern:** The `INSERT OR IGNORE` ensures the same `session_id` always maps to the same `sessionDbId`, enabling conversation continuations.
**Input** (via stdin):
```json
{
"session_id": "claude-session-123",
"cwd": "/path/to/project",
"prompt": "User's actual prompt text"
}
```
**Processing Steps**:
```typescript
// 1. Extract project name from working directory
project = path.basename(cwd)
// 2. Create or get database session (IDEMPOTENT)
sessionDbId = db.createSDKSession(session_id, project, prompt)
// INSERT OR IGNORE: Creates new row if first prompt, returns existing if continuation
// 3. Increment prompt counter
promptNumber = db.incrementPromptCounter(sessionDbId)
// Returns 1 for first prompt, 2 for continuation, etc.
// 4. Strip privacy tags
cleanedPrompt = stripMemoryTagsFromPrompt(prompt)
// Removes <private>...</private> and <claude-mem-context>...</claude-mem-context>
// 5. Skip if fully private
if (!cleanedPrompt || cleanedPrompt.trim() === '') {
return // Don't save, don't call worker
}
// 6. Save user prompt to database
db.saveUserPrompt(session_id, promptNumber, cleanedPrompt)
// 7. Initialize session via worker HTTP
POST http://127.0.0.1:37777/sessions/{sessionDbId}/init
Body: { project, userPrompt, promptNumber }
```
**Output**:
```json
{ "continue": true, "suppressOutput": true }
```
**Implementation**: `src/hooks/new-hook.ts`
<Note>
The same `session_id` flows through ALL hooks in a conversation. The `createSDKSession` call is idempotent - it returns the existing session for continuation prompts.
</Note>
---
## Stage 3: PostToolUse
**Timing**: After Claude uses any tool (Read, Bash, Grep, Write, etc.)
**Hook**: `save-hook.js`
### Sequence Diagram
```mermaid
sequenceDiagram
participant Claude as Claude AI
participant IDE as IDE/Extension
participant SaveHook as save-hook.js
participant Worker as Worker Service
participant Agent as SDK Agent
participant DB as SQLite + Chroma
Claude->>IDE: Uses tool: Read("/src/auth.ts")
IDE->>SaveHook: PostToolUse hook triggered<br/>{ session_id, tool_name, tool_input, tool_response }
SaveHook->>SaveHook: Check skip list<br/>(TodoWrite, AskUserQuestion, etc.)
alt Tool in skip list
SaveHook-->>IDE: Discard (low-value tool)
else Tool allowed
SaveHook->>SaveHook: Strip privacy tags from input/response
SaveHook->>SaveHook: Ensure worker running<br/>(health check)
SaveHook->>Worker: POST /api/sessions/observations<br/>{ claudeSessionId, tool_name, tool_input, tool_response, cwd }<br/>(fire-and-forget, 2s timeout)
SaveHook-->>IDE: { continue: true, suppressOutput: true }
IDE-->>Claude: Tool execution complete
Note over Worker,DB: Async path (doesn't block IDE)
Worker->>Worker: createSDKSession(claudeSessionId)<br/>→ returns sessionDbId
Worker->>Worker: Check if prompt was private<br/>(skip if fully private)
Worker->>Agent: Queue observation for processing
Agent->>Agent: Call Claude SDK to compress<br/>observation into structured format
Agent->>DB: Save compressed observation<br/>to sdk_sessions table
Agent->>DB: Sync to Chroma vector DB
end
Note over SaveHook,DB: Total sync time: ~2ms<br/>AI processing: 1-3s (async)
```
**Key Pattern:** The hook returns immediately after HTTP POST. AI compression happens asynchronously in the worker without blocking Claude's tool execution.
**Input** (via stdin):
```json
{
"session_id": "claude-session-123",
"cwd": "/path/to/project",
"tool_name": "Read",
"tool_input": { "file_path": "/src/index.ts" },
"tool_response": "file contents..."
}
```
**Processing Steps**:
```typescript
// 1. Check blocklist - skip low-value tools
const SKIP_TOOLS = {
'ListMcpResourcesTool', // MCP infrastructure noise
'SlashCommand', // Command invocation
'Skill', // Skill invocation
'TodoWrite', // Task management meta-tool
'AskUserQuestion' // User interaction
}
if (SKIP_TOOLS[tool_name]) return
// 2. Ensure worker is running
await ensureWorkerRunning()
// 3. Send to worker (fire-and-forget HTTP)
POST http://127.0.0.1:37777/api/sessions/observations
Body: {
claudeSessionId: session_id,
tool_name,
tool_input,
tool_response,
cwd
}
Timeout: 2000ms
```
**Worker Processing**:
1. Looks up or creates session: `createSDKSession(claudeSessionId, '', '')`
2. Gets prompt counter
3. Checks privacy (skips if user prompt was entirely private)
4. Strips memory tags from `tool_input` and `tool_response`
5. Queues observation for SDK agent processing
6. SDK agent calls Claude to compress into structured observation
7. Stores observation in database and syncs to Chroma
**Output**:
```json
{ "continue": true, "suppressOutput": true }
```
**Implementation**: `src/hooks/save-hook.ts`
---
## Stage 4: Stop
**Timing**: When user stops or pauses asking questions
**Hook**: `summary-hook.js`
### Sequence Diagram
```mermaid
sequenceDiagram
participant User
participant IDE as IDE/Extension
participant SummaryHook as summary-hook.js
participant Worker as Worker Service
participant Agent as SDK Agent
participant DB as SQLite Database
User->>IDE: Stops asking questions<br/>(pause, idle, or explicit stop)
IDE->>SummaryHook: Stop hook triggered<br/>{ session_id, cwd, transcript_path }
SummaryHook->>SummaryHook: Read transcript JSONL file
SummaryHook->>SummaryHook: Extract last user message<br/>(type: "user")
SummaryHook->>SummaryHook: Extract last assistant message<br/>(type: "assistant", filter &lt;system-reminder&gt;)
SummaryHook->>Worker: POST /api/sessions/summarize<br/>{ claudeSessionId, last_user_message, last_assistant_message }<br/>(fire-and-forget, 2s timeout)
SummaryHook->>Worker: POST /api/processing<br/>{ isProcessing: false }<br/>(stop spinner)
SummaryHook-->>IDE: { continue: true, suppressOutput: true }
IDE-->>User: Session paused/stopped
Note over Worker,DB: Async path
Worker->>Worker: Lookup sessionDbId from claudeSessionId
Worker->>Agent: Queue summarization request
Agent->>Agent: Call Claude SDK with prompt:<br/>"Summarize: request, investigated, learned, completed, next_steps"
Agent->>Agent: Parse XML response
Agent->>DB: INSERT INTO session_summaries<br/>{ session_id, request, investigated, learned, completed, next_steps }
Agent->>DB: Sync to Chroma (for semantic search)
Note over SummaryHook,DB: Total sync time: ~2ms<br/>AI summarization: 2-5s (async)
```
**Key Pattern:** The summary is generated asynchronously and doesn't block the user from resuming work or closing the session.
**Input** (via stdin):
```json
{
"session_id": "claude-session-123",
"cwd": "/path/to/project",
"transcript_path": "/path/to/transcript.jsonl"
}
```
**Processing Steps**:
```typescript
// 1. Extract last messages from transcript JSONL
const lines = fs.readFileSync(transcript_path, 'utf-8').split('\n')
// Find last user message (type: "user")
// Find last assistant message (type: "assistant", filter <system-reminder> tags)
// 2. Ensure worker is running
await ensureWorkerRunning()
// 3. Send summarization request (fire-and-forget HTTP)
POST http://127.0.0.1:37777/api/sessions/summarize
Body: {
claudeSessionId: session_id,
last_user_message: string,
last_assistant_message: string
}
Timeout: 2000ms
// 4. Stop processing spinner
POST http://127.0.0.1:37777/api/processing
Body: { isProcessing: false }
```
**Worker Processing**:
1. Queues summarization for SDK agent
2. Agent calls Claude to generate structured summary
3. Summary stored in database with fields: `request`, `investigated`, `learned`, `completed`, `next_steps`
**Output**:
```json
{ "continue": true, "suppressOutput": true }
```
**Implementation**: `src/hooks/summary-hook.ts`
---
## Stage 5: SessionEnd
**Timing**: When Claude Code session closes (exit, clear, logout, etc.)
**Hook**: `cleanup-hook.js`
### Sequence Diagram
```mermaid
sequenceDiagram
participant User
participant IDE as IDE/Extension
participant CleanupHook as cleanup-hook.js
participant Worker as Worker Service
participant DB as SQLite Database
participant SSE as SSE Clients (Viewer UI)
User->>IDE: Closes session<br/>(exit, clear, logout)
IDE->>CleanupHook: SessionEnd hook triggered<br/>{ session_id, cwd, transcript_path, reason }
CleanupHook->>Worker: POST /api/sessions/complete<br/>{ claudeSessionId, reason }<br/>(fire-and-forget, 2s timeout)
CleanupHook-->>IDE: { continue: true, suppressOutput: true }
IDE-->>User: Session closed
Note over Worker,SSE: Async path
Worker->>Worker: Lookup sessionDbId from claudeSessionId
Worker->>DB: UPDATE sdk_sessions<br/>SET status = 'completed', completed_at = NOW()<br/>WHERE claude_session_id = claudeSessionId
Worker->>SSE: Broadcast session completion event<br/>(for live viewer UI updates)
SSE-->>SSE: Update UI to show session as completed
Note over CleanupHook,SSE: Total sync time: ~2ms
```
**Key Pattern:** Session completion is tracked for analytics and UI updates, but doesn't prevent the user from closing the IDE.
**Input** (via stdin):
```json
{
"session_id": "claude-session-123",
"cwd": "/path/to/project",
"transcript_path": "/path/to/transcript.jsonl",
"reason": "exit"
}
```
**Processing Steps**:
```typescript
// Send session complete (fire-and-forget HTTP)
POST http://127.0.0.1:37777/api/sessions/complete
Body: {
claudeSessionId: session_id,
reason: string // 'exit' | 'clear' | 'logout' | 'prompt_input_exit' | 'other'
}
Timeout: 2000ms
```
**Worker Processing**:
1. Finds session by `claudeSessionId`
2. Marks session as 'completed' in database
3. Broadcasts session completion event to SSE clients
**Output**:
```json
{ "continue": true, "suppressOutput": true }
```
**Implementation**: `src/hooks/cleanup-hook.ts`
---
## Session State Machine
Understanding session lifecycle and state transitions:
```mermaid
stateDiagram-v2
[*] --> Initialized: SessionStart hook<br/>(generate session_id)
Initialized --> Active: UserPromptSubmit<br/>(first prompt)
Active --> Active: UserPromptSubmit<br/>(continuation prompts)<br/>promptNumber++
Active --> ObservationQueued: PostToolUse hook<br/>(tool execution captured)
ObservationQueued --> Active: Observation processed<br/>(async, non-blocking)
Active --> Summarizing: Stop hook<br/>(user pauses/stops)
Summarizing --> Active: User resumes<br/>(new prompt submitted)
Summarizing --> Completed: SessionEnd hook<br/>(session closes)
Active --> Completed: SessionEnd hook<br/>(session closes)
Completed --> [*]
note right of Active
session_id: constant (e.g., "claude-session-abc123")
sessionDbId: constant (e.g., 42)
promptNumber: increments (1, 2, 3, ...)
All operations use same sessionDbId
end note
note right of ObservationQueued
Fire-and-forget HTTP
AI compression happens async
IDE never blocks
end note
```
**Key Insights:**
- `session_id` never changes during a conversation
- `sessionDbId` is the database primary key for the session
- `promptNumber` increments with each user prompt
- State transitions are non-blocking (fire-and-forget pattern)
---
## Database Schema
The session-centric data model that enables cross-session memory:
```mermaid
erDiagram
SDK_SESSIONS ||--o{ USER_PROMPTS : "has many"
SDK_SESSIONS ||--o{ OBSERVATIONS : "has many"
SDK_SESSIONS ||--o{ SESSION_SUMMARIES : "has many"
SDK_SESSIONS {
integer id PK "Auto-increment primary key"
text claude_session_id UK "From IDE (e.g., 'claude-session-123')"
text project "Project name from cwd basename"
text first_user_prompt "Initial prompt that started session"
integer prompt_counter "Increments with each UserPromptSubmit"
text status "initialized | active | completed"
datetime created_at
datetime completed_at
}
USER_PROMPTS {
integer id PK
integer session_id FK "References SDK_SESSIONS.id"
integer prompt_number "1, 2, 3, ... matches prompt_counter"
text prompt "User's actual prompt (tags stripped)"
datetime created_at
}
OBSERVATIONS {
integer id PK
integer session_id FK "References SDK_SESSIONS.id"
integer prompt_number "Which prompt this observation belongs to"
text tool_name "Read, Bash, Grep, Write, etc."
text tool_input_json "Stripped of privacy tags"
text tool_response_text "Stripped of privacy tags"
text compressed_observation "AI-generated structured observation"
datetime created_at
}
SESSION_SUMMARIES {
integer id PK
integer session_id FK "References SDK_SESSIONS.id"
text request "What user requested"
text investigated "What was explored"
text learned "What was discovered"
text completed "What was accomplished"
text next_steps "What remains to be done"
datetime created_at
}
```
**Idempotency Pattern:**
```sql
-- This ensures same session_id always maps to same sessionDbId
INSERT OR IGNORE INTO sdk_sessions (claude_session_id, project, first_user_prompt)
VALUES (?, ?, ?)
RETURNING id;
-- If already exists, returns existing row
-- If new, creates and returns new row
```
**Foreign Key Cascade:**
All child tables (user_prompts, observations, session_summaries) use `session_id` foreign key referencing `SDK_SESSIONS.id`. This ensures:
- All data for a session is queryable by sessionDbId
- Session deletions cascade to child tables
- Efficient joins for context injection
<Warning>
Never generate your own session IDs. Always use the `session_id` provided by the IDE - this is the source of truth for linking all data together.
</Warning>
---
## Privacy & Tag Stripping
### Dual-Tag System
```typescript
// User-Level Privacy Control (manual)
<private>sensitive data</private>
// System-Level Recursion Prevention (auto-injected)
<claude-mem-context>...</claude-mem-context>
```
### Processing Pipeline
**Location**: `src/utils/tag-stripping.ts`
```typescript
// Called by: new-hook.js (user prompts)
stripMemoryTagsFromPrompt(prompt: string): string
// Called by: save-hook.js (tool_input, tool_response)
stripMemoryTagsFromJson(jsonString: string): string
```
**Execution Order** (Edge Processing):
1. `new-hook.js` strips tags from user prompt before saving
2. `save-hook.js` strips tags from tool data before sending to worker
3. Worker strips tags again (defense in depth) before storing
---
## SDK Agent Processing
### Query Loop (Event-Driven)
**Location**: `src/services/worker/SDKAgent.ts`
```typescript
async startSession(session: ActiveSession, worker?: any) {
// 1. Create event-driven message generator
const messageGenerator = this.createMessageGenerator(session)
// 2. Run Agent SDK query loop
const queryResult = query({
prompt: messageGenerator,
options: {
model: 'claude-sonnet-4-5',
disallowedTools: ['Bash', 'Read', 'Write', ...], // Observer-only
abortController: session.abortController
}
})
// 3. Process responses
for await (const message of queryResult) {
if (message.type === 'assistant') {
await this.processSDKResponse(session, text, worker)
}
}
}
```
### Message Types
The message generator yields three types of prompts:
1. **Initial Prompt** (prompt #1): Full instructions for starting observation
2. **Continuation Prompt** (prompt #2+): Context-only for continuing work
3. **Observation Prompts**: Tool use data to compress into observations
4. **Summary Prompts**: Session data to summarize
---
## Implementation Checklist
For developers implementing this pattern on other platforms:
### Hook Registration
- [ ] Define hook entry points in platform config
- [ ] 5 hook types: SessionStart (2 hooks), UserPromptSubmit, PostToolUse, Stop, SessionEnd
- [ ] Pass `session_id`, `cwd`, and context-specific data
### Database Schema
- [ ] SQLite with WAL mode
- [ ] 4 main tables: `sdk_sessions`, `user_prompts`, `observations`, `session_summaries`
- [ ] Indices for common queries
### Worker Service
- [ ] HTTP server on configurable port (default 37777)
- [ ] Bun runtime for process management
- [ ] 3 core services: SessionManager, SDKAgent, DatabaseManager
### Hook Implementation
- [ ] context-hook: `GET /api/context/inject` (with health check)
- [ ] new-hook: createSDKSession, saveUserPrompt, `POST /sessions/{id}/init`
- [ ] save-hook: Skip low-value tools, `POST /api/sessions/observations`
- [ ] summary-hook: Parse transcript, `POST /api/sessions/summarize`
- [ ] cleanup-hook: `POST /api/sessions/complete`
### Privacy & Tags
- [ ] Implement `stripMemoryTagsFromPrompt()` and `stripMemoryTagsFromJson()`
- [ ] Process tags at hook layer (edge processing)
- [ ] Max tag count = 100 (ReDoS protection)
### SDK Integration
- [ ] Call Claude Agent SDK to process observations/summaries
- [ ] Parse XML responses for structured data
- [ ] Store to database + sync to vector DB
---
## Key Design Principles
1. **Session ID is Source of Truth**: Never generate your own session IDs
2. **Idempotent Database Operations**: Use `INSERT OR IGNORE` for session creation
3. **Edge Processing for Privacy**: Strip tags at hook layer before data reaches worker
4. **Fire-and-Forget for Non-Blocking**: HTTP timeouts prevent IDE blocking
5. **Event-Driven, Not Polling**: Zero-latency queue notification to SDK agent
6. **Everything Saves Always**: No "orphaned" sessions
---
## Common Pitfalls
| Problem | Root Cause | Solution |
|---------|-----------|----------|
| Session ID mismatch | Different `session_id` used in different hooks | Always use ID from hook input |
| Duplicate sessions | Creating new session instead of using existing | Use `INSERT OR IGNORE` with `session_id` as key |
| Blocking IDE | Waiting for full response | Use fire-and-forget with short timeouts |
| Memory tags in DB | Stripping tags in wrong layer | Strip at hook layer, before HTTP send |
| Worker not found | Health check too fast | Add retry loop with exponential backoff |
---
## Related Documentation
- [Worker Service](/architecture/worker-service) - HTTP API and async processing
- [Database Schema](/architecture/database) - SQLite tables and FTS5 search
- [Privacy Tags](/usage/private-tags) - Using `<private>` tags
- [Troubleshooting](/troubleshooting) - Common hook issues