diff --git a/gateway/kanban_watchers.py b/gateway/kanban_watchers.py index ad8c64ae8b..6ff4edc77b 100644 --- a/gateway/kanban_watchers.py +++ b/gateway/kanban_watchers.py @@ -422,7 +422,7 @@ class GatewayKanbanWatchersMixin: if _gc_due: # Hourly (plus once at startup) stale-sub GC: # drop subscriptions for tasks that have been - # ``done`` untouched past the retention + # ``done``/``blocked`` untouched past the retention # window. Best-effort — a failed sweep never # blocks delivery; the next hourly gate # retries it. @@ -433,7 +433,7 @@ class GatewayKanbanWatchersMixin: ) if _purged: logger.info( - "kanban notifier: purged %d stale done-task subscription(s) on board %s (retention %dd)", + "kanban notifier: purged %d stale done/blocked-task subscription(s) on board %s (retention %dd)", _purged, slug, _gc_retention_days, ) except Exception as _gc_exc: diff --git a/hermes_cli/kanban_db.py b/hermes_cli/kanban_db.py index cb3863466b..198669792e 100644 --- a/hermes_cli/kanban_db.py +++ b/hermes_cli/kanban_db.py @@ -11685,8 +11685,8 @@ def purge_stale_done_notify_subs( *, max_age_days: int = 30, ) -> int: - """Delete notify subscriptions whose task has sat in ``done`` untouched - for longer than ``max_age_days``. + """Delete notify subscriptions whose task has sat in ``done`` or + ``blocked`` untouched for longer than ``max_age_days``. The notifier keeps subscriptions alive through ``done`` because a completed task can be reopened (review corrections, continuation) and @@ -11695,7 +11695,10 @@ def purge_stale_done_notify_subs( subscription rows forever — each one scanned every notifier tick. This GC bounds that: a task that has been ``done`` with no new events for the retention window is treated as settled and its subscriptions - are purged. Age is measured from the task's most recent event + are purged. ``blocked`` tasks (circuit-breaker trips, dead workers) + are reaped on the same clock — they are abandoned, not idle, unlike a + ``backlog``/``ready`` card that is merely waiting for pickup (#100955). + Age is measured from the task's most recent event (falling back to ``completed_at`` then ``created_at``), so ANY activity — including a reopen, which also moves the task off ``done`` — resets or exempts it. @@ -11714,7 +11717,7 @@ def purge_stale_done_notify_subs( cur = conn.execute( "DELETE FROM kanban_notify_subs WHERE task_id IN (" " SELECT t.id FROM tasks t" - " WHERE t.status = 'done'" + " WHERE t.status IN ('done', 'blocked')" " AND COALESCE(" " (SELECT MAX(e.created_at) FROM task_events e" " WHERE e.task_id = t.id)," diff --git a/website/docs/user-guide/features/kanban.md b/website/docs/user-guide/features/kanban.md index 3a2422ddcf..084a36f3f8 100644 --- a/website/docs/user-guide/features/kanban.md +++ b/website/docs/user-guide/features/kanban.md @@ -614,7 +614,7 @@ Config knobs (all under `kanban:` in `~/.hermes/config.yaml`): | `orchestrator_profile` | `""` | Profile assigned to the root/orchestration task after decomposition. Empty = fall back to active default profile. | | `default_assignee` | `""` | Where a child task lands when the LLM picks an unknown profile. Empty = fall back to active default. | | `auto_subscribe_on_create` | `true` | When `kanban_create` runs inside a persistent gateway/TUI session, terminal events resume that originating agent with a synthetic status turn. Set to `false` for passive completion or to require explicit `kanban_notify-subscribe` calls. Independent of `auto_decompose`. | -| `done_sub_retention_days` | `30` | Notify subscriptions survive `done` (reopen-safe) and are removed on `archived`. The notifier GC purges subscriptions whose task has been `done` with no new events for this many days, bounding sub-table growth on boards that never archive. `0` disables the sweep. | +| `done_sub_retention_days` | `30` | Notify subscriptions survive `done` (reopen-safe) and are removed on `archived`. The notifier GC purges subscriptions whose task has been `done` or `blocked` with no new events for this many days, bounding sub-table growth on boards that never archive. `0` disables the sweep. | And the two auxiliary LLM slots: @@ -887,7 +887,7 @@ bot> ✓ t_9fc1a3 completed by transcriber transcribed 42 minutes, saved to podcast/2026-05-04.md ``` -Subscriptions survive a task reaching `done` — completion is reversible (a reviewer or controller can reopen a done task), so the origin session keeps getting notified through reopen cycles. They auto-remove on `archived` (the irreversible end state). On boards that never archive, a GC sweep purges subscriptions for tasks that have sat in `done` with no new activity for `kanban.done_sub_retention_days` days (default 30; set 0 to disable), so stale rows don't accumulate forever. If you script a create with `--json` (machine output) the auto-subscribe is skipped — the assumption is that scripted callers want to manage subscriptions explicitly via `/kanban notify-subscribe`. +Subscriptions survive a task reaching `done` — completion is reversible (a reviewer or controller can reopen a done task), so the origin session keeps getting notified through reopen cycles. They auto-remove on `archived` (the irreversible end state). On boards that never archive, a GC sweep purges subscriptions for tasks that have sat in `done` or `blocked` with no new activity for `kanban.done_sub_retention_days` days (default 30; set 0 to disable), so stale rows don't accumulate forever. If you script a create with `--json` (machine output) the auto-subscribe is skipped — the assumption is that scripted callers want to manage subscriptions explicitly via `/kanban notify-subscribe`. A chat-originated auto-subscribe is created in `notify+wake` mode: on a terminal event the destination agent both receives the passive message **and** takes a real turn, so it can read the board context and reply in its own voice. See [Delivery modes](#delivery-modes) below.