fix: resolve subagent control authority from the live session slot

Subagent list/tail/steer/interrupt authorized against a per-record copy of
the owning session's transport (`owner_transport`). That copy had to be
re-synced at every reattach site; `_rebind_live_transport` did it for
session.resume/activate but prompt.submit and the queued-prompt drain still
attached bare, so a client that reconnected through a prompt (the common
path on a remote gateway / Bot Mode switch) streamed fine while
`subagent.list` returned [] and controls rejected.

Read `owner_session_record["transport"]` at check time instead: the slot is
already mutated by every attach/detach/viewer-failover path, so no site can
forget the sync. `owner_transport` stays as the capture-time "commissioned
by a gateway session" marker (None = no RPC authority ever); non-dict owners
keep the exact-object rule. Drops the registration-time re-read and the
attach-time registry loop.

Diagnosis credit: nftpoetrist (#106663) — their prompt.submit / drain
regression tests pass against this change with no call-site edits.
This commit is contained in:
Teknium
2026-09-10 09:46:10 -07:00
parent 37eb6e1b05
commit ac07e20407
5 changed files with 52 additions and 25 deletions
+11 -6
View File
@@ -53,11 +53,6 @@ def _register_subagent(record: Dict[str, Any]) -> None:
return
record.setdefault("accepting_steer", True)
with _active_subagents_lock:
owner = record.get("owner_session_record")
if owner is not None and record.get("owner_transport") is not None:
# Child construction can finish after its captured dispatch transport
# was replaced. The exact session object retains generation authority.
record["owner_transport"] = owner.get("transport")
_active_subagents[sid] = record
def _unregister_subagent(subagent_id: str, *, agent: Any = None) -> None:
@@ -110,9 +105,19 @@ def interrupt_subagent(subagent_id: str) -> bool:
return False
def _subagent_transport_matches(record, transport) -> bool:
"""Authority follows the owning session's LIVE transport slot, read at check time.
``owner_transport`` on the record is only the capture-time marker that a gateway session
commissioned the child (``None`` = no RPC authority ever). The slot is authoritative because
every reattach path (prompt.submit, queued drain, resume, activate, viewer failover) already
mutates it; a per-record copy needed a matching registry sync at each of those sites and two
were missed (#106663). Records whose owner is not a session dict keep the exact-object rule."""
from tui_gateway.transport import FanoutTransport
bound = record.get("owner_transport")
if record.get("owner_transport") is None:
return False
owner = record.get("owner_session_record")
bound = owner.get("transport") if isinstance(owner, dict) else record.get("owner_transport")
return bound is transport or (isinstance(bound, FanoutTransport) and bound.contains(transport))