HeliosDB-Nano as an optional read store (a fork to poke at, not a review ask) - #23
Open
danimoya wants to merge 3 commits into
Open
HeliosDB-Nano as an optional read store (a fork to poke at, not a review ask)#23danimoya wants to merge 3 commits into
danimoya wants to merge 3 commits into
Conversation
Add the HeliosDB-Nano integration layer and cut the analytics read path over to it (TD_PRIMARY_STORE=helios), keeping SQLite as the ingest source of truth, mirrored into HeliosDB and read back through materialized views. Read path (db_helios + helios_mv): - All-time queries served from 6 single-table GROUP BY materialized views (overview/daily/projects/sessions/by-model/skills); ranged queries fall back to live SQL; join-heavy routes (expensive_prompts, mcp_*) delegate to SQLite. Every dashboard tab serves sub-second on HeliosDB-primary. - Coerce HeliosDB aggregate results to numbers: pg8000 returns SUM/COUNT as strings while stored MV columns come back as ints, so the ranged cost math (tokens * price) raised TypeError and 502'd the overview/by-model endpoints. _drow/_num coerce by column name (no-op on the int MV reads). Write/mirror path (helios_writer + scanner): - Per-thread pg8000 connections. The server is ThreadingHTTPServer and the frontend fires several /api calls in parallel; a single shared connection is not thread-safe and corrupts the wire protocol under concurrency. - Mirror SQLite's streaming-snapshot eviction onto HeliosDB in one batch per scan (DELETE is ~1s/statement on v3.33, too slow per-message) so HeliosDB token SUMs stay equal to SQLite instead of double-counting superseded partial snapshots. reconcile_orphans() is the backlog/safety net. Supporting features: in-app login (auth), alerts, audit, branches, clusters, code_resolution, docling_ingest, embedder, mcp, pulse, bulk_migrate; Insights and MCP UI tabs; Dockerfile + vendored heliosdb_sqlite wheel for deployment; cutover validation + migration scripts. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Make the helper scripts portable and decouple docs from the private deployment so the branch is safe to publish on a public fork: - scripts/_full_migrate.py / _cutover_validate.py: resolve the repo root relative to __file__ instead of hardcoded container paths; read the DB path from TOKEN_DASHBOARD_DB (env) rather than a fixed /data/cache path. - auth.py: describe the auth mechanism generically (reverse proxy / HTTPS) instead of referencing a specific proxy deployment. No behavior change; 71 unit tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
HeliosDB's CREATE BRANCH / ON BRANCH clauses take the branch name as a
SQL string literal and don't support bound parameters for it, so the name
is interpolated. The name is user-reachable (POST /api/branches/snapshot
{name}; GET /api/branches/overview?branch=), which made both paths
injectable. Whitelist the name to ^[A-Za-z0-9_-]{1,64}$ before it touches
SQL — the dashboard's own snapshot names (td-snap-YYYYMMDD) already fit.
Matches the repo convention: f-strings in SQL may interpolate only
internal, caller-controlled values; user-reachable values are rejected
or bound.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Author
|
Quick update, Nate — since I opened this on v3.34.0, the HeliosDB-Nano side closed out every rough edge I'd flagged, so the "Still open" list in the description is now mostly historical (I've refreshed it in the PR body too). Re-verified against live builds through v3.37.0 (2026-06-04), same ~448k-message corpus: All 7 issues I filed are fixed:
Perf that moved (same corpus, real HTTP path):
Only real remaining gap I'd still flag: the published — danimoya |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Hey Nate —
I built a database — HeliosDB-Nano (Apache-2.0, OSS) — and went looking for a real workload to stress it. Yours was the one I actually wanted to run, so I deployed token-dashboard against my own
~/.claude/projects/and lived on it for weeks. The streaming-snapshot dedup bymessage.idis the right call — that's the part most people get wrong, and it's why your totals actually match what the API billed. Read-only over the JSONL, stdlib-only, no build step: I kept all of that intact. I didn't have to fight your code to extend it, which is rarer than it should be.This PR adds HeliosDB-Nano as an optional primary read store (
TD_PRIMARY_STORE=helios). It's off by default; SQLite stays the ingest source of truth and nothing changes if the flag is unset. I deliberately did not want to drop a giant review burden on you — it's a big diff and your project is rightly stdlib-only — so treat this as a fork to poke at if you're curious, not a review ask. Merge nothing, merge a slice, or just read the numbers. I want to be straight about both halves of it, because the wins are real and so are the warts.Wins. Vector/semantic search over prompts is the thing SQLite can't do at all — median 13 ms across search/clusters/alerts/audit in mirror mode. Once the materialized views were correct, read latency flipped:
/api/overview3.2 s → 0.010 s,/api/projects9.3 s → 0.246 s,/api/sessions7.8 s → 0.087 s, every tab sub-second. And it stayed correct: 6/6 read functions match SQLite to the exact integer (cache_read17,420,235,587 = 17,420,235,587), and the match held after a live scan added ~210 messages.Warts, no spin. Those MVs returned wrong aggregates for weeks —
COUNT(DISTINCT)~60× off — whilepg_mv_staleness()falsely reported FRESH. That parked me. Fixed upstream now, but it was the blocker. "Embedded mode" isn't in-process either — it pipes over a subprocess REPL, 715 ms vs sqlite3's 1.7 ms on a 433k-rowCOUNT(*). Both are filed upstream with repros, not complaints.I think the integration is the receipts, not a pitch — local vector + relational in one engine is a real, mostly-empty gap, and I'm building toward it. If any of this is interesting to you, I'd rather build it with you than at you: poke at the fork, tear it apart, or just compare notes. No pressure either way.
— danimoya
Technical detail — architecture, measured impact, honest caveats, how to try it (click to expand)
What it adds (opt-in, zero impact when off)
db_helios.pyread layer mirroring every read function indb.py, selected at startup only whenTD_PRIMARY_STORE=helios. With the flag unset, none of this code is on the hot path and the dashboard behaves exactly as it does today.helios_writer.py) that pushes the SQLite corpus into HeliosDB after each scan, preserving the(session_id, message_id)streaming-snapshot eviction so totals stay equal to SQLite.td_overview,td_overview_daily,td_project_summary,td_session_summary,td_model_breakdown,td_skill_breakdown) backing the aggregate routes.Architecture
Reads an MV can't serve correctly (the
expensive_promptsself-join, themcp_*rollups) delegate back to SQLite rather than return wrong numbers. Rollback = run without the flag.Measured impact
Validation corpus: 422,982 messages · 194,140 tool_calls · 214 sessions · 59 projects · 339 MB in SQLite.
/api/overview/api/projects/api/sessions/api/mcp/summary/api/prompts/api/daily/api/by-model/api/skillsHonest framing: the SQLite baselines are not an apples-to-apples loss for SQLite — the helios numbers benefit from precomputed MVs, and SQLite could be given the same treatment. What's genuinely SQLite-impossible is the vector search, not the aggregate speed. Container cold-start ~6 s. Full bulk migration: 470 s for 427,339 msgs / 80,750 tool_calls / 116,869 tool_results, 0 failed.
How it stays correct
scripts/_cutover_validate.py), re-passed after a live scan.kill -9.How to try it
Leave it unset and nothing changes. Keep
HOST=127.0.0.1.The one dependency, honestly
The default path stays stdlib-only, no build step, no
pip install— unchanged. When (and only when) the flag is on, this talks to HeliosDB over the Postgres wire viapg8000(pure-Python). I'm not bundling a native extension or asking you to install anything to run the dashboard as it ships. (FWIW the publishedheliosdb_sqlite"embedded" wheel is not in-process — it shells out to a subprocess REPL, 715 ms vs sqlite3's 1.7 ms on 433k rows — so I use the PG wire, not that wheel.)Honest caveats / limitations
Originally measured on HeliosDB-Nano v3.33.0, re-run on v3.34.0, and re-verified through v3.37.0 (2026-06-04, same ~448k-message corpus). Update: every item I'd flagged as open has since been fixed upstream — the list below now reads mostly as a changelog. Most were upstream HeliosDB issues; the rest are worked around client-side.
Fixed in v3.34.0–v3.37.0
kill -9), then merely slow (~140 s for a real 200-row DELETE on v3.33.0). v3.34.0: 1.05 s (~133×); v3.36.1: 0.713 s, and the per-statement floor that forced the batch-DELETE-per-scan workaround is gone (no-op DELETE ~0.6 s → ~0 s)./api/tools) was 0.586 s on v3.33.0. v3.34.0: 0.009 s (~65×) (~0.011 s on v3.36.1). (The MV-backed routes were already sub-second on both.)--http-port— v3.37.0:GET /mcp/inforeturns real capabilities andPOST /mcptools/listreturns a real catalog, so the MCP tab finally has an endpoint. AndCODE_EMBED(text)is now a real SQL function (384-dim vectors), so semantic search / clusters / dup-retrieval run on real embeddings instead of the BLAKE2b hash fallback.COUNT(*)over a materialized view + MV-column aggregate return type — fixed v3.35.0 (MVCOUNT(*)returns the right count; aggregates come back as ints, consistent with base tables).$parambinding — fixed v3.36.0 (a wire-routing bug;WITH … SELECTwas misrouted to the command path).db_helios.pyno longer needs to stay CTE-free.SHOW BRANCHESempty afterCREATE BRANCH— fixed v3.36.2 (now lists named branches).CREATE SCHEMA— fixed v3.36.0 (accepted as a flat-namespace no-op).Fixed earlier
COUNT(DISTINCT)~60× off,COUNT/SUM~1000× off) whilepg_mv_staleness()reported FRESH. Fixed v3.32.2+.Still open
heliosdb_sqlite"embedded" wheel is not in-process — it shells out to a subprocess REPL (715 ms vs sqlite3's 1.7 ms on a 433k-rowCOUNT(*)), so I use the PG wire (pg8000), not that wheel. This is the one item from the original list that hasn't moved.Worked around client-side (version-independent)
SUM/COUNTas ints but MV-column aggregates as strings (v3.33.0) /NULL(v3.34.0); coerced by column name so the cost math doesn't 502.ThreadingHTTPServer; fixed with per-thread connections.Credit where due: HeliosDB upstream closed 8 of 11 dashboard-migration bugs I filed by v3.26.0, the
ON CONFLICT DO UPDATEpath went 0.4 → 690 ops/sec at 424k rows (~1700×) in v3.30, v3.34.0 took the two biggest operational warts (DELETE, live JOIN) off the board, and v3.35.0–v3.37.0 closed all 7 issues I'd filed against the cutover — including MCP-over-HTTP and a realCODE_EMBEDSQL function in v3.37.0.Scope / size
Large diff — 32 files, +5,933 / −62 (biggest new modules:
db_helios.py+733,helios_writer.py+641,mcp.py+442,db.py+423). Happy to split this — e.g. land the read-only mirror + vector features first and the MV primary-store cutover second, or feature-gate the modules so they only load when the flag is set. Tell me the shape you'd prefer.Testing done
scripts/_cutover_validate.py— 6/6 exact-integer match, re-passed after a live scan.TD_PRIMARY_STORE=helios; latencies above are measured.