fix(gateway): declare webhook sessions stateless so background delegations run inline (#69145) - #69159
Conversation
…tions run inline (NousResearch#69145) A webhook delivery is one-shot: the delivery_id is baked into the session key and on_processing_complete ends the per-delivery session the moment the run finishes, so the parent session can never receive a second turn. When the parent agent calls delegate_task (forced background=True at top level), the subagent's result arrives ~tens of seconds later — after ended_at is set — and the completion re-injection is dropped by the NousResearch#55578 fail-closed guard. The subagent's work is fully persisted to the delegation records but the parent is never woken, so it never summarizes or posts back; the user sees only the 'waiting for the subagent's result' placeholder (the NousResearch#53027/ NousResearch#63142 symptom on the gateway/webhook surface). NousResearch#66617 fixed this for hermes -z one-shot and cron by declaring a stateless channel (async_delivery=False) so delegate_task falls back to synchronous inline execution, but it only touched hermes_cli/oneshot.py and cron/scheduler.py — the gateway webhook runner was not covered. Set supports_async_delivery = False on WebhookAdapter (mirroring APIServerAdapter). GatewayRunner._set_session_env already propagates the adapter's flag into the _SESSION_ASYNC_DELIVERY contextvar, so top-level background delegations now run inline and return within the turn. MSGraphWebhookAdapter is intentionally left as-is: it is subscription-based (persistent Teams/Outlook conversations) and does not end sessions per-delivery, so it genuinely supports async delivery.
|
Thanks for the focused fix. The premise remains present on current Webhook delivery sessions are keyed by The added tests cover both the adapter capability and Automated hermes-sweeper review. |
SummaryTwo open PRs address #69145 by setting Related pull requests
Duplicates#69159 and revised #69166 are functional duplicates: both set Suggested consolidationKeep #69159 open with a salvage path: preserve its focused webhook capability flag and gateway-binding regression tests, consistent with its recorded best-fix status and keep-open review. Close #69166 as a duplicate of #69159; despite its keep-open review, its revised full diff now implements the same scope and tests after removing the only formerly distinct MSGraph change. Complex graphflowchart LR
classDef open fill:#dbeafe,stroke:#1d4ed8,color:#1e3a8a
classDef merged fill:#dcfce7,stroke:#15803d,color:#14532d
classDef closed fill:#e5e7eb,stroke:#6b7280,color:#1f2937
classDef unverified fill:#f3f4f6,stroke:#9ca3af,color:#374151
classDef best stroke-width:3px,stroke:#b45309
classDef target stroke-width:3px,stroke:#4338ca
I69145(["issue #69145 (open)"])
subgraph Dup69159 ["PRs duplicating each other"]
P69159["PR #69159 (open)"]
P69166["PR #69166 (open)"]
end
P69159 -->|best fix| I69145
class I69145 open
class P69159 open
class P69166 open
class P69159 best
class P69159 target
click I69145 "https://github.com/NousResearch/hermes-agent/issues/69145"
click P69159 "https://github.com/NousResearch/hermes-agent/pull/69159"
click P69166 "https://github.com/NousResearch/hermes-agent/pull/69166"
Graph: solid arrow = fixes / best fix, dashed arrow = partial or unverified (see edge label); boxed group = PRs duplicating each other; amber border = best fix; indigo border = target; gray node = closed (state tag in the node label). Cross-PR triage: Reviewed 2 pull requests and 1 issue in this complex. Each diff was read against this issue; Assessment working set: 10 kB of PR diffs, 11 kB of issue/PR text, 3 kB of discussion (4 comments), 4 verify verdicts. verdicts reflect diff content, not PR titles. Part of an automated triage batch. |
Summary
Fixes #69145. A webhook delivery is one-shot: the
delivery_idis baked into the session key andWebhookAdapter.on_processing_completeends the per-delivery session the moment the run finishes (gateway/platforms/webhook.py), so the parent session can never receive a second turn.When the parent agent calls
delegate_task(forcedbackground=Trueat top level), the subagent's result arrives tens of seconds later — afterended_atis set — and the completion re-injection is dropped by the #55578 fail-closed guard:The subagent's work is fully persisted to the delegation records, but the parent is never woken, so it never summarizes or posts back. The user sees only the "waiting for the subagent's result" placeholder — the #53027 / #63142 symptom, now on the gateway/webhook surface.
Root cause
#66617 (merged Jul 18) fixed this for
hermes -zone-shot and cron by declaring a stateless channel (async_delivery=False) sodelegate_taskfalls back to synchronous inline execution. But it only touchedhermes_cli/oneshot.pyandcron/scheduler.py— the gateway webhook runner was not covered.GatewayRunner._set_session_envalready propagates each adapter'ssupports_async_deliveryinto the_SESSION_ASYNC_DELIVERYcontextvar.APIServerAdaptersets itFalsefor exactly this reason;WebhookAdapterinherited the base defaultTrue, so webhook sessions boundasync_delivery=Trueand dispatched detached children whose completions could never be delivered.Fix
Set
supports_async_delivery = FalseonWebhookAdapter(one class attribute, mirroringAPIServerAdapter). Top-level background delegations on a webhook session now run inline and return within the turn.MSGraphWebhookAdapteris intentionally left unchanged: it is subscription-based (persistent Teams/Outlook conversations) and does not end sessions per-delivery, so it genuinely supports async delivery.Tests
tests/gateway/test_async_delivery_capability.py:test_webhook_false— locks the class-attribute invariant.TestWebhookSessionBindsStateless— end-to-end wiring throughGatewayRunner._set_session_env: a webhook session bindsasync_delivery_supported() == False, while a delivering adapter still bindsTrue(the fix must not disable delivery for real channels).Full
tests/gateway/test_webhook_adapter.py,test_webhook_session_close.py, andtest_async_delivery_capability.pypass (116 + 18). Preflight gates green vsupstream/main.