Skip to content

Fix multi parallel sessions#879

Open
Soph wants to merge 3 commits intomainfrom
soph/fix-multi-parallel-sessions
Open

Fix multi parallel sessions#879
Soph wants to merge 3 commits intomainfrom
soph/fix-multi-parallel-sessions

Conversation

@Soph
Copy link
Copy Markdown
Collaborator

@Soph Soph commented Apr 8, 2026

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 / no SaveStep) from being condensed into a checkpoint on git commit, even when the session is ACTIVE with 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.

@Soph Soph requested a review from a team as a code owner April 8, 2026 20:22
Copilot AI review requested due to automatic review settings April 8, 2026 20:22
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +213 to +215
// 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.
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
// 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.

Copilot uses AI. Check for mistakes.
Comment on lines +700 to +704
@@ -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,
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment on lines +129 to +136
// 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)
}
}

Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
// 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)
}

Copilot uses AI. Check for mistakes.
Comment on lines +137 to +146
// 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)
}
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
// 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)
}

Copilot uses AI. Check for mistakes.
Comment on lines +251 to +256
// 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)
}
}
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
// 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")
}

Copilot uses AI. Check for mistakes.
Soph and others added 3 commits April 8, 2026 23:09
…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
@Soph Soph force-pushed the soph/fix-multi-parallel-sessions branch from ade99fe to 979445f Compare April 8, 2026 21:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants