Skip to content

feat: widget auto-updater + faster dashboard startup#12

Merged
offbyone1 merged 3 commits into
masterfrom
feat/updater-faster-dashboard
May 17, 2026
Merged

feat: widget auto-updater + faster dashboard startup#12
offbyone1 merged 3 commits into
masterfrom
feat/updater-faster-dashboard

Conversation

@offbyone1
Copy link
Copy Markdown
Owner

Summary

  • Desktop widget auto-updater: new widget/src/update.ts update flow wired into the Tauri shell (updater plugin in Cargo.toml/tauri.conf.json, capability + release-workflow artifact), with UI in index.html/styles.css.
  • Faster dashboard startup: new per-loader cache (src/loaders/cache.ts + store.ts changes) so loaders (claude, amp, codex, gemini, opencode, pi) reuse parsed results instead of re-reading everything on each launch.
  • Adds AGENTS.md and minor .gitignore housekeeping.

Test Plan

  • npm run test — 58/58 pass (incl. new src/loaders/cache.test.ts)
  • npm run lint — clean (tsc --noEmit)
  • Manual: widget update prompt on a stale build
  • Manual: cold vs. warm dashboard startup time

🤖 Generated with Claude Code

@offbyone1 offbyone1 requested a review from urbanlama May 16, 2026 15:21
Copy link
Copy Markdown
Collaborator

@urbanlama urbanlama left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR. I reviewed the diff locally and ran the relevant checks. I think this should not be merged yet because there are two correctness regressions in the caching changes.

Findings:

  1. Store read cache can hide events written by another process

appendEvents() writes store-v1.json from the current in-memory state.events. If another TokenBBQ process wrote events after this process loaded its state, this process can later append its own event and overwrite the cache with an incomplete event list. The cache metadata still matches the full file set, so a subsequent loadStore() can return stale/incomplete data.

I reproduced this locally with two loaded store states: append a, load another state and append b, then append c from the first stale state. A fresh loadStore() returned a,c, missing b.

Relevant area: src/store.ts around appendEvents() / writeStoreCache(...).

  1. Gemini and Pi dedupe moved from global to per-file

The loader cache refactor moved seen into each per-file parse callback for Gemini and Pi. Before this PR, dedupe was global across all files in one load. Now duplicated sessions/messages that appear in multiple files are emitted more than once.

I reproduced this with temporary test data for both loaders: two files containing the same logical Gemini/Pi event returned 2 events instead of 1.

Relevant areas: src/loaders/gemini.ts and src/loaders/pi.ts.

  1. Local npm run widget:build now requires signing secrets

Because createUpdaterArtifacts is enabled, the full widget build reaches installer generation and then fails locally without TAURI_SIGNING_PRIVATE_KEY. That is probably fine for release CI, where the secret is configured, but it means the existing local build command is no longer generally usable unless documented or split into a signed release build path.

Verification I ran:

  • npm test passed: 58 tests, 0 failures
  • npm run lint passed
  • npm run --prefix widget lint passed
  • npm run build passed
  • npm run --prefix widget build passed
  • cargo check passed after npm run build:sidecar
  • npm run widget:build built the app/installers but failed at updater signing because TAURI_SIGNING_PRIVATE_KEY was not set

Recommendation: fix findings 1 and 2 before merging. Finding 3 can be handled by documenting the signing requirement or splitting local unsigned build vs release signed build.

@offbyone1
Copy link
Copy Markdown
Owner Author

@
Addressed in ab9a66c. Verified each finding against the code before changing anything.

Finding 1 — store read-cache race: confirmed, fixed.
appendEvents() paired listStoreFiles() (a fresh on-disk snapshot, including other processes files) with state.events (this process stale in-memory view + its own appends). sameFileSet then matched on the next loadStore() and returned the incomplete list. Fix: appendEvents() no longer refreshes the read-cache — the append already changes this process own file (mtime/size), so the existing cache stops matching and the next loadStore() does a correct full rescan and rewrites it. The cold-start cache (the actual perf goal) is unaffected. Regression test added reproducing your exact a/b/c sequence.

Finding 2 — gemini/pi per-file dedupe: confirmed, fixed.
Both now follow the existing claude.ts pattern: the per-file parse emits { dedupeKey, event } records and dedupe runs globally over all records after the cache merge. Checked amp/codex/opencode too — they have no cross-file dedupe (one session per file), so no regression there; your scoping to gemini+pi was correct. Regression tests added for both (same logical event in two files → one event).

Finding 3 — local widget build needs signing key: confirmed, split.
Release CI uses tauri-action with the secret, not the npm script, so the npm script is local-only. npm run widget:build now merges widget/src-tauri/tauri.dev.conf.json (bundle.createUpdaterArtifacts: false) so no key is needed locally. npm run widget:build:release is the signed path. Documented in AGENTS.md.

Verification: npm test 61/61 (58 + 3 new regression tests), npm run lint clean. Did not run a full local Tauri build end-to-end (expensive); the --config wiring is confirmed via the Tauri v2 CLI and your earlier finding that the build only failed at the signing step, which is now skipped.
@

@offbyone1 offbyone1 requested a review from urbanlama May 16, 2026 19:42
offbyone1 and others added 2 commits May 16, 2026 22:01
… build

Finding 1: appendEvents() paired a fresh on-disk file snapshot with this
process's stale in-memory event list, persisting a read-cache that looked
complete (sameFileSet matched) but dropped events another process had
appended. Stop refreshing the read-cache in appendEvents(); the append
already invalidates the cache via the changed file mtime/size, so the
next loadStore() does a correct full rescan and rewrites it.

Finding 2: the loader-cache refactor moved gemini/pi dedupe into the
per-file parse callback, so duplicate sessions/messages spanning files
were emitted more than once. Mirror the claude.ts pattern: carry a
dedupeKey out of the per-file parse and dedupe globally after the
cache merge.

Finding 3: split the widget build. widget:build now builds locally with
updater artifacts disabled (widget/src-tauri/tauri.dev.conf.json), so no
signing key is required. widget:build:release is the signed path
(TAURI_SIGNING_PRIVATE_KEY), matching release CI tauri-action.

Adds regression tests for findings 1 and 2 (store, gemini, pi).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Second-opinion review (Codex) surfaced a gap in the Finding 1 fix: a
store-v1.json written by the OLD buggy appendEvents() (a fresh file
snapshot paired with an incomplete event list) is still accepted after
upgrade whenever the file set is unchanged, so incomplete data can
persist until the first new event is captured.

Bumping the shared CURRENT_VERSION would have been wrong — it also
versions the on-disk ndjson event lines, so older tokenbbq builds would
skip v2 lines as "future". Introduce a separate STORE_CACHE_VERSION (2)
used only by the store read-cache, and move the cache file to
store-v2.json. Any pre-fix v1 cache is now ignored (different filename
and version field) and rebuilt correctly on the next load.

Regression test added: a poisoned cache at the old path and at the new
path with the old version field is ignored; loadStore() returns the
true on-disk union.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@urbanlama urbanlama force-pushed the feat/updater-faster-dashboard branch from 6fea793 to fa539db Compare May 16, 2026 20:02
@offbyone1
Copy link
Copy Markdown
Owner Author

Follow-up: ran a second-opinion pass (Codex, read-only) over the fix commit. It surfaced one residual gap in the Finding 1 fix worth closing now:

A store-v1.json written by the pre-fix appendEvents() is still accepted after upgrade whenever the file set is unchanged, so an already-poisoned cache would not self-heal until the next captured event. Fixed in fa539db: introduced a separate STORE_CACHE_VERSION (2) and moved the cache to store-v2.json. Note bumping the shared CURRENT_VERSION would have been wrong — it also versions the on-disk ndjson event lines, so older tokenbbq builds would skip v2 lines as "future"; the store-cache version is now decoupled from the event-line schema. Regression test added (poisoned cache at old path and at new path with old version field is ignored).

Codex otherwise concurred: gemini fix matches pre-cache behavior; pi fix correctly restores the pre-loader-cache global dedupe (the timestamp+token-sum key without sessionId is a pre-existing over-dedupe risk, not introduced here — flagging it for a separate decision, out of scope for this PR); widget split is cross-platform sound and no platform-specific tauri configs exist to conflict with the --config merge.

Heads up: I rewrote this branch's two fix commits to repair malformed commit messages (a stray delimiter had leaked in), so the SHAs changed and this was a force-push. Content/trees are unchanged. Tests: 62/62, lint clean.

@offbyone1
Copy link
Copy Markdown
Owner Author

Ready for re-review — all three findings resolved

Consolidated status (head: fa539db). Each finding was verified against the code before changing anything; a Codex second-opinion pass was also run.

Finding 1 — store read-cache race ✅

appendEvents() no longer refreshes the read-cache: the append already changes this process's own file (mtime/size), so the stale cache stops matching sameFileSet and the next loadStore() does a correct full rescan. A follow-up (fa539db) also closes a residual gap surfaced by Codex — a store-v1.json written by the pre-fix code is no longer trusted after upgrade. Fixed via a separate STORE_CACHE_VERSION + store-v2.json (bumping the shared CURRENT_VERSION would have been wrong — it also versions the ndjson event lines, making older builds skip v2 lines as "future"). Regression tests cover the a/b/c race and the poisoned-cache case.

Finding 2 — gemini/pi per-file dedupe ✅

Both now follow the existing claude.ts pattern: per-file parse emits { dedupeKey, event }, dedupe runs globally after the cache merge. amp/codex/opencode checked — no cross-file dedupe there (one session per file), so no regression; your scoping to gemini+pi was correct. Regression tests added for both.

Out of scope, flagging for a separate decision: pi's dedupe key (pi:${timestamp}:${input+output}, no sessionId) is a pre-existing over-dedupe risk — two distinct real events with the same timestamp and token total collapse. This fix only restores the pre-loader-cache global behavior; it does not introduce or worsen it. Worth its own issue if we want stricter pi keys.

Finding 3 — local widget build needs signing key ✅ (verified end-to-end)

npm run widget:build now merges widget/src-tauri/tauri.dev.conf.json (bundle.createUpdaterArtifacts: false); npm run widget:build:release is the signed path; release CI uses tauri-action with the secret, untouched. Documented in AGENTS.md.

Proven, not just plausible: ran the full npm run widget:build locally with TAURI_SIGNING_PRIVATE_KEY unset → completed (exit 0), produced NSIS .exe + MSI .msi, no signing step reached. The previously-failing updater-signing path is correctly skipped.

Verification

  • npm test 62/62 (58 + 4 new regression tests), npm run lint clean
  • Full local widget:build green without a signing key (evidence above)
  • Codex concurred on findings 2 & 3 and surfaced the Finding 1 residual gap (now fixed)

Note: the two fix commits were rewritten once to repair malformed commit messages (force-push); trees/content unchanged.

Copy link
Copy Markdown
Collaborator

@urbanlama urbanlama left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed the current head fa539db after the follow-up fixes.

The previous blocking findings look resolved:

  • appendEvents() no longer refreshes the store read-cache from a stale in-memory state, and the separate STORE_CACHE_VERSION / store-v2.json invalidates poisoned pre-fix read caches without changing the event-line schema.
  • Gemini and Pi now carry per-file cache records through the loader cache and dedupe globally after the cache merge, restoring the pre-cache behavior.
  • Local widget builds are split from signed release builds via widget:build + tauri.dev.conf.json, while release CI keeps the signed updater artifact path.

I also did not find any new blocking issues in the loader cache, store cache, or updater wiring.

Local verification on this head:

  • npm test -> 62/62 pass
  • npm run lint -> clean
  • npm run --prefix widget lint -> clean
  • npm run build -> exit 0

Approving.

@offbyone1 offbyone1 merged commit 5821d4a into master May 17, 2026
5 checks passed
@urbanlama urbanlama deleted the feat/updater-faster-dashboard branch May 17, 2026 01:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants