Skip to content

fix(claude): resume 预检跨桶兜底,修复 /cd 空窗重启后静默丢上下文#506

Open
xu4wang wants to merge 1 commit into
deepcoldy:masterfrom
xu4wang:fix/resume-cross-bucket
Open

fix(claude): resume 预检跨桶兜底,修复 /cd 空窗重启后静默丢上下文#506
xu4wang wants to merge 1 commit into
deepcoldy:masterfrom
xu4wang:fix/resume-cross-bucket

Conversation

@xu4wang

@xu4wang xu4wang commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

问题

Claude 家族 CLI 的 transcript 按 cwd 分桶存(<dataDir>/projects/<slug(realpath(cwd))>/<sid>.jsonl),且 /cd 只改未来写入的落点、不回迁已有文件。于是存在一个竞态:会话 /cd 之后、写下任何 turn 之前发生重启(挂起后续起 / daemon 重启 / crash),旧 transcript 孤儿在旧 cwd 的桶里,而 checkResumeTargetExists 只探当前 cwd 的桶 → 判 false("provably absent")→ worker 丢弃 --resume、静默新开会话,用户看到:

⚠️ 历史会话(xxx)无法恢复,已为你新起一个干净会话(原因:adapter confirmed resume target does not exist on disk)

整段上下文丢失,且历史文件其实还完好地躺在磁盘上(另一个桶里)。现网已两次踩中(角色切换后的空窗重启是最常见触发路径)。

修法

<sid>.jsonl 在同一 dataDir 下全局唯一。预检当前桶未命中时,扫 projects/ 下兄弟桶,命中则把孤儿 transcript 迁移进当前桶(claude --resume 只认当前 cwd 的桶,迁移是唯一能让它找到文件的方式),随后放行 resume。要点:

  • 移动而非拷贝:旧桶留 stale 副本会在日后 /cd 回旧目录时被预检直接命中、resume 到过期上下文,静默丢掉其后的所有 turn(正是本修复要防的形态)。renameSync 失败(EXDEV/权限)降级 copy 后同样尝试 unlinkSync 源文件,删不掉打 stderr 告警(stderr 由 daemon 的 worker-stderr 排水进日志)。
  • 多桶命中取 mtime 最新(claude 自身 /cd 后重写会在旧桶留副本)。
  • sid 过 UUID 校验才允许动文件(该函数会做 rename,防路径插值)。
  • 语义零漂移:rescue 全败时回滚自建的目标桶目录 + probe 侧预先快照 projectDir 存在性,原有 false(干净降级 fresh)/undefined(可能 mid-session 轮换,交给 secondary guard)的分支判定一字不变。
  • 迁移成功/失败均有 stderr 日志可排障。

只在 miss 路径触发(每次 spawn 至多一次),每桶一次 statSync,满足接口 "synchronous, cheap" 的要求。

测试

  • 新增 test/claude-resume-cross-bucket.test.ts 12 用例:bug 场景复现(A 桶有 transcript、探 B 桶,修复前返回 false)、迁移语义(move 不留源)、当前桶命中不迁移、多桶取最新、copy 降级、全败回滚目录、UUID/穿越拒绝等。12 passed (12)
  • 全量 unit 套件与上游基线对比零回归(失败文件均为基线内环境依赖型集成测试:vc-meeting-daemon-sessionv3-distillation-*v2-run-archive;浮动项单跑均过,属并发 flaky)。
  • 真机端到端(真 claude 2.1.210 + 本分支 dist)两轮:
    1. dirA 种暗号 → 用 dist 的 checkResumeTargetExists 探 dirB(触发迁移,返回 true,文件从 A 桶移入 B 桶)→ dirB claude --resume 正确答出暗号。
    2. 最坏场景:Bash 工具飞行中 kill -9 claude,transcript 尾部悬空 tool_use → 跨桶迁移到 dirB → claude --resume 依然完整续回上下文并答出暗号。

关联

与 xu4wang:fix/role-switch-respawn(角色切换改 respawn+resume)配套:该 PR 的 respawn 在新 cwd 探桶,硬依赖本修复接住旧桶 transcript。本 PR 可独立合并(对现有 /cd 冷启动路径本身就是修复);对方 PR 必须晚于或与本 PR 同时合并。

🤖 Generated with Claude Code

Claude 的 transcript 按 cwd 分桶存,/cd 只改未来写入落点、不回迁已有
文件。若 /cd 后没写任何 turn 就重启,transcript 孤儿在旧 cwd 桶,而
checkResumeTargetExists 只探当前 cwd 桶 → 判 false → worker 丢弃
--resume、静默新开会话,整段上下文丢失。

修复:<sid>.jsonl 在同一 dataDir 下全局唯一。当前桶探不到时扫兄弟桶,
命中则把孤儿 transcript 迁进当前桶(claude --resume 只认当前 cwd 桶,
迁移是唯一能让它找到文件的方式)后返回 true。要点:

- 移动而非拷贝:旧桶留 stale 副本会在日后 /cd 回旧目录时被直接命中、
  静默丢掉其后的 turn;rename 失败(EXDEV/权限)降级 copy 后同样尝试
  unlink 源文件,删不掉打 stderr 告警。
- 多桶命中(claude 自身 /cd 后重写留下的旧副本)取 mtime 最新。
- sid 先过 UUID 校验才允许动文件(防路径穿越)。
- 全败时回滚自建的目标桶目录 + probe 侧预先快照 projectDir 存在性,
  保证原有 false(provably absent → 干净降级 fresh)/undefined 语义
  一字不漂移。
- 迁移成功/unlink 失败均有 stderr 日志可排障。

已验证:12 个单测(含 bug 复现、copy 降级、全败回滚、穿越拒绝);
真机端到端两轮——含最坏场景(工具飞行中 kill -9 留下悬空 tool_use
尾部)跨桶迁移后 claude --resume 均完整续回上下文。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

--- codex review 加固(2026-07-17)---
- 符号链接逃逸:扫描改用 lstatSync(不跟随),并对胜出者加 realpath 包含性校验,
  杜绝经植入软链把 dataDir 外文件迁进桶当 transcript 读。
- sidecar 随迁:move `<sid>.jsonl` 的同时迁移相邻 `<sid>/` 目录(tool-results 等),
  否则 resume 后转录里对工具结果的引用取不到。尽力而为、失败不阻断 rescue(jsonl 已就位)。
- 新增 3 个单测:sidecar 随迁、sidecar 缺失不报错、软链逃逸被拒。

- partial-cpSync 兜底:sidecar 拷贝中途失败时清掉半成品目标,避免 existsSync 守卫让后续 rescue 永久跳过(sidecar 永久分裂)。
@xu4wang
xu4wang force-pushed the fix/resume-cross-bucket branch from 44d2671 to 228a04c Compare July 17, 2026 03:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant