Skip to content
2 changes: 1 addition & 1 deletion cmd/entire/cli/attach.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
7 changes: 6 additions & 1 deletion cmd/entire/cli/attribution.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Comment thread
pfleidi marked this conversation as resolved.
Outdated

type attributionResolver struct {
ctx context.Context
repo *git.Repository
store *checkpoint.GitStore
store attributionSessionMetadataPromptsReader
fetchOnMiss bool

commitCache map[string]*object.Commit
Expand Down
56 changes: 8 additions & 48 deletions cmd/entire/cli/checkpoint/checkpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -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/<base-commit-short-hash>.
// 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: <id[:2]>/<id[2:]>/
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.
Expand Down
4 changes: 2 additions & 2 deletions cmd/entire/cli/checkpoint/committed.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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)
Comment thread
pfleidi marked this conversation as resolved.
Outdated
}

// UpdateSummary updates the summary field in the latest session's metadata.
Expand Down
20 changes: 20 additions & 0 deletions cmd/entire/cli/checkpoint/committed_reader_resolve.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
19 changes: 9 additions & 10 deletions cmd/entire/cli/checkpoint/open.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
}

Expand All @@ -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 }
7 changes: 5 additions & 2 deletions cmd/entire/cli/checkpoint/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 5 additions & 5 deletions cmd/entire/cli/explain.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion cmd/entire/cli/resume.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
14 changes: 11 additions & 3 deletions cmd/entire/cli/strategy/manual_commit.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/entire/cli/strategy/manual_commit_condensation.go
Original file line number Diff line number Diff line change
Expand Up @@ -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: <checkpoint_id[:2]>/<checkpoint_id[2:]>/
// 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.
Expand Down
10 changes: 6 additions & 4 deletions cmd/entire/cli/strategy/manual_commit_git.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 {
Expand All @@ -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)
Expand Down
7 changes: 4 additions & 3 deletions cmd/entire/cli/strategy/manual_commit_rewind.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,18 +30,19 @@ 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 {
return nil, fmt.Errorf("failed to open git repository: %w", err)
}
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()
Expand All @@ -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)

Expand Down
4 changes: 3 additions & 1 deletion cmd/entire/cli/strategy/manual_commit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}

Expand Down
Loading