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
SkymeshFramestruct (aeon-monitor.rs:3504-3514), schemagnosis.skymesh.v1, NOT aPleromaticFrame. - The Flow wire publisher
SkymeshUdpPublisher::publish(aeon-monitor.rs:4238) emits the same JSON on Flow stream0x5353(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) carriesdata: Vec<u8>(row-major grayscale OR an embedded PNG file) plus anattributes: HashMap<String,String>map withspec_width/spec_height/apt_width/apt_height/rf_center_hz/mesh_best_if_offset_hzetc.- aeon-tui scenes (
open-source/aeon-tui/src/monitor/scenes.ts) emit aMonitorSceneRenderResult(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'sisValidFrameand the Flowassert*Frameboth 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:raster—data/rgbais 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 ataeon-monitor/static/knot-viz.html).skymesh— the existinggnosis.skymesh.v12-station teleport payload, carried verbatim undervectorand rendered by the current Skymesh view.
width/height— needed for canvas sizing forraster; forvectorthey give the cell grid the existing renderer expects (render.ts works in cells). The waterfall producer already knows these asspec_width/spec_height(pleromatic_frame.rs:713-717); NOAA asapt_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.datafor waterfall/APT, decoded viadecode_png_to_grayscale_row_major, pleromatic_frame.rs:1278) from the RGBA scenes (aeon-tuiMonitorSceneRenderResult.rgba, scenes.ts:79). One number, not a media-type enum, keeps the canvas blit trivial.raster— base64 of exactlywidth*height*channelsbytes, row-major, matchingPleromaticFrame.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 forchannels=1so the client colorizes a grayscale grid (waterfall green ramp, NOAA amber). Mirrors the per-scene ANSI palette the TUI already chooses (AEON_AMBERfor NOAA at aeon-monitor.rs:2876,colorForGreenWaterfallin palette.ts). Default"grayscale". Keeps the wire grayscale (1 byte/px) instead of forcing RGBA.vector— opaque scene-typed object. Forkind:"skymesh"it is literally the currentSkymeshFrameJSON; 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).paramsbecome a query string the WebGL page reads.attributes— verbatim copy ofPleromaticFrame.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 existingspectrum_waterfall/noaa_apt_scanlineshelpers 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 aliveboolean 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:
imageandnoaa-aptandspectrum-waterfallare all grayscale row-major rasters; the only differences are dimensions (differentattributeskeys) and palette. The server normalizes all three tokind:"raster", channels:1.brain-meshis the one RGBA raster. aeon-tui scenes always produce RGBA (scenes.ts:79); when bridging an aeon-tui scene the server shipschannels:4and the raw RGBA, letting the browser skip the ASCII step entirely (the ASCII ramp is a terminal-only concern; the web canvas draws pixels).asciiTuningrides inattributesfor 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>-> onegnosis.monitor.v1envelope for that scene's current state. Defaults toscene=skymeshwhen omitted (so the endpoint is never empty). Mirrors/skymesh/frame.json(aeon-monitor.rs:3941) including the optional&format=bwbitwise variant (aeon-monitor.rs:3910,3943) for native-to-native consumers.GET /monitor/stream?scene=<id>-> SSEdata: <envelope json>\n\nat ~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 forembedscenes. Servesaeon-monitor/static/knot-viz.html(and any future embed pages) withContent-Type: text/html. Path-allowlisted to thestatic/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
--servemode the existing main monitor loop (SharedMesh::ingest, aeon-monitor.rs:1111) already receivesPleromaticFrames over the partyline / IPC. HaveingestALSO write the latest frame intoMonitorShared.frames[scene_id_for(&frame)], wherescene_id_formaps 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). Providemonitor_demo_frame(scene)builders (a synthetic waterfall, a checkerboard image, the handshake cloud, the canonical gate telemetry viacanonical_thoth_gate_telemetry, pleromatic_frame.rs:1147). The catalog'slive:falseflag reflects when the client is seeing a demo. - For
knot-viz/embed,monitor_render_envelopereturns a fixedembedenvelope 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)).streamIddefaults toMONITOR_FLOW_STREAM_ID;sequencedefaults to a stable FNV-1a hash of${scene}:${ts}(reuse thestableSequenceidiom, SkymeshFlow.ts:28); flags default toFORK | FOLD(0x05),| FINwhenterminalBatch(SkymeshFlow.ts:63). On a malformed frame default toVENT(SkymeshFlow.ts:64) — same fail-open posture.decodeMonitorFlowFrame(flowFrame)->MonitorFlowFrame.JSON.parsethe payload, thenassertMonitorFrame(mirror SkymeshFlow.ts:82): reject whenschemaVersion !== MONITOR_FRAME_SCHEMA, whensceneis not a string, or whenkindis not one of the four. Raster frames additionally requirewidth/heightnumbers and a stringrasterwhenkind==='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) — theMonitorEnvelopeTS type (= §1) and anormalizeEnvelope()validator mirroringnormalizeFrame(contract.ts:68) and theisValidFramediscriminant check (live.ts:75). Re-exports the existingSkymeshFrame/normalizeFramefor the unwrapped skymesh payload.lib/raster.ts(NEW) — generic raster renderer: takes{ width, height, channels, raster(base64), palette }and paints a<canvas>. Forchannels:1it expands grayscale -> RGBA via the named palette (port the ramps fromclient/lib/palette.ts:colorForGreenWaterfallforgreen, an amber ramp for NOAA, identity forgrayscale). Forchannels:4itputImageDatas directly. This is the web equivalent of the TUI'srender_spectrum_waterfall/render_noaa_aptASCII ramps (aeon-monitor.rs:2844, 3046) but pixel-exact.lib/live-monitor.ts(NEW) — generalizelive.tsto take asceneand hit/monitor/stream?scene=<id>(SSE) with/monitor/frame.json?scene=<id>poll fallback. Same SSE-first-then-poll structure asstartLive(live.ts:85-161). Validates withnormalizeEnvelope.lib/live-flow-monitor.ts(NEW) — generalizelive-flow.tsto filter onMONITOR_FLOW_STREAM_IDand calldecodeMonitorFlowFrame(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 thelivebadge; sets the activescenestate.views/(NEW) — one component perkind:RasterView—lib/raster.ts-><canvas>; header showsattributes.rf_center_hz/mesh_best_if_offset_hzetc.VectorView— switches onscene:mesh(point cloud canvas),residual(bar ladder),gate-telemetry(per-gate bars),selftalk(text panel). Each is a small procedural draw from thevectorstruct.EmbedView— an<iframe src={embed.url + query(embed.params)}>sized to fill the screen-wrap; this is how knot-viz renders unchanged.SkymeshView— the CURRENTrenderTui+<Screen>path, fed the unwrappedvector(aSkymeshFrame). Zero changes to render.ts.
App.tsx(CHANGED) — read?scene=(defaultskymesh) alongside the existing?live=/?flow=/?mode=params (App.tsx:34-45). Holdscenein state; render<SceneNav>+ the view chosen by the active envelope'skind. Keep the demo-vs-live source toggle (App.tsx:101-150); the live source now callsstartLiveSource(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):
- If
?flow=set,startFlowLiveMonitor(flowUrl, scene, cb)(filterMONITOR_FLOW_STREAM_ID, decode envelope, drop frames whosescenefield does not match the requested scene — one Flow stream multiplexes all scenes). - 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
MonitorEnvelopeserde struct (= §1) +MonitorSceneKindenum. - Add
MonitorShared(§3) and thread it through--serve: the render loop andSharedMesh::ingest(aeon-monitor.rs:1111) write latest frames per scene id. - Add
monitor_handle_connmirroringskymesh_handle_conn(aeon-monitor.rs:3886): route/monitor/scenes,/monitor/frame.json,/monitor/stream,/monitor/embed/<file>; reuseSKYMESH_CORS+skymesh_write_response. - Add
monitor_render_envelope(scene, &MonitorShared)+ per-scenemonitor_demo_frame(scene)synthesizers +scene_id_for(&PleromaticFrame)predicate map. - Add
spawn_monitor_server(port, shared)mirroringspawn_skymesh_server(aeon-monitor.rs:4017); bind on the same--serveport (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
MonitorUdpPublisheronMONITOR_FLOW_STREAM_ID(additive; keep the Skymesh0x5353publisher). --helptext (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.tsxUNCHANGED (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-
pxon 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
512x48grayscale 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 smallvector/skymeshscenes (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*heightpixels per frame should reuse a singleImageData/Uint8ClampedArraybuffer (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=//monitorat, 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-kindview mapping.
7. Reference index (file:line)
- Envelope source structs:
PleromaticFramepleromatic_frame.rs:573-607;attributesmap line 603;dataline 600;source_idline 606;seq/timestamp_ms575/578;media_type/level/phase/deficit581/587/590/593. - Predicates:
is_spectrogram_waterfallpleromatic_frame.rs:749;is_noaa_apt682;is_png_embedded1243;is_paris_residual1271; waterfall helper + mesh fieldsspectrum_waterfall695-747; NOAA helpernoaa_apt_scanlines657-680; PNG decodedecode_png_to_grayscale_row_major1278. - TUI renderers: dispatch aeon-monitor.rs:2009-2091;
render_spectrum_waterfall3046;render_noaa_apt2843;render_png_image2616;render_handshake_cloud2566;render_residual_ladder2652;render_selftalk_dashboard2101;render_skymesh3723. - Serve internals:
skymesh_handle_conn3886; routes 3932-4008;SKYMESH_CORS3849;skymesh_write_response3868;spawn_skymesh_server4017;skymesh_serve_port4049;run_skymeshshared cell 4314. - Flow wire: Rust
encode_skymesh_flow_frame4178; stream id 4082; flags 4084; UDP max 4091;SkymeshUdpPublisher::publish4238. TS authoritySkymeshFlow.ts(full),index.ts:31-40,types.ts(flags 22-34, FlowFrame 56-71),UDPFlowTransportfragmentation 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:
MonitorSceneRenderResulttypes.ts (rgba field);brainMeshDepthScenescenes.ts:1640; RGBA layout scenes.ts:79-105; scene registryMonitorSceneRegistryscenes.ts:2829. - knot-viz embed page:
open-source/gnosis/aeon-monitor/static/knot-viz.html(self-contained HTML/WebGL, 2592 lines).