v0.4.1 — concurrency + cycle-aware run-rate + Reuse HTTP fallback
A quality + follow-up release on top of v0.4.0's marquee. Six issues closed, each as a focused PR that landed cleanly. Pre-release pass: 10/10 PASS, 0 FAIL, 0 UNCLEAR.
🔧 Daemon DB concurrency — the Overview no longer crashes under fan-out
DuckDBBackend.conn is now a per-thread DuckDB cursor (threading.local) over one shared database. Cursors from connect().cursor() are independent connections safe for concurrent use across threads, which is the documented DuckDB pattern.
Pre-fix symptom: with tj serve running, the Overview's parallel endpoint fan-out + Starlette's sync-route threadpool would race on a single shared connection and SIGABRT the daemon. We worked around it by fetching the Overview's panels sequentially. That workaround is now gone — the Overview fetches all six panels via a single Promise.all with deliberately asymmetric error handling (/cost is load-bearing and rejects on failure; the other five panels each carry a .catch fallback so one failing panel renders empty rather than blanking the whole screen).
Empirically verified: 90 concurrent reads against the Overview endpoint set complete with zero errors. Pre-fix this would crash the process. Issue #124, PR #161.
🗓 Cycle-aware run-rate
tj cost and the Lens spend chart's run-rate projection now honor [budget.<provider>] cycle_start_day when configured. Calendar-month users (the default) still see "by end of June"; users with a billing cycle that starts mid-month see "by Jul 15" — a dated form, because "by end of July" would mislead when the cycle ends mid-month.
core/cycle.py is shared between the existing budget_projection analyzer and the cost-API caption, so both surfaces use one piece of cycle math. The API exposes a cycle block on /api/v1/cost; the UI consumes it. Issue #138, PR #158.
📈 tj cost chart no longer silently empties on long windows
buildCostSeries previously had if (xs.length > 5000) return null as a guard against pathological grids — fine for a 90-day cap today, but if anyone ever requests hourly bucketing over a multi-year window the chart would just go blank with no explanation.
It now coarsens up an hour → day → week ladder until the grid fits, computed closed-form so an oversized array is never allocated. When coarsening fires, a footnote on the chart explains it: "Showing day buckets — this range is too long for hourly detail." Genuinely empty windows still render the existing "No spend in this window" state. Issue #139, PR #163.
📊 Status: active (compute) time alongside wall-clock
Pre-fix, a resumed Claude Code session that ran across days showed Duration: 3087m — easy to misread as a runaway. v0.4.1 distinguishes:
- Active — Σ span durations. The work actually done.
- Elapsed — wall-clock from session start to last activity. Resumed Claude Code sessions span days; this can be far larger than Active.
Visible in tj status (Duration: active 12m 3s · elapsed 2d 3h), in tj status --json (both fields), and in the Lens Status tile (two distinct rows with tooltips). A new fmtDurLong formatter renders multi-day spans as 2d 3h instead of 3087m. Issue #147, PR #164.
🔁 tj report --reuse works while the daemon is running
The Reuse report needed direct DB access to fetch each cluster's planning-call completion text — so when tj serve held the write lock, the command errored out and pointed the user at tj stop. v0.4.1 adds a dedicated GET /api/v1/reuse/clusters endpoint that returns the Reuse finding plus the skeleton-rendering extras (planning_texts + pricing_mode).
tj report --reuse now dispatches like tj optimize: direct DB connection when available, ApiBackend.fetch_reuse_clusters when the daemon owns the lock. Renderer accepts pre-fetched planning texts as an alternative to a connection. The endpoint is dedicated rather than bolted onto /optimize because per-cluster planning text can be many KB and the Overview polls /optimize every 30s — we don't make every poll pay for report-only data.
tj report --trim retains the same direct-DB limitation, now explicitly documented in CLAUDE.md. Issue #154, PR #165.
🎨 Recoverable Waste tile consistency
Three small but visible inconsistencies on the Lens Overview's tile band:
reuserendered lowercase while the other analyzer names were title-cased. Added an explicitANALYZER_METAentry plus a centralizedcapitalize()helper used in the fallback path — so the next analyzer that ships will auto-capitalize instead of falling through to its raw lowercase registry key.- Trim's
— not readyhad a leading em-dash while the other tile states didn't. Dropped to plainNot readyso the three states share a prefix-free scheme. - Cache title bold investigated and locked with a regression guard. Couldn't reproduce in current source, but the guard test now asserts no state-specific bold-title rule can be silently added.
Upgrade
pipx upgrade tokenjam
tj stop && tj serve &
tj --version # expect 0.4.1Existing installs keep their config, data, and daemon setup. No breaking changes.
Pre-release verification
A 10-step focused pre-release pass (tests/agent-pre-release-v0.4.1.md) was executed by a sub-agent against the live daemon. Result: 10/10 PASS, 0 FAIL, 0 UNCLEAR.
Coverage included the marquee #124 concurrency reproduction (90 concurrent reads, no crash), /api/v1/cost.cycle block, tj report --reuse with daemon running, the new active/elapsed status fields, the 90d cost chart render, and the tile consistency fixes — plus regression checks on TokenMaxx, all five analyzers, and the API framing block.
Full log committed to tests/results/agent-pre-release-v0.4.1-20260620T002546Z.md as a release-time record.
Honesty discipline
The release continues the v0.4.0 framing rules:
- Run-rate captions remain "linear, not a forecast" — no smoothing, no seasonality, no anomaly bands; just a date-cycle projection
- Coarsened chart windows are explicit about the coarsening, never silent
- The Reuse HTTP fallback is documented as paying-for-what-you-use (dedicated endpoint instead of bolting it onto
/optimize); no hidden cost on Overview polls - Status
ActivevsElapsedlabels distinguish work-done from wall-clock; the misleading bare "Duration" label is gone