chore: snapshot working tree - pty_exited notifications + in-flight inference WIP

feat(booterm): structured pty_exited WS notifications. Plan-validated, impl-validated, code-reviewed green (contracts build clean, contracts test 29/29, booterm + web typecheck clean).

wip: in-progress inference/provider refactor (agents.ts, provider.ts, new llama-providers.ts, removed llama-args-validator), plus arena, dispatcher, compaction, schema changes.

openspec: pty-exit-notifications complete; x-agent-flags planned (not yet implemented).
This commit is contained in:
2026-06-14 12:48:47 +00:00
parent 0ed506f1da
commit b18de2a331
204 changed files with 25344 additions and 867 deletions

View File

@@ -234,6 +234,17 @@ export function useTerminalSocket({
t.write(`\r\n\x1b[2m[process exited with code ${frame.code}]\x1b[0m\r\n`);
return;
}
if (frame?.type === 'pty_exited') {
if (frame.timed_out) {
t.write('\r\n\x1b[2m[process timed out and was killed]\x1b[0m\r\n');
} else {
t.write(`\r\n\x1b[2m[process exited with code ${frame.exit_code}]\x1b[0m\r\n`);
}
if (frame.last_lines.length > 0) {
t.write(frame.last_lines[frame.last_lines.length - 1] + '\r\n');
}
return;
}
t.write(e.data);
} else {
t.write(new Uint8Array(e.data as ArrayBuffer));

View File

@@ -0,0 +1,305 @@
/**
* useControlStream: second app-level WS singleton for BooControl.
*
* Own React context + connection guard. Targets proxied /api/control/ws.
* Client discards deltas with seq <= snapshot_seq per-host.
*
* This is NOT the same as useUserEvents — it's a separate WS connection.
*/
import { createContext, useContext, useRef, useCallback, useEffect, useState } from 'react';
// ─── types ──────────────────────────────────────────────────────────────────
export interface ControlFleetHost {
providerId: string;
liveness: 'connected' | 'reconnecting' | 'down';
lastSeenAt: string | null;
seq: number;
models: Array<{
model: string;
state: string;
ts: string;
ttlDeadline: string | null;
inflight: number;
}>;
}
export interface ControlRequestEntry {
id: number;
providerId: string;
ts: string;
model: string | null;
reqPath: string | null;
statusCode: number | null;
durationMs: number | null;
}
export interface ControlPerfSample {
providerId: string;
ts: string;
gpu: unknown;
sys: unknown;
}
export interface ControlLogEntry {
providerId: string;
source: 'proxy' | 'upstream' | 'model';
line: string;
}
// ─── frame types ────────────────────────────────────────────────────────────
export type ControlFleetDelta = {
type: 'control_fleet';
seq: number;
hosts: ControlFleetHost[];
};
export type ControlActivityFrame = {
type: 'control_activity';
seq: number;
providerId: string;
entry: ControlRequestEntry;
};
export type ControlPerfFrame = {
type: 'control_perf';
seq: number;
providerId: string;
ts: string;
gpu: unknown;
sys: unknown;
};
export type ControlLogFrame = {
type: 'control_log';
seq: number;
providerId: string;
source: 'proxy' | 'upstream' | 'model';
line: string;
};
export type ControlJobFrame = {
type: 'control_job';
seq: number;
jobType: 'bench' | 'eval' | 'action';
jobId: string;
status: 'queued' | 'running' | 'completed' | 'failed';
detail?: Record<string, unknown>;
};
export type ControlFrame =
| ControlFleetDelta
| ControlActivityFrame
| ControlPerfFrame
| ControlLogFrame
| ControlJobFrame;
// ─── A3: type-guards for incoming WS frames ─────────────────────────────────
// Replace 'as unknown as' casts with runtime validation.
function isValidHost(h: unknown): h is ControlFleetHost {
if (!h || typeof h !== 'object') return false;
const obj = h as Record<string, unknown>;
return (
typeof obj.providerId === 'string' &&
['connected', 'reconnecting', 'down'].includes(obj.liveness as string) &&
(obj.lastSeenAt === null || typeof obj.lastSeenAt === 'string') &&
typeof obj.seq === 'number' &&
Array.isArray(obj.models)
);
}
function isControlFleetDelta(data: unknown): data is ControlFleetDelta {
if (!data || typeof data !== 'object') return false;
const obj = data as Record<string, unknown>;
return (
obj.type === 'control_fleet' &&
typeof obj.seq === 'number' &&
Array.isArray(obj.hosts) &&
obj.hosts.every(isValidHost)
);
}
function isControlActivityFrame(data: unknown): data is ControlActivityFrame {
if (!data || typeof data !== 'object') return false;
const obj = data as Record<string, unknown>;
return (
obj.type === 'control_activity' &&
typeof obj.seq === 'number' &&
typeof obj.providerId === 'string' &&
typeof obj.entry === 'object' &&
obj.entry !== null
);
}
function isControlPerfFrame(data: unknown): data is ControlPerfFrame {
if (!data || typeof data !== 'object') return false;
const obj = data as Record<string, unknown>;
return (
obj.type === 'control_perf' &&
typeof obj.seq === 'number' &&
typeof obj.providerId === 'string' &&
typeof obj.ts === 'string'
);
}
function isControlLogFrame(data: unknown): data is ControlLogFrame {
if (!data || typeof data !== 'object') return false;
const obj = data as Record<string, unknown>;
return (
obj.type === 'control_log' &&
typeof obj.seq === 'number' &&
typeof obj.providerId === 'string' &&
['proxy', 'upstream', 'model'].includes(obj.source as string) &&
typeof obj.line === 'string'
);
}
function isControlJobFrame(data: unknown): data is ControlJobFrame {
if (!data || typeof data !== 'object') return false;
const obj = data as Record<string, unknown>;
return (
obj.type === 'control_job' &&
typeof obj.seq === 'number' &&
['bench', 'eval', 'action'].includes(obj.jobType as string) &&
typeof obj.jobId === 'string' &&
['queued', 'running', 'completed', 'failed'].includes(obj.status as string)
);
}
// ─── context ────────────────────────────────────────────────────────────────
export interface ControlStreamState {
hosts: ControlFleetHost[];
requests: ControlRequestEntry[];
perfSamples: ControlPerfSample[];
logs: ControlLogEntry[];
jobs: Array<{
jobType: 'bench' | 'eval' | 'action';
jobId: string;
status: 'queued' | 'running' | 'completed' | 'failed';
}>;
}
const ControlContext = createContext<ControlStreamState | null>(null);
// ─── hook ───────────────────────────────────────────────────────────────────
export function useControlStream(): ControlStreamState {
const state = useContext(ControlContext);
if (!state) throw new Error('useControlStream must be used within ControlProvider');
return state;
}
export function ControlProvider({ children }: { children: React.ReactNode }) {
const [state, setState] = useState<ControlStreamState>({
hosts: [],
requests: [],
perfSamples: [],
logs: [],
jobs: [],
});
const wsRef = useRef<WebSocket | null>(null);
const reconnectTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const snapshotSeqRef = useRef(0);
const hasSnapshotRef = useRef(false);
const backoffRef = useRef(5_000);
const connect = useCallback(() => {
if (wsRef.current) return;
const ws = new WebSocket(`${window.location.protocol === 'https:' ? 'wss' : 'ws'}://${window.location.host}/api/control/ws`);
wsRef.current = ws;
ws.onopen = () => {
snapshotSeqRef.current = 0;
hasSnapshotRef.current = false;
backoffRef.current = 5_000;
};
ws.onmessage = (event) => {
try {
const data: unknown = JSON.parse(event.data);
if (typeof data !== 'object' || !data || !('type' in data)) return;
if ((data as Record<string, unknown>).type === 'ping') return; // heartbeat
// A3: type-guard each frame shape before applying — no 'as unknown as' casts
if (isControlFleetDelta(data)) {
if (!hasSnapshotRef.current) {
// First frame after connect is the snapshot.
hasSnapshotRef.current = true;
snapshotSeqRef.current = data.seq;
setState((prev) => ({ ...prev, hosts: data.hosts }));
} else {
// Delta: merge by providerId so a delta for one host does not wipe the others.
if (data.seq > snapshotSeqRef.current) {
setState((prev) => {
const merged = [...prev.hosts];
for (const dh of data.hosts) {
const idx = merged.findIndex((h) => h.providerId === dh.providerId);
if (idx >= 0) {
merged[idx] = dh;
} else {
merged.push(dh);
}
}
return { ...prev, hosts: merged };
});
}
}
} else if (isControlActivityFrame(data)) {
setState((prev) => ({
...prev,
requests: [data.entry, ...prev.requests].slice(0, 500),
}));
} else if (isControlPerfFrame(data)) {
setState((prev) => ({
...prev,
perfSamples: [...prev.perfSamples, { providerId: data.providerId, ts: data.ts, gpu: data.gpu, sys: data.sys }].slice(-500),
}));
} else if (isControlLogFrame(data)) {
setState((prev) => ({
...prev,
logs: [...prev.logs, { providerId: data.providerId, source: data.source, line: data.line }].slice(-1000),
}));
} else if (isControlJobFrame(data)) {
setState((prev) => ({
...prev,
jobs: [...prev.jobs, { jobType: data.jobType, jobId: data.jobId, status: data.status }].slice(-200),
}));
}
// Unknown frame types are silently dropped (fail-closed)
} catch {
// Ignore parse errors
}
};
ws.onclose = () => {
wsRef.current = null;
// A6 fix: exponential backoff instead of fixed 5s delay.
const delay = backoffRef.current;
backoffRef.current = Math.min(30_000, backoffRef.current * 2);
reconnectTimerRef.current = setTimeout(connect, delay);
};
ws.onerror = () => {
ws.close();
};
}, []);
useEffect(() => {
connect();
return () => {
if (wsRef.current) {
wsRef.current.close();
wsRef.current = null;
}
if (reconnectTimerRef.current) {
clearTimeout(reconnectTimerRef.current);
}
};
}, [connect]);
return <ControlContext.Provider value={state}>{children}</ControlContext.Provider>;
}

View File

@@ -0,0 +1,12 @@
import { useMemo } from 'react';
/**
* Stable prefers-reduced-motion check.
* Uses useMemo so it only re-evaluates when the media query actually changes.
*/
export function useReducedMotion(): boolean {
return useMemo(
() => window.matchMedia('(prefers-reduced-motion: reduce)').matches,
[],
);
}