fix(executor): stale tmux window no longer pins a shared branch forever - #688
Merged
Conversation
A finished step's executor tmux window outlives the agent that opened it — the pane falls back to a shell and nothing reaps the window. taskIsLive consulted that window regardless of status, so a DONE step read as "still working", releaseBranchFromFinishedHolder refused to reclaim its shared branch, and the next step failed setup with ErrBranchBusy on every attempt. ErrBranchBusy re-queues rather than fails (correct), but with no backoff that is a hot loop at the 2s worker tick: each pass re-enters executeTask and re-logs "Starting task #N". One stuck branch produced ~1,100 passes in 38 minutes and flushed the task's entire history out of the log ring buffer, destroying the only record of the work the step had already done. It also made the task un-parkable by hand: setting it 'blocked' was overwritten by the next tick's re-queue. Two changes: - taskIsLive treats a terminal status (done/archived) as authoritative and stops consulting tmux for it. The window remains the tiebreaker for a non-terminal task, since a 'blocked' step may still have a live agent sitting on a question. - Branch-contention deferrals back off geometrically (5s, capped at 2m) and give up into 'blocked' after 30m, so a branch that never frees surfaces to a human instead of spinning silently. processNextTask gates deferred steps on that backoff, which is what stops the log spam. Also folds the duplicated windowExistsFn override into an e.windowExists helper. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The stall
Task 5128 (
[Verify]step of the Collab Product Network pipeline) finished its work — committed, pushed, opened a PR — then re-queued itself every 2 seconds for 38 minutes.Diagnosis, from the live system:
done, but its tmux windowtask-5124was still open.taskIsLiveconsultstmuxWindowExistsForTaskregardless of status → read the leftover window as "still working".releaseBranchFromFinishedHolderrefused to reclaim →addSourceBranchWorktreereturnedErrBranchBusy.executeTaskre-queued the step. Worker ticker is2 * time.Second. Repeat forever.An executor window routinely outlives its agent: when the agent exits the pane falls back to a plain shell, and nothing reaps the window. So this is the normal end state of a finished step, not an edge case — any completed step can pin its branch this way.
Why the spin made it worse
ErrBranchBusyre-queueing rather than failing is right, but unbounded it is a hot loop. Each pass re-entersexecuteTaskand re-logsStarting task #N. ~1,100 passes flushed the task's entire log history out of the 1000-entry ring buffer — the only record of what the step had actually done was destroyed by its own retries. It also made the task un-parkable by hand: setting itblockedwas overwritten by the next tick's re-queue. Onlycloseescaped.The fix
1. A terminal status is authoritative.
taskIsLivestops consulting tmux fordone/archived. The window stays the tiebreaker for a non-terminal task, since ablockedstep may still have a live agent sitting on a question.2. Deferrals back off and give up. Contention deferrals now back off geometrically (5s → capped at 2m) and park in
blockedafter 30m, so a branch that never frees surfaces to a human instead of spinning silently.processNextTaskgates deferred steps on that backoff — that is what stops the log spam.Also folds the duplicated
windowExistsFnoverride into ane.windowExistshelper.Tests
Four new cases in
shared_branch_test.go, on real git repos like the existing ones:...ReclaimsFromDoneHolderWithStaleTmuxWindow— the regression. Verified it fails without the fix, with the exact production error:...KeepsBranchForBlockedHolderWithLiveWindow— guards against over-correcting; a blocked step with a live window keeps its branch....ReclaimsFromBlockedHolderWithNoWindow— an abandoned blocked holder releases.TestBranchWaitBacksOff/TestBranchWaitGivesUpAfterDeadline— backoff grows, respects the cap, gates the retry, isolates per-task, and gives up on elapsed time rather than attempt count.go vet ./...,go test -race ./...(full repo, incl. the parity harness), andmake lint(0 issues) all pass.Not covered here
The verify step also never signalled completion after its successful run — it sat in
processing, which is what fed it into the retry loop to begin with. That is a separate bug in the pipeline completion handoff and is not addressed by this PR.The
task-5124window is still open on the affected machine; until it is closed, that branch stays pinned under the old binary.🤖 Generated with Claude Code