Why
The keepalive runner-dispatch debounce is keyed on (head_sha, provider) and records a
dispatch as completed regardless of whether the agent produced anything. When an agent
run exits reporting success but did no work, the key is burned for that head, and the only
thing that can change the head is a commit from the very agent the debounce now refuses to
dispatch. That is a closed latch: clearing it requires the action it forbids.
Observed live on 2026-09-13. stranske/Doc-Lineage #23 and stranske/Manager-Mosaic #22
each had one codex run whose sandbox failed to initialize:
bwrap: loopback: Failed RTM_NEWADDR: Operation not permitted
Codex reported this as a successful run ("Status: ✅ Success", 0 tasks complete, no
commit). Every later dispatch attempt then returned:
{"prior_status": "completed", "reason": "duplicate-completed", "should_dispatch": "false"}
Both PRs sat frozen at iteration 1/12 for four hours while the hourly keepalive sweep ran
past them.
Scope
Fix the keepalive runner-dispatch debounce in .github/workflows/agents-keepalive-loop.yml and its consumer template copy.
Non-goals
- Changing the debounce's purpose. Suppressing genuine duplicate dispatches for the same
head is correct and should stay.
- Fixing the underlying
bwrap sandbox failure. That is a separate defect; this issue is
about the loop's inability to recover from any zero-output run.
Tasks
Acceptance Criteria
Implementation Notes
The debounce decision is emitted by the Check runner dispatch debounce step of the
Evaluate keepalive loop job in .github/workflows/agents-keepalive-loop.yml. The
zero-output signal is already present in the keepalive state JSON the same job writes:
tasks_completed_delta, and the work log's Files/Commit columns.
This is the latched-gate pattern described in CLAUDE.md: the gate's measuring window
(any completed dispatch) is wider than its draining window (a new head commit, which only
a dispatched agent produces).
Why
The keepalive runner-dispatch debounce is keyed on
(head_sha, provider)and records adispatch as
completedregardless of whether the agent produced anything. When an agentrun exits reporting success but did no work, the key is burned for that head, and the only
thing that can change the head is a commit from the very agent the debounce now refuses to
dispatch. That is a closed latch: clearing it requires the action it forbids.
Observed live on 2026-09-13.
stranske/Doc-Lineage#23 andstranske/Manager-Mosaic#22each had one codex run whose sandbox failed to initialize:
Codex reported this as a successful run ("Status: ✅ Success", 0 tasks complete, no
commit). Every later dispatch attempt then returned:
{"prior_status": "completed", "reason": "duplicate-completed", "should_dispatch": "false"}Both PRs sat frozen at iteration 1/12 for four hours while the hourly keepalive sweep ran
past them.
Scope
Fix the keepalive runner-dispatch debounce in
.github/workflows/agents-keepalive-loop.ymland its consumer template copy.Non-goals
head is correct and should stay.
bwrapsandbox failure. That is a separate defect; this issue isabout the loop's inability to recover from any zero-output run.
Tasks
.github/workflows/agents-keepalive-loop.yml(consumer copy:templates/consumer-repo/.github/workflows/agents-81-gate-followups.yml), distinguish a productive completion (commit or task delta) from an unproductive one; only a productive completion burns the(head_sha, provider)key..github/workflows/agents-keepalive-loop.yml, surface both numbers when refusingduplicate-completed: prior commits, prior task deltas, and what would drain the block..github/workflows/agents-keepalive-loop.yml, add a bounded escape hatch: after N consecutive unproductive completions on the same head, allow one re-dispatch.tests/workflows/test_keepalive_dispatch_debounce.pyproving a completed-but-zero-output dispatch record does not block the next dispatch for the same head.Acceptance Criteria
pytest tests/workflows/test_keepalive_dispatch_debounce.py -qand retain output in the PR body.completeddispatch with zero commits and zero task deltas yieldsshould_dispatch: trueon the next evaluation for the same head.completedrecord burns the key → the new test must FAIL → restore.Implementation Notes
The debounce decision is emitted by the
Check runner dispatch debouncestep of theEvaluate keepalive loopjob in.github/workflows/agents-keepalive-loop.yml. Thezero-output signal is already present in the keepalive state JSON the same job writes:
tasks_completed_delta, and the work log'sFiles/Commitcolumns.This is the latched-gate pattern described in
CLAUDE.md: the gate's measuring window(any
completeddispatch) is wider than its draining window (a new head commit, which onlya dispatched agent produces).