Skip to content

fix(delegation): route async results to their origin session across compression (#55578) - #60863

Merged
teknium1 merged 2 commits into
mainfrom
fix/async-delegation-origin-routing
Jul 8, 2026
Merged

fix(delegation): route async results to their origin session across compression (#55578)#60863
teknium1 merged 2 commits into
mainfrom
fix/async-delegation-origin-routing

Conversation

@teknium1

@teknium1 teknium1 commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Summary

Async delegation completions now carry a precise return address — the live UI session that commissioned them — and compression-rotated session keys are resolved to their continuation before an event is treated as orphaned. This stops detached results from being claimed by whichever desktop poller wakes first (the core misrouting behind #55578).

Salvage of #59767 by @dschnurbusch, cherry-picked with authorship preserved (clean pick, +262/−20 across 6 files, includes tests).

Changes

  • gateway/session_context.py: new HERMES_UI_SESSION_ID context var — the live frontend tab/window id, deliberately separate from the durable session-db id.
  • tools/delegate_tool.py / tools/async_delegation.py: capture origin_ui_session_id at dispatch and carry it on completion events (single + batch).
  • tui_gateway/server.py: ownership check prefers the commissioning UI session; if that window is gone, falls back to durable session_key routing; compressed keys are resolved through resolve_resume_session_id so a pre-compression dispatch maps to the live continuation instead of becoming a free-for-all orphan — and a stale parent tab can't steal the continuation's result. Finalized sessions are excluded from ownership claims.
  • Tests: event-field propagation + ownership-routing coverage in tests/tools/test_async_delegation.py and tests/test_tui_gateway_server.py.

Supersedes #57586 (compression-chain half is included here) and the TUI-path portion of #35667.

Validation

Before After
completion for tab A, tab B polls first tab B injects it tab B skips; tab A delivers
commissioning window closed, session resumed under same durable key any poller resumed session claims it (E2E-verified)
event dispatched pre-compression orphaned → free-for-all routed to chain continuation (real SessionDB E2E)
targeted tests 619 passed (tui_gateway + async delegation suites)

Part of #55578.

Follow-up commit: fail-closed ownership + session-scoped lifecycle

Per review, two invariants added on top of the salvaged commit (24bf55f):

  • Fail closed on orphans. An async-delegation completion is injected ONLY into a session that provably owns it (origin UI id, or session-key/lineage match through the compression chain). Unowned payloads were previously adopted by whichever poller saw them first — a hard cross-chat leak; they are now dropped from injection with a WARNING (the subagent's output remains in the delegation records/output store). Same rule in the shutdown drain. Non-delegation events (background process completions) keep the historical adopt behavior.
  • Delegations die with their session. _finalize_session now interrupts the closing session's in-flight async delegations via a new interrupt_for_session() (per-session sibling of interrupt_all()): always by commissioning UI id; by durable session key only when the TUI owns the lifecycle — closing a viewer tab on a live gateway session never kills the gateway's own background work.

10 new tests (tests/tui_gateway/test_delegation_session_lifecycle.py); 661 targeted tests green.

Infographic

async-delegation-origin-routing

@alt-glitch alt-glitch added type/bug Something isn't working comp/gateway Gateway runner, session dispatch, delivery tool/delegate Subagent delegation comp/tui Terminal UI (ui-tui/ + tui_gateway/) sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages P2 Medium — degraded but workaround exists labels Jul 8, 2026

@tonydwb tonydwb left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review Summary

Verdict: Approved (LGTM)

Salvage of #59767: async delegation completions now carry the commissioning UI session ID and compressed session keys are resolved to their continuation before treating an event as orphaned. Fixes results delivered to wrong tab when user switches sessions during delegation.

Changes

  • gateway/session_context.py: new HERMES_UI_SESSION_ID context var
  • tools/delegate_tool.py + tools/async_delegation.py: capture origin_ui_session_id at dispatch
  • tui_gateway/server.py: ownership check prefers commissioning UI session; falls back to durable session_key; resolves compressed keys via resolve_resume_session_id

Quality

  • Well-scoped: 6 files, 262 additions / 20 deletions
  • Tests: 619 passed (tui_gateway + async delegation suites)
  • Clear before/after table and root cause analysis
  • Part of #55578 async-delegation session-split stack

Reviewed by Hermes Agent

dschnurbusch and others added 2 commits July 8, 2026 06:39
Carry the live TUI session id with async delegation completion events and prefer the commissioning UI session when desktop pollers share the completion queue. Resolve compressed session keys to their continuation before treating events as orphaned, and capture the live parent agent session id for TUI/ACP dispatch.
…ion lifecycle

Two invariants layered on the origin-routing commit (#55578):

1. Fail closed on orphaned async-delegation payloads. The poller's
   belongs-elsewhere check handles events owned by another LIVE session,
   but an event whose owner is gone previously fell through and was
   adopted by whichever poller saw it - injecting one chat's delegation
   output into another chat. Delegation completions are now injected
   only into a session that PROVABLY owns them (origin UI id, or
   session-key/lineage match via the compression chain); unowned
   payloads are dropped from injection with a WARNING (the subagent's
   output is already persisted in the delegation records, so nothing is
   lost). The shutdown drain applies the same rule. Non-delegation
   events keep the historical adopt-orphans behavior.

2. A session's in-flight async delegations end with the session.
   _finalize_session now calls interrupt_for_session(): delegations
   commissioned by the closing UI session are interrupted always;
   key-matched delegations only when the TUI owns the session lifecycle,
   so closing a viewer tab on a live gateway session never kills the
   gateway's own background work.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/gateway Gateway runner, session dispatch, delivery comp/tui Terminal UI (ui-tui/ + tui_gateway/) P2 Medium — degraded but workaround exists sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state tool/delegate Subagent delegation type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants