Skip to content

Commit cd76a73

Browse files
dipreeclaude
andcommitted
Address review: gate window reset on a real turn advance + fix comments
- Only re-anchor the prompt window when SessionTurnCount actually increases, so a repeated/stale hook (same cumulative TurnCount) no longer clears the deferred reset early and break back-to-back checkpoint counts (Cursor bug). Adds a regression test. - Correct the checkpointStepCount doc (attach uses attachStepCount, not this path) and the WriteCommittedOptions.CheckpointsCount comment (now a prompt "steps" count, not a checkpoint count). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 615839a commit cd76a73

4 files changed

Lines changed: 41 additions & 8 deletions

File tree

cmd/entire/cli/checkpoint/checkpoint.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,9 @@ type WriteCommittedOptions struct {
227227
// FilesTouched are files modified during the session
228228
FilesTouched []string
229229

230-
// CheckpointsCount is the number of checkpoints in this session
230+
// CheckpointsCount is the displayed "steps" count for this session: the number
231+
// of user prompts attributed to this checkpoint (floored at 1). Despite the
232+
// historical name/JSON tag, it is no longer a count of checkpoints.
231233
CheckpointsCount int
232234

233235
// SaveStepCount is the number of SaveStep-recorded steps (shadow-branch

cmd/entire/cli/lifecycle.go

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1121,11 +1121,14 @@ func persistEventMetadataToState(event *agent.Event, state *strategy.SessionStat
11211121
} else if event.Type == agent.TurnEnd {
11221122
state.SessionTurnCount++
11231123
}
1124-
// Deferred checkpoint-window reset: the first time a turn is counted after a
1125-
// checkpoint was written, re-anchor the window base to the turn count from
1126-
// before this turn so the current turn becomes the first prompt of the new
1127-
// window. Until then, back-to-back checkpoints keep reporting the same count.
1128-
if (event.TurnCount > 0 || event.Type == agent.TurnEnd) && state.PromptWindowResetPending {
1124+
// Deferred checkpoint-window reset: the first time the turn count actually
1125+
// advances after a checkpoint was written, re-anchor the window base to the
1126+
// count from before this turn so the current turn becomes the first prompt of
1127+
// the new window. Gate on a real advance (not just a TurnEnd / non-zero
1128+
// TurnCount) so a repeated or stale hook reporting the same cumulative count
1129+
// doesn't re-anchor early — that would make a later back-to-back checkpoint
1130+
// report 1 instead of matching the prior count.
1131+
if state.SessionTurnCount > prevTurnCount && state.PromptWindowResetPending {
11291132
state.PromptWindowBase = prevTurnCount
11301133
state.PromptWindowResetPending = false
11311134
}

cmd/entire/cli/lifecycle_test.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2038,3 +2038,30 @@ func TestPromptWindowExecModeCumulativeTurnCount(t *testing.T) {
20382038
t.Fatalf("exec checkpoint B = %d, want 2", got)
20392039
}
20402040
}
2041+
2042+
// TestPromptWindowStaleHookDoesNotResetEarly guards against a repeated/stale hook
2043+
// (same cumulative TurnCount, so the count doesn't actually advance) clearing the
2044+
// deferred reset early. If it did, a later back-to-back checkpoint would report 1
2045+
// instead of matching the prior checkpoint's count.
2046+
func TestPromptWindowStaleHookDoesNotResetEarly(t *testing.T) {
2047+
exec := func(s *strategy.SessionState, cumulative int) {
2048+
persistEventMetadataToState(&agent.Event{Type: agent.TurnEnd, TurnCount: cumulative}, s)
2049+
}
2050+
2051+
s := &strategy.SessionState{}
2052+
exec(s, 1)
2053+
exec(s, 2)
2054+
exec(s, 3)
2055+
if got := writeCheckpoint(s); got != 3 {
2056+
t.Fatalf("checkpoint A = %d, want 3", got)
2057+
}
2058+
2059+
// Stale hook: same cumulative count, no real advance — must not re-anchor.
2060+
exec(s, 3)
2061+
if !s.PromptWindowResetPending {
2062+
t.Fatalf("stale hook should not clear ResetPending")
2063+
}
2064+
if got := writeCheckpoint(s); got != 3 {
2065+
t.Fatalf("back-to-back checkpoint B after stale hook = %d, want 3", got)
2066+
}
2067+
}

cmd/entire/cli/strategy/manual_commit_condensation.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,8 +105,9 @@ var redactSessionJSONLBytes = redact.JSONLBytes
105105
// checkpoint being written: the turns counted since the current window's base.
106106
// The base is re-anchored (deferred) the next time a turn is counted after a
107107
// checkpoint write, so back-to-back checkpoints with no prompt between them share
108-
// a count. Floored at 1 so we never record 0 (covers attach, a fast-path
109-
// checkpoint before any turn, and exec-mode gaps where turns weren't counted).
108+
// a count. Floored at 1 so we never record 0 (covers a fast-path checkpoint
109+
// before any turn, and exec-mode gaps where turns weren't counted). Attach has
110+
// its own count (see attachStepCount); it does not go through this path.
110111
func checkpointStepCount(s *SessionState) int {
111112
if w := s.SessionTurnCount - s.PromptWindowBase; w >= 1 {
112113
return w

0 commit comments

Comments
 (0)