Skip to content

🐛 fix(kanban): resume orchestrator as a distinct turn on a busy-session wake - #29

Merged
cwest merged 1 commit into
cwest/integrationfrom
topic/wake-distinct-self-announcing-turn
Jul 1, 2026
Merged

🐛 fix(kanban): resume orchestrator as a distinct turn on a busy-session wake#29
cwest merged 1 commit into
cwest/integrationfrom
topic/wake-distinct-self-announcing-turn

Conversation

@cwest

@cwest cwest commented Jul 1, 2026

Copy link
Copy Markdown
Owner

Why

An origin-routed kanban-transition wake arrives at the platform adapter as a plain TEXT MessageEvent. When the origin session is mid-turn, handle_message (gateway/platforms/base.py) takes the busy branch and either text-debounces the event or newline-merges it into the in-progress/queued turn via merge_pending_message_event(merge_text=True) — so the wake is absorbed silently instead of producing its own identifiable turn (the busy-session swallow). Verified live: a transition emitted + the wake POSTed 202 to the correct origin session, but the woken run did not resume as a distinct actionable turn because the session was busy at emit time.

What

Make a kanban-transition wake produce its own distinct turn even when the origin session is busy — never queue-merged into an unrelated in-progress turn.

  • Tag the wake on the webhook side: event.metadata["kanban_transition_wake"] = True, keyed on the emitter-stamped origin_* fields (_is_transition_wake_payload) so ordinary webhook routes (GitHub PR, monitoring, cron) are never tagged.
  • Predicate is_transition_wake_event(event) reads the flag.
  • Wake-precedence in merge_pending_message_event: the single pending slot holds one event and the wake wins — a pending wake is never overwritten by a later non-wake, and an incoming wake replaces a pending non-wake intact (never appended). A dropped autonomy wake is worse than a user follow-up the user can resend.
  • Busy-branch bypass in handle_message: a transition wake is queued un-merged (no debounce, no text-merge) so the existing in-band drain cascade runs it as a distinct turn.

Invariants preserved: prompt caching, strict role alternation, byte-stable system prompt. No system-prompt or message-history mutation, no new drain site, no synthetic mid-loop user message, _active_sessions lifecycle untouched — the wake is delivered as an ordinary next-turn user message via the same cascade every follow-up uses.

Scope: this PR is ONLY the distinct-turn delivery. The self-announcing wake banner and latency tightening are separate follow-up work.

Tests

RED→GREEN proven: with the busy-branch bypass removed, test_busy_session_queues_wake_un_merged_without_debounce fails with wake was swallowed (debounced or dropped); with the fix it passes.

  • tests/gateway/test_transition_wake_busy_session.py (new): predicate, merge-precedence (both directions + regression that ordinary text still merges), busy-branch queues the wake un-merged.
  • tests/gateway/test_webhook_origin_routing.py: added test_wake_event_is_tagged_for_busy_bypass.

22 tests across the touched files pass via scripts/run_tests.sh. Full tests/gateway/ suite: the only failures are 6 pre-existing environment/platform artifacts (macOS /tmp/private/tmp path canonicalization, POSIX subprocess-spawn timing, an async race, a 48-worker FD-ulimit Too many open files, and 3 wecom tests needing an uninstalled optional extra) — proven identical on the clean cwest/integration base with these changes stashed, so this change introduces zero regressions.

…on wake

An origin-routed kanban-transition wake arrives at the platform adapter as a
plain TEXT MessageEvent. When the origin session is mid-turn, handle_message
takes the busy branch and either text-debounces the event or newline-merges it
into the in-progress/queued turn via merge_pending_message_event(merge_text=True)
— so the wake is absorbed silently instead of producing its own identifiable
turn (the busy-session swallow).

Tag the origin-routed wake on the webhook side (metadata["kanban_transition_wake"]
= True), keyed on the emitter-stamped origin_* fields so ordinary webhook routes
are unaffected. A pure predicate is_transition_wake_event reads the flag; a
wake-precedence branch in merge_pending_message_event keeps the wake in the
single pending slot un-merged and un-clobbered (a pending wake is never
overwritten by a later non-wake, and an incoming wake replaces a pending non-wake
intact — a dropped autonomy wake is worse than a user follow-up the user can
resend); and the busy branch queues the wake without debounce/merge so the
existing in-band drain cascade runs it as a distinct turn.

Cache- and alternation-safe: no system-prompt or message-history mutation, no
new drain site, no synthetic mid-loop user message, _active_sessions lifecycle
untouched — the wake is delivered as an ordinary next-turn user message via the
same cascade every follow-up uses.
@cwest
cwest marked this pull request as ready for review July 1, 2026 22:19

@cwest cwest left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified end to end. The busy-branch bypass sits in the right place inside the active-session block: after the command and clarify intercepts and the photo branch, before the text-debounce path, so a wake (a plain text event, no command) reaches it and gets queued un-merged instead of being debounced or newline-merged into an unrelated turn. The single-slot precedence in merge_pending_message_event is complete across all four cases — pending wake plus incoming non-wake drops the non-wake, an incoming wake always takes the slot intact, and two non-wakes still fall through to the existing media/text merge untouched. The webhook tag keys on origin_platform + origin_chat_id, the same discriminator _build_origin_source already uses and a field pair only the transition emitter stamps, so GitHub, monitoring, and cron routes are never tagged.

Proof I ran, not assumed:

  • RED->GREEN confirmed: with the busy-branch bypass removed from a throwaway checkout at the head SHA, test_busy_session_queues_wake_un_merged_without_debounce fails with "wake was swallowed (debounced or dropped)"; restored, all 11 tests across the two touched files pass.
  • Regression: 203 passed, 0 failed across the pending-merge, busy-session, text-batching, photo-interrupt, drain-race, internal-event, thread-origin, and webhook test modules — every path that would break if the precedence guard or the bypass disturbed ordinary merging. The wider gateway suite's remaining reds are the pre-existing optional-platform env gaps (whatsapp/matrix/wecom) unrelated to this change.
  • No new secrets, injection, eval/exec, or unsafe deserialization in the added lines. metadata is a default_factory dict, so the tag assignment is safe.

Invariants hold: the wake rides the same next-turn drain cascade every follow-up uses, so no synthetic mid-loop message, no new drain site, no system-prompt or history mutation, and the _active_sessions lifecycle is untouched. Merge-readiness is green — mergeable, all required checks passing, no unresolved threads, mergeStateStatus CLEAN. Ready to merge.

@cwest
cwest merged commit c5b1f1f into cwest/integration Jul 1, 2026
31 checks passed
@cwest
cwest deleted the topic/wake-distinct-self-announcing-turn branch July 1, 2026 22:20
cwest added a commit that referenced this pull request Jul 26, 2026
…on wake (#29)

An origin-routed kanban-transition wake arrives at the platform adapter as a
plain TEXT MessageEvent. When the origin session is mid-turn, handle_message
takes the busy branch and either text-debounces the event or newline-merges it
into the in-progress/queued turn via merge_pending_message_event(merge_text=True)
— so the wake is absorbed silently instead of producing its own identifiable
turn (the busy-session swallow).

Tag the origin-routed wake on the webhook side (metadata["kanban_transition_wake"]
= True), keyed on the emitter-stamped origin_* fields so ordinary webhook routes
are unaffected. A pure predicate is_transition_wake_event reads the flag; a
wake-precedence branch in merge_pending_message_event keeps the wake in the
single pending slot un-merged and un-clobbered (a pending wake is never
overwritten by a later non-wake, and an incoming wake replaces a pending non-wake
intact — a dropped autonomy wake is worse than a user follow-up the user can
resend); and the busy branch queues the wake without debounce/merge so the
existing in-band drain cascade runs it as a distinct turn.

Cache- and alternation-safe: no system-prompt or message-history mutation, no
new drain site, no synthetic mid-loop user message, _active_sessions lifecycle
untouched — the wake is delivered as an ordinary next-turn user message via the
same cascade every follow-up uses.

(cherry picked from commit c5b1f1f)
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