fix(kanban): stop parentless dependency blocks from respawn-churning (#81305) - #81307
Open
vomitorius wants to merge 1 commit into
Open
fix(kanban): stop parentless dependency blocks from respawn-churning (#81305)#81307vomitorius wants to merge 1 commit into
vomitorius wants to merge 1 commit into
Conversation
A `block_task(kind="dependency")` call parks the task in `todo` and lets
`recompute_ready` gate it on parent completion. But when the task has no
parent links at all, that gate is vacuously satisfied ("all parents are
done" over zero parents), so `recompute_ready` promotes it straight back
to `ready` and the dispatcher respawns it on the next tick:
block -> promote -> respawn -> block -> ...
once per dispatcher tick, indefinitely. Neither existing brake applies:
the dependency branch returns before the `BLOCK_RECURRENCE_LIMIT`
escalation, and a dependency block is not a failure, so
`consecutive_failures` never trips the circuit breaker.
Observed in the wild on a card whose worker reported a stale base after
the default branch moved under its PR. The worker classified the block as
`dependency`; the card then respawned every 60s, each spawn burning a
full worker session.
A dependency wait that no parent completion can ever satisfy is a
mis-classified block, so gate the `todo` parking on an actual
`task_links` parent row. Without one, fall through to the truly-blocked
path: the task lands in the human bucket and gets recurrence counting.
The legitimate parent-gated case is unchanged.
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.
What does this PR do?
Stops a Kanban card with no parent links from respawn-churning after a
kind="dependency"block.block_taskroutes dependency blocks tostatus='todo'sorecompute_readycan gate them on parent completion rather than parking them in the humanblockedbucket. That is right when there is a parent — butrecompute_ready's gate isall(p.status in ("done","archived") for p in parents), which over zero parents is vacuously true. So a parentless card is promoted straight back toreadyand the dispatcher spawns it again on the next tick:once per tick (60s by default), indefinitely, each spawn burning a full worker session. Neither existing brake applies: this branch
return Trues before theBLOCK_RECURRENCE_LIMITescalation, and a dependency block is not a failure soconsecutive_failuresnever trips the circuit breaker.The fix gates the
todoparking on an actualtask_linksparent row. Without one, the call falls through to the existing truly-blocked path — the card lands in the human bucket and gets recurrence counting. The legitimate parent-gated case is untouched.Why this approach: the alternative (teaching
recompute_readyto treat "zero parents" as "not satisfied") would change promotion semantics for every parentlesstodocard, including ones that never went throughblock_task. Deciding it at the block site keeps the blast radius to the mis-classified block itself.Related Issue
Fixes #81305
Type of Change
Changes Made
hermes_cli/kanban_db.py— inblock_task, thekind == "dependency"branch now also requires atask_linksparent row before parking the task intodo; otherwise it falls through to the truly-blocked path. Comment explains why both existing brakes miss this case.tests/hermes_cli/test_kanban_block_kinds.py— regression testtest_dependency_block_without_parents_does_not_churn: a parentless dependency block lands inblockedand stays there across arecompute_readypass.How to Test
Reproduction before the fix:
tid = create_task(conn, title="no-parents", assignee="worker")— note: nolink_tasks()call, so no parents.running, thenblock_task(conn, tid, reason="stale base", kind="dependency")→ status istodo.recompute_ready(conn)→ status is back toready. On a live board the dispatcher spawns it next tick, the worker blocks the same way, and the cycle repeats every 60s.After the fix, step 2 lands the card in
blockedand step 3 leaves it there.Test run (project venv, pytest via an ephemeral overlay so the runtime venv stays clean):
The 7 failures are pre-existing on
main— verified by running the identical selection in a pristinegit worktree add --detach <tmp> origin/maincheckout, which produces the same 7 (test_rate_limit_exit_requeues_without_counting_failure,test_connect_works_when_wal_is_silently_refused, two intest_kanban_decompose.py,test_claim_fires_hook, two intest_kanban_write_guard.py). The delta is my one new test.Focused run, all green:
Checklist
Code
fix(scope):,feat(scope):, etc.)pytest tests/ -qand all tests pass — see the pre-existing-failure note aboveDocumentation & Housekeeping
docs/, docstrings) — the changed behavior is explained in an inline comment at the decision site; no user-facing doc describes the parentless case. Otherwise N/Acli-config.yaml.exampleif I added/changed config keys — N/A, no new configCONTRIBUTING.mdorAGENTS.mdif I changed architecture or workflows — N/Akanban_block's schema is unchanged. Note the observable effect: akind="dependency"block on a parentless task now reportsblockedinstead oftodo.Screenshots / Logs
Event trail from the live board that surfaced this (card had no parents; timestamps local):
Note on scope
While tracking this down I also hit the
active_prrespawn guard having no re-queue bypass, andrespawn_guardednot being surfaced indispatchoutput. Both already have open PRs (#62393, #62424 and #46269 respectively), so this PR deliberately leaves them alone and fixes only the parentless-dependency churn, which I could not find covered anywhere. Worth flagging for whoever reviews #62393: relaxing theactive_prguard without this fix makes this loop run at full tick rate instead of once per 24h — that guard is currently the only thing throttling it.