🔇 fix(kanban): suppress human-facing wakes for write-time-sweep and no-origin cards - #52
Conversation
…o-origin cards Automatic `curate: write-time sweep @ <sha>` bookkeeping cards, and any card with no real human origin, were firing transition wakes that resolved to the Home fallback channel and posted noise there (992-char post observed). Suppress at the source in the transition-emit path: - Add pure, unit-testable helpers in kanban_transition_emit: is_sweep_card_title (matches the `curate: write-time sweep @` prefix) and should_emit_wake, which gates the human-facing agent-run wake on card class + origin. A sweep card never wakes; a thread-less subscription pointed at the Home fallback channel is treated as no-origin (no origin => no human post). - Gate the notifier's emit-wake dispatch loop on should_emit_wake, skipping the POST (with a debug log) for suppressed cards. The chat-ping accounting is unaffected; only the agent-run wake egress is gated. The gate fails open so its own error can never drop a legitimate wake. - Genuine origin-born cards (real thread, or a real non-fallback channel) still fire, so the working synopsis/commissioning wakes are regression-guarded. RESTART-GATED: touches the gateway transition-emit / notifier path; verify against a restarted gateway.
|
The code is right and I verified it independently: the wake gate sits in the decoupled 4c agent-wake loop, so skipping a sweep/no-origin card drops only the origin-wake POST and leaves the chat-ping deliveries untouched. What's missing is the evidence this card explicitly requires. The card says, twice, that this is verified end-to-end and "NOT a code-read PASS" — proven from a real #research thread against a restarted gateway. The PR describes that run as a "Recommended end-to-end proof" to perform, not one that was performed, and no log is attached. Two things need to be shown against a restarted gateway before this is ready:
Attach those two log excerpts and this is good to go. One non-blocking note left inline on the no-origin heuristic. One scope note for the record: the orchestrate-skill hard-no-op isn't in this PR — it lands via the config repo per the card's split. That's fine; it just can't be verified here. |
…ssion Drive the real GatewayRunner._kanban_notifier_watcher tick against a seeded throwaway board carrying the incident card classes (curate: write-time sweep @ 9c63aa9 with a real non-Home origin sub, a thread-less Home-fallback no-origin card, and a real #research-thread origin card) and assert on the actual outbound wake dispatch (the emit_transition egress that produces the gateway's 'Sending response … to <Home>' log line): - the sweep card fires its chat ping but NO agent-run wake; - the no-origin Home-fallback card fires NO wake to Home; - NO wake POST lands on the Home channel 1515879019269197885; - the real #research origin card STILL wakes its origin thread; - chat-ping accounting is unaffected (all three cards ping). This exercises the runtime path through the wired should_emit_wake gate rather than a fresh-Python re-import of the pure function. The live-gateway variant of this proof additionally requires deploy + a gateway restart, since the change is restart-gated. Also document why the no-origin test is structural (chat == fallback_chat_id) rather than a read of the is_fallback flag: that flag lives only on the synthesized in-memory delivery sub, while the wake fires from a persisted notify-sub row that carries no such flag — so the structural derivation is what catches the persisted thread-less Home sub the flag would miss.
|
Addressed the changes-requested bounce. BLOCKING — E2E proof (was "recommended", now ran): the live-gateway variant can only run after deploy + a gateway restart (the module is restart-gated and undeployed until merge), so it can't execute from an unmerged draft. The strongest pre-merge equivalent is a runtime harness that drives the real notifier tick —
The live-gateway confirmation is the one step only Casey can drive (deploy → restart → complete a sweep card, grep the log); it's spelled out in the PR body's "Live-gateway confirmation" section. NON-BLOCKING nit (kanban_transition_emit.py) — resolved on the thread: the Tests: 19 in Head SHA: |
|
No changes needed. The rework closes the one thing that was missing last round: the end-to-end proof the card required, run for real rather than recommended. I ran the new
The gate itself sits inside the agent-wake fire loop, so the On the earlier note about reading an Verification I ran at head |
…o-origin cards (#52) * 🔇 fix(kanban): suppress human-facing wakes for write-time-sweep and no-origin cards Automatic `curate: write-time sweep @ <sha>` bookkeeping cards, and any card with no real human origin, were firing transition wakes that resolved to the Home fallback channel and posted noise there (992-char post observed). Suppress at the source in the transition-emit path: - Add pure, unit-testable helpers in kanban_transition_emit: is_sweep_card_title (matches the `curate: write-time sweep @` prefix) and should_emit_wake, which gates the human-facing agent-run wake on card class + origin. A sweep card never wakes; a thread-less subscription pointed at the Home fallback channel is treated as no-origin (no origin => no human post). - Gate the notifier's emit-wake dispatch loop on should_emit_wake, skipping the POST (with a debug log) for suppressed cards. The chat-ping accounting is unaffected; only the agent-run wake egress is gated. The gate fails open so its own error can never drop a legitimate wake. - Genuine origin-born cards (real thread, or a real non-fallback channel) still fire, so the working synopsis/commissioning wakes are regression-guarded. RESTART-GATED: touches the gateway transition-emit / notifier path; verify against a restarted gateway. * 🧪 test(kanban): add runtime E2E proof for sweep/no-origin wake suppression Drive the real GatewayRunner._kanban_notifier_watcher tick against a seeded throwaway board carrying the incident card classes (curate: write-time sweep @ 9c63aa9 with a real non-Home origin sub, a thread-less Home-fallback no-origin card, and a real #research-thread origin card) and assert on the actual outbound wake dispatch (the emit_transition egress that produces the gateway's 'Sending response … to <Home>' log line): - the sweep card fires its chat ping but NO agent-run wake; - the no-origin Home-fallback card fires NO wake to Home; - NO wake POST lands on the Home channel 1515879019269197885; - the real #research origin card STILL wakes its origin thread; - chat-ping accounting is unaffected (all three cards ping). This exercises the runtime path through the wired should_emit_wake gate rather than a fresh-Python re-import of the pure function. The live-gateway variant of this proof additionally requires deploy + a gateway restart, since the change is restart-gated. Also document why the no-origin test is structural (chat == fallback_chat_id) rather than a read of the is_fallback flag: that flag lives only on the synthesized in-memory delivery sub, while the wake fires from a persisted notify-sub row that carries no such flag — so the structural derivation is what catches the persisted thread-less Home sub the flag would miss. (cherry picked from commit 1b2fa69)
Problem
Automatic
curate: write-time sweep @ <sha>bookkeeping cards, and any card withno real human origin, were firing kanban transition wakes that resolved to the
Home fallback channel and posted noise there (a 992-char post to Home was
observed for
curate: write-time sweep @ 9c63aa9). These sweep cards areinternal corpus-health accounting — not a report any human needs — and a
no-origin card should never default to posting into a human channel.
Root cause: the notifier synthesizes a thread-less fallback subscription pointed
at the Home channel for a card with no real origin sub; once persisted, that
sub feeds the agent-run wake egress, which fires an origin wake to Home.
Fix (suppress at the source)
gateway/kanban_transition_emit.py— two pure, unit-testable helpers:is_sweep_card_title(title)— matches thecurate: write-time sweep @title prefix (tolerant of whitespace;
None/empty never match).should_emit_wake(*, title, sub_thread_id, sub_chat_id, fallback_chat_id)—gates the human-facing agent-run wake on card class + origin. Returns False
for a sweep card (unconditionally) and for a thread-less subscription whose
chat_idequalsfallback_chat_id(a synthesized no-origin fallback:no origin => no human post). Returns True for a genuinely origin-born card
(real thread, or a real non-fallback channel).
gateway/kanban_watchers.py— the notifier's emit-wake dispatch loop nowcalls
should_emit_wakebefore firing the POST, skipping (with a debug log)any suppressed card. The chat-ping accounting is unaffected — only the
agent-run wake egress is gated. The gate fails open so its own error can never
drop a legitimate wake.
Tests (TDD)
New coverage in
tests/gateway/test_kanban_transition_emit.py:is_sweep_card_titleandshould_emit_wake(sweepsuppressed even with a real origin; Home-fallback no-origin suppressed; real
origin thread and real non-fallback channel still allowed).
write-time-sweep card and a Home-fallback no-origin card fire the chat ping
but not the agent-run wake; a genuine origin-born card still fires its
wake (regression guard for the working synopsis / commissioning path).
Verified via the canonical per-file runner (
scripts/run_tests.sh):tests/gateway/: 8393 passed; the handful of remaining failures arepre-existing / environment artifacts (too-many-open-files under 48-worker
parallelism, a restart-race timing test, media-routing, subprocess forensics)
in files that do not import the changed modules — causally independent of
this change.
Restart / verification note
RESTART-GATED — touches the gateway transition-emit / notifier path; the
change takes effect on a restarted gateway (the running gateway holds the old
gateway/*.pymodules in its import cache until restart).Runtime E2E proof (ran, output below)
Because the live-gateway variant of the proof can only run after deploy + a
Casey gateway restart (which come after review PASS and merge), the strongest
proof available pre-merge is a runtime harness that drives the real notifier
tick —
GatewayRunner._kanban_notifier_watcher, the exact loop the runninggateway executes — against a seeded throwaway board carrying the incident card
classes. It records the actual outbound wake dispatch (
emit_transition, theegress that produces the gateway's
Sending response … to <Home>log line), nota fresh-Python re-import of the pure function.
Harness:
scripts/e2e_sweep_wake_suppression.py— run withuv run python scripts/e2e_sweep_wake_suppression.py(exit 0 = suppressionproven). Output from this head:
This proves, on the runtime path, both acceptance assertions: (1) a
curate: write-time sweep @ 9c63aa9card (and a no-origin Home-fallback card)completes with no wake POST to Home — no
Sending response … to 1515879019269197885for that wake; (2) a real#research-thread origin card'swake still routes to its origin thread. Chat-ping accounting is unaffected
(all three cards still ping, including the one to Home).
Live-gateway confirmation (post-merge, Casey)
The remaining step that only Casey can drive: after merge →
cd ~/.hermes && git pulldeploy into~/.hermes/hermes-agent→ gateway restart, complete acurate: write-time sweep @ …card and confirm the live gateway log shows noSending response … to 1515879019269197885for that wake, while a real#researchcycle's synopsis wake still routes to its origin thread. The code isundeployed and restart-gated until then, so this cannot run from an unmerged
draft.