✨ feat(kanban): notify on every card transition, never silent - #25
Conversation
The notifier gate delivered only five terminal event kinds (completed/blocked/gave_up/crashed/timed_out) and silently dropped every other lane change — assigned, unblocked, and critically block_loop_detected, the auto-escalate-to-triage signal (the system asking for a human). A card could exhaust its block-recurrence limit, escalate to triage, and sit completely silent. Changes: - Widen the delivery gate to NOTIFY_KINDS: the terminal five plus the meaningful lane changes (block_loop_detected, unblocked, assigned, promoted, reclaimed, stale, dependency_wait). High-frequency bookkeeping kinds stay excluded so routine churn does not ping. - Add per-kind message wording for the new kinds, with a generic transition line for any remaining NOTIFY_KIND so a lane change is never silent. - Guarantee a delivery target for a card with NO subscription: lazily register a fallback subscription to the default channel, cursor-seeded to the latest notifiable event so only the live transition fires (no historical backfill flood). Channel/platform config-overridable via kanban.notify_fallback; empty chat_id opts out. - Seed support in add_notify_sub via initial_cursor (INSERT-only) so the fallback sub never replays a task's entire event history. - Extend the agent-run bridge default kinds to include block_loop_detected so the triage escalation wakes the orchestrator, carrying origin session/thread context. Tests: new E2E tests against a temp HERMES_KANBAN_DB prove an assigned lane change delivers, a block_loop_detected escalation delivers AND wakes, and a no-subscription card surfaces to the fallback channel. All RED before the change, GREEN after; the terminal-kind and origin-payload contract is unregressed.
cwest
left a comment
There was a problem hiding this comment.
No changes needed.
The delivery gate widening is the right shape: NOTIFY_KINDS keeps the terminal five and adds the lane changes that actually matter (block_loop_detected above all), while leaving the high-frequency bookkeeping kinds out so routine churn stays quiet. The per-kind wording plus the generic fallback line means a kind in the set can't slip through unhandled.
The no-subscription fallback holds up under the scrutiny it needs. The cursor is seeded to the second-newest notifiable event (or one below a lone event), so a card that transitions with no origin sub delivers only its latest transition, never a replay of its history. The seed applies on INSERT only, so re-registering never rewinds a live cursor, and once the fallback sub exists the card flows through the normal claim/cursor path on later ticks with no duplicate. The send-failure key matches the delivery loop's key, and done/archived cards are skipped so a completed task can't re-fire through the fallback.
add_notify_sub's initial_cursor is INSERT-only and defaults to 0, so existing callers are unaffected. block_loop_detected in the emit default kinds carries the origin session and thread, so the triage escalation wakes the orchestrator in the thread the work came from.
Verification: the three new tests fail on the base and pass with the change; assigned/block_loop_detected/no-sub-fallback are each exercised against a temp DB through the real notifier path. The kanban suite runs 799 passed with the change. The 19 failures are present identically on the base commit and don't touch the notifier code path (cross-file test pollution that passes in isolation, plus two environment- and timing-sensitive tests). Signed commit, mergeable and CLEAN, no open threads.
The notifier gate delivered only five terminal event kinds (completed/blocked/gave_up/crashed/timed_out) and silently dropped every other lane change — assigned, unblocked, and critically block_loop_detected, the auto-escalate-to-triage signal (the system asking for a human). A card could exhaust its block-recurrence limit, escalate to triage, and sit completely silent. Changes: - Widen the delivery gate to NOTIFY_KINDS: the terminal five plus the meaningful lane changes (block_loop_detected, unblocked, assigned, promoted, reclaimed, stale, dependency_wait). High-frequency bookkeeping kinds stay excluded so routine churn does not ping. - Add per-kind message wording for the new kinds, with a generic transition line for any remaining NOTIFY_KIND so a lane change is never silent. - Guarantee a delivery target for a card with NO subscription: lazily register a fallback subscription to the default channel, cursor-seeded to the latest notifiable event so only the live transition fires (no historical backfill flood). Channel/platform config-overridable via kanban.notify_fallback; empty chat_id opts out. - Seed support in add_notify_sub via initial_cursor (INSERT-only) so the fallback sub never replays a task's entire event history. - Extend the agent-run bridge default kinds to include block_loop_detected so the triage escalation wakes the orchestrator, carrying origin session/thread context. Tests: new E2E tests against a temp HERMES_KANBAN_DB prove an assigned lane change delivers, a block_loop_detected escalation delivers AND wakes, and a no-subscription card surfaces to the fallback channel. All RED before the change, GREEN after; the terminal-kind and origin-payload contract is unregressed. (cherry picked from commit 1520500)
Why
The kanban notifier gate delivered only five terminal event kinds
(
completed/blocked/gave_up/crashed/timed_out) and silently droppedevery other lane change —
assigned,unblocked, and criticallyblock_loop_detected, the auto-escalate-to-triagesignal (the system askingfor a human). Proven live: a card hit
BLOCK_RECURRENCE_LIMIT, auto-escalated totriage, emittedblock_loop_detected— not in the terminal set — so no deliveryand no wake fired. The card sat silent in triage. Compounding: a card carrying no
origin subscription had nowhere for even a terminal event to land.
What
NOTIFY_KINDS: the terminal five plus themeaningful lane changes (
block_loop_detected,unblocked,assigned,promoted,reclaimed,stale,dependency_wait). High-frequency bookkeepingkinds (heartbeat/claimed/commented/spawned/…) stay excluded so routine churn
does not ping.
for any remaining
NOTIFY_KINDso a lane change is never silent.fallback subscription to the default channel, cursor-seeded to the latest
notifiable event so only the live transition fires (no historical backfill
flood). Channel/platform config-overridable via
kanban.notify_fallback; emptychat_idopts out.initial_cursorseed inadd_notify_sub(INSERT-only) so the fallback subnever replays a task's entire event history.
block_loop_detectedsothe triage escalation wakes the orchestrator, carrying origin session/thread
context (unchanged F1/F2/F3 payload contract).
Verification
New E2E tests against a temp
HERMES_KANBAN_DB(real notifier path, not mockedgreen):
assignedlane change delivers a chat ping;block_loop_detected/ triage escalation delivers a ping AND fires theagent-run bridge;
All three RED before the change, GREEN after. The terminal-kind + origin-payload
contract is unregressed (
test_kanban_transition_emit.py/test_thread_origin_autonomy.py: 34 passed). Full kanban suite: the onlyfailures are pre-existing cross-file test-pollution present identically on the
base (verified — they pass in isolation and appear with 796 passed on base vs 799
with this change); this change adds +3 passing and regresses nothing.