Arena is a new pane kind for competitive AI evaluation. A Battle runs the same prompt against 2-6 Contestants across two concurrent lanes: local lane (llama-swap models, serial) and cloud lane (parallel). Added to all three registries: @boocode/contracts WsFrameSchema, server InferenceFrame, and web WsFrame. Backend (apps/coder): - arena-runner: battle scheduler, lane classifier, benchmark, results writer, resume, user winner override - arena-analyzer: two-stage digest→judge analysis on DEFAULT_MODEL - arena-decisions: status transitions and resume logic (unit-tested) - arena-analyzer-helpers: pure helper functions (unit-tested) - arena-model-call: model call utility for analysis - arena routes: create/get/list/stop/analyze/cross-examine/winner/diff - schema: battles, contestants, cross_examinations tables (idempotent) - remove old /api/arena* routes and tasks.arena_id column Frontend (apps/web): - ArenaLauncherDialog: battle type, prompt, contestant selection - ArenaPane: live roster, streaming output, analysis, cross-exam - DiffView: unified diff with line-by-line color for coding contests - Winner override per-row dropdown (Trophy icon) - battle_updated WS handler for live winner/analysis updates - arena pane kind in Workspace, ChatTabBar, useSidebar Cross-app: - ArenaState and ArenaContestantShape/WsFrame types (contracts) - battle_* frames in WsFrameSchema, InferenceFrame, and web WsFrame - manifest.json written per battle results folder - /Arena added to .gitignore
88 lines
3.0 KiB
TypeScript
88 lines
3.0 KiB
TypeScript
// Coder user-channel WS — mirrors useUserEvents but connects to the BooCoder
|
|
// host service's /api/coder/ws/user endpoint. Forwards flow_run_started and
|
|
// flow_run_step_updated frames onto the sessionEvents bus so OrchestratorPane
|
|
// can subscribe to run-level lifecycle updates without a per-session WS.
|
|
//
|
|
// Event-dedup discipline: do NOT additionally emit these frames locally after
|
|
// a POST /api/coder/runs call — this hook forwards the authoritative WS frame.
|
|
import { useEffect } from 'react';
|
|
import { WsFrameSchema } from '@boocode/contracts/ws-frames';
|
|
import { sessionEvents } from './sessionEvents';
|
|
import type {
|
|
BattleStartedEvent,
|
|
BattleUpdatedEvent,
|
|
ContestantUpdatedEvent,
|
|
FlowRunStartedEvent,
|
|
FlowRunStepUpdatedEvent,
|
|
} from './sessionEvents';
|
|
|
|
const RECONNECT_INITIAL_MS = 1000;
|
|
const RECONNECT_MAX_MS = 30_000;
|
|
|
|
export function useCoderUserEvents(): void {
|
|
useEffect(() => {
|
|
let ws: WebSocket | null = null;
|
|
let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
|
let reconnectDelay = RECONNECT_INITIAL_MS;
|
|
let unmounted = false;
|
|
|
|
const connect = () => {
|
|
if (unmounted) return;
|
|
const proto = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
ws = new WebSocket(`${proto}//${window.location.host}/api/coder/ws/user`);
|
|
|
|
ws.onopen = () => {
|
|
reconnectDelay = RECONNECT_INITIAL_MS;
|
|
};
|
|
|
|
ws.onmessage = (ev) => {
|
|
let raw: unknown;
|
|
try {
|
|
raw = JSON.parse(ev.data as string);
|
|
} catch {
|
|
return;
|
|
}
|
|
const validated = WsFrameSchema.safeParse(raw);
|
|
if (!validated.success) {
|
|
console.error('ws-frame-validation-failed (coder user channel)', {
|
|
frame_type: (raw as { type?: unknown })?.type,
|
|
errors: validated.error.flatten(),
|
|
});
|
|
return;
|
|
}
|
|
const frame = validated.data;
|
|
if (frame.type === 'flow_run_started') {
|
|
sessionEvents.emit(frame as unknown as FlowRunStartedEvent);
|
|
} else if (frame.type === 'flow_run_step_updated') {
|
|
sessionEvents.emit(frame as unknown as FlowRunStepUpdatedEvent);
|
|
} else if (frame.type === 'battle_started') {
|
|
sessionEvents.emit(frame as unknown as BattleStartedEvent);
|
|
} else if (frame.type === 'contestant_updated') {
|
|
sessionEvents.emit(frame as unknown as ContestantUpdatedEvent);
|
|
} else if (frame.type === 'battle_updated') {
|
|
sessionEvents.emit(frame as unknown as BattleUpdatedEvent);
|
|
}
|
|
};
|
|
|
|
ws.onclose = () => {
|
|
if (unmounted) return;
|
|
const delay = reconnectDelay;
|
|
reconnectDelay = Math.min(reconnectDelay * 2, RECONNECT_MAX_MS);
|
|
reconnectTimer = setTimeout(connect, delay);
|
|
};
|
|
|
|
ws.onerror = () => {
|
|
try { ws?.close(); } catch {}
|
|
};
|
|
};
|
|
|
|
connect();
|
|
|
|
return () => {
|
|
unmounted = true;
|
|
if (reconnectTimer) clearTimeout(reconnectTimer);
|
|
if (ws) try { ws.close(); } catch {}
|
|
};
|
|
}, []);
|
|
}
|