Skip to content
22 changes: 13 additions & 9 deletions 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 Expand Up @@ -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)
}

Expand Down Expand Up @@ -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
}
}
Expand Down Expand Up @@ -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
Expand Down
20 changes: 17 additions & 3 deletions cmd/entire/cli/attribution.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ type attributionSummary struct {
type attributionResolver struct {
ctx context.Context
repo *git.Repository
store *checkpoint.GitStore
store committedCheckpointReader
fetchOnMiss bool

commitCache map[string]*object.Commit
Expand Down Expand Up @@ -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 := 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
Expand Down Expand Up @@ -484,6 +484,20 @@ 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
Expand Down Expand Up @@ -549,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
}
Expand Down
49 changes: 49 additions & 0 deletions cmd/entire/cli/attribution_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,55 @@ 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) 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) ReadSession(context.Context, checkpoint.SessionRef, ...checkpoint.ReadOption) (*checkpoint.SessionContent, error) {
return s.content, nil
}

func TestAttributionBlameScopesMixedToSessionNotCheckpoint(t *testing.T) {
repoRoot := newAttributionRepo(t)
writeAttributionCheckpoint(t, repoRoot, "a9b2c3d4e5f6", checkpoint.WriteCommittedOptions{
Expand Down
2 changes: 1 addition & 1 deletion cmd/entire/cli/benchutil/benchutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
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
8 changes: 6 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,11 @@ 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)
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.
Expand Down
Loading
Loading