claude-mem server-beta 架构解析:从单机 SQLite 到 Postgres + BullMQ 的多租户共享记忆运行时
本文基于仓库中的架构长文档 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.ts、generation-jobs.ts、server-sessions.ts、auth.ts、observations.ts)以及 ServerJobQueue BullMQ 封装(ServerJobQueue.ts)。PR #2383 在其上构建了全部上层能力:
| Phase | 交付物 | 关键文件 |
|---|---|---|
| 4 | 事件→作业流水线(事务性 outbox + 摄取服务) | IngestEventsService.ts、outbox 封装 |
| 5 | Provider observation 生成器(Claude / Gemini / OpenRouter) | ProviderObservationGenerator.ts、providers/ |
| 6 | 独立的 server session 语义 + 三策略调度 | server-sessions.ts、SessionGenerationPolicy.ts |
| 7 | Hooks 经 HTTP 路由(不再依赖 worker) | runtime-selector.ts、server-client.ts、server-bootstrap.ts |
| 8 | 由 /v1/* 内核支撑的独立 MCP server |
mcp-server.ts |
| 9 | 遗留 worker payload 的兼容适配器 | SessionsObservationsAdapter.ts、SessionsSummarizeAdapter.ts |
| 10 | Docker 栈——分进程可部署 | docker-compose.yml、Dockerfile、e2e-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/retry、POST /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的静态/动态导入混用;MCPobservation_record_event中generate标志被忽略;markGenerationFailed的jsonb_set空值保护。 - Minor 级:debounce 默认值的 NaN 归并 bug;
docker-compose.yml中硬编码的 Postgres 凭据;无限制的api-key list查询(跨租户泄露);wait=true并未真正等待;endSession破坏updated_at幂等性;硬编码的37877server-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 中有逐行对应的实现。源码证实了几个关键设计点:
- 事务性 outbox:
ingestOne()在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 "兼容层是薄翻译器而非平行实现" 这一反模式守卫的代码体现。 - payload 持久化的意义:outbox 行上的
payload字段保存的就是规范化 BullMQ payload。注释写明:reconciliation(启动时对账)与运维重试都依赖这个持久化 payload 来重新入队,且必须能通过 worker 端的assertServerGenerationJobPayload校验。 - 提交后才发布:事务提交后
publishEventJob()才把作业加入 BullMQ 队列,返回值是'enqueued' | 'queued_only' | 'skipped'三态——队列不可用时行保持queued,等待启动对账重新发布。这意味着耐久性住在 Postgres 里,队列只是传输优化。
4.2 Worker 生成侧:从不信任 payload
BullMQ 把作业投递到 ProviderObservationGenerator.ts 的 process() 后(并行或稍后,取决于 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_at、expires_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 中携带correlationId、requestId、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 UI(viewer.html),读取
/api/health的队列车道与/v1读端点的记忆列表; - mem-search skill(mem-search/),无论运行时如何都调用
/v1/search; - 遗留兼容垫片,把旧的
POST /api/sessions/observations与/api/sessions/summarizepayload 翻译成与 canonical/v1/*路由相同的IngestEventsService/EndSessionService调用。
最后一点很重要:任何针对遗留 worker 写的客户端都能通过兼容适配器继续工作,无需重写。兼容层是薄翻译器,不是平行实现——反模式被守卫进同一个共享服务。
从当前源码看,ServerV1PostgresRoutes.ts 中注册的端点比文档表格更丰富,还包括 /v1/keys(签发)、/v1/connect、/v1/usage、GET /v1/jobs 及按 team/project 维度的作业查询、DELETE /v1/memories/:id、DELETE /v1/projects/:projectId/memory(数据删除)以及 /v1/mcp(POST/GET,MCP 通道复用 HTTP 表面)等。
6. 单用户模型:server-beta 的"隐形"
对于在一台机器上运行 claude-mem 的开发者,server-beta 是不可见的。首次运行的完整路径:
npx claude-mem install(或升级到 server-beta 可用构建)。- 首次 hook 触发时,server-bootstrap.ts 中的
bootstrapServerApiKey()自动运行(源码确认其内置的本地团队名常量即local-hook-team)。它:- 在
teams表中 find-or-createlocal-hook-team行; - 在
projects表中 find-or-createlocal-hook-project行; - 生成 48 字节 url-safe 随机 API Key,sha256 哈希后创建
api_keys行,scope 限于该 team+project 的 hook 专用权限(events:write、sessions:write、observations:read、jobs:read); - 把原始 key、project id、server URL 写入
~/.claude-mem/settings.json,供后续 hook 认证。
- 在
- server-beta 守护进程启动在UID 派生的端口上:
37877 + (uid % 100)。这是 Phase-12 审查修复——此前硬编码37877,同一台机器两个 profile 会撞端口。 - Hook 现在把事件
POST /v1/events到该本地端口并携带 API Key。从用户视角:上下文仍出现在下一次会话中,search 仍返回相关 observation,viewer 仍可用。
单用户情形就是 "team_id = local-hook-team,project_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@org、system:server-beta-cli、system:ci-runner),API Key 代表其行动。多个 key 可映射到同一 actor(例如工程师在笔记本和工作站上各有一个 key)。request_id——每调用关联 id,在 HTTP 边界铸造,流入 BullMQ payload、worker 日志行、审计行。是支持(support)问题的枢纽。
7.1 requirePostgresServerAuth:每读每写的五步验证
requirePostgresServerAuth(postgres-auth.ts)在每个读/写上做重活:
- 对入站
X-API-Key头(或Authorization: Bearer …)做哈希; - 按该哈希查找
api_keys行; - 检查
revoked_at、expires_at,以及 scope 与所需 scope(memories:write、memories:read等)的匹配; - 填充
req.authContext = { apiKeyId, teamId, projectId, scopes, actorId }; - 以 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.received、event.batch_received——每次摄取;session.start、session.end——会话生命周期;generation_job.processing、generation_job.completed、generation_job.failed——每次生成;generation_job.retried_by_operator、generation_job.cancelled_by_operator——运维动作;generation_job.scope_violation、generation_job.revoked_key——安全拒绝;api_key.create、api_key.revoke——Key 生命周期;memory.write、observation.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 = platform、project_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:read与audit:readscope(后者属未来工作); - 多区域 Postgres + Valkey 部署,置于按
team_id哈希的路由器后; - 可观测性栈按区域、按 team 消费
/api/health; request_id流入 SIEM,安全事件可回溯到具体 HTTP 调用及其生成的 AI 会话。
隐私:<private> 标签在 hook 层(边缘处理)于内容到达基座前剥离。个人草稿永远不会到达团队基座,更别说组织层。对受监管环境,"默认私有"模式(每条 observation 必须显式 opt-in 才分享)是未来配置。
成本归因:每个生成行都有 team_id、model_id、attempts 与时间戳。夜间作业可以 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 大(整个会话上下文)。
SessionGenerationPolicy(SessionGenerationPolicy.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 响应格式与 processGeneratedResponse(processGeneratedResponse.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记录每次状态迁移,带attempt、details、event_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.json 的 CLAUDE_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.ts 中 generate 为 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_event、observation_search、observation_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. 基座之上可以构建的产品
以下产品想法从基座中自然长出,无需新基础设施:
- 记忆信息流(Memory feeds):Slack bot 订阅
audit_logs WHERE action = 'memory.write' AND team_id = …,每天向 squad 频道投递新 observation 摘要。零捕获工作——AI 在正常会话中作为副作用就在做。 - PR 感知 AI 评审:PR 打开时,服务账号用 diff 的文件路径与函数名查询
/v1/search,AI 评审员把"团队此前关于这段代码的推理"作为 PR 评论浮现。diff 上下文 + 团队记忆提升评审质量,无需任何显式知识整理。 - 入职陪伴(Onboarding companion):新同事第一个会话里,
observation_search"how does authentication work" 返回团队真实经历的解答——包含踩过的 bug、走过的死胡同、结构背后的 why——而不是一份过期的 README。 - 陈旧上下文告警:当 observation 超过 N 周且其源文件此后被改动过,在搜索结果中标记为可能陈旧。数据模型已有
agent_events.created_at与 payload 元数据中的源文件路径。 - 跨项目联邦:一个 "platform" team key 带
observations:read跨多 project scope。平台工程师看到依赖方的记忆,而依赖方什么都不用做。 - 成本仪表板:按 team、按 model 的 token 支出。对
observation_generation_jobsjoinaudit_logs一条 SQL;建一个 Grafana 面板,发布。 - 合规报告:"展示
api_key_id <X>在 A–B 日期间为team <T>创建的所有 observation"。一条查询即达传票级。 - AI agent 记忆市场:开源 observation 包——"React 19 模式"、"Postgres 性能"、"AWS CDK 坑"。可作为只读
team_id命名空间挂载到任何部署,精选内容且保留归因。 - 隐私优先的综合(synthesis):跨团队成员聚合 observation。
<private>在边缘剥离,所以个人草稿永不越界,但提炼出的教训可以。基座的双层隐私(按内容标签 + 按租户 scope)使其安全。 - 跨团队学习传播:一个 team 的安全事件。observation 传播服务把相关 observation 复制到安全 team 的空间,审计链展示跨 team 转移(及其授权的
api_key_id)。 - 语音转记忆站会 bot:每日站会 bot 问"什么在挡你的路?"。回答成为正确 project 上的一条 observation,
actor_id = human:<engineer>。周末 AI 跨团队汇总阻塞——它们已经在同一个记忆池里,供代码建议使用。 - 自我书写的文档:按
kind = 'decision'或kind = 'architecture'过滤 observation,自动生成 ADR(架构决策记录),作者归因来自actor_id、时间戳来自created_at。基座在当下捕获推理,薄转换层稍后渲染成文档。 - AI 建议信任链:每条 observation 已有
(api_key_id, actor_id, model_id, request_id)。浮现层可展示"此建议基于 Alice 的 N 条 + Bob 的 M 条 observation,模型 claude-3-5-sonnet,14 天内生成"。完全可审计的 AI 来源。 - 多模态记忆:今天捕获层是带文本 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. 代码索引(便于导航)
文档中引用的代码,按层组织:
- 捕获层:IngestEventsService.ts、EndSessionService.ts、ServerJobQueue.ts、SessionGenerationPolicy.ts(
buildSummaryJobId、buildSummaryJobPayload) - 生成层:ProviderObservationGenerator.ts(
process、lockOutbox)、processGeneratedResponse.ts、providers/(claude / gemini / openrouter / shared) - 存储:schema.ts、agent-events.ts、generation-jobs.ts、observations.ts、server-sessions.ts、auth.ts
- HTTP 表面:ServerV1PostgresRoutes.ts、postgres-auth.ts、request-id.ts、ServerService.ts
- 兼容层:SessionsObservationsAdapter.ts、SessionsSummarizeAdapter.ts
- Hook 路由:runtime-selector.ts、server-client.ts、server-bootstrap.ts
- MCP:mcp-server.ts
- CLI:server-jobs.ts、ServerService.ts(
runServerApiKeyCli、runServerCli) - 队列:ServerJobQueue.ts、job-id.ts、types.ts
- 部署:docker-compose.yml、Dockerfile、e2e-server-docker.sh
- 测试:tests/server/、tests/compat/、tests/hooks/、tests/cli/、tests/servers/
- 相关文档:server-parity-map.md、server-release-readiness.md、server.md、api.md
结语
server-beta 的工作是隐形。solo 开发者永远不会知道它在那里——他们的 hook 继续照常工作。团队采用它——他们的 AI 会话开始跨人、跨服务、跨机器共享上下文,无需任何人学习新工具。组织部署它——审计链与租户 scope 变成合规原语。三种情形下基座相同,只有接线不同。
claude-mem 最初的信条是自我书写的记忆。server-beta 把它延伸为自我书写的记忆,为所有人。做这件事的基础设施已经合入。有意思的工作——信息流、信任标签、联邦 UX、市场包、成本仪表板、语音捕获、多模态 payload——全部坐在一个已经成形、随时可以承接的基座之上,只隔一层。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00