Skip to content

钩子生命周期

Claude-Mem 实现了一个 五阶段钩子系统,用于捕获 Claude Code 会话中的开发工作。本文档为在其他平台上实现此模式的开发者提供了完整的技术参考。

架构概述

系统架构

这种双进程架构在 Claude Code 和 VS Code 中都可以运行:

关键原则:

  • 扩展过程永不阻塞(即发即忘的 HTTP)
  • 工作进程异步处理观察
  • 会话状态在 IDE 重启后仍然保留

VS Code 扩展 API 集成点

对于移植到 VS Code 的开发者,这里是接入 VS Code 扩展 API 的位置:

实施示例:

typescript
// 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.jsClaude 使用任何工具排队观察以进行 AI 压缩
4. 停止summary-hook.js用户停止提问生成会话摘要
5. 会话结束cleanup-hook.js会话关闭标记会话已完成

钩子配置

钩子配置在 plugin/hooks/hooks.json

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 或恢复会话时

钩子触发(顺序):

  1. smart-install.js - 确保已安装依赖项
  2. worker-service.cjs start - 启动 Worker 服务
  3. context-hook.js - 获取并静默注入先前会话上下文

从 Claude Code 2.1.0(ultrathink 更新)开始,SessionStart 钩子不再显示用户可见的消息。上下文通过 hookSpecificOutput.additionalContext 静默注入。

序列图

上下文钩子 (context-hook.js)

目的:将之前会话的上下文注入 Claude 的初始上下文中。

输入(通过标准输入):

json
{
  "session_id": "claude-session-123",
  "cwd": "/path/to/project",
  "source": "startup"
}

处理中

  1. 等待Worker可用(健康检查,最多10秒)
  2. 呼叫:GET http://127.0.0.1:37777/api/context/inject?project={project}
  3. 将格式化的上下文作为 additionalContext 返回到 hookSpecificOutput

输出(通过标准输出):

json
{
  "hookSpecificOutput": {
    "hookEventName": "SessionStart",
    "additionalContext": "<<formatted context markdown>>"
  }
}

实施: src/hooks/context-hook.ts


阶段 2:用户提示提交

时间:当用户在会话中提交任何提示时

钩子: new-hook.js

序列图

关键模式: INSERT OR IGNORE 确保相同的 session_id 始终映射到相同的 sessionDbId,从而实现对话的延续。

输入(通过标准输入):

json
{
  "session_id": "claude-session-123",
  "cwd": "/path/to/project",
  "prompt": "User's actual prompt text"
}

处理步骤

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 }

输出:

json
{ "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 工具的执行。

输入(通过标准输入):

json
{
  "session_id": "claude-session-123",
  "cwd": "/path/to/project",
  "tool_name": "Read",
  "tool_input": { "file_path": "/src/index.ts" },
  "tool_response": "file contents..."
}

处理步骤:

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 处理

  1. 查找或创建会话:createSDKSession(claudeSessionId, '', '')
  2. 获取提示计数器
  3. 检查隐私(如果用户提示完全私密则跳过)
  4. tool_inputtool_response 中删除内存标签
  5. 用于 SDK 代理处理的队列观察
  6. SDK 代理调用 Claude 将其压缩为结构化观察
  7. 将观察结果存储在数据库中并同步到 Chroma

输出:

json
{ "continue": true, "suppressOutput": true }

实施: src/hooks/save-hook.ts


第四步:停止

时机:当用户停止或暂停提问时

钩子: summary-hook.js

序列图

关键模式: 摘要是异步生成的,不会阻止用户恢复工作或关闭会话。

输入(通过标准输入):

json
{
  "session_id": "claude-session-123",
  "cwd": "/path/to/project",
  "transcript_path": "/path/to/transcript.jsonl"
}

处理步骤:

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 处理

  1. SDK 代理的队列汇总
  2. 代理调用 Claude 生成结构化摘要
  3. 摘要存储在数据库中,字段包括:requestinvestigatedlearnedcompletednext_steps

输出:

json
{ "continue": true, "suppressOutput": true }

实施: src/hooks/summary-hook.ts


阶段5:会议结束

时间:当 Claude Code 会话关闭时(退出、清除、注销等)

钩子: cleanup-hook.js

序列图

关键模式: 会话完成情况会被跟踪用于分析和界面更新,但不会阻止用户关闭 IDE。

输入(通过标准输入):

json
{
  "session_id": "claude-session-123",
  "cwd": "/path/to/project",
  "transcript_path": "/path/to/transcript.jsonl",
  "reason": "exit"
}

处理步骤

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

工作处理

  1. 通过 claudeSessionId 查找会话
  2. 将会话在数据库中标记为“已完成”
  3. 将会话完成事件广播给 SSE 客户端

输出:

json
{ "continue": true, "suppressOutput": true }

实施: src/hooks/cleanup-hook.ts


会话状态机

理解会话生命周期和状态转换:

关键见解:

  • session_id 在对话中从不改变
  • sessionDbId 是会话的数据库主键
  • promptNumber 随着每次用户提示而增加
  • 状态转换是非阻塞的(即触发即忘模式)

数据库模式

支持跨会话记忆的以会话为中心的数据模型:

幂等模式:

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

外键级联:

所有子表(user_prompts、observations、session_summaries)使用 session_id 外键引用 SDK_SESSIONS.id。这确保了:

  • 会话的所有数据都可以通过 sessionDbId 查询
  • 会话删除会级联到子表
  • 用于上下文注入的高效连接

绝不要自己生成会话 ID。始终使用 IDE 提供的 session_id ——这是将所有数据关联在一起的真实来源。


隐私与标签剥离

双标签系统

typescript
// 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

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

执行顺序(边缘处理):

  1. new-hook.js 在保存之前会从用户提示中去掉标签
  2. save-hook.js 在将工具数据发送给 Worker 进程之前会去除标签
  3. Worker 在存储前再次去掉标签(纵深防御)

SDK 代理处理

查询循环(事件驱动)

地点: 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)
    }
  }
}

消息类型

消息生成器产生三种类型的提示:

  1. 初始提示(提示 #1):开始观察的完整指示
  2. 续写提示(提示 #2):仅用于继续工作的上下文
  3. 观察提示:使用工具数据压缩为观察结果
  4. 摘要提示:用于总结的会话数据

实施清单

对于在其他平台上实现此模式的开发者:

钩子注册

  • [ ] 在平台配置中定义钩子入口点
  • [ ] 5 种钩子类型:SessionStart(2 个钩子)、UserPromptSubmit、PostToolUse、Stop、SessionEnd
  • [ ] 传递 session_idcwd 和特定于上下文的数据

数据库模式

  • [ ] 使用 WAL 模式的 SQLite
  • [ ] 4 个主要表:sdk_sessionsuser_promptsobservationssession_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 响应以获取结构化数据
  • [ ] 存储到数据库 同步到向量数据库

关键设计原则

  1. 会话 ID 是真实来源:永远不要自己生成会话 ID
  2. 幂等数据库操作:使用 INSERT OR IGNORE 创建会话
  3. 隐私的边缘处理:在数据到达工作节点之前,在钩子层删除标签
  4. 非阻塞的‘发射即忘’:HTTP 超时防止 IDE 阻塞
  5. 事件驱动,而非轮询:向 SDK 代理零延迟队列通知
  6. 一切始终保存:没有“孤立”的会话

常见陷阱

问题根本原因解决方案
会话 ID 不匹配不同的钩子使用了不同的 session_id始终使用钩子输入中的 ID
重复会话创建新会话而不是使用现有会话使用 INSERT OR IGNORE 并以 session_id 作为键
阻塞 IDE等待完整响应使用带有短超时的即发即忘
数据库中的内存标签在错误层剥离标签在钩子层剥离,HTTP 发送前
未找到 Worker健康检查过快添加带指数回退的重试循环

相关文档