fix(api_server): bind chat_id (raw session id) on /v1/runs

/v1/runs bound only session_key at its _bind_api_server_session call, so
tools.async_delegation._current_origin_session_id() — which reads the
request-scoped HERMES_SESSION_CHAT_ID — returned "" on that route and
runs-originated background delegations stayed forced-sync with no wake
target. Bind chat_id/session_id the same way the other agent-entry routes
do via _run_agent(). Follow-up to #64998 (sweeper review F2).
This commit is contained in:
Teknium 2026-07-23 08:36:37 -07:00
parent 2cc0ff44b6
commit 0796a981d7
2 changed files with 55 additions and 0 deletions

View File

@ -5101,7 +5101,17 @@ class APIServerAdapter(BasePlatformAdapter):
# environment state.
approval_token = set_current_session_key(approval_session_key)
session_tokens = self._bind_api_server_session(
# chat_id carries the raw session id (the
# X-Hermes-Session-Id equivalent) exactly like
# the other agent-entry routes bind it via
# _run_agent(). Without it,
# tools.async_delegation reads an empty
# HERMES_SESSION_CHAT_ID on /v1/runs and
# background delegations stay forced-sync
# (no wake target).
chat_id=session_id or "",
session_key=approval_session_key,
session_id=session_id or "",
)
register_gateway_notify(approval_session_key, _approval_notify)
r = agent.run_conversation(

View File

@ -145,6 +145,51 @@ class TestStartRun:
assert status["status"] in {"queued", "running", "completed"}
assert status["object"] == "hermes.run"
@pytest.mark.asyncio
async def test_start_binds_chat_id_for_delegation_wake_target(self, adapter):
"""/v1/runs must bind the raw session id as the api_server chat_id
(like every other agent-entry route does via _run_agent): the async
delegation dispatch reads HERMES_SESSION_CHAT_ID to pick its wake
self-post target, and an empty binding forces background delegations
on this route back to synchronous execution."""
app = _create_runs_app(adapter)
captured = {}
async with TestClient(TestServer(app)) as cli:
with patch.object(adapter, "_create_agent") as mock_create:
mock_agent = MagicMock()
def _capture_run(user_message=None, conversation_history=None, task_id=None):
from tools.async_delegation import _current_origin_session_id
captured["origin_session_id"] = _current_origin_session_id()
return {"final_response": "done"}
mock_agent.run_conversation.side_effect = _capture_run
mock_agent.session_prompt_tokens = 0
mock_agent.session_completion_tokens = 0
mock_agent.session_total_tokens = 0
mock_create.return_value = mock_agent
resp = await cli.post(
"/v1/runs",
json={"input": "hello", "session_id": "runs-raw-sid"},
)
assert resp.status == 202
data = await resp.json()
run_id = data["run_id"]
for _ in range(40):
status_resp = await cli.get(f"/v1/runs/{run_id}")
status = await status_resp.json()
if status["status"] == "completed":
break
await asyncio.sleep(0.05)
assert captured.get("origin_session_id") == "runs-raw-sid", (
"runs route must bind chat_id so delegation dispatch sees a wake target"
)
@pytest.mark.asyncio
async def test_start_invalid_json_returns_400(self, adapter):
app = _create_runs_app(adapter)