Skip to content

Support resuming from squash merge commits with multiple checkpoints#534

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

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

Conversation

@pfleidi
Copy link
Contributor

@pfleidi pfleidi commented Feb 26, 2026

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-Checkpoint trailers. However, entire resume only parses the first trailer, so only one of potentially many sessions is restored.

Customer-reported reproduction:

  1. Work on feature branch with Entire sessions, creating multiple commits with checkpoints
  2. Squash-merge PR to main
  3. entire resume main only finds the first checkpoint; remaining sessions are invisible
  4. Deleting the feature branch makes the original per-commit checkpoints unreachable

Example squash commit message from GitHub:

Soph/test branch (#2)
* random_letter script

Entire-Checkpoint: 0aa0814d9839

* random color

Entire-Checkpoint: 33fb587b6fbb

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 existing ParseAllSessions() pattern:

  • Uses FindAllStringSubmatch instead of FindStringSubmatch
  • Deduplicates checkpoint IDs while preserving order
  • Returns []checkpointID.CheckpointID

The existing ParseCheckpoint() (single-match) remains unchanged for callers that only need one.

2. Checkpoint Discovery (resume.go)

Change branchCheckpointResult.checkpointID (single) to checkpointIDs (slice).

Update findBranchCheckpoint and findCheckpointInHistory:

  • Use ParseAllCheckpoints() instead of ParseCheckpoint()
  • The HEAD fast-path and history walk both collect all IDs from the first matching commit
  • Empty-check becomes len(result.checkpointIDs) == 0

3. Resume Flow (resume.go)

Single-checkpoint path (common case) stays identical — no behavior change. When len(checkpointIDs) == 1, the existing resumeSession flow is used.

New resumeMultipleCheckpoints function for squash merge commits:

  1. Gets metadata branch tree (with remote fallback)
  2. Loops over checkpoint IDs, calling ReadCheckpointMetadata + strat.RestoreLogsOnly for each
  3. Skips checkpoints whose metadata isn't available (best-effort)
  4. Aggregates all restored sessions into a single slice
  5. Sorts by CreatedAt and displays resume commands

Output matches existing multi-session format:

Restored 2 sessions. To continue, run:
  claude --resume abc123  # implement random_letter script
  claude --resume def456  # add random color (most recent)

Scope

In scope

  • trailers/trailers.go — new ParseAllCheckpoints() function
  • resume.go — updated branchCheckpointResult, findBranchCheckpoint, findCheckpointInHistory, new resumeMultipleCheckpoints
  • Tests for all changed code

Out of scope

  • Dashboard visibility (separate concern)
  • Handling stripped/customized squash commit messages (best-effort: we work with available trailers)
  • Branch-not-found error messaging (keep existing behavior)
  • Changes to condensation, hooks, or checkpoint storage format

Edge Cases

  • User customizes squash message and removes trailers: Best effort — we can only find what's there. Same behavior as today (no checkpoint found).
  • Some checkpoint metadata missing: Skip those checkpoints, restore what we can.
  • Single checkpoint in squash commit: Falls through to existing single-checkpoint path, no behavior change.
  • Non-squash commits: Always have 0 or 1 trailer, existing behavior preserved.

Summary

  • Adds ParseAllCheckpoints() to extract all Entire-Checkpoint trailers from a commit message (squash merge commits contain multiple)
  • Updates branchCheckpointResult and findBranchCheckpoint to return multiple checkpoint IDs
  • Adds resumeMultipleCheckpoints() which iterates over all checkpoint IDs, restores sessions for each (best-effort), and displays resume commands
  • Extracts displayRestoredSessions() helper to deduplicate session display logic between resumeSession and resumeMultipleCheckpoints
  • Adds GitCommitWithMultipleCheckpoints test helper and integration test TestResume_SquashMergeMultipleCheckpoints

Test plan

  • Unit tests for ParseAllCheckpoints (single, multiple, dedup, invalid, mixed trailers)
  • Unit tests for findBranchCheckpoint and findCheckpointInHistory with multiple trailers
  • Unit test for resumeMultipleCheckpoints flow
  • Integration test simulating full squash merge resume workflow (two sessions → two commits → squash merge → resume restores both)
  • mise run fmt && mise run lint && mise run test:ci all pass

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