From 7b88390269291ac1c59770993a00d1b417044124 Mon Sep 17 00:00:00 2001 From: Sven Pfleiderer Date: Thu, 18 Jun 2026 16:27:42 -0700 Subject: [PATCH 01/11] checkpoint: define split store interfaces Replace the combined checkpoint Store interface with committed, temporary, and optional author capabilities. This keeps the current GitStore implementation while making the committed storage surface independent from shadow-branch operations. Entire-Checkpoint: b6e36ed89559 --- cmd/entire/cli/checkpoint/checkpoint.go | 56 +++---------------- .../checkpoint/committed_reader_resolve.go | 20 +++++++ cmd/entire/cli/checkpoint/store.go | 7 ++- 3 files changed, 33 insertions(+), 50 deletions(-) diff --git a/cmd/entire/cli/checkpoint/checkpoint.go b/cmd/entire/cli/checkpoint/checkpoint.go index 2fb564135c..802ba5ed11 100644 --- a/cmd/entire/cli/checkpoint/checkpoint.go +++ b/cmd/entire/cli/checkpoint/checkpoint.go @@ -60,56 +60,16 @@ const ( Committed ) -// Store provides low-level primitives for reading and writing checkpoints. -// This is used by strategies to implement their storage approach. -// -// The interface matches the GitStore implementation signatures directly: -// - WriteTemporary takes WriteTemporaryOptions and returns a result with commit hash and skip status -// - ReadTemporary takes baseCommit (not sessionID) since shadow branches are keyed by commit -// - List methods return implementation-specific info types for richer data -type Store interface { - // WriteTemporary writes a temporary checkpoint (full state) to a shadow branch. - // Shadow branches are named entire/. - // Returns a result containing the commit hash and whether the checkpoint was skipped. - // Checkpoints are skipped (deduplicated) when the tree hash matches the previous checkpoint. +// TemporaryStore provides the production shadow-branch checkpoint surface. +type TemporaryStore interface { WriteTemporary(ctx context.Context, opts WriteTemporaryOptions) (WriteTemporaryResult, error) - - // ReadTemporary reads the latest checkpoint from a shadow branch. - // baseCommit is the commit hash the session is based on. - // worktreeID is the internal git worktree identifier (empty for main worktree). - // Returns nil, nil if the shadow branch doesn't exist. - ReadTemporary(ctx context.Context, baseCommit, worktreeID string) (*ReadTemporaryResult, error) - - // ListTemporary lists all shadow branches with their checkpoint info. + WriteTemporaryTask(ctx context.Context, opts WriteTemporaryTaskOptions) (plumbing.Hash, error) ListTemporary(ctx context.Context) ([]TemporaryInfo, error) - - // WriteCommitted writes a committed checkpoint to the entire/checkpoints/v1 branch. - // Checkpoints are stored at sharded paths: // - WriteCommitted(ctx context.Context, opts WriteCommittedOptions) error - - // ReadCommitted reads a committed checkpoint's summary by ID. - // Returns only the CheckpointSummary (paths + aggregated stats), not actual content. - // Use ReadSessionContent to read actual transcript/prompts. - // Returns nil, nil if the checkpoint does not exist. - ReadCommitted(ctx context.Context, checkpointID id.CheckpointID) (*CheckpointSummary, error) - - // ReadSessionContent reads the actual content for a specific session within a checkpoint. - // sessionIndex is 0-based (0 for first session, 1 for second, etc.). - // Returns the session's metadata, transcript, and prompts. - ReadSessionContent(ctx context.Context, checkpointID id.CheckpointID, sessionIndex int) (*SessionContent, error) - - // ReadSessionContentByID reads a session's content by its session ID. - // Useful when you have the session ID but don't know its index within the checkpoint. - ReadSessionContentByID(ctx context.Context, checkpointID id.CheckpointID, sessionID string) (*SessionContent, error) - - // ListCommitted lists all committed checkpoints. - ListCommitted(ctx context.Context) ([]CommittedInfo, error) - - // UpdateCommitted replaces the transcript and prompts for an existing - // committed checkpoint. Used at stop time to finalize checkpoints with the full - // session transcript (prompt to stop event). - // Returns ErrCheckpointNotFound if the checkpoint doesn't exist. - UpdateCommitted(ctx context.Context, opts UpdateCommittedOptions) error + ListTemporaryCheckpoints(ctx context.Context, baseCommit, worktreeID, sessionID string, limit int) ([]TemporaryCheckpointInfo, error) + ListCheckpointsForBranch(ctx context.Context, branchName, sessionID string, limit int) ([]TemporaryCheckpointInfo, error) + ListAllTemporaryCheckpoints(ctx context.Context, sessionID string, limit int) ([]TemporaryCheckpointInfo, error) + GetTranscriptFromCommit(ctx context.Context, commitHash plumbing.Hash, metadataDir string, agentType types.AgentType) ([]byte, error) + ShadowBranchExists(baseCommit, worktreeID string) bool } // WriteTemporaryResult contains the result of writing a temporary checkpoint. diff --git a/cmd/entire/cli/checkpoint/committed_reader_resolve.go b/cmd/entire/cli/checkpoint/committed_reader_resolve.go index 3c1657a4be..ba1099fcb9 100644 --- a/cmd/entire/cli/checkpoint/committed_reader_resolve.go +++ b/cmd/entire/cli/checkpoint/committed_reader_resolve.go @@ -21,6 +21,26 @@ type CommittedListReader interface { ReadSessionPrompts(ctx context.Context, checkpointID id.CheckpointID, sessionIndex int) (string, error) } +// CommittedWriter provides write access to committed checkpoint data. +type CommittedWriter interface { + WriteCommitted(ctx context.Context, opts WriteCommittedOptions) error + UpdateCommitted(ctx context.Context, opts UpdateCommittedOptions) error + UpdateSummary(ctx context.Context, checkpointID id.CheckpointID, summary *Summary) error + UpdateCheckpointSummary(ctx context.Context, checkpointID id.CheckpointID, combinedAttribution *InitialAttribution) error +} + +// CommittedStore provides the production committed checkpoint storage surface. +type CommittedStore interface { + CommittedListReader + ReadSessionMetadataAndPrompts(ctx context.Context, checkpointID id.CheckpointID, sessionIndex int) (*SessionContent, error) + CommittedWriter +} + +// AuthorReader provides optional checkpoint author lookup. +type AuthorReader interface { + GetCheckpointAuthor(ctx context.Context, checkpointID id.CheckpointID) (Author, error) +} + // ReadCommittedCheckpoint reads a committed checkpoint summary and normalizes // a nil store response into ErrCheckpointNotFound. func ReadCommittedCheckpoint(ctx context.Context, reader CommittedReader, checkpointID id.CheckpointID) (*CheckpointSummary, error) { diff --git a/cmd/entire/cli/checkpoint/store.go b/cmd/entire/cli/checkpoint/store.go index 8cab02effc..8022262cf9 100644 --- a/cmd/entire/cli/checkpoint/store.go +++ b/cmd/entire/cli/checkpoint/store.go @@ -7,8 +7,11 @@ import ( "github.com/go-git/go-git/v6/plumbing" ) -// Compile-time check that GitStore implements the Store interface. -var _ Store = (*GitStore)(nil) +var ( + _ CommittedStore = (*GitStore)(nil) + _ TemporaryStore = (*GitStore)(nil) + _ AuthorReader = (*GitStore)(nil) +) // GitStore provides operations for both temporary and committed checkpoint // storage. Writes target refs.Primary; committed reads resolve against From dddd06efb2ef1197162da1065fb51775aa31ebf4 Mon Sep 17 00:00:00 2001 From: Sven Pfleiderer Date: Thu, 18 Jun 2026 16:35:11 -0700 Subject: [PATCH 02/11] checkpoint: route callers through split stores Expose committed and temporary checkpoint storage through separate interfaces from the Stores facade. Call sites now depend on committed readers and writers or temporary shadow-branch storage instead of the concrete GitStore. Entire-Checkpoint: bb51dce048b6 --- cmd/entire/cli/attach.go | 2 +- cmd/entire/cli/attribution.go | 7 ++++++- cmd/entire/cli/checkpoint/committed.go | 4 ++-- cmd/entire/cli/checkpoint/open.go | 19 +++++++++---------- cmd/entire/cli/explain.go | 10 +++++----- cmd/entire/cli/resume.go | 2 +- cmd/entire/cli/strategy/manual_commit.go | 14 +++++++++++--- .../strategy/manual_commit_condensation.go | 2 +- cmd/entire/cli/strategy/manual_commit_git.go | 10 ++++++---- .../cli/strategy/manual_commit_rewind.go | 7 ++++--- cmd/entire/cli/strategy/manual_commit_test.go | 4 +++- 11 files changed, 49 insertions(+), 32 deletions(-) diff --git a/cmd/entire/cli/attach.go b/cmd/entire/cli/attach.go index 5591e96f5f..3f09e4cd54 100644 --- a/cmd/entire/cli/attach.go +++ b/cmd/entire/cli/attach.go @@ -62,7 +62,7 @@ func (opts attachOptions) committedRefs(ctx context.Context) cpkg.CommittedRefs // openAttachStore opens the committed store for the resolved topology. refs is // passed explicitly so attach preserves PrimaryAsRead() pinning. -func openAttachStore(ctx context.Context, repo *git.Repository, refs cpkg.CommittedRefs) (*cpkg.GitStore, error) { +func openAttachStore(ctx context.Context, repo *git.Repository, refs cpkg.CommittedRefs) (cpkg.CommittedStore, error) { //nolint:ireturn // committed store capability preserves attach's read-ref override stores, err := cpkg.Open(ctx, repo, cpkg.OpenOptions{Refs: &refs}) if err != nil { return nil, fmt.Errorf("open checkpoint store: %w", err) diff --git a/cmd/entire/cli/attribution.go b/cmd/entire/cli/attribution.go index f15909c764..a54214fef4 100644 --- a/cmd/entire/cli/attribution.go +++ b/cmd/entire/cli/attribution.go @@ -114,10 +114,15 @@ type attributionSummary struct { MixedPercentage int `json:"mixed_percentage"` } +type attributionSessionMetadataPromptsReader interface { + checkpoint.CommittedReader + ReadSessionMetadataAndPrompts(ctx context.Context, checkpointID id.CheckpointID, sessionIndex int) (*checkpoint.SessionContent, error) +} + type attributionResolver struct { ctx context.Context repo *git.Repository - store *checkpoint.GitStore + store attributionSessionMetadataPromptsReader fetchOnMiss bool commitCache map[string]*object.Commit diff --git a/cmd/entire/cli/checkpoint/committed.go b/cmd/entire/cli/checkpoint/committed.go index 56069d95dc..74afc563bc 100644 --- a/cmd/entire/cli/checkpoint/committed.go +++ b/cmd/entire/cli/checkpoint/committed.go @@ -1327,7 +1327,7 @@ func (s *GitStore) GetSessionLog(ctx context.Context, cpID id.CheckpointID) ([]b // LookupSessionLog is a convenience function that opens the repository and retrieves // a session log by checkpoint ID. This is the primary entry point for callers that -// don't already have a GitStore instance. +// do not already have a committed store instance. // Returns ErrCheckpointNotFound if the checkpoint doesn't exist. // Returns ErrNoTranscript if the checkpoint exists but has no transcript. func LookupSessionLog(ctx context.Context, cpID id.CheckpointID) ([]byte, string, error) { @@ -1340,7 +1340,7 @@ func LookupSessionLog(ctx context.Context, cpID id.CheckpointID) ([]byte, string if err != nil { return nil, "", fmt.Errorf("open checkpoint store: %w", err) } - return stores.Primary.GetSessionLog(ctx, cpID) + return ReadRawSessionLogForCheckpoint(ctx, stores.Primary, cpID) } // UpdateSummary updates the summary field in the latest session's metadata. diff --git a/cmd/entire/cli/checkpoint/open.go b/cmd/entire/cli/checkpoint/open.go index 2c9c2262fd..b9583adbff 100644 --- a/cmd/entire/cli/checkpoint/open.go +++ b/cmd/entire/cli/checkpoint/open.go @@ -23,11 +23,11 @@ type OpenOptions struct { // Stores is the facade returned by Open: the committed store plus the git-only // temporary capability and resolved committed-ref topology. type Stores struct { - // Primary is the committed store — the source of truth that serves all - // committed reads and writes. - Primary *GitStore + // Primary is the committed store that serves committed reads and writes. + Primary CommittedStore - refs CommittedRefs + temporary TemporaryStore + refs CommittedRefs } // Open resolves the checkpoint storage topology and constructs the backing @@ -41,8 +41,9 @@ func Open(ctx context.Context, repo *git.Repository, opts OpenOptions) (*Stores, store.SetBlobFetcher(opts.BlobFetcher) } return &Stores{ - Primary: store, - refs: refs, + Primary: store, + temporary: store, + refs: refs, }, nil } @@ -53,10 +54,8 @@ func resolveOpenRefs(ctx context.Context, opts OpenOptions) CommittedRefs { return ResolveCommittedRefs(ctx) } -// Temporary returns the git-backed temporary (shadow-branch) store. It is the -// same backing store as Primary; the name marks shadow-branch intent at the -// call site. -func (s *Stores) Temporary() *GitStore { return s.Primary } +// Temporary returns the git-backed temporary shadow-branch store. +func (s *Stores) Temporary() TemporaryStore { return s.temporary } //nolint:ireturn // temporary store capability is the abstraction boundary // Refs returns the resolved committed-ref topology. func (s *Stores) Refs() CommittedRefs { return s.refs } diff --git a/cmd/entire/cli/explain.go b/cmd/entire/cli/explain.go index e7f5805c69..28de77ced9 100644 --- a/cmd/entire/cli/explain.go +++ b/cmd/entire/cli/explain.go @@ -89,7 +89,7 @@ var errCannotGenerateTemporaryCheckpoint = errors.New("cannot generate summary f type explainCheckpointLookup struct { repo *git.Repository - store *checkpoint.GitStore + store checkpoint.CommittedStore committed []checkpoint.CommittedInfo } @@ -729,8 +729,8 @@ func runExplainCheckpointWithLookup(ctx context.Context, w, errW io.Writer, chec Name: associatedCommits[0].Author, Email: associatedCommits[0].Email, } - } else { - author, _ = lookup.store.GetCheckpointAuthor(ctx, fullCheckpointID) //nolint:errcheck // Author is optional + } else if authorReader, ok := lookup.store.(checkpoint.AuthorReader); ok { + author, _ = authorReader.GetCheckpointAuthor(ctx, fullCheckpointID) //nolint:errcheck // Author is optional } // Format and output. Stop spinner BEFORE any write to w to keep stderr @@ -1204,7 +1204,7 @@ func formatSummaryTimeout(d time.Duration) string { // Searches ALL shadow branches, not just the one for current HEAD, to find checkpoints // created from different base commits (e.g., if HEAD advanced since session start). // The writer w is used for raw transcript output to bypass the pager. -func explainTemporaryCheckpoint(ctx context.Context, w, errW io.Writer, repo *git.Repository, store *checkpoint.GitStore, shaPrefix string, verbose, full, rawTranscript bool) (string, bool, error) { +func explainTemporaryCheckpoint(ctx context.Context, w, errW io.Writer, repo *git.Repository, store checkpoint.TemporaryStore, shaPrefix string, verbose, full, rawTranscript bool) (string, bool, error) { // List temporary checkpoints from ALL shadow branches // This ensures we find checkpoints even if HEAD has advanced since the session started tempCheckpoints, err := store.ListAllTemporaryCheckpoints(ctx, "", branchCheckpointsLimit) @@ -2183,7 +2183,7 @@ func readLatestCommittedSessionPrompt(ctx context.Context, store checkpoint.Comm // whose base commit is reachable from the given HEAD hash and that belong to this worktree. // For default branches, all shadow branches for this worktree are included. // For feature branches, only shadow branches whose base commit is in HEAD's history are included. -func getReachableTemporaryCheckpoints(ctx context.Context, repo *git.Repository, store *checkpoint.GitStore, headHash plumbing.Hash, isOnDefault bool, limit int) []strategy.RewindPoint { +func getReachableTemporaryCheckpoints(ctx context.Context, repo *git.Repository, store checkpoint.TemporaryStore, headHash plumbing.Hash, isOnDefault bool, limit int) []strategy.RewindPoint { var points []strategy.RewindPoint // Compute current worktree's hash for filtering shadow branches diff --git a/cmd/entire/cli/resume.go b/cmd/entire/cli/resume.go index 4f6fa908df..65a44b1fab 100644 --- a/cmd/entire/cli/resume.go +++ b/cmd/entire/cli/resume.go @@ -329,7 +329,7 @@ func resumeFromCurrentBranch(ctx context.Context, w, errW io.Writer, branchName // resolveLatestCheckpoint reads metadata for each checkpoint ID and returns // the checkpoint with the latest CreatedAt. -func resolveLatestCheckpoint(ctx context.Context, store *checkpoint.GitStore, checkpointIDs []id.CheckpointID) (*strategy.CheckpointInfo, error) { +func resolveLatestCheckpoint(ctx context.Context, store checkpoint.CommittedListReader, checkpointIDs []id.CheckpointID) (*strategy.CheckpointInfo, error) { infoMap := make(map[id.CheckpointID]strategy.CheckpointInfo, len(checkpointIDs)) for _, cpID := range checkpointIDs { metadata, readErr := readCheckpointInfoFromStore(ctx, store, cpID) diff --git a/cmd/entire/cli/strategy/manual_commit.go b/cmd/entire/cli/strategy/manual_commit.go index d1ee1493d5..4484fc6120 100644 --- a/cmd/entire/cli/strategy/manual_commit.go +++ b/cmd/entire/cli/strategy/manual_commit.go @@ -40,14 +40,22 @@ func (s *ManualCommitStrategy) getStateStore(_ context.Context) (*session.StateS return s.stateStore, s.stateStoreErr } +func (s *ManualCommitStrategy) getCheckpointStores(ctx context.Context, repo *git.Repository) (*checkpoint.Stores, error) { + stores, err := checkpoint.Open(ctx, repo, checkpoint.OpenOptions{BlobFetcher: s.blobFetcher}) + if err != nil { + return nil, fmt.Errorf("open checkpoint store: %w", err) + } + return stores, nil +} + // getCheckpointStore returns a store bound to the resolved committed-metadata // topology. Writes target refs.Primary; reads target refs.Read. The strategy's // blob fetcher is wired in so reads can fetch blobs on demand after a treeless // fetch. -func (s *ManualCommitStrategy) getCheckpointStore(ctx context.Context, repo *git.Repository) (*checkpoint.GitStore, error) { - stores, err := checkpoint.Open(ctx, repo, checkpoint.OpenOptions{BlobFetcher: s.blobFetcher}) +func (s *ManualCommitStrategy) getCheckpointStore(ctx context.Context, repo *git.Repository) (checkpoint.CommittedStore, error) { //nolint:ireturn // committed store capability is the abstraction boundary + stores, err := s.getCheckpointStores(ctx, repo) if err != nil { - return nil, fmt.Errorf("open checkpoint store: %w", err) + return nil, err } return stores.Primary, nil } diff --git a/cmd/entire/cli/strategy/manual_commit_condensation.go b/cmd/entire/cli/strategy/manual_commit_condensation.go index 6c3f569b16..434ad011a3 100644 --- a/cmd/entire/cli/strategy/manual_commit_condensation.go +++ b/cmd/entire/cli/strategy/manual_commit_condensation.go @@ -135,7 +135,7 @@ func checkpointStepCount(s *SessionState) int { // CondenseSession condenses a session's shadow branch to permanent storage. // checkpointID is the 12-hex-char value from the Entire-Checkpoint trailer. // Metadata is stored at sharded path: // -// Uses checkpoint.GitStore.WriteCommitted for the git operations. +// Uses checkpoint.CommittedStore.WriteCommitted for committed storage. // // For mid-session commits (no Stop/SaveStep called yet), the shadow branch may not exist. // In this case, data is extracted from the live transcript instead. diff --git a/cmd/entire/cli/strategy/manual_commit_git.go b/cmd/entire/cli/strategy/manual_commit_git.go index a459f3025f..587d24972b 100644 --- a/cmd/entire/cli/strategy/manual_commit_git.go +++ b/cmd/entire/cli/strategy/manual_commit_git.go @@ -21,7 +21,7 @@ import ( ) // SaveStep saves a checkpoint to the shadow branch. -// Uses checkpoint.GitStore.WriteTemporary for git operations. +// Uses checkpoint.TemporaryStore.WriteTemporary for git operations. func (s *ManualCommitStrategy) SaveStep(ctx context.Context, step StepContext) error { _, openRepoSpan := perf.Start(ctx, "open_repository") repo, err := OpenRepository(ctx) @@ -52,10 +52,11 @@ func (s *ManualCommitStrategy) SaveStep(ctx context.Context, step StepContext) e } migrateSpan.End() - store, err := s.getCheckpointStore(ctx, repo) + stores, err := s.getCheckpointStores(ctx, repo) if err != nil { return err } + store := stores.Temporary() shadowBranchName := checkpoint.ShadowBranchNameForCommit(state.BaseCommit, state.WorktreeID) branchExisted := store.ShadowBranchExists(state.BaseCommit, state.WorktreeID) @@ -168,7 +169,7 @@ func (s *ManualCommitStrategy) ensureSessionInitialized(ctx context.Context, rep } // SaveTaskStep saves a task step checkpoint to the shadow branch. -// Uses checkpoint.GitStore.WriteTemporaryTask for git operations. +// Uses checkpoint.TemporaryStore.WriteTemporaryTask for git operations. func (s *ManualCommitStrategy) SaveTaskStep(ctx context.Context, step TaskStepContext) error { repo, err := OpenRepository(ctx) if err != nil { @@ -185,10 +186,11 @@ func (s *ManualCommitStrategy) SaveTaskStep(ctx context.Context, step TaskStepCo return fmt.Errorf("failed to check/migrate shadow branch: %w", err) } - store, err := s.getCheckpointStore(ctx, repo) + stores, err := s.getCheckpointStores(ctx, repo) if err != nil { return err } + store := stores.Temporary() shadowBranchName := checkpoint.ShadowBranchNameForCommit(state.BaseCommit, state.WorktreeID) branchExisted := store.ShadowBranchExists(state.BaseCommit, state.WorktreeID) diff --git a/cmd/entire/cli/strategy/manual_commit_rewind.go b/cmd/entire/cli/strategy/manual_commit_rewind.go index b13715e4cf..36a60550f5 100644 --- a/cmd/entire/cli/strategy/manual_commit_rewind.go +++ b/cmd/entire/cli/strategy/manual_commit_rewind.go @@ -30,7 +30,7 @@ import ( ) // GetRewindPoints returns available rewind points. -// Uses checkpoint.GitStore.ListTemporaryCheckpoints for reading from shadow branches. +// Uses checkpoint.TemporaryStore for reading from shadow branches. func (s *ManualCommitStrategy) GetRewindPoints(ctx context.Context, limit int) ([]RewindPoint, error) { repo, err := OpenRepository(ctx) if err != nil { @@ -38,10 +38,11 @@ func (s *ManualCommitStrategy) GetRewindPoints(ctx context.Context, limit int) ( } defer repo.Close() - store, err := s.getCheckpointStore(ctx, repo) + stores, err := s.getCheckpointStores(ctx, repo) if err != nil { return nil, err } + store := stores.Temporary() // Get current HEAD to find matching shadow branch head, err := repo.Head() @@ -58,7 +59,7 @@ func (s *ManualCommitStrategy) GetRewindPoints(ctx context.Context, limit int) ( var allPoints []RewindPoint - // Collect checkpoint points from active sessions using checkpoint.GitStore + // Collect checkpoint points from active sessions using temporary storage. // Cache session prompts by session ID to avoid re-reading the same prompt file sessionPrompts := make(map[string]string) diff --git a/cmd/entire/cli/strategy/manual_commit_test.go b/cmd/entire/cli/strategy/manual_commit_test.go index dc40bb8d38..70eadf3a2c 100644 --- a/cmd/entire/cli/strategy/manual_commit_test.go +++ b/cmd/entire/cli/strategy/manual_commit_test.go @@ -4210,7 +4210,9 @@ func TestCondenseSession_RedactionFailure_DropsTranscriptButWritesMetadata(t *te } require.True(t, found, "checkpoint metadata should be written even when transcript redaction fails") - _, err = store.ReadLatestSessionContent(context.Background(), checkpointID) + summary, err := checkpoint.ReadCommittedCheckpoint(context.Background(), store, checkpointID) + require.NoError(t, err) + _, err = checkpoint.ReadLatestSessionContent(context.Background(), store, checkpointID, summary) require.ErrorIs(t, err, checkpoint.ErrNoTranscript, "transcript should be dropped when redaction fails") } From 55032715f69ef52c7587a42315c96bf5da5c49e0 Mon Sep 17 00:00:00 2001 From: Sven Pfleiderer Date: Thu, 18 Jun 2026 16:51:10 -0700 Subject: [PATCH 03/11] Add getTemporaryStore helper to dedup store access SaveStep, SaveTaskStep, and GetRewindPoints each repeated the getCheckpointStores + Temporary() pair. A getTemporaryStore accessor mirrors getCheckpointStore so each call site is a single line, with getCheckpointStores remaining the shared open-with-blob-fetcher wrapper. Entire-Checkpoint: 684fdbdfe046 --- cmd/entire/cli/strategy/manual_commit.go | 10 ++++++++++ cmd/entire/cli/strategy/manual_commit_git.go | 6 ++---- cmd/entire/cli/strategy/manual_commit_rewind.go | 3 +-- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/cmd/entire/cli/strategy/manual_commit.go b/cmd/entire/cli/strategy/manual_commit.go index 4484fc6120..4f9a877011 100644 --- a/cmd/entire/cli/strategy/manual_commit.go +++ b/cmd/entire/cli/strategy/manual_commit.go @@ -60,6 +60,16 @@ func (s *ManualCommitStrategy) getCheckpointStore(ctx context.Context, repo *git return stores.Primary, nil } +// getTemporaryStore returns the git-backed shadow-branch store with the +// strategy's blob fetcher wired in. +func (s *ManualCommitStrategy) getTemporaryStore(ctx context.Context, repo *git.Repository) (checkpoint.TemporaryStore, error) { //nolint:ireturn // temporary store capability is the abstraction boundary + stores, err := s.getCheckpointStores(ctx, repo) + if err != nil { + return nil, err + } + return stores.Temporary(), nil +} + // NewManualCommitStrategy creates a new manual-commit strategy instance. func NewManualCommitStrategy() *ManualCommitStrategy { return &ManualCommitStrategy{} diff --git a/cmd/entire/cli/strategy/manual_commit_git.go b/cmd/entire/cli/strategy/manual_commit_git.go index 587d24972b..1a74eb9d84 100644 --- a/cmd/entire/cli/strategy/manual_commit_git.go +++ b/cmd/entire/cli/strategy/manual_commit_git.go @@ -52,11 +52,10 @@ func (s *ManualCommitStrategy) SaveStep(ctx context.Context, step StepContext) e } migrateSpan.End() - stores, err := s.getCheckpointStores(ctx, repo) + store, err := s.getTemporaryStore(ctx, repo) if err != nil { return err } - store := stores.Temporary() shadowBranchName := checkpoint.ShadowBranchNameForCommit(state.BaseCommit, state.WorktreeID) branchExisted := store.ShadowBranchExists(state.BaseCommit, state.WorktreeID) @@ -186,11 +185,10 @@ func (s *ManualCommitStrategy) SaveTaskStep(ctx context.Context, step TaskStepCo return fmt.Errorf("failed to check/migrate shadow branch: %w", err) } - stores, err := s.getCheckpointStores(ctx, repo) + store, err := s.getTemporaryStore(ctx, repo) if err != nil { return err } - store := stores.Temporary() shadowBranchName := checkpoint.ShadowBranchNameForCommit(state.BaseCommit, state.WorktreeID) branchExisted := store.ShadowBranchExists(state.BaseCommit, state.WorktreeID) diff --git a/cmd/entire/cli/strategy/manual_commit_rewind.go b/cmd/entire/cli/strategy/manual_commit_rewind.go index 36a60550f5..4e630063b5 100644 --- a/cmd/entire/cli/strategy/manual_commit_rewind.go +++ b/cmd/entire/cli/strategy/manual_commit_rewind.go @@ -38,11 +38,10 @@ func (s *ManualCommitStrategy) GetRewindPoints(ctx context.Context, limit int) ( } defer repo.Close() - stores, err := s.getCheckpointStores(ctx, repo) + store, err := s.getTemporaryStore(ctx, repo) if err != nil { return nil, err } - store := stores.Temporary() // Get current HEAD to find matching shadow branch head, err := repo.Head() From c17199d68cdf9d1ba7a86281fb98e42919f943ce Mon Sep 17 00:00:00 2001 From: Sven Pfleiderer Date: Thu, 18 Jun 2026 17:18:58 -0700 Subject: [PATCH 04/11] fix checkpoint reader edge cases Keep empty-session checkpoints distinct from missing checkpoints when reading latest session content. Also narrow resume checkpoint metadata resolution to the methods it actually uses so the split store interfaces stay precise. Entire-Checkpoint: 8e67ab6c5252 --- .../checkpoint/committed_reader_resolve.go | 5 +- .../committed_reader_resolve_test.go | 13 +++++ cmd/entire/cli/resume.go | 9 +++- cmd/entire/cli/resume_test.go | 52 +++++++++++++++++++ 4 files changed, 76 insertions(+), 3 deletions(-) diff --git a/cmd/entire/cli/checkpoint/committed_reader_resolve.go b/cmd/entire/cli/checkpoint/committed_reader_resolve.go index ba1099fcb9..d3fffde23f 100644 --- a/cmd/entire/cli/checkpoint/committed_reader_resolve.go +++ b/cmd/entire/cli/checkpoint/committed_reader_resolve.go @@ -61,9 +61,12 @@ func ReadCommittedCheckpoint(ctx context.Context, reader CommittedReader, checkp // ReadLatestSessionContent reads the latest session from an already-resolved // committed reader and summary. func ReadLatestSessionContent(ctx context.Context, reader CommittedReader, checkpointID id.CheckpointID, summary *CheckpointSummary) (*SessionContent, error) { - if summary == nil || len(summary.Sessions) == 0 { + if summary == nil { return nil, ErrCheckpointNotFound } + if len(summary.Sessions) == 0 { + return nil, fmt.Errorf("checkpoint has no sessions: %s", checkpointID) + } latestIndex := len(summary.Sessions) - 1 content, err := reader.ReadSessionContent(ctx, checkpointID, latestIndex) if err != nil { diff --git a/cmd/entire/cli/checkpoint/committed_reader_resolve_test.go b/cmd/entire/cli/checkpoint/committed_reader_resolve_test.go index 175ded068c..fa91c63eb7 100644 --- a/cmd/entire/cli/checkpoint/committed_reader_resolve_test.go +++ b/cmd/entire/cli/checkpoint/committed_reader_resolve_test.go @@ -32,6 +32,19 @@ func TestReadCommittedCheckpointWrapsReaderError(t *testing.T) { require.ErrorContains(t, err, "read committed checkpoint") } +func TestReadLatestSessionContentEmptySummaryReturnsDistinctError(t *testing.T) { + t.Parallel() + + cpID := id.MustCheckpointID("111111111111") + summary := &CheckpointSummary{} + reader := &committedReaderStub{summary: summary} + + content, err := ReadLatestSessionContent(context.Background(), reader, cpID, summary) + require.Nil(t, content) + require.ErrorContains(t, err, "checkpoint has no sessions") + require.NotErrorIs(t, err, ErrCheckpointNotFound) +} + func TestReadRawSessionLogForCheckpointReadsLatestV1Session(t *testing.T) { t.Parallel() diff --git a/cmd/entire/cli/resume.go b/cmd/entire/cli/resume.go index 65a44b1fab..06e2fff65f 100644 --- a/cmd/entire/cli/resume.go +++ b/cmd/entire/cli/resume.go @@ -329,7 +329,7 @@ func resumeFromCurrentBranch(ctx context.Context, w, errW io.Writer, branchName // resolveLatestCheckpoint reads metadata for each checkpoint ID and returns // the checkpoint with the latest CreatedAt. -func resolveLatestCheckpoint(ctx context.Context, store checkpoint.CommittedListReader, checkpointIDs []id.CheckpointID) (*strategy.CheckpointInfo, error) { +func resolveLatestCheckpoint(ctx context.Context, store checkpointInfoReader, checkpointIDs []id.CheckpointID) (*strategy.CheckpointInfo, error) { infoMap := make(map[id.CheckpointID]strategy.CheckpointInfo, len(checkpointIDs)) for _, cpID := range checkpointIDs { metadata, readErr := readCheckpointInfoFromStore(ctx, store, cpID) @@ -349,7 +349,12 @@ func resolveLatestCheckpoint(ctx context.Context, store checkpoint.CommittedList return &latest, nil } -func readCheckpointInfoFromStore(ctx context.Context, store checkpoint.CommittedListReader, checkpointID id.CheckpointID) (*strategy.CheckpointInfo, error) { +type checkpointInfoReader interface { + checkpoint.CommittedReader + ReadSessionMetadata(ctx context.Context, checkpointID id.CheckpointID, sessionIndex int) (*checkpoint.CommittedMetadata, error) +} + +func readCheckpointInfoFromStore(ctx context.Context, store checkpointInfoReader, checkpointID id.CheckpointID) (*strategy.CheckpointInfo, error) { summary, err := checkpoint.ReadCommittedCheckpoint(ctx, store, checkpointID) if err != nil { return nil, fmt.Errorf("read checkpoint: %w", err) diff --git a/cmd/entire/cli/resume_test.go b/cmd/entire/cli/resume_test.go index a425a139d9..8b3822e8b9 100644 --- a/cmd/entire/cli/resume_test.go +++ b/cmd/entire/cli/resume_test.go @@ -592,6 +592,58 @@ func TestResolveLatestCheckpoint(t *testing.T) { } } +func TestResolveLatestCheckpointUsesCheckpointInfoReader(t *testing.T) { + t.Parallel() + + oldID := id.MustCheckpointID("aaa111bbb222") + newID := id.MustCheckpointID("ccc333ddd444") + reader := &resumeCheckpointInfoReaderStub{ + summaries: map[id.CheckpointID]*checkpoint.CheckpointSummary{ + oldID: {Sessions: []checkpoint.SessionFilePaths{{Metadata: "old"}}}, + newID: {Sessions: []checkpoint.SessionFilePaths{{Metadata: "new"}}}, + }, + metadata: map[id.CheckpointID][]checkpoint.CommittedMetadata{ + oldID: {{ + SessionID: "old-session", + CreatedAt: time.Date(2025, 1, 1, 10, 0, 0, 0, time.UTC), + }}, + newID: {{ + SessionID: "new-session", + CreatedAt: time.Date(2025, 1, 1, 11, 0, 0, 0, time.UTC), + }}, + }, + } + + latest, err := resolveLatestCheckpoint(context.Background(), reader, []id.CheckpointID{oldID, newID}) + if err != nil { + t.Fatalf("resolveLatestCheckpoint() error = %v", err) + } + if latest.CheckpointID != newID { + t.Errorf("resolveLatestCheckpoint() = %s, want %s", latest.CheckpointID, newID) + } +} + +type resumeCheckpointInfoReaderStub struct { + summaries map[id.CheckpointID]*checkpoint.CheckpointSummary + metadata map[id.CheckpointID][]checkpoint.CommittedMetadata +} + +func (r *resumeCheckpointInfoReaderStub) ReadCommitted(_ context.Context, checkpointID id.CheckpointID) (*checkpoint.CheckpointSummary, error) { + return r.summaries[checkpointID], nil +} + +func (r *resumeCheckpointInfoReaderStub) ReadSessionContent(_ context.Context, _ id.CheckpointID, _ int) (*checkpoint.SessionContent, error) { + return nil, checkpoint.ErrCheckpointNotFound +} + +func (r *resumeCheckpointInfoReaderStub) ReadSessionMetadata(_ context.Context, checkpointID id.CheckpointID, sessionIndex int) (*checkpoint.CommittedMetadata, error) { + sessions := r.metadata[checkpointID] + if sessionIndex < 0 || sessionIndex >= len(sessions) { + return nil, checkpoint.ErrCheckpointNotFound + } + return &sessions[sessionIndex], nil +} + func TestReadCheckpointInfoFromStoreUsesLatestSessionMetadata(t *testing.T) { tmpDir := t.TempDir() t.Chdir(tmpDir) From 10faadbe19e8ebf7102601102db632a54fbc5278 Mon Sep 17 00:00:00 2001 From: Sven Pfleiderer Date: Thu, 18 Jun 2026 17:25:30 -0700 Subject: [PATCH 05/11] test redaction failure via git store Exercise the concrete GitStore latest-session reader in the redaction failure regression test. This keeps the test checking that dropped transcripts surface ErrNoTranscript after condensation still writes checkpoint metadata. Entire-Checkpoint: 02eff406bed3 --- cmd/entire/cli/strategy/manual_commit_test.go | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/cmd/entire/cli/strategy/manual_commit_test.go b/cmd/entire/cli/strategy/manual_commit_test.go index 70eadf3a2c..b4fdf00d29 100644 --- a/cmd/entire/cli/strategy/manual_commit_test.go +++ b/cmd/entire/cli/strategy/manual_commit_test.go @@ -4194,8 +4194,7 @@ func TestCondenseSession_RedactionFailure_DropsTranscriptButWritesMetadata(t *te require.NoError(t, err, "redaction failure should not abort condensation") require.NotNil(t, result) - store, err := s.getCheckpointStore(context.Background(), repo) - require.NoError(t, err) + store := checkpoint.NewGitStore(repo, checkpoint.DefaultV1Refs()) committed, err := store.ListCommitted(context.Background()) require.NoError(t, err) @@ -4210,9 +4209,7 @@ func TestCondenseSession_RedactionFailure_DropsTranscriptButWritesMetadata(t *te } require.True(t, found, "checkpoint metadata should be written even when transcript redaction fails") - summary, err := checkpoint.ReadCommittedCheckpoint(context.Background(), store, checkpointID) - require.NoError(t, err) - _, err = checkpoint.ReadLatestSessionContent(context.Background(), store, checkpointID, summary) + _, err = store.ReadLatestSessionContent(context.Background(), checkpointID) require.ErrorIs(t, err, checkpoint.ErrNoTranscript, "transcript should be dropped when redaction fails") } From 772908f85caab625632f152ec2e08810be273ada Mon Sep 17 00:00:00 2001 From: Sven Pfleiderer Date: Thu, 18 Jun 2026 17:32:50 -0700 Subject: [PATCH 06/11] narrow attribution checkpoint reader Keep attribution resolution dependent only on the committed summary and session metadata methods it uses. Add a focused test with a reader that omits unused session-content methods so the local interface stays precise. Entire-Checkpoint: 136650505f9c --- cmd/entire/cli/attribution.go | 22 ++++++++++++--- cmd/entire/cli/attribution_test.go | 45 ++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 4 deletions(-) diff --git a/cmd/entire/cli/attribution.go b/cmd/entire/cli/attribution.go index a54214fef4..055e10ec1f 100644 --- a/cmd/entire/cli/attribution.go +++ b/cmd/entire/cli/attribution.go @@ -114,15 +114,15 @@ type attributionSummary struct { MixedPercentage int `json:"mixed_percentage"` } -type attributionSessionMetadataPromptsReader interface { - checkpoint.CommittedReader +type attributionCheckpointReader interface { + ReadCommitted(ctx context.Context, checkpointID id.CheckpointID) (*checkpoint.CheckpointSummary, error) ReadSessionMetadataAndPrompts(ctx context.Context, checkpointID id.CheckpointID, sessionIndex int) (*checkpoint.SessionContent, error) } type attributionResolver struct { ctx context.Context repo *git.Repository - store attributionSessionMetadataPromptsReader + store attributionCheckpointReader fetchOnMiss bool commitCache map[string]*object.Commit @@ -411,7 +411,7 @@ func (r *attributionResolver) checkpointContext(cpID id.CheckpointID, file strin func (r *attributionResolver) readCheckpointContext(cpID id.CheckpointID, file string) attributionCheckpointContext { ctx := attributionCheckpointContext{CheckpointID: cpID.String()} - summary, err := checkpoint.ReadCommittedCheckpoint(r.ctx, r.store, cpID) + summary, err := readAttributionCheckpointSummary(r.ctx, r.store, cpID) if err != nil && r.fetchOnMiss { if fetched, fetchErr := r.fetchCheckpointContext(cpID, file); fetchErr == nil { return fetched @@ -489,6 +489,20 @@ func (r *attributionResolver) readCheckpointContext(cpID id.CheckpointID, file s return ctx } +func readAttributionCheckpointSummary(ctx context.Context, reader attributionCheckpointReader, cpID id.CheckpointID) (*checkpoint.CheckpointSummary, error) { + if err := ctx.Err(); err != nil { + return nil, err //nolint:wrapcheck // Propagating context cancellation + } + summary, err := reader.ReadCommitted(ctx, cpID) + if err != nil { + return nil, fmt.Errorf("read committed checkpoint: %w", err) + } + if summary == nil { + return nil, checkpoint.ErrCheckpointNotFound + } + return summary, nil +} + func enrichAttributionLineWithFetch(ctx context.Context, file string, line *attributionLine, checkpoints map[string]attributionCheckpointContext) error { if line == nil || len(line.Candidates) == 0 { return nil diff --git a/cmd/entire/cli/attribution_test.go b/cmd/entire/cli/attribution_test.go index ad8a9ce548..384a765830 100644 --- a/cmd/entire/cli/attribution_test.go +++ b/cmd/entire/cli/attribution_test.go @@ -353,6 +353,51 @@ func TestAttributionBlameMixedUsesFileMatchingCheckpoint(t *testing.T) { require.Equal(t, 1, payload.Summary.AILines) } +func TestAttributionResolverUsesCheckpointReader(t *testing.T) { + t.Parallel() + + cpID := checkpointid.MustCheckpointID("d9b2c3d4e5f6") + reader := &attributionCheckpointReaderStub{ + summary: &checkpoint.CheckpointSummary{ + FilesTouched: []string{"auth.py"}, + Sessions: []checkpoint.SessionFilePaths{{Metadata: "metadata.json"}}, + }, + content: &checkpoint.SessionContent{ + Metadata: checkpoint.CommittedMetadata{ + SessionID: "session-ai", + FilesTouched: []string{"auth.py"}, + Agent: agent.AgentTypeClaudeCode, + Model: "claude-test", + }, + Prompts: "Explain the authentication change.", + }, + } + resolver := &attributionResolver{ + ctx: context.Background(), + store: reader, + checkpointCache: make(map[string]attributionCheckpointContext), + } + + ctx := resolver.readCheckpointContext(cpID, "auth.py") + require.Equal(t, "session-ai", ctx.SessionID) + require.Equal(t, "Claude Code", ctx.Agent) + require.Equal(t, "claude-test", ctx.Model) + require.Equal(t, "Explain the authentication change.", ctx.Prompt) +} + +type attributionCheckpointReaderStub struct { + summary *checkpoint.CheckpointSummary + content *checkpoint.SessionContent +} + +func (s *attributionCheckpointReaderStub) ReadCommitted(context.Context, checkpointid.CheckpointID) (*checkpoint.CheckpointSummary, error) { + return s.summary, nil +} + +func (s *attributionCheckpointReaderStub) ReadSessionMetadataAndPrompts(context.Context, checkpointid.CheckpointID, int) (*checkpoint.SessionContent, error) { + return s.content, nil +} + func TestAttributionBlameScopesMixedToSessionNotCheckpoint(t *testing.T) { repoRoot := newAttributionRepo(t) writeAttributionCheckpoint(t, repoRoot, "a9b2c3d4e5f6", checkpoint.WriteCommittedOptions{ From f39e8db21ddd2ec8190d2ea30baddcdc38f59db1 Mon Sep 17 00:00:00 2001 From: Sven Pfleiderer Date: Thu, 18 Jun 2026 17:58:13 -0700 Subject: [PATCH 07/11] preserve empty summary lookup behavior Return ErrCheckpointNotFound for empty committed summaries in the split reader helper. This keeps the interface split behavior-neutral and updates the regression test to cover the pre-split contract. Entire-Checkpoint: b7a703224f22 --- cmd/entire/cli/checkpoint/committed_reader_resolve.go | 5 +---- cmd/entire/cli/checkpoint/committed_reader_resolve_test.go | 5 ++--- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/cmd/entire/cli/checkpoint/committed_reader_resolve.go b/cmd/entire/cli/checkpoint/committed_reader_resolve.go index d3fffde23f..ba1099fcb9 100644 --- a/cmd/entire/cli/checkpoint/committed_reader_resolve.go +++ b/cmd/entire/cli/checkpoint/committed_reader_resolve.go @@ -61,12 +61,9 @@ func ReadCommittedCheckpoint(ctx context.Context, reader CommittedReader, checkp // ReadLatestSessionContent reads the latest session from an already-resolved // committed reader and summary. func ReadLatestSessionContent(ctx context.Context, reader CommittedReader, checkpointID id.CheckpointID, summary *CheckpointSummary) (*SessionContent, error) { - if summary == nil { + if summary == nil || len(summary.Sessions) == 0 { return nil, ErrCheckpointNotFound } - if len(summary.Sessions) == 0 { - return nil, fmt.Errorf("checkpoint has no sessions: %s", checkpointID) - } latestIndex := len(summary.Sessions) - 1 content, err := reader.ReadSessionContent(ctx, checkpointID, latestIndex) if err != nil { diff --git a/cmd/entire/cli/checkpoint/committed_reader_resolve_test.go b/cmd/entire/cli/checkpoint/committed_reader_resolve_test.go index fa91c63eb7..c02e1446e2 100644 --- a/cmd/entire/cli/checkpoint/committed_reader_resolve_test.go +++ b/cmd/entire/cli/checkpoint/committed_reader_resolve_test.go @@ -32,7 +32,7 @@ func TestReadCommittedCheckpointWrapsReaderError(t *testing.T) { require.ErrorContains(t, err, "read committed checkpoint") } -func TestReadLatestSessionContentEmptySummaryReturnsDistinctError(t *testing.T) { +func TestReadLatestSessionContentEmptySummaryReturnsNotFound(t *testing.T) { t.Parallel() cpID := id.MustCheckpointID("111111111111") @@ -41,8 +41,7 @@ func TestReadLatestSessionContentEmptySummaryReturnsDistinctError(t *testing.T) content, err := ReadLatestSessionContent(context.Background(), reader, cpID, summary) require.Nil(t, content) - require.ErrorContains(t, err, "checkpoint has no sessions") - require.NotErrorIs(t, err, ErrCheckpointNotFound) + require.ErrorIs(t, err, ErrCheckpointNotFound) } func TestReadRawSessionLogForCheckpointReadsLatestV1Session(t *testing.T) { From a9fb6e743607bd4fde85380979ba7c0750ae9a64 Mon Sep 17 00:00:00 2001 From: Sven Pfleiderer Date: Thu, 18 Jun 2026 18:11:00 -0700 Subject: [PATCH 08/11] checkpoint: add domain store adapters Introduce session and checkpoint document interfaces over the existing git committed storage layout. The new GitStore adapters preserve the old on-disk behavior while exposing SessionRef-based reads and option-based backfills for later caller migration. Entire-Checkpoint: 51f5a0ee3b11 --- cmd/entire/cli/checkpoint/committed_domain.go | 461 ++++++++++++++++++ .../committed_reader_resolve_test.go | 85 ++++ cmd/entire/cli/checkpoint/store.go | 2 + 3 files changed, 548 insertions(+) create mode 100644 cmd/entire/cli/checkpoint/committed_domain.go diff --git a/cmd/entire/cli/checkpoint/committed_domain.go b/cmd/entire/cli/checkpoint/committed_domain.go new file mode 100644 index 0000000000..6fda4663f2 --- /dev/null +++ b/cmd/entire/cli/checkpoint/committed_domain.go @@ -0,0 +1,461 @@ +package checkpoint + +import ( + "context" + "errors" + "fmt" + + "github.com/entireio/cli/cmd/entire/cli/agent" + "github.com/entireio/cli/cmd/entire/cli/agent/types" + "github.com/entireio/cli/cmd/entire/cli/checkpoint/id" + "github.com/entireio/cli/cmd/entire/cli/jsonutil" + "github.com/entireio/cli/cmd/entire/cli/paths" + "github.com/entireio/cli/redact" + "github.com/go-git/go-git/v6/plumbing" + "github.com/go-git/go-git/v6/plumbing/filemode" + "github.com/go-git/go-git/v6/plumbing/object" +) + +// Session is the committed session document written by SessionWriter. +type Session = WriteCommittedOptions + +type sessionRefMode int + +const ( + sessionRefLatest sessionRefMode = iota + sessionRefIndex + sessionRefID +) + +// SessionRef identifies a session within today's embedded checkpoint layout. +type SessionRef struct { + checkpointID id.CheckpointID + sessionID string + sessionIndex int + sessionRefMode sessionRefMode +} + +// LatestSessionRef targets the latest session in a checkpoint. +func LatestSessionRef(checkpointID id.CheckpointID) SessionRef { + return SessionRef{checkpointID: checkpointID, sessionIndex: -1, sessionRefMode: sessionRefLatest} +} + +// SessionIndexRef targets a session by its checkpoint-local index. +func SessionIndexRef(checkpointID id.CheckpointID, sessionIndex int) SessionRef { + return SessionRef{checkpointID: checkpointID, sessionIndex: sessionIndex, sessionRefMode: sessionRefIndex} +} + +// SessionIDRef targets a session by its session ID. +func SessionIDRef(checkpointID id.CheckpointID, sessionID string) SessionRef { + return SessionRef{checkpointID: checkpointID, sessionID: sessionID, sessionIndex: -1, sessionRefMode: sessionRefID} +} + +// CheckpointID returns the checkpoint that contains the session. +func (r SessionRef) CheckpointID() id.CheckpointID { return r.checkpointID } + +// SessionID returns the target session ID when the ref was created by SessionIDRef. +func (r SessionRef) SessionID() string { return r.sessionID } + +type sessionReadMode int + +const ( + sessionReadFull sessionReadMode = iota + sessionReadMetadataOnly + sessionReadMetadataAndPrompts +) + +type readOptions struct { + mode sessionReadMode +} + +// ReadOption customizes a session read without expanding the reader interface. +type ReadOption func(*readOptions) + +// WithSessionMetadataOnly reads session metadata without transcript or prompts. +func WithSessionMetadataOnly() ReadOption { + return func(opts *readOptions) { + opts.mode = sessionReadMetadataOnly + } +} + +// WithSessionMetadataAndPrompts reads metadata and prompts without transcript. +func WithSessionMetadataAndPrompts() ReadOption { + return func(opts *readOptions) { + opts.mode = sessionReadMetadataAndPrompts + } +} + +type writeOptions struct { + transcript redact.RedactedBytes + transcriptSet bool + prompts []string + promptsSet bool + agent types.AgentType + skillEvents []agent.SkillEvent + skillEventsSet bool + precomputedBlobs *PrecomputedTranscriptBlobs + summary *Summary + summarySet bool + attribution *InitialAttribution + attributionSet bool +} + +// WriteOption customizes session or checkpoint updates. +type WriteOption func(*writeOptions) + +// WithTranscript replaces a session transcript. +func WithTranscript(transcript redact.RedactedBytes, agentType types.AgentType) WriteOption { + return func(opts *writeOptions) { + opts.transcript = transcript + opts.transcriptSet = true + opts.agent = agentType + } +} + +// WithPrompts replaces a session's prompt content. +func WithPrompts(prompts []string) WriteOption { + return func(opts *writeOptions) { + opts.prompts = prompts + opts.promptsSet = true + } +} + +// WithSkillEvents replaces a session's recorded skill events. +func WithSkillEvents(events []agent.SkillEvent) WriteOption { + return func(opts *writeOptions) { + opts.skillEvents = events + opts.skillEventsSet = true + } +} + +// WithPrecomputedTranscriptBlobs reuses already-written transcript blobs. +func WithPrecomputedTranscriptBlobs(blobs *PrecomputedTranscriptBlobs) WriteOption { + return func(opts *writeOptions) { + opts.precomputedBlobs = blobs + } +} + +// WithSummary updates a session summary. +func WithSummary(summary *Summary) WriteOption { + return func(opts *writeOptions) { + opts.summary = summary + opts.summarySet = true + } +} + +// WithAttribution updates checkpoint-level attribution. +func WithAttribution(attribution *InitialAttribution) WriteOption { + return func(opts *writeOptions) { + opts.attribution = attribution + opts.attributionSet = true + } +} + +// SessionReader reads committed session documents. +type SessionReader interface { + ReadSession(ctx context.Context, ref SessionRef, opts ...ReadOption) (*SessionContent, error) +} + +// SessionWriter writes and backfills committed session documents. +type SessionWriter interface { + WriteSession(ctx context.Context, ref SessionRef, session Session) error + UpdateSession(ctx context.Context, ref SessionRef, opts ...WriteOption) error +} + +// SessionStore reads and writes committed sessions. +type SessionStore interface { + SessionReader + SessionWriter +} + +// Reader reads committed checkpoint documents. +type Reader interface { + ListCheckpoints(ctx context.Context) ([]CommittedInfo, error) + ReadCheckpoint(ctx context.Context, checkpointID id.CheckpointID) (*CheckpointSummary, error) +} + +// Writer backfills committed checkpoint documents. +type Writer interface { + UpdateCheckpoint(ctx context.Context, checkpointID id.CheckpointID, opts ...WriteOption) error +} + +// Store reads and writes committed checkpoint documents. +type Store interface { + Reader + Writer +} + +func (s *GitStore) ListCheckpoints(ctx context.Context) ([]CommittedInfo, error) { + return s.ListCommitted(ctx) +} + +func (s *GitStore) ReadCheckpoint(ctx context.Context, checkpointID id.CheckpointID) (*CheckpointSummary, error) { + return ReadCommittedCheckpoint(ctx, s, checkpointID) +} + +func (s *GitStore) ReadSession(ctx context.Context, ref SessionRef, opts ...ReadOption) (*SessionContent, error) { + if err := ref.validate(); err != nil { + return nil, err + } + + readOpts := applyReadOptions(opts) + sessionIndex, err := s.resolveSessionIndex(ctx, ref) + if err != nil { + return nil, err + } + + switch readOpts.mode { + case sessionReadFull: + return s.ReadSessionContent(ctx, ref.checkpointID, sessionIndex) + case sessionReadMetadataOnly: + metadata, readErr := s.ReadSessionMetadata(ctx, ref.checkpointID, sessionIndex) + if readErr != nil { + return nil, readErr + } + if metadata == nil { + return nil, ErrCheckpointNotFound + } + return &SessionContent{Metadata: *metadata}, nil + case sessionReadMetadataAndPrompts: + return s.ReadSessionMetadataAndPrompts(ctx, ref.checkpointID, sessionIndex) + default: + return nil, errors.New("unknown session read mode") + } +} + +func (s *GitStore) WriteSession(ctx context.Context, ref SessionRef, session Session) error { + if err := ref.validateForWrite(); err != nil { + return err + } + + opts := session + switch { + case opts.CheckpointID.IsEmpty(): + opts.CheckpointID = ref.checkpointID + case opts.CheckpointID != ref.checkpointID: + return fmt.Errorf("session checkpoint ID %s does not match ref %s", opts.CheckpointID, ref.checkpointID) + } + switch { + case opts.SessionID == "": + opts.SessionID = ref.sessionID + case opts.SessionID != ref.sessionID: + return fmt.Errorf("session ID %q does not match ref %q", opts.SessionID, ref.sessionID) + } + return s.WriteCommitted(ctx, opts) +} + +func (s *GitStore) UpdateSession(ctx context.Context, ref SessionRef, opts ...WriteOption) error { + if err := ref.validate(); err != nil { + return err + } + + writeOpts := applyWriteOptions(opts) + if !writeOpts.hasSessionUpdate() { + return errors.New("no session update options") + } + + if writeOpts.hasTranscriptUpdate() { + sessionID, err := s.resolveSessionID(ctx, ref) + if err != nil { + return err + } + if err := s.UpdateCommitted(ctx, UpdateCommittedOptions{ + CheckpointID: ref.checkpointID, + SessionID: sessionID, + Transcript: writeOpts.transcript, + Prompts: writeOpts.prompts, + Agent: writeOpts.agent, + SkillEvents: writeOpts.skillEvents, + PrecomputedBlobs: writeOpts.precomputedBlobs, + }); err != nil { + return err + } + } + + if writeOpts.summarySet { + return s.updateSessionSummary(ctx, ref, writeOpts.summary) + } + return nil +} + +func (s *GitStore) UpdateCheckpoint(ctx context.Context, checkpointID id.CheckpointID, opts ...WriteOption) error { + writeOpts := applyWriteOptions(opts) + if !writeOpts.attributionSet { + return errors.New("no checkpoint update options") + } + return s.UpdateCheckpointSummary(ctx, checkpointID, writeOpts.attribution) +} + +func applyReadOptions(opts []ReadOption) readOptions { + readOpts := readOptions{mode: sessionReadFull} + for _, opt := range opts { + opt(&readOpts) + } + return readOpts +} + +func applyWriteOptions(opts []WriteOption) writeOptions { + var writeOpts writeOptions + for _, opt := range opts { + opt(&writeOpts) + } + return writeOpts +} + +func (opts writeOptions) hasTranscriptUpdate() bool { + return opts.transcriptSet || opts.promptsSet || opts.skillEventsSet || opts.precomputedBlobs != nil +} + +func (opts writeOptions) hasSessionUpdate() bool { + return opts.hasTranscriptUpdate() || opts.summarySet +} + +func (r SessionRef) validate() error { + if r.checkpointID.IsEmpty() { + return errors.New("session ref checkpoint ID is required") + } + if r.sessionRefMode == sessionRefID && r.sessionID == "" { + return errors.New("session ref session ID is required") + } + if r.sessionRefMode == sessionRefIndex && r.sessionIndex < 0 { + return fmt.Errorf("session ref index must be non-negative: %d", r.sessionIndex) + } + return nil +} + +func (r SessionRef) validateForWrite() error { + if err := r.validate(); err != nil { + return err + } + if r.sessionRefMode != sessionRefID { + return errors.New("write session requires a session ID ref") + } + return nil +} + +func (s *GitStore) resolveSessionID(ctx context.Context, ref SessionRef) (string, error) { + if ref.sessionID != "" { + return ref.sessionID, nil + } + content, err := s.ReadSession(ctx, ref, WithSessionMetadataOnly()) + if err != nil { + return "", err + } + return content.Metadata.SessionID, nil +} + +func (s *GitStore) resolveSessionIndex(ctx context.Context, ref SessionRef) (int, error) { + summary, err := s.ReadCheckpoint(ctx, ref.checkpointID) + if err != nil { + return 0, err + } + return s.resolveSessionIndexFromSummary(ctx, ref, summary) +} + +func (s *GitStore) resolveSessionIndexFromSummary(ctx context.Context, ref SessionRef, summary *CheckpointSummary) (int, error) { + if summary == nil || len(summary.Sessions) == 0 { + return 0, ErrCheckpointNotFound + } + + switch ref.sessionRefMode { + case sessionRefLatest: + return len(summary.Sessions) - 1, nil + case sessionRefIndex: + if ref.sessionIndex >= len(summary.Sessions) { + return 0, fmt.Errorf("session index %d out of range for checkpoint %s: %w", ref.sessionIndex, ref.checkpointID, ErrCheckpointNotFound) + } + return ref.sessionIndex, nil + case sessionRefID: + for index := range summary.Sessions { + metadata, err := s.ReadSessionMetadata(ctx, ref.checkpointID, index) + if err != nil { + return 0, err + } + if metadata != nil && metadata.SessionID == ref.sessionID { + return index, nil + } + } + return 0, fmt.Errorf("session %q not found in checkpoint %s: %w", ref.sessionID, ref.checkpointID, ErrCheckpointNotFound) + default: + return 0, errors.New("unknown session ref mode") + } +} + +func (s *GitStore) updateSessionSummary(ctx context.Context, ref SessionRef, summary *Summary) error { + if err := ctx.Err(); err != nil { + return err //nolint:wrapcheck // Propagating context cancellation + } + if err := s.ensureSessionsBranch(ctx); err != nil { + return fmt.Errorf("failed to ensure sessions branch: %w", err) + } + + parentHash, rootTreeHash, err := s.getSessionsBranchRef() + if err != nil { + return err + } + + basePath := ref.checkpointID.Path() + "/" + entries, err := s.flattenCheckpointEntries(rootTreeHash, ref.checkpointID.Path()) + if err != nil { + return err + } + + rootMetadataPath := basePath + paths.MetadataFileName + rootEntry, exists := entries[rootMetadataPath] + if !exists { + return ErrCheckpointNotFound + } + checkpointSummary, err := s.readSummaryFromBlob(rootEntry.Hash) + if err != nil { + return fmt.Errorf("failed to read checkpoint summary: %w", err) + } + sessionIndex, err := s.resolveSessionIndexFromSummary(ctx, ref, checkpointSummary) + if err != nil { + return err + } + + sessionMetadataPath := fmt.Sprintf("%s%d/%s", basePath, sessionIndex, paths.MetadataFileName) + sessionEntry, exists := entries[sessionMetadataPath] + if !exists { + return fmt.Errorf("session metadata not found at %s", sessionMetadataPath) + } + existingMetadata, err := s.readMetadataFromBlob(sessionEntry.Hash) + if err != nil { + return fmt.Errorf("failed to read session metadata: %w", err) + } + + existingMetadata.Summary = redactSummary(summary) + metadataHash, err := createCommittedMetadataBlob(s, existingMetadata) + if err != nil { + return err + } + entries[sessionMetadataPath] = object.TreeEntry{ + Name: sessionMetadataPath, + Mode: filemode.Regular, + Hash: metadataHash, + } + + newTreeHash, err := s.spliceCheckpointSubtree(ctx, rootTreeHash, ref.checkpointID, basePath, entries) + if err != nil { + return err + } + authorName, authorEmail := GetGitAuthorFromRepo(s.repo) + commitMsg := fmt.Sprintf("Update summary for checkpoint %s (session: %s)", ref.checkpointID, existingMetadata.SessionID) + newCommitHash, err := s.createCommit(ctx, newTreeHash, parentHash, commitMsg, authorName, authorEmail) + if err != nil { + return err + } + return s.setPrimaryRef(newCommitHash) +} + +func createCommittedMetadataBlob(store *GitStore, metadata *CommittedMetadata) (plumbing.Hash, error) { + metadataJSON, err := jsonutil.MarshalIndentWithNewline(metadata, "", " ") + if err != nil { + return plumbing.ZeroHash, fmt.Errorf("failed to marshal metadata: %w", err) + } + metadataHash, err := CreateBlobFromContent(store.repo, metadataJSON) + if err != nil { + return plumbing.ZeroHash, fmt.Errorf("failed to create metadata blob: %w", err) + } + return metadataHash, nil +} diff --git a/cmd/entire/cli/checkpoint/committed_reader_resolve_test.go b/cmd/entire/cli/checkpoint/committed_reader_resolve_test.go index c02e1446e2..74e96ce2b1 100644 --- a/cmd/entire/cli/checkpoint/committed_reader_resolve_test.go +++ b/cmd/entire/cli/checkpoint/committed_reader_resolve_test.go @@ -79,6 +79,91 @@ func TestReadRawSessionLogForCheckpointReadsLatestV1Session(t *testing.T) { require.Equal(t, []byte("latest transcript\n"), transcript) } +func TestGitStoreSessionStoreReadsByRef(t *testing.T) { + t.Parallel() + + repoDir := t.TempDir() + testutil.InitRepo(t, repoDir) + repo, err := git.PlainOpen(repoDir) + require.NoError(t, err) + + store := NewGitStore(repo, DefaultV1Refs()) + ctx := context.Background() + cpID := id.MustCheckpointID("333333333333") + + writeSessionForStoreTest(t, store, cpID, "session-a", "first transcript\n", "first prompt") + writeSessionForStoreTest(t, store, cpID, "session-b", "latest transcript\n", "latest prompt") + + latest, err := store.ReadSession(ctx, LatestSessionRef(cpID)) + require.NoError(t, err) + require.Equal(t, "session-b", latest.Metadata.SessionID) + require.Equal(t, []byte("latest transcript\n"), latest.Transcript) + + metadataOnly, err := store.ReadSession(ctx, SessionIndexRef(cpID, 0), WithSessionMetadataOnly()) + require.NoError(t, err) + require.Equal(t, "session-a", metadataOnly.Metadata.SessionID) + require.Empty(t, metadataOnly.Transcript) + require.Empty(t, metadataOnly.Prompts) + + metadataAndPrompts, err := store.ReadSession(ctx, SessionIDRef(cpID, "session-a"), WithSessionMetadataAndPrompts()) + require.NoError(t, err) + require.Equal(t, "session-a", metadataAndPrompts.Metadata.SessionID) + require.Equal(t, "first prompt", metadataAndPrompts.Prompts) + require.Empty(t, metadataAndPrompts.Transcript) + + infos, err := store.ListCheckpoints(ctx) + require.NoError(t, err) + require.Len(t, infos, 1) + require.Equal(t, cpID, infos[0].CheckpointID) +} + +func TestGitStoreStoresUpdateSpecificSessionAndCheckpoint(t *testing.T) { + t.Parallel() + + repoDir := t.TempDir() + testutil.InitRepo(t, repoDir) + repo, err := git.PlainOpen(repoDir) + require.NoError(t, err) + + store := NewGitStore(repo, DefaultV1Refs()) + ctx := context.Background() + cpID := id.MustCheckpointID("444444444444") + + writeSessionForStoreTest(t, store, cpID, "session-a", "first transcript\n", "first prompt") + writeSessionForStoreTest(t, store, cpID, "session-b", "latest transcript\n", "latest prompt") + + sessionSummary := &Summary{Intent: "summarize first session"} + require.NoError(t, store.UpdateSession(ctx, SessionIDRef(cpID, "session-a"), WithSummary(sessionSummary))) + + first, err := store.ReadSession(ctx, SessionIDRef(cpID, "session-a"), WithSessionMetadataOnly()) + require.NoError(t, err) + require.Equal(t, sessionSummary.Intent, first.Metadata.Summary.Intent) + + second, err := store.ReadSession(ctx, SessionIDRef(cpID, "session-b"), WithSessionMetadataOnly()) + require.NoError(t, err) + require.Nil(t, second.Metadata.Summary) + + attribution := &InitialAttribution{AgentLines: 7, TotalCommitted: 11} + require.NoError(t, store.UpdateCheckpoint(ctx, cpID, WithAttribution(attribution))) + + checkpointSummary, err := store.ReadCheckpoint(ctx, cpID) + require.NoError(t, err) + require.Equal(t, 7, checkpointSummary.CombinedAttribution.AgentLines) + require.Equal(t, 11, checkpointSummary.CombinedAttribution.TotalCommitted) +} + +func writeSessionForStoreTest(t *testing.T, store *GitStore, cpID id.CheckpointID, sessionID, transcript, prompt string) { + t.Helper() + + require.NoError(t, store.WriteSession(t.Context(), SessionIDRef(cpID, sessionID), Session{ + Strategy: "manual-commit", + Transcript: redact.AlreadyRedacted([]byte(transcript)), + Prompts: []string{prompt}, + AuthorName: "Test", + AuthorEmail: "test@example.com", + })) +} + type committedReaderStub struct { summary *CheckpointSummary readErr error diff --git a/cmd/entire/cli/checkpoint/store.go b/cmd/entire/cli/checkpoint/store.go index 8022262cf9..882623757b 100644 --- a/cmd/entire/cli/checkpoint/store.go +++ b/cmd/entire/cli/checkpoint/store.go @@ -9,6 +9,8 @@ import ( var ( _ CommittedStore = (*GitStore)(nil) + _ SessionStore = (*GitStore)(nil) + _ Store = (*GitStore)(nil) _ TemporaryStore = (*GitStore)(nil) _ AuthorReader = (*GitStore)(nil) ) From 887b2396e6d579208f1dcc37c5d546f40e1b1755 Mon Sep 17 00:00:00 2001 From: Sven Pfleiderer Date: Thu, 18 Jun 2026 19:05:02 -0700 Subject: [PATCH 09/11] checkpoint: route callers through domain stores Move committed checkpoint callers to the split checkpoint and session store surface. GitStore keeps the low-level v1 methods internally, but production callers now depend on ReadCheckpoint, ReadSession, WriteSession, UpdateSession, and UpdateCheckpoint. Entire-Checkpoint: 7319e37bab43 --- cmd/entire/cli/attach.go | 20 ++++++---- cmd/entire/cli/attribution.go | 13 ++---- cmd/entire/cli/attribution_test.go | 8 +++- cmd/entire/cli/benchutil/benchutil.go | 2 +- cmd/entire/cli/checkpoint/committed.go | 6 ++- cmd/entire/cli/checkpoint/committed_domain.go | 21 +++++++++- .../checkpoint/committed_reader_resolve.go | 40 +++++-------------- .../committed_reader_resolve_test.go | 8 +++- cmd/entire/cli/checkpoint_reader.go | 8 ++++ cmd/entire/cli/dispatch/mode_local.go | 11 +++-- cmd/entire/cli/explain.go | 24 ++++++----- cmd/entire/cli/explain_export.go | 26 ++++-------- cmd/entire/cli/explain_export_test.go | 13 +++--- cmd/entire/cli/explain_test.go | 8 ++-- cmd/entire/cli/head_checkpoint_flags.go | 2 +- cmd/entire/cli/resume.go | 17 ++++---- cmd/entire/cli/resume_test.go | 18 ++++++--- cmd/entire/cli/review_context.go | 35 +++------------- cmd/entire/cli/review_context_test.go | 30 ++++---------- cmd/entire/cli/rewind.go | 4 +- cmd/entire/cli/strategy/cleanup.go | 2 +- cmd/entire/cli/strategy/common.go | 2 +- .../strategy/manual_commit_condensation.go | 12 ++---- .../cli/strategy/manual_commit_hooks.go | 26 +++++------- .../cli/strategy/manual_commit_rewind.go | 8 ++-- 25 files changed, 164 insertions(+), 200 deletions(-) create mode 100644 cmd/entire/cli/checkpoint_reader.go diff --git a/cmd/entire/cli/attach.go b/cmd/entire/cli/attach.go index 3f09e4cd54..3ff1af0153 100644 --- a/cmd/entire/cli/attach.go +++ b/cmd/entire/cli/attach.go @@ -337,7 +337,7 @@ func runAttach(ctx context.Context, w io.Writer, sessionID string, agentName typ writeOpts.HasReview = true } - if err := store.WriteCommitted(ctx, writeOpts); err != nil { + if err := store.WriteSession(ctx, cpkg.SessionIDRef(checkpointID, sessionID), writeOpts); err != nil { return fmt.Errorf("failed to write checkpoint: %w", err) } @@ -370,19 +370,20 @@ func checkpointHasSessionMetadata(ctx context.Context, repo *git.Repository, ref if err != nil { return false, err } - summary, err := store.ReadCommitted(ctx, checkpointID) + summary, err := store.ReadCheckpoint(ctx, checkpointID) if err != nil { + if errors.Is(err, cpkg.ErrCheckpointNotFound) { + return false, nil + } return false, fmt.Errorf("read checkpoint summary: %w", err) } - if summary == nil { - return false, nil - } for i := range summary.Sessions { - metadata, err := store.ReadSessionMetadata(ctx, checkpointID, i) + content, err := store.ReadSession(ctx, cpkg.SessionIndexRef(checkpointID, i), cpkg.WithSessionMetadataOnly()) if err != nil { return false, fmt.Errorf("read session %d metadata: %w", i, err) } - if metadata != nil && metadata.SessionID == sessionID { + metadata := content.Metadata + if metadata.SessionID == sessionID { return true, nil } } @@ -473,8 +474,11 @@ func checkpointPresentLocally(ctx context.Context, repo *git.Repository, refs cp if err != nil { return false, err } - summary, err := store.ReadCommitted(ctx, checkpointID) + summary, err := store.ReadCheckpoint(ctx, checkpointID) if err != nil { + if errors.Is(err, cpkg.ErrCheckpointNotFound) { + return false, nil + } return false, err //nolint:wrapcheck // Caller wraps with checkpoint ID context } return summary != nil, nil diff --git a/cmd/entire/cli/attribution.go b/cmd/entire/cli/attribution.go index 055e10ec1f..d644d5299d 100644 --- a/cmd/entire/cli/attribution.go +++ b/cmd/entire/cli/attribution.go @@ -114,15 +114,10 @@ type attributionSummary struct { MixedPercentage int `json:"mixed_percentage"` } -type attributionCheckpointReader interface { - ReadCommitted(ctx context.Context, checkpointID id.CheckpointID) (*checkpoint.CheckpointSummary, error) - ReadSessionMetadataAndPrompts(ctx context.Context, checkpointID id.CheckpointID, sessionIndex int) (*checkpoint.SessionContent, error) -} - type attributionResolver struct { ctx context.Context repo *git.Repository - store attributionCheckpointReader + store committedCheckpointReader fetchOnMiss bool commitCache map[string]*object.Commit @@ -489,11 +484,11 @@ func (r *attributionResolver) readCheckpointContext(cpID id.CheckpointID, file s return ctx } -func readAttributionCheckpointSummary(ctx context.Context, reader attributionCheckpointReader, cpID id.CheckpointID) (*checkpoint.CheckpointSummary, error) { +func readAttributionCheckpointSummary(ctx context.Context, reader committedCheckpointReader, cpID id.CheckpointID) (*checkpoint.CheckpointSummary, error) { if err := ctx.Err(); err != nil { return nil, err //nolint:wrapcheck // Propagating context cancellation } - summary, err := reader.ReadCommitted(ctx, cpID) + summary, err := reader.ReadCheckpoint(ctx, cpID) if err != nil { return nil, fmt.Errorf("read committed checkpoint: %w", err) } @@ -568,7 +563,7 @@ type checkpointSessionForFile struct { } func (r *attributionResolver) readSessionForCheckpoint(cpID id.CheckpointID, index int) (checkpointSessionForFile, error) { - content, err := r.store.ReadSessionMetadataAndPrompts(r.ctx, cpID, index) + content, err := r.store.ReadSession(r.ctx, checkpoint.SessionIndexRef(cpID, index), checkpoint.WithSessionMetadataAndPrompts()) if err != nil { return checkpointSessionForFile{}, err //nolint:wrapcheck // caller skips partial metadata } diff --git a/cmd/entire/cli/attribution_test.go b/cmd/entire/cli/attribution_test.go index 384a765830..55ed0ef755 100644 --- a/cmd/entire/cli/attribution_test.go +++ b/cmd/entire/cli/attribution_test.go @@ -390,11 +390,15 @@ type attributionCheckpointReaderStub struct { content *checkpoint.SessionContent } -func (s *attributionCheckpointReaderStub) ReadCommitted(context.Context, checkpointid.CheckpointID) (*checkpoint.CheckpointSummary, error) { +func (s *attributionCheckpointReaderStub) ListCheckpoints(context.Context) ([]checkpoint.CommittedInfo, error) { + return nil, nil +} + +func (s *attributionCheckpointReaderStub) ReadCheckpoint(context.Context, checkpointid.CheckpointID) (*checkpoint.CheckpointSummary, error) { return s.summary, nil } -func (s *attributionCheckpointReaderStub) ReadSessionMetadataAndPrompts(context.Context, checkpointid.CheckpointID, int) (*checkpoint.SessionContent, error) { +func (s *attributionCheckpointReaderStub) ReadSession(context.Context, checkpoint.SessionRef, ...checkpoint.ReadOption) (*checkpoint.SessionContent, error) { return s.content, nil } diff --git a/cmd/entire/cli/benchutil/benchutil.go b/cmd/entire/cli/benchutil/benchutil.go index d05f5959b4..20853aebd0 100644 --- a/cmd/entire/cli/benchutil/benchutil.go +++ b/cmd/entire/cli/benchutil/benchutil.go @@ -398,7 +398,7 @@ func (br *BenchRepo) SeedMetadataBranch(b *testing.B, checkpointCount int) { files = append(files, fmt.Sprintf("src/file_%03d.go", (i*5+j)%100)) } - err = br.Store.WriteCommitted(context.Background(), checkpoint.WriteCommittedOptions{ + err = br.Store.WriteSession(context.Background(), checkpoint.SessionIDRef(cpID, sessionID), checkpoint.Session{ CheckpointID: cpID, SessionID: sessionID, Strategy: br.Strategy, diff --git a/cmd/entire/cli/checkpoint/committed.go b/cmd/entire/cli/checkpoint/committed.go index 74afc563bc..ea1cf6a21e 100644 --- a/cmd/entire/cli/checkpoint/committed.go +++ b/cmd/entire/cli/checkpoint/committed.go @@ -1340,7 +1340,11 @@ func LookupSessionLog(ctx context.Context, cpID id.CheckpointID) ([]byte, string if err != nil { return nil, "", fmt.Errorf("open checkpoint store: %w", err) } - return ReadRawSessionLogForCheckpoint(ctx, stores.Primary, cpID) + content, err := stores.Primary.ReadSession(ctx, LatestSessionRef(cpID)) + if err != nil { + return nil, "", err //nolint:wrapcheck // Checkpoint store errors are already caller-facing sentinel errors. + } + return content.Transcript, content.Metadata.SessionID, nil } // UpdateSummary updates the summary field in the latest session's metadata. diff --git a/cmd/entire/cli/checkpoint/committed_domain.go b/cmd/entire/cli/checkpoint/committed_domain.go index 6fda4663f2..eaecd5c669 100644 --- a/cmd/entire/cli/checkpoint/committed_domain.go +++ b/cmd/entire/cli/checkpoint/committed_domain.go @@ -56,6 +56,14 @@ func (r SessionRef) CheckpointID() id.CheckpointID { return r.checkpointID } // SessionID returns the target session ID when the ref was created by SessionIDRef. func (r SessionRef) SessionID() string { return r.sessionID } +// SessionIndex returns the target index when the ref was created by SessionIndexRef. +func (r SessionRef) SessionIndex() (int, bool) { + if r.sessionRefMode != sessionRefIndex { + return 0, false + } + return r.sessionIndex, true +} + type sessionReadMode int const ( @@ -190,7 +198,18 @@ func (s *GitStore) ListCheckpoints(ctx context.Context) ([]CommittedInfo, error) } func (s *GitStore) ReadCheckpoint(ctx context.Context, checkpointID id.CheckpointID) (*CheckpointSummary, error) { - return ReadCommittedCheckpoint(ctx, s, checkpointID) + if err := ctx.Err(); err != nil { + return nil, err //nolint:wrapcheck // Propagating context cancellation + } + + summary, err := s.ReadCommitted(ctx, checkpointID) + if err != nil { + return nil, fmt.Errorf("read committed checkpoint: %w", err) + } + if summary == nil { + return nil, ErrCheckpointNotFound + } + return summary, nil } func (s *GitStore) ReadSession(ctx context.Context, ref SessionRef, opts ...ReadOption) (*SessionContent, error) { diff --git a/cmd/entire/cli/checkpoint/committed_reader_resolve.go b/cmd/entire/cli/checkpoint/committed_reader_resolve.go index ba1099fcb9..b73a83cf79 100644 --- a/cmd/entire/cli/checkpoint/committed_reader_resolve.go +++ b/cmd/entire/cli/checkpoint/committed_reader_resolve.go @@ -7,33 +7,10 @@ import ( "github.com/entireio/cli/cmd/entire/cli/checkpoint/id" ) -// CommittedReader provides read access to committed checkpoint data. -type CommittedReader interface { - ReadCommitted(ctx context.Context, checkpointID id.CheckpointID) (*CheckpointSummary, error) - ReadSessionContent(ctx context.Context, checkpointID id.CheckpointID, sessionIndex int) (*SessionContent, error) -} - -// CommittedListReader provides read and list access to committed checkpoint data. -type CommittedListReader interface { - CommittedReader - ListCommitted(ctx context.Context) ([]CommittedInfo, error) - ReadSessionMetadata(ctx context.Context, checkpointID id.CheckpointID, sessionIndex int) (*CommittedMetadata, error) - ReadSessionPrompts(ctx context.Context, checkpointID id.CheckpointID, sessionIndex int) (string, error) -} - -// CommittedWriter provides write access to committed checkpoint data. -type CommittedWriter interface { - WriteCommitted(ctx context.Context, opts WriteCommittedOptions) error - UpdateCommitted(ctx context.Context, opts UpdateCommittedOptions) error - UpdateSummary(ctx context.Context, checkpointID id.CheckpointID, summary *Summary) error - UpdateCheckpointSummary(ctx context.Context, checkpointID id.CheckpointID, combinedAttribution *InitialAttribution) error -} - // CommittedStore provides the production committed checkpoint storage surface. type CommittedStore interface { - CommittedListReader - ReadSessionMetadataAndPrompts(ctx context.Context, checkpointID id.CheckpointID, sessionIndex int) (*SessionContent, error) - CommittedWriter + SessionStore + Store } // AuthorReader provides optional checkpoint author lookup. @@ -43,12 +20,12 @@ type AuthorReader interface { // ReadCommittedCheckpoint reads a committed checkpoint summary and normalizes // a nil store response into ErrCheckpointNotFound. -func ReadCommittedCheckpoint(ctx context.Context, reader CommittedReader, checkpointID id.CheckpointID) (*CheckpointSummary, error) { +func ReadCommittedCheckpoint(ctx context.Context, reader Reader, checkpointID id.CheckpointID) (*CheckpointSummary, error) { if err := ctx.Err(); err != nil { return nil, err //nolint:wrapcheck // Propagating context cancellation } - summary, err := reader.ReadCommitted(ctx, checkpointID) + summary, err := reader.ReadCheckpoint(ctx, checkpointID) if err != nil { return nil, fmt.Errorf("read committed checkpoint: %w", err) } @@ -60,19 +37,22 @@ func ReadCommittedCheckpoint(ctx context.Context, reader CommittedReader, checkp // ReadLatestSessionContent reads the latest session from an already-resolved // committed reader and summary. -func ReadLatestSessionContent(ctx context.Context, reader CommittedReader, checkpointID id.CheckpointID, summary *CheckpointSummary) (*SessionContent, error) { +func ReadLatestSessionContent(ctx context.Context, reader SessionReader, checkpointID id.CheckpointID, summary *CheckpointSummary) (*SessionContent, error) { if summary == nil || len(summary.Sessions) == 0 { return nil, ErrCheckpointNotFound } latestIndex := len(summary.Sessions) - 1 - content, err := reader.ReadSessionContent(ctx, checkpointID, latestIndex) + content, err := reader.ReadSession(ctx, SessionIndexRef(checkpointID, latestIndex)) if err != nil { return nil, fmt.Errorf("read session %d content: %w", latestIndex, err) } return content, nil } -func ReadRawSessionLogForCheckpoint(ctx context.Context, reader CommittedReader, checkpointID id.CheckpointID) ([]byte, string, error) { +func ReadRawSessionLogForCheckpoint(ctx context.Context, reader interface { + Reader + SessionReader +}, checkpointID id.CheckpointID) ([]byte, string, error) { if err := ctx.Err(); err != nil { return nil, "", err //nolint:wrapcheck // Propagating context cancellation } diff --git a/cmd/entire/cli/checkpoint/committed_reader_resolve_test.go b/cmd/entire/cli/checkpoint/committed_reader_resolve_test.go index 74e96ce2b1..c786af1e46 100644 --- a/cmd/entire/cli/checkpoint/committed_reader_resolve_test.go +++ b/cmd/entire/cli/checkpoint/committed_reader_resolve_test.go @@ -169,13 +169,17 @@ type committedReaderStub struct { readErr error } -func (s *committedReaderStub) ReadCommitted(context.Context, id.CheckpointID) (*CheckpointSummary, error) { +func (s *committedReaderStub) ListCheckpoints(context.Context) ([]CommittedInfo, error) { + return nil, nil +} + +func (s *committedReaderStub) ReadCheckpoint(context.Context, id.CheckpointID) (*CheckpointSummary, error) { if s.readErr != nil { return nil, s.readErr } return s.summary, nil } -func (s *committedReaderStub) ReadSessionContent(context.Context, id.CheckpointID, int) (*SessionContent, error) { +func (s *committedReaderStub) ReadSession(context.Context, SessionRef, ...ReadOption) (*SessionContent, error) { return nil, ErrCheckpointNotFound } diff --git a/cmd/entire/cli/checkpoint_reader.go b/cmd/entire/cli/checkpoint_reader.go new file mode 100644 index 0000000000..9815dbc585 --- /dev/null +++ b/cmd/entire/cli/checkpoint_reader.go @@ -0,0 +1,8 @@ +package cli + +import "github.com/entireio/cli/cmd/entire/cli/checkpoint" + +type committedCheckpointReader interface { + checkpoint.Reader + checkpoint.SessionReader +} diff --git a/cmd/entire/cli/dispatch/mode_local.go b/cmd/entire/cli/dispatch/mode_local.go index f2ea825e73..dce2d81ea0 100644 --- a/cmd/entire/cli/dispatch/mode_local.go +++ b/cmd/entire/cli/dispatch/mode_local.go @@ -181,7 +181,7 @@ func enumerateRepoCandidates(ctx context.Context, repoRoot string, opts Options, return nil, fmt.Errorf("open checkpoint store: %w", err) } store := stores.Primary - infos, err := store.ListCommitted(ctx) + infos, err := store.ListCheckpoints(ctx) if err != nil { return nil, fmt.Errorf("list committed checkpoints: %w", err) } @@ -192,14 +192,11 @@ func enumerateRepoCandidates(ctx context.Context, repoRoot string, opts Options, continue } - summary, err := store.ReadCommitted(ctx, info.CheckpointID) + summary, err := store.ReadCheckpoint(ctx, info.CheckpointID) if err != nil { logging.Warn(ctx, "failed to read committed checkpoint for dispatch", "checkpoint_id", info.CheckpointID.String(), "error", err) continue } - if summary == nil { - continue - } if _, onSelectedBranch := branchSet[summary.Branch]; !onSelectedBranch { if !opts.ImplicitCurrentBranch { continue @@ -212,7 +209,9 @@ func enumerateRepoCandidates(ctx context.Context, repoRoot string, opts Options, localSummary := "" if len(summary.Sessions) > 0 { latestIndex := len(summary.Sessions) - 1 - if metadata, err := store.ReadSessionMetadata(ctx, info.CheckpointID, latestIndex); err == nil && metadata != nil && metadata.Summary != nil { + content, err := store.ReadSession(ctx, checkpoint.SessionIndexRef(info.CheckpointID, latestIndex), checkpoint.WithSessionMetadataOnly()) + if err == nil && content != nil && content.Metadata.Summary != nil { + metadata := content.Metadata localSummary = strings.TrimSpace(metadata.Summary.Outcome) if localSummary == "" { localSummary = strings.TrimSpace(metadata.Summary.Intent) diff --git a/cmd/entire/cli/explain.go b/cmd/entire/cli/explain.go index 28de77ced9..ad31c0dae7 100644 --- a/cmd/entire/cli/explain.go +++ b/cmd/entire/cli/explain.go @@ -697,7 +697,7 @@ func runExplainCheckpointWithLookup(ctx context.Context, w, errW io.Writer, chec return fmt.Errorf("open checkpoint store: %w", openErr) } lookup.store = reopened.Primary - content, err = checkpoint.ReadLatestSessionContent(ctx, lookup.store, fullCheckpointID, summary) + content, err = lookup.store.ReadSession(ctx, checkpoint.LatestSessionRef(fullCheckpointID)) if err != nil { stopLoad(false) return fmt.Errorf("failed to reload checkpoint: %w", err) @@ -750,12 +750,12 @@ func loadCheckpointForExplain(ctx context.Context, lookup *explainCheckpointLook prefetchCheckpointBlobs(ctx, lookup.repo, cpID) store := lookup.store - summary, err := checkpoint.ReadCommittedCheckpoint(ctx, store, cpID) + summary, err := store.ReadCheckpoint(ctx, cpID) if err != nil { return nil, nil, fmt.Errorf("failed to read checkpoint: %w", err) } - content, contentErr := checkpoint.ReadLatestSessionContent(ctx, store, cpID, summary) + content, contentErr := store.ReadSession(ctx, checkpoint.LatestSessionRef(cpID)) if contentErr != nil { return nil, nil, fmt.Errorf("failed to read checkpoint content: %w", contentErr) } @@ -879,7 +879,7 @@ func newExplainCheckpointLookup(ctx context.Context) (*explainCheckpointLookup, store: store, } - committed, err := store.ListCommitted(ctx) + committed, err := store.ListCheckpoints(ctx) if err != nil { return nil, fmt.Errorf("failed to list checkpoints: %w", err) } @@ -889,7 +889,7 @@ func newExplainCheckpointLookup(ctx context.Context) (*explainCheckpointLookup, } type checkpointSummaryUpdater interface { - UpdateSummary(ctx context.Context, checkpointID id.CheckpointID, summary *checkpoint.Summary) error + UpdateSession(ctx context.Context, ref checkpoint.SessionRef, opts ...checkpoint.WriteOption) error } // generateCheckpointSummary generates an AI summary for a checkpoint and persists it. @@ -946,7 +946,11 @@ func generateCheckpointSummary(ctx context.Context, w, errW io.Writer, store che } elapsed := time.Since(start) - if err := store.UpdateSummary(ctx, checkpointID, summary); err != nil { + ref := checkpoint.SessionIDRef(checkpointID, content.Metadata.SessionID) + if content.Metadata.SessionID == "" { + ref = checkpoint.LatestSessionRef(checkpointID) + } + if err := store.UpdateSession(ctx, ref, checkpoint.WithSummary(summary)); err != nil { return fmt.Errorf("failed to save summary: %w", err) } @@ -2046,7 +2050,7 @@ func getBranchCheckpoints(ctx context.Context, repo *git.Repository, limit int) store := stores.Primary // Get all committed checkpoints for lookup. - committedInfos, err := store.ListCommitted(ctx) + committedInfos, err := store.ListCheckpoints(ctx) if err != nil { committedInfos = nil // Continue without committed checkpoints } @@ -2163,16 +2167,16 @@ func getBranchCheckpoints(ctx context.Context, repo *git.Repository, limit int) return points, nil } -func readLatestCommittedSessionPrompt(ctx context.Context, store checkpoint.CommittedListReader, cpID id.CheckpointID, sessionCount int) string { +func readLatestCommittedSessionPrompt(ctx context.Context, store checkpoint.SessionReader, cpID id.CheckpointID, sessionCount int) string { if sessionCount <= 0 { return "" } for i := sessionCount - 1; i >= 0; i-- { - prompts, err := store.ReadSessionPrompts(ctx, cpID, i) + content, err := store.ReadSession(ctx, checkpoint.SessionIndexRef(cpID, i), checkpoint.WithSessionMetadataAndPrompts()) if err != nil { continue } - if prompt := strategy.ExtractFirstPrompt(prompts); prompt != "" { + if prompt := strategy.ExtractFirstPrompt(content.Prompts); prompt != "" { return prompt } } diff --git a/cmd/entire/cli/explain_export.go b/cmd/entire/cli/explain_export.go index c07b816d75..907e30891e 100644 --- a/cmd/entire/cli/explain_export.go +++ b/cmd/entire/cli/explain_export.go @@ -261,7 +261,7 @@ func runExplainStreamTranscript(ctx context.Context, w, errW io.Writer, opts exp defer lookup.Close() store := lookup.store - summary, err := checkpoint.ReadCommittedCheckpoint(ctx, store, cpID) + summary, err := store.ReadCheckpoint(ctx, cpID) if err != nil { return fmt.Errorf("failed to read checkpoint: %w", err) } @@ -271,7 +271,7 @@ func runExplainStreamTranscript(ctx context.Context, w, errW io.Writer, opts exp return err } - content, readErr := store.ReadSessionContent(ctx, cpID, idx) + content, readErr := store.ReadSession(ctx, checkpoint.SessionIndexRef(cpID, idx)) if readErr != nil { return fmt.Errorf("failed to read session content: %w", readErr) } @@ -355,7 +355,7 @@ func runExplainCheckpointJSON(ctx context.Context, w, errW io.Writer, opts expla defer lookup.Close() store := lookup.store - summary, err := checkpoint.ReadCommittedCheckpoint(ctx, store, cpID) + summary, err := store.ReadCheckpoint(ctx, cpID) if err != nil { return fmt.Errorf("failed to read checkpoint: %w", err) } @@ -385,7 +385,7 @@ func runExplainCheckpointJSON(ctx context.Context, w, errW io.Writer, opts expla // plus the list of session indexes that failed to read; a non-empty failed // list means envelope.Partial is true. Extracted from runExplainCheckpointJSON so // the envelope-building behavior can be tested independently of git storage. -func buildCheckpointJSONEnvelope(ctx context.Context, reader checkpoint.CommittedReader, summary *checkpoint.CheckpointSummary, cpID id.CheckpointID) (checkpointExportJSON, []int) { +func buildCheckpointJSONEnvelope(ctx context.Context, reader checkpoint.SessionReader, summary *checkpoint.CheckpointSummary, cpID id.CheckpointID) (checkpointExportJSON, []int) { envelope := checkpointExportJSON{ CheckpointID: cpID.String(), Strategy: summary.Strategy, @@ -422,22 +422,10 @@ func buildCheckpointJSONEnvelope(ctx context.Context, reader checkpoint.Committe // readSessionMetadataForExport reads only metadata.json for a session — no // transcript or prompt bytes. GitStore exposes a metadata-only reader, so this // never depends on transcript availability. -func readSessionMetadataForExport(ctx context.Context, reader checkpoint.CommittedReader, cpID id.CheckpointID, idx int) (*checkpoint.CommittedMetadata, error) { - if r, ok := reader.(interface { - ReadSessionMetadata(ctx context.Context, checkpointID id.CheckpointID, sessionIndex int) (*checkpoint.CommittedMetadata, error) - }); ok { - meta, err := r.ReadSessionMetadata(ctx, cpID, idx) - if err != nil { - return nil, fmt.Errorf("read session metadata: %w", err) - } - return meta, nil - } - // CommittedReader doesn't promise a metadata-only method; fall back - // to the heavier ReadSessionContent path. Reachable only if a third - // store implementation is added without exposing metadata reads. - content, err := reader.ReadSessionContent(ctx, cpID, idx) +func readSessionMetadataForExport(ctx context.Context, reader checkpoint.SessionReader, cpID id.CheckpointID, idx int) (*checkpoint.CommittedMetadata, error) { + content, err := reader.ReadSession(ctx, checkpoint.SessionIndexRef(cpID, idx), checkpoint.WithSessionMetadataOnly()) if err != nil { - return nil, fmt.Errorf("read session content: %w", err) + return nil, fmt.Errorf("read session metadata: %w", err) } meta := content.Metadata return &meta, nil diff --git a/cmd/entire/cli/explain_export_test.go b/cmd/entire/cli/explain_export_test.go index 6a7a5c2a42..6cf7f5c3a7 100644 --- a/cmd/entire/cli/explain_export_test.go +++ b/cmd/entire/cli/explain_export_test.go @@ -489,20 +489,17 @@ func TestRunExplainExport_NoModeFlagFailsLoudly(t *testing.T) { require.Empty(t, stdout.String(), "must not emit JSON when no mode is set") } -// stubCommittedReader is a minimal CommittedReader that returns canned -// metadata or errors per session index. Used to exercise the partial-failure -// path in buildCheckpointJSONEnvelope without corrupting a real git tree. type stubCommittedReader struct { summary *checkpoint.CheckpointSummary contents map[int]*checkpoint.SessionContent // idx -> content (nil ⇒ return error) err error // err returned for indexes not in contents } -func (s *stubCommittedReader) ReadCommitted(_ context.Context, _ id.CheckpointID) (*checkpoint.CheckpointSummary, error) { - return s.summary, nil -} - -func (s *stubCommittedReader) ReadSessionContent(_ context.Context, _ id.CheckpointID, idx int) (*checkpoint.SessionContent, error) { +func (s *stubCommittedReader) ReadSession(_ context.Context, ref checkpoint.SessionRef, _ ...checkpoint.ReadOption) (*checkpoint.SessionContent, error) { + idx, ok := ref.SessionIndex() + if !ok { + return nil, errors.New("stub: expected session index ref") + } if c, ok := s.contents[idx]; ok && c != nil { return c, nil } diff --git a/cmd/entire/cli/explain_test.go b/cmd/entire/cli/explain_test.go index 6e4dee1275..e458e6aec8 100644 --- a/cmd/entire/cli/explain_test.go +++ b/cmd/entire/cli/explain_test.go @@ -1011,7 +1011,7 @@ func TestGenerateCheckpointSummary_AdvancesV1Metadata(t *testing.T) { require.NoError(t, err) store := stores.Primary cpID := id.MustCheckpointID("a1b2c3d4e5f6") - require.NoError(t, store.WriteCommitted(ctx, checkpoint.WriteCommittedOptions{ + require.NoError(t, store.WriteSession(ctx, checkpoint.SessionIDRef(cpID, "session-001"), checkpoint.Session{ CheckpointID: cpID, SessionID: "session-001", Strategy: "manual-commit", @@ -1021,9 +1021,9 @@ func TestGenerateCheckpointSummary_AdvancesV1Metadata(t *testing.T) { AuthorEmail: "test@test.com", Agent: agent.AgentTypeClaudeCode, })) - cpSummary, err := checkpoint.ReadCommittedCheckpoint(ctx, store, cpID) + cpSummary, err := store.ReadCheckpoint(ctx, cpID) require.NoError(t, err) - content, err := checkpoint.ReadLatestSessionContent(ctx, store, cpID, cpSummary) + content, err := store.ReadSession(ctx, checkpoint.LatestSessionRef(cpID)) require.NoError(t, err) v1Before, err := repo.Reference(plumbing.NewBranchReferenceName(paths.MetadataBranchName), true) @@ -2137,7 +2137,7 @@ func TestRunExplainCheckpoint_GenerateV1ModeUsesSelectedStore(t *testing.T) { AuthorEmail: "test@example.com", Agent: agent.AgentTypeClaudeCode, })) - summary, err := checkpoint.ReadCommittedCheckpoint(ctx, store, cpID) + summary, err := store.ReadCheckpoint(ctx, cpID) require.NoError(t, err) require.Len(t, summary.Sessions, 1) diff --git a/cmd/entire/cli/head_checkpoint_flags.go b/cmd/entire/cli/head_checkpoint_flags.go index 8d543dbf56..b8ad516492 100644 --- a/cmd/entire/cli/head_checkpoint_flags.go +++ b/cmd/entire/cli/head_checkpoint_flags.go @@ -57,7 +57,7 @@ func headCheckpointFlags(ctx context.Context) (hasReview, hasInvestigation bool, logging.Debug(ctx, "head checkpoint flags: open store", slog.String("error", err.Error())) return false, false, "" } - summary, err := checkpoint.ReadCommittedCheckpoint(ctx, stores.Primary, cpID) + summary, err := stores.Primary.ReadCheckpoint(ctx, cpID) if err != nil || summary == nil { logging.Debug(ctx, "head checkpoint flags: resolve checkpoint summary", slog.String("checkpoint_id", cpID.String()), diff --git a/cmd/entire/cli/resume.go b/cmd/entire/cli/resume.go index 06e2fff65f..61ccc3230c 100644 --- a/cmd/entire/cli/resume.go +++ b/cmd/entire/cli/resume.go @@ -329,7 +329,7 @@ func resumeFromCurrentBranch(ctx context.Context, w, errW io.Writer, branchName // resolveLatestCheckpoint reads metadata for each checkpoint ID and returns // the checkpoint with the latest CreatedAt. -func resolveLatestCheckpoint(ctx context.Context, store checkpointInfoReader, checkpointIDs []id.CheckpointID) (*strategy.CheckpointInfo, error) { +func resolveLatestCheckpoint(ctx context.Context, store committedCheckpointReader, checkpointIDs []id.CheckpointID) (*strategy.CheckpointInfo, error) { infoMap := make(map[id.CheckpointID]strategy.CheckpointInfo, len(checkpointIDs)) for _, cpID := range checkpointIDs { metadata, readErr := readCheckpointInfoFromStore(ctx, store, cpID) @@ -349,13 +349,8 @@ func resolveLatestCheckpoint(ctx context.Context, store checkpointInfoReader, ch return &latest, nil } -type checkpointInfoReader interface { - checkpoint.CommittedReader - ReadSessionMetadata(ctx context.Context, checkpointID id.CheckpointID, sessionIndex int) (*checkpoint.CommittedMetadata, error) -} - -func readCheckpointInfoFromStore(ctx context.Context, store checkpointInfoReader, checkpointID id.CheckpointID) (*strategy.CheckpointInfo, error) { - summary, err := checkpoint.ReadCommittedCheckpoint(ctx, store, checkpointID) +func readCheckpointInfoFromStore(ctx context.Context, store committedCheckpointReader, checkpointID id.CheckpointID) (*strategy.CheckpointInfo, error) { + summary, err := store.ReadCheckpoint(ctx, checkpointID) if err != nil { return nil, fmt.Errorf("read checkpoint: %w", err) } @@ -366,7 +361,7 @@ func readCheckpointInfoFromStore(ctx context.Context, store checkpointInfoReader SessionCount: len(summary.Sessions), } for i := range summary.Sessions { - metadata, metaErr := store.ReadSessionMetadata(ctx, checkpointID, i) + content, metaErr := store.ReadSession(ctx, checkpoint.SessionIndexRef(checkpointID, i), checkpoint.WithSessionMetadataOnly()) if metaErr != nil { logging.Debug(ctx, "read checkpoint metadata: session metadata read failed", slog.String("checkpoint_id", checkpointID.String()), @@ -375,6 +370,7 @@ func readCheckpointInfoFromStore(ctx context.Context, store checkpointInfoReader ) continue } + metadata := content.Metadata info.SessionIDs = append(info.SessionIDs, metadata.SessionID) if metadata.SessionID != "" { info.SessionID = metadata.SessionID @@ -942,7 +938,7 @@ func resumeSingleSession(ctx context.Context, w, _ io.Writer, ag agent.Agent, se if err != nil { return fmt.Errorf("open checkpoint store: %w", err) } - logContent, _, err := checkpoint.ReadRawSessionLogForCheckpoint(ctx, stores.Primary, checkpointID) + content, err := stores.Primary.ReadSession(ctx, checkpoint.SessionIDRef(checkpointID, sessionID)) if err != nil { if errors.Is(err, checkpoint.ErrCheckpointNotFound) || errors.Is(err, checkpoint.ErrNoTranscript) { logging.Debug(ctx, "resume session completed (no metadata)", @@ -961,6 +957,7 @@ func resumeSingleSession(ctx context.Context, w, _ io.Writer, ag agent.Agent, se ) return fmt.Errorf("failed to get session log: %w", err) } + logContent := content.Transcript // By default, never overwrite a session log that already exists locally: the // on-disk transcript is the live session the user is resuming, so we keep it diff --git a/cmd/entire/cli/resume_test.go b/cmd/entire/cli/resume_test.go index 8b3822e8b9..150b50260d 100644 --- a/cmd/entire/cli/resume_test.go +++ b/cmd/entire/cli/resume_test.go @@ -628,20 +628,26 @@ type resumeCheckpointInfoReaderStub struct { metadata map[id.CheckpointID][]checkpoint.CommittedMetadata } -func (r *resumeCheckpointInfoReaderStub) ReadCommitted(_ context.Context, checkpointID id.CheckpointID) (*checkpoint.CheckpointSummary, error) { - return r.summaries[checkpointID], nil +func (r *resumeCheckpointInfoReaderStub) ListCheckpoints(context.Context) ([]checkpoint.CommittedInfo, error) { + return nil, nil } -func (r *resumeCheckpointInfoReaderStub) ReadSessionContent(_ context.Context, _ id.CheckpointID, _ int) (*checkpoint.SessionContent, error) { - return nil, checkpoint.ErrCheckpointNotFound +func (r *resumeCheckpointInfoReaderStub) ReadCheckpoint(_ context.Context, checkpointID id.CheckpointID) (*checkpoint.CheckpointSummary, error) { + return r.summaries[checkpointID], nil } -func (r *resumeCheckpointInfoReaderStub) ReadSessionMetadata(_ context.Context, checkpointID id.CheckpointID, sessionIndex int) (*checkpoint.CommittedMetadata, error) { +func (r *resumeCheckpointInfoReaderStub) ReadSession(_ context.Context, ref checkpoint.SessionRef, _ ...checkpoint.ReadOption) (*checkpoint.SessionContent, error) { + sessionIndex, ok := ref.SessionIndex() + if !ok { + return nil, checkpoint.ErrCheckpointNotFound + } + + checkpointID := ref.CheckpointID() sessions := r.metadata[checkpointID] if sessionIndex < 0 || sessionIndex >= len(sessions) { return nil, checkpoint.ErrCheckpointNotFound } - return &sessions[sessionIndex], nil + return &checkpoint.SessionContent{Metadata: sessions[sessionIndex]}, nil } func TestReadCheckpointInfoFromStoreUsesLatestSessionMetadata(t *testing.T) { diff --git a/cmd/entire/cli/review_context.go b/cmd/entire/cli/review_context.go index c98888bee6..8cd2d141e0 100644 --- a/cmd/entire/cli/review_context.go +++ b/cmd/entire/cli/review_context.go @@ -28,14 +28,6 @@ const ( reviewContextCommitSeparator = "\x1e" ) -type reviewContextSessionMetadataReader interface { - ReadSessionMetadata(ctx context.Context, checkpointID checkpointid.CheckpointID, sessionIndex int) (*checkpoint.CommittedMetadata, error) -} - -type reviewContextSessionMetadataPromptsReader interface { - ReadSessionMetadataAndPrompts(ctx context.Context, checkpointID checkpointid.CheckpointID, sessionIndex int) (*checkpoint.SessionContent, error) -} - func reviewCheckpointContext(ctx context.Context, worktreeRoot string, scopeBaseRef string) string { committed := reviewCommittedCheckpointContext(ctx, worktreeRoot, scopeBaseRef) inProgress := reviewSessionContextForCurrentHead(ctx, worktreeRoot) @@ -120,7 +112,7 @@ func reviewCommittedCheckpointContext(ctx context.Context, worktreeRoot string, continue } - summary, err := checkpoint.ReadCommittedCheckpoint(ctx, store, cpID) + summary, err := store.ReadCheckpoint(ctx, cpID) if err != nil { lines = append(lines, fmt.Sprintf("- %s: checkpoint metadata unavailable", cpID)) continue @@ -273,7 +265,7 @@ func formatReviewSessionLine(worktreeRoot string, st *session.State) string { func reviewCheckpointDetail( ctx context.Context, - reader checkpoint.CommittedReader, + reader checkpoint.SessionReader, cpID checkpointid.CheckpointID, summary *checkpoint.CheckpointSummary, ) string { @@ -307,14 +299,11 @@ type reviewContextSessionDetail struct { func readReviewContextSessionMetadata( ctx context.Context, - reader checkpoint.CommittedReader, + reader checkpoint.SessionReader, cpID checkpointid.CheckpointID, sessionIndex int, ) (*checkpoint.CommittedMetadata, error) { - if r, ok := reader.(reviewContextSessionMetadataReader); ok { - return r.ReadSessionMetadata(ctx, cpID, sessionIndex) //nolint:wrapcheck // Best-effort prompt context. - } - content, err := reader.ReadSessionContent(ctx, cpID, sessionIndex) + content, err := reader.ReadSession(ctx, checkpoint.SessionIndexRef(cpID, sessionIndex), checkpoint.WithSessionMetadataOnly()) if err != nil { return nil, err //nolint:wrapcheck // Best-effort prompt context. } @@ -326,23 +315,11 @@ func readReviewContextSessionMetadata( func readReviewContextSessionPrompts( ctx context.Context, - reader checkpoint.CommittedReader, + reader checkpoint.SessionReader, cpID checkpointid.CheckpointID, sessionIndex int, ) (string, error) { - if r, ok := reader.(reviewContextSessionMetadataPromptsReader); ok { - content, err := r.ReadSessionMetadataAndPrompts(ctx, cpID, sessionIndex) - if err == nil { - if content == nil { - return "", errors.New("session content is nil") - } - return content.Prompts, nil - } - if !errors.Is(err, checkpoint.ErrCheckpointNotFound) { - return "", err //nolint:wrapcheck // Best-effort prompt context. - } - } - content, err := reader.ReadSessionContent(ctx, cpID, sessionIndex) + content, err := reader.ReadSession(ctx, checkpoint.SessionIndexRef(cpID, sessionIndex), checkpoint.WithSessionMetadataAndPrompts()) if err != nil { return "", err //nolint:wrapcheck // Best-effort prompt context. } diff --git a/cmd/entire/cli/review_context_test.go b/cmd/entire/cli/review_context_test.go index 31411f9f1d..6d4caf3058 100644 --- a/cmd/entire/cli/review_context_test.go +++ b/cmd/entire/cli/review_context_test.go @@ -467,31 +467,15 @@ func (r *countingReviewContextReader) ReadCommitted( return nil, checkpoint.ErrCheckpointNotFound } -func (r *countingReviewContextReader) ReadSessionContent( +func (r *countingReviewContextReader) ReadSession( context.Context, - checkpointid.CheckpointID, - int, -) (*checkpoint.SessionContent, error) { - return &checkpoint.SessionContent{ - Metadata: r.metadata, - Prompts: r.prompts, - }, nil -} - -func (r *countingReviewContextReader) ReadSessionMetadata( - context.Context, - checkpointid.CheckpointID, - int, -) (*checkpoint.CommittedMetadata, error) { - r.metadataCalls++ - return &r.metadata, r.metadataErr -} - -func (r *countingReviewContextReader) ReadSessionMetadataAndPrompts( - context.Context, - checkpointid.CheckpointID, - int, + checkpoint.SessionRef, + ...checkpoint.ReadOption, ) (*checkpoint.SessionContent, error) { + if r.metadataCalls == 0 { + r.metadataCalls++ + return &checkpoint.SessionContent{Metadata: r.metadata}, r.metadataErr + } r.promptCalls++ return &checkpoint.SessionContent{ Metadata: r.metadata, diff --git a/cmd/entire/cli/rewind.go b/cmd/entire/cli/rewind.go index e5bbd6ec78..9f43881fa4 100644 --- a/cmd/entire/cli/rewind.go +++ b/cmd/entire/cli/rewind.go @@ -722,10 +722,12 @@ func restoreSessionTranscriptFromStrategy(ctx context.Context, cpID id.Checkpoin if err != nil { return "", fmt.Errorf("open checkpoint store: %w", err) } - content, returnedSessionID, err := checkpoint.ReadRawSessionLogForCheckpoint(ctx, stores.Primary, cpID) + sessionContent, err := stores.Primary.ReadSession(ctx, checkpoint.LatestSessionRef(cpID)) if err != nil { return "", fmt.Errorf("failed to get session log: %w", err) } + content := sessionContent.Transcript + returnedSessionID := sessionContent.Metadata.SessionID // Use session ID returned from checkpoint if available // Otherwise fall back to the passed-in sessionID diff --git a/cmd/entire/cli/strategy/cleanup.go b/cmd/entire/cli/strategy/cleanup.go index d1568790e3..8ba3509319 100644 --- a/cmd/entire/cli/strategy/cleanup.go +++ b/cmd/entire/cli/strategy/cleanup.go @@ -318,7 +318,7 @@ func ListOrphanedSessionStates(ctx context.Context) ([]CleanupItem, error) { } sessionsWithCheckpoints := make(map[string]bool) - checkpoints, listErr := cpStores.Primary.ListCommitted(ctx) + checkpoints, listErr := cpStores.Primary.ListCheckpoints(ctx) if listErr == nil { for _, cp := range checkpoints { // cp.SessionID is the most-recent session in a multi-session checkpoint; diff --git a/cmd/entire/cli/strategy/common.go b/cmd/entire/cli/strategy/common.go index 64f3ffbbb1..8e06546caf 100644 --- a/cmd/entire/cli/strategy/common.go +++ b/cmd/entire/cli/strategy/common.go @@ -309,7 +309,7 @@ func ListCheckpoints(ctx context.Context) ([]CheckpointInfo, error) { if err != nil { return nil, fmt.Errorf("open checkpoint store: %w", err) } - committed, err := stores.Primary.ListCommitted(ctx) + committed, err := stores.Primary.ListCheckpoints(ctx) if err != nil { return nil, fmt.Errorf("failed to list committed checkpoints: %w", err) } diff --git a/cmd/entire/cli/strategy/manual_commit_condensation.go b/cmd/entire/cli/strategy/manual_commit_condensation.go index 434ad011a3..6f482f4d6a 100644 --- a/cmd/entire/cli/strategy/manual_commit_condensation.go +++ b/cmd/entire/cli/strategy/manual_commit_condensation.go @@ -54,7 +54,7 @@ func (s *ManualCommitStrategy) listCheckpoints(ctx context.Context) ([]Checkpoin return nil, err } - committed, err := store.ListCommitted(ctx) + committed, err := store.ListCheckpoints(ctx) if err != nil { return nil, fmt.Errorf("failed to list committed checkpoints: %w", err) } @@ -76,11 +76,7 @@ func (s *ManualCommitStrategy) getCheckpointLog(ctx context.Context, checkpointI return nil, err } - summary, err := cpkg.ReadCommittedCheckpoint(ctx, store, checkpointID) - if err != nil { - return nil, fmt.Errorf("failed to read checkpoint: %w", err) - } - content, err := cpkg.ReadLatestSessionContent(ctx, store, checkpointID, summary) + content, err := store.ReadSession(ctx, cpkg.LatestSessionRef(checkpointID)) if err != nil { return nil, fmt.Errorf("failed to read checkpoint: %w", err) } @@ -135,7 +131,7 @@ func checkpointStepCount(s *SessionState) int { // CondenseSession condenses a session's shadow branch to permanent storage. // checkpointID is the 12-hex-char value from the Entire-Checkpoint trailer. // Metadata is stored at sharded path: // -// Uses checkpoint.CommittedStore.WriteCommitted for committed storage. +// Uses checkpoint.CommittedStore.WriteSession for committed storage. // // For mid-session commits (no Stop/SaveStep called yet), the shadow branch may not exist. // In this case, data is extracted from the live transcript instead. @@ -290,7 +286,7 @@ func (s *ManualCommitStrategy) CondenseSession(ctx context.Context, repo *git.Re writeV1Start := time.Now() writeCtx, writeCommittedSpan := perf.Start(ctx, "write_committed_v1") - if err := store.WriteCommitted(writeCtx, writeOpts); err != nil { + if err := store.WriteSession(writeCtx, cpkg.SessionIDRef(checkpointID, state.SessionID), writeOpts); err != nil { writeCommittedSpan.RecordError(err) writeCommittedSpan.End() return nil, fmt.Errorf("failed to write checkpoint metadata: %w", err) diff --git a/cmd/entire/cli/strategy/manual_commit_hooks.go b/cmd/entire/cli/strategy/manual_commit_hooks.go index 92f4547985..acd8f0732f 100644 --- a/cmd/entire/cli/strategy/manual_commit_hooks.go +++ b/cmd/entire/cli/strategy/manual_commit_hooks.go @@ -1067,7 +1067,7 @@ func (s *ManualCommitStrategy) updateCombinedAttributionForCheckpoint( } store := stores.Primary - summary, err := store.ReadCommitted(ctx, checkpointID) + summary, err := store.ReadCheckpoint(ctx, checkpointID) if err != nil { return fmt.Errorf("reading checkpoint summary: %w", err) } @@ -1083,10 +1083,11 @@ func (s *ManualCommitStrategy) updateCombinedAttributionForCheckpoint( // Old metadata lacks SaveStepCount → 0 → conservatively skipped, matching prior behavior. agentFiles := make(map[string]struct{}) for i := range len(summary.Sessions) { - metadata, readErr := store.ReadSessionMetadata(ctx, checkpointID, i) - if readErr != nil || metadata == nil { + content, readErr := store.ReadSession(ctx, checkpoint.SessionIndexRef(checkpointID, i), checkpoint.WithSessionMetadataOnly()) + if readErr != nil || content == nil { continue } + metadata := content.Metadata if metadata.SaveStepCount == 0 { continue // Skip sessions that used the filesTouched fallback } @@ -1158,7 +1159,7 @@ func (s *ManualCommitStrategy) updateCombinedAttributionForCheckpoint( slog.Float64("agent_percentage", agentPercentage), ) - if err := store.UpdateCheckpointSummary(ctx, checkpointID, combined); err != nil { + if err := store.UpdateCheckpoint(ctx, checkpointID, checkpoint.WithAttribution(combined)); err != nil { return fmt.Errorf("persisting combined attribution: %w", err) } @@ -2820,17 +2821,12 @@ func (s *ManualCommitStrategy) finalizeAllTurnCheckpoints(ctx context.Context, s continue } - updateOpts := checkpoint.UpdateCommittedOptions{ - CheckpointID: cpID, - SessionID: state.SessionID, - Transcript: redactedTranscript, - Prompts: prompts, - Agent: state.AgentType, - SkillEvents: skillEvents, - PrecomputedBlobs: precomputed, - } - - updateErr := store.UpdateCommitted(ctx, updateOpts) + updateErr := store.UpdateSession(ctx, checkpoint.SessionIDRef(cpID, state.SessionID), + checkpoint.WithTranscript(redactedTranscript, state.AgentType), + checkpoint.WithPrompts(prompts), + checkpoint.WithSkillEvents(skillEvents), + checkpoint.WithPrecomputedTranscriptBlobs(precomputed), + ) if updateErr != nil { logging.Warn(logCtx, "finalize: failed to update checkpoint", slog.String("checkpoint_id", cpIDStr), diff --git a/cmd/entire/cli/strategy/manual_commit_rewind.go b/cmd/entire/cli/strategy/manual_commit_rewind.go index 4e630063b5..5477098cd6 100644 --- a/cmd/entire/cli/strategy/manual_commit_rewind.go +++ b/cmd/entire/cli/strategy/manual_commit_rewind.go @@ -647,7 +647,7 @@ func (s *ManualCommitStrategy) RestoreLogsOnly(ctx context.Context, w, errW io.W return nil, fmt.Errorf("open checkpoint store: %w", err) } store := stores.Primary - summary, err := cpkg.ReadCommittedCheckpoint(ctx, store, point.CheckpointID) + summary, err := store.ReadCheckpoint(ctx, point.CheckpointID) if err != nil { return nil, fmt.Errorf("failed to read checkpoint: %w", err) } @@ -681,7 +681,7 @@ func (s *ManualCommitStrategy) RestoreLogsOnly(ctx context.Context, w, errW io.W // Restore all sessions (oldest to newest, using 0-based indexing) var restored []RestoredSession for i := range totalSessions { - content, readErr := store.ReadSessionContent(ctx, point.CheckpointID, i) + content, readErr := store.ReadSession(ctx, cpkg.SessionIndexRef(point.CheckpointID, i)) if readErr != nil { if !errors.Is(readErr, cpkg.ErrNoTranscript) { fmt.Fprintf(errW, " Warning: failed to read session %d: %v\n", i, readErr) @@ -860,13 +860,13 @@ type SessionRestoreInfo struct { // about each session, including whether local logs have newer timestamps. // repoRoot is used to compute per-session agent directories. // Sessions without agent metadata are skipped (cannot determine target directory). -func (s *ManualCommitStrategy) classifySessionsForRestore(ctx context.Context, repoRoot string, store cpkg.CommittedReader, checkpointID id.CheckpointID, summary *cpkg.CheckpointSummary) []SessionRestoreInfo { +func (s *ManualCommitStrategy) classifySessionsForRestore(ctx context.Context, repoRoot string, store cpkg.SessionReader, checkpointID id.CheckpointID, summary *cpkg.CheckpointSummary) []SessionRestoreInfo { var sessions []SessionRestoreInfo totalSessions := len(summary.Sessions) // Check all sessions (0-based indexing) for i := range totalSessions { - content, err := store.ReadSessionContent(ctx, checkpointID, i) + content, err := store.ReadSession(ctx, cpkg.SessionIndexRef(checkpointID, i)) if err != nil || content == nil || len(content.Transcript) == 0 { continue } From 27b0e0d80df2eae4b007ca1e90bc980516eacc56 Mon Sep 17 00:00:00 2001 From: Sven Pfleiderer Date: Thu, 18 Jun 2026 19:46:10 -0700 Subject: [PATCH 10/11] dedupe committed summary helpers Route legacy summary updates through the session update API and reuse the committed checkpoint reader from attribution code. This keeps committed read/write behavior centralized without changing the public call sites. Entire-Checkpoint: 854458fe9b0a --- cmd/entire/cli/attribution.go | 16 +---- cmd/entire/cli/checkpoint/committed.go | 82 +------------------------- 2 files changed, 2 insertions(+), 96 deletions(-) diff --git a/cmd/entire/cli/attribution.go b/cmd/entire/cli/attribution.go index d644d5299d..598f4541c6 100644 --- a/cmd/entire/cli/attribution.go +++ b/cmd/entire/cli/attribution.go @@ -406,7 +406,7 @@ func (r *attributionResolver) checkpointContext(cpID id.CheckpointID, file strin func (r *attributionResolver) readCheckpointContext(cpID id.CheckpointID, file string) attributionCheckpointContext { ctx := attributionCheckpointContext{CheckpointID: cpID.String()} - summary, err := readAttributionCheckpointSummary(r.ctx, r.store, cpID) + summary, err := checkpoint.ReadCommittedCheckpoint(r.ctx, r.store, cpID) if err != nil && r.fetchOnMiss { if fetched, fetchErr := r.fetchCheckpointContext(cpID, file); fetchErr == nil { return fetched @@ -484,20 +484,6 @@ func (r *attributionResolver) readCheckpointContext(cpID id.CheckpointID, file s return ctx } -func readAttributionCheckpointSummary(ctx context.Context, reader committedCheckpointReader, cpID id.CheckpointID) (*checkpoint.CheckpointSummary, error) { - if err := ctx.Err(); err != nil { - return nil, err //nolint:wrapcheck // Propagating context cancellation - } - summary, err := reader.ReadCheckpoint(ctx, cpID) - if err != nil { - return nil, fmt.Errorf("read committed checkpoint: %w", err) - } - if summary == nil { - return nil, checkpoint.ErrCheckpointNotFound - } - return summary, nil -} - func enrichAttributionLineWithFetch(ctx context.Context, file string, line *attributionLine, checkpoints map[string]attributionCheckpointContext) error { if line == nil || len(line.Candidates) == 0 { return nil diff --git a/cmd/entire/cli/checkpoint/committed.go b/cmd/entire/cli/checkpoint/committed.go index ea1cf6a21e..b102d3e174 100644 --- a/cmd/entire/cli/checkpoint/committed.go +++ b/cmd/entire/cli/checkpoint/committed.go @@ -1350,87 +1350,7 @@ func LookupSessionLog(ctx context.Context, cpID id.CheckpointID) ([]byte, string // UpdateSummary updates the summary field in the latest session's metadata. // Returns ErrCheckpointNotFound if the checkpoint doesn't exist. func (s *GitStore) UpdateSummary(ctx context.Context, checkpointID id.CheckpointID, summary *Summary) error { - if err := ctx.Err(); err != nil { - return err //nolint:wrapcheck // Propagating context cancellation - } - - // Ensure sessions branch exists - if err := s.ensureSessionsBranch(ctx); err != nil { - return fmt.Errorf("failed to ensure sessions branch: %w", err) - } - - // Get branch ref and root tree hash (O(1), no flatten) - parentHash, rootTreeHash, err := s.getSessionsBranchRef() - if err != nil { - return err - } - - // Flatten only the checkpoint subtree - basePath := checkpointID.Path() + "/" - checkpointPath := checkpointID.Path() - entries, err := s.flattenCheckpointEntries(rootTreeHash, checkpointPath) - if err != nil { - return err - } - - // Read root CheckpointSummary to find the latest session - rootMetadataPath := basePath + paths.MetadataFileName - entry, exists := entries[rootMetadataPath] - if !exists { - return ErrCheckpointNotFound - } - - checkpointSummary, err := s.readSummaryFromBlob(entry.Hash) - if err != nil { - return fmt.Errorf("failed to read checkpoint summary: %w", err) - } - - // Find the latest session's metadata path (0-based indexing) - latestIndex := len(checkpointSummary.Sessions) - 1 - sessionMetadataPath := fmt.Sprintf("%s%d/%s", basePath, latestIndex, paths.MetadataFileName) - sessionEntry, exists := entries[sessionMetadataPath] - if !exists { - return fmt.Errorf("session metadata not found at %s", sessionMetadataPath) - } - - // Read and update session metadata - existingMetadata, err := s.readMetadataFromBlob(sessionEntry.Hash) - if err != nil { - return fmt.Errorf("failed to read session metadata: %w", err) - } - - // Update the summary - existingMetadata.Summary = redactSummary(summary) - - // Write updated session metadata - metadataJSON, err := jsonutil.MarshalIndentWithNewline(existingMetadata, "", " ") - if err != nil { - return fmt.Errorf("failed to marshal metadata: %w", err) - } - metadataHash, err := CreateBlobFromContent(s.repo, metadataJSON) - if err != nil { - return fmt.Errorf("failed to create metadata blob: %w", err) - } - entries[sessionMetadataPath] = object.TreeEntry{ - Name: sessionMetadataPath, - Mode: filemode.Regular, - Hash: metadataHash, - } - - // Build checkpoint subtree and splice into root (O(depth) tree surgery) - newTreeHash, err := s.spliceCheckpointSubtree(ctx, rootTreeHash, checkpointID, basePath, entries) - if err != nil { - return err - } - - authorName, authorEmail := GetGitAuthorFromRepo(s.repo) - commitMsg := fmt.Sprintf("Update summary for checkpoint %s (session: %s)", checkpointID, existingMetadata.SessionID) - newCommitHash, err := s.createCommit(ctx, newTreeHash, parentHash, commitMsg, authorName, authorEmail) - if err != nil { - return err - } - - return s.setPrimaryRef(newCommitHash) + return s.UpdateSession(ctx, LatestSessionRef(checkpointID), WithSummary(summary)) } // UpdateCommitted replaces the transcript, prompts, and context for an existing From 9efa5de55cf404d45a74bf206fd7ecea4e6b4b1a Mon Sep 17 00:00:00 2001 From: Sven Pfleiderer Date: Thu, 18 Jun 2026 19:53:10 -0700 Subject: [PATCH 11/11] address committed store feedback Rename the committed checkpoint metadata store interface to avoid the generic Store name. Also fast-path indexed session refs so callers that already know a session index do not re-read the checkpoint summary. Entire-Checkpoint: 7ecf2c5cef5a --- cmd/entire/cli/checkpoint/committed_domain.go | 8 ++++++-- cmd/entire/cli/checkpoint/committed_reader_resolve.go | 2 +- .../cli/checkpoint/committed_reader_resolve_test.go | 11 +++++++++++ cmd/entire/cli/checkpoint/store.go | 2 +- 4 files changed, 19 insertions(+), 4 deletions(-) diff --git a/cmd/entire/cli/checkpoint/committed_domain.go b/cmd/entire/cli/checkpoint/committed_domain.go index eaecd5c669..59b2feae9b 100644 --- a/cmd/entire/cli/checkpoint/committed_domain.go +++ b/cmd/entire/cli/checkpoint/committed_domain.go @@ -187,8 +187,8 @@ type Writer interface { UpdateCheckpoint(ctx context.Context, checkpointID id.CheckpointID, opts ...WriteOption) error } -// Store reads and writes committed checkpoint documents. -type Store interface { +// MetadataStore reads and writes committed checkpoint documents. +type MetadataStore interface { Reader Writer } @@ -364,6 +364,10 @@ func (s *GitStore) resolveSessionID(ctx context.Context, ref SessionRef) (string } func (s *GitStore) resolveSessionIndex(ctx context.Context, ref SessionRef) (int, error) { + if ref.sessionRefMode == sessionRefIndex { + return ref.sessionIndex, nil + } + summary, err := s.ReadCheckpoint(ctx, ref.checkpointID) if err != nil { return 0, err diff --git a/cmd/entire/cli/checkpoint/committed_reader_resolve.go b/cmd/entire/cli/checkpoint/committed_reader_resolve.go index b73a83cf79..924201c635 100644 --- a/cmd/entire/cli/checkpoint/committed_reader_resolve.go +++ b/cmd/entire/cli/checkpoint/committed_reader_resolve.go @@ -10,7 +10,7 @@ import ( // CommittedStore provides the production committed checkpoint storage surface. type CommittedStore interface { SessionStore - Store + MetadataStore } // AuthorReader provides optional checkpoint author lookup. diff --git a/cmd/entire/cli/checkpoint/committed_reader_resolve_test.go b/cmd/entire/cli/checkpoint/committed_reader_resolve_test.go index c786af1e46..5085e3057a 100644 --- a/cmd/entire/cli/checkpoint/committed_reader_resolve_test.go +++ b/cmd/entire/cli/checkpoint/committed_reader_resolve_test.go @@ -117,6 +117,17 @@ func TestGitStoreSessionStoreReadsByRef(t *testing.T) { require.Equal(t, cpID, infos[0].CheckpointID) } +func TestResolveSessionIndexUsesIndexRefDirectly(t *testing.T) { + t.Parallel() + + store := &GitStore{} + cpID := id.MustCheckpointID("555555555555") + + sessionIndex, err := store.resolveSessionIndex(context.Background(), SessionIndexRef(cpID, 3)) + require.NoError(t, err) + require.Equal(t, 3, sessionIndex) +} + func TestGitStoreStoresUpdateSpecificSessionAndCheckpoint(t *testing.T) { t.Parallel() diff --git a/cmd/entire/cli/checkpoint/store.go b/cmd/entire/cli/checkpoint/store.go index 882623757b..cd817db7fb 100644 --- a/cmd/entire/cli/checkpoint/store.go +++ b/cmd/entire/cli/checkpoint/store.go @@ -10,7 +10,7 @@ import ( var ( _ CommittedStore = (*GitStore)(nil) _ SessionStore = (*GitStore)(nil) - _ Store = (*GitStore)(nil) + _ MetadataStore = (*GitStore)(nil) _ TemporaryStore = (*GitStore)(nil) _ AuthorReader = (*GitStore)(nil) )