forgo.cloud
Sign in
Repo workspace

forkjoin-ai/gnosis

Aeon Monitor Client — `gnosis.monitor.v1` Frame Contract + Generic Serve Pipeline

distributed-inference/docs/MONITOR_CLIENT_DESIGN.md
forkjoin-ai/gnosis

Aeon Monitor Client — gnosis.monitor.v1 Frame Contract + Generic Serve Pipeline

Status: DESIGN (no code written beyond this doc). Author handoff doc for three implementation workstreams. No emojis anywhere in the wire, server, or client.

0. Problem statement

Today aeon-monitor --serve (open-source/gnosis/distributed-inference/src/bin/aeon-monitor.rs) only exposes the Skymesh teleport scene:

  • HTTP/SSE handlers skymesh_handle_conn (aeon-monitor.rs:3886) route exactly /skymesh/frame.json, /skymesh/stream, /healthz, OPTIONS (aeon-monitor.rs:3932-4008).
  • The served payload is the SkymeshFrame struct (aeon-monitor.rs:3504-3514), schema gnosis.skymesh.v1, NOT a PleromaticFrame.
  • The Flow wire publisher SkymeshUdpPublisher::publish (aeon-monitor.rs:4238) emits the same JSON on Flow stream 0x5353 (aeon-monitor.rs:4082).

Everything else aeon-monitor and aeon-tui can render — the spectrum waterfall, NOAA APT scanlines, PNG images, the handshake cloud, knot-viz, brain-mesh — is ONLY reachable from the terminal. The goal is a generic /monitor/ HTTP/SSE + Flow pipeline that serves EVERY scene as a single envelope shape the web app (apps/skymesh) can render, reusing the exact Skymesh patterns (raw std-only HTTP, Arc<Mutex<_>> shared frame, SSE @ ~4 fps, a MonitorFlow adapter that mirrors open-source/aeon/src/flow/SkymeshFlow.ts).

The two raster representations we already produce:

  • PleromaticFrame (open-source/gnosis/distributed-inference/src/pleromatic_frame.rs:573-607) carries data: Vec<u8> (row-major grayscale OR an embedded PNG file) plus an attributes: HashMap<String,String> map with spec_width/spec_height/ apt_width/apt_height/rf_center_hz/mesh_best_if_offset_hz etc.
  • aeon-tui scenes (open-source/aeon-tui/src/monitor/scenes.ts) emit a MonitorSceneRenderResult (open-source/aeon-tui/src/monitor/types.ts, rgba?: Uint8ClampedArray, prebakedAnsi?, asciiTuning?).

The envelope must subsume both, plus the structured Skymesh payload and the self-contained knot-viz HTML page.


1. The gnosis.monitor.v1 envelope

One JSON object carries any scene. Field names are camelCase to match the existing Skymesh contract (apps/skymesh/client/lib/contract.ts).

{
  "schemaVersion": "gnosis.monitor.v1",  // discriminant; renderer rejects others
  "scene": "spectrum-waterfall",          // scene id (matches /monitor/scenes catalog)
  "kind": "raster",                       // "raster" | "vector" | "embed" | "skymesh"
  "title": "RF Spectrum Waterfall",       // human label for nav + header
  "width": 512,                            // raster columns (cells for vector)
  "height": 48,                            // raster rows
  "channels": 1,                           // 1 = grayscale, 4 = RGBA. raster only.
  "raster": "<base64>",                    // raster only: width*height*channels bytes, row-major
  "palette": "viridis",                    // raster only, optional: named palette for grayscale colorization
  "vector": { /* scene-typed */ },         // vector/skymesh only: structured payload
  "embed": {                               // embed only
    "url": "/monitor/embed/knot-viz.html", // same-origin HTML to iframe
    "params": { "family": "trefoil" }       // optional query params appended to url
  },
  "attributes": {                          // flat string->string passthrough (PleromaticFrame.attributes)
    "rf_center_hz": "101100000",
    "spec_width": "512",
    "mesh_best_if_offset_hz": "1200.5"
  },
  "metadata": {                            // scene-agnostic provenance + numbers
    "sourceId": "auto_bridge",            // PleromaticFrame.source_id
    "seq": 4211,                           // PleromaticFrame.seq
    "live": true,                          // real hardware vs synthetic/demo
    "mediaType": "Image",                  // PleromaticMediaType variant name
    "deficit": 0.0,                        // Stone-space deficit (PleromaticFrame.deficit)
    "level": 10, "phase": 0                // closure level + triton phase
  },
  "ts": 1748102538000                      // UNIX ms (PleromaticFrame.timestamp_ms)
}

Field justification (against the actual scenes)

  • schemaVersion — exact mirror of Skymesh's discriminant (SKYMESH_FRAME_SCHEMA, SkymeshFlow.ts:3). The client's isValidFrame and the Flow assert*Frame both pivot on it (live.ts:78, SkymeshFlow.ts:87). Required.
  • scene — the catalog id, selects which client view renders the payload and which ?scene= the server replays. Required.
  • kind — coarse renderer dispatch. Four values cover every producer:
    • rasterdata/rgba is a pixel grid (waterfall, NOAA APT, PNG-decoded, brain-mesh, video frames). Browser blits to a <canvas>.
    • vector — structured numeric payload the client draws procedurally (handshake cloud points, residual ladder, gate telemetry). Avoids shipping a pre-rasterized grid when the client can draw it crisply.
    • embed — a same-origin URL the client iframes (knot-viz is a 2592-line self-contained WebGL HTML page at aeon-monitor/static/knot-viz.html).
    • skymesh — the existing gnosis.skymesh.v1 2-station teleport payload, carried verbatim under vector and rendered by the current Skymesh view.
  • width/height — needed for canvas sizing for raster; for vector they give the cell grid the existing renderer expects (render.ts works in cells). The waterfall producer already knows these as spec_width/spec_height (pleromatic_frame.rs:713-717); NOAA as apt_width/apt_height (pleromatic_frame.rs:670-673, read back at aeon-monitor.rs:2846-2855).
  • channels — 1 vs 4 distinguishes the grayscale row-major path (PleromaticFrame.data for waterfall/APT, decoded via decode_png_to_grayscale_row_major, pleromatic_frame.rs:1278) from the RGBA scenes (aeon-tui MonitorSceneRenderResult.rgba, scenes.ts:79). One number, not a media-type enum, keeps the canvas blit trivial.
  • raster — base64 of exactly width*height*channels bytes, row-major, matching PleromaticFrame.data's documented layout ("row-major grayscale bytes", pleromatic_frame.rs:658) and the aeon-tui RGBA buffer layout ((y*width+x)*4, scenes.ts:100). base64 because JSON; size risk is in §6.
  • palette — optional named ramp for channels=1 so the client colorizes a grayscale grid (waterfall green ramp, NOAA amber). Mirrors the per-scene ANSI palette the TUI already chooses (AEON_AMBER for NOAA at aeon-monitor.rs:2876, colorForGreenWaterfall in palette.ts). Default "grayscale". Keeps the wire grayscale (1 byte/px) instead of forcing RGBA.
  • vector — opaque scene-typed object. For kind:"skymesh" it is literally the current SkymeshFrame JSON; for residual/gate scenes it is the small struct the client draws. Lets us NOT rasterize things that are cheaper as numbers.
  • embed{ url, params }. The URL is same-origin so the iframe inherits the page's origin and the server serves the static HTML (knot-viz today is a file on disk; §3 adds a static route). params become a query string the WebGL page reads.
  • attributes — verbatim copy of PleromaticFrame.attributes (pleromatic_frame.rs:603). This is the single most load-bearing field: ALL the RF metadata the renderer needs (rf_center_hz, spec_width, iq_rate_hz, mesh_best_if_offset_hz, mesh_best_score, mesh_span_hz, mesh_step_hz, apt_sync, apt_quality) already lives there. Passing it through unmodified means we never have to enumerate it on the wire and the producer's existing spectrum_waterfall/noaa_apt_scanlines helpers need no change.
  • metadata — provenance the renderer shows in the header/status strip: sourceId (PleromaticFrame.source_id), seq, ts, plus the Stone-space numbers (level/phase/deficit) and a live boolean separating real RTL-SDR captures from the synthetic fixtures (the catalog also flags this).
  • ts — top-level too (not only in metadata) so the SSE client can de-dupe and compute fps the way the Skymesh client already keys off frame identity.

How the existing Skymesh frame fits

Skymesh is kind:"skymesh", NOT a raster. The server wraps the current SkymeshFrame JSON as the vector payload and sets scene:"skymesh":

{
  "schemaVersion": "gnosis.monitor.v1",
  "scene": "skymesh", "kind": "skymesh",
  "title": "Skymesh Air-Gapped Teleport",
  "width": 160, "height": 48,
  "vector": { /* the entire gnosis.skymesh.v1 frame, unchanged */
    "schemaVersion": "gnosis.skymesh.v1",
    "stations": [ ... ], "airGap": { ... }, "match": true
  },
  "metadata": { "sourceId": "skymesh", "live": false },
  "ts": 1748102538000
}

The client unwraps vector and feeds it to the existing normalizeFrame + renderTui path untouched (contract.ts:68, render.ts:667). The legacy /skymesh/frame.json endpoint stays for back-compat; /monitor/frame.json?scene=skymesh is the new uniform path. This keeps the spine of the Skymesh demo intact while making it one scene among many.


2. Scene catalog

Every scene aeon-monitor or aeon-tui can render, with its producer, envelope kind, what the browser needs, and whether it is live-real or synthetic.

scene id title produced by kind browser needs live?
skymesh Skymesh Air-Gapped Teleport skymesh_fixture/skymesh_read_frame (aeon-monitor.rs:3519, 3572); render_skymesh (3723) skymesh vector = gnosis.skymesh.v1; reuse current view real when an orchestrator writes .qa-artifacts/skymesh/frame.json, else demo fixture
spectrum-waterfall RF Spectrum Waterfall PleromaticFrame::spectrum_waterfall (pleromatic_frame.rs:695); rendered render_spectrum_waterfall (aeon-monitor.rs:3046) raster ch=1 grayscale grid spec_width x spec_height; attributes.rf_center_hz/iq_rate_hz/mesh_best_*; palette green live (RTL-SDR via iq-waterfall-frame, aeon-monitor.rs:374,400)
noaa-apt NOAA APT Scanlines PleromaticFrame::noaa_apt_scanlines (pleromatic_frame.rs:657); render_noaa_apt (aeon-monitor.rs:2843) raster ch=1 grayscale apt_width x apt_height; attributes.rf_center_hz/apt_sync/apt_quality; palette amber live (NOAA 137 MHz APT, GNOSTIC_ATLAS aeon-monitor.rs:248-252)
image Embedded Image PleromaticFrame::png_image_embedded (pleromatic_frame.rs:1234); render_png_image (aeon-monitor.rs:2616) raster ch=1 server decodes PNG via decode_png_to_grayscale_row_major (pleromatic_frame.rs:1278) then ships grayscale grid; palette grayscale mixed (decoded captures or files)
mesh Handshake / Mesh Cloud render_handshake_cloud (aeon-monitor.rs:2566) over SovereignSieve voxels; fork/race/fold FSM state vector point list [{x,y,z,intensity}] + metadata phase/energy/consciousness from ConstraintFSM synthetic (rotation animation, aeon-monitor.rs:659 rotation_rad)
knot-viz Knot Theory Playground self-contained WebGL HTML aeon-monitor/static/knot-viz.html (2592 lines) embed iframe url (+ optional params like knot family); no per-frame data synthetic (in-browser WebGL)
brain-mesh Brain Mesh Depth aeon-tui brainMeshDepthScene (scenes.ts:1640) -> MonitorSceneRenderResult.rgba + asciiTuning raster ch=4 RGBA grid width x height; optional asciiTuning ramp if rendering as ASCII synthetic (GPU TDA kernel + animation)
selftalk Self-Talk Dashboard render_selftalk_dashboard (aeon-monitor.rs:2101) text panel vector small struct (phase, energy, consciousness, affective kind, latent thought) synthetic
residual Paris Residual Ladder PleromaticFrame::paris_residual (pleromatic_frame.rs:1254); render_residual_ladder (aeon-monitor.rs:2652) vector f32[] residuals + argmax_id; bar ladder live when fed by the Paris probe, else absent
gate-telemetry Thoth Gate Telemetry PleromaticFrame::canonical_thoth_gate_telemetry (pleromatic_frame.rs:1147) vector ThothGateTelemetryMetrics JSON (scores per gate) synthetic/canonical

Notes:

  • image and noaa-apt and spectrum-waterfall are all grayscale row-major rasters; the only differences are dimensions (different attributes keys) and palette. The server normalizes all three to kind:"raster", channels:1.
  • brain-mesh is the one RGBA raster. aeon-tui scenes always produce RGBA (scenes.ts:79); when bridging an aeon-tui scene the server ships channels:4 and the raw RGBA, letting the browser skip the ASCII step entirely (the ASCII ramp is a terminal-only concern; the web canvas draws pixels). asciiTuning rides in attributes for clients that still want ASCII.
  • The TUI's auto-selection (aeon-monitor.rs:2009-2091) prefers the freshest visual PleromaticFrame; the generic serve replaces that implicit choice with an explicit ?scene= (see §3).

3. Serve API

Mirror the existing Skymesh raw-HTTP server (no new crate dependency: manual TcpListener + per-connection thread, exactly as spawn_skymesh_server does at aeon-monitor.rs:4017). Add a /monitor/ router alongside the /skymesh/ one. Reuse SKYMESH_CORS (aeon-monitor.rs:3849) and skymesh_write_response (aeon-monitor.rs:3868) verbatim.

Endpoints

  • GET /monitor/scenes -> catalog JSON. Static list of { id, title, kind, live, defaultParams } derived from §2. Lets the client build its nav without hardcoding. CORS *, Cache-Control: no-store.
  • GET /monitor/frame.json?scene=<id> -> one gnosis.monitor.v1 envelope for that scene's current state. Defaults to scene=skymesh when omitted (so the endpoint is never empty). Mirrors /skymesh/frame.json (aeon-monitor.rs:3941) including the optional &format=bw bitwise variant (aeon-monitor.rs:3910,3943) for native-to-native consumers.
  • GET /monitor/stream?scene=<id> -> SSE data: <envelope json>\n\n at ~4 fps (250 ms, matching aeon-monitor.rs:3997). Mirrors /skymesh/stream (aeon-monitor.rs:3976). One scene per connection; the client opens a new EventSource when the user switches scenes.
  • GET /monitor/embed/<file> -> static HTML for embed scenes. Serves aeon-monitor/static/knot-viz.html (and any future embed pages) with Content-Type: text/html. Path-allowlisted to the static/ dir (no traversal).
  • GET /healthz -> existing handler, unchanged.
  • OPTIONS * -> existing 204 + CORS preflight (aeon-monitor.rs:3912).

How ?scene= selects

Parse the query the same way skymesh_handle_conn already splits path/query (aeon-monitor.rs:3903-3906). Add a helper:

fn monitor_scene_from_query(query: &str) -> &str {
    query.split('&')
        .find_map(|kv| kv.strip_prefix("scene="))
        .unwrap_or("skymesh")
}

A monitor_render_envelope(scene: &str, shared: &MonitorShared) -> String function builds the envelope JSON for the requested scene by reading the shared state (see below) and serializing a MonitorEnvelope serde struct.

Producing the active scene headless

The TUI render loop (run_skymesh, aeon-monitor.rs:4275) already publishes each tick's SkymeshFrame into Arc<Mutex<SkymeshFrame>> (aeon-monitor.rs:4314). Generalize to a shared multi-scene cell:

struct MonitorShared {
    skymesh: SkymeshFrame,                       // existing
    frames: HashMap<String, PleromaticFrame>,    // latest per raster/vector scene id
    // embed scenes need no per-frame state
}
type MonitorSharedRef = Arc<Mutex<MonitorShared>>;
  • In --serve mode the existing main monitor loop (SharedMesh::ingest, aeon-monitor.rs:1111) already receives PleromaticFrames over the partyline / IPC. Have ingest ALSO write the latest frame into MonitorShared.frames[scene_id_for(&frame)], where scene_id_for maps the frame predicates to a scene id: is_spectrogram_waterfall() -> spectrum-waterfall, is_noaa_apt() -> noaa-apt, is_png_embedded() -> image, is_paris_residual() -> residual, etc. This is the SAME predicate set the TUI dispatch already uses (aeon-monitor.rs:2014-2067).
  • When a scene has no live frame yet (no hardware, no orchestrator), the server synthesizes a demo frame the same way the Skymesh path falls back to skymesh_fixture(30) (aeon-monitor.rs:4357). Provide monitor_demo_frame(scene) builders (a synthetic waterfall, a checkerboard image, the handshake cloud, the canonical gate telemetry via canonical_thoth_gate_telemetry, pleromatic_frame.rs:1147). The catalog's live:false flag reflects when the client is seeing a demo.
  • For knot-viz/embed, monitor_render_envelope returns a fixed embed envelope pointing at /monitor/embed/knot-viz.html; no shared state needed.

UDP / Flow path

Generalize SkymeshUdpPublisher (aeon-monitor.rs:4199) into a MonitorUdpPublisher that, per tick, publishes the active scene's envelope as ONE Flow datagram on the MonitorFlow stream id (§4) using the SAME 10-byte big-endian header builder (encode_skymesh_flow_frame, aeon-monitor.rs:4178 — rename/duplicate as encode_monitor_flow_frame with the new stream id and FORK|FOLD flags). Keep the honest size guard (SKYMESH_UDP_MAX_DATAGRAM_BYTES, aeon-monitor.rs:4091): a raster envelope that exceeds the UDP/IPv4 limit is skipped + warned, never truncated (see §6 — large rasters stay HTTP-only). The Skymesh Flow stream 0x5353 keeps broadcasting the legacy skymesh-only frame for existing consumers; MonitorFlow is additive on its own stream id.


4. MonitorFlow adapter spec

New file open-source/aeon/src/flow/MonitorFlow.ts, mirroring SkymeshFlow.ts 1:1 in structure (encode/decode, malformed guard, default flags, stable sequence). Exported from open-source/aeon/src/flow/index.ts next to the Skymesh/BodyPolitick exports (index.ts:31-40).

Constants

export const MONITOR_FRAME_SCHEMA = 'gnosis.monitor.v1';
// Stream id: 0x6d6f = ASCII "mo" (monitor). Distinct from:
//   Skymesh        0x5353 ("SS")  (SkymeshFlow.ts:4)
//   BodyPolitick   0xb0d1         (BodyPolitickFlow.ts:4)
//   Pneuma base    0x07d0         (index.ts:190)
//   StreamSplit A/B/C 0x2000/0x3000/0x4000, residual seed 0x1000 (index.ts:185-188)
export const MONITOR_FLOW_STREAM_ID = 0x6d6f;

Types

export type MonitorSceneKind = 'raster' | 'vector' | 'embed' | 'skymesh';

export interface MonitorFlowFrame {
  readonly schemaVersion: string;       // must equal MONITOR_FRAME_SCHEMA
  readonly scene: string;
  readonly kind: MonitorSceneKind;
  readonly title?: string;
  readonly width: number;
  readonly height: number;
  readonly channels?: 1 | 4;
  readonly raster?: string;             // base64
  readonly palette?: string;
  readonly vector?: unknown;
  readonly embed?: { readonly url: string; readonly params?: Record<string, unknown> };
  readonly attributes?: Record<string, string>;
  readonly metadata?: Record<string, unknown>;
  readonly ts: number;
  readonly [key: string]: unknown;
}

export interface MonitorFlowFrameOptions {
  readonly streamId?: number;
  readonly sequence?: number;
  readonly flags?: number;
  readonly terminalBatch?: boolean;
}

Functions (mirror SkymeshFlow.ts:69-102)

  • encodeMonitorFlowFrame(frame, options) -> FlowFrame. Payload = TextEncoder().encode(JSON.stringify(frame)). streamId defaults to MONITOR_FLOW_STREAM_ID; sequence defaults to a stable FNV-1a hash of ${scene}:${ts} (reuse the stableSequence idiom, SkymeshFlow.ts:28); flags default to FORK | FOLD (0x05), | FIN when terminalBatch (SkymeshFlow.ts:63). On a malformed frame default to VENT (SkymeshFlow.ts:64) — same fail-open posture.
  • decodeMonitorFlowFrame(flowFrame) -> MonitorFlowFrame. JSON.parse the payload, then assertMonitorFrame (mirror SkymeshFlow.ts:82): reject when schemaVersion !== MONITOR_FRAME_SCHEMA, when scene is not a string, or when kind is not one of the four. Raster frames additionally require width/height numbers and a string raster when kind==='raster'.
  • isMalformed(frame) (mirror SkymeshFlow.ts:52) for the encode-side flags.

index.ts exports (mirror index.ts:31-40)

export {
  MONITOR_FLOW_STREAM_ID,
  MONITOR_FRAME_SCHEMA,
  decodeMonitorFlowFrame,
  encodeMonitorFlowFrame,
} from './MonitorFlow';
export type { MonitorFlowFrame, MonitorFlowFrameOptions, MonitorSceneKind } from './MonitorFlow';

The Rust side does NOT import this; it duplicates the wire constants the way aeon-monitor.rs already duplicates the Skymesh ones (aeon-monitor.rs:4081-4084, with the @see open-source/aeon/src/flow/FlowCodec.ts authority comment). Add a matching const MONITOR_FLOW_STREAM_ID: u16 = 0x6d6f; and reuse the existing big-endian header builder.


5. Client architecture (apps/skymesh -> scene switcher)

Today apps/skymesh/client/App.tsx is Skymesh-only: it picks demo vs live (App.tsx:97-175) and renders renderTui into <Screen>. Generalize it into a scene switcher while keeping the Skymesh view byte-identical.

New / changed files under apps/skymesh/client/

  • lib/monitor-contract.ts (NEW) — the MonitorEnvelope TS type (= §1) and a normalizeEnvelope() validator mirroring normalizeFrame (contract.ts:68) and the isValidFrame discriminant check (live.ts:75). Re-exports the existing SkymeshFrame/normalizeFrame for the unwrapped skymesh payload.
  • lib/raster.ts (NEW) — generic raster renderer: takes { width, height, channels, raster(base64), palette } and paints a <canvas>. For channels:1 it expands grayscale -> RGBA via the named palette (port the ramps from client/lib/palette.ts: colorForGreenWaterfall for green, an amber ramp for NOAA, identity for grayscale). For channels:4 it putImageDatas directly. This is the web equivalent of the TUI's render_spectrum_waterfall/render_noaa_apt ASCII ramps (aeon-monitor.rs:2844, 3046) but pixel-exact.
  • lib/live-monitor.ts (NEW) — generalize live.ts to take a scene and hit /monitor/stream?scene=<id> (SSE) with /monitor/frame.json?scene=<id> poll fallback. Same SSE-first-then-poll structure as startLive (live.ts:85-161). Validates with normalizeEnvelope.
  • lib/live-flow-monitor.ts (NEW) — generalize live-flow.ts to filter on MONITOR_FLOW_STREAM_ID and call decodeMonitorFlowFrame (live-flow.ts:108-153 pattern). Same guarded dynamic import + WebTransport check.
  • components/SceneNav.tsx (NEW) — fetches /monitor/scenes, renders a nav (tabs/list) with the live badge; sets the active scene state.
  • views/ (NEW) — one component per kind:
    • RasterViewlib/raster.ts -> <canvas>; header shows attributes.rf_center_hz / mesh_best_if_offset_hz etc.
    • VectorView — switches on scene: mesh (point cloud canvas), residual (bar ladder), gate-telemetry (per-gate bars), selftalk (text panel). Each is a small procedural draw from the vector struct.
    • EmbedView — an <iframe src={embed.url + query(embed.params)}> sized to fill the screen-wrap; this is how knot-viz renders unchanged.
    • SkymeshView — the CURRENT renderTui + <Screen> path, fed the unwrapped vector (a SkymeshFrame). Zero changes to render.ts.
  • App.tsx (CHANGED) — read ?scene= (default skymesh) alongside the existing ?live=/?flow=/?mode= params (App.tsx:34-45). Hold scene in state; render <SceneNav> + the view chosen by the active envelope's kind. Keep the demo-vs-live source toggle (App.tsx:101-150); the live source now calls startLiveSource(scene).

How the live source generalizes to ?scene=

startLiveSource (App.tsx:55) currently wraps startFlowLive (no scene) with a fallback to startLive(liveBase). The new startLiveSource(scene, cb):

  1. If ?flow= set, startFlowLiveMonitor(flowUrl, scene, cb) (filter MONITOR_FLOW_STREAM_ID, decode envelope, drop frames whose scene field does not match the requested scene — one Flow stream multiplexes all scenes).
  2. On Flow error-before-first-frame, fall back to startLiveMonitor('/monitor', scene, cb) (SSE ?scene= then poll ?scene=), exactly the fallback shape at App.tsx:65-71.

Switching scenes tears down the old handle and opens a new one keyed on scene (the existing useEffect([source]) becomes useEffect([source, scene]), App.tsx:138-150).

Demo mode

Each scene ships a tiny client fixture (extend lib/fixture.ts) so the app renders standalone with no server, the same guarantee the Skymesh demo gives today (App.tsx:171). For embed/knot-viz the "demo" is just the iframe (it is already a standalone WebGL page).


6. Implementation breakdown (3 workstreams)

Dependency order: WS-A (wire) -> WS-B (server) || WS-C (client) can start against the §1 contract in parallel once WS-A's types land.

WS-A — aeon MonitorFlow (the wire)

Files:

  • open-source/aeon/src/flow/MonitorFlow.ts (NEW) — §4. Mirror SkymeshFlow.ts.
  • open-source/aeon/src/flow/index.ts (EDIT) — add the exports block (§4).
  • open-source/aeon/src/flow/__tests__/MonitorFlow.test.ts (NEW) — round-trip encode/decode, malformed rejection, stream-id default, mirroring the Skymesh flow tests.

No dependency on B/C. Smallest honest verification: the flow package's own test runner over the new test file.

WS-B — gnosis serve (aeon-monitor --serve /monitor/ pipeline)

File: open-source/gnosis/distributed-inference/src/bin/aeon-monitor.rs (EDIT).

  • Add MonitorEnvelope serde struct (= §1) + MonitorSceneKind enum.
  • Add MonitorShared (§3) and thread it through --serve: the render loop and SharedMesh::ingest (aeon-monitor.rs:1111) write latest frames per scene id.
  • Add monitor_handle_conn mirroring skymesh_handle_conn (aeon-monitor.rs:3886): route /monitor/scenes, /monitor/frame.json, /monitor/stream, /monitor/embed/<file>; reuse SKYMESH_CORS + skymesh_write_response.
  • Add monitor_render_envelope(scene, &MonitorShared) + per-scene monitor_demo_frame(scene) synthesizers + scene_id_for(&PleromaticFrame) predicate map.
  • Add spawn_monitor_server(port, shared) mirroring spawn_skymesh_server (aeon-monitor.rs:4017); bind on the same --serve port (route both /skymesh/ and /monitor/ from ONE listener, or a second default port — recommend one listener, two routers, to avoid a second bind).
  • Generalize the UDP publisher to MonitorUdpPublisher on MONITOR_FLOW_STREAM_ID (additive; keep the Skymesh 0x5353 publisher).
  • --help text (aeon-monitor.rs:4439-4466): document /monitor/* + ?scene=.

Depends on WS-A only for the shared schema string gnosis.monitor.v1 and the stream id value (duplicated as a Rust const, not imported). Verify with the gnosis test runner (./bin/monster test, per repo memory — NOT raw bun/vitest) plus a manual curl /monitor/scenes and curl '/monitor/frame.json?scene=spectrum-waterfall'.

WS-C — app client (scene switcher)

Files under apps/skymesh/client/:

  • lib/monitor-contract.ts, lib/raster.ts, lib/live-monitor.ts, lib/live-flow-monitor.ts (NEW).
  • components/SceneNav.tsx, views/RasterView.tsx, views/VectorView.tsx, views/EmbedView.tsx, views/SkymeshView.tsx (NEW).
  • App.tsx, lib/fixture.ts (EDIT — ?scene=, per-scene fixtures).
  • Keep lib/render.ts, lib/contract.ts, components/Screen.tsx UNCHANGED (the Skymesh view rides them as-is).

Depends on §1 (the contract) and, for the Flow path, WS-A's @a0n/aeon/flow exports. The SSE/poll path can be built and verified against WS-B before the Flow path. Build via the app's build.a0.ts/npx vite build (note the a0 build subpath resolver bug in repo memory — use npx vite build which honors exports).

Risks / flags

  • raect quirks (repo memory): no auto-px on numeric inline styles (use unit strings) and incremental DOM patching — the new canvas views must use string units; the <canvas> element identity must be stable across renders so raect patches in place rather than recreating (otherwise the canvas clears).
  • Raster size over UDP MTU: a 512x48 grayscale waterfall is ~24 KB raw -> ~33 KB base64, under the 65507-byte UDP/IPv4 limit (aeon-monitor.rs:4091) but over the typical ~1500-byte path MTU; a full RGBA brain-mesh or a decoded NOAA image is much larger. Mitigation: large rasters are HTTP/SSE-only (the SSE path has no datagram limit). Over Flow, either (a) keep the existing honest skip-and-warn for oversized datagrams (aeon-monitor.rs:4243), or (b) use the Flow fragmentation already in the transport layer (UDP_MTU/FRAGMENT_HEADER_SIZE/MAX_FRAGMENT_PAYLOAD + FrameReassembler, index.ts:163-175). Recommendation: ship HTTP/SSE first for rasters; reserve Flow datagrams for the small vector/skymesh scenes (which is exactly what Skymesh does today) and only wire fragmentation for rasters if a UDP-only consumer needs them.
  • base64 cost: encoding/decoding a per-frame raster at ~4 fps is cheap, but a grayscale palette colorization in JS over width*height pixels per frame should reuse a single ImageData/Uint8ClampedArray buffer (do not allocate per frame) to stay smooth — mirror the stateless-but-buffer-reusing posture of the existing renderer.
  • Embed origin: /monitor/embed/* must be same-origin as the app for the iframe to load without CORS/frame-ancestor friction; serve it from the same host the app points ?live=//monitor at, or have the app proxy it. Path-allowlist the static dir to prevent traversal.
  • Scene/predicate drift: scene_id_for (server) and the catalog (client) must agree on ids. Single source of truth = /monitor/scenes; the client must not hardcode the list beyond the per-kind view mapping.

7. Reference index (file:line)

  • Envelope source structs: PleromaticFrame pleromatic_frame.rs:573-607; attributes map line 603; data line 600; source_id line 606; seq/timestamp_ms 575/578; media_type/level/phase/deficit 581/587/590/593.
  • Predicates: is_spectrogram_waterfall pleromatic_frame.rs:749; is_noaa_apt 682; is_png_embedded 1243; is_paris_residual 1271; waterfall helper + mesh fields spectrum_waterfall 695-747; NOAA helper noaa_apt_scanlines 657-680; PNG decode decode_png_to_grayscale_row_major 1278.
  • TUI renderers: dispatch aeon-monitor.rs:2009-2091; render_spectrum_waterfall 3046; render_noaa_apt 2843; render_png_image 2616; render_handshake_cloud 2566; render_residual_ladder 2652; render_selftalk_dashboard 2101; render_skymesh 3723.
  • Serve internals: skymesh_handle_conn 3886; routes 3932-4008; SKYMESH_CORS 3849; skymesh_write_response 3868; spawn_skymesh_server 4017; skymesh_serve_port 4049; run_skymesh shared cell 4314.
  • Flow wire: Rust encode_skymesh_flow_frame 4178; stream id 4082; flags 4084; UDP max 4091; SkymeshUdpPublisher::publish 4238. TS authority SkymeshFlow.ts (full), index.ts:31-40, types.ts (flags 22-34, FlowFrame 56-71), UDPFlowTransport fragmentation constants index.ts:163-171.
  • Client patterns: contract.ts (normalizeFrame 68), render.ts (renderTui 667), live.ts (startLive 85, isValidFrame 75, deriveMeta 33), live-flow.ts (startFlowLive 74), App.tsx (params 34, startLiveSource 55, live useEffect 138).
  • aeon-tui scenes: MonitorSceneRenderResult types.ts (rgba field); brainMeshDepthScene scenes.ts:1640; RGBA layout scenes.ts:79-105; scene registry MonitorSceneRegistry scenes.ts:2829.
  • knot-viz embed page: open-source/gnosis/aeon-monitor/static/knot-viz.html (self-contained HTML/WebGL, 2592 lines).