Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
29 changes: 16 additions & 13 deletions cmd/entire/cli/attach.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,11 +170,21 @@
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,10 @@
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 v2Store.WriteCommitted(ctx, opts)

Check failure on line 214 in cmd/entire/cli/attach.go

View workflow job for this annotation

GitHub Actions / lint

error returned from external package is unwrapped: sig: func (*github.com/entireio/cli/cmd/entire/cli/checkpoint.V2GitStore).WriteCommitted(ctx context.Context, opts github.com/entireio/cli/cmd/entire/cli/checkpoint.WriteCommittedOptions) error (wrapcheck)
}

// getHeadCommit returns the HEAD commit object.
Expand Down
26 changes: 26 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,32 @@ 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 wrapped
Comment thread
computermode marked this conversation as resolved.
Outdated
// error 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
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")
}
49 changes: 46 additions & 3 deletions cmd/entire/cli/settings/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -540,6 +540,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 All @@ -550,6 +560,17 @@ func IsPushV2RefsEnabled(ctx context.Context) bool {
return s.IsPushV2RefsEnabled()
}

// IsCheckpointsV1WriteEnabled reports whether v1 checkpoint writes should still
// happen. Defaults to true (fail-safe: keep writing v1) when settings cannot be
// loaded, so a misconfigured settings file does not silently drop checkpoints.
func IsCheckpointsV1WriteEnabled(ctx context.Context) bool {
s, err := Load(ctx)
if err != nil {
return true
}
return s.IsCheckpointsV1WriteEnabled()
}

// IsFilteredFetchesEnabled checks if filtered fetches should be used.
// When enabled, filtered fetches always resolve remote names to URLs first so
// git does not persist promisor settings onto named remotes in local config.
Expand Down Expand Up @@ -631,19 +652,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 All @@ -654,6 +691,12 @@ func (s *EntireSettings) IsPushV2RefsEnabled() bool {
return ok && val
}

// IsCheckpointsV1WriteEnabled reports whether v1 checkpoint writes should still
// happen. checkpoints_v2_only disables the v1 path entirely.
func (s *EntireSettings) IsCheckpointsV1WriteEnabled() bool {
return !s.IsCheckpointsV2OnlyEnabled()
}

// IsFilteredFetchesEnabled checks if fetches should use --filter=blob:none.
// When enabled, filtered fetches always use resolved URLs rather than remote
// names to avoid persisting promisor settings onto named remotes.
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 @@ -687,6 +687,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 @@ -772,6 +783,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 @@ -788,6 +829,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.IsCheckpointsV1WriteEnabled() {
// 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