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:
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user