mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
fix(grok): close the panel relay when the cost-cap kills an interactive chat (M4)
_enforce_grok_cost_budget killed + evicted a container directly. For the interactive roles (intake/secretary) that left the panel SSE relay open with no close sentinel, so the chat froze with no explanation. Add PrompterLiveRegistry.close_by_agent (push a final error event, then close every session bound to that agent) and call it from the cost-cap watchdog when the killed agent is the intake or secretary, so the panel reports the chat ended on the cost cap instead of hanging.
This commit is contained in:
@@ -3963,6 +3963,15 @@ class AgentOrchestrator:
|
||||
)
|
||||
continue
|
||||
self._instances.pop(agent_id, None)
|
||||
# Interactive roles (intake/secretary) have an open panel relay; a
|
||||
# raw kill would leave the SSE hanging (frozen chat). Close it with a
|
||||
# reason so the panel reports why the chat ended.
|
||||
if agent_id in (INTAKE_AGENT_ID, SECRETARY_AGENT_ID):
|
||||
from roboco.services.prompter_live import get_live_registry
|
||||
|
||||
get_live_registry().close_by_agent(
|
||||
agent_id, error="Chat ended: the Grok cost cap was exceeded."
|
||||
)
|
||||
logger.warning(
|
||||
"grok container killed: cost ceiling exceeded",
|
||||
agent_id=agent_id,
|
||||
|
||||
@@ -101,6 +101,25 @@ class PrompterLiveRegistry:
|
||||
session.queue.put_nowait(_CLOSE)
|
||||
self.log.info("Live intake session closed", session_id=session_id)
|
||||
|
||||
def close_by_agent(self, agent_id: str, *, error: str | None = None) -> list[str]:
|
||||
"""Close every live session bound to ``agent_id`` (e.g. on a forced kill).
|
||||
|
||||
When the orchestrator kills an interactive container out-of-band (the
|
||||
Grok cost-cap watchdog), the relay would otherwise stay open and the
|
||||
panel SSE would hang forever. This pushes an optional final ``error``
|
||||
event so the panel shows WHY the chat ended, then closes the stream.
|
||||
Returns the closed session ids.
|
||||
"""
|
||||
closed: list[str] = []
|
||||
for session_id, session in list(self._sessions.items()):
|
||||
if session.agent_id != agent_id or session.closed:
|
||||
continue
|
||||
if error:
|
||||
session.queue.put_nowait({"kind": "error", "text": error})
|
||||
self.close(session_id)
|
||||
closed.append(session_id)
|
||||
return closed
|
||||
|
||||
def park(self, session_id: str, task_id: str) -> bool:
|
||||
"""Mark a session as parked awaiting board review of ``task_id``.
|
||||
|
||||
|
||||
@@ -22,6 +22,20 @@ def test_open_get_close() -> None:
|
||||
assert reg.get("s1") is None
|
||||
|
||||
|
||||
def test_close_by_agent_closes_matching_sessions_with_error() -> None:
|
||||
reg = PrompterLiveRegistry()
|
||||
reg.open("s1", "intake-1")
|
||||
reg.open("s2", "secretary-1") # different agent — must survive
|
||||
closed = reg.close_by_agent("intake-1", error="cost cap")
|
||||
assert closed == ["s1"]
|
||||
assert reg.get("s1") is None # closed + popped
|
||||
assert reg.is_alive("s2") # untouched
|
||||
# The error event is queued before the close sentinel so the panel sees it.
|
||||
sess = reg.open("s3", "intake-1")
|
||||
reg.close_by_agent("intake-1", error="boom")
|
||||
assert sess.queue.get_nowait() == {"kind": "error", "text": "boom"}
|
||||
|
||||
|
||||
def test_park_and_find_by_task() -> None:
|
||||
"""A parked session is discoverable by task id for board-feedback injection."""
|
||||
reg = PrompterLiveRegistry()
|
||||
|
||||
Reference in New Issue
Block a user