From f4cb04a47fe007e9e8a74818b207dc1dd733d4be Mon Sep 17 00:00:00 2001 From: opencode Date: Sat, 8 Aug 2026 12:33:30 +0300 Subject: [PATCH] robustness: vision/depth render off the event loop (deepcopy snapshot), chat auto-reconnect + friendly errors, demo catches connection failures --- .../console-2026-08-08T09-11-01-487Z.log | 1 + chat_map.png | Bin 2159 -> 1916 bytes testbed/bridge.py | 19 +++++++--- testbed/chat.py | 34 ++++++++++++++---- testbed/demo.py | 13 +++++-- 5 files changed, 54 insertions(+), 13 deletions(-) diff --git a/.playwright-mcp/console-2026-08-08T09-11-01-487Z.log b/.playwright-mcp/console-2026-08-08T09-11-01-487Z.log index a98b051..ade2be7 100644 --- a/.playwright-mcp/console-2026-08-08T09-11-01-487Z.log +++ b/.playwright-mcp/console-2026-08-08T09-11-01-487Z.log @@ -95,3 +95,4 @@ [ 183147ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8001/' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8000/:40 [ 188179ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8001/' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8000/:40 [ 192874ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8001/' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8000/:40 +[ 1348483ms] [ERROR] WebSocket connection to 'ws://127.0.0.1:8001/' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://127.0.0.1:8000/:40 diff --git a/chat_map.png b/chat_map.png index 99ba7b740e1ed0564315d6a941bca59c7a97d8e3..6869f7eee6bbf56eaeab31a2df355875fa921170 100644 GIT binary patch literal 1916 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 VisionOutput: tick = self._tick() - img, _ = renderer.render(world) - buf = io.BytesIO() - img.save(buf, format="PNG") + # Render off the event loop so frames never stall the bridge under + # load; deepcopy keeps the snapshot consistent with this tool call. + snapshot = copy.deepcopy(world) + img, _ = await asyncio.to_thread(renderer.render, snapshot) + + def _encode() -> str: + buf = io.BytesIO() + img.save(buf, format="PNG") + return base64.b64encode(buf.getvalue()).decode("ascii") + return VisionOutput( - png_b64=base64.b64encode(buf.getvalue()).decode("ascii"), + png_b64=await asyncio.to_thread(_encode), width=VISION_WIDTH, height=VISION_HEIGHT, tick=tick, @@ -222,7 +230,8 @@ class RoomBridge(Bridge): ) async def depth(max_depth: float = 10.0) -> DepthOutput: tick = self._tick() - _, grid = renderer.render(world, max_depth=max_depth) + snapshot = copy.deepcopy(world) + _, grid = await asyncio.to_thread(renderer.render, snapshot, max_depth) return DepthOutput( width=DEPTH_WIDTH, height=DEPTH_HEIGHT, diff --git a/testbed/chat.py b/testbed/chat.py index 032813a..844db2e 100644 --- a/testbed/chat.py +++ b/testbed/chat.py @@ -191,12 +191,34 @@ async def chat_loop(client: AICCClient, manifest, args: argparse.Namespace) -> i async def run(args: argparse.Namespace) -> int: - transport = WebSocketClientTransport(args.url) - async with AICCClient(transport) as client: - manifest = await client.handshake() - print(f"[handshake] session {manifest.session_id} world {manifest.world.name}") - print(f"[handshake] tools: {[t.id for t in manifest.tools]}") - return await chat_loop(client, manifest, args) + """Run the chat session, reconnecting to the bridge if the connection dies.""" + attempts = 0 + while True: + try: + transport = WebSocketClientTransport(args.url) + async with AICCClient(transport) as client: + manifest = await client.handshake() + print( + f"[handshake] session {manifest.session_id} world {manifest.world.name}" + ) + print(f"[handshake] tools: {[t.id for t in manifest.tools]}") + return await chat_loop(client, manifest, args) + except asyncio.CancelledError: + raise + except KeyboardInterrupt: + raise + except Exception as exc: # noqa: BLE001 - connection lost: reconnect + attempts += 1 + print(f"\n[chat] connection lost ({type(exc).__name__}: {exc})") + if attempts >= 3: + print( + "[chat] giving up after 3 attempts — is the bridge running? (scripts/run_bridge.sh)" + ) + return 1 + print( + "[chat] reconnecting in 2 seconds… (the room keeps its state on the bridge)" + ) + await asyncio.sleep(2.0) def main() -> int: diff --git a/testbed/demo.py b/testbed/demo.py index d388f5f..f44bfe8 100644 --- a/testbed/demo.py +++ b/testbed/demo.py @@ -152,7 +152,9 @@ async def run_llm_agent( log=lambda role, msg: print(f"[{role}] {msg}"), ) try: - return await run_llm_agent_loop(controller, max_steps, log=print_log, recorder=recorder) + return await run_llm_agent_loop( + controller, max_steps, log=print_log, recorder=recorder + ) except RuntimeError as exc: return {"steps": 0, "tool_calls": 0, "result": str(exc), "interacted": False} @@ -398,7 +400,14 @@ def main() -> int: "into this directory, plus demo.gif animation and demo_summary.png", ) args = parser.parse_args() - summary = asyncio.run(run_demo(args)) + try: + summary = asyncio.run(run_demo(args)) + except KeyboardInterrupt: + return 2 + except Exception as exc: # noqa: BLE001 - friendly failure instead of a traceback + print(f"\n[demo] failed: {type(exc).__name__}: {exc}") + print("[demo] is the bridge running? scripts/run_bridge.sh") + return 1 return 0 if summary.get("interacted") else 1