Story 2896: HTTP launch socket attach — server-derived

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.

What changed

SurfaceChange
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.

The none-case, decided and documented

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.

Proof (tests)

TestProves
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.

What is not changing