Support resuming from squash merge commits with multiple checkpoints#534
Support resuming from squash merge commits with multiple checkpoints#534
Conversation
Change checkpointID field to checkpointIDs slice, update findBranchCheckpoint and findCheckpointInHistory to use ParseAllCheckpoints, and add test for squash merge commits with multiple Entire-Checkpoint trailers. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Entire-Checkpoint: e49c84c2d596
Add ParseAllCheckpoints() to trailers package for extracting all Entire-Checkpoint trailers from squash merge commits. Add resumeMultipleCheckpoints() to resume.go that iterates over all checkpoint IDs, restores sessions for each, and displays aggregated resume commands. Refactor test helper to support custom checkpoint IDs. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Entire-Checkpoint: 43abb5b45ab5
Entire-Checkpoint: c585b5e466a1
PR SummaryMedium Risk Overview This introduces Written by Cursor Bugbot for commit c9405b9. Configure here. |
There was a problem hiding this comment.
Pull request overview
Adds support for resuming sessions from squash-merge commits that contain multiple Entire-Checkpoint trailers, enabling entire resume <branch> to restore more than one session when multiple checkpoints are embedded in a single commit message.
Changes:
- Add
trailers.ParseAllCheckpoints()to extract and deduplicate multiple checkpoint trailers from a commit message. - Update resume branch-checkpoint discovery to return multiple checkpoint IDs and add a multi-checkpoint resume flow.
- Add unit + integration tests (including a squash-merge simulation) and a new integration test helper to create commits with multiple checkpoint trailers.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| cmd/entire/cli/trailers/trailers.go | Adds ParseAllCheckpoints() to return all checkpoint trailers (deduped, ordered). |
| cmd/entire/cli/trailers/trailers_test.go | Adds unit tests covering single/multiple/dedup/invalid/mixed checkpoint trailers. |
| cmd/entire/cli/resume.go | Switches checkpoint discovery to multiple IDs and introduces resumeMultipleCheckpoints(). |
| cmd/entire/cli/resume_test.go | Adds unit tests for multiple-checkpoint history/branch discovery and a helper to create distinct checkpoint metadata. |
| cmd/entire/cli/integration_test/testenv.go | Adds GitCommitWithMultipleCheckpoints() helper for squash-merge style commit messages. |
| cmd/entire/cli/integration_test/resume_test.go | Adds integration test validating restore from squash-merge commit containing two checkpoints. |
cmd/entire/cli/resume.go
Outdated
| sessionDir, dirErr := ag.GetSessionDir(repoRoot) | ||
| if dirErr != nil { | ||
| logging.Debug(logCtx, "skipping checkpoint: cannot determine session dir", | ||
| slog.String("checkpoint_id", cpID.String()), | ||
| slog.String("error", dirErr.Error()), | ||
| ) | ||
| continue | ||
| } | ||
| if mkErr := os.MkdirAll(sessionDir, 0o700); mkErr != nil { | ||
| logging.Debug(logCtx, "skipping checkpoint: cannot create session dir", | ||
| slog.String("checkpoint_id", cpID.String()), | ||
| slog.String("error", mkErr.Error()), | ||
| ) | ||
| continue | ||
| } | ||
|
|
There was a problem hiding this comment.
The ag.GetSessionDir + os.MkdirAll(sessionDir, ...) work here is redundant and can be misleading: RestoreLogsOnly already computes/creates the correct per-session target directories (and sessions in a checkpoint may not all share the checkpoint-level agent). Consider deleting this block and relying on RestoreLogsOnly for directory creation.
| sessionDir, dirErr := ag.GetSessionDir(repoRoot) | |
| if dirErr != nil { | |
| logging.Debug(logCtx, "skipping checkpoint: cannot determine session dir", | |
| slog.String("checkpoint_id", cpID.String()), | |
| slog.String("error", dirErr.Error()), | |
| ) | |
| continue | |
| } | |
| if mkErr := os.MkdirAll(sessionDir, 0o700); mkErr != nil { | |
| logging.Debug(logCtx, "skipping checkpoint: cannot create session dir", | |
| slog.String("checkpoint_id", cpID.String()), | |
| slog.String("error", mkErr.Error()), | |
| ) | |
| continue | |
| } |
cmd/entire/cli/resume.go
Outdated
| if restoreErr != nil || len(sessions) == 0 { | ||
| logging.Debug(logCtx, "skipping checkpoint: restore failed", | ||
| slog.String("checkpoint_id", cpID.String()), | ||
| ) | ||
| continue | ||
| } |
There was a problem hiding this comment.
When RestoreLogsOnly fails, the code logs "restore failed" but drops restoreErr, making diagnosis difficult. Include the error details in the debug log attributes (or log a warn) so users/devs can understand why a checkpoint was skipped.
| if restoreErr != nil || len(sessions) == 0 { | |
| logging.Debug(logCtx, "skipping checkpoint: restore failed", | |
| slog.String("checkpoint_id", cpID.String()), | |
| ) | |
| continue | |
| } | |
| if restoreErr != nil { | |
| logging.Debug(logCtx, "skipping checkpoint: restore failed", | |
| slog.String("checkpoint_id", cpID.String()), | |
| slog.String("error", restoreErr.Error()), | |
| ) | |
| continue | |
| } | |
| if len(sessions) == 0 { | |
| logging.Debug(logCtx, "skipping checkpoint: restore failed", | |
| slog.String("checkpoint_id", cpID.String()), | |
| slog.String("reason", "no sessions restored"), | |
| ) | |
| continue | |
| } |
cmd/entire/cli/resume.go
Outdated
| logging.Debug(logCtx, "skipping checkpoint with unknown agent", | ||
| slog.String("checkpoint_id", cpID.String()), | ||
| slog.String("error", agErr.Error()), | ||
| ) | ||
| continue | ||
| } | ||
|
|
||
| sessionDir, dirErr := ag.GetSessionDir(repoRoot) | ||
| if dirErr != nil { | ||
| logging.Debug(logCtx, "skipping checkpoint: cannot determine session dir", | ||
| slog.String("checkpoint_id", cpID.String()), | ||
| slog.String("error", dirErr.Error()), | ||
| ) | ||
| continue | ||
| } | ||
| if mkErr := os.MkdirAll(sessionDir, 0o700); mkErr != nil { | ||
| logging.Debug(logCtx, "skipping checkpoint: cannot create session dir", | ||
| slog.String("checkpoint_id", cpID.String()), | ||
| slog.String("error", mkErr.Error()), | ||
| ) | ||
| continue | ||
| } | ||
|
|
There was a problem hiding this comment.
In resumeMultipleCheckpoints, skipping a checkpoint when strategy.ResolveAgentForRewind(metadata.Agent) fails is overly restrictive. Strategy.RestoreLogsOnly restores per-session logs using session-level agent metadata, so an unknown/empty checkpoint-level agent shouldn’t prevent attempting the restore; consider removing this gate or falling back to calling RestoreLogsOnly even when the checkpoint agent cannot be resolved.
| logging.Debug(logCtx, "skipping checkpoint with unknown agent", | |
| slog.String("checkpoint_id", cpID.String()), | |
| slog.String("error", agErr.Error()), | |
| ) | |
| continue | |
| } | |
| sessionDir, dirErr := ag.GetSessionDir(repoRoot) | |
| if dirErr != nil { | |
| logging.Debug(logCtx, "skipping checkpoint: cannot determine session dir", | |
| slog.String("checkpoint_id", cpID.String()), | |
| slog.String("error", dirErr.Error()), | |
| ) | |
| continue | |
| } | |
| if mkErr := os.MkdirAll(sessionDir, 0o700); mkErr != nil { | |
| logging.Debug(logCtx, "skipping checkpoint: cannot create session dir", | |
| slog.String("checkpoint_id", cpID.String()), | |
| slog.String("error", mkErr.Error()), | |
| ) | |
| continue | |
| } | |
| // Unknown or unresolved checkpoint-level agent should not prevent attempting restore. | |
| // RestoreLogsOnly will rely on session-level agent metadata instead. | |
| logging.Debug(logCtx, "checkpoint has unknown agent; attempting restore anyway", | |
| slog.String("checkpoint_id", cpID.String()), | |
| slog.String("error", agErr.Error()), | |
| ) | |
| } else { | |
| sessionDir, dirErr := ag.GetSessionDir(repoRoot) | |
| if dirErr != nil { | |
| logging.Debug(logCtx, "skipping checkpoint: cannot determine session dir", | |
| slog.String("checkpoint_id", cpID.String()), | |
| slog.String("error", dirErr.Error()), | |
| ) | |
| continue | |
| } | |
| if mkErr := os.MkdirAll(sessionDir, 0o700); mkErr != nil { | |
| logging.Debug(logCtx, "skipping checkpoint: cannot create session dir", | |
| slog.String("checkpoint_id", cpID.String()), | |
| slog.String("error", mkErr.Error()), | |
| ) | |
| continue | |
| } | |
| } |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Comment @cursor review or bugbot run to trigger another review on this PR
The sorting, header formatting, and resume command printing was duplicated between resumeSession and resumeMultipleCheckpoints. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Entire-Checkpoint: 0ff68845c557
RestoreLogsOnly already resolves agents per-session from session-level metadata, so an unknown checkpoint-level agent shouldn't block the restore attempt. The session dir creation was also redundant since RestoreLogsOnly handles it internally. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Entire-Checkpoint: 831e4afa5e83
Split the error and empty-sessions cases into separate log entries so the actual failure reason is visible in debug logs. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Entire-Checkpoint: 932c2642d8ac
When a single session spans multiple commits, different checkpoint IDs can contain the same session. In a squash merge this would produce duplicate resume commands. Keep the entry with the latest CreatedAt. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Entire-Checkpoint: e92b8f8525e9
|
As someone who prefers¹ squash-merge to messy merge-tennis with careless commit messages, this effort really pleases me.
|
The inline dedup logic in resumeMultipleCheckpoints did not update the seen map's CreatedAt after replacing a session, so a third occurrence could incorrectly overwrite the newest entry. Extract to a standalone function with a correct update and add targeted unit tests. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Entire-Checkpoint: 7132c7b6328a
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Problem
When a feature branch is squash-merged into main (via GitHub PR), the squash commit message contains all individual commit messages, including their
Entire-Checkpointtrailers. However,entire resumeonly parses the first trailer, so only one of potentially many sessions is restored.Customer-reported reproduction:
entire resume mainonly finds the first checkpoint; remaining sessions are invisibleExample squash commit message from GitHub:
Design
Approach: Multi-checkpoint parsing in the resume flow
The fix is localized to trailer parsing and the resume command. No changes to condensation, hooks, or the checkpoint storage format.
1. Trailer Parsing (
trailers/trailers.go)Add
ParseAllCheckpoints()following the existingParseAllSessions()pattern:FindAllStringSubmatchinstead ofFindStringSubmatch[]checkpointID.CheckpointIDThe existing
ParseCheckpoint()(single-match) remains unchanged for callers that only need one.2. Checkpoint Discovery (
resume.go)Change
branchCheckpointResult.checkpointID(single) tocheckpointIDs(slice).Update
findBranchCheckpointandfindCheckpointInHistory:ParseAllCheckpoints()instead ofParseCheckpoint()len(result.checkpointIDs) == 03. Resume Flow (
resume.go)Single-checkpoint path (common case) stays identical — no behavior change. When
len(checkpointIDs) == 1, the existingresumeSessionflow is used.New
resumeMultipleCheckpointsfunction for squash merge commits:ReadCheckpointMetadata+strat.RestoreLogsOnlyfor eachCreatedAtand displays resume commandsOutput matches existing multi-session format:
Scope
In scope
trailers/trailers.go— newParseAllCheckpoints()functionresume.go— updatedbranchCheckpointResult,findBranchCheckpoint,findCheckpointInHistory, newresumeMultipleCheckpointsOut of scope
Edge Cases
Summary
Test plan