首页
/ claude-mem Issue 2341 可靠性切片:安装标记兼容、导出契约与待处理队列 Schema 的修复方案详解

claude-mem Issue 2341 可靠性切片:安装标记兼容、导出契约与待处理队列 Schema 的修复方案详解

2026-09-06 13:12:39作者:齐冠琰

claude-mem 的 Issue 2341 是一次"整合问题分诊"后的第一个落地 PR 计划,它刻意不追求清空整个缺陷积压,而是从"安装/启动契约"和"数据库/导出契约"两个桶里摘出几个高置信度的小痛点(paper cuts)集中修复。本文以 issue-2341-reliability-slice.md 计划文档为主体,逐阶段讲解其范围划定、反模式守卫、三个实现阶段与验证方式,并结合仓库中的真实源码与测试印证每个契约修复点,帮助读者掌握 claude-mem 如何管理安装标记格式、数据目录环境变量覆盖和 SQLite 待处理消息队列的 Schema 演进。

计划定位与总体范围

计划文档开篇即给出范围约束:

Scope: first PR from the consolidated issue triage. This PR should not try to solve the full backlog. It should remove a few high-confidence paper cuts from the first two buckets: install/startup contract and DB/export contract.

也就是说,本 PR 只做两件事:

  1. install/startup contract(安装与启动契约):对应 Phase 1 的安装标记(install marker)兼容性修复;
  2. DB/export contract(数据库与导出契约):对应 Phase 2 的导出脚本契约修复与 Phase 3 的待处理队列 Schema 守卫。

这种"切片式"(slice)的缺陷修复方式避免了大 PR 带来的审查与回归风险,下面逐阶段展开。

Phase 0:文档发现——允许触及的 API 与反模式守卫

计划文档的 Phase 0 先枚举了"允许使用的 API 与模式",相当于为本次修复划定了最小变更面:

  • 安装标记辅助函数位于 setup-runtime.ts,现有测试在 setup-runtime.test.ts
  • 运行时启动告警逻辑位于 version-check.js,它目前先解析 CLAUDE_PLUGIN_ROOT,再回退到脚本所在目录来定位插件根目录;
  • 导出脚本通过 SettingsDefaultsManager.loadFromFile 读取 worker 配置,且必须尊重 CLAUDE_MEM_DATA_DIR,因为共享路径辅助函数与设置默认值管理器已经暴露了该环境变量覆盖;
  • /api/sdk-sessions/batch 注册于 DataRoutes.ts,请求体期望字段是 memorySessionIds,现有强转(coercion)测试在 data-routes-coercion.test.ts
  • 当前的 PendingMessageStore 写入并读取 tool_use_id,但不再读取 worker_pidretry_countfailed_at_epochcompleted_at_epoch。文档强调了一个关键原则:当前的 Schema 守卫应当匹配今天实际运行的代码,而不是旧的迁移意图

与之配套的是四条"反模式守卫"(Anti-pattern guards),明确划出禁止项:

反模式守卫 含义
不得在 pending_messages 中重新引入 worker_pid 除非当前 claim 查询真正又开始使用它
不得仅依赖 schema_versions 表来判断当前 SQL 引用的列是否存在 必须按实际列存在性做兜底
不得新增第三种安装标记格式 读取时兼容旧的纯文本标记与当前 JSON 标记,但写入只写 JSON 标记
不得让 export-memories.ts 在设置了 CLAUDE_MEM_DATA_DIR 时回退到 ~/.claude-mem 环境变量必须被尊重

这些守卫条款在后续各阶段的实现与测试中反复出现,是本文理解整个切片的最重要线索。

Phase 1:安装标记(install marker)兼容性

标记文件的两种形态

安装标记文件固定为安装目录下的 .install-version。在当前仓库 setup-runtime.ts 中,markerPath() 返回 join(targetDir, '.install-version')。标记存在两种形态:

  • 当前规范格式(JSON):包含 versionbunuvinstalledAt 四个字段;
  • 遗留格式(plain text):仅包含一个版本字符串,例如 12.4.4v12.4.4

writeInstallMarker()setup-runtime.ts 中写入的正是规范 JSON 格式:

export function writeInstallMarker(
  targetDir: string,
  version: string,
  bunVersion: string,
  uvVersion: string,
): void {
  const payload: MarkerSchema = {
    version,
    bun: bunVersion,
    uv: uvVersion,
    installedAt: new Date().toISOString(),
  };
  writeFileSync(markerPath(targetDir), JSON.stringify(payload));
}

计划文档明确要求:保持 writeInstallMarker() 不变,新安装永远写入规范的 JSON schema;兼容工作全部放在读取侧。

读取侧如何兼容遗留标记

readInstallMarker()setup-runtime.ts)的解析策略是"先 JSON、后纯文本":

export function readInstallMarker(targetDir: string): MarkerSchema | null {
  const path = markerPath(targetDir);
  if (!existsSync(path)) return null;
  const content = readFileSync(path, 'utf-8');
  try {
    const marker = JSON.parse(content);
    if (marker && typeof marker === 'object' && typeof marker.version === 'string') {
      return marker as MarkerSchema;
    }
  } catch {
    // Legacy installs wrote only the version string as plain text.
  }

  const legacyVersion = content.trim();
  if (LEGACY_VERSION_MARKER_RE.test(legacyVersion)) {
    return { version: legacyVersion.replace(/^v/i, '') };
  }

  return null;
}

其中遗留格式由一个严格的语义化版本号正则(setup-runtime.ts)识别:

const LEGACY_VERSION_MARKER_RE =
  /^v?\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;

该正则要求完整的 major.minor.patch 结构,允许 -prerelease+build 后缀,且对可选的前导 v 做归一化(replace(/^v/i, ''))。只有内容整体匹配才被视为遗留版本标记,任意其他无法解析的内容都会返回 null,从而触发上层"标记不可读"的升级提示,而不是产生错误的版本比较。

插件侧 version-check 的同构解析

计划文档指出 plugin/scripts/version-check.js 也要接受同样的遗留标记形态。查看 version-check.js,其 readInstallMarkerVersion()setup-runtime.ts 采用了完全同构的逻辑——相同的 LEGACY_VERSION_MARKER_RE、相同的"先 JSON 后纯文本"结构:

function readInstallMarkerVersion(markerPath) {
  const content = readFileSync(markerPath, 'utf-8');
  try {
    const marker = JSON.parse(content);
    return marker && typeof marker === 'object' && typeof marker.version === 'string'
      ? marker.version
      : null;
  } catch {
    const legacyVersion = content.trim();
    return LEGACY_VERSION_MARKER_RE.test(legacyVersion)
      ? legacyVersion.replace(/^v/i, '')
      : null;
  }
}

version-check.js 运行在插件 Setup hook 中:先通过 resolveRoot()version-check.js)按"CLAUDE_PLUGIN_ROOT 环境变量 → 脚本目录向上回退"的顺序定位插件根目录,再对比标记版本与 package.json 中的 pkg.version,版本不一致或标记不可读时通过 emitUpgradeHint() 输出 npx claude-mem@latest install 的升级提示。这里还体现了计划文档 Phase 0 中提到的"解析插件根目录"的行为来源。

验证方式

计划文档为 Phase 1 指定的验证步骤:

  • tests/setup-runtime.test.ts 中为纯文本 .install-version 增加覆盖;
  • plugin/scripts/version-check.js 增加针对性测试,或扩展现有的插件脚本测试;
  • 运行 bun test tests/setup-runtime.test.ts

在当前的 setup-runtime.test.ts 中可以看到对应的遗留标记测试用例:

it('returns parsed marker when file is a legacy plain-text version', () => {
  writeFileSync(join(tempDir, '.install-version'), '12.4.4\n');
  const marker = readInstallMarker(tempDir);
  expect(marker).toEqual({ version: '12.4.4' });
});

it('normalizes a leading v in legacy plain-text versions', () => {
  writeFileSync(join(tempDir, '.install-version'), 'v12.4.4\n');
  const marker = readInstallMarker(tempDir);
  expect(marker).toEqual({ version: '12.4.4' });
});

另有 isInstallCurrent() 相关的用例验证:遗留纯文本标记即使版本匹配,也会因为缺少 bun 版本字段而判定为"非最新"(setup-runtime.test.ts)——这正呼应了反模式守卫"只读兼容、只写 JSON"的设计意图:旧标记可读,但任何一次 npx claude-mem install 都会把标记升级为规范的 JSON 形态。

Phase 2:导出脚本契约修复

settings.json 路径必须尊重 CLAUDE_MEM_DATA_DIR

计划文档要求 export-memories.tsCLAUDE_MEM_DATA_DIR/settings.json 读取配置,而不是永远使用 ~/.claude-mem/settings.json。当前仓库的实现(export-memories.ts)为:

export async function exportMemories(query: string, outputFile: string, project?: string) {
  const settings = SettingsDefaultsManager.loadFromFile(join(resolveDataDir(), 'settings.json'));
  const port = parseWorkerPort(settings.CLAUDE_MEM_WORKER_PORT);
  const baseUrl = `http://localhost:${port}`;

即:resolveDataDir() 返回数据目录(CLAUDE_MEM_DATA_DIR 设置时即该环境变量指向的目录),再拼接 settings.json,交给 SettingsDefaultsManager.loadFromFile()(实现位于 SettingsDefaultsManager.ts)完成默认值补齐与环境变量覆盖。parseWorkerPort() 还负责对 CLAUDE_MEM_WORKER_PORT 做严格校验(1–65535 的整数且不允许尾部噪声字符),保证导出脚本只会连接合法的 worker 端口。

/api/sdk-sessions/batch 的字段更名

计划文档的第二项修复是把 /api/sdk-sessions/batch 请求体字段从 sdkSessionIds 改为 memorySessionIds,并"可选地"允许 DataRoutes 保留 sdkSessionIds 作为兼容别名,但脚本中优先使用规范字段。

当前 export-memories.ts 的调用方式:

const memorySessionIds = new Set<string>();
observations.forEach((o) => {
  if (o.memory_session_id) memorySessionIds.add(o.memory_session_id);
});
summaries.forEach((s) => {
  if (s.memory_session_id) memorySessionIds.add(s.memory_session_id);
});

const sessionsResponse = await fetchWithTimeout(`${baseUrl}/api/sdk-sessions/batch`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ memorySessionIds: Array.from(memorySessionIds) })
});

而服务端在 DataRoutes.ts 中以 zod schema 定义 memorySessionIds 字段(sdkSessionsBatchSchema),并通过 validateBody 中间件在 app.post('/api/sdk-sessions/batch', ...) 注册处做请求体校验,处理函数再从 req.body 中解构 memorySessionIds 交给 store.getSdkSessionsBySessionIds()。字段名与数据库中的 memory_session_id 概念一一对应,消除了 sdk 前缀带来的历史歧义。

强转(coercion)行为与验证

data-routes-coercion.test.tsmemorySessionIds 的入参强转做了系统性覆盖,可以据此理解该接口的完整容错面:

  • 接受原生字符串数组:{ memorySessionIds: ['abc', 'def'] }
  • 接受 JSON 编码的字符串数组并强转:'["abc","def"]'['abc', 'def']
  • 接受逗号分隔字符串并强转:'abc,def'['abc', 'def']
  • 对逗号分隔值做 trim:'abc, def , ghi'['abc', 'def', 'ghi']
  • 拒绝非数组、非字符串的值:{ memorySessionIds: 42 } → HTTP 400。

计划文档给出的 Phase 2 验证方式:

  • 围绕 SDK-session batch 路由的别名/强转增加或更新测试;
  • 若可行则加脚本级测试;否则用 grep 确认 scripts/export-memories.ts 不再发送 sdkSessionIds、不再硬编码 homedir(), '.claude-mem'
  • 运行聚焦的路由/导出测试。

这条"grep 兜底"的验证方式很实用:对于难以直接单测的独立脚本,用静态检索锁定契约字段,配合路由侧的单测即可闭环。

Phase 3:当前待处理队列(pending_messages)形状守卫

问题本质:schema_versions 不能替代真实列检查

计划文档在 Phase 0 已经点出:PendingMessageStore 当前写入并读取 tool_use_id,但不再读取 worker_pidretry_countfailed_at_epochcompleted_at_epoch;"当前 Schema 守卫应当匹配今天实际运行的代码,而不是旧的迁移意图"。

SessionStore.ts 中的迁移逻辑印证了这一原则——多处使用 PRAGMA table_info(pending_messages) 直接探测列的实际存在性,而不是信任 schema_versions 中声称的迁移状态:

  • tool_use_id 缺失时,当前建表路径会执行 ALTER TABLE pending_messages ADD COLUMN tool_use_id TEXT,随后做按 tool_use_id 去重并创建 (session_db_id, tool_use_id) 上的唯一索引(SessionStore.ts);
  • 历史迁移中引入的 failed_at_epoch 等列在确认不再被读取后会被清理:对 status NOT IN ('pending', 'processing') 的行先删数据,再 DROP COLUMNSessionStore.ts);
  • worker_pid 列及其索引 idx_pending_messages_worker_pid 同样会被显式 DROP 掉(SessionStore.ts)。

这正是计划文档中"不得重新引入 worker_pid"守卫条款的底层依据:当前 claim 查询不再使用该列,Schema 侧随之收敛。

回归测试设计

计划文档为 Phase 3 指定了两条回归测试:

  1. 构造一个"声明旧迁移已应用、但 tool_use_id 缺失"的数据库——即 schema_versions 声称 pending-message 相关迁移已完成,而 pending_messages.tool_use_id 实际不存在。断言:构造 SessionStore 时仍然必须补上缺失的列,因为当前的 enqueue SQL 依赖它。
  2. 断言全新数据库形状不要求 worker_pid——因为当前 claim 查询不使用它。

并附一条处理原则:如果测试暴露了真实的"源码/Schema 不匹配",应当更新文档与 Schema 注释以匹配当前代码,而不是重新引入无用的列。

验证方式:

  • 运行 SessionStore / PendingMessageStore 的聚焦 SQLite 测试;
  • 在 TypeScript 中 grep 活跃的 worker_pid 读取,确认该列是否仍是"当前必需列"。

相关迁移与列行为的测试基线可参考 session-store-migrations.test.ts,运维侧对队列的清理入口则见 clear-pending-queue.ts

最终验证清单

计划文档收尾给出了 PR 级别的最终验证清单,值得作为同类"契约修复 PR"的通用检查单:

  • 运行本 PR 改动过的聚焦测试(focused tests);
  • 依赖可用时运行 npm run typecheck:root
  • 运行 git diff --check 检查空白与冲突标记;
  • 针对上游默认分支开非 draft 的 PR;
  • 未经用户明确批准,不得合并、发布或交付(Do not merge, release, or ship without explicit user approval)。

小结

Issue 2341 的可靠性切片展示了 claude-mem 维护"契约一致性"的一套方法:先做 Phase 0 式的文档发现,把允许触及的 API、既有测试与反模式守卫一次性写死;再按"安装标记兼容(读兼容、写规范)→ 导出脚本契约(尊重 CLAUDE_MEM_DATA_DIR、字段更名 memorySessionIds)→ 队列 Schema 守卫(以真实列探测替代 schema_versions 信任)"三个阶段小步推进;每个阶段都配套"代码变更 + 聚焦测试 + 明确的验证命令"。所有关键落点——setup-runtime.tsversion-check.jsexport-memories.tsDataRoutes.tsSessionStore.ts——都有对应的测试文件锚定行为,使这类高置信度小痛点可以被快速、安全地消除。

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

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.13 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.8 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
529
593
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
915
1.83 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.58 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.35 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.01 K
515
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
388