From 53dbd22dbfe56c631b415a5fddd01fb7c572abd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Salavisa?= Date: Thu, 5 Mar 2026 19:05:41 +0000 Subject: [PATCH] fix: detect changes to already-dirty files in git status polling (#375) --- .../server/process/workspace_monitor.go | 41 ++++++++++++++++--- 1 file changed, 35 insertions(+), 6 deletions(-) diff --git a/apps/backend/internal/agentctl/server/process/workspace_monitor.go b/apps/backend/internal/agentctl/server/process/workspace_monitor.go index 0ad667bf4c..418aa035af 100644 --- a/apps/backend/internal/agentctl/server/process/workspace_monitor.go +++ b/apps/backend/internal/agentctl/server/process/workspace_monitor.go @@ -68,7 +68,7 @@ type workspaceState struct { } // getWorkspaceState returns the current workspace state using fast checks. -// Uses index mtime + git diff-files output + untracked file mtimes. +// Uses index mtime + git diff-files output + file mtimes. func (wt *WorkspaceTracker) getWorkspaceState(ctx context.Context) workspaceState { var state workspaceState @@ -79,13 +79,15 @@ func (wt *WorkspaceTracker) getWorkspaceState(ctx context.Context) workspaceStat } } - // git diff-files shows changed files with their blob hashes - // Output changes whenever file content changes (not just dirty/clean boolean) - cmd := exec.CommandContext(ctx, "git", "diff-files") + // git diff-files shows changed files with their blob hashes. + // However, the output doesn't change when an already-dirty file is modified + // again because the index blob hash stays constant and the worktree hash + // is always shown as 0000000 (not computed). To detect subsequent changes + // to dirty files, we also include the mtime of each dirty file. + cmd := exec.CommandContext(ctx, "git", "diff-files", "--name-only") cmd.Dir = wt.workDir out, _ := cmd.Output() - // Use the raw output as identity - content changes produce different output - state.diffFilesID = string(out) + state.diffFilesID = wt.buildDirtyFilesID(string(out)) // Check untracked files - git diff-files doesn't include them // Use git ls-files to get untracked files, then check their mtimes @@ -94,6 +96,33 @@ func (wt *WorkspaceTracker) getWorkspaceState(ctx context.Context) workspaceStat return state } +// buildDirtyFilesID builds an identifier string for dirty tracked files. +// Includes file paths and their mtimes so subsequent changes are detected. +func (wt *WorkspaceTracker) buildDirtyFilesID(diffFilesOutput string) string { + if diffFilesOutput == "" { + return "" + } + + lines := strings.Split(strings.TrimSpace(diffFilesOutput), "\n") + var hashInput strings.Builder + for _, file := range lines { + if file == "" { + continue + } + hashInput.WriteString(file) + // Include mtime so we detect content changes to already-dirty files + safePath, err := wt.sanitizePath(file) + if err != nil { + continue // Skip files with invalid paths + } + if info, err := os.Stat(safePath); err == nil { + hashInput.WriteString(fmt.Sprintf(":%d", info.ModTime().UnixNano())) + } + hashInput.WriteString(";") + } + return hashInput.String() +} + // getUntrackedFilesID returns a string identifying the current state of untracked files. // Uses file list + mtimes to detect when untracked files are added, removed, or modified. func (wt *WorkspaceTracker) getUntrackedFilesID(ctx context.Context) string {