1 Commits

Author SHA1 Message Date
voidborne-d
511d05e545 fix: use run_coroutine_threadsafe in send_message_sync to prevent cross-loop crash
WebSocketManager.send_message_sync is called from background worker threads
(via asyncio.get_event_loop().run_in_executor) during workflow execution — by
WebSocketLogger, ArtifactDispatcher, and WebPromptChannel.

Previous implementation:
  try:
      loop = asyncio.get_running_loop()
      if loop.is_running():
          asyncio.create_task(...)          # path only reachable from main thread
      else:
          asyncio.run(...)                  # creates a NEW event loop
  except RuntimeError:
      asyncio.run(...)                      # also creates a new event loop

The problem: WebSocket objects are bound to the *main* uvicorn event loop.
asyncio.run() spins up a separate event loop and calls websocket.send_text()
there, which in Python 3.12 raises:

  RuntimeError: Task got Future attached to a different loop

...causing all log/artifact/prompt messages emitted from workflow threads to be
silently dropped or to crash the worker thread.

Fix:
- Store the event loop that created the first WebSocket connection as
  self._owner_loop (captured in connect(), which always runs on the main loop).
- send_message_sync schedules the coroutine on that loop via
  asyncio.run_coroutine_threadsafe(), then waits with a 10 s timeout.
- Calling from the main thread still works (run_coroutine_threadsafe is safe
  when called from any thread, including the loop thread itself).

Added 7 tests covering:
- send from main thread
- send from worker thread (verifies send_text runs on the owner loop thread)
- 8 concurrent workers with no lost messages
- send after disconnect does not crash
- send before connect (no owner loop) does not crash
- owner loop captured on first connect
- owner loop stable across multiple connects
2026-04-02 16:01:46 +00:00