W
AI-Wiki
SOURCE

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 个阶段:

阶段HookTriggerPurpose
1. SessionStartcontext-hook.jsUser opens Claude CodeInject prior context silently
2. UserPromptSubmitnew-hook.jsUser submits a promptCreate/get session, save prompt, init worker
3. PostToolUsesave-hook.jsClaude uses any toolQueue observation for AI compression
4. Stopsummary-hook.jsUser stops asking questionsGenerate session summary
5. SessionEndcleanup-hook.jsSession closesMark 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

并且该命令配置的:

  • timeout120

这也是文档中对第 2 阶段最直接、最可执行的配置证据。

Stage 2: UserPromptSubmit 详细整理

触发时机

文档原文写法是:

  • Timing: When user submits any prompt in a session

这里有两个边界很重要:

  1. 不是只在第一条消息时触发,而是 会话中的任意 prompt 都会触发。
  2. 不是在 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 文本。

核心职责

文档把该阶段的目的概括成三件事:

  1. 创建或获取 session。
  2. 保存用户 prompt。
  3. 初始化 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 返回 23 等递增值。

这说明系统不是只记录会话级别状态,还显式维护每个 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_id flows through ALL hooks in a conversation.

这条规则把 UserPromptSubmitSessionStartPostToolUsesummary-hook.jscleanup-hook.js 串了起来。第 2 阶段的 createSDKSession 也是围绕这个原则设计成幂等的:continuation prompt 不新建会话,而是返回已有 session。

createSDKSession 是幂等调用

文档再次强调:

  • createSDKSession is 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

数据库与状态机部分都明确给出约束: