feat: realtime WebSocket layer for live events and streaming logs
- Backend: ConnectionManager, Redis-backed bus with in-process fallback, channel authorization, and typed emit helpers wired into workers (ai_runtime, sandbox_gateway), workforce/sandbox/billing/audit/error services. - Frontend: single shared WS client with backoff + heartbeat, RealtimeProvider, useLiveChannel/useLiveEvent hooks, live indicators on task detail, manager chat, dashboard, traces, sandbox terminal, admin errors, and billing. - Contracts: WsEvent/WsServerFrame/WsClientFrame types mirroring the port. All 255 backend tests pass; web typecheck and build pass.
This commit is contained in:
@@ -27,6 +27,10 @@ from app.interfaces.http.routers import (
|
||||
sandbox,
|
||||
workforce,
|
||||
)
|
||||
from app.interfaces.realtime.bus import install as install_realtime_bus
|
||||
from app.interfaces.realtime.bus import start_relay, stop_relay
|
||||
from app.interfaces.realtime.router import realtime_manager
|
||||
from app.interfaces.realtime.router import router as realtime_router
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
@@ -62,8 +66,19 @@ async def lifespan(_: FastAPI):
|
||||
# loudly — a half-started API must not accept traffic.
|
||||
await _record_startup_failure(exc)
|
||||
raise
|
||||
|
||||
# --- realtime wiring ---------------------------------------------------
|
||||
# This process is the publisher for API-driven events and the relay for
|
||||
# anything the standalone workers publish. Both are best-effort: if Redis
|
||||
# is not reachable the in-process fan-out keeps every HTTP-driven feed live.
|
||||
bus = install_realtime_bus()
|
||||
bus.attach(realtime_manager().dispatch)
|
||||
await start_relay()
|
||||
|
||||
yield
|
||||
|
||||
await stop_relay()
|
||||
|
||||
|
||||
async def _record_startup_failure(exc: BaseException) -> None:
|
||||
"""Persist a boot failure. Never raises; the caller re-raises regardless."""
|
||||
@@ -123,6 +138,8 @@ def create_app() -> FastAPI:
|
||||
application.include_router(sandbox.router, prefix="/api")
|
||||
application.include_router(admin.router, prefix="/api")
|
||||
application.include_router(agy.router, prefix="/api")
|
||||
# The live feed: one WebSocket per browser, every push on it.
|
||||
application.include_router(realtime_router, prefix="/api")
|
||||
|
||||
@application.get("/", tags=["meta"])
|
||||
async def root() -> dict:
|
||||
|
||||
Reference in New Issue
Block a user