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:
mjalmoudy94
2026-10-13 12:56:36 +03:30
parent afdef4b444
commit f1e5df29ce
31 changed files with 2981 additions and 28 deletions
+83
View File
@@ -666,3 +666,86 @@ export interface ErrorQuery {
limit?: number;
offset?: number;
}
// ---------------------------------------------------------------------------
// Realtime (WebSocket)
//
// The live feed is the same product surface as the REST API: one envelope
// shape, mirrored key-for-key from ``app/application/ports/realtime.py``.
// A page that switches on ``WsEventType`` never needs to know which part of
// the backend produced the event.
//
// Channel naming is what a page subscribes to — ``org:{id}`` for the tenant
// feed, ``task:{id}`` for one task, and so on. The server authorizes every
// subscribe against the caller's memberships, so a channel the user may not
// see simply comes back rejected.
// ---------------------------------------------------------------------------
export type WsEventType =
| "run.step"
| "run.status"
| "task.status"
| "task.created"
| "chat.pending"
| "message.created"
| "approval.pending"
| "artifact.created"
| "sandbox.command"
| "sandbox.session"
| "sandbox.reaped"
| "usage.recorded"
| "billing.wallet"
| "error.recorded"
| "audit.recorded"
| "worker.status";
/** One pushed event, as the browser receives it. */
export interface WsEvent<T = Record<string, unknown>> {
id: string;
type: WsEventType;
data: T;
organization_id: string | null;
/** ISO-8601 UTC; render with ``toLocaleString`` like every other timestamp. */
at: string;
}
/** Every frame the server sends. Control frames and events share ``type``. */
export type WsServerFrame =
| { type: "hello"; user_id: string; server_time: string }
| { type: "subscribed"; accepted: string[]; rejected: string[] }
| { type: "unsubscribed"; channels: string[] }
| { type: "pong" }
| { type: "error"; message: string }
| { type: "event"; event: WsEvent };
/** Every frame the client may send. */
export type WsClientFrame =
| { action: "subscribe"; channels: string[] }
| { action: "unsubscribe"; channels: string[] }
| { action: "ping" };
/** Live run-step payload — what the task timeline renders as it arrives. */
export interface WsRunStepData {
run_id: string;
task_id: string;
step: {
id: string;
index: number;
kind: string;
title: string;
payload: Record<string, unknown>;
};
}
/** Live sandbox command payload — the terminal panel's stream. */
export interface WsSandboxCommandData {
session_id: string;
run_id: string;
command: string;
status: string;
exit_code: number | null;
stdout: string;
stderr: string;
duration_ms: number;
truncated: boolean;
}