fix: persist API delegation units once without waking the model

This commit is contained in:
Teknium
2026-09-07 02:35:42 -07:00
parent 705bcdcf37
commit d5926b2494
15 changed files with 293 additions and 580 deletions

View File

@@ -43,7 +43,7 @@ class _Batch:
origin_ui_session_id: str
origin_owner_transport: Any
origin_owner_session_record: Any
origin_wake_capable: bool
origin_session_history_delivery: bool
overall_start: float
# Set on per-group units carved out by ``_dispatch_background``; None for the whole batch / ungrouped units.
group: Optional[str] = None
@@ -68,7 +68,7 @@ def _announce_batch(parent_agent, n_tasks: int, live_deleg_id: Optional[str]) ->
_print_completion_line(parent_agent, getattr(parent_agent, "_delegate_spinner", None), _hdr, console_line=_hdr)
def _capture_origin() -> tuple[str, str, Any, Any, bool]:
"""``(wake_sid, ui_session_id, owner_transport, owner_session_record, wake_capable)`` of the
"""``(wake_sid, ui_session_id, owner_transport, owner_session_record, session_history_delivery)`` of the
ORIGINATING session, captured BEFORE building any child: AIAgent construction
clobbers the HERMES_SESSION_ID ContextVar/os.environ with the subagent's id. The wake-
capability flag rides the same request-scoped binding and is captured here for the same
@@ -77,12 +77,12 @@ def _capture_origin() -> tuple[str, str, Any, Any, bool]:
from tools.async_delegation import _current_origin_session_id
_origin_wake_sid = _current_origin_session_id()
_origin_ui_session_id = ""
_origin_wake_capable = False
_origin_session_history_delivery = False
with _quiet(None):
from gateway.session_context import get_session_env, wake_capable_session
from gateway.session_context import get_session_env, session_history_delivery_supported
_origin_ui_session_id = get_session_env("HERMES_UI_SESSION_ID", "")
_origin_wake_capable = wake_capable_session()
return (_origin_wake_sid, _origin_ui_session_id, *_capture_gateway_steer_authority(_origin_ui_session_id), _origin_wake_capable)
_origin_session_history_delivery = session_history_delivery_supported()
return (_origin_wake_sid, _origin_ui_session_id, *_capture_gateway_steer_authority(_origin_ui_session_id), _origin_session_history_delivery)
def _report_child_done(parent_agent, spinner_ref, entry, tag, task_labels, n_tasks, remaining) -> None:
"""Print one completion line for a finished child and refresh the spinner text. Failed/errored/timed-out children
@@ -210,17 +210,11 @@ def _run_sync_with_note(batch: _Batch, reason: str) -> str:
result["note"] = _SYNC_FALLBACK_NOTES[reason]
return json.dumps(result, ensure_ascii=False)
def _resolve_async_wake_sid(origin_wake_sid: str, origin_wake_capable: bool = False) -> Optional[str]:
"""Wake target for a detached batch, or None to force synchronous execution.
def _resolve_async_wake_sid(origin_wake_sid: str, origin_session_history_delivery: bool = False) -> Optional[str]:
"""Detached result target: empty for push, a resumable API id, or None for inline.
Finite sessions (stateless HTTP requests, one-shot Kanban workers) cannot route a detached result back after their
turn/process ends — but if a raw, WAKE-CAPABLE session id is bound (one its client can address again: an explicit
X-Hermes-Session-Id, a native /api/sessions/{id} id, a /v1/runs id), gateway.wake can still reach it by self-POSTing
/v1/chat/completions, so only fall back to sync when there is truly no such id. A bound id alone is NOT enough
(#98619): a fingerprint-derived id from a header-less client makes the self-post hard-fail (no API_SERVER_KEY) or
land in a session whose history the client never reloads, so the result would be undeliverable by construction.
Uses the origin captured BEFORE child construction — HERMES_SESSION_ID here would be the subagent's internal id.
"""
API completion only persists a row; this does not authorize a model wake. The
continuation must read that row, not an authoritative caller-owned snapshot."""
try:
# Finite sessions cannot route a detached subagent result back to the agent after their turn/process
# ends. This includes stateless HTTP requests (#10760) and one-shot Kanban workers (#63169). Fall
@@ -231,18 +225,16 @@ def _resolve_async_wake_sid(origin_wake_sid: str, origin_wake_capable: bool = Fa
return ""
except Exception:
return ""
if origin_wake_sid and origin_wake_capable:
if origin_wake_sid and origin_session_history_delivery:
logger.info(
"delegate_task: async delivery unsupported on this session, but a wake-capable session id is bound (%s) — "
"dispatching in the background and waking the session via self-post when it completes instead of forcing "
"synchronous execution.", origin_wake_sid,
"delegate_task: session %s resumes server history — detached result will be persisted "
"for the next client turn (no model wake).", origin_wake_sid,
)
return origin_wake_sid
if origin_wake_sid:
logger.info(
"delegate_task: session id %s is bound but not wake-capable (fingerprint-derived for a header-less client "
"— the wake self-post cannot deliver where the client will read it, #98619) — running the batch "
"synchronously instead.", origin_wake_sid,
"delegate_task: session %s has no declared server-history consumer — running the batch "
"synchronously so the result returns in this turn.", origin_wake_sid,
)
return None
@@ -380,7 +372,7 @@ def _dispatch_background(batch: _Batch) -> str:
running synchronously (with an explanatory ``note``) when the session cannot receive detached completions or the
async pool is at capacity."""
from tools.delegate_tool import _get_max_async_children
wake_sid = _resolve_async_wake_sid(batch.origin_wake_sid, batch.origin_wake_capable)
wake_sid = _resolve_async_wake_sid(batch.origin_wake_sid, batch.origin_session_history_delivery)
if wake_sid is None:
logger.info("delegate_task: async delivery unsupported on this session runtime; running the batch synchronously instead.")
return _run_sync_with_note(batch, "no_async")