Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 17 additions & 11 deletions cmd/entire/cli/attach.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,11 +170,21 @@ func runAttach(ctx context.Context, w io.Writer, sessionID string, agentName typ
writeOpts.CompactTranscript = compacted
}

if err := store.WriteCommitted(ctx, writeOpts); err != nil {
return fmt.Errorf("failed to write checkpoint: %w", err)
v2Only := settings.IsCheckpointsV2OnlyEnabled(logCtx)
if !v2Only {
if err := store.WriteCommitted(ctx, writeOpts); err != nil {
return fmt.Errorf("failed to write checkpoint: %w", err)
}
}
// IsCheckpointsV2Enabled is true whenever v2Only is true, so this covers both
// the v2-only and dual-write paths. Only v2-only propagates the error.
if settings.IsCheckpointsV2Enabled(logCtx) {
writeAttachCheckpointV2(logCtx, repo, writeOpts)
if err := writeAttachCheckpointV2(logCtx, repo, writeOpts); err != nil {
if v2Only {
return fmt.Errorf("failed to write checkpoint to v2: %w", err)
}
logging.Warn(logCtx, "attach v2 dual-write failed", "error", err)
}
}

// Create or update session state.
Expand All @@ -198,17 +208,13 @@ func runAttach(ctx context.Context, w io.Writer, sessionID string, agentName typ
return nil
}

// writeAttachCheckpointV2 mirrors attach-created checkpoints into the v2 refs.
// The caller is responsible for checking whether checkpoints_v2 is enabled.
// v2 failures are logged and do not fail attach.
func writeAttachCheckpointV2(ctx context.Context, repo *git.Repository, opts cpkg.WriteCommittedOptions) {
// 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, strategy.ResolveCheckpointURL(ctx, "origin"))
if err := v2Store.WriteCommitted(ctx, opts); err != nil {
logging.Warn(ctx, "attach v2 dual-write failed",
"checkpoint_id", opts.CheckpointID.String(),
"error", err,
)
return fmt.Errorf("v2 write committed: %w", err)
}
return nil
}

// getHeadCommit returns the HEAD commit object.
Expand Down
27 changes: 27 additions & 0 deletions cmd/entire/cli/checkpoint/v2_read.go
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,33 @@ func readTranscriptFromObjectTree(tree *object.Tree, agentType types.AgentType)
return nil, nil
}

// ReadSessionContentByID finds the session with the given sessionID in a checkpoint
// and returns its content. Mirrors GitStore.ReadSessionContentByID for v2 refs.
// Returns ErrCheckpointNotFound if the checkpoint doesn't exist; returns a
// non-wrapped error (containing the session ID and checkpoint ID for context)
// if no session in the checkpoint matches sessionID.
func (s *V2GitStore) ReadSessionContentByID(ctx context.Context, checkpointID id.CheckpointID, sessionID string) (*SessionContent, error) {
summary, err := s.ReadCommitted(ctx, checkpointID)
if err != nil {
return nil, err
}
if summary == nil {
return nil, ErrCheckpointNotFound
}

for i := range summary.Sessions {
content, readErr := s.ReadSessionContent(ctx, checkpointID, i)
if readErr != nil {
continue
}
if content != nil && content.Metadata.SessionID == sessionID {
return content, nil
}
}

return nil, fmt.Errorf("session %q not found in checkpoint %s", sessionID, checkpointID)
}

// GetSessionLog reads the latest session's raw transcript and session ID from v2 refs.
// Convenience wrapper matching the GitStore.GetSessionLog signature.
func (s *V2GitStore) GetSessionLog(ctx context.Context, cpID id.CheckpointID) ([]byte, string, error) {
Expand Down
1 change: 1 addition & 0 deletions cmd/entire/cli/hooks_git_cmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,7 @@ func TestHooksGitCmd_ExposesPostRewriteSubcommand(t *testing.T) {
}
if found == nil {
t.Fatal("expected post-rewrite subcommand, got nil")
return
}
if found.Use != "post-rewrite <rewrite-type>" {
t.Fatalf("post-rewrite Use = %q, want %q", found.Use, "post-rewrite <rewrite-type>")
Expand Down
49 changes: 49 additions & 0 deletions cmd/entire/cli/integration_test/v2_dual_write_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -244,3 +244,52 @@ func TestV2DualWrite_StopTimeFinalization(t *testing.T) {
require.True(t, found, "transcript.jsonl should exist on v2 /main after finalization")
assert.Contains(t, compactTranscript, `"v":1`)
}

// TestV2Only_SkipsV1Write verifies the v2-only specific deltas: v1 metadata is
// not written and v2 refs still exist. The full v2 payload shape is already
// covered by TestV2DualWrite_FullWorkflow.
func TestV2Only_SkipsV1Write(t *testing.T) {
t.Parallel()
env := NewTestEnv(t)
defer env.Cleanup()

env.InitRepo()
env.WriteFile("README.md", "# Test")
env.WriteFile(".gitignore", ".entire/\n")
env.GitAdd("README.md")
env.GitAdd(".gitignore")
env.GitCommit("Initial commit")
env.GitCheckoutNewBranch("feature/v2-only-test")

env.InitEntireWithOptions(map[string]any{
"checkpoints_v2_only": true,
})

session := env.NewSession()
require.NoError(t, env.SimulateUserPromptSubmitWithPrompt(session.ID, "Add greeting function"))

env.WriteFile("greet.go", "package main\n\nfunc Greet() string { return \"hello\" }")
session.CreateTranscript(
"Add greeting function",
[]FileChange{{Path: "greet.go", Content: "package main\n\nfunc Greet() string { return \"hello\" }"}},
)
require.NoError(t, env.SimulateStop(session.ID, session.TranscriptPath))

env.GitCommitWithShadowHooks("Add greeting function", "greet.go")

cpIDStr := env.GetLatestCheckpointIDFromHistory()
require.NotEmpty(t, cpIDStr, "checkpoint ID should be in commit trailer")

cpID, err := id.NewCheckpointID(cpIDStr)
require.NoError(t, err)
cpPath := cpID.Path()

// v1: should NOT be written.
_, found := env.ReadFileFromBranch(paths.MetadataBranchName, cpPath+"/"+paths.MetadataFileName)
assert.False(t, found,
"v1 committed checkpoint metadata should NOT exist when checkpoints_v2_only is enabled")

// v2: smoke check that the checkpoint still landed.
assert.True(t, env.RefExists(paths.V2MainRefName), "v2 /main ref should exist")
assert.True(t, env.RefExists(paths.V2FullCurrentRefName), "v2 /full/current ref should exist")
}
44 changes: 44 additions & 0 deletions cmd/entire/cli/integration_test/v2_push_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,3 +123,47 @@ func TestV2Push_Disabled_NoV2Refs(t *testing.T) {
assert.True(t, bareRefExists(t, bareDir, "refs/heads/"+paths.MetadataBranchName),
"v1 metadata branch should still exist on remote")
}

// TestV2Push_V2OnlySkipsV1Branch verifies that the v1 metadata branch is not
// pushed when checkpoints_v2_only is enabled; v2 ref pushing itself is covered
// by TestV2Push_FullCycle.
func TestV2Push_V2OnlySkipsV1Branch(t *testing.T) {
t.Parallel()
env := NewTestEnv(t)
defer env.Cleanup()

env.InitRepo()
env.WriteFile("README.md", "# Test")
env.WriteFile(".gitignore", ".entire/\n")
env.GitAdd("README.md")
env.GitAdd(".gitignore")
env.GitCommit("Initial commit")
env.GitCheckoutNewBranch("feature/v2-only-push-test")

env.InitEntireWithOptions(map[string]any{
"checkpoints_v2_only": true,
})

bareDir := env.SetupBareRemote()

session := env.NewSession()
require.NoError(t, env.SimulateUserPromptSubmitWithPrompt(session.ID, "Add feature"))

env.WriteFile("feature.go", "package main\n\nfunc Feature() {}")
session.CreateTranscript(
"Add feature",
[]FileChange{{Path: "feature.go", Content: "package main\n\nfunc Feature() {}"}},
)
require.NoError(t, env.SimulateStop(session.ID, session.TranscriptPath))

env.GitAdd("feature.go")
env.GitCommitWithShadowHooks("Add feature")

env.RunPrePush("origin")

assert.False(t, bareRefExists(t, bareDir, "refs/heads/"+paths.MetadataBranchName),
"v1 metadata branch should NOT exist on remote when checkpoints_v2_only is enabled")
// Smoke: v2 refs still land; full payload asserted in TestV2Push_FullCycle.
assert.True(t, bareRefExists(t, bareDir, paths.V2MainRefName),
"v2 /main ref should exist on remote after push")
}
32 changes: 29 additions & 3 deletions cmd/entire/cli/settings/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -567,6 +567,16 @@ func IsCheckpointsV2Enabled(ctx context.Context) bool {
return settings.IsCheckpointsV2Enabled()
}

// IsCheckpointsV2OnlyEnabled checks if checkpoints should be written and pushed
// only via v2 refs.
func IsCheckpointsV2OnlyEnabled(ctx context.Context) bool {
s, err := Load(ctx)
if err != nil {
return false
}
return s.IsCheckpointsV2OnlyEnabled()
}

// IsPushV2RefsEnabled checks if pushing v2 refs is enabled in settings.
// Returns false by default if settings cannot be loaded or flags are missing.
func IsPushV2RefsEnabled(ctx context.Context) bool {
Expand Down Expand Up @@ -658,19 +668,35 @@ func (s *EntireSettings) GetCheckpointRemote() *CheckpointRemoteConfig {
return &CheckpointRemoteConfig{Provider: provider, Repo: repo}
}

// IsCheckpointsV2Enabled checks if checkpoints v2 (dual-write to refs/entire/) is enabled.
// Returns false by default if the key is missing or not a bool.
// IsCheckpointsV2Enabled checks if checkpoints v2 is enabled.
// Returns true when either checkpoints_v2 or checkpoints_v2_only is enabled.
func (s *EntireSettings) IsCheckpointsV2Enabled() bool {
if s.IsCheckpointsV2OnlyEnabled() {
return true
}
if s.StrategyOptions == nil {
return false
}
val, ok := s.StrategyOptions["checkpoints_v2"].(bool)
return ok && val
}

// IsCheckpointsV2OnlyEnabled checks if checkpoints should be written and pushed
// only via v2 refs, with no v1 dual-write.
func (s *EntireSettings) IsCheckpointsV2OnlyEnabled() bool {
if s.StrategyOptions == nil {
return false
}
val, ok := s.StrategyOptions["checkpoints_v2_only"].(bool)
return ok && val
}

// IsPushV2RefsEnabled checks if pushing v2 refs is enabled.
// Requires both checkpoints_v2 and push_v2_refs to be true.
// checkpoints_v2_only forces v2 ref pushes on, regardless of push_v2_refs.
func (s *EntireSettings) IsPushV2RefsEnabled() bool {
if s.IsCheckpointsV2OnlyEnabled() {
return true
}
if !s.IsCheckpointsV2Enabled() {
return false
}
Expand Down
42 changes: 42 additions & 0 deletions cmd/entire/cli/settings/settings_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -688,6 +688,17 @@ func TestIsCheckpointsV2Enabled_True(t *testing.T) {
}
}

func TestIsCheckpointsV2Enabled_V2Only(t *testing.T) {
t.Parallel()
s := &EntireSettings{
Enabled: true,
StrategyOptions: map[string]any{"checkpoints_v2_only": true},
}
if !s.IsCheckpointsV2Enabled() {
t.Error("expected IsCheckpointsV2Enabled to be true when checkpoints_v2_only is enabled")
}
}

func TestIsCheckpointsV2Enabled_ExplicitlyFalse(t *testing.T) {
t.Parallel()
s := &EntireSettings{
Expand Down Expand Up @@ -773,6 +784,36 @@ func TestIsCheckpointsV2Enabled_LocalOverride(t *testing.T) {
}
}

func TestIsCheckpointsV2OnlyEnabled_DefaultsFalse(t *testing.T) {
t.Parallel()
s := &EntireSettings{Enabled: true}
if s.IsCheckpointsV2OnlyEnabled() {
t.Error("expected IsCheckpointsV2OnlyEnabled to default to false")
}
}

func TestIsCheckpointsV2OnlyEnabled_True(t *testing.T) {
t.Parallel()
s := &EntireSettings{
Enabled: true,
StrategyOptions: map[string]any{"checkpoints_v2_only": true},
}
if !s.IsCheckpointsV2OnlyEnabled() {
t.Error("expected IsCheckpointsV2OnlyEnabled to be true")
}
}

func TestIsCheckpointsV2OnlyEnabled_WrongType(t *testing.T) {
t.Parallel()
s := &EntireSettings{
Enabled: true,
StrategyOptions: map[string]any{"checkpoints_v2_only": "yes"},
}
if s.IsCheckpointsV2OnlyEnabled() {
t.Error("expected IsCheckpointsV2OnlyEnabled to be false for non-bool value")
}
}

func TestIsPushV2RefsEnabled_DefaultsFalse(t *testing.T) {
t.Parallel()
s := &EntireSettings{Enabled: true}
Expand All @@ -789,6 +830,7 @@ func TestIsPushV2RefsEnabled_RequiresBothFlags(t *testing.T) {
opts map[string]any
expected bool
}{
{"v2 only supersedes both", map[string]any{"checkpoints_v2": false, "push_v2_refs": false, "checkpoints_v2_only": true}, true},
{"both true", map[string]any{"checkpoints_v2": true, "push_v2_refs": true}, true},
{"only checkpoints_v2", map[string]any{"checkpoints_v2": true}, false},
{"only push_v2_refs", map[string]any{"push_v2_refs": true}, false},
Expand Down
22 changes: 13 additions & 9 deletions cmd/entire/cli/strategy/checkpoint_remote.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,17 +119,21 @@ func resolvePushSettings(ctx context.Context, pushRemoteName string) pushSetting

ps.checkpointURL = checkpointURL

// If the checkpoint branch doesn't exist locally, try to fetch it from the URL.
// This is a one-time operation — once the branch exists locally, subsequent pushes
// skip the fetch entirely. Only fetch the metadata branch; trails are always pushed
// to the user's push remote, not the checkpoint remote.
if err := fetchMetadataBranchIfMissing(ctx, checkpointURL); err != nil {
logging.Warn(ctx, "checkpoint-remote: failed to fetch metadata branch",
slog.String("error", err.Error()),
)
// Skip the v1 metadata-branch fetch entirely in v2-only mode — there is no
// v1 branch being written or pushed, so there is nothing to sync.
if !s.IsCheckpointsV2OnlyEnabled() {
// If the v1 checkpoint branch doesn't exist locally, try to fetch it from the URL.
// This is a one-time operation — once the branch exists locally, subsequent pushes
// skip the fetch entirely. Only fetch the metadata branch; trails are always pushed
// to the user's push remote, not the checkpoint remote.
if err := fetchMetadataBranchIfMissing(ctx, checkpointURL); err != nil {
logging.Warn(ctx, "checkpoint-remote: failed to fetch metadata branch",
slog.String("error", err.Error()),
)
}
}

// Also fetch v2 /main ref if push_v2_refs is enabled
// Also fetch v2 /main ref if v2 refs are enabled
if s.IsPushV2RefsEnabled() {
if err := fetchV2MainRefIfMissing(ctx, checkpointURL); err != nil {
logging.Warn(ctx, "checkpoint-remote: failed to fetch v2 /main ref",
Expand Down
Loading
Loading