claude-mem 架构解析:Hook 生命周期、Worker 守护进程与双数据库存储设计
2026/9/7 4:31:05 网站建设 项目流程

claude-mem 架构解析:Hook 生命周期、Worker 守护进程与双数据库存储设计

【免费下载链接】claude-memPersistent Context Across Sessions for Every Agent – Captures everything your agent does during sessions, compresses it with AI, and injects relevant context back into future sessions. Works with Claude Code, OpenClaw, Codex, Gemini, Hermes, Copilot, OpenCode + More项目地址: https://gitcode.com/GitHub_Trending/cl/claude-mem

本文基于 claude-mem 仓库内的 架构总览文档,完整还原该系统的四层结构(宿主 Hook 层、CLI 层、Worker 守护进程、存储层),并逐一对照源码验证 Hook 生命周期表、数据流 API、Pending 队列、生成器重启循环、优雅降级、观察去重与双会话 ID 等关键设计。读完你可以掌握:claude-mem 如何在 Claude Code 会话中无侵入地捕获工具调用、用 AI 压缩成观察记录写入 SQLite + ChromaDB,以及这套架构"绝不打断用户会话"的错误处理哲学是如何落地的。

系统分层总览

claude-mem 的官方架构文档将系统划分为四层。以下分层图完整继承自 docs/architecture-overview.md:

+-----------------------------------------------------------+ | Claude Code (host) | | +-- Hook System (Setup + 5 lifecycle events) | | +-- MCP Client (search tools) | +-----------------------------------------------------------+ | CLI Layer (Bun) | | +-- bun-runner.js (Node->Bun bridge) | | +-- hook-command.ts (orchestrator) | | +-- handlers/ (context, session-init, observation, | | summarize, session-complete) | +-----------------------------------------------------------+ | Worker Daemon (Express, per-user port 37700+(uid%100)) | | +-- SessionManager (session lifecycle) | | +-- SDKAgent (Claude Agent SDK) | | +-- SearchManager (search orchestration) | | +-- ProcessRegistry (subprocess management) | | +-- ChromaSync (embedding synchronization) | +-----------------------------------------------------------+ | Storage Layer | | +-- SQLite (claude-mem.db) -- structured data | | +-- ChromaDB (chroma.sqlite3) -- vector embeddings | | +-- MCP Server (interface for Claude Code) | +-----------------------------------------------------------+

各层的职责边界如下:

  • Claude Code(宿主层):通过 Hook System 在生命周期事件点调用插件脚本,并通过 MCP Client 消费 claude-mem 暴露的搜索工具。
  • CLI 层(Bun):负责协议适配。入口是 plugin/scripts/bun-runner.js(Node 到 Bun 的桥接器),它被 Claude Code 的 Node 环境调起后再转发给 Bun 执行真正的 TypeScript 逻辑;真正的编排者是 src/cli/hook-command.ts,具体事件分发到 src/cli/handlers/ 下的 context、session-init、observation、summarize、session-complete 等处理器。
  • Worker 守护进程:基于 Express 的长驻服务,每个用户独占一个端口(公式为37700 + (uid % 100))。这一默认值可以在 src/shared/SettingsDefaultsManager.ts 中得到印证:CLAUDE_MEM_WORKER_PORT: String(37700 + ((process.getuid?.() ?? 77) % 100)),且该端口还可经CLAUDE_MEM_WORKER_PORT环境变量覆盖。
  • 存储层:SQLite 存结构化数据,ChromaDB 存向量嵌入,MCP Server 作为对 Claude Code 的对外接口。

关于 CLI 层有一个值得注意的实现细节:plugin/scripts/bun-runner.js 在启动前会读取~/.claude/settings.json,若检测到enabledPlugins['claude-mem@thedotmack'] === false,直接process.exit(0)静默退出——用户禁用插件时,Hook 完全不产生副作用。而findBun()则按which bun/where bun~/.bun/bin、Homebrew 路径等多级策略定位 Bun 可执行文件(见 bun-runner.js)。

Hook 生命周期与事件注册

架构文档给出的 Hook 生命周期表完整继承如下:

EventHandler作用超时
Setupversion-check.js小于 100ms 的版本标记检查;不匹配时提示运行npx claude-mem repair60s
SessionStartworker start + context启动 worker 服务并注入上下文60s
UserPromptSubmitsession-init注册会话 + 启动 SDK agent + 语义注入60s
PostToolUseobservation捕获工具使用 -> 入队到 worker120s
Summarysummarize向 SDK agent 请求会话摘要120s
SessionEndsession-complete结束会话 + 排空待处理消息30s

这些声明在当前的 Hook 注册配置 plugin/hooks/hooks.json 中可以逐条对上,且能看到更多落地细节:

  • SessionStart(matcher 为startup|clear|compact)实际注册了两个 Hook:第一个是worker-service.cjs start(启动守护进程),第二个是hook claude-code context(注入上下文),timeout 均为 60 秒;
  • UserPromptSubmit注册hook claude-code session-init,timeout 60 秒;
  • PostToolUse(matcher 为*)注册hook claude-code observation,timeout 120 秒且标记async: true——观察捕获异步执行,不阻塞工具调用返回;
  • Stop注册hook claude-code summarize,timeout 120 秒、async: true。从源码结构看,文档表中的 "Summary / SessionEnd" 在当前注册配置中分别对应 Stop 事件驱动的 summarize 与内部 session-complete 处理器;
  • 当前配置中还额外注册了一个文档表格未列出的PreToolUse(matcher 为Read)事件,调用file-context处理器,timeout 60 秒、async: true
  • 文档表中 Setup 标注超时 60s,而当前 hooks.json 中 Setup 条目实际配置为timeout: 300

每条 Hook 命令都遵循同一个模式:先用一段 bash 在~/.claude/plugins/cache/thedotmack/claude-mem/下按语义化版本排序选出最新插件缓存,再用node bun-runner.js <script>启动。这种"版本号排序选择器"保证了多版本缓存共存时始终运行最新版,并显式跳过带.orphaned_at标记的孤儿缓存目录。

文档还说明了首次安装的行为:npx claude-mem install会全局安装 Bun 和 uv、在插件缓存目录执行bun install、并写入.install-version版本标记文件,全程在可见的 clack spinner 下完成。此后每次启动 Claude Code,Setup Hook 都会运行version-check.js做版本标记比对;如果插件被外部升级(例如claude plugin update),它向 stderr 写入提示,要求用户运行npx claude-mem repair。该 Hook 始终 exit 0,绝不阻塞宿主。

数据流:从用户提示到向量库

文档描述的核心数据流完整继承如下:

User prompt -> session-init -> /api/sessions/init + /api/context/semantic | Tool use -> observation -> /api/sessions/observations | | | PendingMessageStore.enqueue() | | | SDKAgent.startSession() | | | Claude Agent SDK -> ResponseProcessor | | | +-- storeObservations() -> SQLite | +-- chromaSync.sync() -> ChromaDB | +-- broadcastObservation() -> SSE/UI | Stop -> summarize -> /api/sessions/summarize -> session-complete -> /api/sessions/complete + drain

对照 Worker 的路由实现 src/services/worker/http/routes/SessionRoutes.ts,setupRoutes恰好注册了文档所述的三个端点,且每个端点都套了 zod 请求体校验:

  • POST /api/sessions/init(schema 定义):字段为contentSessionId(必填)、projectpromptplatformSourcecustomTitle。处理逻辑包括:超过 256KB 的 prompt 会在边界处按 UTF-8 字节安全截断;stripMemoryTags会剥离隐私标记,若 prompt 剥离后为空则直接返回skipped: private;重复 prompt 会经findRecentDuplicateUserPrompt在去重窗口内命中并返回skipped: duplicate;最后store.saveUserPrompt落库,调用sessionManager.initializeSession初始化会话,并触发ensureGeneratorRunning(sessionDbId, 'init')启动 SDK agent。
  • POST /api/sessions/observations(schema 定义):字段为contentSessionIdtool_nametool_inputtool_responsecwdagentIdagentType等,委托给ingestObservation完成入队,成功时返回{ status: 'queued' }
  • POST /api/sessions/summarize(schema 定义):接收last_assistant_messageobservedModelobservedBilling等字段;若带agentId(子代理上下文)则跳过;先经PrivacyCheckValidator隐私检查,再通过sessionManager.queueSummarize入队并broadcastSummarizeQueued

数据流中"入队 -> SDK 生成 -> 三路分发(SQLite / ChromaDB / SSE)"的环节,分别对应PendingMessageStore队列、ClaudeProvider等生成器实现,以及ChromaSyncSSEBroadcaster(均位于 src/services/worker/ 目录)。

优雅降级:Worker 不可用绝不阻塞宿主

数据流的第一原则是Worker 挂了也不影响用户会话。文档的表述是:

Transport errors (ECONNREFUSED, timeout, 5xx) -> exit 0 (never block Claude Code) Client bugs (4xx, TypeError, ReferenceError) -> exit 2 (blocking, needs fix)

这一策略在 src/cli/hook-command.ts 的isWorkerUnavailableError中有完整实现:它匹配econnrefusedeconnresetepipeetimedoutfetch failedsocket hang up等传输层错误模式、超时关键字、5xx/429 状态码,一律判定为"Worker 不可用";而 4xx、TypeErrorReferenceErrorSyntaxError则被明确排除——这些是代码自身 bug,需要暴露出来。

对应的处理路径在 hookCommand 的 catch 块:

  1. Worker 不可用:记一条 warn 日志,recordWorkerUnreachable累加失败计数(连续失败达到阈值后触发 fail-loud 提示与阈值门控的hook_failed遥测),然后exit 0静默放行;
  2. 客户端 bug:上报hook_failed遥测后调用emitBlockingError把错误冲刷到 stderr 并exit 2,让 Claude Code 将错误呈现给模型和用户;
  3. 适配器拒绝输入 / transcript 缺失:返回 no-op 结果并 exit 0。

退出码常量定义在 src/shared/hook-constants.ts:SUCCESS: 0BLOCKING_ERROR: 2。同一文件还给出了 Hook 内部的超时预算(如API_REQUEST: 30000READINESS_WAIT: 30000),并提供getTimeout函数在 Windows 上统一乘以 1.5 的系数——Windows 冷启动更慢,超时放宽是这套跨平台系统的常规手段。

另外值得一提的细节:hook 执行期间 stderr 是被缓冲的(installHookStderrBuffer,见 hook-command.ts),只有"决定呈现"的路径(阻塞错误、fail-loud 计数)才冲刷缓冲区,成功退出则丢弃缓冲——保证第三方库的意外 stderr 写入不会污染注入到模型上下文的输出。

关键模式

Pending 队列(PendingMessageStore)

文档描述的队列语义:

enqueue() -> INSERT row with `pending` status clearPendingForSession() -> DELETE all pending rows for session (called whenever the parser returns a parseable response, regardless of whether observations were extracted)

解析器是二值的:{ valid: true, observations, summary }{ valid: false }。不可解析的响应不触碰队列,会话迭代器继续运行。这个设计的精妙之处在于以"解析成功"而非"是否抽取出观察"作为清队条件:即使某次响应里没有可提取的观察,也说明 Agent 输出通道是健康的,积压消息可以安全丢弃,避免旧消息污染后续上下文。clearPendingForSession的实现可在 src/services/worker/SessionManager.ts 中找到。

生成器重启循环(SessionRoutes)

Generator crash -> retry 1 (1s) -> retry 2 (2s) -> retry 3 (4s) -> consecutiveRestarts > 3 -> stop and let the iterator end

指数退避(1s -> 2s -> 4s),连续重启超过 3 次即放弃,让迭代器自然结束;生成器自然完成工作后计数器归零。待处理消息跨重启保留在队列中,等下一次解析器产出合法响应时再被清除。consecutiveRestarts字段在 src/services/worker-types.ts 的类型定义中可以确认,初始化为 0 的位置在 SessionManager.ts。

值得补充的是,当前的生成器启动路径ensureGeneratorRunning(SessionRoutes.ts)在重启循环之外还叠加了两个"熔断器":

  • 溢出熔断(overflow breaker)session.overflowPausedUntilMs冷却期内直接跳过启动,防止两次回收(recycle)仍装不下上下文时陷入"每次工具调用都 spawn 一次、随即因预算检查中止"的循环;
  • 配额熔断(quota breaker):通过tryAdmitQuotaProbe声明式抢占探针资格,配额耗尽期间扣留请求、冷却结束后只放一个"重探针"通过,避免每个观察都买回同一个配额拒绝。

这两层保护与文档描述的重试循环共同构成生成器的完整自愈体系。

观察去重(Deduplication)

SHA256(memory_session_id + title + narrative)[:16] -> content_hash (16 hex chars) If hash exists within 30s window -> return existing ID (no insert)

16 位十六进制内容哈希的截断在 src/services/sqlite/observations/store.ts 的.slice(0, 16)中得到印证。同一观察在 30 秒窗口内重复到达时(例如 Hook 重试、同一工具调用被多次捕获)直接返回已有 ID,不产生重复行。

两种会话 ID

  • contentSessionId—— 来自 Claude Code,会话期间保持不变;
  • memorySessionId—— 来自 SDK Agent,每次 Worker 重启都会变化。

两者的映射关系由 SessionStore 维护,是外键约束正确性的关键。SessionRoutes.ts 中的日志可以直观看到这个映射过程:store.createSDKSession(contentSessionId, ...)创建或复用数据库会话行,新会话尚无memory_session_id(日志标注will be captured on first SDK response),等 SDK 首次响应后才回填。所有对外 API(init / observations / summarize)都以contentSessionId为入口参数,内部再换算为数据库行 ID——这正是"两种 ID"设计对 API 表面透明化的体现。

存储层

SQLite(claude-mem.db)

文档给出的表结构一览:

关键字段用途
sdk_sessionscontent_session_id, memory_session_id, status会话生命周期
observationsmemory_session_id, type, title, narrative, content_hash工具使用观察
session_summariesmemory_session_id, request, learned, completed会话摘要
user_promptscontent_session_id, prompt_text用户提示历史
pending_messagessession_db_id, message_type每会话的待处理队列
observation_feedbackobservation_id, signal_type使用信号追踪

这几张表的写入路径与上文数据流一一对应:user_prompts由 session-init 写入(saveUserPrompt),pending_messages由 observation 入队写入,observationssession_summaries由生成器响应解析后写入。测试侧可以在 tests/sqlite/session-store-sessions.test.ts、tests/sqlite/session-store-observations.test.ts 等用例中查看各表的读写契约。

ChromaDB(chroma.sqlite3)

ChromaDB 承载语义搜索所需的向量嵌入。每条观察会生成多个文档:

obs_{id}_narrative -> 主文本 obs_{id}_fact_0 -> 第一条事实 obs_{id}_fact_1 -> 第二条事实 ...

将一条观察拆成"主叙事 + 逐条事实"多个嵌入文档,可以让语义检索在细粒度事实上命中,而不是只在整段叙事上模糊匹配。访问链路是经 chroma-mcp(独立 MCP 进程,stdio 通信)完成,对应实现位于 src/services/sync/ChromaMcpManager.ts;用户 prompt 的向量化同步则由 SessionRoutes.ts 中的dbManager.getChromaSync()?.syncUserPrompt(...)在 session-init 阶段 fire-and-forget 触发,同步失败只记错误日志、"continuing without vector search",不阻塞主流程。

搜索侧的编排由 src/services/worker/search/SearchManager.ts 与搜索策略(ChromaSearchStrategy、SQLiteSearchStrategy、HybridSearchStrategy)完成,最终通过 src/servers/mcp-server.ts 暴露给 Claude Code 的 MCP Client。

进程管理

文档对 Worker 进程管理给出三条要点:

  • ProcessRegistry:追踪所有 Claude SDK 子进程,管理其 PID 生命周期。对应 src/supervisor/process-registry.ts;
  • Orphan Reaper(5 分钟):周期性清理没有活跃会话的孤儿进程;
  • GracefulShutdown:7 步关停序列——PID 文件、子进程、HTTP 服务器、会话、MCP、数据库、最终强制 kill。关停逻辑分布在 src/services/worker-shutdown.ts 与 src/supervisor/shutdown.ts,行为契约由 tests/services/worker-shutdown-sequence.test.ts 等测试验证。

这套进程管理与 Hook 层的"永远 exit 0"策略形成互补:Hook 侧保证单次调用故障不外溢,Worker 侧的注册表 + 孤儿收割 + 分步关停保证长驻进程自身不泄漏、可干净重启。

小结

回到 docs/architecture-overview.md 的核心命题,claude-mem 的架构可以用三个词概括:

  1. 分层解耦——宿主 Hook、CLI 编排、Worker 守护、双库存储各管一段,Hook 通过 HTTP API 与 Worker 通信,任何一层失效都不会跨层传导;
  2. 无阻塞优先——从 Setup Hook 的"永远 exit 0",到传输错误 exit 0 / 客户端 bug exit 2 的分类退出码,再到 Chroma 同步失败仅降级为日志,整个系统把"绝不打断用户会话"做成了贯穿各层的硬约束;
  3. 自愈与幂等——Pending 队列 + 解析器清队、生成器指数退避重启 + 溢出/配额双熔断、30 秒窗口内的观察内容去重、跨重启保留的 pending 消息,共同让异步的"捕获 -> 压缩 -> 存储"流水线在崩溃、重试、重启场景下保持不丢不重。

如果你想进一步深入,建议从 src/cli/handlers/ 的事件处理器、src/services/worker/http/routes/ 的完整路由集,以及 tests/ 下按模块组织的测试用例入手,它们与本文各章节一一对应。

【免费下载链接】claude-memPersistent Context Across Sessions for Every Agent – Captures everything your agent does during sessions, compresses it with AI, and injects relevant context back into future sessions. Works with Claude Code, OpenClaw, Codex, Gemini, Hermes, Copilot, OpenCode + More项目地址: https://gitcode.com/GitHub_Trending/cl/claude-mem

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询