Skip to content

Support resuming from squash merge commits with multiple checkpoints#534

Draft
pfleidi wants to merge 14 commits intomainfrom
squash-merge-resume
Draft

Support resuming from squash merge commits with multiple checkpoints#534
pfleidi wants to merge 14 commits intomainfrom
squash-merge-resume

Conversation

@pfleidi
Copy link
Contributor

@pfleidi pfleidi commented Feb 26, 2026

Summary

  • Support entire resume on squash merge commits that contain multiple Entire-Checkpoint trailers
  • Fix checkpoint ordering bug where git CLI squash merges list trailers newest-first, causing the oldest transcript to overwrite the newest on disk
  • Deduplicate sessions by SessionID across checkpoints, keeping the most recent transcript

Problem

When a feature branch with multiple commits (each with its own Entire-Checkpoint trailer) is squash-merged, all trailers end up in a single commit message. entire resume previously only parsed the first trailer, losing all other sessions.

Additionally, GitHub squash merges list trailers chronologically (oldest first), but git CLI squash merges list them in reverse order (newest first). Since RestoreLogsOnly writes session files eagerly, the last checkpoint processed wins on disk — meaning reverse-ordered trailers caused the oldest transcript to overwrite the newest.

Changes

cmd/entire/cli/trailers/trailers.go — Add ParseAllCheckpoints() to extract all Entire-Checkpoint trailers from a commit message (not just the first).

cmd/entire/cli/resume.go

  • Switch from ParseCheckpoint (single) to ParseAllCheckpoints (multi) throughout findBranchCheckpoint and findCheckpointInHistory
  • Change branchCheckpointResult.checkpointIDcheckpointIDs []CheckpointID
  • Add resumeMultipleCheckpoints: reads metadata for all checkpoint IDs, sorts by CreatedAt ascending, then restores in order so the newest checkpoint always writes last
  • Add deduplicateSessions: merges sessions across checkpoints, keeping the one with the latest CreatedAt when a SessionID appears in multiple checkpoints
  • Extract displayRestoredSessions to share session display logic between single and multi-checkpoint paths

cmd/entire/cli/resume_test.go — Add unit tests for deduplicateSessions (including three-occurrence staleness edge case), findCheckpointInHistory with multiple trailers, findBranchCheckpoint with squash merge, and checkpoint timestamp sorting.

cmd/entire/cli/integration_test/resume_test.go — Add integration test simulating a full squash merge workflow: two sessions on a feature branch, squash merge to main, then entire resume restoring both sessions.

cmd/entire/cli/trailers/trailers_test.go — Add tests for ParseAllCheckpoints.

Test plan

  • mise run test:ci passes (unit + integration)
  • Integration test TestResume_SquashMergeMultipleCheckpoints covers the end-to-end squash merge flow
  • Unit test TestResumeMultipleCheckpoints_SortsByCreatedAt verifies reverse-ordered checkpoints are sorted correctly
  • Unit tests for deduplicateSessions cover no-duplicates, newer-wins, older-loses, three-occurrence, and mixed scenarios

pfleidi and others added 4 commits February 26, 2026 13:57
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
@pfleidi pfleidi requested a review from a team as a code owner February 26, 2026 23:11
Copilot AI review requested due to automatic review settings February 26, 2026 23:11
@cursor
Copy link

cursor bot commented Feb 26, 2026

PR Summary

Medium Risk
Changes resume’s checkpoint discovery/restoration flow to handle multiple Entire-Checkpoint trailers, adding new best-effort iteration and remote-metadata fallback behavior that could affect which sessions are restored and what output users see.

Overview
resume now supports GitHub-style squash merge commits that contain multiple Entire-Checkpoint trailers by parsing all checkpoint IDs and restoring sessions from each checkpoint (best-effort), printing resume commands for every restored session.

This introduces trailers.ParseAllCheckpoints(), updates branch checkpoint search results to return []CheckpointID, and adds integration/unit coverage plus a new test helper to create commits with multiple checkpoint trailers.

Written by Cursor Bugbot for commit c9405b9. Configure here.

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

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.

Comment on lines +237 to +252
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
}

Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

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

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.

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

Copilot uses AI. Check for mistakes.
Comment on lines +260 to +265
if restoreErr != nil || len(sessions) == 0 {
logging.Debug(logCtx, "skipping checkpoint: restore failed",
slog.String("checkpoint_id", cpID.String()),
)
continue
}
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

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

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.

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

Copilot uses AI. Check for mistakes.
Comment on lines +230 to +252
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
}

Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

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

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.

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

Copilot uses AI. Check for mistakes.
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 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

pfleidi and others added 2 commits February 26, 2026 15:22
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
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

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

pfleidi and others added 2 commits February 26, 2026 15:26
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
@toothbrush
Copy link
Contributor

toothbrush commented Feb 26, 2026

As someone who prefers¹ squash-merge to messy merge-tennis with careless commit messages, this effort really pleases me.

  1. Out of an abundance of caution i want to add that doesn't mean it's my favouritest merge.

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

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@pfleidi pfleidi marked this pull request as draft February 27, 2026 02:16
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

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Comment on lines +550 to +556
// TestResumeMultipleCheckpoints_SortsByCreatedAt verifies that resumeMultipleCheckpoints
// sorts checkpoints by CreatedAt ascending before restoring, so that the newest checkpoint
// writes last and wins on disk. This fixes the git CLI squash merge bug where trailers
// appear in reverse chronological order (newest first).
func TestResumeMultipleCheckpoints_SortsByCreatedAt(t *testing.T) {
tmpDir := t.TempDir()
t.Chdir(tmpDir)
Copy link

Copilot AI Mar 3, 2026

Choose a reason for hiding this comment

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

TestResumeMultipleCheckpoints_SortsByCreatedAt doesn’t exercise resumeMultipleCheckpoints (it re-implements the sort inline), so it will keep passing even if the production code stops sorting before restoring. To make this test meaningful, either (a) extract the “sort checkpoints by CreatedAt” logic into a helper used by resumeMultipleCheckpoints and unit-test that helper, or (b) refactor resumeMultipleCheckpoints to accept an injected restorer so the test can assert the restore call order end-to-end.

Copilot uses AI. Check for mistakes.
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