Hook 生命周期¶
Claude-Mem 实现了一套 5 阶段 Hook 系统,用于捕获跨 Claude Code 会话的开发工作。本文档为在其他平台上实现此模式的开发者提供了完整的技术参考。
架构概览¶
系统架构¶
这种双进程架构在 Claude Code 和 VS Code 中均可工作:
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/>Configured port]
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
关键原则: - 扩展进程永不阻塞(fire-and-forget HTTP) - Worker 异步处理观察数据 - 会话状态在 IDE 重启后依然持久化
VS Code 扩展 API 集成点¶
对于移植到 VS Code 的开发者,以下是在 VS Code 扩展 API 中接入 Hook 的位置:
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
实现示例:
const workerPort = process.env.CLAUDE_MEM_WORKER_PORT ?? "<worker-port>"
const workerBaseUrl = `http://127.0.0.1:${workerPort}`
// 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(`${workerBaseUrl}/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(`${workerBaseUrl}/sessions/init`, {
method: 'POST',
body: JSON.stringify({ sessionId, project, userPrompt: prompt })
})
})
// VS Code Extension - PostToolUse Hook (middleware pattern)
workspace.onDidSaveTextDocument(async (document) => {
await fetch(`${workerBaseUrl}/api/sessions/observations`, {
method: 'POST',
body: JSON.stringify({
claudeSessionId: sessionId,
tool_name: 'FileSave',
tool_input: { path: document.uri.path },
tool_response: 'File saved successfully'
})
})
})
异步处理管道¶
观察数据如何在不阻塞 IDE 的情况下从扩展流向数据库:
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/><private>...</private>"]
C --> D["HTTP POST to Worker<br/>Configured port"]
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
关键模式: 扩展的 HTTP 调用具有 2 秒超时,且不会等待 AI 处理。Worker 使用事件驱动的队列异步执行压缩。
5 个生命周期阶段¶
| 阶段 | Hook | 触发时机 | 目的 |
|---|---|---|---|
| 1. SessionStart | claude-mem hook claude-code context |
用户打开 Claude Code | 静默注入先前上下文 |
| 2. UserPromptSubmit | claude-mem hook claude-code session-init |
用户提交 Prompt | 创建/获取会话、保存 Prompt、初始化 worker |
| 3. PostToolUse | claude-mem hook claude-code observation |
Claude 使用任意工具 | 将观察数据加入队列以供 AI 压缩 |
| 4. Stop | claude-mem hook claude-code summarize |
用户停止提问 | 生成会话摘要 |
| 5. SessionEnd | claude-mem hook claude-code session-end |
会话关闭 | 将会话标记为已完成 |
PreToolUse(匹配器 Read 以及搜索工具)还会同步运行 claude-mem hook claude-code file-context,以便在 Claude 读取文件之前添加该文件的历史记录,并让 File Read Gate 阻止整文件的 Read 操作。PostToolUseFailure 对于失败的工具调用,会运行与 PostToolUse 相同的 observation Hook。
钩子配置¶
钩子在 plugin/hooks/hooks.json 中配置(由 scripts/build-hooks.js 生成)。每个运行时钩子都采用 exec 形式:command + args,没有 shell。Claude Code 会在 PATH 中解析 claude-mem 并直接启动它,中间不经过 bash:
{
"hooks": {
"Setup": [{
"matcher": "*",
"hooks": [{
"type": "command",
"shell": "bash",
"command": "… resolve the plugin root …; node \"$_P/scripts/version-check.js\"",
"timeout": 300
}]
}],
"SessionStart": [{
"matcher": "startup|resume|clear|compact",
"hooks": [
{ "type": "command", "command": "claude-mem", "args": ["start"], "timeout": 60, "async": true }
]
}, {
"matcher": "startup|clear|compact",
"hooks": [
{ "type": "command", "command": "claude-mem", "args": ["hook", "claude-code", "context"], "timeout": 60 }
]
}, {
"matcher": "startup",
"hooks": [
{ "type": "command", "command": "node", "args": ["${CLAUDE_PLUGIN_ROOT}/scripts/ensure-launcher.cjs"], "timeout": 180 }
]
}],
"UserPromptSubmit": [{
"hooks": [{ "type": "command", "command": "claude-mem", "args": ["hook", "claude-code", "session-init"], "timeout": 15 }]
}],
"PreToolUse": [{
"matcher": "Read|^Bash$|^Grep$|^Glob$|^memory_search$|^memory$|^mcp__.+__memory_search$",
"hooks": [{ "type": "command", "command": "claude-mem", "args": ["hook", "claude-code", "file-context"], "timeout": 15 }]
}],
"PostToolUse": [{
"matcher": "*",
"hooks": [{ "type": "command", "command": "claude-mem", "args": ["hook", "claude-code", "observation"], "timeout": 120, "async": true }]
}],
"PostToolUseFailure": [{
"matcher": "*",
"hooks": [{ "type": "command", "command": "claude-mem", "args": ["hook", "claude-code", "observation"], "timeout": 120, "async": true }]
}],
"Stop": [{
"hooks": [{ "type": "command", "command": "claude-mem", "args": ["hook", "claude-code", "summarize"], "timeout": 120, "async": true }]
}],
"SessionEnd": [{
"hooks": [{ "type": "command", "command": "claude-mem", "args": ["hook", "claude-code", "session-end"], "async": true }]
}]
}
}
claude-mem是一个版本稳定的编译启动器,不是 npm CLI。npx claude-mem install/update会将其放置在~/.local/bin(POSIX)或%LOCALAPPDATA%\claude-mem\bin(Windows),并将该目录添加到 PATH。它会找到插件根目录,在 Bun 下使用相同的参数运行worker-service.cjs,并在任何失败时以 0 退出。ensure-launcher.cjs(仅启动时执行)是针对仅通过插件市场安装的自我修复机制:当启动器缺失或过期时,它会放置或刷新启动器。- Setup 是唯一仍然使用 bash 的入口。Claude Code 仅在
claude --init、--init-only和-p --maintenance时触发它,而从不在插件安装、更新或正常会话启动时触发。
有关启动器的行为以及启动器缺失时你会看到的错误,请参阅 钩子架构。
阶段 1:SessionStart¶
时机:当用户打开 Claude Code 或恢复会话时
触发的钩子(按顺序):
1. claude-mem start - 启动 worker service(异步)
2. claude-mem hook claude-code context - 获取并静默注入先前会话的上下文
3. node ${CLAUDE_PLUGIN_ROOT}/scripts/ensure-launcher.cjs - 仅启动时执行,同步:将 claude-mem 启动器保留在 PATH 中,并在记忆钩子未激活时显示 systemMessage
(运行时设置由 npx claude-mem install / npx claude-mem repair 处理。Setup 钩子仅在 claude --init / --maintenance 时运行 version-check.js:当插件的依赖缺失时安装它们,安装完成后记录 .install-version 标记,并且仅在依赖仍然缺失时要求你运行 npx claude-mem@latest install。)
Note
从 Claude Code 2.1.0(ultrathink 更新)开始,SessionStart 钩子不再显示用户可见的消息。上下文会通过 hookSpecificOutput.additionalContext 静默注入。
序列图¶
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)
上下文钩子(claude-mem hook claude-code context){#context-hook-claude-mem-hook-claude-code-context}¶
用途:将先前会话的上下文注入 Claude 的初始上下文。
输入(通过 stdin):
处理过程:
1. 等待 worker 可用(健康检查,最多 10 秒)
2. 调用:GET http://127.0.0.1:<worker-port>/api/context/inject?project={project}
3. 将格式化上下文作为 hookSpecificOutput 中的 additionalContext 返回
输出(通过 stdout):
{
"hookSpecificOutput": {
"hookEventName": "SessionStart",
"additionalContext": "<<formatted context markdown>>"
}
}
实现:src/cli/handlers/context.ts
阶段 2:UserPromptSubmit¶
时机:当用户在会话中提交任何提示时
挂钩:claude-mem hook claude-code session-init
序列图¶
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/><private>...</private>
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
关键模式: INSERT OR IGNORE 确保相同的 session_id 始终映射到相同的 sessionDbId,从而实现对话的连续。
输入(通过 stdin):
{
"session_id": "claude-session-123",
"cwd": "/path/to/project",
"prompt": "User's actual prompt text"
}
处理步骤:
// 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 = stripMemoryTags(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:<worker-port>/sessions/{sessionDbId}/init
Body: { project, userPrompt, promptNumber }
输出:
实现:src/cli/handlers/session-init.ts
Note
相同的 session_id 会在对话中流经所有钩子。createSDKSession 调用是幂等的——它会返回用于后续提示的现有会话。
阶段 3:PostToolUse¶
时机:当 Claude 使用任何工具(Read、Bash、Grep、Write 等)之后
挂钩:claude-mem hook claude-code observation
序列图¶
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)
关键模式: 钩子在 HTTP POST 后立即返回。AI 压缩在 Worker 中异步完成,不会阻塞 Claude 的工具执行。
输入(通过 stdin):
{
"session_id": "claude-session-123",
"cwd": "/path/to/project",
"tool_name": "Read",
"tool_input": { "file_path": "/src/index.ts" },
"tool_response": "file contents..."
}
处理步骤:
// 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:<worker-port>/api/sessions/observations
Body: {
claudeSessionId: session_id,
tool_name,
tool_input,
tool_response,
cwd
}
Timeout: 2000ms
Worker 处理过程:
1. 查找或创建会话:createSDKSession(claudeSessionId, '', '')
2. 获取提示计数器
3. 检查隐私(如果用户提示完全私有则跳过)
4. 从 tool_input 和 tool_response 中剥离记忆标签
5. 将观察结果加入队列,交给 SDK Agent 处理
6. SDK Agent 调用 Claude 将观察结果压缩为结构化格式
7. 将压缩后的观察结果存储到数据库中,并同步到 Chroma
输出:
实现:src/cli/handlers/observation.ts
阶段 4:停止¶
时机:当用户停止或暂停提问时
Hook:claude-mem hook claude-code summarize
序列图¶
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 <system-reminder>)
SummaryHook->>Worker: POST /api/sessions/summarize<br/>{ claudeSessionId, last_user_message, last_assistant_message }<br/>(fire-and-forget, 2s timeout)
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)
关键模式: 摘要为异步生成,不会阻塞用户恢复工作或关闭会话。
输入(通过 stdin):
{
"session_id": "claude-session-123",
"cwd": "/path/to/project",
"transcript_path": "/path/to/transcript.jsonl"
}
处理步骤:
// 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:<worker-port>/api/sessions/summarize
Body: {
claudeSessionId: session_id,
last_user_message: string,
last_assistant_message: string
}
Timeout: 2000ms
Note
summary 钩子不会调用 POST /api/processing,该路由并不是加载动画开关。它是仅限 localhost 的运维重试机制。{ "isProcessing": false } 会为每个持有缓冲工作且没有正在运行的生成器的活动会话重启 observer。其中包括自动恢复扫描不会处理的情况,例如凭据被拒绝或传输失败。响应中的 scheduledSessions 字段报告重试了多少个会话。{ "isProcessing": true } 会被接受,但不会执行任何操作。
Worker 处理流程:
1. 将摘要任务加入 SDK Agent 的队列
2. Agent 调用 Claude 生成结构化摘要
3. 摘要存储到数据库中,字段包括:request、investigated、learned、completed、next_steps
输出:
实现:src/cli/handlers/summarize.ts
阶段 5:SessionEnd¶
时机:当 Claude Code 会话关闭时(exit、clear、logout 等)
Hook:claude-mem hook claude-code session-end
序列图¶
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
关键模式: 会话完成状态会被跟踪以用于分析和 UI 更新,但不会阻止用户关闭 IDE。
输入(通过 stdin):
{
"session_id": "claude-session-123",
"cwd": "/path/to/project",
"transcript_path": "/path/to/transcript.jsonl",
"reason": "exit"
}
处理步骤:
// Send session complete (fire-and-forget HTTP)
POST http://127.0.0.1:<worker-port>/api/sessions/complete
Body: {
claudeSessionId: session_id,
reason: string // 'exit' | 'clear' | 'logout' | 'prompt_input_exit' | 'other'
}
Timeout: 2000ms
Worker 处理流程:
1. 根据 claudeSessionId 查找会话
2. 在数据库中将会话标记为 'completed'
3. 向 SSE 客户端广播会话完成事件
输出:
实现:src/cli/handlers/session-end.ts
会话状态机¶
理解会话生命周期和状态转换:
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
关键要点:
- session_id 在对话过程中始终保持不变
- sessionDbId 是该会话的数据库主键
- promptNumber 随每次用户提示递增
- 状态转换为非阻塞式(即发即忘模式)
数据库架构¶
以会话为中心的数据模型,可实现跨会话记忆:
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
}
幂等性模式:
-- 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
外键级联:
所有子表(user_prompts、observations、session_summaries)都使用 session_id 外键引用 SDK_SESSIONS.id。这可以确保:
- 一个会话的所有数据均可通过 sessionDbId 查询
- 会话删除会级联到子表
- 为上下文注入提供高效的连接操作
Warning
绝不要自行生成会话 ID。务必使用 IDE 提供的 session_id——这是将所有数据关联在一起的唯一事实来源。
隐私与标签剥离¶
双标签系统¶
// User-Level Privacy Control (manual)
<private>sensitive data</private>
// System-Level Recursion Prevention (auto-injected)
<claude-mem-context>...</claude-mem-context>
处理流水线¶
位置:src/utils/tag-stripping.ts
// Called by: new-hook.js (user prompts) and save-hook.js (tool_input, tool_response)
stripMemoryTags(content: string): string
执行顺序(边缘处理):
1. new-hook.js 在保存前从用户提示中剥离标签
2. save-hook.js 在发送给 worker 前从工具数据中剥离标签
3. worker 在存储前再次剥离标签(纵深防御)
SDK Agent 处理¶
查询循环(事件驱动)¶
位置:src/services/worker/SDKAgent.ts
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-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)
}
}
}
消息类型¶
消息生成器会产生三种类型的提示:
- 初始提示(第 1 次提示):启动观察的完整指令
- 继续提示(第 2 次及之后):仅包含继续工作的上下文
- 观察提示:用于压缩为观察结果的工具使用数据
- 摘要提示:需要汇总的会话数据
实现清单¶
对于在其他平台上实现此模式的开发者:
Hook 注册¶
- [ ] 在平台配置中定义 Hook 入口点
- [ ] 5 种 Hook 类型:SessionStart(2 个 Hook)、UserPromptSubmit、PostToolUse、Stop、SessionEnd
- [ ] 当宿主支持时,优先使用 exec 形式的 Hook 条目(
command+args,不使用 shell) - [ ] 传入
session_id、cwd及上下文相关的数据
数据库架构¶
- [ ] 启用 WAL 模式的 SQLite
- [ ] 4 个主表:
sdk_sessions、user_prompts、observations、session_summaries - [ ] 为常见查询建立索引
Worker Service¶
- [ ] 在配置的 worker 端口上运行 HTTP 服务器
- [ ] 使用 Bun 运行时进行进程管理
- [ ] 3 个核心服务:SessionManager、SDKAgent、DatabaseManager
Hook 实现¶
- [ ] context-hook:
GET /api/context/inject(带健康检查) - [ ] new-hook:createSDKSession、saveUserPrompt、
POST /sessions/{id}/init - [ ] save-hook:跳过低价值工具,
POST /api/sessions/observations - [ ] summary-hook:解析转录文本,
POST /api/sessions/summarize - [ ] cleanup-hook:
POST /api/sessions/complete
隐私与标签¶
- [ ] 实现
stripMemoryTags() - [ ] 在 Hook 层处理标签(边缘处理)
- [ ] 最大标签数为 100(ReDoS 防护)
SDK 集成¶
- [ ] 调用 Claude Agent SDK 处理观察结果/摘要
- [ ] 解析 XML 响应以获取结构化数据
- [ ] 存储到数据库 + 同步到向量数据库
核心设计原则¶
- Session ID 是唯一真相来源:绝不自行生成 Session ID
- 幂等数据库操作:使用
INSERT OR IGNORE创建会话 - 边缘处理保障隐私:在 Hook 层、数据到达 Worker 之前移除标签
- Fire-and-Forget 非阻塞模式:HTTP 超时防止阻塞 IDE
- 事件驱动而非轮询:向 SDK Agent 发送零延迟队列通知
- 始终保存一切:不存在“孤儿”会话
常见陷阱¶
| 问题 | 根本原因 | 解决方案 |
|---|---|---|
| Session ID 不匹配 | 不同 Hook 中使用了不同的 session_id |
始终使用来自 Hook 输入的 ID |
| 重复会话 | 创建新会话而不是使用现有会话 | 使用 INSERT OR IGNORE,以 session_id 作为键 |
| 阻塞 IDE | 等待完整响应 | 使用短超时的 Fire-and-Forget |
| 数据库中存在 Memory 标签 | 在错误的层移除标签 | 在 Hook 层、发送 HTTP 之前移除标签 |
| 找不到 Worker | 健康检查过快 | 添加带指数退避的重试循环 |
相关文档¶
- Worker Service - HTTP API 与异步处理
- 数据库模式 - SQLite 数据表与 FTS5 搜索
- 隐私标签 - 使用
<private>标签 - 故障排除 - 常见 Hook 问题
本页原文 Markdown:在 AtomGit 查看·内容源自开源项目 cl/claude-mem