Resuming a task that released its slot (human-parked) re-enters without re-acquiring a slot, pushing in-flight tasks over the cap - #2038
Merged
ryanrborn merged 12 commits intoSep 24, 2026
Conversation
…d-92mx1m) WIP: pure SlotGate.slot_holders/2, Phase hand-off rule, ResumeSlot.admit/2. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s first (bd-92mx1m) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ns or is given up (bd-92mx1m) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… work first (bd-92mx1m) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… cap; force overrides (bd-92mx1m) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…al offers a recorded override (bd-92mx1m) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rigger (bd-92mx1m) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…policy citations (bd-92mx1m) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ryanrborn
deleted the
bugfix/2010-resuming-task-that-released-its-slot
branch
September 24, 2026 17:26
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.
Summary
Resuming a task that had released its worker slot (parked for a human, stopped, completed) used to re-enter it without asking about the cap. On 2026-09-23 (cap 1) that let
arb worker resume bd-3usjdjstart after Autopilot had already admitted bd-d7mfdq into the freed slot, leaving two tasks in flight. The resume gate now checks whether the task holds a slot right now, not who is resuming it.The rule:
Arbiter.Worker.ResumeSlot.admit/2(new), called byDispatch.resume/2andDispatch.resume_session/2. It runs after the resume's own validity checks and before the prior worker is stopped, so a refusal or a deferral leaves the task exactly as it was.SlotGate.slot_holders/2overPhase.annotate/1. When no worker is registered (after a reboot), the task counts as holding if its latest main run was cut off by the restart (:running,:interrupted, or "server restarted"). A resume of such a task passes through uncapped, whatever the caller, which keeps the Scheduler slots count live agent sessions, not worker records; show the issue's real phase instead of "running" after the main agent exits #1969/Autopilot admission should hold a slot per in-flight TASK until its PR merges, not per live agent session (operator rule; builds on SlotGate/Phase from #1969) #1995 no-deadlock guarantee.Snapshot.effective_max_concurrent/2). When the cap is full:resume_origin: :human(the default): refused with{:slot_cap_full, %{cap, holders}}.refusal_message/1names the cap, the tasks holding the slots, and how to force. Withforce_slot: truethe resume goes over the cap, and the override is recorded as aslot_cap_overrideaudit event (Arbiter.Events: actor, cap, holders) and in the worker'smeta[:slot_cap_override].resume_origin: :automatic: deferred. The resume is handed toBoard.Autopilot.defer_resume/4and the caller gets{:ok, %{deferred: true, ...}}. If no scheduler can take the deferral, the resume is refused, never let over the cap.Hand-offs keep the slot. The fix round and the Watchdog auto-resume both fail the author worker only so an automatic round can replace it. Before this change that failed state read as
:waiting_on_you, which releases the slot. Those failures now carrymeta[:slot_handoff](Worker.fail/3,Worker.clear_slot_handoff/1), andPhasereads a failed worker with that marker as:handing_off, which holds the slot. Every give-up arm drops the marker before it pages: budget spent, not converging, dispatch failed, deferrals exhausted. This also closes the short window where the board showed a free slot during a fix-round hand-off.Deferral queue (
Board.Autopilot). Deferred resumes wait in an in-memory queue, oldest first; deferring the same task again replaces its options but keeps its place. On each pass:Board.Drain).Dispatch.resume/2(orresume_session/2) with the caller's own options plusslot_admitted: true.worker_phaseevent towaiting_on_youordoneasks for a pass while a resume is queued.Every
Dispatch.resume/resume_sessioncallerworker_resume(mcp/tools/worker.ex)forcearg (in the catalog schema) overrides; actorcoordinatorPOST /api/workers/:id/resume(worker_controller.ex,resume_session)details: {cap, holders}."force": trueoverrides; actorapiarb worker resume/arb resume(arbiter_cli/cmd/worker.ex)--forceflag sendsforce: true; the 409 message is surfacedboard_live.ex,Worker.resume/1on an:awaitingworker)worker_detail_live.ex)#worker-retry-force-btn), recorded with actordashboardworkers/reconciler.ex)resume_origin: :automatic; restart-interrupted tasks (holding) are resumed first, then released tasks compete for what's left and are deferred if nothing isauto_resume_dispatcher.ex, fromwatchdog.ex)revise_dispatcher.ex)review_gate_fix_round_dispatcher.ex)slot_admitted: true; the scheduler already found the free slotOther touch points:
:resume_deferrersetting atArbiter.Test.StubResumeDeferrer, so no test can queue a real resume on the app's Autopilot.Arbiter.Test.ResumeSlotFixturebuilds the incident onArbiter.TestSandbox, so every agent CLI is stubbed.watchdog.ex/worker.ex, and the guard-registry exemption is updated toWorker.fail/3.Test plan
resume_slot_test.exs, both origins). A ReviewGate fix round spawns at a full cap (dispatch_resume_slot_test.exs, "a ReviewGate fix round spawns at a full cap").slot_gate_no_deadlock_test.exsstill passes. Hand-off held and released:review_gate_fix_round_test.exs,watchdog_test.exs,watchdog_reviewed_sha_test.exs. The release assertions were mutation-checked: removing the release makes them fail.mcp/worker_resume_slot_test.exs), API (api/worker_resume_slot_test.exs, 409 then 201 withforce), CLI (resume_test.exs,--forcebody and 409 surfaced), board (board_live_test.exs), worker detail (worker_detail_resume_slot_test.exs, refusal then override).dispatch_resume_slot_test.exs). Queue order, ahead of Ready, draining while paused, and quiet vs escalated drops (autopilot_deferred_resume_test.exs). The real Autopilot replays a real deferredDispatch.resumeonce a slot frees ("the scheduler replays it for real once a slot frees"). The reconciler resumes restart-interrupted work first (reconciler_test.exs).dispatch_resume_slot_test.exs, "the 2026-09-23 incident").test/arbiter/board,test/arbiter/worker,test/arbiter/workflows,test/arbiter/tasks,reconciler_test.exs,guard_registry_test.exs,review_coverage_design_test.exs,worker_controller_test.exs,worker_detail_live_test.exs,board_live_test.exs.mix precommit: exit 0 (7450 + 1716 + 889 tests, 0 failures), run with the worker'sARB_*env unset.mix credo --strict,mix format --check-formatted,mix deps.unlock --check-unused, sobelow: clean.mix dialyzer: one unskipped warning, atarbiter_cli/cmd/doctor/checks.ex:177(dev_install?guard_fail). That file is untouched by this branch, so the warning predates it.References
🤖 Generated with Claude Code