Skip to content
Open
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
17 changes: 15 additions & 2 deletions cmd/entire/cli/strategy/content_overlap.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,10 +186,14 @@ func filesOverlapWithContent(ctx context.Context, repo *git.Repository, shadowBr
func stagedFilesOverlapWithContent(ctx context.Context, repo *git.Repository, shadowTree *object.Tree, stagedFiles, filesTouched []string) bool {
logCtx := logging.WithComponent(ctx, "checkpoint")

// Build set of filesTouched for quick lookup
// Build set of filesTouched for quick lookup.
// Normalize to base filenames as a fallback to handle path format mismatches
// (absolute vs repo-relative) from stale sessions or older CLI versions (#768).
touchedSet := make(map[string]bool)
touchedByBase := make(map[string]bool) // fallback: basename-only lookup
for _, f := range filesTouched {
touchedSet[f] = true
touchedByBase[filepath.Base(f)] = true
}

// Get HEAD tree to determine if files are being modified or newly created
Expand Down Expand Up @@ -232,8 +236,17 @@ func stagedFilesOverlapWithContent(ctx context.Context, repo *git.Repository, sh

// Check each staged file
for _, stagedPath := range stagedFiles {
// Try exact path match first, then fall back to basename match.
// This handles stale sessions or older CLI versions that stored
// absolute paths in FilesTouched (#768).
if !touchedSet[stagedPath] {
continue // Not in filesTouched, skip
if !touchedByBase[filepath.Base(stagedPath)] {
continue // Not in filesTouched by exact or basename, skip
}
logging.Debug(logCtx, "stagedFilesOverlapWithContent: path format mismatch, matched by basename",
slog.String("staged_path", stagedPath),
slog.Any("files_touched", filesTouched),
)
}

// Check if this is a modified file (exists in HEAD) or new file
Expand Down
26 changes: 21 additions & 5 deletions cmd/entire/cli/strategy/manual_commit_git.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,8 @@ func (s *ManualCommitStrategy) SaveStep(ctx context.Context, step StepContext) e
state.PromptAttributions = append(state.PromptAttributions, promptAttr)

// Track touched files (modified, new, and deleted)
state.FilesTouched = mergeFilesTouched(state.FilesTouched, step.ModifiedFiles, step.NewFiles, step.DeletedFiles)
// Normalize paths to repo-relative form to prevent path format mismatches (#768)
state.FilesTouched = mergeFilesTouched(state.WorktreePath, state.FilesTouched, step.ModifiedFiles, step.NewFiles, step.DeletedFiles)

// On first checkpoint, record the transcript identifier for this session
if state.StepCount == 1 {
Expand Down Expand Up @@ -273,7 +274,8 @@ func (s *ManualCommitStrategy) SaveTaskStep(ctx context.Context, step TaskStepCo
}

// Track touched files (modified, new, and deleted)
state.FilesTouched = mergeFilesTouched(state.FilesTouched, step.ModifiedFiles, step.NewFiles, step.DeletedFiles)
// Normalize paths to repo-relative form to prevent path format mismatches (#768)
state.FilesTouched = mergeFilesTouched(state.WorktreePath, state.FilesTouched, step.ModifiedFiles, step.NewFiles, step.DeletedFiles)

// Save updated state
if err := s.saveSessionState(ctx, state); err != nil {
Expand Down Expand Up @@ -315,15 +317,17 @@ func (s *ManualCommitStrategy) SaveTaskStep(ctx context.Context, step TaskStepCo
}

// mergeFilesTouched merges multiple file lists into existing touched files, deduplicating.
func mergeFilesTouched(existing []string, fileLists ...[]string) []string {
// All paths are normalized to repo-relative form using worktreePath to prevent
// path format mismatches between absolute agent paths and repo-relative git paths (#768).
func mergeFilesTouched(worktreePath string, existing []string, fileLists ...[]string) []string {
seen := make(map[string]bool)
for _, f := range existing {
seen[f] = true
seen[normalizeToRepoRelative(f, worktreePath)] = true
}

for _, list := range fileLists {
for _, f := range list {
seen[f] = true
seen[normalizeToRepoRelative(f, worktreePath)] = true
}
}

Expand All @@ -337,6 +341,18 @@ func mergeFilesTouched(existing []string, fileLists ...[]string) []string {
return result
}

// normalizeToRepoRelative converts a file path to repo-relative form.
// If the path is already relative or worktreePath is empty, the path is returned as-is.
func normalizeToRepoRelative(filePath, worktreePath string) string {
if worktreePath == "" || !filepath.IsAbs(filePath) {
return filePath
}
if rel := paths.ToRelativePath(filePath, worktreePath); rel != "" {
return rel
}
return filePath
}

// accumulateTokenUsage adds new token usage to existing accumulated usage.
// If existing is nil, returns a copy of incoming. If incoming is nil, returns existing unchanged.
func accumulateTokenUsage(existing, incoming *agent.TokenUsage) *agent.TokenUsage {
Expand Down