首页
/ claude-mem server-beta 架构解析:从单机 SQLite 到 Postgres + BullMQ 的多租户共享记忆运行时

claude-mem server-beta 架构解析:从单机 SQLite 到 Postgres + BullMQ 的多租户共享记忆运行时

2026-09-06 15:15:42作者:霍妲思

本文基于仓库中的架构长文档 server-architecture-and-team-vision.md 展开,结合仓库内真实源码(事件摄取服务、生成工作进程、作业队列与 HTTP 路由),完整还原 claude-mem server-beta 从 Phase 4 到 Phase 13 构建的多租户运行时:事务性 outbox 摄取、异步水平可扩展的 LLM 生成、四层身份审计体系,以及它如何把"单人单机记忆"平滑升级为"团队级共享记忆"。读完后你可以理解 server-beta 的完整事件流与关键代码实现,掌握 API Key 签发、队列调度、运维重试与调试排障的实操路径。

1. 核心概览(TL;DR)

server-beta 把 claude-mem 从一个单机 SQLite 工具,改造成一个由 Postgres + BullMQ(Valkey) 支撑的多租户运行时,同时保留了这个项目最初被用起来的根本属性:开发者什么都不用改变。Hooks、MCP 工具、viewer UI、search skill 全部保持原有契约。

在底层,每一个事件都携带完整的身份三元组——api_key_id × actor_id × request_id——落在按租户隔离的存储基座上,从而支撑团队、项目、scope、审计链,以及分进程的生成 worker。

据文档记载,PR #2383 一次性落地了 Phase 4–13 的全部能力(约 13K 行代码、72 个文件),经过五轮自动化审查和约 20 项修复(从 provider.generate() 的 P1 级竞态,到 prompt 中的 XML 转义)之后获批合入。最终结果是:同一套代码路径可以同时承载 solo 开发者记忆、小团队共享记忆,乃至组织级联邦记忆。

2. 起点问题:单机 SQLite 工具为什么撑不起团队

claude-mem 最初的定位是:装一次,正常干活,AI 突然就有了跨会话记忆,"just works"。整体链路是:生命周期 hook 写入事件,异步 worker 调用 Claude 解析出 observation 并持久化,search skill 让它们可检索。这条链路不要求开发者思考任何基础设施问题。

这套设计对"一个开发者、一台机器、一个 SQLite 文件"完美成立,但一旦出现以下任意需求就会立刻失效:

  • 团队里第二个开发者希望受益于第一个人的 observation;
  • 多个 AI agent(CI、MCP 客户端、IDE 扩展)写入同一个记忆池;
  • 需要能回答安全/合规审查中"是谁告诉 AI 这些信息的"审计链;
  • 同一台机器上两个 profile 互不端口冲突;
  • 生成过程水平扩展(一次缓慢的 Anthropic 调用不应阻塞 HTTP 路径)。

遗留的 worker-service.cjs 运行时无法在不放弃其单进程/单租户假设的前提下长成上述任何一项。server-beta 就是为此构建的并行运行时,同时保留遗留 worker 给不需要这些能力的用户。

运行时切换由 runtime-selector.ts 决定:它读取 ~/.claude-mem/settings.json 中的 CLAUDE_MEM_RUNTIME,当前源码中 'server' 与旧的 'server-beta' 字面量都被归一化为 server 运行时(保留向后兼容),默认值则是 worker

3. Phases 4–13 建设清单

Phase 1–3(已在 #2351 合入)交付了基座:Postgres schema(schema.ts)、租户作用域仓储(agent-events.tsgeneration-jobs.tsserver-sessions.tsauth.tsobservations.ts)以及 ServerJobQueue BullMQ 封装(ServerJobQueue.ts)。PR #2383 在其上构建了全部上层能力:

Phase 交付物 关键文件
4 事件→作业流水线(事务性 outbox + 摄取服务) IngestEventsService.ts、outbox 封装
5 Provider observation 生成器(Claude / Gemini / OpenRouter) ProviderObservationGenerator.tsproviders/
6 独立的 server session 语义 + 三策略调度 server-sessions.tsSessionGenerationPolicy.ts
7 Hooks 经 HTTP 路由(不再依赖 worker) runtime-selector.tsserver-client.tsserver-bootstrap.ts
8 /v1/* 内核支撑的独立 MCP server mcp-server.ts
9 遗留 worker payload 的兼容适配器 SessionsObservationsAdapter.tsSessionsSummarizeAdapter.ts
10 Docker 栈——分进程可部署 docker-compose.ymlDockerfilee2e-server-docker.sh
11 团队感知生成 + 审计链 scope 检查与 audit 写入位于 ProviderObservationGenerator.ts 内部;身份上下文位于 IngestEventsService.ts
12 可观测性 + 运维 request-id.ts、BullMQ payload 内嵌 request_id、/api/health 队列车道、server-jobs.ts、运维路由(POST /v1/jobs/:id/retryPOST /v1/jobs/:id/cancel
13 发布就绪审计 server-release-readiness.md

此后五轮审查反馈落地了约 20 项跟进修复:

  • P1 级:BullMQ 重投卡死作业时 provider 被双重调用;运维重试入队了错误 payload;resolveServerSession 的 TOCTOU 在并发兼容负载下造成 500;batch 端点给所有事件盖了第一个事件的 sourceAdapter 戳;重试 completed 作业导致 observation 重复。
  • Major 级:原始 server_session_id 的 XML 注入;worker 与 QueueEvents 双重计数 stalled 事件;PostgresObservationRepository 的静态/动态导入混用;MCP observation_record_eventgenerate 标志被忽略;markGenerationFailedjsonb_set 空值保护。
  • Minor 级:debounce 默认值的 NaN 归并 bug;docker-compose.yml 中硬编码的 Postgres 凭据;无限制的 api-key list 查询(跨租户泄露);wait=true 并未真正等待;endSession 破坏 updated_at 幂等性;硬编码的 37877 server-beta 端口(多账号隔离问题);测试池清理;文档润色。

每一项都是 PR 中独立的审计条目,但更有意思的故事是:当它们全部落齐后,整个基座长成了什么样子。

4. 解剖一次事件的全程旅程

以代码自顶向下阅读:当一个 Claude Code hook 带 wait=true 触发一个工具使用事件时,发生的事情如下:

Hook → bun-runner → POST /v1/events?wait=true (X-API-Key: cmem_…)
                     │
                     ▼
            requestIdMiddleware()  [src/server/middleware/request-id.ts]
                     │   mints uuid (or honors X-Request-Id)
                     ▼
       requirePostgresServerAuth(scopes: ['memories:write'])
                     │   resolves api_key_id, team_id, project_id, scopes, actor_id
                     ▼
       IngestEventsService.ingestOne()         [transactional]
         INSERT agent_events row
         pre-generate outbox id (newId())
         build BullMQ payload {
           kind: 'event',
           team_id, project_id, source_type, source_id,
           generation_job_id, agent_event_id,
           api_key_id, actor_id, source_adapter, request_id
         }
         INSERT observation_generation_jobs (status=queued, payload=<canonical bullmq payload>)
         APPEND generation_job_events (eventType=queued)
         tx commits
                     │
                     ▼
       publishEventJob() → SessionGenerationPolicy 决策
         policy: per-event | debounce | end-of-session
                     │
                     ▼
          BullMQ Queue.add(deterministic jobId, payload)
                     │
                     ▼
       auditWrite('event.received', request_id, …)
                     │
                     ▼
       waitForTerminalJob()  [polls outbox row, 100ms × up to 30s]
                     │
                     ▼
       HTTP 201 { event, generationJob: { status: 'completed' | 'failed' | … } }

4.1 HTTP 摄取侧:一次事务、三行写入

这段流程在 IngestEventsService.ts 中有逐行对应的实现。源码证实了几个关键设计点:

  1. 事务性 outboxingestOne()withPostgresTransaction 内连续完成三次写入——插入 agent_events 行、预生成 outbox id(newId())并写入 observation_generation_jobs(status=queued,payload 就是规范化的 BullMQ payload)、追加 generation_job_events 生命周期日志(eventType=queued)。文件头注释明确了设计意图:该模块同时被 /v1/events(canonical)与兼容适配器调用,两处共享完全相同的 outbox-then-publish 保证,且禁止src/services/worker/* 导入——这正是 Phase 9 "兼容层是薄翻译器而非平行实现" 这一反模式守卫的代码体现。
  2. payload 持久化的意义:outbox 行上的 payload 字段保存的就是规范化 BullMQ payload。注释写明:reconciliation(启动时对账)与运维重试都依赖这个持久化 payload 来重新入队,且必须能通过 worker 端的 assertServerGenerationJobPayload 校验。
  3. 提交后才发布:事务提交后 publishEventJob() 才把作业加入 BullMQ 队列,返回值是 'enqueued' | 'queued_only' | 'skipped' 三态——队列不可用时行保持 queued,等待启动对账重新发布。这意味着耐久性住在 Postgres 里,队列只是传输优化

4.2 Worker 生成侧:从不信任 payload

BullMQ 把作业投递到 ProviderObservationGenerator.tsprocess() 后(并行或稍后,取决于 worker 池),源码中的处理顺序与文档完全一致,且每一步都有明确的防攻击/防 bug 语义:

BullMQ delivers job to ProviderObservationGenerator.process()
   │
   ├─ assertServerGenerationJobPayload(job.data)        ← shape validation
   ├─ scope check: payload.team_id === canonical row?   ← refuses cross-tenant
   ├─ api-key revocation check
   ├─ lockOutbox(): atomic queued→processing, OR skip if processing already
   ├─ loadEvents() — pulls the agent_event(s) for this source
   ├─ provider.generate({ job, events, project }) — Anthropic / Gemini / OpenRouter
   ├─ processGeneratedResponse() — parse XML, persist observations + sources,
   │   transition outbox to completed, write 'generation.completed' audit
   └─ BullMQ removes the job

源码中的几处细节值得特别指出:

  • scope 反篡改检查process() 中):worker 按 id 加载不带作用域过滤的规范 outbox 行,然后比较 team_id/project_id。不匹配即抛出 ServerGenerationScopeViolationError('scope_mismatch'),写入 generation_job.scope_violation 审计,markGenerationFailed 标记不可重试,然后抛错——被污染(poisoned)的 BullMQ payload 无法逃出它的租户。注释明确标注:篡改 payload 的攻击者绝不该被重试回队列
  • 吊销检查:入队与执行之间 API Key 若被吊销(revoked_atexpires_at 或行已删除),worker 拒绝生成并审计为 generation_job.revoked_key
  • lockOutbox 的 P1 修复lockedOutbox() 把行原子地从 queued 迁到 processing;若行已在 processing,直接跳过而不是继续执行。源码注释解释了原因:这通常在 BullMQ 把卡死作业重投给第二个 worker、而第一个 worker 仍在 provider.generate() 中时触发——若返回该行,两个 worker 都会发出这次要花真钱、受速率限制的外部 provider 调用。若第一个 worker 确实死了,reconcileOnStartup 与下一次 BullMQ 重试会复活该行。没有这道保护,一次 stalled 重投就会让 provider 被调用两次。
  • 审计全覆盖:处理开始时写 generation_job.processing 审计(即使 provider 随后崩溃也留有行),details 中携带 correlationIdrequestId、attempt 等信息。

如果 worker 在生成中途死亡,启动对账逻辑会用已持久化的 payload(经 P1 修复后是规范 BullMQ payload 而非仅元数据)重新发布所有卡在 queued/processing 的行;确定性的 BullMQ job id 保证重复项在队列侧自动折叠。这就是整条脊柱,系统其余所有表面都是这条流程片段的复用。

5. 系统集成图:所有客户端收敛到同一 /v1 表面

插件的 hook 层没有改变——hooks.json 仍然把事件分发给 worker-service.cjs(由 src/services/worker-service.ts 构建)。改变的是分发之后发生的事:

┌──────────────────────────────────────────────────────────┐
│  Claude Code session                                     │
│   ├─ UserPromptSubmit hook                              │
│   ├─ PreToolUse / PostToolUse hooks                     │
│   ├─ Stop hook                                           │
│   └─ Setup / SessionStart hooks                         │
└──────────────────────────────────────────────────────────┘
                       │
                       ▼ bun-runner.js dispatches subcommand
┌──────────────────────────────────────────────────────────┐
│  worker-service.cjs                                      │
│   ├─ runtime-selector.ts decides:                       │
│   │    • CLAUDE_MEM_RUNTIME=worker     → legacy SQLite  │
│   │    • CLAUDE_MEM_RUNTIME=server-beta → HTTP client   │
│   └─ ServerBetaClient.recordEvent(input) → /v1/events   │
└──────────────────────────────────────────────────────────┘
                       │
                       ▼
       ┌──────────────────────────────────────────────────┐
       │  claude-mem-server (HTTP)                       │
       │   /v1/events            ← hook event ingest     │
       │   /v1/events/batch      ← batch ingest          │
       │   /v1/sessions/start    ← session creation      │
       │   /v1/sessions/:id/end  ← summary trigger       │
       │   /v1/search            ← FTS search            │
       │   /v1/context           ← context pack          │
       │   /v1/memories          ← direct insert         │
       │   /v1/observations/:id  ← scoped read           │
       │   /v1/jobs/:id/retry    ← operator             │
       │   /v1/jobs/:id/cancel   ← operator             │
       │   /api/health           ← per-lane queue stats │
       │                                                  │
       │  + auth middleware, request_id middleware,      │
       │    compat adapters mounted at /api/sessions/*   │
       └──────────────────────────────────────────────────┘
                       │             │
       Postgres ◄──────┘             └──────► Valkey (BullMQ)
                                                 │
                                                 ▼
                                  ┌──────────────────────────┐
                                  │ claude-mem-worker        │
                                  │  ProviderObservationGen  │
                                  │  (no HTTP listener)      │
                                  └──────────────────────────┘
                                                 │
                                                 ▼
                                  Postgres observations + audit

同一个 /v1 表面被以下方共同命中:

  • Hooks,经 worker-service.cjs 内部的 ServerBetaClient
  • MCP 客户端(Claude Desktop、Cursor 等),经 mcp-server.ts 把 MCP 工具调用翻译成 /v1/events/v1/search/v1/context/v1/memories
  • viewer UIviewer.html),读取 /api/health 的队列车道与 /v1 读端点的记忆列表;
  • mem-search skillmem-search/),无论运行时如何都调用 /v1/search
  • 遗留兼容垫片,把旧的 POST /api/sessions/observations/api/sessions/summarize payload 翻译成与 canonical /v1/* 路由相同的 IngestEventsService / EndSessionService 调用。

最后一点很重要:任何针对遗留 worker 写的客户端都能通过兼容适配器继续工作,无需重写。兼容层是薄翻译器,不是平行实现——反模式被守卫进同一个共享服务。

从当前源码看,ServerV1PostgresRoutes.ts 中注册的端点比文档表格更丰富,还包括 /v1/keys(签发)、/v1/connect/v1/usageGET /v1/jobs 及按 team/project 维度的作业查询、DELETE /v1/memories/:idDELETE /v1/projects/:projectId/memory(数据删除)以及 /v1/mcp(POST/GET,MCP 通道复用 HTTP 表面)等。

6. 单用户模型:server-beta 的"隐形"

对于在一台机器上运行 claude-mem 的开发者,server-beta 是不可见的。首次运行的完整路径:

  1. npx claude-mem install(或升级到 server-beta 可用构建)。
  2. 首次 hook 触发时,server-bootstrap.ts 中的 bootstrapServerApiKey() 自动运行(源码确认其内置的本地团队名常量即 local-hook-team)。它:
    • teams 表中 find-or-create local-hook-team 行;
    • projects 表中 find-or-create local-hook-project 行;
    • 生成 48 字节 url-safe 随机 API Key,sha256 哈希后创建 api_keys 行,scope 限于该 team+project 的 hook 专用权限(events:writesessions:writeobservations:readjobs:read);
    • 把原始 key、project id、server URL 写入 ~/.claude-mem/settings.json,供后续 hook 认证。
  3. server-beta 守护进程启动在UID 派生的端口上:37877 + (uid % 100)。这是 Phase-12 审查修复——此前硬编码 37877,同一台机器两个 profile 会撞端口。
  4. Hook 现在把事件 POST /v1/events 到该本地端口并携带 API Key。从用户视角:上下文仍出现在下一次会话中,search 仍返回相关 observation,viewer 仍可用。

单用户情形就是 "team_id = local-hook-teamproject_id = local-hook-project,你是唯一的 actor_id"。整个多租户模型在这个映射下干净地退化为单租户。

同机多账号:把工作 profile 的 CLAUDE_MEM_DATA_DIR 设为 $HOME/.claude-mem-work。所有路径(DB、settings、pid、端口文件)都由它派生。UID 派生端口加上每用户数据目录,使两个 profile 互不冲突地共存。

7. 多用户模型:身份四元组与纵深防御

一旦跨过"多于一个人或一个服务账号在使用"的边界,基座的真实形状就显露出来。三个身份维度贯穿系统中的每一行

  • team_id × project_id——租户作用域。每个读查询都以此二元组为键。不存在任何能把其他作用域的行返回给未授权调用者的 API 表面。
  • api_key_id——传输身份。认证本次调用的 HTTP key,可吊销,按机器/CI 作业/服务账号粒度管理。审计行为每个动作都记录它。
  • actor_id——语义身份。人类可解读的标识符(human:alice@orgsystem:server-beta-clisystem:ci-runner),API Key 代表其行动。多个 key 可映射到同一 actor(例如工程师在笔记本和工作站上各有一个 key)。
  • request_id——每调用关联 id,在 HTTP 边界铸造,流入 BullMQ payload、worker 日志行、审计行。是支持(support)问题的枢纽。

7.1 requirePostgresServerAuth:每读每写的五步验证

requirePostgresServerAuthpostgres-auth.ts)在每个读/写上做重活:

  1. 对入站 X-API-Key 头(或 Authorization: Bearer …)做哈希;
  2. 按该哈希查找 api_keys 行;
  3. 检查 revoked_atexpires_at,以及 scope 与所需 scope(memories:writememories:read 等)的匹配;
  4. 填充 req.authContext = { apiKeyId, teamId, projectId, scopes, actorId }
  5. 以 401(吊销/未知 key)、403(scope 不足)或 400(格式错误)拒绝——从不静默跳过

Phase 11 又在 worker 侧加了纵深防御。BullMQ payload 携带 team/project,但 worker 不信任 payload——它从 Postgres 重新加载规范的 observation_generation_jobs 行,若 payload.team_id !== canonical.team_id 则拒绝行动(审计为 generation_job.scope_violation)。

7.2 Phase-12 审计链

审计链捕获的动作集合:

  • event.receivedevent.batch_received——每次摄取;
  • session.startsession.end——会话生命周期;
  • generation_job.processinggeneration_job.completedgeneration_job.failed——每次生成;
  • generation_job.retried_by_operatorgeneration_job.cancelled_by_operator——运维动作;
  • generation_job.scope_violationgeneration_job.revoked_key——安全拒绝;
  • api_key.createapi_key.revoke——Key 生命周期;
  • memory.writeobservation.read——直接记忆操作。

每一行都携带 (team_id, project_id, api_key_id, actor_id, request_id)。这就是 SOC2 / ISO 27001 审计所需的链条,呈现为一张可以 join 的 Postgres 表。

7.3 跨租户泄露的显式围栏

  • API 层:任何读/写前先做 scope 检查;
  • Worker 层:规范行 vs payload 的再验证(见 4.2 节源码);
  • CLI 层api-key list 现在是 LIMIT/OFFSET + 可选 --team 过滤(Phase-12 修复);
  • 兼容层resolveServerSession 的 TOCTOU 捕获 23505 唯一约束冲突并重新获取,而不是返回 500(第二轮审查修复)。

8. 团队规模 playbook:同一基座,三种接法

无论规模大小,基座相同;变化的是如何接线 team、project、key 与 search。

8.1 小团队(2–5 人,如初创小队)

拓扑:一个 team,每个 repo 一个 project(monorepo 则一个 project 搞定全部)。

接线

  • 由部署的所有者一次性引导共享 team:claude-mem server api-key create --team <id> --project <id> --scope memories:write,memories:read
  • 每位开发者拥有自己的 API Key(这样吊销可按人进行),actor_id = human:alice@org
  • 所有 hook 写入共享的 (team, project),observation 落入团队池。

Search 变得社会化mem-search "BullMQ stalled jobs" 返回团队中任何处理过它的成员的 observation。无需协调,just works。

入职:新同事的第一个会话就能跑 observation_search 查询,立刻看到团队已经学到什么。时间到生产力(time-to-productivity)下降,因为隐性上下文变成了显性的。

CI:服务 API Key(actor_id = system:ci)为构建失败、部署摘要、测试 flake 检测写入事件。团队 AI 会话可以搜索"这周什么一直在挂",拿到真实答案。

8.2 中型团队(5–50 人,多个 squad)

拓扑:每个 squad 一个 team,每个 service/repo 一个 project,外加一个持有共享基础设施的 "platform" team。

接线

  • 每个 squad 一行 team,squad 内开发者的 key 限定到该 team;
  • 每个 project 的 key 实现更细的访问控制(例如不该写前端团队记忆的 backend 开发);
  • 一个 platform team,其只读 key 可跨多个 project(scopes: ['observations:read']team_id = platformproject_id = NULL 是合法的只读 scope;跨 project 读按 team 过滤);
  • 每个 squad 一个 CI/CD 服务账号,actor_id = system:ci-<squad>

跨 squad 联邦:当 squad A 想知道 squad B 对某个共享依赖学到什么时,"federation key" 可授予跨 team 只读访问,审计链展示这次联邦转移。

可观测性/api/health 的按 team 队列车道。某个 squad 的失控生成成本出现在它自己的车道指标里,而不是 platform 的。

治理:key 通过 claude-mem server api-key revoke + create 轮换。审计链同时记录吊销与新 key 的首次使用;合规团队可以 grep api_key.revoke 事件。

8.3 大型团队(50+,受监管/企业环境)

拓扑:team 即组织单元——engineering、data-platform、security。每个 repo/微服务一个 project。一个联邦 team 提供全组织只读。

接线

  • 每位工程师一个短过期 API Key(由 key 轮换 cron 轮换);
  • 每个 CI 作业、部署 bot、AI agent 各一个服务账号 key;
  • 一个 "compliance" team key,带全组织 observations:readaudit:read scope(后者属未来工作);
  • 多区域 Postgres + Valkey 部署,置于按 team_id 哈希的路由器后;
  • 可观测性栈按区域、按 team 消费 /api/health
  • request_id 流入 SIEM,安全事件可回溯到具体 HTTP 调用及其生成的 AI 会话。

隐私<private> 标签在 hook 层(边缘处理)于内容到达基座前剥离。个人草稿永远不会到达团队基座,更别说组织层。对受监管环境,"默认私有"模式(每条 observation 必须显式 opt-in 才分享)是未来配置。

成本归因:每个生成行都有 team_idmodel_idattempts 与时间戳。夜间作业可以 SUM(duration_ms) + COUNT(*) GROUP BY team_id, model_id 出账单仪表板。

审计驱动的合规:调查者问"我们的 AI 在 A、B 日期之间对客户 X 知道什么?"查询即:按租户 scope 的 observations FTS,join 按 team_id 与日期范围过滤的 audit_logs。传票级就绪。

9. 概念架构:三层松耦合

claude-mem 中的"记忆"是一个写为主的事件日志 + 派生的 observation 视图。架构叠放三层松耦合:

            ┌────────────────────────────────────┐
            │  READ LAYER                        │
            │   /v1/search (FTS GIN)             │
            │   /v1/context (context pack)       │
            │   /v1/observations/:id             │
            │   Chroma vector embeddings         │
            └────────────────────────────────────┘
                       ▲
                       │ derived view
            ┌────────────────────────────────────┐
            │  GENERATION LAYER                  │
            │   ProviderObservationGenerator     │
            │   processGeneratedResponse         │
            │   processSessionSummaryResponse    │
            │   (BullMQ workers, scaled            │
            │    horizontally, decoupled from     │
            │    HTTP latency)                    │
            └────────────────────────────────────┘
                       ▲
                       │ outbox + queue lanes
            ┌────────────────────────────────────┐
            │  CAPTURE LAYER                     │
            │   IngestEventsService              │
            │   EndSessionService                │
            │   compat adapters                  │
            │   (single transactional unit:      │
            │    event row + outbox row + audit)  │
            └────────────────────────────────────┘
  • 捕获是廉价且同步的。一次 hook 触发 = 一个 HTTP 调用、一个事务、三行 INSERT。延迟有界。
  • 生成是异步且水平可扩展的。outbox 模式意味着队列只是传输优化,耐久性住在 Postgres。worker 可任意扩缩而不影响 HTTP 延迟。
  • 读是租户 scope 的 FTS +(未来)向量搜索。tsvector 列上的 GIN 索引为典型负载提供亚 100ms 的搜索。Chroma 插接语义召回。

9.1 两条队列车道

  • Event 车道——逐事件 observation,由 /v1/events 与兼容 sessions/observations 适配器供给。吞吐重,随 worker 并发度扩缩。
  • Summary 车道——会话结束摘要,由 /v1/sessions/:id/end 与兼容 sessions/summarize 适配器供给。量小、payload 大(整个会话上下文)。

SessionGenerationPolicySessionGenerationPolicy.ts)决定进哪条车道、何时进:

  • per-event(默认)——每个事件立即触发一个 event 车道作业;
  • debounce——窗口内的事件经确定性 job id 折叠;delay: <window> 调度后重复 add 实现替换;
  • end-of-session——跳过逐事件作业,只触发会话结束摘要。

策略当前按 team 可配(今天环境变量,明天 team 表)。源码中 buildSummaryJobId / buildSummaryJobPayload 正是 summary 车道确定性 id 与审计完整 payload 的构造点。

9.2 确定性作业 id

buildServerJobId({ kind, team_id, project_id, source_type, source_id }) 生成稳定的 id,BullMQ 对 jobId 强制唯一。从当前源码结构看(job-id.ts),实现把五个字段 JSON 规范化后做 SHA-256 摘要,拼成 ${kindPrefix}_${sha256hex} 的形式——注释特别说明了不能含冒号(BullMQ 内部用 : 作 key 分隔符,嵌入会引发 scan/state 混乱)。这一点与文档中早期示例格式 event:t123:p456:agent_event:e789 不同,但设计目标一致:

  • 重复入队同一逻辑作业在队列侧是 no-op;
  • debounce 通过重复 add 同一 id 并替换延迟 payload 实现;
  • 重试(运维触发或 stalled 恢复)干净碰撞;
  • 对账可以按 id 寻址行,无需维护侧状态。

这个单一设计选择让整个作业生命周期故事无需分布式锁即幂等

9.3 身份三元组

每条审计行、每个 BullMQ payload、每条日志行都包含:

字段 生命周期 用途
api_key_id 经 CLI 或 bootstrap 创建;可吊销 "哪个 key 发起了这次调用?"——安全
actor_id 创建 API Key 时设置 "哪个人/服务?"——分析、归因
request_id 每次调用在 HTTP 边界铸造 "这一次 HTTP 请求的完整生命周期?"——支持、调试
team_id × project_id 内建于 API Key 每个读查询的租户 scope

三元组把"AI 记住了 X"从一个黑箱,变成一条可追踪、可归因、可撤销的声明。

9.4 Provider 抽象

ProviderObservationGenerator.ts 经一个小接口实现 provider 无关(源码中的 ServerGenerationProvider 类型与 generate() → { rawText, modelId, providerLabel, tokensUsed } 契约可在 providers/shared/types.ts 中核对)。当前 provider:Claude(Anthropic SDK)、Gemini(Google Generative AI)、OpenRouter(其网关后的任意模型)。新增一个 provider = 实现一个方法(generate(input))并注册,XML 响应格式与 processGeneratedResponseprocessGeneratedResponse.ts)保持不变。

这是"我们不选赢家"的属性:偏好 Gemini 的成本,或想要 OpenRouter 做 failover 的团队,只需设置 CLAUDE_MEM_SERVER_PROVIDER,基座毫不在意。

9.5 可观测性原语

  • request_id 端到端:一个标识符穿越 HTTP → 审计 → BullMQ payload → worker 日志行 → 完成审计。支持枢纽查询即 SELECT * FROM audit_logs WHERE request_id = '<uuid>' ORDER BY created_at
  • 按车道队列指标/api/health/v1/info 按车道返回 { waiting, active, completed, failed, delayed, stalled }。对 Grafana 仪表板或 Kubernetes HPA 够用。
  • 按作业生命周期事件observation_generation_job_events 记录每次状态迁移,带 attemptdetailsevent_type。审计 + 生命周期两张表合起来可重建任何作业的完整历史。
  • Stalled 事件去重:近期的 ServerJobQueue 审查修复使 stalled jobId 只计一次,尽管 BullMQ 同时经 worker.on('stalled')QueueEvents 'stalled' 两个通道上报。

10. 开发者体验走查(五个工作日)

Day one(单用户)npx claude-mem install。打开 Claude Code,敲代码。observation 被捕获。几个会话后,search 返回相关历史上下文。没有需要额外学习的东西。

Day one(团队)。team admin 对项目根的 docker-compose.yml 执行 docker compose up -d。为每位开发者签发 API Key:

POSTGRES_USER=… POSTGRES_PASSWORD=… POSTGRES_DB=… docker compose exec claude-mem-server \
  bun /opt/claude-mem/scripts/server-service.cjs server api-key create \
    --team <team_id> --project <project_id> \
    --scope events:write,sessions:write,observations:read,jobs:read \
    --name alice-laptop

输出是带原始 key 的 JSON。每位开发者把它贴进自己 ~/.claude-mem/settings.jsonCLAUDE_MEM_SERVER_BETA_API_KEY。完成。他们照常使用 Claude Code;hook 现在写入团队基座。

Day two — 运维路径。某作业卡在 processing

claude-mem server jobs list --team <team_id> --status processing
claude-mem server jobs retry <job_id>     # if cancelled or failed
claude-mem server jobs cancel <job_id>    # active jobs ride out their lifecycle

retry 端点在 Phase-12 + 第 4 轮审查修复后对所有状态都安全:queued 上 no-op,processing 上 409,completed 上 409(否则因 LLM 非确定性会重复 observation),failed/cancelled 上重置+重新入队。

Day three — 调试慢查询。开发者问"这为什么花了 30 秒?"。他们从 HTTP 响应中拿到 request_id,然后进 Postgres:

SELECT created_at, action, details
FROM audit_logs
WHERE request_id = '<uuid>'
ORDER BY created_at;

返回完整生命周期:event.received(HTTP 边界)、generation_job.processing(worker 锁住行)、generation_job.completed(worker 完成,含 model_id 与 duration)。枢纽完成。

Day four — 测试自动化。想写"POST 事件、期望 observation 生成"的测试。有了 wait=true 轮询修复,一次调用即可:

curl -X POST 'http://server:37877/v1/events?wait=true' \
  -H 'X-API-Key: cmem_…' \
  -d '{ "projectId": "<id>", "eventType": "test", "occurredAtEpoch": 0, "sourceType": "api" }'
# returns: { event: {…}, generationJob: { status: "completed", … } }

无轮询循环、无竞态。端点阻塞直到 outbox 行到达终态或 30s 上限(超限返回 waitTimedOut: true)。

Day five — MCP。队友经 MCP 用 Cursor,以 generate: false 调用 observation_record_event 工具(只想记录纯元数据事件、不为生成付费)。第 2 轮审查修复后,该标志真正流向 REST 端点的 ?generate=false,而不是被静默丢弃(对应 IngestEventsService.tsgenerate 为 false 时跳过 outbox 写入、直接返回 'skipped' 的分支)。

11. 开发者获得了什么

浓缩清单:

  • 团队 scope 的跨会话记忆。"Sarah 上周四解决了这个"可以搜到。
  • 多账号隔离。同一台 Mac 两个 profile,无端口冲突。
  • 读后即写语义?wait=true 真的在等。
  • Provider 无关生成。一个环境变量从 Anthropic 切到 Gemini。
  • 合规级审计。每个动作可归因到 (api_key_id, actor_id, request_id, team_id, project_id)。
  • 运维表面。retry、cancel、list、分页。
  • 默认隐私<private> 标签在边缘剥离。
  • 水平扩展--scale claude-mem-worker=N
  • 崩溃安全持久化reconcileOnStartup 恢复在途行。
  • 租户纵深防御。HTTP 层认证、worker 层 scope 检查、存储层 ON CONFLICT、每次拒绝都审计。
  • 身份锚定的建议。未来:AI 建议可携带"基于 actor Y 在 Z 时间的 observation X",因为基座已经知道。

基座本身就是产品表面。以上全部由已合入的代码解锁。

12. "It just works" 理念的团队化延伸

claude-mem 原始承诺:装一次、正常干活、记忆作为副作用获得。团队模式必须保持同样的承诺——否则任何降级都会因为"得说服每个工程师 opt in"而让采用率停滞。

server-beta 通过保持 hook 契约完全一致来刻意保留这一点:

  • 相同的 hook 脚本(hooks.json);
  • 相同的 MCP 工具(observation_record_eventobservation_searchobservation_context);
  • 相同的 viewer UI 端口与表面;
  • 相同的 search skill 行为。

变化的是基座,而基座变化对调用点的开发者不可见。team admin 一次性搭好部署;其他人照旧使用 claude-mem。

正是这一属性让"在上面叠产品"成为可能:

  • 浮现上下文中的自动归因。当队友的 observation 出现在你的上下文里,基座已知其 actor_id 作者。呈现"这来自 Alice 三天前的会话"是 UI 变化,不是基座变化。
  • 陈旧记忆检测。新 observation 与旧的冲突(同 source_id、不同 content)时,数据模型可以标记。无需新摄取管道。
  • 实时活动流。订阅按 team_id 过滤的 audit_logs,投影成 SSE 流到 Slack、Linear 或 sidecar 仪表板。
  • 信任标签。AI 浮现的每条建议都可携带"已验证 N 次观察"或"单次观察、低置信度",因为基座统计引用。
  • 成本仪表板SUM(duration_ms) GROUP BY team_id, model_id 一条查询;chargeback 离一个 cron 作业之远。
  • 审计即服务。"展示 api_key X 在 A、B 日期间生成的所有 observation"一条查询。合规报告变得平凡。

模式:基座建模一切;产品只是其上的薄视图。

13. 基座之上可以构建的产品

以下产品想法从基座中自然长出,无需新基础设施:

  1. 记忆信息流(Memory feeds):Slack bot 订阅 audit_logs WHERE action = 'memory.write' AND team_id = …,每天向 squad 频道投递新 observation 摘要。零捕获工作——AI 在正常会话中作为副作用就在做。
  2. PR 感知 AI 评审:PR 打开时,服务账号用 diff 的文件路径与函数名查询 /v1/search,AI 评审员把"团队此前关于这段代码的推理"作为 PR 评论浮现。diff 上下文 + 团队记忆提升评审质量,无需任何显式知识整理。
  3. 入职陪伴(Onboarding companion):新同事第一个会话里,observation_search "how does authentication work" 返回团队真实经历的解答——包含踩过的 bug、走过的死胡同、结构背后的 why——而不是一份过期的 README。
  4. 陈旧上下文告警:当 observation 超过 N 周且其源文件此后被改动过,在搜索结果中标记为可能陈旧。数据模型已有 agent_events.created_at 与 payload 元数据中的源文件路径。
  5. 跨项目联邦:一个 "platform" team key 带 observations:read 跨多 project scope。平台工程师看到依赖方的记忆,而依赖方什么都不用做。
  6. 成本仪表板:按 team、按 model 的 token 支出。对 observation_generation_jobs join audit_logs 一条 SQL;建一个 Grafana 面板,发布。
  7. 合规报告:"展示 api_key_id <X> 在 A–B 日期间为 team <T> 创建的所有 observation"。一条查询即达传票级。
  8. AI agent 记忆市场:开源 observation 包——"React 19 模式"、"Postgres 性能"、"AWS CDK 坑"。可作为只读 team_id 命名空间挂载到任何部署,精选内容且保留归因。
  9. 隐私优先的综合(synthesis):跨团队成员聚合 observation。<private> 在边缘剥离,所以个人草稿永不越界,但提炼出的教训可以。基座的双层隐私(按内容标签 + 按租户 scope)使其安全。
  10. 跨团队学习传播:一个 team 的安全事件。observation 传播服务把相关 observation 复制到安全 team 的空间,审计链展示跨 team 转移(及其授权的 api_key_id)。
  11. 语音转记忆站会 bot:每日站会 bot 问"什么在挡你的路?"。回答成为正确 project 上的一条 observation,actor_id = human:<engineer>。周末 AI 跨团队汇总阻塞——它们已经在同一个记忆池里,供代码建议使用。
  12. 自我书写的文档:按 kind = 'decision'kind = 'architecture' 过滤 observation,自动生成 ADR(架构决策记录),作者归因来自 actor_id、时间戳来自 created_at。基座在当下捕获推理,薄转换层稍后渲染成文档。
  13. AI 建议信任链:每条 observation 已有 (api_key_id, actor_id, model_id, request_id)。浮现层可展示"此建议基于 Alice 的 N 条 + Bob 的 M 条 observation,模型 claude-3-5-sonnet,14 天内生成"。完全可审计的 AI 来源。
  14. 多模态记忆:今天捕获层是带文本 payload 的 hook 事件。明天:IDE 截图(payload 中 PNG 字节)、语音转录(音频 + 文字)、终端录像。基座的 payload jsonb 列容纳一切;source_type 可延伸。

所有想法的统一属性:开发者什么都不用做不同的。捕获层不可见,基座处理 scope 与身份,产品读取同一个 /v1 表面。这就是 "it just works",放大到团队。

14. 为什么团队开发特别需要持久化共享记忆

这是上述一切背后更深的 "why",值得单独一节。

隐性知识缺口:大部分工程知识靠口口相传——PR 评论、1:1、会过期的 Slack 线程。AI agent 放大使用者,但只在本地。资深工程师对代码库的心智模型在他们休假、离职、或换个项目两周之后并不留存。server-beta 让该心智模型可被寻址:他们的会话写入团队可搜索的 observation。

入职不对称:新同事要数周才能跑起来,一半是在重新发现已经做过的决定。有了共享记忆,"为什么这个 service 选 Postgres 而不是 SQLite"返回的是当时做选择时的真实推理,而不是事后有人补的文档。

代码评审疲劳:资深工程师反复解释同样的模式。每一句"我们这里不这么做,因为 X"都是候选 observation。一旦被捕获,下次给其他工程师的 AI 建议就能携带该约束——且有归因,所以可解释。

部落知识流失:人会离开。他们的 git commit 留下,推理却随人而去。共享 observation 把 "why" 和 "what" 一起捕获。工程师离开后,其 actor_id 还在浮现上下文中出现数月——他们的推理延续。

AI 平权:重度使用 AI 工具的工程师积累个人上下文并复利增长;不用的人落后。共享记忆部分地拉平这一点——每个人都受益于每个人的 AI 使用(这是"每个人受益于一个人的测试"的团队开发平行版)。

跨服务理解:微服务架构把知识割裂到各 repo。按 project 的 observation + 团队 scope 搜索让 backend 工程师能拉取"前端团队对这个 auth 流程知道什么",无需跨文档边界。

事件响应:每篇 postmortem 都以"我们记下来"结尾,而几乎没人真的写。Observation 自动捕获诊断过程——包括死胡同;文档几乎从不包含死胡同,但它们对未来的调查者最有价值。

通过归因建立信任:团队抗拒"AI 往共享存储里写东西"的根源是怕垃圾数据。server-beta 的审计链(api_key_id + actor_id + request_id + model_id + scope-violation 拒绝)让每条 observation 可回溯到某个人的具体会话、某次模型运行、某次 provider 调用。你可以吊销 key、审计会话、向合规证明"是的,AI 在 Z 时间因为 Y 知道 X"。可审计性是信任的前提。

复利效应:10 人团队,每人每天约 5 条 observation,一个月 1000+ 条。六个月后,团队的集体 AI 记忆含 6000+ 条结构化、有归因、可搜索的洞察——比多数团队的书面文档语料更大。"每个人的 AI 使用反哺每个人的 AI 使用"的复利,长期看是这里最重要的属性。

15. 诚实的边界与未解问题

基座是丰富的,但表面尚不完整。刻意尚未构建的:

  • 跨 team 联邦 UX:基座支持;但设置只读跨 team key 的一等 CLI/UI 尚不存在。
  • 默认私有模式<private> 标签依赖用户自觉。team 模式的默认私有(opt-in 才分享)反转信任模型,受监管环境大概应该存在。
  • 成本归因表面:数据在;账单仪表板不在。
  • 陈旧 observation 检测:从数据模型看显然可行;服务未接线。
  • observation 合并/取代 UX:同一 source 上的两条 observation 都可以是有效的。合并、取代、矛盾的工具有待未来。
  • 搜索排序调优:FTS 对精确词处理良好。团队 scope 的 ranker(recency × authorship × 主题相关性加权)仍开放。
  • 地理复制:今天是单区域。多区域需要唯一幂等键上的冲突解决。
  • Worker 自动扩缩docker compose --scale 手动可用;基于队列深度的 Kubernetes HPA 需要一个尚不存在的 Prom exporter(指标表面本身已有——/api/health)。
  • Provider 故障切换CLAUDE_MEM_SERVER_PROVIDER 单值。"换 provider 重试"是 ProviderObservationGenerator 之上的一个小 wrapper。
  • 在线 schema 迁移bootstrapServerBetaPostgresSchema 在启动时运行。线上部署需要正经的迁移工具。
  • 既有的遗留测试失败:遗留 worker 路径有 7 个测试保持 skip/fail——非 server-beta 引入,推迟到后续处理。

这些是有明确范围的工单,不是架构阻塞。基座的形状是对的;产品与打磨是下一步。

16. 代码索引(便于导航)

文档中引用的代码,按层组织:

结语

server-beta 的工作是隐形。solo 开发者永远不会知道它在那里——他们的 hook 继续照常工作。团队采用它——他们的 AI 会话开始跨人、跨服务、跨机器共享上下文,无需任何人学习新工具。组织部署它——审计链与租户 scope 变成合规原语。三种情形下基座相同,只有接线不同。

claude-mem 最初的信条是自我书写的记忆。server-beta 把它延伸为自我书写的记忆,为所有人。做这件事的基础设施已经合入。有意思的工作——信息流、信任标签、联邦 UX、市场包、成本仪表板、语音捕获、多模态 payload——全部坐在一个已经成形、随时可以承接的基座之上,只隔一层。

登录后查看全文
热门项目推荐
相关项目推荐