mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-18 04:41:56 +00:00
fix(kanban): call recompute_ready after unlink_tasks removes a dependency
Problem: unlink_tasks() removes a parent→child dependency edge but does not trigger recompute_ready(). A child whose last blocking parent is unlinked stays stuck in 'todo' indefinitely — it only promotes to 'ready' on the next dispatcher tick or a manual 'hermes kanban recompute'. For CLI-only users without a dispatcher, the child is permanently stuck. Root cause: complete_task() and unblock_task() both call recompute_ready() after their write transaction so downstream children are evaluated immediately. unlink_tasks() was missing this call — removing a dependency is semantically equivalent to completing one, so the same recompute is needed. Fix: Capture the rowcount result before the write_txn exits, then call recompute_ready(conn) outside the transaction when a row was actually deleted (so the child sees the updated task_links state). Tests: Added test_unlink_tasks_triggers_recompute_ready in tests/hermes_cli/test_kanban_db.py: creates parent A (done) + parent C (running), child B with both parents (todo), unlinks C→B, asserts B is ready immediately. Stash-verified: FAILS without fix (child stays todo), PASSES with fix. 62/62 tests green in tests/hermes_cli/test_kanban_db.py. Closes #22459.
This commit is contained in:
parent
b9c001116e
commit
0c22434f03
2 changed files with 42 additions and 1 deletions
|
|
@ -1504,7 +1504,14 @@ def unlink_tasks(conn: sqlite3.Connection, parent_id: str, child_id: str) -> boo
|
|||
conn, child_id, "unlinked",
|
||||
{"parent": parent_id, "child": child_id},
|
||||
)
|
||||
return cur.rowcount > 0
|
||||
removed = cur.rowcount > 0
|
||||
if removed:
|
||||
# Dependency edge removed — re-evaluate promotion eligibility for the
|
||||
# child immediately. Matches the contract of complete_task and
|
||||
# unblock_task; without this the child stays stuck in todo until the
|
||||
# next dispatcher tick or a manual `hermes kanban recompute` (issue #22459).
|
||||
recompute_ready(conn)
|
||||
return removed
|
||||
|
||||
|
||||
def parent_ids(conn: sqlite3.Connection, task_id: str) -> list[str]:
|
||||
|
|
|
|||
|
|
@ -966,3 +966,37 @@ def test_connect_falls_back_to_delete_on_locking_protocol(kanban_home, caplog):
|
|||
tasks = kb.list_tasks(conn)
|
||||
assert any(row.id == t for row in tasks)
|
||||
conn.close()
|
||||
|
||||
|
||||
def test_unlink_tasks_triggers_recompute_ready(kanban_home):
|
||||
"""Regression test for issue #22459.
|
||||
|
||||
Removing a dependency via unlink_tasks must immediately promote the child
|
||||
to ready when all remaining parents are done — same contract as
|
||||
complete_task and unblock_task.
|
||||
|
||||
Before the fix, child stayed 'todo' indefinitely after unlink; only the
|
||||
next dispatcher tick or a manual 'hermes kanban recompute' would promote it.
|
||||
"""
|
||||
with kb.connect() as conn:
|
||||
# A is done.
|
||||
a = kb.create_task(conn, title="parent-done")
|
||||
kb.complete_task(conn, a)
|
||||
|
||||
# C is running (not done) — blocks child B.
|
||||
c = kb.create_task(conn, title="parent-running")
|
||||
kb.claim_task(conn, c, claimer="worker:1")
|
||||
|
||||
# B depends on both A (done) and C (running) → stays todo.
|
||||
b = kb.create_task(conn, title="child", parents=[a, c])
|
||||
assert kb.get_task(conn, b).status == "todo"
|
||||
|
||||
# Remove the blocking dependency C → B.
|
||||
removed = kb.unlink_tasks(conn, c, b)
|
||||
assert removed is True
|
||||
|
||||
# B's only remaining parent is A (done) → must be ready immediately.
|
||||
assert kb.get_task(conn, b).status == "ready", (
|
||||
"child should promote to ready immediately after unlink_tasks "
|
||||
"removes its last blocking dependency"
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue