---
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 opens Claude Code | Inject prior context silently |
| **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": {
    "Setup": [{
      "hooks": [{
        "type": "command",
        "command": "node ${CLAUDE_PLUGIN_ROOT}/scripts/version-check.js",
        "timeout": 60
      }]
    }],
    "SessionStart": [{
      "matcher": "startup|clear|compact",
      "hooks": [{
        "type": "command",
        "command": "bun ${CLAUDE_PLUGIN_ROOT}/scripts/worker-service.cjs start",
        "timeout": 60
      }, {
        "type": "command",
        "command": "bun ${CLAUDE_PLUGIN_ROOT}/scripts/context-hook.js",
        "timeout": 60
      }]
    }],
    "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. `worker-service.cjs start` - Starts the worker service
2. `context-hook.js` - Fetches and silently injects prior session context

(Runtime setup is handled out-of-band by `npx claude-mem install` / `npx claude-mem repair`. The Setup phase runs a sub-100ms `version-check.js` that prompts the user to repair if the `.install-version` marker is stale.)

<Note>
As of Claude Code 2.1.0 (ultrathink update), SessionStart hooks no longer display user-visible messages. Context is silently injected via `hookSpecificOutput.additionalContext`.
</Note>

### 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`

---

## 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-6',
      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
