Skip to content

[bug] WebSocket never starts initial connection when init() runs before an event loop exists #41

@d180

Description

@d180

What went wrong

When adrian.init() is called from synchronous startup code,
asyncio.get_running_loop() raises and loop becomes None, so the SDK
skips _ws_client.schedule_connect(loop).

The code logs that the WebSocket will connect later "on first send from within
an async context", but there is no code path that actually starts the initial
connection from _send_frame(). _send_frame() only buffers frames while
disconnected and returns.

As a result, events are silently buffered forever and the backend never
receives them unless some other code explicitly starts the WebSocket
connection.


Reproduction steps

  1. Call adrian.init(...) from normal synchronous startup code, before any
    event loop is running.
  2. Later enter async code with asyncio.run(...) or equivalent.
  3. Emit an Adrian event so the SDK attempts to send a frame.
  4. Observe that the frame is buffered, but no WebSocket connection task is
    created.

Minimal local reproduction:

import asyncio
import adrian
from adrian.proto import event_pb2 as pb

adrian.init(
    api_key="test",
    ws_url="ws://127.0.0.1:9999/ws",
    auto_instrument=False,
)

ws = adrian._ws_client
print("after init:", ws._connect_task, ws._connected.is_set(),
len(ws._replay_buffer))

async def main():
    frame = pb.ClientFrame()
    ev = frame.paired_batch.events.add()
    ev.event_id = "evt-1"
    ev.invocation_id = "inv-1"
    ev.session_id = "sess-1"
    ev.pair_type = pb.PAIR_TYPE_TOOL
    ev.tool.tool_name = "demo"

    await ws._send_frame(frame)
    print("after send:", ws._connect_task, ws._connected.is_set(),
    len(ws._replay_buffer))

asyncio.run(main())

Expected behaviour

If init() runs before an event loop exists, the SDK should still establish the
initial WebSocket connection once it later reaches an async context and tries
to send its first frame.

At minimum, the first async send path should trigger the initial connect
attempt instead of buffering forever.


Actual behaviour

The initial connection is never started.

Observed local output:

after init: None False 0
after send: None False 1

This shows:

  • no connect task after init()
  • no connect task after first async send
  • socket still disconnected
  • event buffered in memory

Environment

  • Adrian version / commit: d16881a5807c08ee23a69e74e9c334d62c5973bc
  • OS: macOS 26.3.1 (Build 25D771280a)
  • Docker version: Docker version 29.4.3, build 055a478
  • GPU model (if relevant): N/A

Logs

Click to expand
Relevant code path:

sdk/adrian/__init__.py
- init() catches RuntimeError from asyncio.get_running_loop()
- if loop is None, it skips _ws_client.schedule_connect(loop)
- it logs: "No running event loop at init(); WebSocket will connect on first send from within an async context."

sdk/adrian/ws.py
- _send_frame() buffers when disconnected and returns
- it does not start connect()
- the only connect triggers found are:
  - schedule_connect()
  - _handle_disconnect() after a prior connection has already existed

Local reproduction output:
after init: connect_task= None connected= False buffer= 0
after async send: connect_task= None connected= False buffer= 1

Relevant code paths

  • sdk/adrian/__init__.py
  • sdk/adrian/ws.py

Likely fix

Ensure that the first async send path can trigger the initial connection when
init() was called before an event loop existed.

For example, if _send_frame() detects that:

  • no active connection exists
  • no connect task exists

it could schedule the initial connect attempt before buffering or replaying
frames.

This would make the runtime behaviour match the log message that the SDK will
connect on first send from within an async context.


Offer to contribute

I’d be happy to work on a fix for this issue.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions