title: Implement Chatto realtime WebSocket in chattolib status: completed type: feature priority: high created_at: 2026-07-08T23:55:28Z
Chatto 0.4.x's realtime channel is a binary protobuf WebSocket at /api/realtime (chatto.realtime.v1). Replace the stub in realtime.py with a real WS client: fetch/generate protobuf bindings for RealtimeClientFrame/RealtimeServerFrame and their imports, handle hello/subscribe/heartbeat/error/close, and expose a Pythonic async iterator that yields typed events.
Implemented the realtime WebSocket client for Chatto's chatto.realtime.v1 binary-protobuf protocol.
proto/ vendored source (from chattocorp/chatto main) with realtime.proto plus its transitive api/v1 imports and buf/validate/validate.proto.scripts/generate_pb.sh that re-fetches the protos and regenerates the bindings under src/chattolib/_pb/. Requires protoc on PATH.*_pb2.py modules under src/chattolib/_pb/. Package __init__.py inserts its own directory onto sys.path so the generated from chatto.api.v1 import ... imports resolve without polluting the top-level namespace of dependent projects.realtime.py on top of the bindings and websockets:
RealtimeConnection async context manager: connects to /api/realtime, sends binary RealtimeClientHello (bearer token from ChattoClient, session cookie in WS headers), receives RealtimeServerHello, sends RealtimeSubscribeEvents..events() async iterator decodes RealtimeServerFrame variants (event, heartbeat, subscribed, pong, error, close). Fatal errors and close frames raise ChattoRealtimeError / ChattoRealtimeCloseError; heartbeats and pongs are swallowed..ping(nonce) sends a client ping for callers that want an explicit RTT check.stream_events(client) module-level async iterator wraps the connection lifecycle for the common case.RealtimeEvent dataclass exposes id, created_at, actor_id, kind (protobuf oneof case name), payload (the concrete sub-message), and raw (the full envelope).chattolib.__init__.websockets>=13 and protobuf>=5.28 to the [realtime] extra.pyproject.toml: mypy follow_imports = skip and ruff excludes for chattolib._pb so generated modules don't fight the type checker.tests/test_realtime.py: URL helper, event wrapping, exception fields, protobuf frame roundtrips (10 tests). pytest reports 44 passed, 4 skipped (integration).presence_changed events, clean close.Refreshed README with a Realtime section and CLAUDE.md with the new package layout, realtime service row, and _pb regeneration notes.