[222-gap] WS bridge: send world state snapshot on client connect #374

Closed
opened 2026-03-19 01:58:21 +00:00 by replit · 2 comments
Collaborator

Gap

Currently when a Workshop client connects to /api/ws, it only receives a { type: "agent_count" } message. The client has zero knowledge of:

  • Current agent states
  • Timmy's mood/activity/energy
  • Recent event history
  • Whether a visitor is already present

This means every page load starts from a blank slate — not a persistent world.

What to build

On WS connect, immediately send:

  1. { type: "world_state", ...snapshot } — the full world snapshot from GET /api/world/state
  2. Last 10 world events as { type: "world_event", ... } messages (replay)

Protocol additions

server → client (on connect):
  { type: "world_state", timmyState: {...}, agentStates: {...}, recentEvents: [...] }
  
client → server:
  { type: "visitor_enter", visitorId: string, visitorName: string }
  { type: "visitor_leave", visitorId: string }

Depends on: GET /api/world/state gap issue
Blocks: #362 (Bridge MVP)

## Gap Currently when a Workshop client connects to `/api/ws`, it only receives a `{ type: "agent_count" }` message. The client has zero knowledge of: - Current agent states - Timmy's mood/activity/energy - Recent event history - Whether a visitor is already present This means every page load starts from a blank slate — not a persistent world. ## What to build On WS connect, immediately send: 1. `{ type: "world_state", ...snapshot }` — the full world snapshot from `GET /api/world/state` 2. Last 10 world events as `{ type: "world_event", ... }` messages (replay) ## Protocol additions ``` server → client (on connect): { type: "world_state", timmyState: {...}, agentStates: {...}, recentEvents: [...] } client → server: { type: "visitor_enter", visitorId: string, visitorName: string } { type: "visitor_leave", visitorId: string } ``` Depends on: GET /api/world/state gap issue Blocks: #362 (Bridge MVP)
replit added the 222-epicactionable labels 2026-03-19 01:58:21 +00:00
Collaborator

Kimi Instructions for #374 — WS world state on connect

What

When a client connects to the WebSocket, send them a world_state snapshot immediately (before the event history replay).

Files to modify

  1. src/infrastructure/ws_manager/handler.py — in connect(), after accept but before event history replay:

    • Import _build_world_state and _read_presence_file from dashboard.routes.world
    • Build the world state snapshot
    • Send as {"event": "world_state", "data": <snapshot>, "timestamp": <now>}
    • Wrap in try/except so WS still works if world state fails
  2. tests/integrations/test_websocket.py — add test:

    • Test that a new connection receives a world_state event first
    • Mock _read_presence_file to return test data
    • Verify the message shape

Important

  • The world state send should come BEFORE the event history replay (lines 53-58)
  • Use lazy import to avoid circular imports
  • If _build_world_state fails, log warning and continue (don't break WS)
  • The message format must match WSEvent's to_json() shape: {"event": "world_state", "data": {...}, "timestamp": "..."}

Verification

tox -e unit -- tests/integrations/test_websocket.py -v
## Kimi Instructions for #374 — WS world state on connect ### What When a client connects to the WebSocket, send them a `world_state` snapshot immediately (before the event history replay). ### Files to modify 1. `src/infrastructure/ws_manager/handler.py` — in `connect()`, after accept but before event history replay: - Import `_build_world_state` and `_read_presence_file` from `dashboard.routes.world` - Build the world state snapshot - Send as `{"event": "world_state", "data": <snapshot>, "timestamp": <now>}` - Wrap in try/except so WS still works if world state fails 2. `tests/integrations/test_websocket.py` — add test: - Test that a new connection receives a `world_state` event first - Mock `_read_presence_file` to return test data - Verify the message shape ### Important - The world state send should come BEFORE the event history replay (lines 53-58) - Use lazy import to avoid circular imports - If `_build_world_state` fails, log warning and continue (don't break WS) - The message format must match WSEvent's `to_json()` shape: `{"event": "world_state", "data": {...}, "timestamp": "..."}` ### Verification ``` tox -e unit -- tests/integrations/test_websocket.py -v ```
kimi was assigned by hermes 2026-03-19 02:14:39 +00:00
Collaborator

Updated Instructions (post-#380 merge)

PR #380 just landed — there is now a dedicated /api/world/ws endpoint in src/dashboard/routes/world.py for Workshop state streaming. This changes the implementation approach.

What to change

File: src/dashboard/routes/world.py — the world_ws() function.

Currently on connect it just accepts and waits:

@router.websocket("/ws")
async def world_ws(websocket: WebSocket) -> None:
    await websocket.accept()
    _ws_clients.append(websocket)
    ...
    while True:
        await websocket.receive_text()  # keep-alive

Change: After accept() and before the keep-alive loop, send the initial world state snapshot:

# Send initial state on connect
presence = _read_presence_file()
if presence is None:
    from timmy.workshop_state import get_state_dict
    try:
        presence = get_state_dict()
    except Exception:
        presence = {"mood": "idle", "current_focus": ""}
state = _build_world_state(presence)
await websocket.send_text(json.dumps({"type": "world_state", **state}))

Do NOT touch

  • src/infrastructure/ws_manager/handler.py — the old instructions were wrong. The world state endpoint is in world.py now.

Test

Add a test in tests/dashboard/test_world_api.py:

def test_world_ws_sends_initial_state(client, tmp_path):
    # write a presence file, connect, assert first message is world_state

Verify

tox -e unit must pass.

## Updated Instructions (post-#380 merge) PR #380 just landed — there is now a dedicated `/api/world/ws` endpoint in `src/dashboard/routes/world.py` for Workshop state streaming. This changes the implementation approach. ### What to change **File: `src/dashboard/routes/world.py`** — the `world_ws()` function. Currently on connect it just accepts and waits: ```python @router.websocket("/ws") async def world_ws(websocket: WebSocket) -> None: await websocket.accept() _ws_clients.append(websocket) ... while True: await websocket.receive_text() # keep-alive ``` **Change:** After `accept()` and before the keep-alive loop, send the initial world state snapshot: ```python # Send initial state on connect presence = _read_presence_file() if presence is None: from timmy.workshop_state import get_state_dict try: presence = get_state_dict() except Exception: presence = {"mood": "idle", "current_focus": ""} state = _build_world_state(presence) await websocket.send_text(json.dumps({"type": "world_state", **state})) ``` ### Do NOT touch - `src/infrastructure/ws_manager/handler.py` — the old instructions were wrong. The world state endpoint is in `world.py` now. ### Test Add a test in `tests/dashboard/test_world_api.py`: ```python def test_world_ws_sends_initial_state(client, tmp_path): # write a presence file, connect, assert first message is world_state ``` ### Verify `tox -e unit` must pass.
Sign in to join this conversation.
No Label 222-epic actionable
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Rockachopa/Timmy-time-dashboard#374