mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
fix(grok): surface a dead opencode-serve clearly instead of a zombie chat (M2)
If `opencode serve` died after the session opened, every subsequent turn failed with an opaque httpx connection error while the container lingered. send() now detects the exited subprocess (returncode set) and yields a clear error chunk + turn_end so the panel shows a real "session ended — start a new chat" message; the idle watchdog / a human reap then tears the container down.
This commit is contained in:
@@ -206,6 +206,22 @@ class OpencodeServeSession:
|
|||||||
"""Run one turn (synchronous message) and yield its normalized chunks."""
|
"""Run one turn (synchronous message) and yield its normalized chunks."""
|
||||||
if self._client is None or self._session_id is None:
|
if self._client is None or self._session_id is None:
|
||||||
raise RuntimeError("OpencodeServeSession used outside its context")
|
raise RuntimeError("OpencodeServeSession used outside its context")
|
||||||
|
# If the serve subprocess has died, the session is gone — every turn
|
||||||
|
# would otherwise fail with an opaque httpx connection error while the
|
||||||
|
# container lingers as a zombie. Surface it clearly (the panel shows a
|
||||||
|
# real message) and end the turn; the idle watchdog / a human reap tears
|
||||||
|
# the container down.
|
||||||
|
if self._proc is not None and self._proc.returncode is not None:
|
||||||
|
logger.error("opencode serve exited", returncode=self._proc.returncode)
|
||||||
|
yield StreamChunk(
|
||||||
|
kind="error",
|
||||||
|
text=(
|
||||||
|
f"opencode serve exited (rc={self._proc.returncode}); this "
|
||||||
|
"chat session ended — please start a new chat."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
yield StreamChunk(kind="turn_end", data={})
|
||||||
|
return
|
||||||
body: dict[str, Any] = {"parts": [{"type": "text", "text": text}]}
|
body: dict[str, Any] = {"parts": [{"type": "text", "text": text}]}
|
||||||
# Per-role reasoning effort: the orchestrator sets ROBOCO_GROK_VARIANT on
|
# Per-role reasoning effort: the orchestrator sets ROBOCO_GROK_VARIANT on
|
||||||
# the container; the serve message endpoint accepts a `variant` field
|
# the container; the serve message endpoint accepts a `variant` field
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
import pytest
|
||||||
from roboco.agent_sdk.opencode_session import (
|
from roboco.agent_sdk.opencode_session import (
|
||||||
|
OpencodeServeSession,
|
||||||
_extract_session_id,
|
_extract_session_id,
|
||||||
_message_error,
|
_message_error,
|
||||||
normalize_opencode_message,
|
normalize_opencode_message,
|
||||||
@@ -51,6 +53,21 @@ def test_fenced_draft_in_text_becomes_draft_chunk() -> None:
|
|||||||
assert draft.data["title"] == "Add login"
|
assert draft.data["title"] == "Add login"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_send_on_dead_serve_yields_clear_error(
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
# A crashed `opencode serve` must surface a clear error + end the turn, not
|
||||||
|
# hang the chat with opaque connection errors while the container zombies.
|
||||||
|
sess = OpencodeServeSession()
|
||||||
|
monkeypatch.setattr(sess, "_session_id", "ses-1")
|
||||||
|
monkeypatch.setattr(sess, "_client", object()) # unused: dead-proc guard wins
|
||||||
|
monkeypatch.setattr(sess, "_proc", type("P", (), {"returncode": 1})())
|
||||||
|
chunks = [c async for c in sess.send("hi")]
|
||||||
|
assert [c.kind for c in chunks] == ["error", "turn_end"]
|
||||||
|
assert "exited" in chunks[0].text
|
||||||
|
|
||||||
|
|
||||||
def test_propose_draft_tool_part_becomes_draft_chunk() -> None:
|
def test_propose_draft_tool_part_becomes_draft_chunk() -> None:
|
||||||
# The intake-tools.js propose_draft tool call (its input nested under
|
# The intake-tools.js propose_draft tool call (its input nested under
|
||||||
# `draft`) is intercepted into a draft chunk — NOT rendered as a tool_use —
|
# `draft`) is intercepted into a draft chunk — NOT rendered as a tool_use —
|
||||||
|
|||||||
Reference in New Issue
Block a user