Skip to content

Commit df23932

Browse files
committed
pref(checkpoint):增加版本号比较
1 parent a48533d commit df23932

3 files changed

Lines changed: 129 additions & 14 deletions

File tree

internal/checkpoint/per_edit_snapshot.go

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -627,7 +627,11 @@ func (s *PerEditSnapshotStore) RunEndCapture(ctx context.Context, checkpointIDs
627627
// 此时若文件 mtime 晚于 run 最后一个 checkpoint 的创建时间,该文件会被跳过并记录警告。
628628
//
629629
// checkpointIDs 应为 PerEditCheckpointIDFromRef 提取后的值(不含 "peredit:" 前缀)。
630-
func (s *PerEditSnapshotStore) RunAggregateDiff(ctx context.Context, checkpointIDs []string) (string, []FileChangeEntry, error) {
630+
//
631+
// prevFileVersions 为上一个 run 最后一个 checkpoint 的 FileVersions 快照。
632+
// 版本号未变的文件(同一 hash 在相邻 run 中版本号相同)会被跳过,
633+
// 因为这些文件在本 run 中未产生新 capture,内容没有变化。传 nil 表示不过滤。
634+
func (s *PerEditSnapshotStore) RunAggregateDiff(ctx context.Context, checkpointIDs []string, prevFileVersions map[string]int) (string, []FileChangeEntry, error) {
631635
type versionRange struct {
632636
minV int
633637
maxV int
@@ -657,6 +661,16 @@ func (s *PerEditSnapshotStore) RunAggregateDiff(ctx context.Context, checkpointI
657661
}
658662
}
659663

664+
// 过滤历史文件:版本号与上一个 run 结束时相同 = 本 run 未产生新 capture = 跳过。
665+
if prevFileVersions != nil {
666+
for hash, vr := range versionByHash {
667+
if vr.minV == vr.maxV {
668+
if prevV, ok := prevFileVersions[hash]; ok && prevV == vr.minV {
669+
delete(versionByHash, hash)
670+
}
671+
}
672+
}
673+
}
660674
s.indexMu.Lock()
661675
defer s.indexMu.Unlock()
662676

@@ -702,7 +716,7 @@ func (s *PerEditSnapshotStore) RunAggregateDiff(ctx context.Context, checkpointI
702716
if beforeIsDir && beforeExists && afterIsDir && afterExists {
703717
continue
704718
}
705-
if afterIsDir {
719+
if beforeIsDir && afterIsDir {
706720
continue
707721
}
708722
if beforeExists == afterExists && bytes.Equal(beforeContent, afterContent) {
@@ -984,6 +998,16 @@ func (s *PerEditSnapshotStore) writeCheckpointMeta(meta CheckpointMeta) error {
984998
return writeFileAtomic(s.checkpointMetaPath(meta.CheckpointID), data, 0o644)
985999
}
9861000

1001+
// GetCheckpointFileVersions 读取指定 checkpoint 的 FileVersions 映射,
1002+
// 供调用方用于版本号比较(如 RunAggregateDiff 的跨 run 过滤)。
1003+
func (s *PerEditSnapshotStore) GetCheckpointFileVersions(checkpointID string) (map[string]int, error) {
1004+
meta, err := s.readCheckpointMeta(checkpointID)
1005+
if err != nil {
1006+
return nil, err
1007+
}
1008+
return meta.FileVersions, nil
1009+
}
1010+
9871011
func (s *PerEditSnapshotStore) readCheckpointMeta(checkpointID string) (CheckpointMeta, error) {
9881012
var meta CheckpointMeta
9891013
data, err := os.ReadFile(s.checkpointMetaPath(checkpointID))

internal/checkpoint/per_edit_snapshot_test.go

Lines changed: 67 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1163,7 +1163,7 @@ func TestRunAggregateDiff_ModifiedFileAcrossCheckpoints(t *testing.T) {
11631163
}
11641164
store.Reset()
11651165

1166-
patch, changes, err := store.RunAggregateDiff(context.Background(), []string{"cp1", "cp2"})
1166+
patch, changes, err := store.RunAggregateDiff(context.Background(), []string{"cp1", "cp2"}, nil)
11671167
if err != nil {
11681168
t.Fatalf("RunAggregateDiff: %v", err)
11691169
}
@@ -1206,7 +1206,7 @@ func TestRunAggregateDiff_CreatedFile(t *testing.T) {
12061206
}
12071207
store.Reset()
12081208

1209-
patch, changes, err := store.RunAggregateDiff(context.Background(), []string{"cp1"})
1209+
patch, changes, err := store.RunAggregateDiff(context.Background(), []string{"cp1"}, nil)
12101210
if err != nil {
12111211
t.Fatalf("RunAggregateDiff: %v", err)
12121212
}
@@ -1244,7 +1244,7 @@ func TestRunAggregateDiff_DeletedFile(t *testing.T) {
12441244
}
12451245
store.Reset()
12461246

1247-
patch, changes, err := store.RunAggregateDiff(context.Background(), []string{"cp1"})
1247+
patch, changes, err := store.RunAggregateDiff(context.Background(), []string{"cp1"}, nil)
12481248
if err != nil {
12491249
t.Fatalf("RunAggregateDiff: %v", err)
12501250
}
@@ -1305,7 +1305,7 @@ func TestRunAggregateDiff_CreatedThenDeleted(t *testing.T) {
13051305
}
13061306
store.Reset()
13071307

1308-
_, changes, err := store.RunAggregateDiff(context.Background(), []string{"cp1", "cp2"})
1308+
_, changes, err := store.RunAggregateDiff(context.Background(), []string{"cp1", "cp2"}, nil)
13091309
if err != nil {
13101310
t.Fatalf("RunAggregateDiff: %v", err)
13111311
}
@@ -1349,7 +1349,7 @@ func TestRunAggregateDiff_UnchangedFileOmitted(t *testing.T) {
13491349
}
13501350
store.Reset()
13511351

1352-
_, changes, err := store.RunAggregateDiff(context.Background(), []string{"cp1", "cp2"})
1352+
_, changes, err := store.RunAggregateDiff(context.Background(), []string{"cp1", "cp2"}, nil)
13531353
if err != nil {
13541354
t.Fatalf("RunAggregateDiff: %v", err)
13551355
}
@@ -1360,7 +1360,7 @@ func TestRunAggregateDiff_UnchangedFileOmitted(t *testing.T) {
13601360

13611361
func TestRunAggregateDiff_EmptyCheckpointIDs(t *testing.T) {
13621362
store, _ := newTestStore(t)
1363-
patch, changes, err := store.RunAggregateDiff(context.Background(), nil)
1363+
patch, changes, err := store.RunAggregateDiff(context.Background(), nil, nil)
13641364
if err != nil {
13651365
t.Fatalf("RunAggregateDiff with nil: %v", err)
13661366
}
@@ -1374,7 +1374,7 @@ func TestRunAggregateDiff_EmptyCheckpointIDs(t *testing.T) {
13741374

13751375
func TestRunAggregateDiff_NonexistentCheckpoint(t *testing.T) {
13761376
store, _ := newTestStore(t)
1377-
_, _, err := store.RunAggregateDiff(context.Background(), []string{"nonexistent_cp"})
1377+
_, _, err := store.RunAggregateDiff(context.Background(), []string{"nonexistent_cp"}, nil)
13781378
if err == nil {
13791379
t.Fatal("expected error for nonexistent checkpoint")
13801380
}
@@ -1407,7 +1407,7 @@ func TestRunAggregateDiff_MultipleFilesAggregated(t *testing.T) {
14071407
}
14081408
store.Reset()
14091409

1410-
patch, changes, err := store.RunAggregateDiff(context.Background(), []string{"cp1"})
1410+
patch, changes, err := store.RunAggregateDiff(context.Background(), []string{"cp1"}, nil)
14111411
if err != nil {
14121412
t.Fatalf("RunAggregateDiff: %v", err)
14131413
}
@@ -1428,3 +1428,62 @@ func TestRunAggregateDiff_MultipleFilesAggregated(t *testing.T) {
14281428
t.Fatalf("patch missing file headers:\n%s", patch)
14291429
}
14301430
}
1431+
1432+
func TestRunAggregateDiff_HistoricalFileFilteredByVersion(t *testing.T) {
1433+
store, workdir := newTestStore(t)
1434+
1435+
// Simulate "previous run": capture file A, finalize prev_cp.
1436+
absA := writeWorkdirFile(t, workdir, "a.txt", "from prev run\n")
1437+
if _, err := store.CapturePreWrite(absA); err != nil {
1438+
t.Fatalf("capture a: %v", err)
1439+
}
1440+
if err := os.WriteFile(absA, []byte("modified in prev run\n"), 0o644); err != nil {
1441+
t.Fatalf("write a: %v", err)
1442+
}
1443+
if _, err := store.Finalize("prev_cp"); err != nil {
1444+
t.Fatalf("finalize prev_cp: %v", err)
1445+
}
1446+
store.Reset()
1447+
1448+
// Get prev run's FileVersions.
1449+
prevFV, err := store.GetCheckpointFileVersions("prev_cp")
1450+
if err != nil {
1451+
t.Fatalf("get prev vers: %v", err)
1452+
}
1453+
1454+
// "Current run": capture file B only, a.txt is NOT touched.
1455+
absB := writeWorkdirFile(t, workdir, "b.txt", "old b\n")
1456+
if _, err := store.CapturePreWrite(absB); err != nil {
1457+
t.Fatalf("capture b: %v", err)
1458+
}
1459+
if err := os.WriteFile(absB, []byte("new b\n"), 0o644); err != nil {
1460+
t.Fatalf("write b: %v", err)
1461+
}
1462+
if _, err := store.Finalize("cur_cp1"); err != nil {
1463+
t.Fatalf("finalize cur_cp1: %v", err)
1464+
}
1465+
store.Reset()
1466+
1467+
// Second checkpoint in current run (still no touch on a.txt).
1468+
if _, err := store.Finalize("cur_cp2"); err != nil {
1469+
t.Fatalf("finalize cur_cp2: %v", err)
1470+
}
1471+
store.Reset()
1472+
1473+
patch, changes, err := store.RunAggregateDiff(context.Background(),
1474+
[]string{"cur_cp1", "cur_cp2"}, prevFV)
1475+
if err != nil {
1476+
t.Fatalf("RunAggregateDiff: %v", err)
1477+
}
1478+
// a.txt should be filtered out: version unchanged from prev run.
1479+
// b.txt should appear.
1480+
if len(changes) != 1 {
1481+
t.Fatalf("expected 1 change (b.txt only), got %d: %+v", len(changes), changes)
1482+
}
1483+
if changes[0].Path != "b.txt" {
1484+
t.Fatalf("expected b.txt, got %s", changes[0].Path)
1485+
}
1486+
if strings.Contains(patch, "a.txt") {
1487+
t.Fatalf("patch should NOT contain a.txt (filtered by version):\n%s", patch)
1488+
}
1489+
}

internal/runtime/checkpoint_restore.go

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,21 @@ func (s *Service) restoreCheckpointCore(ctx context.Context, sessionID, checkpoi
6464
if guardWritten {
6565
s.perEditStore.Reset()
6666
}
67-
guardRecord, guardErr := s.createGuardCheckpoint(ctx, sessionID, record.RunID, guardID, guardWritten)
67+
// 当 pending 为空时回退到最近 end-of-turn checkpoint 作为 undo 基线
68+
var fallbackRef string
69+
if !guardWritten && s.checkpointStore != nil {
70+
records, listErr := s.checkpointStore.ListCheckpoints(ctx, sessionID, checkpoint.ListCheckpointOpts{Limit: 5})
71+
if listErr == nil {
72+
for _, r := range records {
73+
if r.Reason == agentsession.CheckpointReasonEndOfTurn &&
74+
checkpoint.IsPerEditRef(r.CodeCheckpointRef) {
75+
fallbackRef = r.CodeCheckpointRef
76+
break
77+
}
78+
}
79+
}
80+
}
81+
guardRecord, guardErr := s.createGuardCheckpoint(ctx, sessionID, record.RunID, guardID, guardWritten, fallbackRef)
6882
if guardErr != nil {
6983
if guardWritten {
7084
_ = s.perEditStore.DeleteCheckpoint(guardID)
@@ -200,8 +214,10 @@ func (s *Service) UndoRestoreCheckpoint(ctx context.Context, sessionID string) (
200214
}
201215

202216
// createGuardCheckpoint 创建 pre_restore_guard 类型的 checkpoint。
203-
// guardWritten=true 时 guardID 对应的 per-edit cp_<id>.json 已写入,CodeCheckpointRef 指向它;否则仅记 session 状态。
204-
func (s *Service) createGuardCheckpoint(ctx context.Context, sessionID, runID, guardID string, guardWritten bool) (agentsession.CheckpointRecord, error) {
217+
// guardWritten=true 时 guardID 对应的 per-edit cp_<id>.json 已写入,CodeCheckpointRef 指向它;
218+
// guardWritten=false 时若 fallbackRef 非空,则用它作为 CodeCheckpointRef 以保证 undo 可走代码恢复路径。
219+
// fallbackRef 应为完整的 "peredit:<id>" 格式引用。
220+
func (s *Service) createGuardCheckpoint(ctx context.Context, sessionID, runID, guardID string, guardWritten bool, fallbackRef string) (agentsession.CheckpointRecord, error) {
205221
session, err := s.sessionStore.LoadSession(ctx, sessionID)
206222
if err != nil {
207223
return agentsession.CheckpointRecord{}, fmt.Errorf("checkpoint: load session for guard: %w", err)
@@ -220,6 +236,8 @@ func (s *Service) createGuardCheckpoint(ctx context.Context, sessionID, runID, g
220236
var ref string
221237
if guardWritten {
222238
ref = checkpoint.RefForPerEditCheckpoint(guardID)
239+
} else if fallbackRef != "" {
240+
ref = fallbackRef
223241
}
224242

225243
now := time.Now()
@@ -446,7 +464,21 @@ func (s *Service) runDiff(ctx context.Context, sessionID, runID string) (Checkpo
446464
return CheckpointDiffResult{}, fmt.Errorf("checkpoint: no code checkpoints found for run_id %s", runID)
447465
}
448466

449-
patch, changes, err := s.perEditStore.RunAggregateDiff(ctx, perEditIDs)
467+
// 查找上一个 run 最后一个 checkpoint 的 FileVersions,用于版本号比较过滤历史文件。
468+
var prevFileVersions map[string]int
469+
if allRecords, listErr := s.checkpointStore.ListCheckpoints(ctx, sessionID, checkpoint.ListCheckpointOpts{}); listErr == nil {
470+
for _, r := range allRecords {
471+
if r.RunID != runID && checkpoint.IsPerEditRef(r.CodeCheckpointRef) {
472+
prevPerEditID := checkpoint.PerEditCheckpointIDFromRef(r.CodeCheckpointRef)
473+
if fv, fvErr := s.perEditStore.GetCheckpointFileVersions(prevPerEditID); fvErr == nil {
474+
prevFileVersions = fv
475+
}
476+
break
477+
}
478+
}
479+
}
480+
481+
patch, changes, err := s.perEditStore.RunAggregateDiff(ctx, perEditIDs, prevFileVersions)
450482
if err != nil {
451483
return CheckpointDiffResult{}, fmt.Errorf("checkpoint: run aggregate diff: %w", err)
452484
}

0 commit comments

Comments
 (0)