Skip to content

Commit 389f1ef

Browse files
wesmclaude
andcommitted
refactor: unified diff file fallback for all agents
Replace the Codex-specific large-diff handling with a single path that works the same for every agent: 1. Worker writes the full diff to a snapshot file before building the prompt 2. Builder inlines the diff if it fits; references the file if it doesn't 3. No agent-specific branching, no prompt parsing, no placeholders Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 7b40338 commit 389f1ef

File tree

5 files changed

+113
-479
lines changed

5 files changed

+113
-479
lines changed

internal/daemon/worker.go

Lines changed: 44 additions & 126 deletions
Original file line numberDiff line numberDiff line change
@@ -422,39 +422,23 @@ func (wp *WorkerPool) processJob(workerID string, job *storage.ReviewJob) {
422422
// Dirty job - use pre-captured diff
423423
reviewPrompt, err = pb.BuildDirty(effectiveRepoPath, *job.DiffContent, job.RepoID, cfg.ReviewContextCount, job.Agent, job.ReviewType)
424424
} else {
425-
// Normal job - build prompt from git ref.
426-
reviewPrompt, err = pb.Build(
427-
effectiveRepoPath, job.GitRef, job.RepoID,
428-
cfg.ReviewContextCount, job.Agent, job.ReviewType,
425+
// Normal job - build prompt from git ref. Write the full
426+
// diff to a snapshot file so the builder can reference it
427+
// when the diff is too large to inline.
428+
excludes := config.ResolveExcludePatterns(
429+
effectiveRepoPath, cfg, job.ReviewType,
429430
)
430-
// If the diff was truncated for a Codex review, write a
431-
// snapshot file so the sandboxed agent can read it directly
432-
// instead of needing git commands.
433-
if err == nil && strings.EqualFold(job.Agent, "codex") &&
434-
strings.Contains(reviewPrompt, prompt.DiffTruncatedHint) {
435-
excludes := config.ResolveExcludePatterns(
436-
effectiveRepoPath, cfg, job.ReviewType,
437-
)
438-
diffFile, cleanup, diffFileErr := prepareDiffFileForCodex(
439-
effectiveRepoPath, job, excludes,
440-
)
441-
if cleanup != nil {
442-
defer cleanup()
443-
}
444-
if diffFile != "" {
445-
reviewPrompt, err = pb.BuildWithDiffFile(
446-
effectiveRepoPath, job.GitRef, job.RepoID,
447-
cfg.ReviewContextCount, job.Agent,
448-
job.ReviewType, diffFile,
449-
)
450-
} else if diffFileErr != nil &&
451-
requiresSandboxedCodexDiffFile(job, cfg) {
452-
err = fmt.Errorf("prepare codex diff file: %w", diffFileErr)
453-
} else if diffFileErr != nil {
454-
log.Printf("[%s] Warning: codex diff file: %v",
455-
workerID, diffFileErr)
456-
}
431+
diffFile, cleanup := prepareDiffFile(
432+
effectiveRepoPath, job, excludes,
433+
)
434+
if cleanup != nil {
435+
defer cleanup()
457436
}
437+
reviewPrompt, err = pb.BuildWithDiffFile(
438+
effectiveRepoPath, job.GitRef, job.RepoID,
439+
cfg.ReviewContextCount, job.Agent,
440+
job.ReviewType, diffFile,
441+
)
458442
}
459443
if err != nil {
460444
log.Printf("[%s] Error building prompt: %v", workerID, err)
@@ -1044,60 +1028,21 @@ func (wp *WorkerPool) failoverOrFail(
10441028
func preparePrebuiltCodexPrompt(
10451029
repoPath string, job *storage.ReviewJob, reviewPrompt string, excludes []string,
10461030
) (string, func(), error) {
1047-
hasPlaceholder := strings.Contains(
1048-
reviewPrompt, prompt.CodexDiffFilePathPlaceholder,
1049-
)
1050-
// Legacy prebuilt prompts (created before diff-file support) won't
1051-
// have the placeholder. For those, only write a diff file when the
1052-
// prompt indicates the diff was truncated.
1053-
if !hasPlaceholder {
1054-
if !strings.EqualFold(job.Agent, "codex") ||
1055-
!strings.Contains(reviewPrompt, prompt.DiffTruncatedHint) {
1056-
return reviewPrompt, nil, nil
1057-
}
1058-
diffFile, cleanup, err := prepareDiffFileForCodex(
1059-
repoPath, job, excludes,
1060-
)
1061-
if err != nil || diffFile == "" {
1062-
if cleanup != nil {
1063-
cleanup()
1064-
}
1065-
// Best-effort for legacy prompts: return as-is so the
1066-
// old git-command fallback can still be attempted.
1067-
return reviewPrompt, nil, nil
1068-
}
1069-
reviewPrompt += fmt.Sprintf(
1070-
"\n\nThe full diff is also available at: `cat %s`\n",
1071-
shellQuoteForPrompt(diffFile),
1072-
)
1073-
return reviewPrompt, cleanup, nil
1074-
}
1075-
1076-
diffFile, cleanup, err := prepareDiffFileForCodex(repoPath, job, excludes)
1077-
if err != nil {
1078-
if cleanup != nil {
1079-
cleanup()
1080-
}
1081-
return "", nil, fmt.Errorf("prepare prebuilt codex prompt diff file: %w", err)
1031+
if !strings.Contains(reviewPrompt, prompt.CodexDiffFilePathPlaceholder) {
1032+
return reviewPrompt, nil, nil
10821033
}
1034+
diffFile, cleanup := prepareDiffFile(repoPath, job, excludes)
10831035
if diffFile == "" {
1084-
if cleanup != nil {
1085-
cleanup()
1086-
}
10871036
return "", nil, fmt.Errorf(
1088-
"prebuilt codex prompt needs diff file but none could be prepared",
1037+
"prebuilt prompt needs diff file but none could be prepared",
10891038
)
10901039
}
1091-
quotedPlaceholder := shellQuoteForPrompt(
1092-
prompt.CodexDiffFilePathPlaceholder,
1093-
)
1094-
reviewPrompt = strings.ReplaceAll(
1095-
reviewPrompt, quotedPlaceholder, shellQuoteForPrompt(diffFile),
1040+
replacer := strings.NewReplacer(
1041+
shellQuoteForPrompt(prompt.CodexDiffFilePathPlaceholder),
1042+
shellQuoteForPrompt(diffFile),
1043+
prompt.CodexDiffFilePathPlaceholder, diffFile,
10961044
)
1097-
reviewPrompt = strings.ReplaceAll(
1098-
reviewPrompt, prompt.CodexDiffFilePathPlaceholder, diffFile,
1099-
)
1100-
return reviewPrompt, cleanup, nil
1045+
return replacer.Replace(reviewPrompt), cleanup, nil
11011046
}
11021047

11031048
func shellQuoteForPrompt(s string) string {
@@ -1107,24 +1052,6 @@ func shellQuoteForPrompt(s string) string {
11071052
return "'" + strings.ReplaceAll(s, "'", `'\''`) + "'"
11081053
}
11091054

1110-
func requiresSandboxedCodexDiffFile(
1111-
job *storage.ReviewJob, cfg *config.Config,
1112-
) bool {
1113-
if !strings.EqualFold(job.Agent, "codex") ||
1114-
job.Agentic || agent.AllowUnsafeAgents() {
1115-
return false
1116-
}
1117-
// Use the same config-aware resolution as the worker to check
1118-
// whether Codex is the effective agent. If it falls back to
1119-
// another agent (e.g., Codex is unavailable or overridden via
1120-
// codex_cmd), the diff file is not required.
1121-
a, err := agent.GetAvailableWithConfig(job.Agent, cfg)
1122-
if err != nil {
1123-
return false
1124-
}
1125-
return strings.EqualFold(a.Name(), "codex")
1126-
}
1127-
11281055
// logJobFailed logs a job failure to the activity log
11291056
func (wp *WorkerPool) logJobFailed(
11301057
jobID int64, workerID, agentName, errorMsg string,
@@ -1144,21 +1071,13 @@ func (wp *WorkerPool) logJobFailed(
11441071
)
11451072
}
11461073

1147-
// prepareDiffFileForCodex writes the full diff for a job into the
1148-
// checkout's git dir so a sandboxed Codex agent can read it without
1149-
// needing git commands or access outside the repository boundary.
1150-
// Returns the file path and a cleanup function, or ("", nil) when no
1151-
// file was written.
1152-
func prepareDiffFileForCodex(
1074+
// prepareDiffFile writes the full diff for a job into the checkout's
1075+
// git dir so the prompt builder can reference it when the diff is too
1076+
// large to inline. Returns the file path and a cleanup function, or
1077+
// ("", nil) on failure.
1078+
func prepareDiffFile(
11531079
repoPath string, job *storage.ReviewJob, excludes []string,
1154-
) (string, func(), error) {
1155-
if !strings.EqualFold(job.Agent, "codex") {
1156-
return "", nil, nil
1157-
}
1158-
// Job-specific agentic mode keeps the existing direct-tooling path.
1159-
if job.Agentic {
1160-
return "", nil, nil
1161-
}
1080+
) (string, func()) {
11621081
var fullDiff string
11631082
var err error
11641083
if gitpkg.IsRange(job.GitRef) {
@@ -1170,27 +1089,26 @@ func prepareDiffFileForCodex(
11701089
repoPath, job.GitRef, excludes...,
11711090
)
11721091
}
1173-
if err != nil {
1174-
return "", nil, fmt.Errorf("capture full diff: %w", err)
1175-
}
1176-
if fullDiff == "" {
1177-
return "", nil, nil
1092+
if err != nil || fullDiff == "" {
1093+
if err != nil {
1094+
log.Printf("Warning: capture diff for snapshot: %v", err)
1095+
}
1096+
return "", nil
11781097
}
11791098
gitDir, err := gitpkg.ResolveGitDir(repoPath)
11801099
if err != nil {
1181-
return "", nil, fmt.Errorf("resolve git dir: %w", err)
1100+
log.Printf("Warning: resolve git dir for diff snapshot: %v", err)
1101+
return "", nil
11821102
}
1183-
if info, err := os.Stat(gitDir); err != nil || !info.IsDir() {
1184-
if err != nil {
1185-
return "", nil, fmt.Errorf("stat git dir: %w", err)
1186-
}
1187-
return "", nil, fmt.Errorf("git dir %s is not a directory", gitDir)
1188-
}
1189-
diffFile := filepath.Join(gitDir, fmt.Sprintf("roborev-review-%d.diff", job.ID))
1103+
diffFile := filepath.Join(
1104+
gitDir,
1105+
fmt.Sprintf("roborev-review-%d.diff", job.ID),
1106+
)
11901107
if err := os.WriteFile(diffFile, []byte(fullDiff), 0o644); err != nil {
1191-
return "", nil, fmt.Errorf("write diff file %s: %w", diffFile, err)
1108+
log.Printf("Warning: write diff snapshot %s: %v", diffFile, err)
1109+
return "", nil
11921110
}
1193-
return diffFile, func() { os.Remove(diffFile) }, nil
1111+
return diffFile, func() { os.Remove(diffFile) }
11941112
}
11951113

11961114
// markCompactSourceJobs marks all source jobs as closed for a completed compact job

internal/daemon/worker_test.go

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"os"
77
"os/exec"
88
"path/filepath"
9+
"runtime"
910

1011
"github.com/roborev-dev/roborev/internal/agent"
1112
"github.com/roborev-dev/roborev/internal/config"
@@ -538,9 +539,8 @@ func TestPrepareDiffFileForCodex_WritesDiffInWorktreeGitDir(t *testing.T) {
538539
sha := strings.TrimSpace(string(shaBytes))
539540

540541
job := &storage.ReviewJob{ID: 42, Agent: "codex", GitRef: sha}
541-
diffFile, cleanup, err := prepareDiffFileForCodex(worktreeDir, job, nil)
542-
require.NoError(t, err)
543-
require.NotEmpty(t, diffFile, "expected diff file for worktree-backed Codex review")
542+
diffFile, cleanup := prepareDiffFile(worktreeDir, job, nil)
543+
require.NotEmpty(t, diffFile, "expected diff file for worktree-backed review")
544544
require.NotNil(t, cleanup)
545545

546546
gitDirBytes, err := exec.Command("git", "-C", worktreeDir, "rev-parse", "--git-dir").Output()
@@ -635,19 +635,19 @@ func TestPreparePrebuiltCodexPrompt_AllowsUnsafeModeByStillWritingDiffFile(t *te
635635
cleanup()
636636
}
637637

638-
func TestProcessJob_SandboxedCodexRetriesWhenDiffSnapshotCannotBeWritten(t *testing.T) {
639-
prevUnsafe := agent.AllowUnsafeAgents()
640-
agent.SetAllowUnsafeAgents(false)
641-
t.Cleanup(func() { agent.SetAllowUnsafeAgents(prevUnsafe) })
638+
func TestProcessJob_DiffSnapshotFailureStillRunsReview(t *testing.T) {
639+
if runtime.GOOS == "windows" {
640+
t.Skip("chmod does not restrict writes on Windows")
641+
}
642642

643643
originalCodex, err := agent.Get("codex")
644644
require.NoError(t, err)
645645

646-
agentCalled := false
646+
var receivedPrompt string
647647
agent.Register(&agent.FakeAgent{
648648
NameStr: "codex",
649-
ReviewFn: func(ctx context.Context, repoPath, commitSHA, reviewPrompt string, output io.Writer) (string, error) {
650-
agentCalled = true
649+
ReviewFn: func(ctx context.Context, repoPath, commitSHA, prompt string, output io.Writer) (string, error) {
650+
receivedPrompt = prompt
651651
return "No issues found.", nil
652652
},
653653
})
@@ -681,6 +681,7 @@ func TestProcessJob_SandboxedCodexRetriesWhenDiffSnapshotCannotBeWritten(t *test
681681
})
682682
require.NoError(t, err)
683683

684+
// Make git dir read-only so diff file write fails
684685
gitDir, err := gitpkg.ResolveGitDir(tc.TmpDir)
685686
require.NoError(t, err)
686687
info, err := os.Stat(gitDir)
@@ -695,11 +696,12 @@ func TestProcessJob_SandboxedCodexRetriesWhenDiffSnapshotCannotBeWritten(t *test
695696

696697
tc.Pool.processJob(testWorkerID, claimed)
697698

698-
tc.assertJobStatus(t, job.ID, storage.JobStatusQueued)
699-
assert.False(t, agentCalled, "Codex should not run when snapshot creation fails in sandboxed mode")
700-
retryCount, err := tc.DB.GetJobRetryCount(job.ID)
701-
require.NoError(t, err)
702-
assert.Equal(t, 1, retryCount)
699+
// Diff file failure is best-effort; the review still runs
700+
// with a truncated-diff prompt
701+
tc.assertJobStatus(t, job.ID, storage.JobStatusDone)
702+
assert.NotEmpty(t, receivedPrompt, "agent should have been called")
703+
assert.Contains(t, receivedPrompt, "Diff too large",
704+
"prompt should indicate diff was truncated")
703705
}
704706

705707
func TestWorkerPoolCancelJobFinalCheckDeadlockSafe(t *testing.T) {

0 commit comments

Comments
 (0)