The Wave-1 HTTP launch adapter (POST /app/agents/{agent_key},
Story-2840) accepts a socketId to attach the run to a live
WebSocket for streaming. But socketId is matched against
agents.ws_connection.connection_id — the API-Gateway
server-assigned requestContext.connectionId — which the
browser never learns ($connect posts no frame, pong
carries no id, no endpoint exposes the registry). A real client therefore
cannot supply it, and a run launched without it enqueues with
connection_id=None, so the clerk worker's
_FramePusher drops every non-terminal frame
(text.delta, turn.delta, phase.changed,
tool.*) — live streaming silently degrades to terminal-only
delivery and CC-01 ("the answer streams live to the app") fails for every
HTTP-launched run.
Fix per the ticket's recommended option A (RFD-AGT-013 D4: delivery
addresses are server-derived; the caller never decides where output goes):
when socketId is omitted, the server resolves the caller's
newest live agents.ws_connection row, scoped to
(firm_id, user_id), and attaches it to the launch envelope.
| Surface | Change |
|---|---|
src/app_api/agent_launch_core.py |
New resolve_newest_live_connection() — the same lookup the
clerk worker's _resolve_live_connection runs at push time
(ORDER BY connected_at DESC LIMIT 1), pinned to the
caller's (firm_id, user_id) so an attach can never cross a
tenant or user boundary.
|
src/app_api/router_agents.py |
When socketId is omitted (the only case a real browser can
produce), the launch resolves the newest live connection server-side
and attaches it. An explicit socketId is still accepted
and validated exactly as before (tests and server-side callers use it).
|
src/app_api/agent_launch_dtos.py |
socket_id field description now documents the
server-derived fallback and the no-socket behavior.
|
openapi/app-api.json |
Regenerated snapshot (make openapi). |
tests/factories.py |
cleanup_seeded_data now deletes agents.chat_turn
BEFORE agents.agent_run_key — chat_turn.agent_run_id
FKs to agent_run_key (children before parents). The first
seeded_firm test that drives the real worker (the
delta-delivery proof below) tripped the old order in teardown.
|
A caller with no live socket still launches (HTTP 202,
attachedSocketId: null, envelope connection_id=None).
This is acceptable — and does not lose truth delivery — because the clerk
worker's _FramePusher re-resolves the caller's newest live
socket at delivery time for every must-deliver frame
(turn.completed, run.done, run.failed,
render.data); only mid-run deltas are dropped, which is the
pusher's documented design. The channel-launch path (WhatsApp), where no
socket ever exists, is unaffected: the run persists and terminal truth
stays replayable via query. Attach is fail-closed on scope:
the resolver is pinned to (firm_id, user_id), so a socket
owned by another user is never attached.
| Test | Proves |
|---|---|
test_http_mira_launch_without_socket_id_attaches_newest_live_socket |
Two live sockets with staggered connected_at; a launch
with no socketId attaches the NEWEST one, and the response
tells the caller which socket was attached.
|
test_http_mira_launch_without_socket_id_and_no_live_socket_launches_unattached |
No socket rows at all → 202, attachedSocketId: null,
envelope connection_id=None, run idempotency key persisted.
|
test_http_mira_launch_without_socket_id_never_attaches_another_users_socket |
A live socket owned by a different user in the same firm is never attached — the run launches unattached (fail-closed scoping). |
test_http_mira_launch_streams_deltas_to_the_server_derived_socket |
CC-01 at the system boundary: HTTP launch with no client-known socket
id → the enqueued command drives the REAL clerk worker loop (only
AgentCore and the WS push are faked) → text.delta frames
are pushed LIVE to the server-derived connection; every pushed frame
lands on that one socket.
|
tests/unit/test_router_agents.py (2 new) |
Component wiring: omitted socketId consults the resolver
and attaches its result; resolver-None launches unattached without
calling attach.
|
socketId validation (attach_socket_connection,
pramaan.agent_launch.socket_not_found on unknown/foreign ids) —
the four pre-existing integration cases stay green unchanged.