Skip to content

Rework multi session flow#131

Closed
Soph wants to merge 4 commits intomainfrom
soph/improved-shadow-v2
Closed

Rework multi session flow#131
Soph wants to merge 4 commits intomainfrom
soph/improved-shadow-v2

Conversation

@Soph
Copy link
Collaborator

@Soph Soph commented Jan 31, 2026

This is an alternative approach to #125

This branch implements shadow branch overlap detection to fix the "ghost files" bug where dismissed work from previous sessions would remain in the shadow branch.

Key changes:

  • Added PromptHooks interface with OnPromptStart() and OnPromptEnd() methods to the strategy pattern
  • OnPromptStart() checks if worktree files overlap with shadow branch files before the prompt runs
  • If no overlap detected (user dismissed previous work via git restore or git stash), the shadow branch is automatically reset on the next checkpoint
  • Shadow branch reset deletes the branch and rebuilds it from HEAD tree + new session changes
  • Includes untracked files in overlap detection to handle files created by Claude in previous sessions

Result: When users dismiss their work and start fresh on different files, the shadow branch is automatically cleaned up instead of accumulating ghost files from previous sessions.

More detailed design in docs/architecture/shadow-branch-overlap.md


Note

Medium Risk
Medium risk because it changes checkpointing behavior in the manual-commit strategy (including deleting/resetting shadow branches) and alters CLI/settings semantics for multi-session warnings, which could affect existing workflows if misconfigured.

Overview
Implements shadow branch overlap detection for the manual-commit strategy to prevent “ghost files” when users dismiss prior work (e.g., git restore/git stash): a new strategy.PromptHooks (OnPromptStart/OnPromptEnd) sets ShouldResetShadowBranch based on worktree-vs-shadow file overlap, and SaveChanges will reset (delete/rebuild) the shadow branch on the next checkpoint when divergence is detected.

Reworks multi-session warnings to be opt-in (enable_multisession_warning) with updated entire enable --enable-multisession-warning flag, hook gating logic, and suppression messaging; updates integration tests accordingly and adds new integration coverage for overlap/reset scenarios plus a design doc (docs/architecture/shadow-branch-overlap.md).

Written by Cursor Bugbot for commit d11ad3f. This will update automatically on new commits. Configure here.

Soph and others added 2 commits January 31, 2026 09:07
- Add architecture document for single-branch overlap detection approach
- Implement comprehensive integration tests for all 6 scenarios
- Add multi-session warning feature tests
- Tests follow TDD approach (written before implementation)

Scenarios covered:
1. Continue work (file overlap detected)
2. Dismiss and start fresh (no overlap, reset branch)
3. Partial dismiss (some overlap, continue)
4. Stash, answer questions, unstash (safe)
5. Stash, new work on same files (reset correctly)
6. Stash, new work on different files (accepted limitation)

Multi-session warning:
- Opt-in warning when starting session with uncommitted work
- Tests for enabled/disabled states
- Tests for edge cases (committed work, different base commit)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Entire-Checkpoint: 1e868c83dbb7
Entire-Checkpoint: 8ea9e4a4e751
@Soph Soph requested a review from a team as a code owner January 31, 2026 08:58
Copilot AI review requested due to automatic review settings January 31, 2026 08:58
Copy link
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 implements shadow branch overlap detection to fix the "ghost files" bug where dismissed work from previous sessions would remain in the shadow branch. It adds a PromptHooks interface with OnPromptStart() and OnPromptEnd() methods that check file overlap between the worktree and shadow branch before each prompt runs. When no overlap is detected (indicating the user dismissed previous work), the shadow branch is automatically reset on the next checkpoint.

Changes:

  • Adds PromptHooks interface with OnPromptStart() and OnPromptEnd() methods for detecting worktree/shadow branch overlap
  • Implements ShouldResetShadowBranch flag in session state to track when shadow branch should be reset
  • Adds comprehensive integration tests covering 6 scenarios including continue work, dismiss and start fresh, partial dismiss, stash workflows

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
docs/architecture/shadow-branch-overlap.md Architecture documentation describing single-branch overlap detection approach (includes unimplemented multi-session warning feature)
cmd/entire/cli/strategy/strategy.go Defines PromptHooks interface with OnPromptStart/OnPromptEnd methods
cmd/entire/cli/strategy/manual_commit_types.go Adds ShouldResetShadowBranch field to SessionState
cmd/entire/cli/session/state.go Adds ShouldResetShadowBranch field to session.State
cmd/entire/cli/strategy/manual_commit_hooks.go Implements OnPromptStart and OnPromptEnd hook handlers for overlap detection
cmd/entire/cli/strategy/manual_commit_git.go Updates SaveChanges to check and use ShouldResetShadowBranch flag
cmd/entire/cli/strategy/manual_commit.go Adds helper functions getFilesTouchedByShadow and fileSetIntersection
cmd/entire/cli/hooks_claudecode_handlers.go Integrates PromptHooks calls into UserPromptSubmit and Stop hooks
cmd/entire/cli/integration_test/shadow_branch_overlap_test.go Integration tests for 6 overlap detection scenarios
cmd/entire/cli/integration_test/multi_session_warning_test.go Integration tests for unimplemented multi-session warning feature

Comment on lines +280 to +293
// If there's no parent (first checkpoint), return all files in shadow tree
if len(shadowCommit.ParentHashes) == 0 {
var files []string
err := shadowTree.Files().ForEach(func(f *object.File) error {
// Skip .entire directory
if paths.IsInfrastructurePath(f.Name) {
return nil
}
files = append(files, f.Name)
return nil
})
if err != nil {
return nil, fmt.Errorf("failed to iterate shadow tree files: %w", err)
}
Copy link

Copilot AI Jan 31, 2026

Choose a reason for hiding this comment

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

When the shadow branch has no parent (first checkpoint), the function iterates through all files in the shadow tree and filters out infrastructure files. However, this approach misses an important case: deleted files.

If a file exists in the base commit but was deleted in the shadow branch checkpoint, it won't appear in shadowTree.Files() because it's not present in the tree. However, this file WAS "touched" by the session (deleted), and should be included in the overlap detection.

Consider this scenario:

  1. Session A deletes file1.ts (exists in base, deleted in checkpoint)
  2. User runs git checkout . to restore file1.ts
  3. OnPromptStart checks overlap: shadowFiles won't include file1.ts (it's deleted)
  4. worktree has file1.ts (restored)
  5. No overlap detected → incorrectly resets shadow branch

To fix this, when there's no parent, we should compare the shadow tree against the base tree to find ALL touched files (added, modified, AND deleted), similar to how it's done when there IS a parent (lines 307-321).

Suggested change
// If there's no parent (first checkpoint), return all files in shadow tree
if len(shadowCommit.ParentHashes) == 0 {
var files []string
err := shadowTree.Files().ForEach(func(f *object.File) error {
// Skip .entire directory
if paths.IsInfrastructurePath(f.Name) {
return nil
}
files = append(files, f.Name)
return nil
})
if err != nil {
return nil, fmt.Errorf("failed to iterate shadow tree files: %w", err)
}
// If there's no parent (first checkpoint), compute diff against base (HEAD) tree
if len(shadowCommit.ParentHashes) == 0 {
headRef, err := repo.Head()
if err != nil {
return nil, fmt.Errorf("failed to get HEAD reference: %w", err)
}
headCommit, err := repo.CommitObject(headRef.Hash())
if err != nil {
return nil, fmt.Errorf("failed to get HEAD commit: %w", err)
}
baseTree, err := headCommit.Tree()
if err != nil {
return nil, fmt.Errorf("failed to get HEAD tree: %w", err)
}
changes, err := baseTree.Diff(shadowTree)
if err != nil {
return nil, fmt.Errorf("failed to diff base and shadow trees: %w", err)
}
files := make([]string, 0, len(changes))
for _, change := range changes {
// Get file path (from either source or destination)
var name string
if change.To.Name != "" {
name = change.To.Name
} else {
name = change.From.Name
}
if name == "" {
continue
}
// Skip .entire directory and other infrastructure paths
if paths.IsInfrastructurePath(name) {
continue
}
files = append(files, name)
}

Copilot uses AI. Check for mistakes.
Comment on lines +314 to +318
// We simulate a prompt but don't write files or call Stop
if err := env.SimulateUserPromptSubmit(session1.ID); err != nil {
t.Fatalf("SimulateUserPromptSubmit (session 2) failed: %v", err)
}
// Note: No SimulateStop call here - simulating that the session just answered questions
Copy link

Copilot AI Jan 31, 2026

Choose a reason for hiding this comment

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

This test simulates a prompt that doesn't create a checkpoint by not calling SimulateStop. However, this doesn't match real-world behavior where the Stop hook would still be called even if no checkpoint is created.

In reality, when an AI session ends (user asks a question that doesn't result in code changes), the Stop hook would still be invoked. The current test skips the Stop hook entirely, which means OnPromptEnd is never called to clear the ShouldResetShadowBranch flag. This could cause the flag to incorrectly persist to the next prompt.

The test should call SimulateStop even when no code changes are made, to properly test the scenario where OnPromptEnd clears the flag after a no-op prompt.

Suggested change
// We simulate a prompt but don't write files or call Stop
if err := env.SimulateUserPromptSubmit(session1.ID); err != nil {
t.Fatalf("SimulateUserPromptSubmit (session 2) failed: %v", err)
}
// Note: No SimulateStop call here - simulating that the session just answered questions
// We simulate a prompt but don't write files; Stop is still called to mirror real behavior
if err := env.SimulateUserPromptSubmit(session1.ID); err != nil {
t.Fatalf("SimulateUserPromptSubmit (session 2) failed: %v", err)
}
if err := env.SimulateStop(session1.ID); err != nil {
t.Fatalf("SimulateStop (session 2) failed: %v", err)
}

Copilot uses AI. Check for mistakes.
Comment on lines +537 to +714
## Multi-Session Warning

### Current Behavior

When a second session starts while there's an existing session with uncommitted checkpoints, both sessions can proceed and their checkpoints will interleave on the shadow branch. This can be confusing for users who may not realize there's already an active session.

### Opt-In Warning (Inverted Setting)

Add an opt-in warning that alerts users when starting a new session while another session has uncommitted work:

**Setting:** `enable-multisession-warning` (default: `false`)

When enabled:

1. **At session start**, check if there's an existing session with:
- Same base commit
- Uncommitted checkpoints (shadow branch exists, not yet condensed)

2. **If found**, show warning:
```
Warning: An existing session has uncommitted work on this branch.

Session ID: 2026-01-31-abc123
Checkpoints: 3 uncommitted
Files touched: src/auth.go, src/server.go

Starting a new session will merge with the existing session.
- If files overlap: Continue on same shadow branch
- If no overlap: Reset shadow branch (old session data will be lost)

Options:
1. Cancel and commit existing work first
2. Continue (merge sessions)
```

3. **If user chooses to continue**, the normal overlap detection proceeds:
- Prompt start checks overlap
- Sets `ShouldResetShadowBranch` flag
- On checkpoint, follows normal reset/continue logic

### Implementation

```go
// In strategy/manual_commit_hooks.go

type MultiSessionWarningConfig struct {
Enabled bool `json:"enabled"`
}

func (s *ManualCommitStrategy) CheckMultiSessionWarning(
ctx context.Context,
sessionID string,
) error {
// Check if warning is enabled
config := s.getMultiSessionWarningConfig()
if !config.Enabled {
return nil
}

state, err := s.stateStore.LoadSessionState(sessionID)
if err != nil {
return err
}

// Find other sessions with same base commit
allSessions, err := s.stateStore.ListSessionStates()
if err != nil {
return err
}

var conflictingSessions []SessionState
for _, otherState := range allSessions {
if otherState.SessionID == sessionID {
continue
}
if otherState.BaseCommit == state.BaseCommit && otherState.CheckpointCount > 0 {
conflictingSessions = append(conflictingSessions, otherState)
}
}

if len(conflictingSessions) == 0 {
return nil
}

// Show warning and prompt user
return s.showMultiSessionWarning(ctx, conflictingSessions)
}

func (s *ManualCommitStrategy) showMultiSessionWarning(
ctx context.Context,
conflictingSessions []SessionState,
) error {
// Build warning message
var msg strings.Builder
msg.WriteString("Warning: Existing session(s) with uncommitted work:\n\n")

for _, session := range conflictingSessions {
msg.WriteString(fmt.Sprintf("Session ID: %s\n", session.SessionID))
msg.WriteString(fmt.Sprintf("Checkpoints: %d uncommitted\n", session.CheckpointCount))
if len(session.FilesTouched) > 0 {
msg.WriteString(fmt.Sprintf("Files touched: %s\n", strings.Join(session.FilesTouched, ", ")))
}
msg.WriteString("\n")
}

msg.WriteString("Starting a new session will merge with the existing session.\n")
msg.WriteString("- If files overlap: Continue on same shadow branch\n")
msg.WriteString("- If no overlap: Reset shadow branch (old session data will be lost)\n\n")

// Prompt user
var shouldContinue bool
prompt := huh.NewConfirm().
Title(msg.String()).
Description("Continue with new session?").
Affirmative("Continue (merge sessions)").
Negative("Cancel").
Value(&shouldContinue)

if err := prompt.Run(); err != nil {
return err
}

if !shouldContinue {
return fmt.Errorf("session cancelled by user")
}

return nil
}
```

### Migration from Old Setting

The old `disable-multisession-warning` setting is inverted:

| Old Setting | New Setting | Behavior |
|-------------|-------------|----------|
| Not set (default) | `enable-multisession-warning: false` | No warning (default) |
| `disable-multisession-warning: true` | `enable-multisession-warning: false` | No warning |
| `disable-multisession-warning: false` | `enable-multisession-warning: true` | Show warning |

Migration logic:
```go
func migrateMultiSessionWarningSetting(config *Config) {
if config.Has("disable-multisession-warning") {
oldValue := config.Get("disable-multisession-warning").(bool)
config.Set("enable-multisession-warning", !oldValue)
config.Delete("disable-multisession-warning")
}
}
```

### When to Check

The multi-session warning check should happen:

1. **At session initialization** - when a new session is created or resumed
2. **Before OnPromptStart** - so user can cancel before any overlap checking

Flow:
```
Session start
Check multi-session warning (if enabled)
↓ (user confirms)
OnPromptStart (check overlap, set flag)
User prompt
SaveChanges (use flag to reset or continue)
```

### Benefits

- **Explicit opt-in** - users who want safety warnings can enable it
- **Clear consequences** - warning explains what will happen (merge or reset)
- **Escape hatch** - users can cancel and commit existing work first
- **No change to default behavior** - warning is off by default, doesn't interrupt existing workflows

Copy link

Copilot AI Jan 31, 2026

Choose a reason for hiding this comment

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

The documentation describes a multi-session warning feature with CheckMultiSessionWarning function and enable_multisession_warning setting (lines 537-714 in docs/architecture/shadow-branch-overlap.md), but this functionality is not implemented in the code. The documentation includes detailed implementation pseudocode that doesn't exist in the actual codebase.

Either the documentation should be marked as future work/not implemented, or the feature needs to be implemented to match the documentation. Having detailed implementation documentation for non-existent features creates confusion for maintainers and reviewers.

Copilot uses AI. Check for mistakes.
Comment on lines +109 to +111
// IsFirstCheckpoint should only be true for the VERY FIRST checkpoint of the session,
// not after a reset (which happens within an existing session)
isFirstCheckpointOfSession := state.CheckpointCount == 0 && !wasReset
Copy link

Copilot AI Jan 31, 2026

Choose a reason for hiding this comment

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

The isFirstCheckpointOfSession calculation excludes checkpoints after a reset (!wasReset), but this is incorrect based on how IsFirstCheckpoint is used in checkpoint/temporary.go.

According to temporary.go line 78-84, when IsFirstCheckpoint is true, the checkpoint captures ALL files in the working directory, not just modified files. This is necessary when starting fresh with a new shadow branch.

When the shadow branch is reset (wasReset = true):

  1. The shadow branch is deleted
  2. A new empty shadow branch will be created
  3. The checkpoint needs to build a complete tree from scratch
  4. Therefore, IsFirstCheckpoint should be TRUE to capture all working directory files

The current logic isFirstCheckpointOfSession := state.CheckpointCount == 0 && !wasReset means:

  • After a reset, CheckpointCount is set to 0 (line 77)
  • But !wasReset is false
  • So isFirstCheckpointOfSession is false
  • The checkpoint will only capture modified files (line 85 in temporary.go)
  • But the shadow branch tree is empty, so we need all files, not just modified ones

This will cause incomplete shadow branch state after a reset. The condition should be: isFirstCheckpointOfSession := state.CheckpointCount == 0

Suggested change
// IsFirstCheckpoint should only be true for the VERY FIRST checkpoint of the session,
// not after a reset (which happens within an existing session)
isFirstCheckpointOfSession := state.CheckpointCount == 0 && !wasReset
// IsFirstCheckpoint should be true for the first checkpoint against a fresh shadow branch,
// including after a reset where the shadow branch is recreated from scratch.
isFirstCheckpointOfSession := state.CheckpointCount == 0

Copilot uses AI. Check for mistakes.
Entire-Checkpoint: 80e94f3d953a
// Build message - matches Claude Code format but with Gemini-specific instructions
var message string
suppressHint := "\n\nTo suppress this warning in future sessions, run:\n entire enable --disable-multisession-warning"
suppressHint := "\n\nTo disable this warning, remove enable_multisession_warning from .entire/settings.json"
Copy link

Choose a reason for hiding this comment

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

Gemini CLI handlers missing overlap detection hook calls

Medium Severity

The PR adds PromptHooks interface with OnPromptStart() and OnPromptEnd() methods for shadow branch overlap detection and integrates them into Claude Code handlers, but the Gemini CLI handlers were not updated. The handleGeminiBeforeAgent() function is missing the OnPromptStart() call after InitializeSession, and commitGeminiSession() is missing the OnPromptEnd() call after SaveChanges. This means the shadow branch overlap detection feature (which prevents "ghost files") will not work for Gemini CLI users.

Additional Locations (1)

Fix in Cursor Fix in Web

Entire-Checkpoint: b16a9b7bd167
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.

t.Fatalf("Failed to parse settings: %v", err)
}

settings["enable_multisession_warning"] = true
Copy link

Choose a reason for hiding this comment

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

Test sets option at wrong settings level

Medium Severity

The tests set settings["enable_multisession_warning"] = true at the root level of the settings JSON. However, IsMultiSessionWarningEnabled() in config.go looks for this option inside settings.StrategyOptions["enable_multisession_warning"]. This means these tests won't actually enable the multi-session warning feature and may pass or fail for incorrect reasons. The option needs to be placed in strategy_options like rewind_test.go does correctly.

Additional Locations (2)

Fix in Cursor Fix in Web

isFirstCheckpointOfSession := state.CheckpointCount == 0
// IsFirstCheckpoint should only be true for the VERY FIRST checkpoint of the session,
// not after a reset (which happens within an existing session)
isFirstCheckpointOfSession := state.CheckpointCount == 0 && !wasReset
Copy link

Choose a reason for hiding this comment

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

State not saved after shadow branch reset if checkpoint skipped

Medium Severity

When ShouldResetShadowBranch is true and the shadow branch is deleted (lines 63-86), the session state is reset in memory (CheckpointCount = 0, FilesTouched = nil, PromptAttributions = nil). However, if WriteTemporary returns Skipped: true due to deduplication, the function returns early at line 140 without calling saveSessionState. The reset state changes are lost, leaving the persisted state with stale values. On subsequent checkpoints, CheckpointCount will be incorrect and old PromptAttributions entries will persist.

Fix in Cursor Fix in Web

@gtrrz-victor
Copy link
Contributor

Alternative to this: #138

Copy link
Contributor

@khaong khaong left a comment

Choose a reason for hiding this comment

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

let's hold fire on this, @gtrrz-victor has got a session notification at initialise PR #138 and we added a mechanism for the user to reset #134

We have been discussing a potentially better way to handle multi-session state which tracks the changes the whole way through - let's discuss next week.

@khaong khaong marked this pull request as draft February 3, 2026 05:57
@Soph Soph closed this Feb 8, 2026
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.

3 participants