Status: spec (v1 not yet implemented). The perf spike + real-data proof this builds on are done — see
PROOF.md. This document is the plan to take Fathom from a proven foundation to a rich, shippable v1.
Fathom is a real-time, GPU-rendered "observability cinema" for LLM-ops telemetry. Every LLM-gateway span becomes a glowing comet flowing into a lane by its real outcome — cache hits fork off as a cyan tributary, 429s retry and fail over in amber, PII is caught and blocked as red flares, plain spans run as the blue river. It plugs onto any OpenTelemetry-emitting gateway and turns the flat 2D dashboards everyone stares at (Langfuse, Phoenix, Helicone, LiteLLM) into something you can see — and use.
The dual goal: a recruiter/interview showpiece and a star-magnet OSS repo. The one bet that decides success: it must be a tool people use, not a screensaver. Everything below prioritizes the interaction (drill-down) and honesty (real spans, labeled replay-vs-live) that separate Fathom from cinematic-log gimmicks.
Already proven (don't re-litigate): 1M span-particles at 0.60 ms GPU/frame on an RTX 4070 (~28×
headroom) and 3.47 ms on integrated Intel UHD; 460 real sentinel spans render legibly with all four
outcomes visible at once. Perf and the look are not v1 risks — ingestion, interaction, and distribution are.
| Decision | Choice | Why |
|---|---|---|
| Rendering | Raw WebGPU/WGSL core (evolve the spike) + thin React DOM shell | Keeps the proven 0.60 ms pipeline + max systems-depth; three.js WebGPU would abstract the core and is immature |
| Build | Vite + TypeScript (strict, ES2022/Bundler) | Standard, fast, matches lifted configs |
| Ingestion | Generic OTLP/HTTP receiver (primary) + sentinel /traces?since= poller (enrichment) |
OTLP = works with any gateway (distribution); poller adds LLM-judge scores that never hit the OTLP span |
| v1 scope | Live streaming · drill-down · 3D cost flame graph · hosted demo · richness pass | The "rich" v1 the flagship needs |
any OTel gateway ─┐ OTLP/HTTP JSON (push, ~s)
(sentinel, …) ├──────────────▶ ┌───────────────── Fathom server (Node + TS) ─────────────────┐
│ OTEL_EXPORTER_ │ POST /v1/traces → span→normalized mapper → ring buffer │
sentinel /traces ─┘ OTLP_ENDPOINT │ GET /stream → SSE push (snapshot + live spans + judge) │
?since= (pull, judge scores) ───▶│ poller adapter → dedupe by id, trail cursor, enrich judge │
│ GET / (static) → serves the client; replay mode from file │
└───────────────────────────┬──────────────────────────────────┘
SSE (EventSource)│ normalized spans
┌───────────────────────────▼──────────────── Fathom client ───┐
│ React shell: HUD · legend · controls · DRILL-DOWN panel │
│ Raw WebGPU core: spawn queue → particle sim → additive render │
│ navigator.gpu capability gate · design tokens │
└───────────────────────────────────────────────────────────────┘
- OTLP receiver —
POST /v1/tracesaccepting OTLP/HTTP JSON (ExportTraceServiceRequest;Content-Type: application/json). WalkresourceSpans[].scopeSpans[].spans[]; extract the typed attribute union (stringValue|boolValue|intValue|doubleValue) into the normalized schema. This is the format sentinel's@opentelemetry/exporter-trace-otlp-httpemits (see Reuse map) — and any OTel source speaks it. - span→normalized mapper — port
ingest.mjsto TS; the OTLP attribute keys (sentinel.cache_hit,sentinel.guardrail_status,sentinel.guardrail_violations,sentinel.fallback_used,sentinel.cost_usd,gen_ai.request.model,gen_ai.usage.*,http.response.status_code) map to the same fieldsingest.mjsalready produces. One normalizer, two front-doors (OTLP push + poller pull). - ring buffer — bounded in-memory store of the last N normalized spans (no DB in v1). New connections get a
snapshot, then live deltas. - push channel —
GET /streamServer-Sent Events (text/event-stream). Envelope:{type:'snapshot', events:[…]} | {type:'span', event:{…}} | {type:'judge', id, judgeScore}. WebSocket is a later option; SSE is enough (one-way, auto-reconnect, trivial to host). - sentinel poller adapter (optional, per-source) — polls
GET {gateway}/traces?since=<cursor>with the admin bearer; dedupe byid, advance the cursor tomax(timestamp)trailed by a safety window (spantimestampis start-time), and emitjudgeenrichment events for spans already streamed. This is the only way to surface LLM-judge scores (they never hit the OTLP span). - replay mode —
--replay traces.jsonfeeds a captured file through the exact same mapper→buffer→SSE path at a configurable rate, for the offline demo and CI. The client can't tell replay from live except by the label.
- Raw WebGPU core — port
fathom.js/main.jsto TS modules:gpu/device.ts(adapter/device, capability gate),gpu/particles.ts(buffers, pipelines),gpu/shaders/*.wgsl,render/loop.ts. A spawn queue turns incoming spans into comets in a recycling pool (fixed capacity; oldest recycled) so live traffic of any rate stays bounded and 60fps. Keep the proven additive-sprite + closed-form motion; the richness pass reintroduces a compute pass for curl-noise flow. (M4 note: the compute pass landed stateless — same pure function, evaluated per-particle in compute — so the M2 pick mirror stays exact; seeARCHITECTURE.md.) - React shell — DOM only (canvas stays raw WGSL): HUD (live counters), legend, controls (live/replay toggle, pause, lane filters), and the drill-down panel. React never touches the render loop.
- Capability gate —
navigator.gpuprobe (adapted from Portfolio'scapabilities.ts) with a graceful fallback message; tab-visibility pause (from Portfolio'sSceneCanvas).
The renderer consumes only the normalized schema from ARCHITECTURE.md
(events[] = {t, model, provider, status, latencyMs, tokens, costUsd, cacheHit, fallbackUsed, guardrail, pii, piiCategories}), wrapped in the SSE envelope above. Any source that produces this shape works — the OTLP
receiver and the poller are just two mappers to it.
- Live streaming — spans render as they arrive over SSE; a live/replay toggle in the HUD. Bursts show as real density waves. Honest label in the panel: live vs replay.
- Drill-down (the daily-use hook) — click a comet → a detail panel shows that span's real attributes
(model/provider, status, latency, cost, tokens, cache/fallback, guardrail violations, judge score if enriched).
Hit-testing: primary = CPU spatial hash over the sparse comet heads (hundreds of live click targets,
cheap, exact); scalable option = a GPU ID-buffer pick pass (render comet id to an
r32uinttarget, read back the pixel under the cursor). Ship the spatial hash; document the ID-buffer path for scale. - 3D cost flame graph — a second mode aggregating token/$ cost by
provider → model → outcomeas a rotating 3D flame/treemap (WebGPU instanced boxes), reading the same ring buffer. Answers "where does the money go." - Hosted public demo — shipped (Opus half): the static client is deployed to GitHub Pages via
CI (
.github/workflows/deploy.ymlbuildsapp/diston push tomain;ci.ymlgates PRs). The demo runs?source=real— the labeled client-side replay, no server needed (Vitebase:'./'so it works under the Pages subpath). URL:https://manvendra-github.github.io/Fathom/. Deferred: hosting the Node server for public?source=live(SSE needs a live process — Fly.io/Render; Cloudflare Pages is static-only, a Worker+Durable Object is the advanced alt). A clickable live link is the retention hook the research calls out. - Richness pass — bloom (downsample→blur→add post pass), curl-noise flow (compute pass — reuse the spike's proven compute path), model-colored sub-streams within a lane, and live counters ($ saved, cache-hit %, PII caught) driven by the ring buffer.
app/ Vite + React client (raw-WebGPU core under app/gpu, shaders app/gpu/shaders/*.wgsl)
server/ Node + TS: OTLP receiver, mapper, ring buffer, SSE, poller adapter, replay
shared/ the normalized schema + envelope types (imported by both app/ and server/)
spike/ the current perf spike moved here (index.html, main.js, bench.mjs) — kept runnable
tools/ sentinel-dump.ts and other reproducibility scripts
docs/ CLAUDE.md, README.md, ARCHITECTURE.md, PROOF.md, SPEC.md (or keep at root)
Restructuring is M0 work, not done in this spec turn.
| # | Milestone | Exit criterion (pass/fail) | Model |
|---|---|---|---|
| M0 ✅ done | Scaffold: Vite+TS+React, raw-WebGPU core ported to TS modules, spike moved to spike/ (still runs) |
✅ npm run build green + app renders 24.9k particles from traces.json; spike/bench.mjs prints GO (see PROOF.md §3) |
— |
| M1 ✅ done | Live OTLP end-to-end | ✅ real sentinel gateway → OTLP → Fathom (144 real spans, cache/fallback/pii mapped); live SSE → browser comets (see PROOF.md §4) |
— |
| M2 ✅ done | Drill-down | ✅ clicking a comet opens the correct span's real attributes, reconciled against GET /traces/:id on the real GPU (see PROOF.md §5); pick is CPU math mirroring the shader (app/src/gpu/motion.ts) |
Opus (pick math, /traces/:id, wiring) ✅; Fable (attribute-panel UI — outcome-lit callout card: reticle + leader line, card parked opposite the comet) ✅ |
| M3 ✅ done | 3D cost flame graph | ✅ toggle + cost-by-model/provider from the live buffer, reconciles with the HUD on screen; rendered as a 3D WGSL scene (orbiting monoliths + embers, 4.5 ms/frame — PROOF.md §6) |
Opus (cost aggregation + toggle + reconciliation) ✅; Fable (3D WGSL flame: monoliths, embers, orbit camera, ray pick, tracked DOM labels) ✅ |
| M4 ✅ done | Richness pass | ✅ bloom (HDR post chain + toggle) + curl-noise (stateless compute pass, pick mirror intact) + model sub-streams + est. $ saved counter — measured at 0.106 ms GPU/frame with bloom on (157× headroom, timestamp-query; see PROOF.md §7) |
Fable (curl field, bloom look, sub-streams) ✅; Opus (perf measurement, $-saved honesty, pick-mirror integrity) ✅ |
| M5 🚧 in progress | Hosted demo + launch | Opus half ✅: static replay demo deployed to GitHub Pages via CI (deploy.yml builds app/dist on main, ci.yml gates PRs; ?source=real, base:'./'), README autoplay GIF captured on the 4070 (app/fathom-demo.gif, real-capture-replayed, PROOF.md §8). Fable half ✅: README rebuilt to the §7 launch shape (hook → hero → number → why); unfurl cards + favicon; init errors surfaced; live-mode "no gateway" notice; river pick hint; mobile pass (PROOF.md §8c). Launch (X/HN) pending |
Opus (deploy/CI config + GIF capture) ✅; Fable (README hero + public-demo polish) ✅ |
Model policy (Opus + Fable only): Fable for anything people look at (shaders, viz, UI polish, launch surface); Opus for anything that must be provably correct (pick math, cost reconciliation, OTLP mapping) and all plumbing/tests/doc-sync. Highest-value Fable milestone: M4. Don't drop below Opus on M3's aggregation — a wrong cost number undercuts the "money deserves to be seen" pitch.
- README = autoplay hero GIF in the first viewport → a live hosted demo link → one honest headline number → a short "why this exists" (LLM-ops dashboards are flat; money and PII deserve to be seen). Long, substantive.
- Launch X first (WebGPU clips are the proven viral vector), then HN in the 12–17 UTC window, then r/LocalLLaMA / r/programming / r/webgpu. Title in the ChartGPU shape: "I made LLM-gateway telemetry run at 60fps on the GPU."
- Generic OTLP so any team points one env var at Fathom and their data flows — every adopter is a channel.
- Honesty is the moat: label replay vs live; state real numbers; never fake the headline.
- Screensaver risk → drill-down is M2, not deferred; positioned as an incident/cost tool that happens to be gorgeous.
- OTLP batching latency (~seconds via
BatchSpanProcessor) → label it "near-real-time"; don't claim instant. - Pick perf at scale → spatial-hash heads for v1; ID-buffer pass documented for when comet counts explode.
- WebGPU/three pitfalls → avoided by staying raw (no three.js dependency risk).
- Non-goals (v1): auth / multi-tenant, durable storage beyond the ring buffer, non-OTel sources, mobile.
Jolt-UI (D:\Jolt-UI)
packages/core/src/webgl/aurora.ts— the imperative controller lifecycle: capability probe (:92-94), DPR clamp (:129), RAF loop (:158-164),ResizeObserver(:166-172), full dispose incl.forceContextLoss()(:143-151), reduced-motion branch (:153-156). SwapWebGLRenderer→ raw WebGPU.packages/tokens/src/theme.css(:1-125) — adopt the@theme+[data-theme]semantic token system.tsconfig.base.json— ES2022 / Bundler / strict base for the Vite config.
Portfolio (D:\Portfolio)
src/lib/capabilities.ts(:1-56) —useSyncExternalStoregate; swap the WebGL probe fornavigator.gpu/requestAdapter().src/components/ui/Hud.tsx(:1-14) + HUD CSS insrc/app/globals.css(:200-223) — the telemetry HUD chrome.src/lib/utils.tscn()(:1-8) and the tab-visibility pause insrc/components/three/SceneCanvas.tsx(:128-132).
sentinel (D:\sentinel)
- OTLP env
OTEL_EXPORTER_OTLP_ENDPOINT(packages/gateway/src/main.ts:22,.env.example:26) — must include/v1/traces. - Read API
GET /traces?since=<epochMs>&limit=<=500(packages/gateway/src/routes.traces.ts), admin bearer (auth.ts:28-35), cap 500 (routes.traces.ts:81);TraceRecordshape (telemetry/trace.ts:5-43). - From this repo:
ingest.mjs(mapper to port) andtools/sentinel-dump.ts(capture) already exist.