钩子生命周期
Claude-Mem 实现了一个 五阶段钩子系统,用于捕获 Claude Code 会话中的开发工作。本文档为在其他平台上实现此模式的开发者提供了完整的技术参考。
架构概述
系统架构
这种双进程架构在 Claude Code 和 VS Code 中都可以运行:
关键原则:
- 扩展过程永不阻塞(即发即忘的 HTTP)
- 工作进程异步处理观察
- 会话状态在 IDE 重启后仍然保留
VS Code 扩展 API 集成点
对于移植到 VS Code 的开发者,这里是接入 VS Code 扩展 API 的位置:
实施示例:
// VS Code Extension - SessionStart Hook
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'
})
})
})异步处理管道
观察如何从扩展流向数据库而不会阻塞 IDE:
关键模式: 扩展的 HTTP 调用有 2 秒的超时,并且不等待 AI 处理。Worker 进程使用基于事件的队列异步处理压缩。
生命周期的五个阶段
| 阶段 | 钩子 | 触发器 | 目的 |
|---|---|---|---|
| 1. 会话开始 | context-hook.js | 用户打开 Claude Code | 静默注入先前上下文 |
| 2. 用户提交提示 | new-hook.js | 用户提交提示 | 创建/获取会话,保存提示,初始化工作进程 |
| 3. 使用工具后 | save-hook.js | Claude 使用任何工具 | 排队观察以进行 AI 压缩 |
| 4. 停止 | summary-hook.js | 用户停止提问 | 生成会话摘要 |
| 5. 会话结束 | cleanup-hook.js | 会话关闭 | 标记会话已完成 |
钩子配置
钩子配置在 plugin/hooks/hooks.json:
{
"hooks": {
"SessionStart": [{
"matcher": "startup|clear|compact",
"hooks": [{
"type": "command",
"command": "node ${CLAUDE_PLUGIN_ROOT}/scripts/smart-install.js",
"timeout": 300
}, {
"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
}]
}]
}
}阶段 1:会话开始
时间:当用户打开 Claude Code 或恢复会话时
钩子触发(顺序):
smart-install.js- 确保已安装依赖项worker-service.cjs start- 启动 Worker 服务context-hook.js- 获取并静默注入先前会话上下文
从 Claude Code 2.1.0(ultrathink 更新)开始,SessionStart 钩子不再显示用户可见的消息。上下文通过 hookSpecificOutput.additionalContext 静默注入。
序列图
上下文钩子 (context-hook.js)
目的:将之前会话的上下文注入 Claude 的初始上下文中。
输入(通过标准输入):
{
"session_id": "claude-session-123",
"cwd": "/path/to/project",
"source": "startup"
}处理中:
- 等待Worker可用(健康检查,最多10秒)
- 呼叫:
GET http://127.0.0.1:37777/api/context/inject?project={project} - 将格式化的上下文作为
additionalContext返回到hookSpecificOutput
输出(通过标准输出):
{
"hookSpecificOutput": {
"hookEventName": "SessionStart",
"additionalContext": "<<formatted context markdown>>"
}
}实施: src/hooks/context-hook.ts
阶段 2:用户提示提交
时间:当用户在会话中提交任何提示时
钩子: new-hook.js
序列图
关键模式: INSERT OR IGNORE 确保相同的 session_id 始终映射到相同的 sessionDbId,从而实现对话的延续。
输入(通过标准输入):
{
"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 = 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 }输出:
{ "continue": true, "suppressOutput": true }实施: src/hooks/new-hook.ts
相同的 session_id 会流经对话中的所有钩子。createSDKSession 调用是幂等的——它会返回现有的会话以继续使用提示。
阶段 3:使用工具后
时机:在Claude使用任何工具(Read、Bash、Grep、Write等)之后
钩子: save-hook.js
序列图
关键模式: 钩子在 HTTP POST 后立即返回。AI 压缩在Worker 进程中异步进行,不会阻塞 Claude 工具的执行。
输入(通过标准输入):
{
"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:37777/api/sessions/observations
Body: {
claudeSessionId: session_id,
tool_name,
tool_input,
tool_response,
cwd
}
Timeout: 2000msWorker 处理:
- 查找或创建会话:
createSDKSession(claudeSessionId, '', '') - 获取提示计数器
- 检查隐私(如果用户提示完全私密则跳过)
- 从
tool_input和tool_response中删除内存标签 - 用于 SDK 代理处理的队列观察
- SDK 代理调用 Claude 将其压缩为结构化观察
- 将观察结果存储在数据库中并同步到 Chroma
输出:
{ "continue": true, "suppressOutput": true }实施: src/hooks/save-hook.ts
第四步:停止
时机:当用户停止或暂停提问时
钩子: summary-hook.js
序列图
关键模式: 摘要是异步生成的,不会阻止用户恢复工作或关闭会话。
输入(通过标准输入):
{
"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: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 处理:
- SDK 代理的队列汇总
- 代理调用 Claude 生成结构化摘要
- 摘要存储在数据库中,字段包括:
request、investigated、learned、completed、next_steps
输出:
{ "continue": true, "suppressOutput": true }实施: src/hooks/summary-hook.ts
阶段5:会议结束
时间:当 Claude Code 会话关闭时(退出、清除、注销等)
钩子: cleanup-hook.js
序列图
关键模式: 会话完成情况会被跟踪用于分析和界面更新,但不会阻止用户关闭 IDE。
输入(通过标准输入):
{
"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:37777/api/sessions/complete
Body: {
claudeSessionId: session_id,
reason: string // 'exit' | 'clear' | 'logout' | 'prompt_input_exit' | 'other'
}
Timeout: 2000ms工作处理:
- 通过
claudeSessionId查找会话 - 将会话在数据库中标记为“已完成”
- 将会话完成事件广播给 SSE 客户端
输出:
{ "continue": true, "suppressOutput": true }实施: src/hooks/cleanup-hook.ts
会话状态机
理解会话生命周期和状态转换:
关键见解:
session_id在对话中从不改变sessionDbId是会话的数据库主键promptNumber随着每次用户提示而增加- 状态转换是非阻塞的(即触发即忘模式)
数据库模式
支持跨会话记忆的以会话为中心的数据模型:
幂等模式:
-- 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 查询
- 会话删除会级联到子表
- 用于上下文注入的高效连接
绝不要自己生成会话 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)
stripMemoryTagsFromPrompt(prompt: string): string
// Called by: save-hook.js (tool_input, tool_response)
stripMemoryTagsFromJson(jsonString: string): string执行顺序(边缘处理):
new-hook.js在保存之前会从用户提示中去掉标签save-hook.js在将工具数据发送给 Worker 进程之前会去除标签- Worker 在存储前再次去掉标签(纵深防御)
SDK 代理处理
查询循环(事件驱动)
地点: 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-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)
}
}
}消息类型
消息生成器产生三种类型的提示:
- 初始提示(提示 #1):开始观察的完整指示
- 续写提示(提示 #2):仅用于继续工作的上下文
- 观察提示:使用工具数据压缩为观察结果
- 摘要提示:用于总结的会话数据
实施清单
对于在其他平台上实现此模式的开发者:
钩子注册
- [ ] 在平台配置中定义钩子入口点
- [ ] 5 种钩子类型:SessionStart(2 个钩子)、UserPromptSubmit、PostToolUse、Stop、SessionEnd
- [ ] 传递
session_id、cwd和特定于上下文的数据
数据库模式
- [ ] 使用 WAL 模式的 SQLite
- [ ] 4 个主要表:
sdk_sessions、user_prompts、observations、session_summaries - [ ] 常用查询的索引
Worker 服务
- [ ] 可配置端口的 HTTP 服务器(默认 37777)
- [ ] 用于进程管理的 Bun 运行时
- [ ] 三个核心服务:SessionManager、SDKAgent、DatabaseManager
钩子实现
- [ ] 上下文钩子:
GET /api/context/inject(带健康检查) - [ ] 新建钩子: createSDKSession, saveUserPrompt,
POST /sessions/{id}/init - [ ] 保存钩子:跳过低价值工具,
POST /api/sessions/observations - [ ] 摘要挂钩:解析文字记录,
POST /api/sessions/summarize - [ ] 清理钩子:
POST /api/sessions/complete
隐私与标签
- [ ] 实现
stripMemoryTagsFromPrompt()和stripMemoryTagsFromJson() - [ ] 在钩子层处理标签(边缘处理)
- [ ] 最大标签数 = 100(防 ReDoS 攻击)
SDK 集成
- [ ] 调用 Claude Agent SDK 处理观察/摘要
- [ ] 解析 XML 响应以获取结构化数据
- [ ] 存储到数据库 同步到向量数据库
关键设计原则
- 会话 ID 是真实来源:永远不要自己生成会话 ID
- 幂等数据库操作:使用
INSERT OR IGNORE创建会话 - 隐私的边缘处理:在数据到达工作节点之前,在钩子层删除标签
- 非阻塞的‘发射即忘’:HTTP 超时防止 IDE 阻塞
- 事件驱动,而非轮询:向 SDK 代理零延迟队列通知
- 一切始终保存:没有“孤立”的会话
常见陷阱
| 问题 | 根本原因 | 解决方案 |
|---|---|---|
| 会话 ID 不匹配 | 不同的钩子使用了不同的 session_id | 始终使用钩子输入中的 ID |
| 重复会话 | 创建新会话而不是使用现有会话 | 使用 INSERT OR IGNORE 并以 session_id 作为键 |
| 阻塞 IDE | 等待完整响应 | 使用带有短超时的即发即忘 |
| 数据库中的内存标签 | 在错误层剥离标签 | 在钩子层剥离,HTTP 发送前 |
| 未找到 Worker | 健康检查过快 | 添加带指数回退的重试循环 |
相关文档
- Worker Service - HTTP API 与异步处理
- Database Schema - SQLite 表和 FTS5 搜索
- Privacy Tags - 使用
<private>标签 - Troubleshooting - 常见钩子问题