Dispatcher self-healing + QA report classification fix
- Poll now includes blocked work items: when Hermes retries a transiently blocked task and it is active again, the item auto-resumes (task.resumed event) instead of staying blocked forever. Blocked items whose kanban task is done/review are left alone — re-finishing them every tick would spam events and duplicate cost entries. - Extraction prompt: a QA/review task that finished its checks is task.completed even when it found defects — findings are the deliverable. Previously "accessibility fails" wording classified the task as blocked. Verified live: misclassified QA task re-extracted to done; transiently blocked task auto-resumed; wall-clock cap fired on a 30-min runaway. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a0bae6dc51
commit
a858337edc
2 files changed
+26
-1
No files matched your search
+23
-1
@@ -208,10 +208,12 @@ class Dispatcher:
|
||||
# ------------------------------------------------------------ polling
|
||||
|
||||
async def _poll_in_flight(self) -> None:
|
||||
# blocked items are polled too: Hermes retries transient blocks itself,
|
||||
# so a blocked work item whose kanban task is active again must resume
|
||||
items = await db.fetch_all(
|
||||
"SELECT wi.*, e.id AS employee_id FROM work_item wi "
|
||||
"JOIN employee e ON e.id = wi.owner_id "
|
||||
"WHERE wi.status = 'in_progress' AND wi.hermes_task_id IS NOT NULL"
|
||||
"WHERE wi.status IN ('in_progress','blocked') AND wi.hermes_task_id IS NOT NULL"
|
||||
)
|
||||
for item in items:
|
||||
try:
|
||||
@@ -220,6 +222,13 @@ class Dispatcher:
|
||||
log.warning("poll failed for %s: %s", item["hermes_task_id"], e)
|
||||
continue
|
||||
status = kt.get("status")
|
||||
if item["status"] == "blocked":
|
||||
# resume only on ACTIVE kanban states; a done/review kanban task
|
||||
# under a blocked item was already classified — re-finishing it
|
||||
# every tick would spam events and duplicate cost entries
|
||||
if status in ("running", "ready", "todo", "scheduled"):
|
||||
await self._resume(item)
|
||||
continue
|
||||
if status in ("review", "done"):
|
||||
await self._finish(item, kt)
|
||||
elif status == "blocked":
|
||||
@@ -229,6 +238,19 @@ class Dispatcher:
|
||||
await self._cap(item, kt)
|
||||
# todo/scheduled/ready/running: still waiting
|
||||
|
||||
async def _resume(self, item: dict) -> None:
|
||||
async with db.pool.connection() as conn:
|
||||
async with conn.transaction():
|
||||
await conn.execute(
|
||||
"UPDATE work_item SET status='in_progress', updated_at=now() WHERE id=%s",
|
||||
(item["id"],))
|
||||
await conn.execute(
|
||||
"UPDATE employee SET status='working' WHERE id=%s", (item["employee_id"],))
|
||||
await db.append_event(conn, actor="system", event_type="task.resumed",
|
||||
company_id=item["company_id"], work_item_id=item["id"],
|
||||
payload={"note": "hermes task active again"})
|
||||
log.info("resumed %s", item["id"])
|
||||
|
||||
async def _over_budget(self, item: dict) -> bool:
|
||||
if not item["dispatched_at"]:
|
||||
return False
|
||||
|
||||
@@ -26,6 +26,9 @@ EXTRACT_SYSTEM = (
|
||||
"the options given\n"
|
||||
"- approval.requested: needs authority the employee lacks\n"
|
||||
"- incident.declared: something broke that affects others\n"
|
||||
"A QA/review/audit task that finished its checks is task.completed even "
|
||||
"when it found defects in the thing under test — findings are its "
|
||||
"deliverable, not a blocker.\n"
|
||||
"Copy concrete facts (files, commits, test counts) into summary/detail. "
|
||||
"Do not invent facts not present in the report."
|
||||
)
|
||||
|
||||
Reference in new issue
Block a user