✨ feat(kanban): wake origin thread session on every transition - #26
Conversation
The transition-emit bridge stamped origin_* fields on its payload, but the webhook route ignored them and always minted a contextless webhook:<route>:<delivery> session — so a woken orchestrator never resumed the thread the work was born in. And the emit gate covered only blocked/ block_loop_detected, so a card moving to review (status_changed + assigned) woke nothing at all. - webhook route: when a payload carries origin_platform/chat_id/thread_id, build a SessionSource targeting that origin thread instead of the synthetic webhook session, so the run resumes the origin session and reports back there. Falls back to the webhook session when origin fields are absent (backward-compatible). The origin source is shaped to mirror the live inbound session key (chat_type=thread, chat_id+thread_id set, no per-user suffix) so the wake lands in the exact origin session, not a phantom one. - widen DEFAULT_EMIT_KINDS to include the actionable lane-move kinds (status_changed, assigned, unblocked) alongside blocked/block_loop_detected; completed stays out (done is a close-loop ping, not a reasoning task). - tests: origin routing + key-mirror invariant + emit-kind coverage; RED before, GREEN after; full gateway suite green.
cwest
left a comment
There was a problem hiding this comment.
The origin-routing half is right and I verified it end to end. The webhook route now builds a SessionSource against the origin thread when origin_platform/chat_id/thread_id are present, falls back to the webhook session when they aren't, and the source is shaped to mirror the live inbound key so the wake lands in the real thread rather than a phantom session. I reran the two new test files against pre-fix source (7 fail) and at head (11 pass), so they genuinely exercise the change, and CI is green after clearing an unrelated flake.
One thing to fix before this lands: the widened wake set is missing the terminal-failure kinds. The emit gate runs inside the notifier loop that delivers NOTIFY_KINDS, which includes gave_up, crashed, and timed_out. Those three reach should_emit_transition exactly like status_changed does, but they're not in the new DEFAULT_EMIT_KINDS, so a worker that gives up, crashes, or times out still pings the chat and wakes no one to act on it. That's the same silent-escalation gap this change is meant to close, just on the failure path instead of the review path. Add them to the set. Detail inline.
DEFAULT_EMIT_KINDS omitted the terminal-failure kinds gave_up, crashed, and timed_out. Those flow through the same gate as the chat-ping notifier (should_emit_transition is invoked inside the loop over NOTIFY_KINDS, whose TERMINAL_KINDS = completed, blocked, gave_up, crashed, timed_out), so a worker that gave up, crashed, or timed out pinged chat but woke no orchestrator — the same silent-escalation gap this change set closes, left open on the failure path. Add the three kinds to the default emit set so the failure path wakes the orchestrator exactly like blocked/block_loop_detected do. completed stays out by design (done is a close-loop chat ping, not a reasoning task). Add a default-coverage test so the failure-path wake cannot silently regress.
cwest
left a comment
There was a problem hiding this comment.
The rework closes the gap from the last round. DEFAULT_EMIT_KINDS now carries gave_up, crashed, and timed_out, so a worker that fails terminally wakes the orchestrator the same way blocked does — the failure-path twin of the case this change exists to fix. I confirmed the wiring: should_emit_transition runs inside the notifier loop over NOTIFY_KINDS (which includes those three via TERMINAL_KINDS), so before this they pinged chat but woke nothing. The new test pins it: I removed the three kinds and it failed on gave_up, restored them and it passed.
Origin routing is unchanged and was already correct. Targeted suite is green here (12 passed across the wake-origin and webhook-origin-routing modules). Checks are all through, no conversation threads are open, and it merges clean.
Nothing left on my side.
* ✨ feat(kanban): wake origin thread session on every transition The transition-emit bridge stamped origin_* fields on its payload, but the webhook route ignored them and always minted a contextless webhook:<route>:<delivery> session — so a woken orchestrator never resumed the thread the work was born in. And the emit gate covered only blocked/ block_loop_detected, so a card moving to review (status_changed + assigned) woke nothing at all. - webhook route: when a payload carries origin_platform/chat_id/thread_id, build a SessionSource targeting that origin thread instead of the synthetic webhook session, so the run resumes the origin session and reports back there. Falls back to the webhook session when origin fields are absent (backward-compatible). The origin source is shaped to mirror the live inbound session key (chat_type=thread, chat_id+thread_id set, no per-user suffix) so the wake lands in the exact origin session, not a phantom one. - widen DEFAULT_EMIT_KINDS to include the actionable lane-move kinds (status_changed, assigned, unblocked) alongside blocked/block_loop_detected; completed stays out (done is a close-loop ping, not a reasoning task). - tests: origin routing + key-mirror invariant + emit-kind coverage; RED before, GREEN after; full gateway suite green. * 🐛 fix(kanban): wake orchestrator on terminal-failure transitions DEFAULT_EMIT_KINDS omitted the terminal-failure kinds gave_up, crashed, and timed_out. Those flow through the same gate as the chat-ping notifier (should_emit_transition is invoked inside the loop over NOTIFY_KINDS, whose TERMINAL_KINDS = completed, blocked, gave_up, crashed, timed_out), so a worker that gave up, crashed, or timed out pinged chat but woke no orchestrator — the same silent-escalation gap this change set closes, left open on the failure path. Add the three kinds to the default emit set so the failure path wakes the orchestrator exactly like blocked/block_loop_detected do. completed stays out by design (done is a close-loop chat ping, not a reasoning task). Add a default-coverage test so the failure-path wake cannot silently regress. (cherry picked from commit 7f29695)
Why
The event-driven autonomy contract — a card transition wakes the ORIGIN thread's session so the orchestrator resumes in-thread and acts — was not met. Proven by a live E2E test: a card moving ready→running→review woke nothing into the origin thread, forcing manual polling. Two defects:
blocked/block_loop_detected. A move to review firesstatus_changed+assigned— neither woke anything.origin_*fields the emitter sends, so every wake ran in a contextlesswebhook:<route>:<delivery>session, never the origin thread.What
origin_platform/origin_chat_id/origin_thread_id, build aSessionSourcetargeting the origin thread instead of the synthetic webhook session, so the run resumes the origin session and reports back there. Falls back to the webhook session when origin fields are absent (backward-compatible). The origin source mirrors the live inbound session key (chat_type=thread,chat_id+thread_idset, no per-user suffix) so the wake lands in the exact origin session — not a phantom one (the key-mismatch trap).DEFAULT_EMIT_KINDSto include actionable lane-move kinds (status_changed,assigned,unblocked) alongsideblocked/block_loop_detected.completedstays out deliberately (done is a close-loop chat ping, not a reasoning task).Tests
test_wake_origin_on_transition.py— emit-kind coverage (status_changed/assigned/unblocked now wake; explicit config override still wins; disabled stays dark).test_webhook_origin_routing.py— origin routing targets the origin thread; falls back to webhook session without origin fields; key-mirror invariant (origin source yields the exact live thread session key); unknown-platform + absent-field fallbacks.Deploy note
Touches
gateway/modules cached in the running gateway — requires a restart to go live. Configkanban.transition_emit.emit_kindsshould be cleared/updated to pick up the new defaults.