claude-mem Issue 2341 可靠性切片:安装标记兼容、导出契约与待处理队列 Schema 的修复方案详解
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 只做两件事:
- install/startup contract(安装与启动契约):对应 Phase 1 的安装标记(install marker)兼容性修复;
- 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_pid、retry_count、failed_at_epoch、completed_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):包含
version、bun、uv、installedAt四个字段; - 遗留格式(plain text):仅包含一个版本字符串,例如
12.4.4或v12.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.ts 从 CLAUDE_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.ts 对 memorySessionIds 的入参强转做了系统性覆盖,可以据此理解该接口的完整容错面:
- 接受原生字符串数组:
{ 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_pid、retry_count、failed_at_epoch、completed_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 COLUMN(SessionStore.ts); worker_pid列及其索引idx_pending_messages_worker_pid同样会被显式DROP掉(SessionStore.ts)。
这正是计划文档中"不得重新引入 worker_pid"守卫条款的底层依据:当前 claim 查询不再使用该列,Schema 侧随之收敛。
回归测试设计
计划文档为 Phase 3 指定了两条回归测试:
- 构造一个"声明旧迁移已应用、但
tool_use_id缺失"的数据库——即schema_versions声称 pending-message 相关迁移已完成,而pending_messages.tool_use_id实际不存在。断言:构造SessionStore时仍然必须补上缺失的列,因为当前的 enqueue SQL 依赖它。 - 断言全新数据库形状不要求
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.ts、version-check.js、export-memories.ts、DataRoutes.ts 与 SessionStore.ts——都有对应的测试文件锚定行为,使这类高置信度小痛点可以被快速、安全地消除。
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 StartedRust0627
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