Skip to content

fix(tui): route async delegation results across the compression chain - #57586

Closed
Bartok9 wants to merge 2 commits into
NousResearch:mainfrom
Bartok9:fix/57576-async-delegation-compression-chain
Closed

fix(tui): route async delegation results across the compression chain#57586
Bartok9 wants to merge 2 commits into
NousResearch:mainfrom
Bartok9:fix/57576-async-delegation-compression-chain

Conversation

@Bartok9

@Bartok9 Bartok9 commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Async-delegation (and other background-process) completions now route to the launching TUI session even after that session has context-compressed.
  • Fixes results being silently delivered to the wrong conversation.

Motivation

Closes #57576.

All desktop TUI sessions share one process-wide completion_queue. Each session's poller calls _notification_event_belongs_elsewhere() to skip events it doesn't own, comparing evt["session_key"] against session["session_key"].

Context compression rotates session["session_key"] to a new continuation id (_reanchor_session_key_after_compression). A background delegation dispatched before compression recorded the old key. Once the launching session compresses, it no longer recognizes its own completion event — the event looks orphaned, and whichever poller dequeues first grabs it. The result is delivered to an unrelated conversation, and the dispatching session never receives its result.

Fix

  • Preserve rotated-out keys in a per-session session_key_chain when compression re-anchors the key (_record_session_key_history).
  • Match ownership against the whole chain via a new _session_owns_key() helper, used in both branches of _notification_event_belongs_elsewhere() (current-session check + live-owner scan).

This is Option A from the issue (track the compression chain). The dispatching session reclaims its event; unrelated live sessions still defer to the true owner.

Verification

  • python3 -m pytest tests/test_tui_gateway_server.py -k "notification_event_routing or record_session_key_history" — 3 passed
  • Added regression tests:
    • test_notification_event_routing_follows_compression_chain — a completion dispatched under a pre-compression key routes to the (now-rotated) launching session, and an unrelated session defers.
    • test_record_session_key_history_dedupes_and_preserves_order
  • Confirmed the routing test fails on main without the fix.
  • Did NOT change: 3 pre-existing failures in this file (test_browser_manage_connect_default_local_reports_launch_hint, test_persist_model_switch_*) are unrelated env issues (No module named 'ruamel') and fail identically on main.

Closes NousResearch#57576

All desktop TUI sessions share one process-wide completion_queue. Each
session poller uses _notification_event_belongs_elsewhere() to skip events
it does not own, comparing evt["session_key"] to session["session_key"].

Context compression rotates session["session_key"] to a new continuation id
(_reanchor_session_key_after_compression). A background delegation dispatched
BEFORE compression recorded the old key, so after the launching session
compresses it no longer recognizes its own completion event. The event looks
orphaned and whichever poller dequeues first grabs it — delivering the result
to the wrong conversation (and the dispatcher never gets its result).

Fix: preserve rotated-out keys in a per-session session_key_chain and match
ownership against the chain (_session_owns_key) instead of only the current
key. The dispatching session reclaims its event; unrelated sessions still
defer to the true owner.

Tests: routing now follows the compression chain; chain recording dedupes
and preserves order. Verified both fail without the fix.
@alt-glitch alt-glitch added type/bug Something isn't working comp/tui Terminal UI (ui-tui/ + tui_gateway/) tool/delegate Subagent delegation 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 3, 2026
@ruanbarroso

Copy link
Copy Markdown

Thanks for the fix — the session_key_chain approach looks like the right direction for #57576, and it matches the failure mode where a completion was dispatched under a pre-compression key but the launching TUI session later rotated to a continuation session.

I tested the PR locally with the targeted regression tests:

uv run --extra dev pytest tests/test_tui_gateway_server.py -k 'notification_event_routing or record_session_key_history' -q -o 'addopts='
# 3 passed, 302 deselected

I also simulated the incident shape we observed:

  • launcher current key: 20260704_172726_89cdf8
  • launcher chain: [20260704_171936_1bc6c5]
  • unrelated session key: 20260704_173437_7c82f0
  • async delegation event key: 20260704_171936_1bc6c5

With this PR, the launcher handles the event and the unrelated session defers, so this would likely prevent the observed cross-session delivery while the launching session is still live.

One remaining edge case seems worth covering before closing the whole bug class: if an async_delegation event carries a non-empty session_key, but no live session currently owns that key or has it in its session_key_chain, the existing fallback still lets the current poller handle it as an orphan. For async delegation, that can still inject a completion block into an arbitrary unrelated conversation.

Suggested additional behavior/test:

  • If evt["type"] == "async_delegation" and evt["session_key"] is non-empty:
    • current session owns current key or chain -> handle it;
    • another live session owns current key or chain -> requeue/defer;
    • no live owner exists -> do not inject it into the current arbitrary session. Log/mark it as stale or orphaned, or retain it for the correct session to resume, but do not convert it into a synthetic user prompt in a different conversation.

So I think this PR likely fixes the reported #57576 manifestation, but adding the explicit-owner/no-live-owner regression would make the routing invariant much stronger: a background delegation result with an explicit owner must never be delivered to a session that does not own that key.

…ring

Per review on NousResearch#57586: an async_delegation completion carries an explicit
owner session_key. If no live session owns that key (directly or via its
session_key_chain), the previous fallback let the current poller convert it
into a synthetic user prompt in an arbitrary unrelated conversation.

Add _async_delegation_event_is_orphaned() and guard both the main poller
loop and the shutdown drain: explicit-owner + no-live-owner delegations are
dropped (logged) rather than injected elsewhere. Genuine global/system
events (empty session_key) and non-delegation events are unaffected.

Strengthens the routing invariant: a background delegation result with an
explicit owner must never be delivered to a session that does not own that
key.
@Bartok9

Bartok9 commented Jul 4, 2026

Copy link
Copy Markdown
Contributor Author

Great catch — that's exactly the remaining hole. Pushed a81a7a52b to close it.

Added _async_delegation_event_is_orphaned(session, evt) and wired it into both the main poller loop and the shutdown drain. The invariant is now:

  • async_delegation event with explicit session_key, current session owns it (key or chain) → handle it;
  • another live session owns it → defer/requeue (unchanged _notification_event_belongs_elsewhere path);
  • explicit session_key but no live ownerdrop it (logged as orphaned), never converted into a synthetic prompt in an arbitrary conversation.

Deliberately scoped narrowly so we don't regress the good cases:

  • empty session_key is still treated as a genuine global/system event and handled by the current poller (not dropped);
  • non-async_delegation event types are untouched.

New regression: test_async_delegation_orphan_not_delivered_to_arbitrary_session covers direct-owner, chain-owner, ghost/no-owner, empty-key, and wrong-type cases.

uv run --extra dev pytest tests/test_tui_gateway_server.py -k 'notification_event_routing or record_session_key_history or async_delegation_orphan' -q -o 'addopts='
# 4 passed, 302 deselected

@teknium1

teknium1 commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Closing — superseded by PR #60863 (salvage of #59767 + hardening), which includes the compression-chain resolution your PR pioneered: the ownership check now resolves an event's session_key through resolve_resume_session_id to the live continuation before treating it as orphaned, plus an origin-session return address stamped at dispatch and fail-closed handling for unowned payloads. Your diagnosis of the rotated-key orphan mechanism was correct and is credited in the salvage lineage. Thanks!

@teknium1 teknium1 closed this Jul 8, 2026
@Bartok9

Bartok9 commented Jul 8, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @teknium1 — glad the rotated-key orphan diagnosis and the compression-chain resolution carried forward into #60863. The origin-session return address stamped at dispatch plus fail-closed handling for unowned payloads is a stronger invariant than my drop-on-no-live-owner approach, since it can still route a late continuation correctly instead of discarding it. Happy to see it land in the salvage. 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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.

Async delegation result delivered to wrong TUI session after context compression

4 participants