fix(daemon): do not retry workers that finished with a final exit code - #1112
PierrunoYT wants to merge 1 commit into
Conversation
The pool stopped retrying only on exit 0 or ExitPermanent (76), so the other codes `zero exec` uses to say a run is over (2 usage, 3 provider, 4 incomplete, 130 interrupted) were treated as crashes and retried up to MaxAttempts with backoff. Exit 4 means the agent already ran, so each retry repeated its side effects, re-billed the provider and appended a second run to the same session stream. Treat those four codes as final: return the worker's exit code with ErrPermanent. Crash-type exits (1, signals) and launch/read errors still retry. Fixes Twigpine#1100 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Copilot review overview
🔵 Needs a closer look
Required repository-wide validation and the issue-approval gate remain outstanding.
Review effort: Balanced
Findings: None
What changed in this PR
Prevents daemon retries for final zero exec outcomes, avoiding repeated side effects and provider charges.
Changes:
- Classifies exit codes 2, 3, 4, and 130 as final.
- Adds regression coverage confirming no retry or duplicated output.
| File | Description |
|---|---|
internal/daemon/pool.go |
Handles final worker exit codes without retrying. |
internal/daemon/pool_test.go |
Tests final-code handling and output behavior. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository: Twigpine/zero/.coderabbit.yaml Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (2)
Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 1 remain after this review. Walkthrough
ChangesPool exit handling
Priority: ➖ Normal Estimated code review effort: 2 (Simple) | ~10 minutes Change: Bug fix · Severity of issue fixed: Medium Merge Risk: ⚪ Minimal · up to The pool now stops retrying the specified completed worker outcomes while preserving their exit codes; no actionable merge risk is evident. Security Architecture ReviewSecurity architecture risk: ⚪ Minimal · up to The change prevents repeated execution after explicitly terminal worker outcomes. The inspected completion path preserves output and cleanup without expanding access or worker privileges. No introduced or worsened security finding was identified. Retained concerns Security review detailsSecurity Blast Radius
Security Findings and Attack Paths
Trust Boundaries and Controls
Resilience and Maintainability Implications
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
Summary
Pool.Runstopped retrying only on exit 0 andExitPermanent(76) and treated everything else, except 75 (tempfail), as a crash to retry up toMaxAttempts(5) with backoff.zero execalso exits 2 (usage), 3 (provider), 4 (incomplete) and 130 (interrupted) (internal/cli/exec.go), all of which mean the run is over. Exit 4 in particular means the agent already ran (edits, shell commands, pushes), so every retry repeated those side effects, re-billed the provider, and appended another run to the same session buffer.This change treats 2, 3, 4 and 130 as final:
Runreturns the worker's exit code with an error wrappingErrPermanentand does not relaunch. Crash-type exits (1, signals) and launch/read errors still retry as before. The codes are duplicated ininternal/daemon/pool.gobecause the daemon cannot import the CLI package; a comment says to keep them in sync.Not included: the issue's "better" option of never retrying once a worker has emitted output. That also changes how a crash after partial output behaves, so I kept this PR to the exit-code mapping.
Linked issue
Fixes #1100
Note: #1100 does not currently carry the
issue-approvedlabel. Opening this anyway at the author's request; it can be held until the issue is approved.Verification
TestPoolRunFinalExitCodesDoNotRetryuses a fake launcher for exit codes 2, 3, 4 and 130, with a second worker queued that would succeed. It asserts one launch, the worker's exit code,ErrPermanent, and that only the first run's output reached the sink. On the old code all four subtests fail (Run err = <nil>, want ErrPermanent, because the retry succeeded); on the new code they pass.go build ./...,go vet ./internal/daemon/,gofmt -l internal/daemon,git diff HEAD --checkclean;go test ./internal/daemon/...passes.go test ./internal/cli/has failures on this Windows machine (for exampleTestRunExecLogsMCPRuntimeCloseError,TestRunProvidersUseEnvDerivedJSONIncludesConfigPath, andTestRunChangesBareRemotePushThenPRUsablewithgit initfailing onNUL). I re-ran three of them with this change stashed and they fail the same way, so they are not caused by it.go test -race(cgo unavailable here), fullgo test ./..., smoke,maketargets. Relying on CI.Checklist
issue-approvedlabel. (not yet)go build ./...,go vet ./..., andgo test ./...pass locally. (build passes; vet and tests only for the daemon packages)gofmtclean.-racewhere relevant). (test added;-racenot run locally)🤖 Generated with Claude Code
Summary by CodeRabbit