Skip to content

Commit a19d840

Browse files
peyton-altclaude
andcommitted
fix(strategy): warn when ambiguous worktree sessions block commit linking
When the sibling/parent worktree fallback finds live sessions but they span multiple worktrees, it refuses to guess and the commit silently loses its Entire-Checkpoint linkage with only a DEBUG trace. Surface that refusal at WARN with the candidate worktrees and a pointer to 'entire session adopt' as the explicit remedy. Requested in #1852 alongside the matching fix that landed in #1440. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Entire-Checkpoint: 01KYAQV3Z2X1A2FTS5K5MXTT83
1 parent ec5d9a7 commit a19d840

2 files changed

Lines changed: 92 additions & 2 deletions

File tree

cmd/entire/cli/strategy/manual_commit_session.go

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"errors"
66
"fmt"
7+
"log/slog"
78
"os"
89
"os/exec"
910
"path/filepath"
@@ -13,6 +14,7 @@ import (
1314
"github.com/entireio/cli/cmd/entire/cli/agent/types"
1415
"github.com/entireio/cli/cmd/entire/cli/checkpoint"
1516
"github.com/entireio/cli/cmd/entire/cli/checkpoint/id"
17+
"github.com/entireio/cli/cmd/entire/cli/logging"
1618
"github.com/entireio/cli/cmd/entire/cli/paths"
1719
"github.com/entireio/cli/cmd/entire/cli/session"
1820
"github.com/entireio/cli/cmd/entire/cli/versioninfo"
@@ -221,9 +223,40 @@ func (s *ManualCommitStrategy) findSessionsForWorktree(ctx context.Context, work
221223
}
222224

223225
if len(parentWorktreeMatches) > 0 {
224-
return sessionsFromSingleWorktree(parentWorktreeMatches), nil
226+
matches := sessionsFromSingleWorktree(parentWorktreeMatches)
227+
if matches == nil {
228+
warnAmbiguousWorktreeSessions(ctx, worktreePath, parentWorktreeMatches)
229+
}
230+
return matches, nil
231+
}
232+
matches := sessionsFromSingleWorktree(commonDirMatches)
233+
if matches == nil && len(commonDirMatches) > 0 {
234+
warnAmbiguousWorktreeSessions(ctx, worktreePath, commonDirMatches)
225235
}
226-
return sessionsFromSingleWorktree(commonDirMatches), nil
236+
return matches, nil
237+
}
238+
239+
// warnAmbiguousWorktreeSessions surfaces refused fallback matches: live
240+
// sessions exist in other worktrees of this repo, but they span multiple
241+
// worktrees so no automatic match is safe. Without this warning, commits made
242+
// here silently lose their Entire-Checkpoint linkage (#1852) with only a
243+
// DEBUG-level trace.
244+
func warnAmbiguousWorktreeSessions(ctx context.Context, worktreePath string, candidates []*SessionState) {
245+
logCtx := logging.WithComponent(ctx, "checkpoint")
246+
seen := make(map[string]struct{}, len(candidates))
247+
worktrees := make([]string, 0, len(candidates))
248+
for _, state := range candidates {
249+
if _, ok := seen[state.WorktreePath]; ok {
250+
continue
251+
}
252+
seen[state.WorktreePath] = struct{}{}
253+
worktrees = append(worktrees, state.WorktreePath)
254+
}
255+
logging.Warn(logCtx, "session matching: live sessions in multiple worktrees of this repo match; refusing to guess, so commits here will not be linked — run 'entire session adopt --from <worktree>' to link one explicitly",
256+
slog.String("commit_worktree", worktreePath),
257+
slog.Int("candidate_sessions", len(candidates)),
258+
slog.Any("candidate_worktrees", worktrees),
259+
)
227260
}
228261

229262
// sessionsFromSingleWorktree returns the candidates only when they were all

cmd/entire/cli/strategy/manual_commit_worktree_session_test.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"testing"
99
"time"
1010

11+
"github.com/entireio/cli/cmd/entire/cli/logging"
1112
"github.com/entireio/cli/cmd/entire/cli/paths"
1213
"github.com/entireio/cli/cmd/entire/cli/session"
1314
"github.com/entireio/cli/cmd/entire/cli/testutil"
@@ -323,6 +324,62 @@ func TestGitCommonDirForWorktree_IgnoresHookGitDirEnv(t *testing.T) {
323324
require.Equal(t, filepath.Join(mainDir, ".git"), commonDir)
324325
}
325326

327+
func TestManualCommitStrategy_FindSessionsForWorktree_WarnsOnAmbiguousSiblingSessions(t *testing.T) {
328+
testutil.IsolateGitConfigEnv(t)
329+
ctx := context.Background()
330+
mainDir := setupSessionMatchRepo(t)
331+
firstWorktree := resolvedRemovedTempDir(t)
332+
secondWorktree := resolvedRemovedTempDir(t)
333+
commitWorktree := resolvedRemovedTempDir(t)
334+
createSessionMatchWorktree(t, mainDir, firstWorktree, "first")
335+
t.Cleanup(func() { removeSessionMatchWorktree(mainDir, firstWorktree) })
336+
createSessionMatchWorktree(t, mainDir, secondWorktree, "second")
337+
t.Cleanup(func() { removeSessionMatchWorktree(mainDir, secondWorktree) })
338+
createSessionMatchWorktree(t, mainDir, commitWorktree, "commit")
339+
t.Cleanup(func() { removeSessionMatchWorktree(mainDir, commitWorktree) })
340+
341+
s := &ManualCommitStrategy{}
342+
saveSessionMatchState(ctx, t, s, mainDir, &SessionState{
343+
SessionID: "first-session",
344+
WorktreePath: firstWorktree,
345+
})
346+
saveSessionMatchState(ctx, t, s, mainDir, &SessionState{
347+
SessionID: "second-session",
348+
WorktreePath: secondWorktree,
349+
})
350+
351+
t.Chdir(commitWorktree)
352+
clearSessionMatchCaches()
353+
require.NoError(t, logging.Init(ctx, "warn-test-session"))
354+
t.Cleanup(logging.Close)
355+
356+
finder := &ManualCommitStrategy{}
357+
matching, err := finder.findSessionsForWorktree(ctx, commitWorktree)
358+
require.NoError(t, err)
359+
require.Empty(t, matching)
360+
361+
logging.Close()
362+
logs := readSessionMatchLogs(t, commitWorktree)
363+
require.Contains(t, logs, `"level":"WARN"`, "ambiguous sibling sessions must be surfaced at WARN, not DEBUG")
364+
require.Contains(t, logs, "refusing to guess")
365+
require.Contains(t, logs, "entire session adopt")
366+
}
367+
368+
func readSessionMatchLogs(t *testing.T, repoDir string) string {
369+
t.Helper()
370+
371+
entries, err := filepath.Glob(filepath.Join(repoDir, ".entire", "logs", "*"))
372+
require.NoError(t, err)
373+
require.NotEmpty(t, entries, "expected a log file under .entire/logs")
374+
var combined []byte
375+
for _, entry := range entries {
376+
content, err := os.ReadFile(entry)
377+
require.NoError(t, err)
378+
combined = append(combined, content...)
379+
}
380+
return string(combined)
381+
}
382+
326383
func setupSessionMatchRepo(t *testing.T) string {
327384
t.Helper()
328385

0 commit comments

Comments
 (0)