Skip to content

fix(status): list every session without mutating state - #2363

Open
peyton-alt wants to merge 3 commits into
mainfrom
fix/status-readonly-sessions
Open

fix(status): list every session without mutating state#2363
peyton-alt wants to merge 3 commits into
mainfrom
fix/status-readonly-sessions

Conversation

@peyton-alt

@peyton-alt peyton-alt commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

https://entire.io/gh/entireio/cli/trails/1299

Problem

entire status had two defects in how it reports sessions, both fixed here.

--json collapsed active_sessions to one entry per agent. Two sessions for the same agent — one per worktree, a normal setup — showed as one, and nothing in the output let a consumer recover the second. The text path already listed every session, so the two surfaces disagreed.

Reporting mutated what it reported. Both paths read through StateStore.List, which deletes stale records as it reads, and both ran finalizeExitedSessions. Running the command people run to look at state changed that state: a stale session vanished because you looked at it, and a dead-owner session was finalized on read instead of being reported as exited.

Summary

  • add StateStore.ListReadOnly beside the existing mutating reads, splitting stale cleanup behind a parameter
  • status reads read-only on both the text and JSON paths and finalizes nothing
  • --json reports one entry per session, carrying session_id, worktree_path, and branch

List and Load keep their cleanup contract exactly as before — entire doctor and the session sweeper still call them and still finalize, verified by test.

Behaviour changes worth calling out in review

  1. active_sessions JSON shape: one entry per session instead of one per agent. Consumers grouping on agent still work; anything relying on exactly one entry per agent does not. The reverse was impossible — a collapsed entry could not be un-collapsed.
  2. Status no longer finalizes exited sessions. A dead-owner session now shows as exited until doctor or the sweeper reaches it. CLAUDE.md and sessions-and-checkpoints.md both documented the old behaviour and are corrected here.

TestRunStatusJSON_DeduplicatesSessions pinned the old shape and is replaced by TestRunStatusJSON_ReportsEachSessionSeparately rather than deleted.

Verification

  • mise run check — unit, integration, and the Vogon canary, all green
  • new: TestStateStore_ListReadOnly_ReturnsStaleWithoutDeleting asserts stale records survive a read and that List still deletes them
  • new: TestRunStatusJSON_ListsEverySessionWithoutMutating — went red on both defects before the fix
  • ported: TestStatusMultipleSessionsPreservedAcrossWorktrees, TestStatusReadOnlyPreservesDeadOwnerStateAndTranscript (asserts state file and transcript are byte- and mtime-identical after status)

Fixes #2362

Split out of #2343, which bundled this with unrelated Git-hook work.

🤖 Generated with Claude Code


Note

Medium Risk
Changes observable status --json shape (breaking for consumers that assumed one entry per agent) and defers finalization of exited sessions until doctor/sweeper, which may leave sessions visible longer but avoids side effects on a read-only command.

Overview
entire status is now read-only for session state. Both text and JSON paths use StateStore.ListReadOnly instead of List, so stale session files are not deleted on read and finalizeExitedSessions is no longer run from status. Dead-owner sessions show as exited until entire doctor or __sweep_sessions finalizes them; docs (CLAUDE.md, sessions-and-checkpoints.md) match that split.

status --json lists one row per session, not one per agent. active_sessions entries add session_id, worktree_path, and branch so multiple concurrent sessions for the same agent (e.g. per worktree) are all visible and align with the text output.

List / Load still delete stale sessions when deleteStale is true; doctor and the sweeper keep using List. New unit and integration tests cover read-only listing, JSON shape, and no mutation of state or transcript after status.

Reviewed by Cursor Bugbot for commit b5c900d. Configure here.

peyton-alt and others added 3 commits September 10, 2026 21:24
`entire status` reports what sessions exist, and reporting should not change
what it reports. Today status goes through List, which deletes any record
that has gone stale — so asking the question mutates the answer.

Split the stale cleanup behind a parameter and expose ListReadOnly for the
passive path. List and Load keep their existing contract: doctor and the
session sweeper own cleanup and are unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 01M27BBHCNZ7H36WTWRFT6TQB1
Two defects in how status reports sessions, both fixed by the same change.

`--json` collapsed active_sessions to one entry per agent, so a developer
running two sessions for the same agent — one per worktree, a normal setup —
saw only one of them, and there was no way to recover the other from the
output. Each session is now its own entry, carrying session_id. Consumers
wanting a per-agent view can group on the agent field; they could not do the
reverse.

Both the text and JSON paths also read through List, which deletes stale
records as a side effect, and ran finalizeExitedSessions. So looking at status
changed what status reported. Both now read through ListReadOnly and finalize
nothing. Cleanup is unchanged for the callers that own it: doctor and the
session sweeper still call List and still finalize.

Replaces TestRunStatusJSON_DeduplicatesSessions, which pinned the old shape,
with a test asserting each session is reported separately.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 01M27BPPKF4WMPH5FJRK1ZV126
Ports the two integration regressions: two sessions for one agent across
worktrees both appear with their worktree_path, and a dead-owner session is
reported as exited with its state file and transcript byte- and
mtime-identical afterwards.

Adds worktree_path and branch to the per-session JSON entries — without them
two entries for the same agent are reported but not distinguishable, which
was the point.

CLAUDE.md and sessions-and-checkpoints.md both stated that
finalizeExitedSessions sweeps inside `entire status`. That is no longer true
and is now recorded as a deliberate property: doctor and the sweeper own
cleanup, status observes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 01M27C71EV3V1V1MQYXE4ZQ6S1

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 Needs a closer look

Unresolved moderate findings remain in status behavior and integration coverage.

Pull request overview

Updates entire status to read session state without mutation and report every session separately in JSON.

Changes:

  • Adds read-only session listing while retaining cleanup for mutating callers.
  • Adds per-session JSON metadata and removes status-time finalization.
  • Expands tests and updates lifecycle documentation.
File summaries
File Reviewed changes
docs/architecture/sessions-and-checkpoints.md Documents read-only status behavior; nit remains to update contradictory finalization comments.
cmd/entire/cli/status.go Adds read-only reporting and per-session JSON output; moderate findings remain around branch-resolution caching, stale status labeling, and branch assertions.
cmd/entire/cli/status_test.go Covers per-session JSON output and read-only behavior.
cmd/entire/cli/session/state.go Adds ListReadOnly; clarify that other List callers retain cleanup behavior.
cmd/entire/cli/session/state_test.go Tests preservation of stale records during read-only listing.
cmd/entire/cli/integration_test/status_health_test.go Tests session visibility and immutability; moderate findings remain around platform-aware liveness assertions and branch validation.
CLAUDE.md Updates session lifecycle guidance.
Review details

Suppressed comments (7)

cmd/entire/cli/integration_test/status_health_test.go:59

  • This fixture assumes proclive.Check can prove that the recorded owner is dead. On platforms where process introspection is unsupported (for example Windows), Check returns LivenessUnknown; because lastActive is only one minute old, sessionStatusLabel falls back to active, so the assertions for exited fail. Make the test platform-aware (or assert the platform's time-based fallback) while retaining the byte/mtime checks.
		Owner: &proclive.Identity{PID: os.Getpid(), Start: "not-this-process"}, TranscriptPath: transcriptPath,

cmd/entire/cli/integration_test/status_health_test.go:39

  • The new branch field is not decoded or asserted here, so the integration test would still pass if status --json omitted it or returned the wrong branch. Since this field is part of the new per-session shape, include it in the fixture and assert the two worktree branches as well.
		ActiveSessions []struct {
			SessionID    string `json:"session_id"`
			WorktreePath string `json:"worktree_path"`
		} `json:"active_sessions"`

cmd/entire/cli/session/state.go:1111

  • This says cleanup stays with only doctor and the sweeper, but List is also reached through strategy.ListSessionStates by commands such as session stop/resume, cleanup, and caller resolution. Clarify that status opts out while other callers that use List retain its cleanup contract.
// `entire status` is the caller this exists for: asking what is happening must
// not change what is happening. Cleanup stays with the callers that own it —
// doctor and the session sweeper — which keep using List.

cmd/entire/cli/status.go:976

  • This fallback runs inside the per-session loop, so every legacy or detached session with an empty Branch resolves the same worktree independently. resolveWorktreeBranch performs filesystem reads and shells out to git for reftable stubs (with a 5-second timeout), so N sessions in one worktree can turn status into N repeated resolutions. Cache the resolved branch by WorktreePath for this status snapshot before appending the entries.
					branch := st.Branch
					if branch == "" && st.WorktreePath != "" {
						branch = resolveWorktreeBranch(ctx, st.WorktreePath)

cmd/entire/cli/status.go:985

  • Now that ListReadOnly returns stale ACTIVE records, this can emit them with status:"active": sessionStatusLabel has no IsStuckActive branch, while the text path renders those same states as stale. Preserve that health distinction in JSON (or filter stale records) and add a regression test.
						Status:          sessionStatusLabel(st),
						CaptureDegraded: st.CaptureDegradedAt != nil,

cmd/entire/cli/status.go:978

  • Branch is newly emitted for every JSON session, including the resolveWorktreeBranch fallback, but the added tests only assert session IDs and worktree paths. A regression could therefore return an empty or wrong branch without failing; please assert both persisted branches and the fallback case.
					branch := st.Branch
					if branch == "" && st.WorktreePath != "" {
						branch = resolveWorktreeBranch(ctx, st.WorktreePath)
					}
					result.ActiveSessions = append(result.ActiveSessions, sessionBriefJSON{

docs/architecture/sessions-and-checkpoints.md:358

  • This documentation change leaves the authoritative comments in cmd/entire/cli/session_finalize.go:22-23 and :47-48 saying that entire status invokes the exited-session sweep. Status no longer calls finalizeExitedSessions, so those comments now describe a nonexistent caller and contradict this update; please revise them together with the architecture docs.
sweep (`finalizeExitedSessions`, run inside `entire doctor` and the session sweeper — not `entire status`, which is read-only), which
  • Files reviewed: 7/7 changed files
  • Comments generated: 0
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

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.

entire status collapses multiple sessions per agent, and mutates session state while reporting it

2 participants