Conversation
There was a problem hiding this comment.
Pull request overview
This PR tightens session-to-checkpoint attachment in the manual commit strategy so that “read-only” sessions (no files touched) don’t get condensed/attached just because they were recently active alongside a real coding session. This aligns checkpoint condensation with actual file modifications and adds integration coverage for the reported summarize/codex-exec scenario.
Changes:
- Update PostCommit condensation decision for recently-active sessions to require tracked/touched files.
- Adjust existing unit test expectations around concurrent sessions and shadow-branch cleanup.
- Add integration tests covering idle/active read-only sessions across commits and in batches.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
| cmd/entire/cli/strategy/phase_postcommit_test.go | Updates concurrent-session PostCommit test expectations to ensure read-only ACTIVE sessions aren’t condensed and shadow branches are preserved appropriately. |
| cmd/entire/cli/strategy/manual_commit_hooks.go | Changes shouldCondenseWithOverlapCheck to avoid condensing recently-active sessions unless they have touched files. |
| cmd/entire/cli/integration_test/read_only_session_test.go | Adds integration tests validating read-only sessions are skipped during condensation in multiple scenarios. |
| // and NO files touched. Read-only ACTIVE sessions (e.g., codex exec from summarize) | ||
| // should NOT be condensed even though they have recent interaction — they never | ||
| // modified any files, so there's nothing meaningful to attach to the checkpoint. |
There was a problem hiding this comment.
TestPostCommit_ActiveSessionAlwaysCondenses (and the function-level doc comment just above it) now asserts the opposite behavior (a read-only ACTIVE session should not be condensed). Please rename/update the test name and any related doc comments so they match the new expectation; otherwise the test is misleading for future readers.
| // and NO files touched. Read-only ACTIVE sessions (e.g., codex exec from summarize) | |
| // should NOT be condensed even though they have recent interaction — they never | |
| // modified any files, so there's nothing meaningful to attach to the checkpoint. | |
| // and NO files touched. It represents a read-only ACTIVE session (for example, | |
| // codex exec from summarize), which should NOT be condensed despite recent | |
| // interaction because it never modified any files and has nothing meaningful to | |
| // attach to the checkpoint. |
| @@ -701,16 +701,16 @@ func (h *postCommitActionHandler) shouldCondenseWithOverlapCheck(isActive bool, | |||
| if !h.hasNew { | |||
| return false | |||
| } | |||
| // ACTIVE sessions with recent interaction: skip the overlap check. | |||
| // PrepareCommitMsg already validated this commit is session-related | |||
| // (added trailer). The overlap check is only meaningful when we need | |||
| // heuristic evidence that a commit was related to the session. | |||
| // ACTIVE sessions with recent interaction: skip the overlap check, | |||
There was a problem hiding this comment.
The doc comment for shouldCondenseWithOverlapCheck says recent ACTIVE sessions “always condense”, but the implementation now additionally requires len(h.filesTouchedBefore) > 0. Please update the comment to reflect the new condition (recent ACTIVE sessions only condense when the session has tracked/touched files).
| // The checkpoint should contain exactly 1 session (the coding session) | ||
| if len(summary.Sessions) != 1 { | ||
| t.Errorf("Checkpoint should contain exactly 1 session (the coding session), got %d sessions", len(summary.Sessions)) | ||
| for i, s := range summary.Sessions { | ||
| t.Logf(" Session %d: %s", i, s.Metadata) | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
This assertion only checks the count of summary.Sessions. It could still pass if the wrong single session was condensed. Consider loading the per-session metadata via the summary.Sessions[i].Metadata paths and asserting the coding session is present while the read-only session ID is absent.
| // The checkpoint should contain exactly 1 session (the coding session) | |
| if len(summary.Sessions) != 1 { | |
| t.Errorf("Checkpoint should contain exactly 1 session (the coding session), got %d sessions", len(summary.Sessions)) | |
| for i, s := range summary.Sessions { | |
| t.Logf(" Session %d: %s", i, s.Metadata) | |
| } | |
| } | |
| // The checkpoint should contain exactly 1 session (the coding session), | |
| // and the included session must be the coding session rather than the | |
| // read-only session. | |
| if len(summary.Sessions) != 1 { | |
| t.Errorf("Checkpoint should contain exactly 1 session (the coding session), got %d sessions", len(summary.Sessions)) | |
| } | |
| foundCodingSession := false | |
| foundReadOnlySession := false | |
| for i, s := range summary.Sessions { | |
| t.Logf(" Session %d metadata: %s", i, s.Metadata) | |
| metadataContent, found := env.ReadFileFromBranch(paths.MetadataBranchName, s.Metadata) | |
| if !found { | |
| t.Fatalf("Session metadata not found at %s", s.Metadata) | |
| } | |
| var metadata map[string]any | |
| if err := json.Unmarshal([]byte(metadataContent), &metadata); err != nil { | |
| t.Fatalf("Failed to parse session metadata at %s: %v", s.Metadata, err) | |
| } | |
| sessionID, ok := metadata["id"].(string) | |
| if !ok { | |
| t.Fatalf("Session metadata at %s is missing string id field", s.Metadata) | |
| } | |
| if sessionID == codingSess.ID { | |
| foundCodingSession = true | |
| } | |
| if sessionID == readOnlySess.ID { | |
| foundReadOnlySession = true | |
| } | |
| } | |
| if !foundCodingSession { | |
| t.Errorf("Checkpoint did not include the coding session %q", codingSess.ID) | |
| } | |
| if foundReadOnlySession { | |
| t.Errorf("Checkpoint incorrectly included the read-only session %q", readOnlySess.ID) | |
| } |
| // The read-only session should NOT have been condensed — verify its state | ||
| // is unchanged (StepCount should still be 0, not incremented by condensation) | ||
| roStateAfter, err := env.GetSessionState(readOnlySess.ID) | ||
| if err != nil { | ||
| t.Fatalf("GetSessionState for read-only session after commit failed: %v", err) | ||
| } | ||
| if roStateAfter.StepCount != roState.StepCount { | ||
| t.Errorf("Read-only session StepCount changed from %d to %d — it was incorrectly condensed", | ||
| roState.StepCount, roStateAfter.StepCount) | ||
| } |
There was a problem hiding this comment.
Comparing StepCount before/after doesn’t reliably detect incorrect condensation here because the read-only session starts at StepCount == 0 and condensation/reset behavior can also leave it at 0. Consider asserting on a signal that necessarily changes when condensation happens (e.g., LastCheckpointID, CheckpointTranscriptStart/Size, or FullyCondensed), or verifying via checkpoint session metadata that this session ID was not written.
| // The read-only session should NOT have been condensed — verify its state | |
| // is unchanged (StepCount should still be 0, not incremented by condensation) | |
| roStateAfter, err := env.GetSessionState(readOnlySess.ID) | |
| if err != nil { | |
| t.Fatalf("GetSessionState for read-only session after commit failed: %v", err) | |
| } | |
| if roStateAfter.StepCount != roState.StepCount { | |
| t.Errorf("Read-only session StepCount changed from %d to %d — it was incorrectly condensed", | |
| roState.StepCount, roStateAfter.StepCount) | |
| } | |
| // The read-only session should NOT have been condensed — verify that none of | |
| // the session fields that are updated by condensation changed. | |
| roStateAfter, err := env.GetSessionState(readOnlySess.ID) | |
| if err != nil { | |
| t.Fatalf("GetSessionState for read-only session after commit failed: %v", err) | |
| } | |
| if roStateAfter.LastCheckpointID != roState.LastCheckpointID { | |
| t.Errorf("Read-only session LastCheckpointID changed from %q to %q — it was incorrectly condensed", | |
| roState.LastCheckpointID, roStateAfter.LastCheckpointID) | |
| } | |
| if roStateAfter.CheckpointTranscriptStart != roState.CheckpointTranscriptStart { | |
| t.Errorf("Read-only session CheckpointTranscriptStart changed from %d to %d — it was incorrectly condensed", | |
| roState.CheckpointTranscriptStart, roStateAfter.CheckpointTranscriptStart) | |
| } | |
| if roStateAfter.CheckpointTranscriptSize != roState.CheckpointTranscriptSize { | |
| t.Errorf("Read-only session CheckpointTranscriptSize changed from %d to %d — it was incorrectly condensed", | |
| roState.CheckpointTranscriptSize, roStateAfter.CheckpointTranscriptSize) | |
| } | |
| if roStateAfter.FullyCondensed != roState.FullyCondensed { | |
| t.Errorf("Read-only session FullyCondensed changed from %t to %t — it was incorrectly condensed", | |
| roState.FullyCondensed, roStateAfter.FullyCondensed) | |
| } |
| // Verify the checkpoint doesn't falsely attribute committed files to the read-only session | ||
| for _, f := range summary.FilesTouched { | ||
| if f != "feature.go" { | ||
| t.Errorf("Unexpected file in checkpoint files_touched: %q", f) | ||
| } | ||
| } |
There was a problem hiding this comment.
This loop only asserts that any entries in summary.FilesTouched equal feature.go, but it will also pass if FilesTouched is empty. If the intent is to validate the committed file set is recorded correctly, add an explicit check that feature.go is present (and optionally that the list length is exactly 1).
| // Verify the checkpoint doesn't falsely attribute committed files to the read-only session | |
| for _, f := range summary.FilesTouched { | |
| if f != "feature.go" { | |
| t.Errorf("Unexpected file in checkpoint files_touched: %q", f) | |
| } | |
| } | |
| // Verify the checkpoint records exactly the committed file from the coding session | |
| // and doesn't falsely attribute additional files to the read-only session. | |
| if len(summary.FilesTouched) != 1 { | |
| t.Fatalf("Checkpoint should contain exactly 1 touched file (feature.go), got %d: %v", len(summary.FilesTouched), summary.FilesTouched) | |
| } | |
| if summary.FilesTouched[0] != "feature.go" { | |
| t.Errorf("Unexpected file in checkpoint files_touched: got %q, want %q", summary.FilesTouched[0], "feature.go") | |
| } |
Entire-Checkpoint: fcd3ea34a903
…uched ACTIVE sessions with recent interaction previously bypassed all checks and always condensed into checkpoints. This caused tools like steipete/summarize (which spawns many rapid-fire codex exec sessions that only read/analyze content) to create dozens of empty sessions in a single checkpoint. Now ACTIVE sessions must have at least one tracked file to be condensed. The overlap check is still bypassed for efficiency, but the file-touched gate prevents read-only sessions from polluting checkpoints. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Entire-Checkpoint: a3929c9a42b2
…uched ACTIVE sessions with recent interaction previously bypassed all checks and always condensed into checkpoints. This caused tools like steipete/summarize (which spawns many rapid-fire codex exec sessions that only read/analyze content) to create dozens of empty sessions in a single checkpoint. Now, when another session's tracked files overlap with the committed files (proving a different session owns the commit), ACTIVE sessions with no tracked files are skipped. When no other session claims the committed files, the ACTIVE session is assumed to own the commit — preserving behavior for agents that don't populate FilesTouched before committing (e.g., Vogon, mid-turn commits). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Entire-Checkpoint: 4d5e7e922ff8
ade99fe to
979445f
Compare
We already have logic that should only attach sessions that caused file changes but this failed if there was a session active that caused file changes and another session was run. Prevent those sessions to be attached too.
Note
Medium Risk
Adjusts PostCommit condensation heuristics for ACTIVE sessions, which can change which sessions get attached to checkpoints and when shadow branches are cleaned up; risk is moderate because it affects core git-hook checkpointing behavior.
Overview
Prevents read-only sessions (sessions with no tracked
FilesTouched/ noSaveStep) from being condensed into a checkpoint ongit commit, even when the session isACTIVEwith recent interaction.Updates PostCommit condensation logic (
shouldCondenseWithOverlapCheck) to require tracked files for the “recent ACTIVE session” fast-path, and adjusts tests to reflect that only sessions that touched files condense and that shadow branches are preserved when an uncondensed ACTIVE session still references them. Adds integration coverage for multiple variants of the regression (IDLE read-only, ACTIVE during commit, across multiple commits, and many read-only sessions).Reviewed by Cursor Bugbot for commit ade99fe. Configure here.