Skip to content

Commit a7f463b

Browse files
committed
fix(agent-server): emit interrupted status when the goal loop hits an unexpected error
1 parent 610bbcd commit a7f463b

2 files changed

Lines changed: 29 additions & 0 deletions

File tree

openhands-agent-server/openhands/agent_server/event_service.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1148,6 +1148,12 @@ def _persist() -> None:
11481148
raise
11491149
except Exception:
11501150
logger.exception("Goal loop failed")
1151+
# An unexpected failure (judge LLM error, controller bug, ...) leaves
1152+
# the loop dead: record an interrupted status (resumable) so the UI
1153+
# doesn't show it running. Skip during close(), like the cancel path.
1154+
if not self._closing:
1155+
with suppress(Exception):
1156+
await _emit_status(active=False, status="interrupted")
11511157
finally:
11521158
self._goal_task = None
11531159

tests/agent_server/test_goal_loop.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,3 +381,26 @@ async def test_goal_halts_on_run_error_as_interrupted(event_service, tmp_path):
381381
assert event_service._goal_outcome is None
382382
finally:
383383
await event_service.close()
384+
385+
386+
@pytest.mark.asyncio
387+
async def test_goal_emits_interrupted_on_unexpected_error(event_service, tmp_path):
388+
# A judge LLM that *raises* (e.g. a network error) crashes the loop via the
389+
# generic `except Exception` path -- distinct from a run error surfaced as
390+
# ConversationExecutionStatus.ERROR (test above). The loop must still record
391+
# a terminal interrupted (resumable) status; otherwise the last persisted
392+
# event stays active=True/running and the UI shows a dead goal as running.
393+
await _start(event_service, tmp_path, "did the work")
394+
judge = TestLLM.from_messages(
395+
[RuntimeError("judge network error")], usage_id="judge"
396+
)
397+
try:
398+
await event_service.start_goal("build x", judge_llm=judge, max_iterations=5)
399+
await asyncio.wait_for(event_service._goal_task, timeout=15)
400+
401+
updates = _goal_status_updates(event_service)
402+
assert updates[-1]["status"] == "interrupted"
403+
assert updates[-1]["active"] is False
404+
assert event_service._goal_outcome is None
405+
finally:
406+
await event_service.close()

0 commit comments

Comments
 (0)