Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions gateway/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -10325,6 +10325,21 @@ async def _handle_message_with_agent(self, event, source, _quick_key: str, run_g

session_entry = self.session_store.get_or_create_session(source)
session_key = session_entry.session_key
pinned_session_id = str(
(getattr(event, "metadata", None) or {}).get("gateway_session_id") or ""
).strip()
if pinned_session_id and pinned_session_id != session_entry.session_id:
prior_session_id = session_entry.session_id
switched = self.session_store.switch_session(session_key, pinned_session_id)
if switched is not None:
session_entry = switched
logger.info(
"Pinned async-delegation completion to spawning session %s "
"(was %s) for routing key %s (#57498)",
pinned_session_id,
prior_session_id,
session_key,
)
self._cache_session_source(session_key, source)
if await asyncio.to_thread(self._is_telegram_topic_lane, source):
try:
Expand Down Expand Up @@ -14719,12 +14734,17 @@ async def _inject_watch_notification(self, synth_text: str, evt: dict) -> None:
if not adapter:
return
try:
metadata = {}
parent_session_id = str(evt.get("parent_session_id") or "").strip()
if parent_session_id:
metadata["gateway_session_id"] = parent_session_id
synth_event = MessageEvent(
text=synth_text,
message_type=MessageType.TEXT,
source=source,
internal=True,
message_id=str(evt.get("message_id") or "").strip() or None,
metadata=metadata,
)
logger.info(
"Watch pattern notification — injecting for %s chat=%s thread=%s",
Expand Down
2 changes: 2 additions & 0 deletions tests/tools/test_async_delegation.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ def runner():
res = ad.dispatch_async_delegation(
goal="compute X", context="some context", toolsets=["web", "file"],
role="leaf", model="test-model", session_key="agent:main:cli:dm:local",
parent_session_id="20260703_parent_sid",
runner=runner, max_async_children=3,
)
assert res["status"] == "dispatched"
Expand All @@ -108,6 +109,7 @@ def runner():
assert evt["type"] == "async_delegation"
assert evt["summary"] == "the result"
assert evt["session_key"] == "agent:main:cli:dm:local"
assert evt["parent_session_id"] == "20260703_parent_sid"
assert evt["delegation_id"] == res["delegation_id"]


Expand Down
11 changes: 11 additions & 0 deletions tools/async_delegation.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ def dispatch_async_delegation(
role: str,
model: Optional[str],
session_key: str,
parent_session_id: Optional[str] = None,
runner: Callable[[], Dict[str, Any]],
interrupt_fn: Optional[Callable[[], None]] = None,
max_async_children: int = _DEFAULT_MAX_ASYNC_CHILDREN,
Expand All @@ -145,6 +146,11 @@ def dispatch_async_delegation(
captured on the parent thread BEFORE dispatch, because the daemon
worker thread won't carry the contextvar. Used to route the
completion back to the originating session.
parent_session_id
The durable ``state.db`` session id of the parent agent that spawned
the delegation. Carried on the completion event so the gateway can
pin routing to the spawning session instead of recovering the latest
``ended_at IS NULL`` row for the peer tuple (#57498).
runner
Zero-arg callable that builds + runs the child and returns the same
result dict ``_run_single_child`` produces. Runs on the worker thread.
Expand Down Expand Up @@ -172,6 +178,7 @@ def dispatch_async_delegation(
"role": role,
"model": model,
"session_key": session_key,
"parent_session_id": parent_session_id,
"status": "running",
"dispatched_at": dispatched_at,
"completed_at": None,
Expand Down Expand Up @@ -282,6 +289,7 @@ def _push_completion_event(
# session_key routes the completion back to the originating gateway
# session; empty string => CLI (single-session) path.
"session_key": record.get("session_key", ""),
"parent_session_id": record.get("parent_session_id"),
"goal": record.get("goal", ""),
"context": record.get("context"),
"toolsets": record.get("toolsets"),
Expand Down Expand Up @@ -316,6 +324,7 @@ def dispatch_async_delegation_batch(
role: str,
model: Optional[str],
session_key: str,
parent_session_id: Optional[str] = None,
runner: Callable[[], Dict[str, Any]],
interrupt_fn: Optional[Callable[[], None]] = None,
max_async_children: int = _DEFAULT_MAX_ASYNC_CHILDREN,
Expand Down Expand Up @@ -356,6 +365,7 @@ def dispatch_async_delegation_batch(
"role": role,
"model": model,
"session_key": session_key,
"parent_session_id": parent_session_id,
"status": "running",
"dispatched_at": dispatched_at,
"completed_at": None,
Expand Down Expand Up @@ -453,6 +463,7 @@ def _finalize_batch(
"type": "async_delegation",
"delegation_id": delegation_id,
"session_key": event_record.get("session_key", ""),
"parent_session_id": event_record.get("parent_session_id"),
"goal": event_record.get("goal", ""),
"goals": event_record.get("goals"),
"context": event_record.get("context"),
Expand Down
2 changes: 2 additions & 0 deletions tools/delegate_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -2807,6 +2807,7 @@ def _execute_and_aggregate() -> dict:
return json.dumps(_sync_result, ensure_ascii=False)

_session_key = get_current_session_key(default="")
_parent_session_id = getattr(parent_agent, "session_id", None)
_child_agents = [c for (_, _, c) in children]

# Detach every child from the parent's interrupt-propagation list — the
Expand Down Expand Up @@ -2847,6 +2848,7 @@ def _batch_interrupt():
role=top_role,
model=creds["model"],
session_key=_session_key,
parent_session_id=_parent_session_id,
runner=_batch_runner,
interrupt_fn=_batch_interrupt,
max_async_children=_get_max_async_children(),
Expand Down
Loading