fix(acp): restore not-in-memory sessions off the event loop in every handler (#58083)
prompt / cancel / set_session_model / set_session_mode / set_config_option still called session_manager.get_session inline. For an id not in memory that runs _restore -> _make_agent (config, memory-provider import, SessionDB) on the loop — the hang class session/new just left — and, since restores are single-flight, it also parks the loop on _restore_lock while an off-loop session/load is in flight. Route the five sites through asyncio.to_thread like new/load/resume/fork. Test: a parametrized invariant over the five handlers with a slow DB restore; ticks=0 on the previous head, green now.
This commit is contained in:
@@ -628,7 +628,9 @@ class HermesACPAgent(SlashCommandsMixin, acp.Agent):
|
||||
return ResumeSessionResponse(**await self._session_response_fields(state, "resume"))
|
||||
|
||||
async def cancel(self, session_id: str, **kwargs: Any) -> None:
|
||||
state = self.session_manager.get_session(session_id)
|
||||
# get_session restores a not-in-memory id from the DB (full AIAgent build) and waits
|
||||
# on the restore lock — off the loop, like new/load/resume/fork (#58083).
|
||||
state = await asyncio.to_thread(self.session_manager.get_session, session_id)
|
||||
if not (state and state.cancel_event):
|
||||
return
|
||||
with state.runtime_lock:
|
||||
@@ -801,7 +803,7 @@ class HermesACPAgent(SlashCommandsMixin, acp.Agent):
|
||||
|
||||
async def prompt(self, prompt: list[PromptBlock], session_id: str, **kwargs: Any) -> PromptResponse:
|
||||
"""Run Hermes on the user's prompt and stream events back to the editor."""
|
||||
state = self.session_manager.get_session(session_id)
|
||||
state = await asyncio.to_thread(self.session_manager.get_session, session_id)
|
||||
if state is None:
|
||||
logger.error("prompt: session %s not found", session_id)
|
||||
return PromptResponse(stop_reason="refusal")
|
||||
@@ -996,7 +998,7 @@ class HermesACPAgent(SlashCommandsMixin, acp.Agent):
|
||||
|
||||
async def set_session_model(self, model_id: str, session_id: str, **kwargs: Any) -> SetSessionModelResponse | None:
|
||||
"""Switch the model for a session (called by ACP protocol)."""
|
||||
state = self.session_manager.get_session(session_id)
|
||||
state = await asyncio.to_thread(self.session_manager.get_session, session_id)
|
||||
if state:
|
||||
# switch_model() does synchronous network I/O (models.dev, custom-endpoint probes,
|
||||
# ~10 s cold) — off the loop, like the gateway, so other ACP sessions keep flowing.
|
||||
@@ -1011,7 +1013,7 @@ class HermesACPAgent(SlashCommandsMixin, acp.Agent):
|
||||
|
||||
async def set_session_mode(self, mode_id: str, session_id: str, **kwargs: Any) -> SetSessionModeResponse | None:
|
||||
"""Persist the editor-requested mode so ACP clients do not fail on mode switches."""
|
||||
state = self.session_manager.get_session(session_id)
|
||||
state = await asyncio.to_thread(self.session_manager.get_session, session_id)
|
||||
if state is None:
|
||||
logger.warning("Session %s: mode switch requested for missing session", session_id)
|
||||
return None
|
||||
@@ -1027,7 +1029,7 @@ class HermesACPAgent(SlashCommandsMixin, acp.Agent):
|
||||
self, config_id: str, session_id: str, value: str, **kwargs: Any
|
||||
) -> SetSessionConfigOptionResponse | None:
|
||||
"""Accept ACP config option updates even when Hermes has no typed ACP config surface yet."""
|
||||
state = self.session_manager.get_session(session_id)
|
||||
state = await asyncio.to_thread(self.session_manager.get_session, session_id)
|
||||
if state is None:
|
||||
logger.warning("Session %s: config update requested for missing session", session_id)
|
||||
return None
|
||||
|
||||
Reference in New Issue
Block a user