How a stdlib-only Python server, a vendored three.js snapshot, and a strict "metadata-only" contract turned scattered agent session stores into an explorable city — with no network, no database, and no build step.
The problem: my agents work while I sleep, and I have no idea what they did
I run half a dozen AI coding harnesses — Claude Code, Codex, OpenCode,
Antigravity, Pi, Goose… Each one keeps its own session store, in its own
format, in its own corner of my home directory: JSONL files here, SQLite
databases there, proprietary blobs somewhere else. I had hundreds of sessions and zero answers to simple questions: which project eats my tokens? When do I actually work? What does each model cost me? What tools get used?
The existing "solutions" were dashboards that wanted my API keys, my prompts, my code — uploaded to somebody's cloud. That felt backwards. The data was already on my disk. What I needed was a reader, not another service.
So I built Prism: open a localhost page, fly through your coding life in 3D.
Every design decision follows from three constraints: offline (zero runtime
network), read-only (never touch the harness stores), metadata-only
(never read a prompt, reply, or file).
Architecture: boring on purpose
The whole backend is Python stdlib only — http.server, sqlite3,
argparse, unittest. No pip install step exists, which means the install script is basically "make a venv, download one JS file, run":
./install.sh # venv + vendor three.js (SHA256-pinned) + serve
python3 server.py --export prism-data.json --no-browser # headless dump
The one network call in the project's life happens at install time
(curl three.js r128, hash-verified), and CI enforces the invariant forever:
make offline-check # grep web/ for remote URLs → fail the build if any
Frontend is vanilla JS with no bundler. Each lens is a single file exporting { create }, and create(payload) returns { group, desc, legend, camera, tick, raycast, dispose }. That tiny interface is why nine lenses were cheap to build: Cityscape, Galaxy, Constellation, Timeline, Energy, Calendar, Tools, Hours, Models.
The heart: a schema that says "no"
The most important file is core/schema.py. Every reader normalizes into one document, and the schema gate rejects it before serving if anything is off — NaN/Infinity, negatives, boolean-as-number, malformed nested lists. A drifting upstream store format degrades to status: unavailable in the UI, never a 500.
A session (trimmed from a real export):
{
"harness": "claude_code", "project": "flybench",
"model": "claude-sonnet-4-6", "startedAt": "2026-07-09T14:02:11+00:00",
"durationSec": 412,
"tokens": { "fresh": 12011, "output": 3402, "total": 15413 },
"tools": [{ "name": "Bash", "calls": 41, "errors": 2 }],
"costUsd": 0.31, "costKnown": true
}
Notice what's absent: no prompts, no replies, no code. That absence is load-bearing — it's what makes "read my agent history" feel safe.
Readers: the extension surface
Each harness is one ~100-line file. The OpenCode reader shows the whole
pattern — including the discipline that matters most:
con = C.sqlite_ro(db) # file:path?mode=ro + 500ms busy_timeout, ALWAYS
try:
cur.execute("SELECT id, name, worktree FROM project") # metadata tables
# …never the message/part tables. Those hold conversation content.
finally:
con.close()
Three helpers do the heavy lifting across all 28 readers: iter_jsonl
(skips malformed lines), sqlite_ro (read-only open), and
finalize_reader_payload (newest-first sort, 500-session cap, totals, meta).
Adding a harness is a 30-line file plus one fixture test — several Tier-2
readers (Cursor, Zed, Ollama…) sit stubbed in readers/ waiting for someone with the app installed to map the real layout.
Rendering 500+ sessions at 60fps with 2017-era three.js
The trick is old and unglamorous: InstancedMesh. Cityscape draws every session building in a single draw call; per-frame updates only rewrite instance matrices and colors for the time-scrub reveal. Same for the timeline river (markers + spikes), the calendar wall, the treemap. r128 is pinned because it's the last version that loads as one global script with zero modules — no bundler, no import maps, works from file:// if needed.
The fun engineering was anti-blob work. Real histories clump: same-day
sessions stack exactly, daily-driver projects form complete graphs, one tool (exec_command, 12k calls) eats the scene. Each lens got a declutter rule: timeline fans collisions into per-harness lanes, constellation keeps only ≥2-shared-day edges capped at top-2 per star, tools are log-scaled, treemap areas stay linear so "what you see is what you pay."
My favorite lens technically is Cityscape's true-shape maps: each of the 10 cities is hand-charted polygon data — Manhattan's diagonal island, the Seine S-curve with Île de la Cité, Sydney's branching harbour — with districts rejection-sampled onto real land, clustered downtown. No downloaded tiles, no textures; water is a plane, streets are line segments, landmarks are primitives. A subway map, not a satellite photo — and instantly readable.
Lessons worth stealing
- Constraints are features. "No network, no writes, no content" killed entire categories of bugs (auth, sync, PII leaks) before they were born — and it's the whole pitch.
- A validation gate beats defensive rendering. One strict schema check means nine lenses never think about malformed data.
- Stdlib-only is a superpower for tools like this. The install is 30 seconds because there's nothing to install.
- Graceful degradation > coverage. 8 verified readers + 20 honest "unavailable" stubs beats 28 half-broken parsers.
Prism is MIT-licensed. If you run coding agents, your history is already on your disk — go look at it: ./install.sh.
Code & more: https://www.dailybuild.xyz/project/249-prism







Top comments (0)