Skip to content

Keepalive runner-dispatch debounce latches on a zero-output run (duplicate-completed deadlock) #3433

Description

@stranske

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

  • In .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.
  • In .github/workflows/agents-keepalive-loop.yml, surface both numbers when refusing duplicate-completed: prior commits, prior task deltas, and what would drain the block.
  • In .github/workflows/agents-keepalive-loop.yml, add a bounded escape hatch: after N consecutive unproductive completions on the same head, allow one re-dispatch.
  • Add a regression in tests/workflows/test_keepalive_dispatch_debounce.py proving a completed-but-zero-output dispatch record does not block the next dispatch for the same head.

Acceptance Criteria

  • Run pytest tests/workflows/test_keepalive_dispatch_debounce.py -q and retain output in the PR body.
  • The new test asserts that a completed dispatch with zero commits and zero task deltas yields should_dispatch: true on the next evaluation for the same head.
  • Deliberate-break gate: revert the productivity check so any completed record burns the key → the new test must FAIL → restore.

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).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions