Skip to content

🔇 fix(kanban): suppress human-facing wakes for write-time-sweep and no-origin cards - #52

Merged
cwest merged 2 commits into
cwest/integrationfrom
topic/suppress-sweep-wake
Jul 7, 2026
Merged

🔇 fix(kanban): suppress human-facing wakes for write-time-sweep and no-origin cards#52
cwest merged 2 commits into
cwest/integrationfrom
topic/suppress-sweep-wake

Conversation

@cwest

@cwest cwest commented Jul 7, 2026

Copy link
Copy Markdown
Owner

Problem

Automatic curate: write-time sweep @ <sha> bookkeeping cards, and any card with
no 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 are
internal 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 the curate: 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_id equals fallback_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 now
    calls should_emit_wake before 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:

  • Pure-function tests for is_sweep_card_title and should_emit_wake (sweep
    suppressed even with a real origin; Home-fallback no-origin suppressed; real
    origin thread and real non-fallback channel still allowed).
  • Integration tests through the wired notifier tick (existing harness): a
    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):

  • Wake/notifier/transition slice: 99 passed, 0 failed.
  • Full tests/gateway/: 8393 passed; the handful of remaining failures are
    pre-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/*.py modules 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 running
gateway executes — against a seeded throwaway board carrying the incident card
classes. It records the actual outbound wake dispatch (emit_transition, the
egress that produces the gateway's Sending response … to <Home> log line), not
a fresh-Python re-import of the pure function.

Harness: scripts/e2e_sweep_wake_suppression.py — run with
uv run python scripts/e2e_sweep_wake_suppression.py (exit 0 = suppression
proven). Output from this head:

=== Driving one real GatewayRunner._kanban_notifier_watcher tick ===
    fallback (Home) chat: 1515879019269197885

  [CHAT PING] Sending response to 1515909683171557416
  [CHAT PING] Sending response to 1515879019269197885
  [CHAT PING] Sending response to 1515909683171557416
  [WAKE POST] origin wake dispatching task=<synopsis> chat=1515909683171557416 thread=1523894059851186186  (title='curate: audit & steward the intent-driven-development brief')

=== Results ===
chat pings delivered: 3 -> ['1515909683171557416', '1515879019269197885', '1515909683171557416']
agent-run wake POSTs: 1
    wake -> task=<synopsis> chat=1515909683171557416 thread=1523894059851186186

PASS: sweep card fired NO agent-run wake
PASS: no-origin card fired NO wake to Home
PASS: NO agent-run wake POST landed on Home 1515879019269197885 (no 'Sending response … to 1515879019269197885')
PASS: real #research origin card STILL woke its origin thread (1515909683171557416/1523894059851186186)
PASS: chat-ping accounting unaffected (all 3 cards pinged)

=== OVERALL: PASS ===

This proves, on the runtime path, both acceptance assertions: (1) a
curate: write-time sweep @ 9c63aa9 card (and a no-origin Home-fallback card)
completes with no wake POST to Home — no Sending response … to 1515879019269197885 for that wake; (2) a real #research-thread origin card's
wake 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 pull deploy into ~/.hermes/hermes-agentgateway restart, complete a
curate: write-time sweep @ … card and confirm the live gateway log shows no
Sending response … to 1515879019269197885 for that wake, while a real
#research cycle's synopsis wake still routes to its origin thread. The code is
undeployed and restart-gated until then, so this cannot run from an unmerged
draft.

…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.
Comment thread gateway/kanban_transition_emit.py
@cwest

cwest commented Jul 7, 2026

Copy link
Copy Markdown
Owner Author

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. fallback_chat_id resolves to the live Home channel (1515879019269197885, config-overridable), and the notifier builds the no-origin fallback sub exactly as the gate expects (thread_id empty, chat_id == fallback). The suppression fails open. Commit is signed and formatted correctly. I ran the wake/notifier/transition slice in a throwaway clone at the head SHA: 80 passed, 0 failed, including the three integration tests that drive the notifier tick and assert no wake fires while the chat ping still lands, plus the real-origin regression guard.

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:

  1. A curate: write-time sweep @ … card completes and the log shows NO Sending response … to 1515879019269197885 for that wake.
  2. A real #research cycle's synopsis/commissioning wake still routes to its origin thread (the path that already worked stays working).

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.
@cwest

cwest commented Jul 7, 2026

Copy link
Copy Markdown
Owner Author

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 — GatewayRunner._kanban_notifier_watcher, the exact loop the gateway runs — against a seeded throwaway board carrying the incident card classes, recording the actual emit_transition wake dispatch (the egress that produces the Sending response … to <Home> log line). Added as scripts/e2e_sweep_wake_suppression.py; output is in the PR body under "Runtime E2E proof". It proves both acceptance assertions on the runtime path:

  • a curate: write-time sweep @ 9c63aa9 card (and a thread-less Home-fallback no-origin card) completes with no wake POST to Home — no Sending response … to 1515879019269197885 for that wake;
  • a real #research-thread origin card's wake still routes to its origin thread (1515909683171557416/1523894059851186186);
  • chat-ping accounting unaffected (all three cards still ping, including one to Home).

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 is_fallback flag lives only on the in-memory delivery sub; the wake fires from a persisted notify-sub row (add_notify_sub) that carries no such flag, which is the exact F3 leak. The structural derivation catches that persisted row; the flag would miss it. Kept the structural check and documented the reasoning in the code. Detail in the resolved inline thread.

Tests: 19 in test_kanban_transition_emit.py green; full wake/notifier/transition slice (13 files) 87 passed, 0 failed at the new head.

Head SHA: b879c3066391263e20256a8e068be0a36dab589a. Stays draft.

@cwest

cwest commented Jul 7, 2026

Copy link
Copy Markdown
Owner Author

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 scripts/e2e_sweep_wake_suppression.py myself against a fresh clone at head b879c30. It drives the actual GatewayRunner._kanban_notifier_watcher tick over a seeded board carrying all three card classes and records the real emit_transition egress — the dispatch that produces the Sending response … to <Home> gateway line. Exit 0, all assertions held:

  • The curate: write-time sweep @ 9c63aa9 card — seeded with a real research-channel origin sub, not just a fallback — fires its chat ping but no agent-run wake. Suppression is by card class, which is the point.
  • The thread-less Home-fallback card fires no wake.
  • No wake POST lands on the Home channel 1515879019269197885 — the incident line does not appear for any wake.
  • The real #research origin card still wakes its origin thread 1515909683171557416/1523894059851186186. The working synopsis path is intact.
  • All three cards still ping; chat accounting is untouched.

The gate itself sits inside the agent-wake fire loop, so the continue skips only the wake POST for that sub; the chat-ping delivery ran earlier and is unaffected. It fails open — a gate exception allows the wake rather than dropping a legitimate one.

On the earlier note about reading an is_fallback flag instead of re-deriving the no-origin condition: the reasoning for keeping the structural check is right. That flag is only stamped on the in-memory delivery sub; the wake fires from a persisted notify-sub row that carries no such field (is_fallback appears zero times in the DB layer — I checked). Reading the flag would miss exactly the persisted row that caused the leak. The structural no thread + chat == fallback derivation catches it. Documented in the code and the thread resolved.

Verification I ran at head b879c30: the targeted suite (19 passed), the full set of wake/notifier/transition files that import the changed modules (84 passed, 0 failed — the two aiohttp collection errors were a missing test extra in my environment, resolved by installing it), and the E2E harness (exit 0). No new regressions. Commit is signed and follows the commit convention with no stray attribution. Merge state is clean and mergeable.

@cwest
cwest marked this pull request as ready for review July 7, 2026 04:28
@cwest
cwest merged commit 1b2fa69 into cwest/integration Jul 7, 2026
31 checks passed
@cwest
cwest deleted the topic/suppress-sweep-wake branch July 7, 2026 12:14
cwest added a commit that referenced this pull request Jul 26, 2026
…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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant