fix(core): Persist file history snapshot updates#5057
Conversation
Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
wenshao
left a comment
There was a problem hiding this comment.
[Critical] resolveBackupPath (fileHistoryService.ts:210) passes backupFileName directly to path.join(baseDir, sessionId, backupFileName) with zero path-traversal validation. backupFileName is deserialized from the JSONL session file in deserializeSnapshots (line 155) with no sanitization — it is accepted as-is. An attacker with write access to the JSONL session file can craft backupFileName: "../../../../../../etc/hosts" so the next /rewind copies that file over the user's workspace file via safeCopyFile. Pre-existing, but this PR increases the persistence frequency of file_history_snapshot records, making the attack surface proportionally more active.
function resolveBackupPath(backupFileName: string, sessionId: string): string {
const base = join(Storage.getGlobalQwenDir(), FILE_HISTORY_DIR, sessionId);
const resolved = join(base, backupFileName);
if (!resolved.startsWith(base + sep) && resolved !== base) {
throw new Error(`backupFileName escapes base directory: ${backupFileName}`);
}
return resolved;
}
— qwen3.7-max via Qwen Code /review
[Critical] startNewSession calls finalize() on the old ChatRecordingService but does NOT await flush(). finalize() now queues pending snapshot writes onto the async writeChain via flushPendingFileHistorySnapshots(). The service is immediately replaced (line 2253), orphaning pending writes. The other two callers (shutdown() at line 2851 and prepareSessionArtifactMigration() at line 2732) correctly pair finalize() with await flush().
On /clear, /new, or /reset, file-history snapshots from the previous session can be silently lost.
try {
this.chatRecordingService?.finalize();
await this.chatRecordingService?.flush();
} catch {
// Best-effort — don't block session switch
}
— qwen3.7-max via Qwen Code /review
Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
qqqys
left a comment
There was a problem hiding this comment.
Prior critical feedback appears resolved on head 47fabf6: backup paths are now constrained to the session backup directory, stale callbacks are covered, and file-history snapshot writes are no longer deferred behind the session switch. I did not find a new critical blocker in this pass.
✅ Verification report — local runtime A/B (Linux)I verified this PR by building the actual post-merge tree ( Environment: Linux, Node 1. Claimed suites + typecheck (AFTER / merged tree)
(The full 2. Revert-proof — the tests pin the new behaviorReverting only the 4 source files to The reverted source also fails to even typecheck the new tests ( 3. Service-level A/B on the shipped artifactImported the built
The turn-boundary snapshot (what 4. Real TUI E2E (tmux) — the headlineDrove the real Same single edit, same clean exit — AFTER persists the pre-edit backup immediately (so resume + last-turn Notes (non-blocking)
Verdict: LGTM. The fix is correct, tested, regression-free on the merge result, and the BEFORE/AFTER behavior is exactly as described. 中文说明✅ 验证报告 — 本地真实运行 A/B(Linux)我在真正的合并后代码树( 环境:Linux,Node 1. PR 声称的测试 + 类型检查(AFTER / 合并树)
2. Revert 反证 —— 测试确实锁定了新行为 3. 对已构建产物的 service 级 A/B
4. 真实 TUI 端到端(tmux)—— 重点
说明(非阻塞)
结论:LGTM。 修复正确、有测试、在合并结果上无回归,BEFORE/AFTER 行为与描述完全一致。 Verified on Linux with a real TUI + mock-provider tool-edit E2E and built-artifact A/B; complements the author's macOS run. |
* fix(core): persist file history snapshot updates Co-authored-by: Qwen-Coder <[email protected]> * codex: address PR review feedback (#5057) Co-authored-by: Qwen-Coder <[email protected]> * fix(core): Address file history persistence review feedback Co-authored-by: Qwen-Coder <[email protected]> * fix(core): Address file history review follow-ups Co-authored-by: Qwen-Coder <[email protected]> --------- Co-authored-by: Qwen-Coder <[email protected]>
What this PR does
This PR makes file-history snapshot updates durable during a turn by recording the latest snapshot immediately after a tracked edit actually adds or heals a backup. The change keeps the existing append-only
file_history_snapshotrecord shape, preserves explicit turn-boundary recording, and documents whyschemaVersionandisSnapshotUpdateare not needed.Why it's needed
A session could previously exit after an edit/write tool captured file backups but before the next
UserQuerymakeSnapshot(), leaving resume without the last turn's updated file-history state. Persisting the mutated latest snapshot closes that last-turn/rewindgap while preserving backward compatibility and last-wins reconstruction bypromptId.Reviewer Test Plan
How to verify
Run
cd packages/core && npx vitest run src/services/fileHistoryService.test.ts src/services/sessionService.test.ts src/services/chatRecordingService.test.ts src/core/client.test.ts src/config/config.test.tsand confirm all 543 focused core tests pass. Runcd packages/cli && npx vitest run src/acp-integration/session/Session.test.tsand confirm all 125 ACP session tests pass. Runnpm run build && npm run typecheckand confirm it exits 0. Reviewers should also confirm that repeated snapshot records for the samepromptIdremain append-only and resume keeps the later record, that duplicatetrackEditcalls for an already captured non-failed file do not record again, and that recorder failures remain best-effort and do not block edit/write behavior.Evidence (Before & After)
Before: if the process exited after a tool edit but before another prompt created the next snapshot, resume could reconstruct only the turn-boundary snapshot and lose the edited turn's file-history state. After: the updated latest snapshot is recorded immediately after
trackEditmutates it, and focused tests cover snapshot update recording, JSONL serialization, resume last-wins reconstruction, client prompt flow, ACP prompt flow, and recorder error handling. Manual TUI E2E evidence is not included; an E2E plan is added for the resume plus/rewindscenario.Tested on
Environment (optional)
macOS local checkout, Node.js v22.22.3, npm 10.9.8.
npm run build && npm run typecheckexits 0 with existing warnings from the VS Code companion curly lint rules plus Browserslist/chunk-size warnings.Risk & Scope
sed -icoverage,getDiffStatsconcurrency limiting, and per-file failure reasons remain out of scope.file_history_snapshotpayload shape is unchanged.Linked Issues
Refs #4204
中文说明
这个 PR 做了什么
这个 PR 让单轮内的 file-history snapshot 更新变成持久化:当被跟踪的编辑真正新增备份或修复 failed 备份后,立即记录最新 snapshot。改动保持现有追加式
file_history_snapshot记录形态,保留显式的 turn-boundary 记录,并说明为什么不需要schemaVersion和isSnapshotUpdate。为什么需要
之前会话可能在 edit/write 工具捕获文件备份之后、下一次
UserQuerymakeSnapshot()之前退出,导致 resume 时缺少最后一轮更新后的 file-history 状态。持久化被修改过的最新 snapshot 可以补上这个 last-turn/rewind缺口,同时保持向后兼容,并继续使用按promptIdlast-wins 的恢复模型。Reviewer Test Plan
How to verify
运行
cd packages/core && npx vitest run src/services/fileHistoryService.test.ts src/services/sessionService.test.ts src/services/chatRecordingService.test.ts src/core/client.test.ts src/config/config.test.ts,确认 543 个 core focused tests 全部通过。运行cd packages/cli && npx vitest run src/acp-integration/session/Session.test.ts,确认 125 个 ACP session tests 全部通过。运行npm run build && npm run typecheck,确认退出码为 0。Reviewer 还应确认同一个promptId的重复 snapshot 记录仍然是 append-only 且 resume 保留后写入记录,已捕获且非 failed 文件的重复trackEdit不会再次记录,并且 recorder 失败仍保持 best-effort,不阻塞 edit/write 行为。Evidence (Before & After)
Before:如果进程在工具编辑后、下一次 prompt 创建新 snapshot 前退出,resume 可能只能重建 turn-boundary snapshot,从而丢失该编辑轮的 file-history 状态。After:
trackEdit修改最新 snapshot 后会立即记录更新后的 snapshot,focused tests 覆盖 snapshot update recording、JSONL 序列化、resume last-wins 重建、client prompt flow、ACP prompt flow 和 recorder 错误处理。本 PR 未包含手动 TUI E2E 证据;已为 resume 加/rewind场景补充 E2E 计划。Tested on
Environment (optional)
macOS 本地 checkout,Node.js v22.22.3,npm 10.9.8。
npm run build && npm run typecheck退出码为 0,同时保留既有的 VS Code companion curly lint warnings 以及 Browserslist/chunk-size warnings。Risk & Scope
sed -i覆盖、getDiffStats并发限制和 per-file failure reasons 仍不在范围内。file_history_snapshotpayload 形态保持不变。Linked Issues
关联 #4204。