Skip to content
83 changes: 16 additions & 67 deletions cmd/entire/cli/attach.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import (
"github.com/entireio/cli/cmd/entire/cli/logging"
"github.com/entireio/cli/cmd/entire/cli/paths"
"github.com/entireio/cli/cmd/entire/cli/session"
"github.com/entireio/cli/cmd/entire/cli/settings"
"github.com/entireio/cli/cmd/entire/cli/strategy"
"github.com/entireio/cli/cmd/entire/cli/trailers"
"github.com/entireio/cli/cmd/entire/cli/transcript/compact"
Expand Down Expand Up @@ -298,22 +297,8 @@ func runAttach(ctx context.Context, w io.Writer, sessionID string, agentName typ
writeOpts.CompactTranscript = compacted
}

v2 := settings.CheckpointsVersion(logCtx) == 2
if !v2 {
if err := store.WriteCommitted(ctx, writeOpts); err != nil {
return fmt.Errorf("failed to write checkpoint: %w", err)
}
}
// IsCheckpointsV2Enabled is true whenever v2 writes are enabled, including
// both v2-only mode (checkpoints_version == 2) and dual-write mode. Only
// v2-only mode propagates the error.
if settings.IsCheckpointsV2Enabled(logCtx) {
if err := writeAttachCheckpointV2(logCtx, repo, writeOpts); err != nil {
if v2 {
return fmt.Errorf("failed to write checkpoint to v2: %w", err)
}
logging.Warn(logCtx, "attach v2 dual-write failed", "error", err)
}
if err := store.WriteCommitted(ctx, writeOpts); err != nil {
return fmt.Errorf("failed to write checkpoint: %w", err)
}

// Create or update session state.
Expand All @@ -337,15 +322,6 @@ func runAttach(ctx context.Context, w io.Writer, sessionID string, agentName typ
return nil
}

// writeAttachCheckpointV2 writes attach-created checkpoints into the v2 refs.
func writeAttachCheckpointV2(ctx context.Context, repo *git.Repository, opts cpkg.WriteCommittedOptions) error {
v2Store := cpkg.NewV2GitStore(repo)
if err := v2Store.WriteCommitted(ctx, opts); err != nil {
return fmt.Errorf("v2 write committed: %w", err)
}
return nil
}

// getHeadCommit returns the HEAD commit object.
func getHeadCommit(repo *git.Repository) (*object.Commit, error) {
headRef, err := repo.Head()
Expand Down Expand Up @@ -378,9 +354,7 @@ func ensureCheckpointAvailable(ctx, logCtx context.Context, repo *git.Repository
return repo, nil
}

v2Only := settings.CheckpointsVersion(logCtx) == 2

present, readErr := checkpointPresentLocally(ctx, repo, checkpointID, v2Only)
present, readErr := checkpointPresentLocally(ctx, repo, checkpointID)
if readErr != nil {
return repo, fmt.Errorf("failed to read checkpoint %s: %w", checkpointID, readErr)
}
Expand All @@ -389,16 +363,14 @@ func ensureCheckpointAvailable(ctx, logCtx context.Context, repo *git.Repository
}

// Missing locally — try to refresh, then re-check. Use the same fetch
// chain `entire resume` uses for the active storage version (v2 refs live
// under refs/entire/, not refs/heads/, so v1 and v2 need different
// refspecs).
freshRepo, fetchErr := refreshCheckpointRefs(ctx, v2Only)
// chain `entire resume` uses for the v1 metadata branch.
freshRepo, fetchErr := refreshCheckpointRefs(ctx)
if fetchErr != nil {
logging.Warn(logCtx, "failed to refresh metadata branch before attach; proceeding with local state",
slog.String("error", fetchErr.Error()))
} else {
repo = freshRepo
present, readErr = checkpointPresentLocally(ctx, repo, checkpointID, v2Only)
present, readErr = checkpointPresentLocally(ctx, repo, checkpointID)
if readErr != nil {
return repo, fmt.Errorf("failed to read checkpoint %s after refresh: %w", checkpointID, readErr)
}
Expand All @@ -408,42 +380,24 @@ func ensureCheckpointAvailable(ctx, logCtx context.Context, repo *git.Repository
}

branchDescription := "entire/checkpoints/v1 branch"
if v2Only {
branchDescription = "v2 /main ref"
}
return repo, fmt.Errorf(
"checkpoint %s referenced by HEAD is missing from the local %s after a refresh attempt. Creating a fresh checkpoint here would overwrite the original session data on push. Run:\n\n %s\n\nthen re-run attach. If the colleague who made this commit hasn't pushed their checkpoint metadata yet, ask them to do so first",
checkpointID.String(), branchDescription, suggestCheckpointFetchCommand(logCtx, v2Only),
checkpointID.String(), branchDescription, suggestCheckpointFetchCommand(logCtx),
)
}

// refreshCheckpointRefs runs the resume-equivalent fetch chain for the storage
// version we're about to write to. Returns a freshly-opened repo so go-git
// sees any newly-fetched packfiles and ref updates.
func refreshCheckpointRefs(ctx context.Context, v2Only bool) (*git.Repository, error) {
if v2Only {
_, repo, err := getV2MetadataTree(ctx)
return repo, err
}
// refreshCheckpointRefs runs the resume-equivalent fetch chain for the v1
// metadata branch. Returns a freshly-opened repo so go-git sees any
// newly-fetched packfiles and ref updates.
func refreshCheckpointRefs(ctx context.Context) (*git.Repository, error) {
_, repo, err := getMetadataTree(ctx)
return repo, err
}

// checkpointPresentLocally reports whether the checkpoint already exists on
// the local ref we would write to. For v1 / dual-write, that's the local
// entire/checkpoints/v1 branch (remote-tracking alone is not enough — see
// ensureCheckpointAvailable). For v2-only mode, it's the v2 /main ref, which
// has no remote-tracking analog and is therefore already local-only by
// construction.
func checkpointPresentLocally(ctx context.Context, repo *git.Repository, checkpointID id.CheckpointID, v2Only bool) (bool, error) {
if v2Only {
summary, err := cpkg.NewV2GitStore(repo).ReadCommitted(ctx, checkpointID)
if err != nil {
return false, err //nolint:wrapcheck // Caller wraps with checkpoint ID context
}
return summary != nil, nil
}

// the local v1 ref we would write to. Remote-tracking alone is not enough;
// see ensureCheckpointAvailable.
func checkpointPresentLocally(ctx context.Context, repo *git.Repository, checkpointID id.CheckpointID) (bool, error) {
localRef := plumbing.NewBranchReferenceName(paths.MetadataBranchName)
if _, err := repo.Reference(localRef, true); err != nil {
// Local branch ref doesn't exist — treat as "not present locally".
Expand All @@ -459,14 +413,9 @@ func checkpointPresentLocally(ctx context.Context, repo *git.Repository, checkpo
}

// suggestCheckpointFetchCommand returns a git fetch command the user can
// paste to pull the missing metadata ref. v2 refs live under refs/entire/
// (not refs/heads/), so they need an explicit fully-qualified refspec;
// v1 lives on a regular branch and its short name is enough.
func suggestCheckpointFetchCommand(ctx context.Context, v2Only bool) string {
// paste to pull the missing v1 metadata branch.
func suggestCheckpointFetchCommand(ctx context.Context) string {
ref := "entire/checkpoints/v1:entire/checkpoints/v1"
if v2Only {
ref = paths.V2MainRefName + ":" + paths.V2MainRefName
}
if remote.Configured(ctx) {
if url, err := remote.FetchURL(ctx); err == nil && url != "" {
return fmt.Sprintf("git fetch %s %s", url, ref)
Expand Down
147 changes: 20 additions & 127 deletions cmd/entire/cli/attach_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -299,66 +299,16 @@ func TestAttach_OutputContainsCheckpointID(t *testing.T) {
}
}

func TestAttach_V2DualWriteEnabled(t *testing.T) {
setupAttachTestRepo(t)

repoDir := mustGetwd(t)
setAttachCheckpointsV2Enabled(t, repoDir)

sessionID := "test-attach-v2-dual-write"
setupClaudeTranscript(t, sessionID, `{"type":"user","message":{"role":"user","content":"create hello.txt"},"uuid":"uuid-1"}
{"type":"assistant","message":{"role":"assistant","content":[{"type":"tool_use","id":"tu_1","name":"Write","input":{"file_path":"hello.txt","content":"hello"}}]},"uuid":"uuid-2"}
{"type":"user","message":{"content":[{"type":"tool_result","tool_use_id":"tu_1","content":"wrote file"}]},"uuid":"uuid-3"}
{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"Done."}]},"uuid":"uuid-4"}
`)

var out bytes.Buffer
if err := runAttach(context.Background(), &out, sessionID, agent.AgentNameClaudeCode, attachOptions{Force: true}); err != nil {
t.Fatalf("runAttach failed: %v", err)
}

store, err := session.NewStateStore(context.Background())
if err != nil {
t.Fatal(err)
}
state, err := store.Load(context.Background(), sessionID)
if err != nil {
t.Fatal(err)
}
if state == nil || state.LastCheckpointID.IsEmpty() {
t.Fatal("expected attach to persist a checkpoint ID")
}

repo, err := git.PlainOpen(repoDir)
if err != nil {
t.Fatal(err)
}

cpPath := state.LastCheckpointID.Path()
mainCompact, found := readFileFromRef(t, repo, paths.V2MainRefName, cpPath+"/0/"+paths.CompactTranscriptFileName)
if !found {
t.Fatalf("expected %s on %s", paths.CompactTranscriptFileName, paths.V2MainRefName)
}
if !strings.Contains(mainCompact, "create hello.txt") {
t.Errorf("compact transcript missing prompt, got:\n%s", mainCompact)
}

fullTranscript, found := readFileFromRef(t, repo, paths.V2FullCurrentRefName, cpPath+"/0/"+paths.V2RawTranscriptFileName)
if !found {
t.Fatalf("expected %s on %s", paths.V2RawTranscriptFileName, paths.V2FullCurrentRefName)
}
if !strings.Contains(fullTranscript, "hello.txt") {
t.Errorf("raw transcript missing file content, got:\n%s", fullTranscript)
}
}

func TestAttach_CheckpointsVersion2(t *testing.T) {
// TestAttach_CheckpointsVersion2_FallsBackToV1 verifies that
// strategy_options.checkpoints_version: 2 is now ignored — attach writes
// v1 metadata only, and never creates v2 refs solely because of the setting.
func TestAttach_CheckpointsVersion2_FallsBackToV1(t *testing.T) {
setupAttachTestRepo(t)

repoDir := mustGetwd(t)
setAttachCheckpointsV2Only(t, repoDir)

sessionID := "test-attach-v2-only"
sessionID := "test-attach-v2-disallowed"
setupClaudeTranscript(t, sessionID, `{"type":"user","message":{"role":"user","content":"create hello.txt"},"uuid":"uuid-1"}
{"type":"assistant","message":{"role":"assistant","content":[{"type":"tool_use","id":"tu_1","name":"Write","input":{"file_path":"hello.txt","content":"hello"}}]},"uuid":"uuid-2"}
{"type":"user","message":{"content":[{"type":"tool_result","tool_use_id":"tu_1","content":"wrote file"}]},"uuid":"uuid-3"}
Expand Down Expand Up @@ -388,53 +338,12 @@ func TestAttach_CheckpointsVersion2(t *testing.T) {
}

cpPath := state.LastCheckpointID.Path()
if _, found := readFileFromRef(t, repo, paths.MetadataBranchName, cpPath+"/"+paths.MetadataFileName); found {
t.Fatalf("did not expect %s metadata for %s when checkpoints_version is 2", paths.MetadataBranchName, cpPath)
}

mainCompact, found := readFileFromRef(t, repo, paths.V2MainRefName, cpPath+"/0/"+paths.CompactTranscriptFileName)
if !found {
t.Fatalf("expected %s on %s", paths.CompactTranscriptFileName, paths.V2MainRefName)
}
if !strings.Contains(mainCompact, "create hello.txt") {
t.Errorf("compact transcript missing prompt, got:\n%s", mainCompact)
}

fullTranscript, found := readFileFromRef(t, repo, paths.V2FullCurrentRefName, cpPath+"/0/"+paths.V2RawTranscriptFileName)
if !found {
t.Fatalf("expected %s on %s", paths.V2RawTranscriptFileName, paths.V2FullCurrentRefName)
}
if !strings.Contains(fullTranscript, "hello.txt") {
t.Errorf("raw transcript missing file content, got:\n%s", fullTranscript)
}
}

func TestAttach_V2DualWriteDisabled(t *testing.T) {
setupAttachTestRepo(t)

repoDir := mustGetwd(t)

sessionID := "test-attach-v2-disabled"
setupClaudeTranscript(t, sessionID, `{"type":"user","message":{"role":"user","content":"create hello.txt"},"uuid":"uuid-1"}
{"type":"assistant","message":{"role":"assistant","content":[{"type":"tool_use","id":"tu_1","name":"Write","input":{"file_path":"hello.txt","content":"hello"}}]},"uuid":"uuid-2"}
{"type":"user","message":{"content":[{"type":"tool_result","tool_use_id":"tu_1","content":"wrote file"}]},"uuid":"uuid-3"}
{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"Done."}]},"uuid":"uuid-4"}
`)

var out bytes.Buffer
if err := runAttach(context.Background(), &out, sessionID, agent.AgentNameClaudeCode, attachOptions{Force: true}); err != nil {
t.Fatalf("runAttach failed: %v", err)
}

repo, err := git.PlainOpen(repoDir)
if err != nil {
t.Fatal(err)
v1Ref := string(plumbing.NewBranchReferenceName(paths.MetadataBranchName))
if _, found := readFileFromRef(t, repo, v1Ref, cpPath+"/"+paths.MetadataFileName); !found {
t.Fatalf("expected v1 metadata at %s for %s — checkpoints_version: 2 should fall back to v1", paths.MetadataBranchName, cpPath)
}
if _, err := repo.Reference(plumbing.ReferenceName(paths.V2MainRefName), true); err == nil {
t.Fatalf("did not expect %s when checkpoints_v2 is disabled", paths.V2MainRefName)
}
if _, err := repo.Reference(plumbing.ReferenceName(paths.V2FullCurrentRefName), true); err == nil {
t.Fatalf("did not expect %s when checkpoints_v2 is disabled", paths.V2FullCurrentRefName)
t.Fatalf("did not expect %s when checkpoints_version: 2 is configured but disallowed", paths.V2MainRefName)
}
}

Expand Down Expand Up @@ -622,36 +531,32 @@ func TestAttach_RefusesWhenCheckpointOnlyInRemoteTrackingRef(t *testing.T) {
}
}

// In v2-only mode, the refuse hint must reference the v2 /main ref and
// its fully-qualified refspec (refs/entire/checkpoints/v2/main lives under
// refs/entire/, not refs/heads/, so a short refspec won't resolve).
func TestAttach_RefuseHint_V2Only(t *testing.T) {
// TestAttach_RefuseHint_CheckpointsVersion2IgnoredFallsBackToV1 verifies that
// strategy_options.checkpoints_version: 2 no longer flips attach into v2-only
// mode — the refuse hint references the v1 metadata branch, not the v2 /main
// ref, because v2 is disallowed and the system falls back to v1.
func TestAttach_RefuseHint_CheckpointsVersion2IgnoredFallsBackToV1(t *testing.T) {
setupAttachTestRepo(t)

repoRoot := mustGetwd(t)
setAttachCheckpointsV2Only(t, repoRoot)

runGitInDir(t, repoRoot, "commit", "--amend", "-m", "init\n\nEntire-Checkpoint: ffffffffeeee")

sessionID := "v2-orphaned-attach"
sessionID := "v2-disallowed-attach"
setupClaudeTranscript(t, sessionID, `{"type":"user","message":{"role":"user","content":"hi"},"uuid":"u1"}
`)

var out bytes.Buffer
err := runAttach(context.Background(), &out, sessionID, agent.AgentNameClaudeCode, attachOptions{Force: true})
if err == nil {
t.Fatal("expected v2-only attach to refuse when checkpoint is missing")
t.Fatal("expected attach to refuse when checkpoint is missing")
}
if !strings.Contains(err.Error(), "missing from the local v2 /main ref") {
t.Errorf("error should describe the v2 /main ref; got: %v", err)
}
v2Refspec := paths.V2MainRefName + ":" + paths.V2MainRefName
if !strings.Contains(err.Error(), v2Refspec) {
t.Errorf("error should include v2 refspec %q; got: %v", v2Refspec, err)
if !strings.Contains(err.Error(), "missing from the local entire/checkpoints/v1 branch") {
t.Errorf("error should describe the v1 branch (v2 is disallowed); got: %v", err)
}
// And must NOT suggest the v1 refspec.
if strings.Contains(err.Error(), "entire/checkpoints/v1:entire/checkpoints/v1") {
t.Errorf("v2-only hint should not reference the v1 branch; got: %v", err)
if !strings.Contains(err.Error(), "entire/checkpoints/v1:entire/checkpoints/v1") {
t.Errorf("error should include v1 refspec; got: %v", err)
}
}

Expand Down Expand Up @@ -1627,18 +1532,6 @@ func enableEntire(t *testing.T, repoDir string) {
}
}

func setAttachCheckpointsV2Enabled(t *testing.T, repoDir string) {
t.Helper()
entireDir := filepath.Join(repoDir, ".entire")
if err := os.MkdirAll(entireDir, 0o750); err != nil {
t.Fatal(err)
}
settingsContent := `{"enabled": true, "strategy_options": {"checkpoints_v2": true}}`
if err := os.WriteFile(filepath.Join(entireDir, "settings.json"), []byte(settingsContent), 0o600); err != nil {
t.Fatal(err)
}
}

func setAttachCheckpointsV2Only(t *testing.T, repoDir string) {
t.Helper()
entireDir := filepath.Join(repoDir, ".entire")
Expand Down
Loading
Loading