-
Notifications
You must be signed in to change notification settings - Fork 2.5k
feat(core): persist file history snapshots for cross-session /rewind (T2.1) #4897
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
3862455
8cbe523
47c3257
bc4e719
ad61928
7565f81
aa46284
25507df
4e08c42
b20a69b
f2f26ee
34cab7c
714ce9d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -614,9 +614,20 @@ export class Session implements SessionContext { | |
| chat.truncateHistory(apiTruncateIndex); | ||
| chat.stripThoughtsFromHistory(); | ||
|
|
||
| this.config.getChatRecordingService()?.rewindRecording(targetTurnIndex, { | ||
| truncatedCount: Math.max(0, apiHistory.length - apiTruncateIndex), | ||
| }); | ||
| const fileHistoryService = this.config.getFileHistoryService(); | ||
| const survivingSnapshots = fileHistoryService | ||
| .getSnapshots() | ||
| .slice(0, targetTurnIndex + 1); | ||
|
|
||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] Consider exposing const allSnapshots = fileHistoryService.getSnapshots();
const survivingSnapshots = allSnapshots.slice(0, targetTurnIndex + 1);
const removed = allSnapshots.slice(targetTurnIndex + 1);
fileHistoryService.restoreFromSnapshots(survivingSnapshots);
fileHistoryService.cleanupOrphanedBackups(removed);— qwen3.7-max via Qwen Code /review
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Already addressed — orphaned backup cleanup deferred in wenshao review. Bounded by MAX_SNAPSHOTS, cleaned up by subsequent makeSnapshot calls. |
||
| fileHistoryService.restoreFromSnapshots(survivingSnapshots); | ||
|
doudouOUC marked this conversation as resolved.
doudouOUC marked this conversation as resolved.
doudouOUC marked this conversation as resolved.
doudouOUC marked this conversation as resolved.
|
||
|
|
||
| this.config | ||
| .getChatRecordingService() | ||
| ?.rewindRecording( | ||
| targetTurnIndex, | ||
| { truncatedCount: Math.max(0, apiHistory.length - apiTruncateIndex) }, | ||
| survivingSnapshots, | ||
| ); | ||
|
|
||
| return { targetTurnIndex, apiTruncateIndex }; | ||
| } | ||
|
|
@@ -1054,6 +1065,27 @@ export class Session implements SessionContext { | |
| } | ||
| } | ||
|
|
||
| // Snapshot file state before this turn (mirrors the makeSnapshot | ||
|
doudouOUC marked this conversation as resolved.
|
||
| // block in GeminiClient.sendMessageStream). Placed after | ||
| // slash-command and hook early-returns so locally handled commands | ||
| // don't create phantom snapshots that desync the snapshot index. | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] This makeSnapshot + recordFileHistorySnapshot block (lines 1012–1028) is nearly identical to the one in Consider extracting a shared helper: async function snapshotAndRecordFileHistory(
config: Config,
promptId: string,
): Promise<void> {
try {
const fhs = config.getFileHistoryService();
await fhs.makeSnapshot(promptId);
try {
const latest = fhs.getSnapshots().at(-1);
if (latest) {
config.getChatRecordingService()?.recordFileHistorySnapshot(latest);
}
} catch (e) {
debugLogger.error(`FileHistory: recordSnapshot failed: ${e}`);
}
} catch (e) {
debugLogger.error(`FileHistory: makeSnapshot failed: ${e}`);
}
}— qwen3.7-max via Qwen Code /review
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Already addressed — two call sites with different contexts don't justify extraction. Addressed in multiple prior review rounds. |
||
| try { | ||
| const fileHistoryService = this.config.getFileHistoryService(); | ||
| await fileHistoryService.makeSnapshot(promptId); | ||
| try { | ||
| const latestSnapshot = fileHistoryService.getSnapshots().at(-1); | ||
| if (latestSnapshot) { | ||
| this.config | ||
| .getChatRecordingService() | ||
| ?.recordFileHistorySnapshot(latestSnapshot); | ||
| } | ||
| } catch (e) { | ||
| debugLogger.error(`FileHistory: recordSnapshot failed: ${e}`); | ||
| } | ||
| } catch (e) { | ||
| debugLogger.error(`FileHistory: makeSnapshot failed: ${e}`); | ||
| } | ||
|
|
||
| // Prepend session-level system reminders (plan mode / subagent / | ||
| // arena) so the model sees them, matching the behaviour of | ||
| // `GeminiClient.sendMessageStream` in the CLI/TUI path. Without this, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -28,6 +28,11 @@ import type { | |
| import type { Status } from '../core/coreToolScheduler.js'; | ||
| import type { AgentResultDisplay, FileDiff } from '../tools/tools.js'; | ||
| import type { UiEvent } from '../telemetry/uiTelemetry.js'; | ||
| import type { | ||
| FileHistorySnapshot, | ||
| SerializedFileHistorySnapshot, | ||
| } from './fileHistoryService.js'; | ||
| import { serializeSnapshot } from './fileHistoryService.js'; | ||
|
|
||
| const debugLogger = createDebugLogger('CHAT_RECORDING'); | ||
|
|
||
|
|
@@ -235,7 +240,8 @@ export interface ChatRecord { | |
| | 'custom_title' | ||
| | 'rewind' | ||
| | 'agent_bootstrap' | ||
| | 'agent_launch_prompt'; | ||
| | 'agent_launch_prompt' | ||
| | 'file_history_snapshot'; | ||
| /** Working directory at time of message */ | ||
| cwd: string; | ||
| /** CLI version for compatibility tracking */ | ||
|
|
@@ -281,7 +287,8 @@ export interface ChatRecord { | |
| | CustomTitleRecordPayload | ||
| | NotificationRecordPayload | ||
| | RewindRecordPayload | ||
| | AgentBootstrapRecordPayload; | ||
| | AgentBootstrapRecordPayload | ||
| | FileHistorySnapshotRecordPayload; | ||
|
|
||
| /** Background subagent that produced this record (e.g. "explore-7f3c"). */ | ||
| agentId?: string; | ||
|
|
@@ -428,6 +435,14 @@ export interface RewindRecordPayload { | |
| truncatedCount: number; | ||
| } | ||
|
|
||
| /** | ||
| * Stored payload for file history snapshot persistence. | ||
| * Each entry records one or more snapshots for session resume. | ||
| */ | ||
| export interface FileHistorySnapshotRecordPayload { | ||
| snapshots: SerializedFileHistorySnapshot[]; | ||
| } | ||
|
|
||
| /** | ||
| * Service for recording the current chat session to disk. | ||
| * | ||
|
|
@@ -1140,7 +1155,11 @@ export class ChatRecordingService { | |
| * nothing before it), 1 means keep the first user turn, etc. | ||
| * @param payload Additional metadata to persist with the rewind record. | ||
| */ | ||
| rewindRecording(targetTurnIndex: number, payload: RewindRecordPayload): void { | ||
| rewindRecording( | ||
| targetTurnIndex: number, | ||
| payload: RewindRecordPayload, | ||
| survivingFileHistorySnapshots?: FileHistorySnapshot[], | ||
| ): void { | ||
| try { | ||
| // Re-root: point back to the record just before the target user turn. | ||
| this.lastRecordUuid = this.turnParentUuids[targetTurnIndex] ?? null; | ||
|
|
@@ -1161,6 +1180,12 @@ export class ChatRecordingService { | |
| }; | ||
|
|
||
| this.appendRecord(record); | ||
|
|
||
| // Re-record surviving file history snapshots on the active branch so | ||
| // they are visible to reconstructHistory on resume. | ||
| if (survivingFileHistorySnapshots?.length) { | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] The new 3-arg branch of If this branch breaks, resumed sessions lose file history state for surviving turns after rewind. Suggested test: call — qwen3.7-max via Qwen Code /review |
||
| this.recordFileHistorySnapshotBatch(survivingFileHistorySnapshots); | ||
| } | ||
| } catch (error) { | ||
| debugLogger.error('Error saving rewind record:', error); | ||
| } | ||
|
|
@@ -1346,4 +1371,23 @@ export class ChatRecordingService { | |
| debugLogger.error('Error saving attribution snapshot:', error); | ||
| } | ||
| } | ||
|
|
||
|
doudouOUC marked this conversation as resolved.
doudouOUC marked this conversation as resolved.
|
||
| recordFileHistorySnapshot(snapshot: FileHistorySnapshot): void { | ||
|
doudouOUC marked this conversation as resolved.
doudouOUC marked this conversation as resolved.
doudouOUC marked this conversation as resolved.
|
||
| this.recordFileHistorySnapshotBatch([snapshot]); | ||
| } | ||
|
|
||
| recordFileHistorySnapshotBatch(snapshots: FileHistorySnapshot[]): void { | ||
| if (snapshots.length === 0) return; | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] Suggested tests:
— qwen3.7-max via Qwen Code /review |
||
| try { | ||
| const record: ChatRecord = { | ||
| ...this.createBaseRecord('system'), | ||
| type: 'system', | ||
| subtype: 'file_history_snapshot', | ||
| systemPayload: { snapshots: snapshots.map(serializeSnapshot) }, | ||
| }; | ||
| this.appendRecord(record); | ||
| } catch (error) { | ||
| debugLogger.error('Error saving file history snapshot batch:', error); | ||
| } | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.