Interactive Resume Picker for Sessions · Entire

Home

Log in

I have the following use case:

I start a claude session with a new branch to experiment. some commits are added but it's not done yet. Now I switch to something else and usually I have a few worktrees in parallel. But sometimes I'm not going to pick up what I left for a while.

Now "entire resume" easily let's me pickup a branch again but it means I need to remember which branch it was and what I worked on. I wonder if we could have some automated use case where I can list stoped sessions with entire resume and more easily switch to them (checking out the branch, giving me the right resume command)

1mo ago·40.2s·3 calls

The exploration agent is mapping the current session list/resume/state model. I'll design the feature once it reports back.

a32cbbe688c54e813 toolu\_01H5FxJxHC1TFL526MAHzrqY /private/tmp/claude-501/-Users-soph-Work-entire-devenv-cli/d7eb2c7b-65ce-4dbe-b20a-285326019897/tasks/a32cbbe688c54e813.output completed Agent "Map session list and resume commands" came to rest A task-notification fires each time this agent comes to rest with no live background children of its own. The user can send it another message and resume it, so the same task-id may notify more than once. Perfect! Now I have enough information. Let me create a comprehensive report with all the findings:

Session Management Investigation Report

Based on my thorough investigation of the Entire CLI codebase, here are the detailed findings:


1. entire session resume Command

File:/Users/soph/Work/entire/devenv/cli/cmd/entire/cli/resume.go (lines 30-66, 68-142)

Arguments: Takes a single required argument: <branch> name

Behavior:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

// Lines 36-49 from resume.go:
Long: `Switch to a local branch and resume the agent session from its last commit.

This command:
1. Checks out the specified branch
2. Finds the session ID from commits unique to this branch (not on main)
3. Restores the session log if it doesn't exist locally
4. Shows the command to resume the session

If the branch doesn't exist locally but exists on origin, you'll be prompted
to fetch it.

If newer commits without checkpoints exist on the branch (e.g., after merging main
or cherry-picking from elsewhere), this operation will reset your Git status to the
most recent commit with a checkpoint.  You'll be prompted to confirm resuming in this case.`,

What it does:

  • Checks out the branch (with optional fetch from origin if needed, or skips with --force)
  • Finds the most recent checkpoint on branch-only commits (not inherited from main)
  • Restores the session log from checkpoint metadata (via resumeSession())
  • Displays the command to continue the session with the agent
  • Distinguishes between "newer commits without checkpoints" (counted as branch work) and merge commits (not counted)

Key quote on session restoration (lines 740-797):

1
2
3
4
5
6
7
8
9
10

func resumeSession(ctx context.Context, w, errW io.Writer, metadata *strategy.CheckpointInfo, force bool) error {
    // ...
    // Use RestoreLogsOnly via LogsOnlyRestorer interface for multi-session support
    // Create a logs-only rewind point with Agent populated (same as rewind)
    point := strategy.RewindPoint{
        IsLogsOnly:   true,
        CheckpointID: checkpointID,
        Agent:        metadata.Agent,
    }
    sessions, restoreErr := strat.RestoreLogsOnly(ctx, w, errW, point, force)

2. entire session list Command

File:/Users/soph/Work/entire/devenv/cli/cmd/entire/cli/sessions.go (lines 318-388)

What it lists:

  • All sessions (both active and ended) across all worktrees
  • Sorted by StartedAt descending (newest first)
  • Returns [] empty array for no sessions

Output fields (displayed in human-readable card format, lines 409-447):

  • Line 1: Agent name, model name (if set), worktree identifier, session ID, last checkpoint ID
  • Line 2: First prompt (truncated to 60 runes with "...")
  • Line 3: Status (phase label), started time, last activity time (if > 1 minute), token count

JSON mode (--json flag, lines 360-406):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

type sessionInfoJSON struct {
    SessionID      string         `json:"session_id"`
    Agent          string         `json:"agent"`
    Model          string         `json:"model,omitempty"`
    Status         string         `json:"status"`
    WorktreeID     string         `json:"worktree_id,omitempty"`
    WorktreePath   string         `json:"worktree_path,omitempty"`
    StartedAt      time.Time      `json:"started_at"`
    EndedAt        *time.Time     `json:"ended_at,omitempty"`
    LastActive     *time.Time     `json:"last_active,omitempty"`
    Turns          int            `json:"turns"`
    Checkpoints    int            `json:"checkpoints"`
    LastCheckpoint string         `json:"last_checkpoint_id,omitempty"`
    Tokens         *tokenInfoJSON `json:"tokens,omitempty"`
    LastPrompt     string         `json:"last_prompt,omitempty"`
    FilesTouched   []string       `json:"files_touched,omitempty"`
}

3. entire session info and entire session current

File:/Users/soph/Work/entire/devenv/cli/cmd/entire/cli/sessions.go (lines 461-533) and session_current.go (lines 12-56)

session info <session-id>:

  • Shows detailed state for a specific session (active or ended)
  • Supports three output modes:
    • Default: human-readable summary with all details (agent, model, status, worktree, timing, token usage, checkpoints, files touched)
    • --json: metadata-only JSON envelope (same as buildSessionInfoJSON)
    • --transcript: streams raw agent transcript bytes to stdout in agent's native format (JSONL for Claude/Cursor, JSON for Gemini)

session current:

  • Shows the most recently active session for the current worktree

  • Uses strategy.FindMostRecentSession(ctx) which:

  • Filters sessions to current worktree first

    • Falls back to most recently started session if none match worktree
    • Prefers sessions by LastInteractionTime over StartedAt
  • Supports same three output modes as info


4. Session State Model

Location:/Users/soph/Work/entire/devenv/cli/cmd/entire/cli/session/state.go (lines 82-286)

Session state is the struct State (aliased as strategy.SessionState in manual_commit_types.go line 29):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

// Lines 84-286 from session/state.go - key fields only:
type State struct {
    SessionID              string                    // Unique session identifier
    BaseCommit             string                    // Current shadow branch base commit
    AttributionBaseCommit  string                    // Reference point for attribution calculations
    WorktreePath           string                    // Absolute path to worktree root
    WorktreeID             string                    // Internal git worktree identifier (empty for main)
    StartedAt              time.Time                 // When session started
    EndedAt                *time.Time                // When session was closed (nil = still active/not cleanly closed)
    Phase                  Phase                     // Lifecycle stage: "idle", "active", or "ended"
    LastInteractionTime    *time.Time                // Last agent-interaction event timestamp
    StepCount              int                       // Number of checkpoints created
    AgentType              types.AgentType           // Agent identifier (e.g., "Claude Code", "Gemini CLI")
    ModelName              string                    // LLM model name
    LastPrompt             string                    // Most recent user prompt (truncated, ~100 runes)
    LastCheckpointID       id.CheckpointID           // Most recent checkpoint ID
    FilesTouched           []string                  // Files modified/created/deleted
    TokenUsage             *agent.TokenUsage         // Input/output/cache token counts
    SessionTurnCount       int                       // Number of prompts/turns in session
    TranscriptPath         string                    // Path to live transcript file
    // ... plus many more fields for state machine, attribution, compaction tracking
}

Key fields for your feature design:

  • ✓ Stores git branch via periodic shadow branch naming (in BaseCommit, not directly)
  • ✓ Stores worktree path and worktree ID (lines 102-107)
  • ✓ Stores last activity timestamp (LastInteractionTime, line 167)
  • ✓ Stores status/phase (idle/active/ended) (line 118)
  • ✓ Stores first/last prompt (line 277, limited to ~100 runes)
  • ✓ Stores agent type (line 229)

5. Stopped vs Active Sessions

File:/Users/soph/Work/entire/devenv/cli/cmd/entire/cli/session/phase.go (lines 15-22)

Phase values:

1
2
3
4
5

const (
    PhaseActive Phase = "active"  // Agent is working on a turn
    PhaseIdle   Phase = "idle"    // Session exists but no turn is active
    PhaseEnded  Phase = "ended"   // Session has been explicitly closed
)

How system knows a session is "stopped":

  1. session stop command marks session as ended by:
  • Setting Phase = "ended" via state machine transition EventSessionStop
    • Setting EndedAt = time.Now() (lines 1106 in lifecycle.go)
    • Quote from markSessionEnded() (lifecycle.go lines 1096-1116):
1
2
3
4
5
6
7
8
9

func markSessionEnded(ctx context.Context, event *agent.Event, sessionID string) error {
    mutErr := strategy.MutateSessionState(ctx, sessionID, func(state *strategy.SessionState) error {
        if transErr := strategy.TransitionAndLog(ctx, state, session.EventSessionStop, ...); transErr != nil {
            // ...
        }
        now := time.Now()
        state.EndedAt = &now
        return nil
    })
  1. Active detection (sessions.go lines 279-290):
1
2
3
4
5
6
7
8
9
10

func filterActiveSessions(states []*strategy.SessionState) []*strategy.SessionState {
       var active []*strategy.SessionState
       for _, s := range states {
           if s == nil { continue }
           if s.Phase != session.PhaseEnded && s.EndedAt == nil {
               active = append(active, s)
           }
       }
       return active
}

A session is active if: Phase != "ended" AND EndedAt == nil


6. Worktree Awareness

Storage: Session state is shared across all worktrees (line 30 in session_state.go):

1
2
3
4
5
6
7
8

// This is stored in the git common dir so it's shared across all worktrees.
func getSessionStateDir(ctx context.Context) (string, error) {
    commonDir, err := GetGitCommonDir(ctx)  // .git/ (not .git/worktrees/<name>/)
    if err != nil {
        return "", err
    }
    return filepath.Join(commonDir, session.SessionStateDirName), nil
}

Location: .git/entire-sessions/{session-id}.json

Per-session worktree tracking: Each session record stores both:

  • WorktreePath (absolute path to worktree root)
  • WorktreeID (internal git worktree identifier, empty for main worktree)

Resume handling across worktrees (session_state.go lines 134-178):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

func FindMostRecentSession(ctx context.Context) string {
    states, err := ListSessionStates(ctx)

// Scope to current worktree to prevent cross-worktree pollution.
    worktreePath, wpErr := paths.WorktreeRoot(ctx)
    if wpErr == nil && worktreePath != "" {
        var filtered []*SessionState
        for _, s := range states {
            if s.WorktreePath == worktreePath {  // Filter by current worktree
                filtered = append(filtered, s)
            }
        }
        if len(filtered) > 0 {
            states = filtered  // Use filtered list if matches exist
        }
        // If no sessions match the worktree, fall back to all sessions
    }

// Sort by LastInteractionTime, fall back to StartedAt
    var best *SessionState
    for _, s := range states {
        if s.LastInteractionTime == nil { continue }
        if best == nil || s.LastInteractionTime.After(*best.LastInteractionTime) {
            best = s
        }
    }
    if best != nil { return best.SessionID }

// Fallback: return most recently started session
    for _, s := range states {
        if best == nil || s.StartedAt.After(best.StartedAt) {
            best = s
        }
    }
    if best != nil { return best.SessionID }
    return ""
}

Resume logic (resume.go lines 144-242):

  • When resuming from a branch, the user explicitly checks out that branch first
  • The session's WorktreePath is preserved in state (set during session init, line 321 in manual_commit_session.go)
  • Session logs are restored to the agent's canonical session directory in the current worktree (lines 764-772):
1
2
3

repoRoot, err := paths.WorktreeRoot(ctx)  // Current worktree root
sessionDir, err := ag.GetSessionDir(repoRoot)  // Agent's session dir in current worktree
if err := os.MkdirAll(sessionDir, 0o700); err != nil { ... }

This ensures resuming a session restores logs to the correct worktree's agent session directory, regardless of where the session was originally created.


Summary: Session state is globally shared (stored in .git/), but each session record stores its originating worktree path. The resume flow handles cross-worktree sessions by filtering to the current worktree first, falling back to most recent if no match, and restoring session logs to the current worktree's agent directory. <subagent_tokens>68798</subagent_tokens><tool_uses>35</tool_uses><duration_ms>105653</duration_ms>

1mo ago·1h 6m·106 calls

Done. Here's what I built for your "pick up where I left off" use case.

What entire resume does now

Bare entire resume (no argument) opens an interactive picker of your stopped sessions across all worktrees — so you don't have to remember the branch. Each row shows branch · "last prompt" · agent · stopped X ago. Pick one and it checks out the branch and prints the command to continue the agent. entire resume <branch> is unchanged.

Per your decisions:

  • Stopped sessions only, all worktrees, newest-first.
  • Worktree clash: if the branch is already checked out in another worktree, it doesn't try to check out — it prints that worktree's path and the cd … && entire resume <branch> to run there.
  • Unresolvable sessions (stopped with no committed checkpoint → no branch to resolve) are listed but annotated (no branch) … — can't resume rather than hidden.
  • Never overwrites an existing local log: if a session log is already on disk, resume keeps it and just prints the command (--force overwrites from the checkpoint). I made this driven by file existence, not timestamps, so even a log without a parseable timestamp is preserved — matching "if there is a session log, keep it."

How it maps a session → branch

Session state didn't store the branch, so I added a Branch field captured each turn (captureSessionBranch in the hooks), surfaced in session list --json. For sessions recorded before the field existed, it derives the branch by matching the session's last checkpoint ID against branch-only commit trailers.

Verification

  • New unit tests (picker filtering/sorting, labels, branch index/derivation, keep-existing-log) + updated the timestamp resume integration tests to the new keep-by-default semantics; deleted the now-obsolete interactive overwrite-prompt test file.
  • Full suite green: 6338 unit, 368 integration, 4 e2e canary; lint clean; docs updated (CLAUDE.md, sessions-and-checkpoints.md).

One incidental thing: mise run fmt had stripped a //nolint directive from explain.go (unrelated to this work) and broke lint — I reverted that file so it's untouched.

Your entire pause idea

I noted it as a follow-up but didn't build it: a command that creates a commit (even empty) to capture current state so a session is resumable later even when nothing was committed. That would also make such sessions show up as selectable (rather than "no branch") in this picker. Want me to spec/build that next?

Nothing is committed — all changes are in the working tree for you to review.

"go run cmd/entire/main.go resume" is not showing anything for over a minute wait time now, still running

1mo ago·4m·5 calls

Fixed. The hang was go-git's MergeBase, which I was calling once per local branch — and this repo has 460 branches, each merge-base walking full history. That's the O(branches × history) blowup.

Two changes:

  • Dropped MergeBase entirely. Instead of scoping to branch-only commits via merge-base, each branch is walked from its tip back a bounded 50 commits collecting checkpoint trailers. A session's last checkpoint sits near its branch tip, so a shallow walk finds it; the documented trade-off is that a checkpoint shared with the base branch might be attributed to a feature branch, which is harmless for lookups keyed on a specific session's checkpoint ID.
  • Made the index lazy. It's only built for the derivation fallback — sessions that carry the stored Branch field (the normal case going forward) skip the branch scan completely. So once your sessions have the field, there's no scan at all.

Result in this repo: the full command (agent discovery + indexing all 460 branches + reaching the picker) runs in ~1.9s, down from never-finishing. Unit tests pass, lint clean, explain.go still untouched.

Retry with go run cmd/entire/main.go resume (it'll recompile once, then be fast) — in a real terminal it'll now render the picker of your stopped sessions.

is it now worktree aware?

1mo ago·25m·13 calls

git diff on explain.go is empty — my echo logic was just backwards; the file is untouched. Lint is clean.

Yes — it's worktree-aware, in three ways

  1. Lists across all worktrees. The picker pulls stopped sessions from the shared session store with no per-worktree filter, so a session you left in worktree A shows up when you run entire resume from anywhere — which is the whole point of your use case.

  2. Routes you to the owning worktree on a clash. Branches can only be checked out in one worktree at a time. When you pick a session whose branch is currently checked out in another worktree, it doesn't attempt (and fail) a checkout — it detects the clash via git worktree list and prints that worktree's path plus the command to run there:

Branch 'exp' is already checked out in another worktree:
     /path/to/other/worktree

Resume it there with:
     cd "/path/to/other/worktree" && entire resume exp

I just added TestBranchCheckedOutElsewhere (with real git worktree add) confirming it detects the other worktree's path, and correctly does not flag the current worktree's own branch or unknown branches.

  1. Branch resolution is worktree-correct. Branch refs are global to the repo (shared across worktrees), and the Branch field is captured per-session, so resolving each session to its branch works regardless of which worktree you invoke from.

One thing it does not currently do: the picker rows don't display which worktree each session belongs to (they show branch · prompt · agent · stopped X ago). And if a session's branch is no longer checked out anywhere, picking it checks the branch out in your current worktree rather than steering you back to the original worktree dir. Both are easy additions if you want them — want me to surface the worktree in the picker label?

there is this session I had before 4f5c58db-00cd-4a97-a1f6-80809fcfb00a it's not showing up, can you check?

1mo ago·41m·23 calls

All green. Here's the rundown.

Your session was never lost — and the branch-arg form always worked. That's the "used to work" you remember: entire resume soph/trail-agent-guide resolves straight from the branch's checkpoint trailer regardless of session state. Still works today.

Why the picker hid it: exiting Claude Code does not mark a session ended — it leaves it idle. Only an explicit entire session stop (or a clean shutdown that fires the stop hook) sets ended. My picker filtered to ended-only, so your idle session (and the 2 others like it) never appeared. That was my bug — the wrong mental model of "stopped."

Fixes:

  1. Picker now lists all resumable sessions = anything not currently mid-turn (idle + ended), not just explicitly-stopped ones. Only a session with a turn actively running is excluded. Your 4f5c58db… now shows up.
  2. Excluded Entire's internal refs from branch resolution. While checking, I found the checkpoint also matched entire/checkpoints/v1 (the metadata branch), and my index was scanning all refs — including the hundreds of entire/<hash> shadow branches. It could have mis-resolved a session to a non-resumable internal ref (and was wasted work). Now entire/-prefixed refs are skipped, so 4f5c58db… correctly resolves to soph/trail-agent-guide. Labels also now say "last active X ago" instead of "stopped."

Tests (unit + resume/session integration) and lint are green; explain.go untouched.

So: rebuild and entire resume will list it now — or just run entire resume soph/trail-agent-guide to pick it up immediately.

One thing I deliberately did not change: a genuinely-active session (a live, running agent) is still excluded from the picker. If you'd also want stale/crashed "active" sessions to show (4 of yours are marked active and some may be dead), that's a separate staleness call — say the word and I'll add a "last active > N hours ⇒ treat as resumable" rule.

or a clean shutdown that fires the stop hook

if I type "exit" in claude, isn't that a clean shutdown?

1mo ago·15m·5 calls

Good instinct to push on this — and the answer is the opposite of what you'd expect: typing the word exit does not quit Claude Code.

Per the official Claude Code docs, you exit an interactive session only via:

  • Ctrl-C (twice), or
  • Ctrl-D, or
  • a slash command (/clear, etc.)

A bare exit/quit typed at the prompt is just sent to the model as a normal message — it doesn't terminate anything. And your session's last prompt was "ok, I feel we should just add a local skill…", a normal prompt, after which it went idle. So no exit ever happened from Entire's point of view.

Here's the full chain, verified against this repo:

  • The wiring is real and was in place. Entire has handled Claude Code's SessionEnd → mark session ended since Feb 16 (well before your Jun 15 session), and the SessionEnd hook is installed in this project's .claude/settings.json. So that part works.
  • SessionEnd only fires on a genuine clean exit — reasons clear, logout, prompt_input_exit, resume, etc. It does not fire when you close the terminal, Ctrl-C-kill, drop SSH, or — crucially — when you just type "exit" and walk away.
  • So your session never received a SessionEnd, stayed idle, and my "stopped/ended-only" picker filter hid it.

Two corrections to what I told you earlier:

  1. I said "stop hook" — that was sloppy. The per-turn Stop hook fires after every turn and leaves the session idle; it's the separate SessionEnd hook that marks it ended.
  2. "Exiting" the way you describe (typing exit) was never a clean shutdown, so it would never have ended the session — which is exactly why the picker needed to include idle sessions, not just ended ones. That fix (already in place) is the right call regardless.

Net: the fix stands, and your 4f5c58db… session now shows in the picker (resolving to soph/trail-agent-guide).

One adjacent thing I noticed while tracing this: Entire marks a session ended on anySessionEnd, including reason clear and resume — so /clear-ing context would flag the session ended too. That's arguably wrong but harmless now that the picker shows idle+ended anyway. Want me to make the SessionEnd handler ignore clear/resume reasons so the ended state stays meaningful?

prompt_input_exit <- what could this mean then it sounds like "exit"

1mo ago·54.0s·2 calls

I've launched a focused lookup on what prompt_input_exit actually means. Here's my current understanding while it confirms:

prompt_input_exit almost certainly refers to exiting the app from the prompt-input box via the keyboard — i.e. Ctrl-C (twice) or Ctrl-D/EOF while the input line is focused. The "exit" in the name is about leaving the prompt input, not the typed string exit. It's the reason that pairs with the documented keyboard quits, alongside clear (/clear), logout, etc.

So I don't think typing the word exit triggers it — but I'd rather confirm than guess, since the name is genuinely ambiguous. I'll report back when the guide returns.

ad4e213bf6f3a640e toolu\_01YAhdcovtu8g2ZcATwicX9E /private/tmp/claude-501/-Users-soph-Work-entire-devenv-cli/d7eb2c7b-65ce-4dbe-b20a-285326019897/tasks/ad4e213bf6f3a640e.output completed Agent "Define prompt\_input\_exit reason" came to rest A task-notification fires each time this agent comes to rest with no live background children of its own. The user can send it another message and resume it, so the same task-id may notify more than once. Perfect. I now have the authoritative answer from the official Claude Code documentation. Let me provide you with precise answers to each question:

Answers (Cited from Official Documentation)

1. What prompt_input_exit means

From the hooks.md documentation, prompt_input_exit is triggered when the user exits the session through the prompt input interface — specifically keyboard-initiated exits.

2. Does it correspond to keyboard exits (Ctrl-C/Ctrl-D)?

Yes, explicitly. According to the interactive-mode.md documentation:

  • Ctrl+C (when nothing is running): First press clears the prompt input; second press exits Claude Code
  • Ctrl+D: Exit Claude Code session (EOF signal)

Both of these keyboard actions trigger prompt_input_exit in the SessionEnd hook.

3. Does typing the literal word "exit" or "quit" produce prompt_input_exit?

No. The documentation makes clear that:

  • Typing "exit" or "quit" as text would be sent to the model as a normal prompt — not a session termination
  • Only keyboard signals (Ctrl+C on the second press, or Ctrl+D) trigger prompt_input_exit
  • Typing the word "exit" produces no SessionEnd hook at all — it's just conversation text

4. Other reason values (from hooks.md table):

Reason Triggered By
clear User runs /clear command
resume User runs --resume or --continue flag, or /resume command
logout User logs out
bypass_permissions_disabled Bypass permissions mode is disabled
other Any other session termination reason

Summary

prompt_input_exit is definitively the keyboard quit signal — Ctrl+C (second press) or Ctrl+D at the interactive prompt input box. Typing the word "exit" does not produce it; that's just a normal message sent to Claude.

Sources:

1mo ago·15.3s

Confirmed authoritatively. Despite the name, prompt_input_exit is not about typing the word "exit."

It means: the user exited the app from the prompt-input box via the keyboard — i.e. Ctrl-C (second press) or Ctrl-D / EOF. Parse it as prompt-input → exit ("exited from the prompt input"), not prompt → input "exit".

So:

  • Ctrl-C ×2 or Ctrl-D → fires SessionEnd with reason prompt_input_exit → Entire marks the session ended. ✅
  • Typing exit/quit as text → sent to the model as a normal prompt → no SessionEnd at all → session stays idle. ❌

That matches your session exactly: its last entry is a normal prompt, it's idle, and no SessionEnd ever arrived — because it was never a keyboard exit.

The reason table, for completeness:

reason triggered by
prompt_input_exit Ctrl-C ×2 or Ctrl-D at the prompt
clear /clear
resume --resume/--continue//resume
logout logging out
bypass_permissions_disabled bypass-permissions mode turned off
other anything else

Bottom line: your instinct that it "sounds like exit" is a naming-coincidence trap — it's the keyboard quit, not the typed word. Either way, the picker now lists idle sessions, so this one shows up regardless of how (or whether) you formally exited.

And note from that table: clear and resume also fire SessionEnd, which Entire currently treats as ended — so /clear-ing or resuming would mark a session ended too. Still want me to make the handler ignore clear/resume so ended stays meaningful?

question: could we capture the pid in one of the hooks on a session and then when looking for active/idle session check if the pid is still active and is a claude code session (the same thing we would need for codex too)

1mo ago·7m·3 calls

What would you like to clarify? Happy to dig into any part before we settle on an approach — for example:

  • Reliability of the PID capture — whether the shell-wrapper / ppid-walk concern actually applies to how Entire's hooks are invoked for Claude Code and Codex (I can verify exactly how each agent spawns the hook).
  • The cross-host/worktree angle — how liveness should behave when you run entire resume from a different machine or worktree than where the session ran.
  • What "active" should mean in the UI — e.g. should a genuinely-running session be hidden entirely, or shown but marked "running (live)" vs "active (stale)"?
  • Dependency policy — whether adding gopsutil is acceptable here or you'd rather keep it stdlib-only.
  • Whether PID is even the right layer — vs. a simpler time-based staleness rule, or combining both.

What's on your mind?

What happens if I resume a session?

1mo ago

What happens if I resume a session? more specific the case: Session is idle, I type exit and resume it again. Will we update the pid time pair in this case?

1mo ago·1m·1 call

Confirmed the lifecycle wiring I need. Here's the precise answer.

Yes — it gets updated, if we capture on every (re)start, not once

The capture has to live on the hooks that fire each time an agent attaches to the session — SessionStart (which fires on claude -r <id> with reason resume) and turn-start (UserPromptSubmit, where I already put captureSessionBranch). On those hooks we overwrite the stored (pid, start_time, host) with the current process's values. So:

Your scenario, step by step:

  1. Session is idle, agent process P1 (pid+start-time stored).
  2. You exit → P1 dies. Stored pair now points at a dead process. (Phase becomes ended only if it was a real Ctrl-C/Ctrl-D exit; if you typed the word "exit" it stays idle and P1 is actually still alive — see caveat below.)
  3. You resume → claude -r <id> launches P2 (new pid, new start-time) → its SessionStart/first UserPromptSubmit hook overwrites the stored pair with P2's values.
  4. Liveness check now reads P2 → alive → correctly "running."

So the design requirement your question surfaces is the important one: capture must be refresh-on-every-attach, never write-once-at-session-creation. If we only stamped it at the original session start, after resume the pair would point at dead P1 and we'd wrongly show a live session as resumable. Refreshing on SessionStart + turn-start handles it.

Two nuances worth deciding

  • The post-resume-before-first-prompt window. Right after claude -r starts, SessionStart fires but no turn has begun, so phase is still idle/ended even though P2 is live. If we only refreshed on turn-start, that window would still show dead P1. Capturing on SessionStart too closes it. This also argues for treating "an agent is attached" = pid-alive (same host), and using phase==active only for "mid-turn" — pid-liveness is the better attach signal than phase.

  • Your literal "type exit" case. Since typing exit doesn't actually quit Claude Code, P1 stays alive and the stored pair stays valid — so no update happens and nothing's stale. The pair only needs refreshing when a genuinely new process attaches (real exit + resume, or resuming from another terminal — where you could briefly have two live processes for one session id; we'd just track the most recent).

Net: the (pid, start-time) pair stays correct across resume as long as capture is wired to session-start + turn-start. Want me to fold that capture-point requirement into the design (and pick a liveness approach), or keep exploring edge cases first?

I think we should pause on this and first commit what we have and maybe create a PR and then we can work on this approach on a new branch.

1mo ago·11m·2 calls

The full gate (fmt + lint + test:ci, including integration and the e2e canary) is running in the background — it takes a few minutes. I'll commit and open the PR once it's green. While it runs, here's the commit/PR I'll create so you can redirect me if needed:

  • Branch:soph/resume-session-picker
  • Commit/PR title:Add interactive resume picker for stopped/idle sessions
  • Scope: bare entire resume picker (all worktrees, idle+ended, worktree-clash routing), Branch field on session state + checkpoint-trailer derivation fallback (internal entire/ refs excluded), keep-existing-local-log-by-default on resume, plus tests and docs.
  • Not included (intentionally deferred to a later branch): the PID/start-time liveness detection.

I'll report back when the checks finish.

b3f6uczld REDACTED /private/tmp/claude-501/-Users-soph-Work-entire-devenv-cli/d7eb2c7b-65ce-4dbe-b20a-285326019897/tasks/b3f6uczld.output completed Background command "Run full pre-commit gate (fmt, lint, test:ci)" completed (exit code 0)

1mo ago·17m·5 calls

Re-running the full test:ci to /tmp/resume-testci.log in the background (~12 min). I'll inspect the complete log when it finishes — if it's green I'll commit and open the PR; if it fails again I'll have the full failure detail to diagnose. Standing by for the completion notification.

bozgritl9 toolu\_01XsBR9tk9bLJdYE6fwJBrok /private/tmp/claude-501/-Users-soph-Work-entire-devenv-cli/d7eb2c7b-65ce-4dbe-b20a-285326019897/tasks/bozgritl9.output completed Background command "Re-run full test:ci to a log file" completed (exit code 0)

1mo ago·1m·4 calls

Done. Everything is locked in.

  • Branch:soph/resume-session-picker (pushed)
  • Commit:605ece819 — Add interactive resume picker for stopped/idle sessions
  • PR: https://github.com/entireio/cli/pull/1445
  • Pre-merge gate:mise run check (fmt + lint + unit + integration + e2e canary) green. The one earlier FAIL was a flaky test — the clean re-run confirmed it.

The PID/process-liveness work is explicitly carved out as a follow-up in the PR body, so it's clear that's intentionally not here.

When you're ready to pick up the liveness approach, just say so and I'll branch off main (or off this branch if you want it stacked) and start there — we left off with the design settled around capturing (pid, start_time, host) on session-start + turn-start, treating pid-liveness (not phase) as the "agent attached" signal, and the open choice between gopsutil vs stdlib-only.

605ece8Add interactive resume picker for stopped/idle sessions\ `entire resume` with no argument now opens an interactive picker of\ resumable sessions across all worktrees, so you don't have to remember\ which branch you left work on. Picking a session checks out its branch\ and prints the command to continue the agent; if the branch is already\ checked out in another worktree, it points you there instead of failing\ a checkout. `entire resume ` is unchanged.\ \ Details:\ - Resumable = any session not currently mid-turn (idle + ended), not just\ sessions explicitly ended via `session stop`. Exiting an agent leaves a\ session idle (only a real SessionEnd marks it ended), so idle is the\ common "walked away" case and must be included.\ - Adds a `Branch` field to session state, captured on each turn start, and\ surfaced in `session list --json`. For sessions recorded before the\ field existed, the branch is derived by matching the session's last\ checkpoint ID against branch-only commit trailers. Internal `entire/`\ refs (metadata branch + shadow branches) are excluded from that index,\ both to avoid mis-resolving to a non-resumable ref and to keep the scan\ fast (the index is also built lazily and avoids go-git MergeBase).\ - Resume now keeps an existing local session log as-is by default; only a\ missing log is restored from the checkpoint. `--force` overwrites. This\ is driven by file existence, not transcript timestamps.\ \ Tests cover the picker filtering/sorting, labels, branch derivation,\ internal-ref exclusion, worktree-clash detection, and keep-existing-log\ behavior. Obsolete overwrite-prompt tests removed; timestamp resume\ integration tests updated to keep-by-default semantics.\ \ Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com\ Entire-Checkpoint: 5f34a04f3d49+838/-240

I wonder now if this is confusing when you are used to the prior behaviour. Like before it worked on local and remote sessions (by using the branch) but now it's a local only picker. But I don't think we have a good way of doing a remote picker? like how even to figure out which branches are still relevant

1mo ago·1m

Good instinct, and worth untangling — because I think the worry is partly a framing issue and partly a genuine hard problem.

What actually changed (and what didn't)

The remote capability isn't lost. entire resume <branch> is 100% intact — including the "branch isn't local? fetch it from origin and resume" path. The picker is purely additive on top of that; before this PR there was no picker at all, so no one loses the remote-branch flow. The only new expectation gap is: someone might assume the new no-arg picker also surfaces remote sessions, and it doesn't.

Why the picker is local-only

It's driven by session-state files in .git/entire-sessions/ — which are per-machine. So it can only know about sessions that ran on this machine. That's actually a feature for the "what was I doing" use case (it includes idle/uncommitted sessions with rich live state — phase, last-interaction, tokens — none of which exist remotely), but it's intrinsically local.

The remote picker — your relevance instinct is the crux

It's technically doable: enumerate origin branches (cheaply via already-fetched refs/remotes/origin/*, or with a network ls-remote), find each branch's checkpoint trailer, read session metadata (prompt/agent/time) from the pushed entire/checkpoints/v1 metadata. That's the same data resume <branch> already consumes.

But "which branches are still relevant" is exactly the wall — and this repo (460 branches) is the perfect cautionary example. A raw list of every branch-with-a-checkpoint is useless. You'd need filters, each imperfect:

  • Not merged into main (work still outstanding) — best signal, but merge-detection against remote may need a fetch.
  • Recent — sort by latest checkpoint date; cut off old ones.
  • Authored by you — filter by committer/session, since a shared remote has everyone's branches.
  • Has an Entire checkpoint — drops plain branches.

Even with all of those, it's heuristic, needs network round-trips, and reads metadata per branch. It's a meaningfully bigger, fuzzier feature than the local picker — fundamentally "branch-oriented" rather than "session-oriented."

My recommendation

  1. Now (tiny, in this PR if you want): add a one-line footer to the picker so the scope is explicit, e.g. "Showing sessions from this machine. To resume a branch from origin: entire resume <branch>." That removes the confusion for someone used to the old branch flow, at near-zero cost.
  2. Later (separate feature): an opt-in entire resume --remote / --all that does the branch-oriented listing with the relevance filters above — keeping the default fast and local. I'd group it with or after the PID work, since both are "make the session list smarter" efforts.

Want me to add the footer hint to PR #1445 before it merges? And should I write up the remote/branch-picker (with the relevance heuristics) as a tracked follow-up alongside the PID note?

add the footer

1mo ago·5m·3 calls

Done — footer added and pushed to PR #1445.

The picker now shows, below the title:

Resume a session
Checks out the branch and prints the command to continue the agent.
Lists sessions from this machine — to resume a branch from origin, run: entire resume <branch>

(huh's Select has no dedicated footer slot, so this renders as the description directly under the title — the reliably-visible spot.) Build/lint/tests green, explain.go untouched, pushed as commit 5910581e8.

So #1445 now has the picker + the scope-clarifying hint. Still queued as separate follow-ups whenever you want them: the PID/process-liveness detection, and the opt-in remote/branch picker (--remote/--all with relevance filters).

5910581resume picker: clarify local scope with a hint\ \ The picker lists sessions from the local machine only (session state is\ per-machine). Add a line to the picker so users coming from the\ branch-argument flow know that resuming a branch from origin still works\ via `entire resume `.\ \ Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com\ Entire-Checkpoint: b6cc4eeb00bc+2/-1

High: The picker can resume the wrong session. In cmd/entire/cli/resume_picker.go:98, the selected row is a specific SessionState, but cmd/entire/cli/resume_picker.go:115 passes only chosen.branch into runResume. resumeFromCurrentBranch then searches branch history for the latest checkpoint in cmd/entire/cli/resume.go:164, ignoring chosen.state.SessionID and LastCheckpointID. So two stopped sessions on the same branch both resume the same latest branch checkpoint; an idle session with a stored branch but no committed checkpoint is selectable and then either fails with “No Entire checkpoint found” or resumes an unrelated older checkpoint. The picker should either resume from the selected state/checkpoint ID, or list branches rather than sessions. Add coverage for two stopped sessions on one branch and a Branch-only idle session with empty LastCheckpointID.

1mo ago·22m·31 calls

Fixed and pushed to PR #1445 (commit 32bed47a2). The reviewer's finding was spot-on; here's what changed.

The bug

The picker let you select a specific session, but on selection it only passed chosen.branch to runResume, which re-derived the latest checkpoint on that branch via findBranchCheckpoints — completely ignoring which session you picked. So:

  • Two sessions on one branch → both resumed the branch's latest checkpoint.
  • An idle session with a stored branch but no committed checkpoint was selectable, then either failed with "No Entire checkpoint found" or resumed unrelated older work.

The fix

  1. Carry the checkpoint ID through the picker.resumableSession now holds checkpointID (the session's LastCheckpointID). A row is selectable only if it has both a branch and a committed checkpoint — otherwise it's shown disabled with the reason (no branch / no committed checkpoint). That makes the branch-only idle session non-selectable instead of misleading.
  2. Resume the selected session specifically. New resumeSessionOnBranch → resumeByCheckpointID switches to the branch and restores that checkpoint's session(s), rather than searching branch history. The checkout logic is shared with runResume via an extracted switchToBranchForResume (no duplication).

Coverage added (the two cases the reviewer named)

  • Two sessions on one branch → TestResolveResumableBranches_TwoSessionsSameBranch asserts each item carries its own distinct checkpoint, plus TestResumeByCheckpointID_ResumesRequestedSession proves resuming the older session restores only it (session-a) and not the newer session-b.
  • Branch-only idle session, empty LastCheckpointID → TestResumableSession_RequiresCheckpoint + the updated buildResumeOptions test assert it's not selectable.

Lint clean, explain.go untouched, unit + resume integration tests green.

One judgment call worth flagging: I made the picker resume log-only at the branch tip (checkout branch + restore the chosen session's transcript) rather than resetting the working tree to the selected checkpoint's commit. That matches "continue this conversation" and avoids surprise resets, but it does mean if you pick an older session on a branch that has since moved on, your code is at the branch tip, not that checkpoint. If you'd rather it reset to the selected checkpoint's commit, that's a small follow-up — let me know.

32bed47resume picker: resume the selected session, not the branch's latest\ \ The picker is session-oriented but the action only passed the branch to\ runResume, which re-derived the latest checkpoint on that branch and\ ignored the selected session. So two sessions on the same branch both\ resumed the latest, and an idle session with a stored branch but no\ committed checkpoint was selectable yet resumed unrelated work (or failed\ with "No Entire checkpoint found").\ \ Fix:\ - Carry each session's checkpoint ID through the picker. A row is\ selectable only when it has BOTH a branch and a committed checkpoint;\ otherwise it's shown disabled with the reason (no branch / no committed\ checkpoint).\ - On selection, resume that specific checkpoint via a new\ resumeSessionOnBranch/resumeByCheckpointID path that switches to the\ branch and restores the chosen session, instead of searching branch\ history. Checkout logic is shared with runResume via\ switchToBranchForResume (no duplication).\ \ Tests: two sessions on one branch each carry their own checkpoint;\ resuming a specific checkpoint restores that session and not the newer\ one; a branch-only session with no checkpoint is not selectable.\ \ Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com\ Entire-Checkpoint: f0b960dc6333+243/-45

so this would now show multiple lines per branch if there are multiple?

1mo ago·28.3s

Yes — exactly. The picker is session-oriented: one row per session, so two sessions on the same branch show as two rows. After the fix, that's now correct (selecting each resumes its own session), where before it was the bug (both pointed at the branch's latest).

The rows are disambiguated by the other fields, since the branch repeats:

my-branch · "refactor the auth flow"   · Claude Code · last active 2h ago
my-branch · "add rate limiting tests"  · Claude Code · last active 3d ago

Same branch, different prompt + last-active time. So you can tell them apart and resume the specific conversation you want.

Whether that's good or noisy depends on how you work a branch:

  • If you typically have one session per branch, you'll see one row per branch — clean.
  • If you re-open an agent on the same branch across several sittings, each is a distinct session → multiple rows for that branch. Could feel repetitive.

If the repetition bothers you, the alternative is to collapse to one row per branch (showing the most-recently-active session, resuming that). Trade-off: you'd lose the ability to pick an older session on a branch from the list — though entire resume <branch> would still land on the latest anyway.

My lean is to keep it per-session (it's the more capable model and matches "resume the conversation I left," including older ones), but I can add collapsing — or a --by-branch view — if you'd prefer the tidier list. Want either, or leave it per-session?

I wonder if an improvement would be to have a "v" key that let's you view the session

1mo ago·2m·2 calls

Good news on feasibility: the repo already leans on bubbletea heavily (search_tui.go, activity_tui.go, dispatch_tui.go, recap_tui.go, review/tui_model.go), with bubbletea/v2 + bubbles/v2 as deps — and there are reusable renderers in sessions.go: writeSessionInfoText (summary) and writeSessionTranscript (the conversation). So the building blocks are all here.

The one constraint

The current picker is a huh.NewSelect, and huh's Select has no clean hook for a per-row secondary action like "press v." So a v-to-view key means replacing the huh picker with a small custom bubbletea model — which is exactly what the other *_tui.go files do, so there's a well-worn pattern to copy.

What it'd look like

A bubbletea list with keybindings:

  • ↑/↓ navigate, Enter resume, v view, q/esc cancel.
  • vopens a preview of the highlighted session. Two depths to choose from:
  1. Quick summary — reuse writeSessionInfoText: prompt, agent/model, branch, worktree, last-active, tokens, files touched, checkpoint. Cheap (data's already in memory), shows in a side panel or toggled pane.
  2. Full transcript — reuse writeSessionTranscript in a scrollable bubbles/viewport. Richer ("what was I actually doing"), but reads the transcript file and is more UI.

My lean: do both in layers — a summary panel always visible for the highlighted row (so you get context just by scrolling, no keypress), and v to expand into the scrollable transcript. The summary-on-highlight alone might even remove the need for a keypress in most cases.

Caveats / cost

  • Accessibility: the current code respects ACCESSIBLE=1 via NewAccessibleForm. A custom TUI needs a deliberate accessible fallback (numbered list + "enter a number to view" prompt), which huh gave us for free. That's the main added cost.
  • Scope: this is a bigger change than #1445 (swap huh → custom model + preview + accessible path), so it's a follow-up branch, not part of the current PR.

So: definitely worth doing and well-supported here. The decision I'd want before building: should v show the quick summary, the full scrollable transcript, or the layered approach (summary on highlight + v for transcript)? I'd recommend layered. Want me to write this up as a tracked follow-up alongside the PID and remote-picker notes?

can you take a look at the activity command? how did we do the list view there?

1mo ago·5m·4 calls

Here's what I found — and it changes the recommendation slightly.

How activity does its "list"

activity is not a selectable list — it's a read-only scrolling dashboard:

  • A bubbletea model (activityModel, AltScreen) that loads data async (fetchData cmd → activityDataMsg/activityErrMsg), showing a spinner while loading.
  • Layout = a static header (renderHeader: stat cards + charts) printed above a single viewport.Model. The "commit list" is just rendered to a string (renderCommitListN) and stuffed into viewport.SetContent — there's no cursor, no per-row selection, no Enter.
  • Navigation is scroll only (↑/↓, j/k, home/end, g/G, q), with a footer help line + scroll-percent. On WindowSizeMsg it recomputes the viewport height (height − header − footer).

So activity gives us the scrollable-content pattern (great for a transcript preview pane), but it has no selection model — wrong template for a picker.

The right template: search_tui.go

This one is exactly your idea already built:

  • A hand-rolled selectable list: cursor, highlighted selected row style, selectedResult(), pagination (resultsPerPage).
  • Mode switching: modeBrowse (the list) ↔ modeDetail (a full-screen detail view in its own detailVP viewport.Model) ↔ modeSearch. Selecting a row and viewing its detail is precisely the v-to-view flow we discussed — it's done here as a mode transition into a viewport.

So for the resume picker

Reuse search_tui's structure, not activity's:

  • cursor + selectedResult() for navigation, Enter → resume the selected session.
  • v → switch to a modeDetail with a detailVP viewport (the activity viewport pattern) rendering the session via the existing writeSessionInfoText (summary) and/or writeSessionTranscript (full conversation).
  • esc from detail → back to the list; q → cancel.

Net: search_tui.go is the blueprint (selection + detail-mode), activity_tui.go is the reference for the scrollable detail pane itself. Both already exist in the repo, so building a custom resume picker with v-to-view is well-supported.

Want me to write this up as the tracked follow-up (custom picker TUI modeled on search_tui, with v → session view), alongside the PID and remote-picker notes?

can you actually create a draft trail for each of them? "entire trail create --help" should give you all you need

1mo ago·5m·6 calls

All three draft trails are created and visible. Done.

# Branch Trail Status
583 soph/session-liveness-pid Session liveness: detect crashed 'active' sessions via PID + start-time draft
586 soph/resume-remote-picker Resume picker: opt-in remote/branch view (entire resume --remote) draft
587 soph/resume-picker-tui-view Resume picker: custom TUI with 'v' to preview a session draft

Each captures the design we worked out (including the footguns/decisions — e.g. ppid-walk + PID-reuse + cross-host fallback for liveness; the relevance-filter problem for the remote picker; search_tui.go as the template + accessibility caveat for the TUI). Each got its own branch off main, pushed to origin, all as draft. I stayed on soph/resume-session-picker (the current PR #1445 branch) — no checkout switch.

Note: trail create pushes the new branch to origin, so those three branches now exist on the remote (empty, just based on main). They'll surface under entire trail list --status draft. Nothing was merged or opened as a PR.

So the state now:

  • PR #1445 (soph/resume-session-picker): the picker feature + footer + the resume-the-selected-session fix — ready for review.
  • 3 draft trails: the follow-ups, queued and documented.

on the current work:

  • Medium: cmd/entire/cli/resume_picker.go:128 still drops the selected checkpoint in the “branch checked out in another worktree” path. It tells the user to run entire resume , which resumes the branch’s latest checkpoint, not the selected session. This reintroduces the old bug for the worktree-clash case when multiple sessions share a branch. The guidance should preserve the selected-session flow, likely by telling the user to cd there and run the picker again, or by adding a checkpoint/session-specific resume entrypoint.

  • Medium: cmd/entire/cli/resume_picker.go:128 prints the branch unquoted in a copy-paste shell command. Git accepts branch names like x;echo-pwn and x&echo-pwn, so the suggested command can execute unintended shell tokens if copied. Quote/shell-escape the branch argument too, not just otherPath.

1mo ago·6m·14 calls

Both findings fixed and pushed to PR #1445 (commit 607d7060b).

What changed in the worktree-clash path

Finding 1 — wrong session. The clash path used to print cd <path> && entire resume <branch>, which resumes the branch's latest checkpoint — the exact bug we just fixed everywhere else, sneaking back in for the clash case. Now it tells the user to re-run the picker in that worktree (cd <path> && entire session resume). The branch is already checked out there, so the picker lists the session, the clash no longer triggers (it's the current worktree), and selecting it resumes the specific session by its checkpoint — selected-session flow preserved. I also name the session by its prompt in the message so it's easy to re-pick.

Finding 2 — shell injection. The branch is no longer in the executable command at all (it only appears in non-executable %q prose), so a branch like x;echo pwn can't inject. The worktree path — the one value that is in the command — is now shellQuoted (POSIX single-quote escaping, same as the strategy package's helper), so $(...), ;, spaces, and apostrophes in a path stay inert.

Coverage

  • TestShellQuote — including $(echo pwn), x;echo pwn, and apostrophe escaping.
  • TestWorktreeClashMessage — three subtests proving: (a) it points at the picker, never entire (session) resume <branch>; (b) a malicious branch name never reaches the command line; (c) a path with $(...) is single-quoted and an apostrophe is escaped.

Message building was factored into worktreeClashMessage to make it unit-testable. Lint clean, explain.go untouched, resume unit tests green.

One thing I deliberately did not do: add a public session/checkpoint-specific resume entrypoint (the reviewer's alternative). Pointing at the picker is the smaller, surface-free fix and fully preserves correctness. If you'd rather have a direct entire session resume --session <id> (also useful for scripting), that's a clean follow-up — say the word and I'll spin up a trail for it.

[P2] cmd/entire/cli/resume_picker.go:250: the legacy branch-resolution fallback skips the default branch, but still walks default-branch history through every non-default branch. For pre-PR session states with Branch == "" and a LastCheckpointID on main/ master, the picker will either show “no branch” or mis-map that checkpoint to some feature branch that contains the commit. Selecting it then checks out the wrong branch before restoring the exact checkpoint. This contradicts the documented fallback for old sessions. I’d add a test for a legacy session checkpoint on main, and either index the default branch explicitly or only attribute a checkpoint to feature branches when it is actually branch-only.

1mo ago·2h 8m·37 calls

All 8 threads resolved, 0 unresolved. Done.

Summary

Addressed every Copilot/Cursor comment on PR #1445 with code fixes, pushed as 9f95b6cad, then replied to each thread (citing the commit + what changed) and resolved all of them.

# Bot Sev Issue Fix
1 cursor High Bare resume had no TTY gate — opened a form that can't render in CI/pipes Gate on interactive.CanPromptInteractively(); bail with guidance
2 cursor Med Picker used form.Run(), ignoring command context form.RunWithContext(ctx) (already used NewAccessibleForm)
3 cursor Med Worktree-clash false positive when WorktreeRoot fails Return ("", false) when current root is unknown
4 cursor Low Ctrl+C wrapped as noisy "selection failed" context.Canceled + ErrUserAborted exit cleanly
5 Copilot — captureSessionBranch left stale branch on detached HEAD Clear state.Branch on detached HEAD; added TestCaptureSessionBranch
6 Copilot — resumeSingleSession doc comment outdated Rewrote to keep-by-default semantics
7 Copilot — Single-session message differed from multi ("Keeping existing") Aligned wording
8 Copilot — Branch iterator not closed defer iter.Close() (thread was already auto-resolved)

All green: build, lint (0 issues), unit + resume integration tests; explain.go untouched.

PR #1445 state now

Commits on soph/resume-session-picker:

  1. Picker feature
  2. Local-scope footer hint
  3. Resume selected session (not branch-latest)
  4. Worktree-clash guidance + shell-injection fix
  5. Default-branch checkpoint attribution (legacy fallback)
  6. Copilot/Cursor comment fixes

All review threads resolved. The three deferred items (PID liveness, remote picker, v-to-view TUI) remain as draft trails. Worth noting: the bots review per-commit, so new commits may draw a fresh round of comments — ping me and I'll work through those too.

can you check the "entire trail finding"

1mo ago·9m·11 calls

Checked and cleared. Here's what entire trail finding surfaced and what I did.

What it is

entire trail finding is the dashboard of agent-native findings attached to a trail (separate from GitHub PR review comments) — reviewers/agents file findings with severity, confidence, file:line, and a suggested fix; you show/resolve/dismiss/apply them.

What was open

Trail #581 (this branch) had 1 open medium finding (confidence 0.85) on branchCheckedOutElsewhere: the git worktree list --porcelain parser could, in theory, mis-pair a branch line with a stale worktree path when a detached-HEAD worktree (no branch line) is in the output.

The specific mechanism it described wasn't actually live (curPath is overwritten at each worktree line, so a branch line already pairs with its own block) — but the defensive suggestion was sound, so I implemented it rather than dismissing:

  • Factored the parsing into parseWorktreeForBranch, now block-scoped: reset the path at each blank-line block boundary, and only match a branch line when a worktree line was seen in the same block. A detached worktree can no longer pair a branch with a stale path or return an empty path.
  • Dropped the now-dead rawRoot != "" guard (the empty case returns earlier).
  • Added TestParseWorktreeForBranch covering a detached worktree.

Committed/pushed as eed087fe2, lint clean, explain.go untouched, then resolved the finding with a note. Dashboard now shows 0 open findings (1 resolved).

So the trail's findings are clean, and PR #1445's review threads are all resolved too. Want me to also update the trail's status (it's currently open) or post a recap, or move on to one of the draft follow-ups?

103fcd7Add interactive resume picker for stopped/idle sessions\ `entire resume` with no argument now opens an interactive picker of\ resumable sessions across all worktrees, so you don't have to remember\ which branch you left work on. Picking a session checks out its branch\ and prints the command to continue the agent; if the branch is already\ checked out in another worktree, it points you there instead of failing\ a checkout. `entire resume ` is unchanged.\ \ Details:\ - Resumable = any session not currently mid-turn (idle + ended), not just\ sessions explicitly ended via `session stop`. Exiting an agent leaves a\ session idle (only a real SessionEnd marks it ended), so idle is the\ common "walked away" case and must be included.\ - Adds a `Branch` field to session state, captured on each turn start, and\ surfaced in `session list --json`. For sessions recorded before the\ field existed, the branch is derived by matching the session's last\ checkpoint ID against branch-only commit trailers. Internal `entire/`\ refs (metadata branch + shadow branches) are excluded from that index,\ both to avoid mis-resolving to a non-resumable ref and to keep the scan\ fast (the index is also built lazily and avoids go-git MergeBase).\ - Resume now keeps an existing local session log as-is by default; only a\ missing log is restored from the checkpoint. `--force` overwrites. This\ is driven by file existence, not transcript timestamps.\ \ Tests cover the picker filtering/sorting, labels, branch derivation,\ internal-ref exclusion, worktree-clash detection, and keep-existing-log\ behavior. Obsolete overwrite-prompt tests removed; timestamp resume\ integration tests updated to keep-by-default semantics.\ \ Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com\ Entire-Checkpoint: 5f34a04f3d49+838/-240

b0e068bresume picker: clarify local scope with a hint\ \ The picker lists sessions from the local machine only (session state is\ per-machine). Add a line to the picker so users coming from the\ branch-argument flow know that resuming a branch from origin still works\ via `entire resume `.\ \ Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com\ Entire-Checkpoint: b6cc4eeb00bc+2/-1

5866380resume picker: resume the selected session, not the branch's latest\ \ The picker is session-oriented but the action only passed the branch to\ runResume, which re-derived the latest checkpoint on that branch and\ ignored the selected session. So two sessions on the same branch both\ resumed the latest, and an idle session with a stored branch but no\ committed checkpoint was selectable yet resumed unrelated work (or failed\ with "No Entire checkpoint found").\ \ Fix:\ - Carry each session's checkpoint ID through the picker. A row is\ selectable only when it has BOTH a branch and a committed checkpoint;\ otherwise it's shown disabled with the reason (no branch / no committed\ checkpoint).\ - On selection, resume that specific checkpoint via a new\ resumeSessionOnBranch/resumeByCheckpointID path that switches to the\ branch and restores the chosen session, instead of searching branch\ history. Checkout logic is shared with runResume via\ switchToBranchForResume (no duplication).\ \ Tests: two sessions on one branch each carry their own checkpoint;\ resuming a specific checkpoint restores that session and not the newer\ one; a branch-only session with no checkpoint is not selectable.\ \ Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com\ Entire-Checkpoint: f0b960dc6333+243/-45

784ad29resume picker: fix worktree-clash guidance (wrong session + shell injection)\ \ The "branch checked out in another worktree" path told the user to run\ `entire resume `, which resumes the branch's LATEST checkpoint —\ reintroducing the wrong-session bug for the clash case when several\ sessions share a branch. It also interpolated the branch (and path) into\ a copy-paste shell command unquoted, so a branch like `x;echo pwn` or a\ path with shell metacharacters could execute unintended tokens.\ \ Now the guidance points the user to re-run the picker in that worktree\ (`cd && entire session resume`), which preserves the\ selected-session flow (the picker resumes the exact session by its\ checkpoint). The branch is no longer part of the command — it appears\ only in non-executable prose — and the worktree path is shell-quoted via\ a new shellQuote helper.\ \ Message building is factored into worktreeClashMessage; tests cover the\ picker-not-branch-arg guidance and injection-safety for both a malicious\ branch name and a path containing $(...) or apostrophes.\ \ Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com\ Entire-Checkpoint: 2fe06f6e777d+115/-5

098e392resume picker: attribute default-branch checkpoints correctly (legacy fallback)\ \ The legacy branch-resolution fallback skipped the default branch entirely\ but still walked default-branch history through every feature branch. A\ pre-Branch-field session whose checkpoint was committed on main/master\ would either show "no branch" or be mis-mapped to a feature branch that\ merely contains the commit — and selecting it then checked out the wrong\ branch before restoring the checkpoint.\ \ Now buildCheckpointBranchIndex indexes the default branch FIRST (so its\ checkpoints map to the default branch) and records its commit hashes as a\ stop-set; feature-branch walks halt at shared default history, so they\ only claim their own branch-only checkpoints. The default branch commit\ is resolved from the local ref or origin's remote-tracking ref. Still no\ go-git MergeBase (too slow at scale) — the default-commit set is the cheap\ stand-in for branch-only scoping.\ \ Test: a checkpoint committed on the default branch maps to it, while a\ feature-only checkpoint still maps to the feature branch.\ \ Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com\ Entire-Checkpoint: 39489eeb2fdf+131/-28

5d9e2a5resume picker: address Copilot/Cursor review comments\ - TTY gate: bare `entire resume` now bails with guidance when there's no\ interactive terminal instead of opening a form that can't render\ (cursor, High).\ - huh form uses RunWithContext(ctx) so it honors the command context, and\ context.Canceled (like ErrUserAborted) exits cleanly without a noisy\ "selection failed" error (cursor, Medium/Low).\ - branchCheckedOutElsewhere returns false when the current worktree root\ can't be resolved, instead of treating every worktree (incl. the current\ checkout) as "elsewhere" and printing a misleading clash hint (cursor,\ Medium).\ - captureSessionBranch clears state.Branch on a detached HEAD so a stale\ branch from a previous turn can't mis-map the session on resume; the\ derivation fallback takes over (Copilot).\ - buildCheckpointBranchIndex defers iter.Close() on the branch iterator\ (Copilot).\ - resumeSingleSession doc comment updated to the keep-by-default behavior,\ and its kept-log message aligned to the "Keeping existing ..." wording\ used by the multi-session path (Copilot).\ \ Adds TestCaptureSessionBranch (on-branch capture + detached-HEAD clear).\ \ Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com\ Entire-Checkpoint: cb36c5558f2c+80/-7

5ef1842resume picker: make worktree porcelain parsing block-scoped (trail finding)\ \ Factor the `git worktree list --porcelain` parsing in\ branchCheckedOutElsewhere into parseWorktreeForBranch and make it\ block-scoped: reset the worktree path at each blank-line block boundary\ and only consider a `branch` line when a worktree line was seen in the\ same block. A detached worktree (no branch line) can no longer pair a\ branch with a stale path or return an empty path. Also drops the now-dead\ rawRoot!="" guard (the empty case returns earlier).\ \ Adds TestParseWorktreeForBranch covering a detached worktree.\ \ Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com\ Entire-Checkpoint: dc7a0655039b+59/-5