Cache normalized turns/calls to ~/.cache/codeburn/session-cache.json so
the CLI skips re-parsing unchanged JSONL files on subsequent runs.
File reconciliation uses dev+ino+mtime+size fingerprinting; cost,
classification, and summaries are recomputed at query time. Atomic
writes via temp+fsync+rename, deep structural validation on load,
per-provider env fingerprinting, and best-effort save so cache failures
never break the CLI. ~6x speedup on warm cache.
Three-layer fix for V8 heap exhaustion when parsing heavy session data:
1. Buffer-based readSessionLines (fs-utils.ts): Replace readline with raw
Buffer streaming using Buffer.indexOf(0x0a). Eliminates ConsString trees
that caused OOM when regex-flattening 100MB+ lines. Two-state machine
(ACCUMULATING/SCANNING) skips old lines at ~2KB cost instead of 200MB.
2. Large-line streaming parser (parser.ts): Hand-written JSON scanner for
lines >32KB extracts only cost/token/tool fields without JSON.parse,
avoiding full object graph allocation. Dual string/Buffer paths.
3. Dashboard memory management (dashboard.tsx): Disable auto-refresh for
heavy periods (30d/month/all), clear old dataset before reload via
nextTick to allow GC, prevent overlapping reloads with mutex, lazy
optimize scanning on keypress instead of useEffect.
Also fixes three race conditions in dashboard reload deduplication:
- Early return after nextTick bypassing finally block (permanent mutex lock)
- A->B->A period switching dropping final reload (stale pending)
- Stale pendingReloadRef not cleared when in-flight matches request
package-lock.json was stale at 0.9.7 with engines >=22; now matches
package.json 0.9.9 / >=22.13.0.
The menubar-json CLI test used hardcoded 10:00/11:00 UTC timestamps
which fall in the "future" when the test runs before those hours,
causing the menubar's todayRange (start..now) to exclude them.
Use timestamps relative to now instead.
All Unreleased items (IBM Bob, cache write pricing, OpenCode MCP,
project names, Cursor bubbles) are already on main, so they belong
under 0.9.9. Replaced the narrow status-only hydrateCache bullet
with the broader 9-command removal.
Only `status --format menubar-json` uses getDaysInRange from the hydrated
cache. The other 9 call sites (report, today, month, export, optimize,
compare, models, yield) parse their own date ranges directly via
parseAllSessions. Removing hydrateCache from these paths avoids a 365-day
backfill parse that was the primary OOM multiplier on large session dirs.
Strip heavy fields from JournalEntry immediately after JSON.parse in the
JSONL hot loop. Keeps only what downstream consumers need: type, timestamp,
sessionId, cwd, compacted user text (2000 char total cap), assistant
model/usage/id, tool_use names with Skill and Bash inputs, and MCP
inventory attachments. Text, thinking, and tool_result blocks are dropped.
Also removes redundant hydrateCache() from status --format json and
terminal status paths, and clears the session cache between period
parses to avoid pinning both today and month result sets.
This is a mitigation, not a full fix. Very large month ranges still
materialize full ProjectSummary.turns arrays. The real fix is the
streaming single-pass parser refactor.
Two related bugs in the macOS menubar `AgentTabStrip`:
1. With more detected providers than fit at the default 360pt popover
width (~7+), the off-screen provider chips were unreachable. SwiftUI's
horizontal `ScrollView` does not scroll from click-drag, and there
was no other affordance to reveal the hidden tabs.
2. Independent mouse wheels could not scroll the horizontal strip.
Standard wheels emit only vertical `deltaY` with
`hasPreciseScrollingDeltas == false`, and a horizontal SwiftUI
`ScrollView` ignores vertical-only deltas. Trackpads (which emit
horizontal deltas natively) already worked.
Fix:
- Wrap the strip in `ScrollViewReader` and add overflow-aware
left/right chevron buttons that programmatically scroll to the
next/previous visible provider via `proxy.scrollTo(_, anchor: .center)`.
Buttons only appear when `stripContentWidth > stripViewportWidth - 30`
and disable at the ends.
- Install an `NSEvent.addLocalMonitorForEvents(matching: .scrollWheel)`
in `.onAppear` (removed in `.onDisappear`). When the cursor is over
the strip and the event is non-precise (`!hasPreciseScrollingDeltas`)
with `deltaX≈0` and `deltaY≠0`, copy the `CGEvent` and transpose
`scrollWheelEventDeltaAxis1` / `PointDeltaAxis1` / `FixedPtDeltaAxis1`
onto axis 2 so the underlying NSScrollView receives a real horizontal
delta.
- Track strip hover via a `@MainActor` singleton
`AgentTabStripScrollState` so the local-monitor closure can read the
latest hover status without capturing stale SwiftUI state.
Trackpad events are passed through untouched, so vertical scrolling
elsewhere in the popover is unaffected.
Co-authored-by: Cursor <cursoragent@cursor.com>
Timestamps with Z are interpreted as UTC, causing date bucketing tests
to fail in non-UTC timezones (e.g. UTC+12 shifts Apr 9 10:00Z to Apr 8).
Local timestamps without Z are interpreted in the runtime timezone,
matching how the aggregator actually buckets dates.
Based on #112 by @lfl1337, extended to cover all affected timestamps.
Bubble rows without createdAt were defaulting to new Date(), which
misattributed historical or undated usage to Today and inflated the
daily chart. Now filtered at the SQL level and skipped in application
code.
Based on the bubble-side fix from #262 by @darthrevanyunka.
* Fix mangled project paths in By Project and Top Sessions panels
shortProject() decoded Claude Code slugs by splitting on '-', which
broke directory names containing dashes ('foo-bar' became 'foo/bar').
Switch the dashboard to consume ProjectSummary.projectPath (the
canonical cwd already extracted by parser.ts) and rewrite shortProject
to operate on a real absolute path.
* shortProject: cache homedir, normalize Windows backslashes, fix stale test helper
---------
Co-authored-by: Abdallah Meghraoui <abdallah.meghraoui@outlook.com>
Split CLI into a tiny launcher (src/cli.ts) that checks for Node >= 22.13.0
before dynamically importing the full CLI (src/main.ts). Users on Node 18
now get a clear upgrade message instead of a cryptic regex parse error from
string-width. Closes#232.
* Add IBM Bob provider
* Add workspace extraction for Cline-family providers
Extract project name from workspace directory in api_conversation_history.json
so sessions show actual folder names instead of the provider display name.
Thread projectPath through ParsedProviderCall to avoid unsanitizePath mangling
hyphenated folder names.
---------
Co-authored-by: ozymandiashh <234437643+ozymandiashh@users.noreply.github.com>
Co-authored-by: iamtoruk <hello@agentseal.org>
node:sqlite calls v8::String::NewFromUtf8 with kAbort on TEXT columns.
Cursor chat blobs often contain truncated multi-byte chars from streaming
boundaries, which triggers a V8 CHECK abort (not a JS exception).
Select all text-content columns as CAST(col AS BLOB) so node:sqlite
returns Uint8Array instead. Decode in JS with TextDecoder fatal:false
which replaces bad bytes with U+FFFD. Covers all three SQLite providers
(Cursor, Goose, OpenCode).
Removes the version blocklist (MIN_NODE_22_PATCH) and lowers engines
requirement from >=22.20 to >=22 since the BLOB cast approach works
on all Node 22.x versions.
Closes#264Closes#250
PR #296 (Cursor per-project breakdown) bumped DAILY_CACHE_VERSION
from 4 to 5 but left MIN_SUPPORTED_VERSION at 2. The migration
path (isMigratableCache + migrateDays) only fills in missing
default fields; it does NOT recompute the providers / categories
/ models rollups from session data, because raw sessions are not
retained in the cache. So a v4 cache migrated to v5 carried
forward its old per-day provider totals (single 'cursor' bucket)
for the full retention window.
Effect on users post-#296: the macOS menubar's
`current.providers.cursor` would show the orphan-bucket subtotal
instead of the full Cursor cost for any historical day whose
daily entry was computed before #296 landed. Live-test on my
machine showed cursor=$3.78 against a migrated v4 cache vs
cursor=$4.08 (correct) after the daily cache was discarded — the
$0.30 gap was the workspace projects whose costs were no longer
aggregated under the 'cursor' label by the new code.
Fix: raise MIN_SUPPORTED_VERSION to 5 so any cache with
version < DAILY_CACHE_VERSION is renamed to `.bak` and the cache
is recomputed from scratch on next run. The recompute is the same
operation that backfills the cache for a new user, so the cost is
a one-time cold-path hit (~3s on the test machine).
Test for the migration case updated to assert the new
discard-and-bak behavior. Full suite: 46 files / 654 tests pass.
Cursor's chat history showed as a single row labeled 'cursor' in
the dashboard because the global state.vscdb has no workspace
field on individual bubbles. The fix joins through Cursor's
per-workspace storage:
1. Walk ~/Library/Application Support/Cursor/User/workspaceStorage/*
2. For each hash dir, read workspace.json -> folder URI
3. Open that dir's state.vscdb, read
ItemTable['composer.composerData'] -> allComposers list
4. Build Map<composerId, folder URI>
5. emit one SessionSource per workspace plus a catch-all 'cursor'
source for composers that did not register against any
workspace (multi-root workspaces, no-folder-open windows,
deleted workspaces with surviving global rows)
The parser decodes source.path's #cursor-ws= tag, filters the
parsed bubbles to the composerIds that belong to this workspace,
and yields only those. The orphan-tag source negates the filter so
it captures every composer not in any workspace.
In passing, fix a real bug in the old code: parseBubbles set
`sessionId: row.conversation_id ?? 'unknown'`, but the JSON
`conversationId` field is empty in current Cursor builds, so every
call shipped with `sessionId: 'unknown'`. We now derive the
composer id from the row key (`bubbleId:<composerId>:<bubbleUuid>`)
which is what the workspace map joins on. The old behavior masked
the bug because every call went into a single 'cursor' project
anyway; with per-workspace bucketing the bug becomes load-bearing.
Cache version bumped 2 -> 3 to invalidate caches that still record
'unknown' as the session id.
Live-tested against my real 1.9 GB Cursor DB: the single 'cursor'
row with 1904 calls / $4.08 now breaks into 5 workspaces plus an
orphan bucket, totals reconcile exactly. 8 fixture-based tests
cover multi-workspace routing, orphan filtering, legacy bare DB
path backwards compat, multi-root workspace skip, vscode-remote
URI slugification, and total reconciliation across all sources.
Full suite: 46 files, 653 tests passing.
The Project Structure tree was duplicating information that
docs/architecture.md already covers in better detail (and updates
faster). Removing it from the README keeps the marketing-facing
README scoped to "what is this and how do I install it" and
points contributors at the proper map.
In its place, a short Sponsor section pointing at
https://github.com/sponsors/iamtoruk so users who find the tool
useful know where to support development.