Skip to content

File-history follow-ups: persistence, shell tracking, bridge tests, perf, failure reasons #4204

Description

@doudouOUC

Tracks deferred follow-up items from PR #4064 (/rewind file restoration). All real but each was large enough to belong outside the introduction PR. Sibling follow-ups: #4173 (session backup cleanup), #4187 (RewindSelector + handleRewindConfirm tests), #4216 (TOCTOU + sticky failed-marker fixes).

Current status (updated 2026-06-14)

  • Item A — Cross-session snapshot persistence: implemented by PR fix(core): Persist file history snapshot updates #5057 (fix(core): Persist file history snapshot updates), merged on 2026-06-13 UTC. qwen-code persists file_history_snapshot records, restores them on session load, and immediately re-records the latest snapshot when trackEdit(...) mutates it.
  • Item B1 — simulated sed -i edit tracking: still open. claude-code supports this specific shell path by parsing simple in-place sed substitutions and applying them through a direct file write that triggers file-history tracking.
  • Item B2 — generic shell tracking decision: decided out of scope for now. claude-code does not track arbitrary shell writes such as cp, mv, rm, git apply, package scripts, or manual edits.
  • Item C — GeminiClient.sendMessageStream bridge tests: implemented by PR fix(core): Persist file history snapshot updates #5057. The tests cover UserQuery snapshot creation/recording, non-UserQuery and retry skips, and best-effort error swallowing.
  • ⏸️ Item D — getDiffStats concurrency limit: deferred. claude-code also uses unbounded Promise.all; this would be qwen-code hardening beyond upstream parity.
  • ⏸️ Item E — per-file failure reasons: deferred. claude-code logs restore failures but does not expose or persist per-file failure reasons.

Remaining active upstream-parity work: only Item B1. Generic shell tracking, diff concurrency limiting, and per-file failure reasons should stay deferred unless upstream adds a concrete design or qwen-code deliberately chooses to go beyond upstream.

Item A — Cross-session snapshot persistence ✅ Done in PR #5057

Original problem: FileHistoryService.getSnapshots() and restoreFromSnapshots() existed but were not wired into session persistence. After a process restart or /resume, the in-memory state was empty, so /rewind could not see snapshots from earlier sessions even though backup files existed under ~/.qwen/file-history/{prevSessionId}/.

Upstream reference: claude-code/src/utils/sessionStorage.ts builds a FileHistorySnapshotChain keyed by message uuid into the session transcript; claude-code/src/utils/sessionRestore.ts calls fileHistoryRestoreStateFromLog on resume to hydrate appState.fileHistory. fileHistoryTrackEdit(...) also records an update for the current message after an edit mutates the latest snapshot.

Resolved in qwen-code by PR #5057:

  • ChatRecordingService persists file-history snapshots in existing type: 'system', subtype: 'file_history_snapshot' records keyed by promptId.
  • SessionService.loadSession() restores the snapshot chain and deduplicates same-promptId records with last-wins semantics.
  • FileHistoryService.trackEdit(...) calls an optional recorder callback after it actually adds or heals a backup in the latest snapshot, fixing the last-turn-exit case where edits happened after makeSnapshot(...) but before the next turn.
  • Older logs without snapshot records still load with no file-history state, and malformed snapshot records are skipped with warning.
  • No dedicated schemaVersion or isSnapshotUpdate field was added because qwen-code kept the persisted record shape unchanged; append-only same-promptId records plus last-wins resume semantics provide the needed snapshot-update behavior.

Item B — Shell-tool change tracking

PR #4064 hooks file history into edit and write_file only. Changes made via run_shell_command (sed -i, cp, mv, rm, npm run ..., git apply, etc.) and out-of-tool manual edits are not captured.

This mostly matches claude-code scope. Upstream tracks edit/write/notebook paths plus one special simulated-sed-edit path; it does not track arbitrary shell writes.

Remaining work:

  • B1: port the simulated-sed-edit path from upstream. When the model emits a qualifying sed -i 's/a/b/g' file, parseSedEditCommand re-routes execution through applySedEdit (a direct file write), which lets file-history tracking capture the recognized file path.
  • B2: generic shell tracking decision. Keep out of scope for now because upstream punts on this and post-hoc approaches such as git status --porcelain are semantically risky: they only work in git repos, observe changes after mutation instead of before it, and can clobber in-flight backups.

Item C — GeminiClient.sendMessageStreammakeSnapshot bridge tests ✅ Done in PR #5057

Original gap: client.ts called getFileHistoryService().makeSnapshot(prompt_id) for SendMessageType.UserQuery turns, but the integration was not directly tested.

Resolved in qwen-code by PR #5057:

  • UserQuery turns call makeSnapshot(promptId).
  • UserQuery turns record the latest snapshot when one exists.
  • ToolResult and Retry turns do not call makeSnapshot(...).
  • makeSnapshot(...) failures are swallowed and the stream still yields content.
  • recordFileHistorySnapshot(...) failures are swallowed and the stream still yields content.

Item D — getDiffStats concurrency limit ⏸️ Deferred

FileHistoryService.getDiffStats runs Promise.all over every entry in state.trackedFiles with no bound. Each iteration does stat + readFile (both original and backup) + diffLines. On a long-running session with many tracked large files this can spike memory and freeze the UI during the rewind selector's phase-2 diff load.

Reference: upstream claude-code/src/utils/fileHistory.ts:fileHistoryGetDiffStats has the same unbounded Promise.all pattern. Treat this as a possible qwen-code hardening task, not an upstream-parity requirement.

Deferred acceptance criteria if revived later:

  • Bound the parallelism to a small concurrency (for example 10) using a worker-pool helper or p-limit.
  • Add a regression test for bounded behavior on a large trackedFiles set.

Item E — Per-file failure reason for filesFailed ⏸️ Deferred

When /rewind reports Failed to restore N file(s): foo.ts, the user has no information about why: disk full, permission, race, missing backup, etc. The cause is in debug logging but invisible under default logging.

Reference: claude-code logs per-file restore failures and telemetry events but does not expose filesFailed reasons to users or persist a failedReason field. Keep qwen-code's current file-name-only reporting unless a separate UX/schema decision is made.

Deferred acceptance criteria if revived later:

  • FileHistoryBackup.failedReason?: string populated when snapshot or restore work fails.
  • applySnapshot(...) surfaces the reason alongside the file name in filesFailed.
  • Persisted schema shape is explicitly versioned if this becomes part of the stored record.

claude-code alignment notes

Local upstream reference checked in ~/Projects/claude-code on 2026-06-12.

  • Item A (cross-session persistence): supported upstream. claude-code records standalone JSONL entries with type: 'file-history-snapshot', keyed by the user message UUID. fileHistoryMakeSnapshot(...) records the initial snapshot for the turn, and fileHistoryTrackEdit(...) records isSnapshotUpdate: true for the same message after an edit mutates that snapshot. On resume, sessionStorage.ts builds the file-history snapshot chain in transcript order and fileHistoryRestoreStateFromLog(...) hydrates appState.fileHistory. When the session id changes, copyFileHistoryForResume(...) hard-links/copies backup files into the new session's file-history directory. Upstream does not use a dedicated file-history schemaVersion; older logs simply have no file-history-snapshot entries and load without file-history state.
  • Item B1 (simulated sed -i edit): supported upstream. BashTool/sedEditParser.ts recognizes simple in-place sed substitutions, the permission UI previews the exact file diff, and after approval an internal-only _simulatedSedEdit is injected. BashTool then applies the edit by direct file write and calls fileHistoryTrackEdit(...) before writing. This is the only shell-command path upstream routes through file history.
  • Item B2 (generic shell tracking): not supported upstream; out of scope for now. Arbitrary shell writes such as cp, mv, rm, git apply, package scripts, and manually edited files are not tracked by claude-code. We should not implement generic shell tracking in this issue unless upstream adds a concrete design later.
  • Item C (qwen-code GeminiClient.sendMessageStream bridge tests): qwen-specific. claude-code has no GeminiClient equivalent; snapshots are triggered from prompt-submit / query-engine paths. This is qwen-code focused test coverage for our own bridge and is now covered by PR fix(core): Persist file history snapshot updates #5057.
  • Item D (getDiffStats concurrency limit): not supported upstream; defer for now. claude-code/src/utils/fileHistory.ts:fileHistoryGetDiffStats still uses unbounded Promise.all over state.trackedFiles. Treat this as a possible qwen-code hardening task, not a port-alignment requirement.
  • Item E (per-file failure reasons): not supported upstream; defer for now. claude-code logs per-file restore failures and telemetry events but does not expose filesFailed reasons to users or persist a failedReason field. Keep qwen-code's current file-name-only failure reporting unless a separate UX/schema decision is made.

Related

🤖 Generated with Qwen Code

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions