Skip to content

Commit 126d8d2

Browse files
committed
fix(tool):修复runtime内部缺少run级diff对比问题
1 parent 8c0fee7 commit 126d8d2

5 files changed

Lines changed: 134 additions & 0 deletions

File tree

internal/runtime/checkpoint_flow_test.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package runtime
22

33
import (
44
"context"
5+
"errors"
56
"os"
67
"path/filepath"
78
"strings"
@@ -776,6 +777,76 @@ func TestCheckpointDiffRejectsMissingStateAndReturnsEmptyWhenNoPreviousSnapshot(
776777
}
777778
}
778779

780+
func TestCreateEndOfTurnCheckpoint_SetsLastCheckpointID(t *testing.T) {
781+
fixture := newRuntimeCheckpointFixture(t)
782+
fixture.captureFile(t, "tracked.go", []byte("package main\n"))
783+
784+
state := newRunState("run-eot-id", fixture.session)
785+
fixture.service.createEndOfTurnCheckpoint(context.Background(), &state, true)
786+
787+
if state.lastEndOfTurnCheckpointID == "" {
788+
t.Fatal("expected lastEndOfTurnCheckpointID to be set after end-of-turn checkpoint creation")
789+
}
790+
791+
records, err := fixture.checkpointStore.ListCheckpoints(context.Background(), fixture.session.ID, checkpoint.ListCheckpointOpts{})
792+
if err != nil {
793+
t.Fatalf("ListCheckpoints() error = %v", err)
794+
}
795+
if len(records) != 1 {
796+
t.Fatalf("records = %#v, want 1", records)
797+
}
798+
wantRef := checkpoint.PerEditCheckpointIDFromRef(records[0].CodeCheckpointRef)
799+
if state.lastEndOfTurnCheckpointID != wantRef {
800+
t.Fatalf("lastEndOfTurnCheckpointID = %q, want %q", state.lastEndOfTurnCheckpointID, wantRef)
801+
}
802+
}
803+
804+
func TestFindPreviousEndOfTurnCheckpoint(t *testing.T) {
805+
spy := &checkpointStoreSpy{
806+
listRecords: []agentsession.CheckpointRecord{
807+
{CheckpointID: "cp-skip-current", SessionID: "session-1", Reason: agentsession.CheckpointReasonEndOfTurn, CodeCheckpointRef: checkpoint.RefForPerEditCheckpoint("cp-skip-current"), RunID: "current-run", Status: agentsession.CheckpointStatusAvailable},
808+
{CheckpointID: "cp-skip-reason", SessionID: "session-1", Reason: agentsession.CheckpointReasonCompact, CodeCheckpointRef: checkpoint.RefForPerEditCheckpoint("cp-skip-reason"), RunID: "old-run", Status: agentsession.CheckpointStatusAvailable},
809+
{CheckpointID: "cp-valid", SessionID: "session-1", Reason: agentsession.CheckpointReasonEndOfTurn, CodeCheckpointRef: checkpoint.RefForPerEditCheckpoint("cp-valid"), RunID: "old-run", Status: agentsession.CheckpointStatusAvailable},
810+
},
811+
}
812+
service := &Service{checkpointStore: spy}
813+
814+
got := service.findPreviousEndOfTurnCheckpoint(context.Background(), "session-1", "current-run")
815+
if got != "cp-valid" {
816+
t.Fatalf("findPreviousEndOfTurnCheckpoint() = %q, want cp-valid", got)
817+
}
818+
if spy.listSessionID != "session-1" || !spy.listOpts.RestorableOnly || spy.listOpts.Limit != 50 {
819+
t.Fatalf("list opts = %#v, want session-1 restorableOnly=true limit=50", spy.listOpts)
820+
}
821+
}
822+
823+
func TestFindPreviousEndOfTurnCheckpoint_NoStore(t *testing.T) {
824+
service := &Service{}
825+
if got := service.findPreviousEndOfTurnCheckpoint(context.Background(), "session-1", "run-1"); got != "" {
826+
t.Fatalf("expected empty, got %q", got)
827+
}
828+
}
829+
830+
func TestFindPreviousEndOfTurnCheckpoint_ListError(t *testing.T) {
831+
spy := &checkpointStoreSpy{listErr: errors.New("db down")}
832+
service := &Service{checkpointStore: spy}
833+
if got := service.findPreviousEndOfTurnCheckpoint(context.Background(), "session-1", "run-1"); got != "" {
834+
t.Fatalf("expected empty on list error, got %q", got)
835+
}
836+
}
837+
838+
func TestFindPreviousEndOfTurnCheckpoint_SkipsNonPerEditRef(t *testing.T) {
839+
spy := &checkpointStoreSpy{
840+
listRecords: []agentsession.CheckpointRecord{
841+
{CheckpointID: "cp-no-ref", SessionID: "session-1", Reason: agentsession.CheckpointReasonEndOfTurn, CodeCheckpointRef: "", RunID: "old-run", Status: agentsession.CheckpointStatusAvailable},
842+
},
843+
}
844+
service := &Service{checkpointStore: spy}
845+
if got := service.findPreviousEndOfTurnCheckpoint(context.Background(), "session-1", "current-run"); got != "" {
846+
t.Fatalf("expected empty when no per-edit ref available, got %q", got)
847+
}
848+
}
849+
779850
func mustReadRuntimeFile(t *testing.T, path string) []byte {
780851
t.Helper()
781852
data, err := os.ReadFile(path)

internal/runtime/checkpoint_gate.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,11 @@ func (s *Service) createEndOfTurnCheckpoint(ctx context.Context, state *runState
6666
defer s.perEditStore.Reset()
6767
if err := s.createCheckpointRecord(ctx, session, runID, state, checkpointID, agentsession.CheckpointReasonEndOfTurn); err != nil {
6868
log.Printf("checkpoint: end-of-turn record: %v", err)
69+
return
6970
}
71+
state.mu.Lock()
72+
state.lastEndOfTurnCheckpointID = checkpointID
73+
state.mu.Unlock()
7074
}
7175

7276
// createCheckpointRecord 写入 SQLite checkpoint 记录 + session 快照,并发出 EventCheckpointCreated。
@@ -191,3 +195,32 @@ func (s *Service) createSessionOnlyCheckpoint(
191195
})
192196
return nil
193197
}
198+
199+
// findPreviousEndOfTurnCheckpoint 查询指定 session 中、不属于当前 run 的最新可用 end_of_turn checkpoint。
200+
// 用于 run-scoped diff 的 baseline 定位;找不到时返回空字符串,不报错。
201+
func (s *Service) findPreviousEndOfTurnCheckpoint(ctx context.Context, sessionID string, currentRunID string) string {
202+
if s.checkpointStore == nil {
203+
return ""
204+
}
205+
records, err := s.checkpointStore.ListCheckpoints(ctx, sessionID, checkpoint.ListCheckpointOpts{
206+
Limit: 50,
207+
RestorableOnly: true,
208+
})
209+
if err != nil {
210+
log.Printf("checkpoint: find previous end-of-turn list failed: %v", err)
211+
return ""
212+
}
213+
for _, r := range records {
214+
if r.Reason != agentsession.CheckpointReasonEndOfTurn {
215+
continue
216+
}
217+
if !checkpoint.IsPerEditRef(r.CodeCheckpointRef) {
218+
continue
219+
}
220+
if r.RunID == currentRunID {
221+
continue
222+
}
223+
return checkpoint.PerEditCheckpointIDFromRef(r.CodeCheckpointRef)
224+
}
225+
return ""
226+
}

internal/runtime/events.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -432,6 +432,8 @@ const (
432432
EventCheckpointUndoRestore EventType = "checkpoint_undo_restore"
433433
// EventBashSideEffect 表示 bash 命令在 workdir 内产生了文件变更。
434434
EventBashSideEffect EventType = "bash_side_effect"
435+
// EventRunDiffSummary 表示一次完整 run 的端到端代码变更摘要已生成。
436+
EventRunDiffSummary EventType = "run_diff_summary"
435437
)
436438

437439
// TokenUsagePayload 承载单轮 token 用量统计。
@@ -500,6 +502,14 @@ type ToolDiffPayload struct {
500502
Diffs []FileDiffEntry `json:"diffs,omitempty"`
501503
}
502504

505+
// RunDiffSummaryPayload 描述一次完整 run 结束时的端到端代码变更摘要。
506+
type RunDiffSummaryPayload struct {
507+
FromCheckpointID string `json:"from_checkpoint_id,omitempty"`
508+
ToCheckpointID string `json:"to_checkpoint_id,omitempty"`
509+
Diff string `json:"diff,omitempty"`
510+
ChangedFiles []FileDiffEntry `json:"changed_files,omitempty"`
511+
}
512+
503513
// BashSideEffectPayload 描述 bash 命令在 workdir 内的文件变更。
504514
type BashSideEffectPayload struct {
505515
ToolCallID string `json:"tool_call_id"`

internal/runtime/run.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,23 @@ func (s *Service) Run(ctx context.Context, input UserInput) (err error) {
101101
}
102102
s.updateResumeCheckpoint(runCtx, statePtr, "stopped", completion)
103103
}
104+
if statePtr != nil && s.perEditStore != nil && statePtr.baselineCheckpointID != "" && statePtr.lastEndOfTurnCheckpointID != "" {
105+
diffStr, _ := s.perEditStore.Diff(context.Background(), statePtr.baselineCheckpointID, statePtr.lastEndOfTurnCheckpointID)
106+
files, _ := s.perEditStore.ChangedFiles(context.Background(), statePtr.baselineCheckpointID, statePtr.lastEndOfTurnCheckpointID)
107+
var changedFiles []FileDiffEntry
108+
for _, f := range files {
109+
changedFiles = append(changedFiles, FileDiffEntry{
110+
Path: f.Path,
111+
Kind: string(f.Kind),
112+
})
113+
}
114+
s.emitRunScopedOptional(EventRunDiffSummary, statePtr, RunDiffSummaryPayload{
115+
FromCheckpointID: statePtr.baselineCheckpointID,
116+
ToCheckpointID: statePtr.lastEndOfTurnCheckpointID,
117+
Diff: diffStr,
118+
ChangedFiles: changedFiles,
119+
})
120+
}
104121
s.emitRunTermination(runCtx, input, statePtr, err)
105122
}()
106123
ctx = runCtx
@@ -187,6 +204,7 @@ func (s *Service) Run(ctx context.Context, input UserInput) (err error) {
187204
s.updateResumeCheckpoint(ctx, &state, "plan", "")
188205

189206
maxTurns := resolveRuntimeMaxTurns(initialCfg.Runtime)
207+
state.baselineCheckpointID = s.findPreviousEndOfTurnCheckpoint(ctx, sessionID, input.RunID)
190208
for turn := 0; ; turn++ {
191209
if turn >= maxTurns {
192210
state.maxTurnsReached = true

internal/runtime/state.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ type runState struct {
5050
hasUnknownUsage bool
5151
completion controlplane.CompletionState
5252
progress controlplane.ProgressState
53+
lastEndOfTurnCheckpointID string
54+
baselineCheckpointID string
5355
hookAnnotations []string
5456
hookNotifications []queuedHookNotification
5557
hookNotificationSeen map[string]time.Time

0 commit comments

Comments
 (0)