Implement fleet topology epidemiology tracker and strain plague map.
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Adds epidemiology package, auth epidemiology_fix push, strain-based 3D map, and Vitest coverage.
This commit is contained in:
@@ -46,6 +46,8 @@ export const FIELD_HELP: Record<string, string> = {
|
||||
'Server-side failure atlas records conditioned tier failures (e.g. ps_inmemory under Defender on). After five failures it hard-skips subtrees in adaptive_strategy and pushes atlas_skips on agent auth. LOTL Timeline marks tiers skipped_by_atlas.',
|
||||
erasure_lanes:
|
||||
'Optional Reed–Solomon 4+2 shard encoding on signed deploy plans — spreads payload bytes across parallel lane URLs (dns_txt, bits_curl, do_peer, wsus_cache_peer). Agents reassemble from any k shards when server.erasure_lanes_enabled is on and primary single-lane staging fails. Foundation only — no live multi-hop lane orchestration yet.',
|
||||
fleet_torrent:
|
||||
'Fleet Torrent extends erasure with a content-addressed shard DHT across seeder-role agents. One primary seeder per /24 (subnet_primary_seeder on auth). fleet_torrent_gossip relays have_shard / healthy / know_node fleet-wide (cross-subnet). BGP spread_route_hint attaches swarm_magnet + shard_manifest_urls. C2 super-seeder holds canonical shards at /api/v1/public/erasure-torrent/{token}/manifest. Zero-server mode uses last policy snapshot + 30m HTTPS reconnect.',
|
||||
ai_court_session:
|
||||
'When a host is stuck or all spread tiers fail, AI Control runs a Singular Machine Court: Prosecutor cites failure atlas + LOTL attempts, Defender cites a matching fleet phenotype, Judge returns at most three commands. Decisions persist with court_session=true on LOTL Timeline.',
|
||||
calibration_ai_control:
|
||||
|
||||
155
server/web/src/pages/SeerPage.css
Normal file
155
server/web/src/pages/SeerPage.css
Normal file
@@ -0,0 +1,155 @@
|
||||
/* The Seer — pure black read-only terminal */
|
||||
|
||||
.seer-page {
|
||||
background: #000;
|
||||
color: #c8ffc8;
|
||||
min-height: calc(100vh - 4rem);
|
||||
padding: 1.25rem 1.5rem 2rem;
|
||||
}
|
||||
|
||||
.seer-header {
|
||||
margin-bottom: 1rem;
|
||||
border-bottom: 1px solid #1a3a1a;
|
||||
padding-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.seer-title {
|
||||
margin: 0;
|
||||
font-size: 1.35rem;
|
||||
color: #39ff14;
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
|
||||
.seer-subtitle {
|
||||
margin: 0.35rem 0 0;
|
||||
font-size: 0.72rem;
|
||||
color: #5a8a5a;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.seer-error {
|
||||
color: #ff6b6b;
|
||||
font-family: var(--font-tech, monospace);
|
||||
font-size: 0.85rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.seer-panes {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1rem;
|
||||
min-height: 70vh;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.seer-panes {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.seer-pane {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 1px solid #1a3a1a;
|
||||
background: #000;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.seer-pane-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.45rem 0.75rem;
|
||||
background: #050505;
|
||||
border-bottom: 1px solid #1a3a1a;
|
||||
}
|
||||
|
||||
.seer-pane-label {
|
||||
font-family: var(--font-tech, monospace);
|
||||
font-size: 0.68rem;
|
||||
letter-spacing: 0.12em;
|
||||
color: #39ff14;
|
||||
}
|
||||
|
||||
.seer-autoscroll {
|
||||
font-family: var(--font-tech, monospace);
|
||||
font-size: 0.65rem;
|
||||
color: #5a8a5a;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.seer-terminal {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 0.75rem;
|
||||
font-family: 'Consolas', 'Courier New', monospace;
|
||||
font-size: 0.78rem;
|
||||
line-height: 1.45;
|
||||
max-height: calc(100vh - 12rem);
|
||||
}
|
||||
|
||||
.seer-terminal--notes {
|
||||
color: #a8d8ff;
|
||||
}
|
||||
|
||||
.seer-placeholder {
|
||||
color: #3a5a3a;
|
||||
font-style: italic;
|
||||
padding: 1rem 0;
|
||||
}
|
||||
|
||||
.seer-stream-entry,
|
||||
.seer-note-entry {
|
||||
margin-bottom: 1rem;
|
||||
padding-bottom: 0.75rem;
|
||||
border-bottom: 1px dashed #142814;
|
||||
}
|
||||
|
||||
.seer-stream-meta,
|
||||
.seer-note-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.35rem;
|
||||
font-size: 0.65rem;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.seer-stream-dir {
|
||||
color: #39ff14;
|
||||
}
|
||||
|
||||
.seer-stream-agent,
|
||||
.seer-note-agent {
|
||||
color: #00e8f5;
|
||||
}
|
||||
|
||||
.seer-stream-ts,
|
||||
.seer-note-ts {
|
||||
color: #5a8a5a;
|
||||
}
|
||||
|
||||
.seer-note-src {
|
||||
color: #8866aa;
|
||||
}
|
||||
|
||||
.seer-json {
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
color: #b8ffb8;
|
||||
}
|
||||
|
||||
.seer-note-text {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.seer-pane--notes .seer-note-text {
|
||||
color: #cce8ff;
|
||||
}
|
||||
69
server/web/src/pages/SeerPage.test.tsx
Normal file
69
server/web/src/pages/SeerPage.test.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* @vitest-environment happy-dom
|
||||
*/
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { cleanup, render, screen, waitFor } from '@testing-library/react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import SeerPage from './SeerPage';
|
||||
import { routerFuture } from '../routerFuture';
|
||||
import { api } from '../api/client';
|
||||
|
||||
vi.mock('../hooks/useWebSocket', () => ({
|
||||
useWebSocket: () => ({ latestMessage: null, isConnected: true }),
|
||||
}));
|
||||
|
||||
function renderSeer() {
|
||||
return render(
|
||||
<MemoryRouter initialEntries={['/seer']} future={routerFuture}>
|
||||
<SeerPage />
|
||||
</MemoryRouter>,
|
||||
);
|
||||
}
|
||||
|
||||
describe('SeerPage', () => {
|
||||
beforeEach(() => {
|
||||
vi.spyOn(api, 'getSeerStream').mockResolvedValue({
|
||||
events: [
|
||||
{
|
||||
id: 1,
|
||||
event_type: 'llm_request',
|
||||
agent_id: 'agent-abc',
|
||||
payload: { direction: 'request', payload: { model: 'llama3.2' } },
|
||||
ts: '2026-06-07T12:00:00.000Z',
|
||||
},
|
||||
],
|
||||
notes: [
|
||||
{
|
||||
id: 2,
|
||||
agent_id: 'agent-abc',
|
||||
note: 'dns_txt lane preferred on subnet 10.0.1.0/24',
|
||||
source: 'abc123',
|
||||
ts: '2026-06-07T12:00:01.000Z',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it('renders black terminal panes without input', async () => {
|
||||
renderSeer();
|
||||
expect(await screen.findByRole('heading', { name: /The Seer/i })).toBeInTheDocument();
|
||||
expect(screen.getByText(/FLEET AI STREAM/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/SEER NOTES/i)).toBeInTheDocument();
|
||||
expect(screen.queryByRole('textbox')).toBeNull();
|
||||
expect(screen.queryByRole('searchbox')).toBeNull();
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/llama3.2/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/dns_txt lane preferred/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('shows live status when websocket connected', async () => {
|
||||
renderSeer();
|
||||
expect(await screen.findByText(/LIVE — streaming fleet AI payloads/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
210
server/web/src/pages/SeerPage.tsx
Normal file
210
server/web/src/pages/SeerPage.tsx
Normal file
@@ -0,0 +1,210 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useWebSocket } from '../hooks/useWebSocket';
|
||||
import { HelpTip } from '../components/HelpTip';
|
||||
import { api } from '../api/client';
|
||||
import type { SeerEventRecord, SeerNoteRecord } from '../types';
|
||||
import './SeerPage.css';
|
||||
|
||||
const MAX_STREAM = 300;
|
||||
const MAX_NOTES = 200;
|
||||
|
||||
function fmtJson(value: unknown): string {
|
||||
if (value == null) return '';
|
||||
if (typeof value === 'string') {
|
||||
try {
|
||||
return JSON.stringify(JSON.parse(value), null, 2);
|
||||
} catch {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
try {
|
||||
return JSON.stringify(value, null, 2);
|
||||
} catch {
|
||||
return String(value);
|
||||
}
|
||||
}
|
||||
|
||||
function fmtTs(ts?: string): string {
|
||||
if (!ts) return '—';
|
||||
const d = new Date(ts);
|
||||
if (Number.isNaN(d.getTime())) return ts;
|
||||
return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
||||
}
|
||||
|
||||
function eventLabel(evt: SeerEventRecord): string {
|
||||
const payload = evt.payload as { direction?: string } | undefined;
|
||||
const dir = payload?.direction ?? evt.event_type;
|
||||
if (dir.includes('request')) return '▶ REQUEST';
|
||||
if (dir.includes('response')) return '◀ RESPONSE';
|
||||
return evt.event_type.toUpperCase();
|
||||
}
|
||||
|
||||
function StreamLine({ evt }: { evt: SeerEventRecord }) {
|
||||
const payload = evt.payload as { payload?: unknown } | undefined;
|
||||
const inner = payload?.payload ?? evt.payload;
|
||||
return (
|
||||
<div className="seer-stream-entry">
|
||||
<div className="seer-stream-meta">
|
||||
<span className="seer-stream-dir">{eventLabel(evt)}</span>
|
||||
{evt.agent_id && <span className="seer-stream-agent">{evt.agent_id.slice(0, 8)}</span>}
|
||||
<span className="seer-stream-ts">{fmtTs(evt.ts)}</span>
|
||||
</div>
|
||||
<pre className="seer-json">{fmtJson(inner)}</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function NoteLine({ note }: { note: SeerNoteRecord }) {
|
||||
return (
|
||||
<div className="seer-note-entry">
|
||||
<div className="seer-note-meta">
|
||||
{note.agent_id && <span className="seer-note-agent">{note.agent_id.slice(0, 8)}</span>}
|
||||
<span className="seer-note-ts">{fmtTs(note.ts)}</span>
|
||||
{note.source && <span className="seer-note-src">{note.source}</span>}
|
||||
</div>
|
||||
<div className="seer-note-text">{note.note}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function SeerPage() {
|
||||
const { latestMessage, isConnected } = useWebSocket();
|
||||
const [events, setEvents] = useState<SeerEventRecord[]>([]);
|
||||
const [notes, setNotes] = useState<SeerNoteRecord[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
const streamRef = useRef<HTMLDivElement>(null);
|
||||
const notesRef = useRef<HTMLDivElement>(null);
|
||||
const [autoScrollStream, setAutoScrollStream] = useState(true);
|
||||
const [autoScrollNotes, setAutoScrollNotes] = useState(true);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
try {
|
||||
const data = await api.getSeerStream(150);
|
||||
setEvents((data.events ?? []).slice().reverse());
|
||||
setNotes(data.notes ?? []);
|
||||
setError('');
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : 'Failed to load Seer stream');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
void load();
|
||||
}, [load]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!latestMessage) return;
|
||||
if (latestMessage.type === 'seer_events') {
|
||||
const evt = latestMessage.payload as SeerEventRecord;
|
||||
if (evt && typeof evt === 'object') {
|
||||
setEvents((prev) => [evt, ...prev].slice(0, MAX_STREAM));
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (latestMessage.type === 'seer_notes_updated') {
|
||||
const p = latestMessage.payload as { note?: string; agent_id?: string; source?: string };
|
||||
if (p?.note) {
|
||||
setNotes((prev) => [
|
||||
{
|
||||
id: Date.now(),
|
||||
note: p.note,
|
||||
agent_id: p.agent_id,
|
||||
source: p.source,
|
||||
ts: new Date().toISOString(),
|
||||
},
|
||||
...prev,
|
||||
].slice(0, MAX_NOTES));
|
||||
} else {
|
||||
void load();
|
||||
}
|
||||
}
|
||||
}, [latestMessage, load]);
|
||||
|
||||
useEffect(() => {
|
||||
if (autoScrollStream && streamRef.current) {
|
||||
streamRef.current.scrollTop = 0;
|
||||
}
|
||||
}, [events, autoScrollStream]);
|
||||
|
||||
useEffect(() => {
|
||||
if (autoScrollNotes && notesRef.current) {
|
||||
notesRef.current.scrollTop = 0;
|
||||
}
|
||||
}, [notes, autoScrollNotes]);
|
||||
|
||||
const emptyStream = !loading && events.length === 0;
|
||||
const emptyNotes = !loading && notes.length === 0;
|
||||
|
||||
const statusLine = useMemo(() => {
|
||||
if (!isConnected) return 'WS DISCONNECTED — replaying REST snapshot only';
|
||||
return 'LIVE — streaming fleet AI payloads via seer_events';
|
||||
}, [isConnected]);
|
||||
|
||||
return (
|
||||
<div className="page seer-page">
|
||||
<header className="seer-header">
|
||||
<h1 className="seer-title font-display">
|
||||
The Seer <HelpTip field="seer_overview" />
|
||||
</h1>
|
||||
<p className="seer-subtitle font-tech">{statusLine}</p>
|
||||
</header>
|
||||
|
||||
{error && <div className="seer-error">{error}</div>}
|
||||
|
||||
<div className="seer-panes">
|
||||
<section className="seer-pane seer-pane--stream" aria-label="Fleet AI stream">
|
||||
<div className="seer-pane-bar">
|
||||
<span className="seer-pane-label">◈ FLEET AI STREAM</span>
|
||||
<label className="seer-autoscroll">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={autoScrollStream}
|
||||
onChange={(e) => setAutoScrollStream(e.target.checked)}
|
||||
/>
|
||||
autoscroll
|
||||
</label>
|
||||
</div>
|
||||
<div className="seer-terminal" ref={streamRef}>
|
||||
{loading && <div className="seer-placeholder">Loading stream…</div>}
|
||||
{emptyStream && (
|
||||
<div className="seer-placeholder">
|
||||
Waiting for Fleet AI turns — enable AI Control on Calibrate.
|
||||
</div>
|
||||
)}
|
||||
{events.map((evt) => (
|
||||
<StreamLine key={`${evt.id}-${evt.ts}`} evt={evt} />
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="seer-pane seer-pane--notes" aria-label="Seer Notes">
|
||||
<div className="seer-pane-bar">
|
||||
<span className="seer-pane-label">◈ SEER NOTES</span>
|
||||
<label className="seer-autoscroll">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={autoScrollNotes}
|
||||
onChange={(e) => setAutoScrollNotes(e.target.checked)}
|
||||
/>
|
||||
autoscroll
|
||||
</label>
|
||||
</div>
|
||||
<div className="seer-terminal seer-terminal--notes" ref={notesRef}>
|
||||
{loading && <div className="seer-placeholder">Loading notes…</div>}
|
||||
{emptyNotes && (
|
||||
<div className="seer-placeholder">
|
||||
Server-persisted AI memory — appended each turn as SEER_NOTE lines.
|
||||
</div>
|
||||
)}
|
||||
{notes.map((note) => (
|
||||
<NoteLine key={`${note.id}-${note.ts}`} note={note} />
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -377,6 +377,8 @@ export interface ServerSettings {
|
||||
fleet_roles_enabled?: boolean;
|
||||
/** Reed–Solomon multi-lane shard metadata on signed deploy plans (default off). */
|
||||
erasure_lanes_enabled?: boolean;
|
||||
/** Fleet Torrent shard DHT + cross-subnet gossip (default off). */
|
||||
fleet_torrent_enabled?: boolean;
|
||||
/** Triple onion recon/deploy gates pushed to agents at auth. */
|
||||
triple_onion_policy?: {
|
||||
patch_first?: boolean;
|
||||
|
||||
Reference in New Issue
Block a user