Skip to content

fix(kanban): stop parentless dependency blocks from respawn-churning (#81305) - #81307

Open
vomitorius wants to merge 1 commit into
NousResearch:mainfrom
vomitorius:fix/kanban-parentless-dependency-block-churn
Open

fix(kanban): stop parentless dependency blocks from respawn-churning (#81305)#81307
vomitorius wants to merge 1 commit into
NousResearch:mainfrom
vomitorius:fix/kanban-parentless-dependency-block-churn

Conversation

@vomitorius

Copy link
Copy Markdown

What does this PR do?

Stops a Kanban card with no parent links from respawn-churning after a kind="dependency" block.

block_task routes dependency blocks to status='todo' so recompute_ready can gate them on parent completion rather than parking them in the human blocked bucket. That is right when there is a parent — but recompute_ready's gate is all(p.status in ("done","archived") for p in parents), which over zero parents is vacuously true. So a parentless card is promoted straight back to ready and the dispatcher spawns it again on the next tick:

block -> promote -> respawn -> block -> ...

once per tick (60s by default), indefinitely, each spawn burning a full worker session. Neither existing brake applies: this branch return Trues before the BLOCK_RECURRENCE_LIMIT escalation, and a dependency block is not a failure so consecutive_failures never trips the circuit breaker.

The fix gates the todo parking on an actual task_links parent 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_ready to treat "zero parents" as "not satisfied") would change promotion semantics for every parentless todo card, including ones that never went through block_task. Deciding it at the block site keeps the blast radius to the mis-classified block itself.

Related Issue

Fixes #81305

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)

Changes Made

  • hermes_cli/kanban_db.py — in block_task, the kind == "dependency" branch now also requires a task_links parent row before parking the task in todo; 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 test test_dependency_block_without_parents_does_not_churn: a parentless dependency block lands in blocked and stays there across a recompute_ready pass.

How to Test

Reproduction before the fix:

  1. tid = create_task(conn, title="no-parents", assignee="worker") — note: no link_tasks() call, so no parents.
  2. Drive it to running, then block_task(conn, tid, reason="stale base", kind="dependency") → status is todo.
  3. recompute_ready(conn) → status is back to ready. 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 blocked and step 3 leaves it there.

Test run (project venv, pytest via an ephemeral overlay so the runtime venv stays clean):

pytest tests/hermes_cli/ -k kanban -q
167 passed, 7 failed

The 7 failures are pre-existing on main — verified by running the identical selection in a pristine git worktree add --detach <tmp> origin/main checkout, which produces the same 7 (test_rate_limit_exit_requeues_without_counting_failure, test_connect_works_when_wal_is_silently_refused, two in test_kanban_decompose.py, test_claim_fires_hook, two in test_kanban_write_guard.py). The delta is my one new test.

Focused run, all green:

pytest tests/hermes_cli/test_kanban_block_kinds.py tests/hermes_cli/test_kanban_blocked_sticky.py \
       tests/hermes_cli/test_kanban_promote.py tests/hermes_cli/test_kanban_db.py -q
37 passed

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits (fix(scope):, feat(scope):, etc.)
  • I searched for existing PRs to make sure this isn't a duplicate
  • My PR contains only changes related to this fix/feature (no unrelated commits)
  • I've run pytest tests/ -q and all tests pass — see the pre-existing-failure note above
  • I've added tests for my changes (required for bug fixes, strongly encouraged for features)
  • I've tested on my platform: Ubuntu (Linux 7.0.0-28-generic), Python 3.11, Hermes 0.18.2

Documentation & Housekeeping

  • I've updated relevant documentation (README, 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/A
  • I've updated cli-config.yaml.example if I added/changed config keys — N/A, no new config
  • I've updated CONTRIBUTING.md or AGENTS.md if I changed architecture or workflows — N/A
  • I've considered cross-platform impact — N/A, pure SQL + control flow, no platform-specific primitives
  • I've updated tool descriptions/schemas if I changed tool behavior — N/A; kanban_block's schema is unchanged. Note the observable effect: a kind="dependency" block on a parentless task now reports blocked instead of todo.

Screenshots / Logs

Event trail from the live board that surfaced this (card had no parents; timestamps local):

commented        17:53:43   (worker's status comment)
dependency_wait  17:53:50   {"reason": "stale base ... needs rebase", "kind": "dependency"}
promoted         17:54:18   <-- 28s later, back in the work pool

Note on scope

While tracking this down I also hit the active_pr respawn guard having no re-queue bypass, and respawn_guarded not being surfaced in dispatch output. 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 the active_pr guard 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.

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>
@alt-glitch alt-glitch added type/bug Something isn't working comp/cron Cron scheduler and job management P3 Low — cosmetic, nice to have labels Aug 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/cron Cron scheduler and job management P3 Low — cosmetic, nice to have type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Parentless dependency block respawn-churns: recompute_ready re-promotes it every tick with no brake

2 participants