From 7e0a1974b67d391e85c3701a076837fc8ad2d554 Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Sun, 19 Apr 2026 14:21:33 +0800 Subject: [PATCH] fix(box): handle RPC failure in get_status/get_sessions gracefully When the Box runtime disconnects, there is a race between the heartbeat flipping _available=false and the frontend polling get_status(). If the poll arrives first, client.get_status() throws a ConnectionClosedError which propagated as a 500, causing the frontend to show a grey dot (null status) instead of a red dot with error details. Now get_status() catches RPC errors and returns available=false with the exception message as connector_error. get_sessions() returns an empty list when unavailable or on RPC failure. --- src/langbot/pkg/box/service.py | 19 +++++++++++++++++-- tests/unit_tests/box/test_box_service.py | 1 + 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/langbot/pkg/box/service.py b/src/langbot/pkg/box/service.py index 71f950f0..7f89459f 100644 --- a/src/langbot/pkg/box/service.py +++ b/src/langbot/pkg/box/service.py @@ -219,7 +219,12 @@ class BoxService: self._shutdown_task = loop.create_task(self.shutdown()) async def get_sessions(self) -> list[dict]: - return await self.client.get_sessions() + if not self._available: + return [] + try: + return await self.client.get_sessions() + except Exception: + return [] def build_spec(self, spec_payload: dict, skip_host_mount_validation: bool = False) -> BoxSpec: spec_payload = dict(spec_payload) @@ -581,7 +586,17 @@ class BoxService: 'recent_error_count': len(self._recent_errors), 'connector_error': self._connector_error, } - runtime_status = await self.client.get_status() + try: + runtime_status = await self.client.get_status() + except Exception as exc: + # RPC failed — the runtime likely just disconnected and the + # heartbeat hasn't flipped _available yet. + return { + 'available': False, + 'profile': self.profile.name, + 'recent_error_count': len(self._recent_errors), + 'connector_error': str(exc), + } return { **runtime_status, 'available': True, diff --git a/tests/unit_tests/box/test_box_service.py b/tests/unit_tests/box/test_box_service.py index dbe71a6d..38be8301 100644 --- a/tests/unit_tests/box/test_box_service.py +++ b/tests/unit_tests/box/test_box_service.py @@ -182,6 +182,7 @@ async def test_box_service_get_sessions_delegates_to_client(): client.get_sessions = AsyncMock(return_value=[{'session_id': 'test-session'}]) service = BoxService(make_app(Mock()), client=client) + service._available = True sessions = await service.get_sessions()