Hook Lifecycle - Claude-Mem 摘要
文档概览
该页说明 claude-mem 的 hook 生命周期架构,核心结论是系统采用 5 阶段 hook system,用于在 Claude Code 会话期间持续捕获开发工作,并将其异步送入 worker 做持久化、压缩和总结。
文档一开始先给出整体架构要点:
- 扩展进程不应阻塞,采用 fire-and-forget HTTP 调用。
- worker 进程异步处理 observation。
- session 状态可跨 IDE 重启持续存在。
文档还给出一个适配 VS Code 的示例,把不同生命周期阶段映射到扩展 API:
- SessionStart:扩展激活时拉取上下文并注入聊天或 UI。
- UserPromptSubmit:命令执行时向 worker 发起 session 初始化请求。
- PostToolUse:保存文档等工具使用后,上报 observation。
此外,文档强调异步处理管线的关键模式:扩展侧 HTTP 调用只设置 2 秒超时,不会等待 AI 处理完成;压缩与后续存储由 worker 的事件驱动队列异步完成。这一设计是整个 hook 体系“不阻塞 IDE”的基础。
5 阶段生命周期总表
文档把生命周期明确分成 5 个阶段:
| 阶段 | 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 |
其中,文档块末也再次把目标部分明确标为 Stage 2: UserPromptSubmit。
关键事实
Stage 2 是 UserPromptSubmit
- 第 2 阶段明确是 UserPromptSubmit。
- 对应 hook 明确为
new-hook.js。 - 触发条件明确写为:
User submits a prompt. - 触发时机进一步展开为:当用户在某个会话中提交任意 prompt 时。
- 该阶段用途明确写为:
Create/get session, save prompt, init worker。
这意味着第 2 阶段并不是“首次打开会话时初始化”,也不是“Claude 调用工具后存储结果”,而是严格绑定在“用户提交 prompt”这个动作上。
hooks.json 中的实际配置
文档给出了 hook 配置代码,其中 UserPromptSubmit 的配置为:
"UserPromptSubmit": [{
"hooks": [{
"type": "command",
"command": "node ${CLAUDE_PLUGIN_ROOT}/scripts/new-hook.js",
"timeout": 120
}]
}]
因此,本阶段在配置文件中的实际执行命令是:
node ${CLAUDE_PLUGIN_ROOT}/scripts/new-hook.js
并且该命令配置的:
timeout为120
这也是文档中对第 2 阶段最直接、最可执行的配置证据。
Stage 2: UserPromptSubmit 详细整理
触发时机
文档原文写法是:
Timing: When user submits any prompt in a session
这里有两个边界很重要:
- 不是只在第一条消息时触发,而是 会话中的任意 prompt 都会触发。
- 不是在 Claude 回复完成后触发,而是在用户提交 prompt 时触发。
对应 Hook
- Hook 文件:
new-hook.js - 实现位置:
src/hooks/new-hook.ts
文档同时说明:同一个会话中的 所有 hooks 都沿用同一个 session_id。这对第 2 阶段尤其关键,因为它负责把会话层身份和后续数据流对齐。
输入
第 2 阶段通过 stdin 接收输入,文档给出的结构为:
{
"session_id": "claude-session-123",
"cwd": "/path/to/project",
"prompt": "User's actual prompt text"
}
这说明 UserPromptSubmit 至少依赖三类输入:
session_id:IDE 提供的会话 ID。cwd:当前工作目录,用于推导项目名。prompt:用户真实输入的 prompt 文本。
核心职责
文档把该阶段的目的概括成三件事:
- 创建或获取 session。
- 保存用户 prompt。
- 初始化 worker。
这三件事在实现层面对应一串明确步骤,而不是泛泛而谈。
处理步骤
文档给出的处理流程如下。
1)从工作目录提取项目名
project = path.basename(cwd)
即项目名不是从用户 prompt 中解析,而是直接从当前工作目录 basename 提取。
2)创建或获取数据库会话,且要求幂等
sessionDbId = db.createSDKSession(session_id, project, prompt)
// INSERT OR IGNORE: Creates new row if first prompt, returns existing if continuation
文档特别强调这里的 Key Pattern:
INSERT OR IGNORE保证同一个session_id总是映射到同一个sessionDbId。- 这使得会话 continuation 能够复用同一个数据库 session,而不会重复创建。
这也是文档后面“Session ID is Source of Truth”原则的具体落地。
3)递增 prompt 计数器
promptNumber = db.incrementPromptCounter(sessionDbId)
// Returns 1 for first prompt, 2 for continuation, etc.
文档给了明确数值语义:
- 首次 prompt 返回
1。 - 后续 continuation prompt 返回
2、3等递增值。
这说明系统不是只记录会话级别状态,还显式维护每个 session 内的 prompt 序号。
4)剥离隐私与系统标签
cleanedPrompt = stripMemoryTags(prompt)
// Removes <private>...</private> and <claude-mem-context>...</claude-mem-context>
第 2 阶段在保存前就做边缘处理(edge processing):
- 去掉用户级隐私标签
<private>...</private>。 - 去掉系统级递归防护标签
<claude-mem-context>...</claude-mem-context>。
文档后文还说明这是整套防护链条中的第一层,而不是唯一一层。
5)如果内容完全私有,则直接跳过
if (!cleanedPrompt || cleanedPrompt.trim() === '') {
return // Don't save, don't call worker
}
这是本阶段最重要的例外分支之一:
- 如果标签剥离后为空字符串,说明内容被完全视作私有。
- 此时 既不保存 prompt,也不调用 worker。
也就是说,“创建/获取 session”与“保存 prompt、初始化 worker”并不一定全部发生;若 prompt 在清洗后为空,后两步会被跳过。
6)保存用户 prompt 到数据库
db.saveUserPrompt(session_id, promptNumber, cleanedPrompt)
这里保存的是:
session_id对应的会话- 当前
promptNumber - 已经过标签剥离的
cleanedPrompt
因此,数据库中保存的不是原始 prompt,而是清洗后的可持久化版本。
7)通过 worker HTTP 初始化 session
POST http://127.0.0.1:<worker-port>/sessions/{sessionDbId}/init
Body: { project, userPrompt, promptNumber }
文档把这一步视为“init worker”的具体实现。也就是说,第 2 阶段并不是在本地直接完成所有后续处理,而是通过 worker 端点继续推进会话初始化。
输出
文档给出的输出为:
{ "continue": true, "suppressOutput": true }
这表明该 hook 完成后:
- 不阻止主流程继续执行。
- 不向用户显示额外输出。
从交互层面看,它是一个后台式、静默式 hook。
重要细节
同一个 session_id 贯穿整个会话
文档明确写道:
- The same
session_idflows through ALL hooks in a conversation.
这条规则把 UserPromptSubmit 与 SessionStart、PostToolUse、summary-hook.js、cleanup-hook.js 串了起来。第 2 阶段的 createSDKSession 也是围绕这个原则设计成幂等的:continuation prompt 不新建会话,而是返回已有 session。
createSDKSession 是幂等调用
文档再次强调:
createSDKSessionis idempotent- continuation prompts return the existing session
这与后文数据库部分的 SQL 模式一致:
INSERT OR IGNORE INTO sdk_sessions (claude_session_id, project, first_user_prompt)
VALUES (?, ?, ?)
RETURNING id;
文档把它解释为:同一个 session_id 必须始终映射到同一个数据库主键,不应由实现方自行重新生成会话 ID。
不要自己生成 session ID
数据库与状态机部分都明确给出约束: