首页
/ claude-mem:把基于 Postgres 的 server 运行时封装为可嵌入的 cmem-sdk,以及 `server-beta` → `server` 改名全路径

claude-mem:把基于 Postgres 的 server 运行时封装为可嵌入的 cmem-sdk,以及 `server-beta` → `server` 改名全路径

2026-09-06 16:02:15作者:仰钰奇

本文基于 claude-mem 仓库中的实施计划 plans/2026-05-25-cmem-sdk-and-server-rename.md 展开:它定义了 claude-mem/sdk 这一可导入库形态的完整设计——剥离 HTTP/daemon/Redis 外壳后,如何在纯 Node 进程内完成"事件捕获 → AI 压缩生成 → Chroma 语义检索 + FTS 兜底"全链路,并给出了从 server-betaserver 的改名方案与持久化值兼容策略。读完本文,你可以掌握该 SDK 的分层架构、每个 Phase 的具体 API 契约、禁用依赖清单与验证方法,并能对照当前仓库源码确认哪些部分已经落地。

一、执行决策:SDK 不是新系统,是既有 server 运行时的"去壳"组合

计划开篇给出了一条硬性定性:

cmem-sdk is not a new system.

它就是把既有的进程内 server 运行时(src/server/* + src/storage/postgres/* + 既有的 src/services/sync/* Chroma 引擎)暴露为一个可导入的库,并剥掉 HTTP/daemon/Redis 外壳。SDK 需要的一切能力都已存在,且核心部分本来就是无 daemon 的;SDK 的工作是"组合 + 打包",外加一次谨慎的改名。

计划给出的目标对象图如下(消费方应用视角):

consumer app
  └─ import { createCmemClient } from 'claude-mem/sdk'
       ├─ Postgres (pg)         ← system of record (capture, observations, sessions, jobs)  [src/storage/postgres/*]
       ├─ in-process generation ← provider.generate() (fetch) → parseAgentXml → processGeneratedResponse  [src/server/generation/*]
       ├─ Chroma (REQUIRED)      ← semantic index over the SAME observations, via uvx chroma-mcp subprocess  [src/services/sync/*]
       └─ search                ← Chroma semantic (primary). Postgres FTS exists only as a runtime safety net when Chroma transiently fails — it is NOT a feature toggle.  [src/storage/postgres/observations.ts]

这里有三个必须理解的约束:

  1. Chroma 不是可选的。 计划明确:没有语义检索的 claude-mem 是坏的——observations 无法以用户真实的方式被检索。因此 createCmemClient(...) 在构造时必须初始化并验证 Chroma;若 uvx chroma-mcp 子进程起不来,构造函数直接 reject。Postgres FTS 路径只保留为一个运行时安全网(镜像 SearchManager 中"Chroma 瞬死时降级"的韧性模式),使用时大声打日志,且不作为用户可配置模式暴露
  2. 必须排除在外的一切: Express、BullMQ、ioredis/Redis、better-auth、HTTP 路由、daemon/pidfile 生命周期、worker 的 bun:sqlite 存储、Claude Code 子进程生成路径。这些全是可复用核心外面包的"壳"。
  3. 要研究的接线中枢createServerService()(计划写作时为 createServerBetaService(),改名后即 src/server/runtime/create-server-service.ts 中的 createServerService)。它已经构建出 SDK 想要的完整对象图(pool → schema bootstrap → repositories),再挂上 SDK 不要的部分(HTTP 服务、队列管理器、生成 worker)。SDK 复现的是"图",不是"服务"。

对照当前仓库源码可以看到该中枢的实际结构:createServerService(options) 依次执行环境校验 → loadServerMode()(启动期加载 code 模式,加载失败立即抛出)→ 取 pool(options.pool ?? getSharedPostgresPool({requireDatabaseUrl:true}))→ initializePostgres(pool)(幂等 bootstrap)→ 构建 queueManager 与 generationWorkerManager → 组装 ServerServiceGraphnew ServerService({graph})。见 create-server-service.ts。计划指出 SDK 要复制的正是"pool → bootstrap → repositories"这一段,而丢弃 queueManagergenerationWorkerManagerServerService 外壳。

术语决策(继承并强制执行)

  • 领域对象叫 observation,永不叫 "memory"。保留 observationsobservation_sourcesPostgresObservationRepository/v1/observations;/v1/memoriesmemory_* MCP 工具只是别名。
  • 运行时叫 server(本计划起),永不叫 "server-beta";worker 仍是遗留 SQLite 运行时。
  • 公开客户端叫 CmemClient,由 createCmemClient(...) 构造,从 claude-mem/sdk 导入。

二、Phase 0:文档发现——先读透接线中枢,再谈实现

计划要求在执行前先完整阅读源码并固定一份"允许使用的 API"清单,禁止凭印象发明或扩展 API。以下是该清单中与 SDK 直接相关的部分(均摘自计划的 verbatim 签名,路径为当前仓库实际位置):

连接 / 启动(src/storage/postgres/):

  • parsePostgresConfig(options?): PostgresConfig | null(config.ts)——只读 CLAUDE_MEM_SERVER_DATABASE_URL 这一个连接变量,外加 pool/SSL 调优参数;
  • createPostgresPool(config: PostgresConfig): PostgresPoolgetSharedPostgresPool(options?: {requireDatabaseUrl?})(pool.ts);
  • bootstrapServerPostgresSchema(client): Promise<void>(schema.ts)——幂等的进程内迁移 runner,不装扩展、不用 pgvector。observations 表 DDL 含 content TEXTcontent_search TSVECTOR GENERATED ALWAYSembedding JSONB(可空且从未被读取),GIN 索引用于 FTS;
  • createPostgresStorageRepositories(client): PostgresStorageRepositories(index.ts)——一次返回全部 repository;
  • withPostgresTransaction<T>(pool, fn);PostgresQueryable = { query(text, values?) },pg.Poolpg.PoolClient 都满足该接口。

捕获:

  • new IngestEventsService({ pool, resolveEventQueue: () => null }) 后调 ingestOne(input, { generate })(IngestEventsService)——resolveEventQueue 返回 null 使 BullMQ 入队变成 no-op(enqueueState='queued_only'),这是 SDK 不碰 Redis 的关键;
  • PostgresAgentEventsRepository.create(input: CreatePostgresAgentEventInput)(agent-events.ts)。

生成(进程内,无 BullMQ):

  • new ClaudeObservationProvider({ apiKey, model? })(或 Gemini/OpenRouter 兄弟实现)——构造函数接受 {apiKey, model?, maxOutputTokens?, fetchImpl?},用fetch 请求 https://api.anthropic.com/v1/messages,不依赖 @anthropic-ai/claude-agent-sdk、不起子进程;
  • PostgresObservationGenerationJobRepository.transitionStatus({ id, projectId, teamId, status:'processing', lockedBy })——状态机只允许合法迁移:queued→processing→completed;queued→completed 直接抛错;
  • provider.generate({ job, events, project }, signal?)(ServerGenerationContext = {job, events, project:{projectId,teamId,serverSessionId,projectName}},结果为 {rawText, modelId?, providerLabel, tokensUsed?});
  • processGeneratedResponse({ pool, job, rawText, modelId, providerLabel, ... })processSessionSummaryResponse(...)(processGeneratedResponse.ts)——包一层 withPostgresTransaction,经 parseAgentXml 解析,写入 observations + observation_sources,完成 job。从源码结构看,该路径从不写 embedding——语义索引完全交给 Chroma;
  • parseAgentXml(raw, correlationId?)(src/sdk/parser.ts)——注意:它内部调用 ModeManager.getInstance().getActiveMode()无回退,没有激活模式会抛错,因此 SDK 必须在构造客户端时初始化模式(见 Phase 5)。

检索:

  • PostgresObservationRepository.search({ projectId, teamId, query, limit? })(observations.ts)——FTS 通过 websearch_to_tsquery + ts_rank 实现,这是 Postgres 侧唯一的搜索手段;
  • getByIdForScope({ id, projectId, teamId }) / listByProject(...)

Chroma(必需的语义层,复用而非分叉):

  • ChromaMcpManager.getInstance() + callTool('chroma_add_documents' | 'chroma_query_documents' | 'chroma_create_collection' | 'chroma_delete_documents', args)(ChromaMcpManager)——单例,负责拉起 uvx chroma-mcp 子进程,本地 all-MiniLM 嵌入,不需要任何 API key;
  • new ChromaSync(project) + queryChroma(...) + close()(ChromaSync);collection 命名沿用 cm__<project> 约定;其中 addDocuments(ChromaDocument[]) 这一"文档层"是存储无关的可复用接缝,而 syncObservation(observationId:number, ...) 是 SQLite 形状(整型 id),SDK 不得复用它处理 Postgres 的 UUID。

反模式清单(计划存在的原因:这些坑已经踩过)

  1. 不要"建混合体""适配""迁移""分叉"任何东西——每个引擎都已存在,SDK 是胶水 + 打包;任务描述里出现 "new system" 或 "reimplement" 即是错的。
  2. 不要加 pgvector / vector 列 / embeddings API 调用。Postgres 语义检索不存在也不在范围内,语义检索由既有 Chroma 引擎交付;FTS 是 Postgres 侧搜索。
  3. 不要把 Express、BullMQ、ioredis、better-auth、React、bun:sqlite 拉进 SDK bundle,用构建期 import guard 强制执行。
  4. 不要调 transitionStatus(queued → completed)——它抛错;必须先 queued → processing
  5. 不要在无激活模式时调 parseAgentXml
  6. 不要盲目字符串替换改名——持久化值(迁移表名、job 枚举字符串、用户 settings.json 键、CLAUDE_MEM_RUNTIME 取值)需要双向兼容;只有代码标识符可以随意改。
  7. 不要只靠 grep 碎片拼全局认知——完整读接线中枢与组合根。

三、Phase 1:改名 server-betaserver(基础 + 回归修复)

改名放在 SDK 之前,原因有二:SDK 建立在 server 运行时之上,应该带着干净的命名出厂;且它本身就可独立交付——单独修复"静默回退"回归。

1a. 修复回归(价值最高、改动最小)

问题根源在运行时选择器:此前 CLAUDE_MEM_RUNTIME 必须精确等于 'server-beta',否则 hook 会静默回退到 worker,且回退不易察觉。修复要求:

  • 运行时选择器同时接受 'server'(规范值)与 'server-beta'(向后兼容);
  • 服务端环境校验接受两种字面量;
  • 设置键新增 CLAUDE_MEM_SERVER_{URL,API_KEY,PROJECT_ID},先读新键、再回退旧键(CLAUDE_MEM_SERVER_BETA_*),保证既有 settings.json 继续工作。

验证标准:CLAUDE_MEM_RUNTIME=server + CLAUDE_MEM_SERVER_DATABASE_URL 已设时,hook 解析到 server 运行时而非 worker;并补一个对 'server''server-beta' 都断言 selectRuntime()==='server' 的单测。

这一部分在当前仓库中已经落地,可直接对照阅读:runtime-selector.tsselectRuntime() 对两个字面量都返回 'server',并有注释"Phase 1a (cmem-sdk rename): the canonical runtime value is 'server'";buildServerContextpickFirstNonEmpty 先读 CLAUDE_MEM_SERVER_URL 再回退 CLAUDE_MEM_SERVER_BETA_URL(API key、project id 同理);对应单测在 tests/hooks/runtime-selector.test.ts。服务端侧的 validateServerEnv 同样接受 serverserver-beta 两种取值(见该文件 L93 注释)。

1b. 代码标识符改名(安全、机械)

约 80 个 ServerBeta* / serverBeta* / SERVER_BETA_* 代码符号(类、类型、接口、变量、非持久化常量)改为 Server* / server* / SERVER_*,例如:

旧名 新名
ServerBetaService ServerService
createServerBetaService createServerService
ServerBetaClient ServerClient
ActiveServerBetaQueueManager ActiveServerQueueManager
ServerBetaServiceGraph ServerServiceGraph
bootstrapServerBetaPostgresSchema bootstrapServerPostgresSchema
SERVER_BETA_POSTGRES_SCHEMA_VERSION SERVER_POSTGRES_SCHEMA_VERSION

验证:npm run typecheck 通过;rg -i 'serverbeta' 在代码标识符中为 0。当前仓库可以确认这一步已执行:src/server/runtime/ 目录下已只有 ServerService.tsActiveServerQueueManager.tsActiveServerGenerationWorkerManager.tscreate-server-service.ts 等新命名文件,bootstrapServerPostgresSchemaSERVER_POSTGRES_SCHEMA_VERSION 也已是新名。

1c. 文件与构建/分派目标改名

源文件整体重命名(create-server-service.tsServerService.tsserver-client.tsserver-bootstrap.tsActiveServer*.tsscripts/e2e-server-docker.shdocs/server-*.md),构建目标 server-beta-serviceserver-service(产物 plugin/scripts/server-service.cjs),worker-service 中的 CLI 分派函数改名为 runServerServiceCli 并查找 server-service.cjs;server <cmd> 子命令名保持不变。验证:npm run build 产出 plugin/scripts/server-service.cjs,claude-mem server status 分派正确。当前仓库中 package.jsone2e:server:docker 脚本已指向 bash scripts/e2e-server-docker.sh,与计划一致;并保留"若已安装插件缓存中仍有 server-beta-service.cjs 则回退查找"的升级期保护。

1d. 持久化值——向后兼容(危险区)

这是改名中最容易把线上数据弄坏的部分,计划逐项给出策略:

  • 迁移表 server_beta_schema_migrations:在 bootstrapServerPostgresSchema 顶部加一条幂等、带保护的 ALTER TABLE IF EXISTS server_beta_schema_migrations RENAME TO server_schema_migrations;,再 CREATE ... IF NOT EXISTS,并更新读取语句。(零风险替代方案:物理表名不动,只改 TS 常量。)
  • job 的 job_type/source_type 枚举字符串(server_beta_generate_eventserver_beta_generate_summaryserver_beta_generate_event_batchserver_beta_reindexserver_beta_observation_request):写入时发 server_*,读取/匹配时两种都接受,配一个小的 normalize 助手。
  • settings 键 / runtime 值:由 1a 的"先新后旧"读取覆盖;安装器今后写新键 + CLAUDE_MEM_RUNTIME=server

验证:打开改名前的既有 Postgres 库,bootstrap 干净跑通、迁移行保留、无重复表;带旧键的既有 settings.json 仍能解析到 server 运行时;排队的旧式 server_beta_generate_event job 仍能被处理。反模式护栏:绝不 DROP 或重建有数据的表;绝不在不做双接受的情况下改列里存历史枚举值。

当前仓库印证了该阶段的中间状态:create-server-service.ts L198-L202ServerServiceGraph.runtime 仍写死持久化字面量 'server-beta',注释明确"Phase 1d will migrate this value. The TS identifiers above are now Server*; the wire/storage value remains 'server-beta' for back-compat";而 initializePostgres 仍在读 server_beta_schema_migrations 表。也就是说,代码标识符已新、存储值仍旧,正是计划刻意选择的兼容姿态。

四、Phase 2:SDK 包骨架 + 构建 + export 接线

计划写作时,package.json 曾留下一个"空槽":exports["."] → dist/index.jsexports["./sdk"] → dist/sdk/index.js,但从未有 src/index.tssrc/sdk/index.ts,也没有构建步骤产出它们(构建脚本 scripts/build-hooks.js 只产出 dist/npx-cli + dist/opencode-plugin,且已有 bun:sqlite import guard 先例)。当前仓库快照中,package.jsonexports 仅有 "./modes/*",src/index.tssrc/sdk/index.ts 均不存在,src/sdk/ 下只有既有的 parser.tsprompts.tshardened-options.tsoutput-classifier.ts——即该导出槽仍处于"占位未兑现"状态,这正是计划要补齐的内容:

  • 新建 src/sdk/index.ts 作为公开入口(重导出 createCmemClientCmemClient 与公开类型);既有的 parser.ts/prompts.ts 留在原处内部复用;
  • 新建 src/index.ts(. 导出),重导出 SDK 面,保持最小;
  • 增加真正产出 JS + .d.ts 的构建:方案 A 用 tsconfig.sdk.json(继承根配置、rootDir: srcoutDir: distdeclaration: truetypes: ["node"]——去掉 "bun",include 仅限 SDK 传递依赖源);方案 B 引入 tsup(devDep),entries 为 src/index.ts + src/sdk/index.ts,format: esmdts: trueplatform: node。新增 "build:sdk" 脚本并接入 buildprepublishOnly;
  • 核对 package.jsonexports 映射并确认 files 字段携带 dist

验证清单:

  • npm run build 产出 dist/sdk/index.js dist/sdk/index.d.ts;
  • 在干净的 node 项目里 import { createCmemClient } from 'claude-mem/sdk' 能解析且类型可加载;
  • Import guard:构建/测试步骤 grep SDK bundle(或其解析后的 import 图),一旦出现 expressbullmqioredisbetter-authreactbun:sqlite 即失败。

反模式护栏:不要对全仓库做 tsc 产物输出(会拖进 worker 与 bun:sqlite);SDK 构建只圈定自己的入口。不要给 SDK 加 @anthropic-ai/claude-agent-sdk 依赖——server 生成 provider 用 fetch

五、Phase 3–5:连接与租户、捕获 API、进程内生成 API

Phase 3:核心——连接、schema bootstrap、repositories、tenancy

create-server-service 的组合段复制对象图(去掉 service/queue/worker 部分):

createCmemClient(options),options = { databaseUrl?, pool?, teamId?, projectId?, provider?, chroma?: ChromaOptions }。注意:chroma 只用于调参(collection 前缀、MCP 路径等),不能用于禁用——不存在 enabled: false 开关。

  • Pool:options.pool ?? createPostgresPool(parsePostgresConfig({ env: { CLAUDE_MEM_SERVER_DATABASE_URL: options.databaseUrl ?? process.env... } })!),或直接用 getSharedPostgresPool;
  • await bootstrapServerPostgresSchema(pool)(幂等);
  • repos = createPostgresStorageRepositories(pool);
  • Chroma 必需:chromaSync = new ChromaSync(projectId);await chromaSync.ensureReady()(或首次 addDocuments/queryChroma 调用)。uvx chroma-mcp 起不来时,createCmemClient 以清晰错误 REJECT——SDK 绝不返回一个半残的客户端。

Tenancy 自举:Postgres 每次调用都要求 teamId + projectId,而 ProjectsRepository 没有按名查找(只有 getByIdForTeam)。因此:

  • options.teamId/projectId 已给 → 直接用;
  • 否则 ensureDefaults():创建一次默认 team(teams.create({name:'default'}))+ 默认 project(projects.create({teamId, name: options.projectName ?? 'default'})),并把 ID 持久化到 SDK 本地状态文件(如 $CLAUDE_MEM_DATA_DIR/sdk-tenant.json),后续运行复用。文档要注明:生产消费者应显式传 ID。

反模式护栏:不要要求 Redis/bullmq 环境变量(validateServerEnv 的 Docker 检查是给 HTTP 容器的,SDK 永不调用它);不要给 repository 发明 getProjectByName——持久化 ID 即可。

Phase 4:捕获 API

  • client.capture(event) / client.captureBatch(events) 包装 new IngestEventsService({ pool, resolveEventQueue: () => null }).ingestOne(input, { generate: false })——写一条 agent_event + 一行 queued 状态的 generation-job outbox,无 Redis;
  • SDK 友好事件形 → CreatePostgresAgentEventInput 的映射:{ projectId, teamId, serverSessionId?, sourceAdapter, sourceEventId?, eventType, payload, occurredAt };
  • 可选经 PostgresServerSessionsRepository 暴露 client.startSession()/endSession() 做分组。

验证:capture(...) 之后,该租户恰好一行 agent_events + 一行 observation_generation_jobs(状态 queued),且未尝试任何 Redis 连接。

Phase 5:生成/压缩 API(进程内,无 worker)

复现 ProviderObservationGenerator 中"内联核心"部分(ProviderObservationGenerator.ts),跳过 BullMQ 仪式段:

  • Provider:options.provider → 实例化 ClaudeObservationProvider({apiKey, model?})(或 Gemini/OpenRouter),或复用 buildServerGenerationProviderFromEnv() 的 env 驱动逻辑——当前仓库中该函数支持 claude/anthropic(读 ANTHROPIC_API_KEYCLAUDE_MEM_ANTHROPIC_API_KEY)、geminiopenrouter(支持可选 base URL)三种,见 create-server-service.ts L256-L282;
  • parseAgentXml 保证激活模式:构造客户端时用默认模式初始化 ModeManager(或用 prompt-builder 的回退语义);
  • client.generate(jobOrEventId) 的四步:
    1. job = transitionStatus({ id, projectId, teamId, status:'processing', lockedBy:'sdk' })(强制 queued→processing);
    2. 加载 events(agentEvents.getByIdForScope/listByProject)+ project(projects.getByIdForTeam);
    3. result = await provider.generate({ job, events, project:{ projectId, teamId, serverSessionId, projectName } });
    4. await processGeneratedResponse({ pool, job, rawText: result.rawText, modelId: result.modelId, providerLabel: result.providerLabel, sourceAdapter:'sdk' })
  • 便捷方法:client.captureAndGenerate(event) = Phase 4 + 5 的顺序组合。

验证:captureAndGenerate(...) 产出一行 observations,其 metadata 携带 {title,subtitle,facts,narrative,concepts,files_*},job 终结于 completed,observation_sources 把它关联回源 agent_event

六、Phase 6:搜索——Chroma 语义为主 + FTS 运行时安全网 + context

再次强调:Chroma 是必需的。下面的纯 FTS 分支只用于镜像 SearchManager 的"捕获异常、降级一次"行为,应对 Chroma 瞬死——它不是特性开关,不是可配置关闭的路径,并且会发出响亮的 logger.error,让坏状态可见。

实现要点:

  • client.search({ query, limit }) 镜像 SearchManager.search 的分支逻辑,但作用于 Postgres:
    • 默认路径 → queryChroma(query, limit, whereFilter) → 返回按语义距离排序的 observation UUID → 经 observations.getByIdForScope/批量水合;
    • 空查询路径 → PostgresObservationRepository.listByProject(...)(没有语义意图可表达);
    • 仅在 Chroma 运行时失败时(不是配置问题):回退 PostgresObservationRepository.search({projectId, teamId, query, limit})(FTS),记 logger.error('CHROMA', 'semantic search failed; returning degraded FTS results — investigate uvx chroma-mcp', err),并在响应中带 { degraded: true },让调用方自行决定重试或让本方请求失败。
  • client.context({ query, limit }) = 执行 searchresults.map(o => o.content).join('\n\n')(镜像服务端 V1 路由的 context 端点语义)。
  • Chroma↔Postgres 胶水(全计划中唯一真正的新代码,保持最小):复用存储无关的文档层,不要复用 SQLite 形状的 syncObservation——在 observation 落库时(Phase 5)即时索引:构造 ChromaDocument { id: observation.id /*UUID 字符串*/, document: observation.content, metadata: { projectId, teamId, kind, serverSessionId } },走既有 chroma_add_documents 路径(经 ChromaMcpManager.callTool,或把 addDocumentsprivate 重构成可复用接缝);collection 名沿用 cm__<projectId> 约定。写时同步意味着不涉及 SQLite 的 backfill/watermark 路径(ChromaSyncState/整型 ID 留在 SQLite 侧)。

验证矩阵:

  • 构造期 Chroma 必需:故意让 uvx/chroma-mcp 不可用,createCmemClient(...) 必须 REJECT(不存在静默 FTS-only 模式);
  • 快乐路径:createCmemClient + captureAndGenerate + search('semantic query') 返回按语义距离排序、已水合的 Postgres observations;
  • 运行时失败路径:成功 search 后杀掉 chroma-mcp,再 search——结果带 { degraded: true } 返回,发出 logger.error('CHROMA', …),而随后的一次冷启动 createCmemClient 应 REJECT;
  • context(...) 返回 { observations, context }(内容以 \n\n 连接),且底层 search 降级时同样上浮 { degraded: true }

反模式护栏:不加 pgvector;不复用整型 id 的 syncObservation;不要求 embeddings API key(Chroma 本地嵌入);不加 chroma.enabled = false 选项(那会重新引入被明确拒绝的"静默坏掉"状态)。

七、Phase 7–9:公开门面、测试与最终验证

Phase 7(门面与类型):CmemClient 统一暴露 capturecaptureBatchgeneratecaptureAndGeneratesearchcontextstartSessionendSessionclose()(关 pool + 关 Chroma);公开类型保持小而稳,重导出 PostgresObservation、捕获输入类型、检索/上下文类型及相关 src/core/schemas 的 Zod 类型。close() 必须 await chromaSync?.close(),且仅当 SDK 自己创建的 pool 才执行 closePostgresPool。验证:一个端到端测试在 Postgres 测试库上跑通 createCmemClient → captureAndGenerate → search → context → close。反模式:无 HTTP 服务、无 pidfile、无 process.exit、无 daemon。

Phase 8(测试 + 无 worker 示例应用 + 文档):

  • 对 Postgres 测试库做单元/集成测试(复用改名后的 scripts/e2e-server-docker.sh docker 脚手架),覆盖:schema bootstrap 幂等、capture、内联生成、FTS 搜索、Chroma 降级、tenancy 自举;
  • examples/sdk-node/——一个纯 Node(不是 Bun)脚本,导入 claude-mem/sdk,指向 CLAUDE_MEM_SERVER_DATABASE_URL,在没有 worker/daemon 运行的条件下跑 capture → generate → search。这是"标题要求"的实证;
  • 文档:在 docs/public/ 加一页 "Using claude-mem in your app (SDK)" 并更新导航。

验证:npm test 全绿;示例在 node 下运行(无 Bun),打印生成的 observations 与检索命中,期间没有任何 worker 进程存活。

Phase 9(最终验证):

  1. 改名完整且安全:rg -i 'server[-_]?beta' 只剩刻意保留的持久化字面量(记录在 1d)与 changelog/历史计划文件;npm run typecheck + npm test 全绿;CLAUDE_MEM_RUNTIME=server 能到达 Postgres(1a 的回归测试);
  2. SDK 无禁用依赖:自动 guard 确认 claude-mem/sdk 的 import 图不含 expressbullmqioredisbetter-authreactbun:sqlite@anthropic-ai/claude-agent-sdk;
  3. 导出为真:dist/index.jsdist/index.d.tsdist/sdk/index.jsdist/sdk/index.d.ts 构建后全部存在,且可从外部项目解析;
  4. 无发明 API:grep SDK 确认没有 pgvector/vector(/embedding 写入;生成走 fetch provider 而非 agent SDK;parseAgentXml 总在激活模式下被调用;
  5. 标题要求达成:Phase 8 示例证明"纯 Node、进程内、无 HTTP worker"下的完整 capture → 压缩 → 语义+FTS 检索链路。

八、遗留问题与修正日志:计划本身如何被纠偏

计划尾部保留了两个未决执行决策与一条重要修正记录,对读者理解"计划文档如何演进"很有价值:

  • tsup vs tsconfig.sdk.json(Phase 2):执行时二选一——tsup 一步出 JS+dts,tsconfig 方案避免新增 devDep;
  • Chroma addDocuments 的暴露方式(Phase 6):把 private addDocuments 重构成可复用接缝,还是 SDK 直接调 ChromaMcpManager.callTool('chroma_add_documents')。倾向选择"保持 chroma-mcp 协议单一路径的最小改动"。

修正日志(2026-05-29)记录了这次计划最关键的自我纠偏:原稿把 Chroma 表述为"可选",这是错的——没有语义检索的 claude-mem 就是坏的。修正后:架构图与执行决策标注 Chroma 为 REQUIRED;createCmemClient 选项去掉布尔关闭开关,ChromaOptions 只负责调参;Phase 6 默认路径是 Chroma,FTS 仅在瞬态失败时作为运行时安全网出现(上浮 { degraded: true } 并打 logger.error),不是特性开关;验证新增"Chroma 不可用时构造函数必须 REJECT";反模式新增"禁止 chroma.enabled = false 选项"。

九、小结:如何读这份计划与当前仓库

plans/2026-05-25-cmem-sdk-and-server-rename.md 当作两份文档来读是最高效的:

  1. 改名工程(Phase 1):当前仓库已完成 1a/1b/1c——runtime-selector.ts 双字面量接受、新键优先旧键回退、src/server/runtime/src/services/hooks/server-client.tsserver-bootstrap.ts 等新文件均已就位;1d 处于计划预期的中间态:createServerService 的 graph 里持久化字面量仍为 'server-beta'(见 create-server-service.ts L198-L202),server_beta_schema_migrations 表名亦被刻意保留。理解"TS 标识符已新、存储值仍旧"的双轨状态,是看懂该仓库 server 侧代码的关键。
  2. SDK 化工程(Phase 2–9):当前快照中 src/sdk/ 只有既有的 parser.ts/prompts.ts(已被计划列为复用件),尚无 src/sdk/index.ts 公开入口,package.json 也暂未兑现 . / ./sdk 导出——也就是说,SDK 的"导出槽"仍是计划要兑现的核心目标,读者可对照 Phase 2 的验证清单跟踪其后续落地。

整体来看,这份计划的方法论价值在于:先用 Phase 0 的 verbatim API 清单与反模式清单锁死边界,再按"回归修复 → 机械改名 → 危险持久化值"的风险递增顺序推进改名,最后以"对象图复制 + 最小胶水"的方式把既有 Postgres 运行时产品化为库——每一步都有明确的验证命令与反模式护栏,是一套可直接借鉴的"存量运行时 SDK 化"实施模板。

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