Skip to content

fix(gateway): dispatch origin-routed wake on the owning platform adapter - #34

Merged
cwest merged 2 commits into
cwest/integrationfrom
topic/wake-owning-adapter
Jul 2, 2026
Merged

fix(gateway): dispatch origin-routed wake on the owning platform adapter#34
cwest merged 2 commits into
cwest/integrationfrom
topic/wake-owning-adapter

Conversation

@cwest

@cwest cwest commented Jul 2, 2026

Copy link
Copy Markdown
Owner

The bug (the real root cause)

The three prior wake fixes (#29 distinct-turn, #32 busy-handler exemption, #33
stale-suppression exemption) were all correct — and all ran on the wrong
adapter instance
, so none of them ever fired for a live wake.

An origin-routed kanban-transition wake carries a SessionSource whose
platform is the origin platform (discord), targeting a session owned by the
discord adapter. But the webhook receiver dispatched it with
self.handle_message(event) — running it on the webhook adapter.

Each adapter keeps its own _active_sessions / _pending_messages. On the
webhook adapter the origin session looks idle, so the wake takes the cold path,
spawns a turn via the shared runner, collides with the already-running origin
turn at the runner's _running_agents guard, and is silently dropped — no
turn_context, banner never surfaces. Exactly the observed symptom.

The fix

Dispatch an origin wake through gateway_runner.adapters[origin_platform] (the
gateway reference already wired onto the webhook adapter for cross-platform
delivery), so busy-detection, the wake-precedence pending slot, and the post-turn
drain all operate on the same session state as the running origin turn. Falls
back to the webhook adapter when the owner can't be resolved (backward-compatible;
ordinary non-origin webhook events are unaffected).

How it was root-caused

Driving the real BasePlatformAdapter.handle_message in a standalone harness
proved the base-adapter logic was correct end-to-end (busy → queue → drain runs
the wake as a distinct turn). That eliminated base.py and pointed at the
adapter-instance split as the only remaining explanation, confirmed by reading
the webhook dispatch site.

Verification

  • New tests/gateway/test_326ce4d1_dispatch.py (5 tests):
    • selects the owning (discord) adapter for an origin wake;
    • falls back to the webhook adapter when the owner is missing;
    • leaves non-origin events on the webhook adapter;
    • end-to-end on the real handle_message: with the origin session busy on
      the discord adapter, the wake is queued as a distinct pending turn there;
    • demonstrates the pre-fix bug: dispatching on the webhook adapter leaves the
      owner's pending slot empty.
  • Full gateway suite: 8634 passed; the only bulk failures are the known
    cross-test-contamination flakes (telegram-markdown / shutdown-forensics /
    memory-timer / busy-session-ack), all green in isolation and independent of
    this change.

After merge

Requires a gateway restart (sys.modules-cached webhook.py) before the
end-to-end autonomous-wake path goes live.

An origin-routed kanban-transition wake carries a SessionSource whose platform
is the ORIGIN platform (e.g. discord) and targets a session owned by that
platform's adapter. The webhook receiver was processing the wake with
self.handle_message(event) — running it on the WEBHOOK adapter, not the origin
adapter.

Each adapter keeps its own _active_sessions / _pending_messages. Processed on
the webhook adapter, the wake never sees the origin session's busy state: it
takes the cold path, spawns a turn via the shared runner, collides with the
already-running origin turn at the runner's _running_agents guard, and is
silently dropped — no turn_context line, banner never surfaces. This is why the
adapter-level wake fixes never took effect: they ran on the wrong adapter
instance.

Dispatch an origin wake through gateway_runner.adapters[origin_platform] (the
reference already wired for cross-platform delivery) so busy-detection, the
wake-precedence pending slot, and the post-turn drain all operate on the same
session state as the running origin turn. Fall back to the webhook adapter when
the owning adapter can't be resolved (backward-compatible; ordinary non-origin
webhook events are unaffected).

Adds regression tests that drive the real handle_message on two independent
adapter instances: proving the wake reaches the owning adapter's pending slot
while the origin session is busy, and demonstrating the pre-fix bug where the
wrong adapter leaves the owner's pending slot empty.

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

The root cause here is right and the fix reads correctly. source is origin_source on the wake path, so keying the adapter lookup on source.platform resolves the origin platform's adapter, the owner is not self guard covers the webhook-is-origin edge, and the fallback to self keeps ordinary webhook events on their current path. I traced all of that against the file at head and ran the new suite locally: 5 passed.

One gap before this is done, plus a readiness note.

The routing decision that actually ships lives in _handle_webhook, but the three routing tests exercise a hand-copied _select_dispatch_adapter rather than that code. If the real block drifts (say source.platform becomes origin_source.platform, or the self-guard changes), those tests stay green while production breaks. The end-to-end pair (tests 4 and 5) drive the real handle_message, which is good, but nothing drives the real selection path in _handle_webhook. test_webhook_integration.py's cross-platform test already has the harness for this: a TestClient POST through the live route with a mocked gateway_runner.adapters and handle_message stubbed to capture which adapter it lands on. Anchoring one test to the real _handle_webhook and asserting the origin wake reaches the owning adapter's handle_message would close it and let the copy go.

Readiness: the PR is still a draft and the CI test slices are in progress (mergeStateStatus is UNSTABLE), so it isn't ready to land yet independent of the above.

Comment thread gateway/platforms/webhook.py
Comment thread tests/gateway/test_wake_owning_adapter_dispatch.py
Add two integration tests that POST an origin-wake payload through the live
aiohttp route (WebhookAdapter._handle_webhook) with a mocked
gateway_runner.adapters registry, and assert the SHIPPED dispatch-adapter
selection routes to the owning platform adapter (not the webhook adapter), plus
the fallback-to-webhook path when no owner is registered.

Closes the coverage gap where the routing unit tests asserted against a
hand-copied selection helper that could drift from production without failing.
Verified the new integration test fails against the pre-fix dispatch (owner
never called) and passes with it.

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

The gap from the last round is closed. The two new integration tests POST an origin-wake payload through the live aiohttp route and drive the shipped selection block in _handle_webhook directly, with a mocked adapters registry — so the routing decision is verified against production code, not a hand-copied re-implementation that could drift. I confirmed they bite: neutralizing the fix (forcing dispatch back onto self) turns test_real_handle_webhook_routes_origin_wake_to_owning_adapter red, and the fallback test stays green as it should. Full file at head passes, 7 tests, and CI is green.

One thing left before this is ready, and it is bookkeeping rather than code: the two review threads from the last round are still open. Please reply on each and resolve them so the conversation state matches the work. Once those are resolved this is done from my side.

@cwest
cwest marked this pull request as ready for review July 2, 2026 00: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.

This is ready. The routing fix is correct: on the wake path the source matches the origin, so the owning platform's adapter is resolved and the wake is dispatched there instead of on the webhook adapter that took the cold path before. The owner-is-self and owner-missing edges both fall back cleanly to the existing behavior.

What sealed it this round is the coverage. There's now a test that drives the real _handle_webhook selection block through the live route, not a hand-copied stand-in, so the production path is exercised and drift will surface. Neutralizing the fix turns that test red while the fallback test stays green, which is the property you want from it.

Checks are all green, the branch merges cleanly, and the two earlier threads are resolved. No changes needed.

@cwest
cwest merged commit 51ab0ef into cwest/integration Jul 2, 2026
31 checks passed
@cwest
cwest deleted the topic/wake-owning-adapter branch July 2, 2026 00:22
cwest added a commit that referenced this pull request Jul 26, 2026
…ter (#34)

* fix(gateway): dispatch origin-routed wake on the owning platform adapter

An origin-routed kanban-transition wake carries a SessionSource whose platform
is the ORIGIN platform (e.g. discord) and targets a session owned by that
platform's adapter. The webhook receiver was processing the wake with
self.handle_message(event) — running it on the WEBHOOK adapter, not the origin
adapter.

Each adapter keeps its own _active_sessions / _pending_messages. Processed on
the webhook adapter, the wake never sees the origin session's busy state: it
takes the cold path, spawns a turn via the shared runner, collides with the
already-running origin turn at the runner's _running_agents guard, and is
silently dropped — no turn_context line, banner never surfaces. This is why the
adapter-level wake fixes never took effect: they ran on the wrong adapter
instance.

Dispatch an origin wake through gateway_runner.adapters[origin_platform] (the
reference already wired for cross-platform delivery) so busy-detection, the
wake-precedence pending slot, and the post-turn drain all operate on the same
session state as the running origin turn. Fall back to the webhook adapter when
the owning adapter can't be resolved (backward-compatible; ordinary non-origin
webhook events are unaffected).

Adds regression tests that drive the real handle_message on two independent
adapter instances: proving the wake reaches the owning adapter's pending slot
while the origin session is busy, and demonstrating the pre-fix bug where the
wrong adapter leaves the owner's pending slot empty.

* test(gateway): drive the real _handle_webhook origin-wake selection path

Add two integration tests that POST an origin-wake payload through the live
aiohttp route (WebhookAdapter._handle_webhook) with a mocked
gateway_runner.adapters registry, and assert the SHIPPED dispatch-adapter
selection routes to the owning platform adapter (not the webhook adapter), plus
the fallback-to-webhook path when no owner is registered.

Closes the coverage gap where the routing unit tests asserted against a
hand-copied selection helper that could drift from production without failing.
Verified the new integration test fails against the pre-fix dispatch (owner
never called) and passes with it.

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