claude-mem 项目边界与会话完整性:从 parent/basename 项目命名到待处理队列竞态修复
这篇指南围绕 claude-mem 的问题分诊手册中 Phase 05 展开:解决项目身份使用 basename(cwd) 导致的跨项目记忆串扰,以及异步观测(observation)流水线中"会话提前终结、观测重复入库、待处理队列无界增长"三类数据完整性问题。读完本文,你将理解项目命名从裸 basename 升级为 parent/basename 的动机与边界条件、存量数据的一次性迁移策略,以及 SQLite 层如何用唯一索引把内容去重从 30 秒时间窗收紧到会话级,并能对照当前仓库源码验证这些修复的实际落地情况。
问题背景:两个相关的数据完整性缺陷
Phase 05 的手册(Phase-05-Project-Scoping-And-Session-Integrity.md)指出,claude-mem 作为"跨会话持久上下文"产品,其核心承诺是:未来会话能注入过去会话中压缩后的相关记忆。一旦下列任一问题发生,这条承诺即被破坏:
- 项目身份冲突(关联 5 个 issue):项目名取自
basename(cwd),两个目录名相同的无关项目(例如~/work/myapp和~/playground/myapp)会共享同一项目名,记忆相互污染; - 异步观测流水线竞态(关联 6 个 issue):会话可能在观测消息仍积压在待处理队列中时就被提前终结(summary 已生成、会话标记完成),导致观测静默丢失;并发处理还会产生内容哈希相同的重复观测;worker 过载时
pending_messages表还会无界增长。
手册给出的修复范围包括五项工程任务加测试与构建验证,下面逐项展开。
升级项目身份:从 basename 到 parent/basename
手册要求把项目标识从 basename(cwd) 统一为 parent/basename 形式。任务拆解为:
- 通读 src/utils/project-name.ts 中的
getProjectName(cwd),理解其当前返回值; - 对照 src/shared/paths.ts 中
getCurrentProjectName()的既有实现——手册指出它已返回basename(dirname(gitRoot))/basename(gitRoot)的抗冲突形式; - 用
grep -r "getProjectName"与对getCurrentProjectName()的搜索找出全部调用方,然后把getProjectName()收敛为唯一入口,与getCurrentProjectName()行为对齐; - 处理边界情况:Windows 盘符根目录(返回
drive-C)、home 目录(返回home/<basename>)、单级路径(返回root/<basename>); - 同步更新
getProjectContext(),让 primary 与 parent 项目名都使用新格式; - 关键约束:暂不改动 ChromaDB collection 命名(
ChromaSync.ts),因为那需要数据迁移,由下节单独处理。
当前仓库源码印证:src/utils/project-name.ts 的 getProjectName() 已经演进出更稳健的解析链:先经 findGitRepoRoot() 用 git rev-parse --show-toplevel 定位仓库根(issue #2663,使项目名在子目录与 worktree 间保持稳定),再取 basename;非 git 目录、git 不可用或路径不存在时回退到 cwd 的 basename。边界分支在源码中同样存在:空 cwd 返回 unknown-project;Windows 下匹配 ^([A-Z]):\\/i 的盘符根返回 drive-<字母>(project-name.ts 第 50-63 行)。getProjectContext()(第 75-103 行)则额外处理 git worktree:检测到 worktree 时返回复合键 parentProjectName/cwdProjectName,并给出 parent、isWorktree、allProjects 字段(issue #3262)。这说明"单一命名函数 + 边界条件收敛"的方向已落地,且相关行为有 tests/utils/project-name.test.ts 与 tests/utils/project-name-isolation.test.ts 等测试覆盖。
存量项目名的一次性数据迁移
用户升级前,既有观测存储在旧项目名下(如 myapp),升级后新观测将写入新格式(如 work/myapp)。手册设计的迁移函数 migrateProjectNames() 位于数据库层:
- 查询
sessions表中所有 distinct 项目名; - 对每个旧格式名(不含
/分隔符),尝试在常见位置解析匹配的 git 仓库以恢复完整路径; - 解析失败时加
legacy/前缀(如legacy/myapp)以避免冲突; - 在同一个事务内更新
sessions与observations两张表的project列。
迁移的挂接方式:加入 worker 的后台初始化序列——在数据库初始化之后、搜索服务启动之前执行,并在 settings.json 中用一次性标志 projectNameMigrationComplete: true 防止重复运行。
对向量检索侧,手册要求同步更新 ChromaDB 元数据。由于 Chroma 不支持 metadata 更新,实际做法是对受影响项目触发 backfill 重新同步:项目名用于 collection 命名(cm__<project>)与 metadata 过滤条件,迁移后旧 collection 中的文档必须按新名重放。实现依据来自 src/services/sqlite/SessionStore.ts(sessions/observations 的 project 列)与 src/services/sync/ChromaSync.ts(collection 命名与过滤)。
修复会话提前终结:终结前排空待处理队列
问题:会话可以在观测消息还积压在 pending 队列时就被终结——summary 已生成、会话被标记完成,此后入队的观测无处归属,形成数据丢失。
修复方案(手册原文):
- 在
PendingMessageStore中新增:
SELECT COUNT(*) FROM pending_messages
WHERE content_session_id = ? AND status IN ('pending', 'processing')
封装为 hasPendingMessages(contentSessionId: string): boolean;
- 在会话终结路径(
src/services/worker-service.ts中搜索 "finalize" / "summary" / "SessionEnd" 可定位触发点)加入等待环,最长等待 30 秒:
while (await pendingStore.hasPendingMessages(sessionId)) {
await sleep(500);
}
// 超过 30s 仍未排空则强制终结并告警
- 强制终结时记录告警日志:
Session finalized with ${count} pending messages remaining — some observations may be lost
调用链佐证:待处理消息的领取/确认遵循 claimNextMessage() / confirmProcessed() 模式(见 src/services/sqlite/SessionStore.ts 中 pending_messages 表相关实现),观测在 src/services/worker/agents/ResponseProcessor.ts 完成 AI 处理后落库。仓库中的 pending_messages 表还带有部分唯一索引 ux_pending_session_tool ON pending_messages(session_db_id, tool_use_id) WHERE tool_use_id IS NOT NULL(SessionStore.ts 第 1832-1838 行),保证同一 tool-use 事件不会重复入队——这与"终结前排空队列"的守卫互为补充:前者防入队重复,后者防终结早于消费完成。运维侧还有 scripts/check-pending-queue.ts 与 scripts/clear-pending-queue.ts 两个脚本用于人工检查与清空积压。
修复重复观测入库:把去重从 30 秒窗收紧到会话级
问题:SessionStore.storeObservation() 的内容哈希去重只检查 30 秒时间窗:
SELECT id FROM observations
WHERE content_hash = ? AND created_at_epoch > ? AND memory_session_id = ?
并发处理同一会话消息时,两条内容相同的观测若落在窗口之外,检查双双通过,产生重复行。
修复方案(手册原文):
- 把时间窗改为会话生命周期范围:
SELECT id FROM observations WHERE content_hash = ? AND memory_session_id = ?
- 在数据库层加唯一索引兜底,作为迁移加入 schema 演进:
CREATE UNIQUE INDEX IF NOT EXISTS idx_obs_session_hash
ON observations(memory_session_id, content_hash)
- 优雅处理约束冲突:在
storeObservation()中捕获UNIQUE constraint failed,返回已存在的观测 ID 而非抛错。
当前仓库源码印证:该修复已经在库中落地。SessionStore.ts 第 1843-1894 行 的 schema 版本 29 迁移 addObservationsUniqueContentHashIndex() 在事务内先执行 dedupeObservationsByContentHash()——用 ROW_NUMBER() OVER (PARTITION BY memory_session_id, content_hash ORDER BY id) 删除同一会话内的重复行(NULL 哈希先被改写为 __null_migration_<id>__ 哨兵值避免误伤),再创建唯一索引 ux_observations_session_hash ON observations(memory_session_id, content_hash);失败则整体回滚。写入路径同样对齐:storeObservations() 使用 ON CONFLICT(memory_session_id, content_hash) DO NOTHING,并在冲突后回查已存在行的 id(SessionStore.ts 第 2665-2672 行),正是"捕获冲突、返回已有 ID"的实现形态。迁移行为由 tests/sqlite/session-store-migrations.test.ts 覆盖。
修复待处理队列无界增长
问题:worker 过载或 AI 处理反复失败时,pending_messages 表只增不减,消耗磁盘并拖慢查询。手册给出的三层限额:
| 约束 | 阈值 | 超限行为 |
|---|---|---|
| 单会话待处理消息 | 最多 100 条 | 丢弃最旧一条并记录告警日志 |
| 全会话总待处理消息 | 最多 1000 条 | 暂停新消息入队,直至队列排空 |
| 陈旧消息 | 启动时已清理 6 小时以上的消息 | 新增运行时周期性清理(每 30 分钟一次),避免两次重启之间持续累积 |
监控面:新增 getQueueSize(): number 返回总待处理条数,并暴露到 /api/health 端点的 pendingQueueSize 字段,便于外部探测队列水位。实现入口是 PendingMessageStore 的队列管理与 src/services/worker-service.ts 中 processPendingQueues() 的恢复逻辑(手册标注约在第 811 行)。
测试与构建验证
手册为这一阶段列出的测试矩阵,与仓库测试目录可一一对应:
getProjectName():验证普通路径的parent/basename形式、盘符根、home 目录、单级路径——对应 tests/utils/project-name.test.ts、tests/utils/project-name-isolation.test.ts 与 tests/utils/project-filter.test.ts;- 项目名迁移:造旧格式测试数据 → 跑迁移 → 断言新格式与 ChromaDB 重同步被触发;
- 提前终结守卫:mock 待处理消息,验证终结流程会等待处理完成;
- 去重:同一会话内插入两条内容哈希相同的观测,断言只存一条(对应 tests/sqlite/session-store-migrations.test.ts 的索引迁移用例);
- 队列限额:单会话入队 101 条,断言最旧一条被丢弃且有告警日志。
验证步骤:
- 运行
npm run build-and-sync完成构建与产物同步; - 运行完整测试套件并修复所有失败项;
- 在全新数据库上验证迁移为干净 no-op——无旧数据可迁移时不应产生任何副作用。
小结
Phase 05 的修复链条可以概括为"命名收敛 → 存量迁移 → 写入兜底":先用单一 getProjectName() 消除项目身份冲突,再用一次性事务迁移把旧 basename 数据(含 ChromaDB backfill)搬到新命名空间,最后用会话级唯一索引 + ON CONFLICT DO NOTHING 把去重从应用层时间窗下沉到数据库约束。围绕异步流水线的两处守卫——终结前排空 pending 队列、队列三层限额加 /api/health 水位暴露——则分别堵住了"观测静默丢失"与"磁盘无界占用"两个方向的用户信任损伤。对照当前仓库,唯一索引迁移、worktree 复合键、pending 队列检查脚本等实现均已可在源码与测试中核验。
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 StartedRust0622
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