* feat(cli): cache the menubar-json base payload on the optimize path (#1135)
The status snapshot keyed on a corpus fingerprint (#999) currently
excludes the default optimize path: persisting scanAndDetect's
findings alongside the base payload would pin a moment-in-time render
of the mutable, non-fingerprinted inputs the corpus fingerprint cannot
observe (settings.json, CLAUDE.md, defined skills/agents/commands,
MCP config) under a key with no way to invalidate them. That is the
exact defect commit 113ebb1c excluded the optimize path to avoid.
This change picks option (b) from the #1135 plan: the snapshot now
covers BOTH optimize=true and optimize=false (the queryKey already
carries optimize as a boolean, so the two are naturally partitioned
without a separate cache namespace). On a hit, the base payload is
served from disk and only the optimize block is re-derived fresh
through a new computeOptimizeBlock helper (re-running scanAndDetect
over a fresh parseAllSessions of the unchanged corpus). On a save,
the optimize block is stripped from the persisted record and
replaced with an empty placeholder, so a mutation in the
non-fingerprinted inputs between two requests at the same fingerprint
is reflected in the second response even though the rest of the
payload is byte-identical to the first. The save gate from #999 is
preserved verbatim (payload.stale !== true, payload.hydration ===
undefined, isSessionHydrationComplete()), the resident codeburn serve
in-memory output memo already keys on the full argv so the two
optimize values land in different memo entries, and the desktop
app's buildOverviewArgs has always sent the menubar-json query
without --no-optimize, so the new hit rate lights up with zero app
changes. (#1135)
* fix(cli): revert optimize-path snapshot reuse, re-validate snapshots on load (#1135)
Post-build review fix round on the optimize-path snapshot change:
1. Snapshot reuse is no longer attempted for one-shot optimize polls.
One-shot codeburn status invocations behave exactly as on main:
fresh compute, fresh generated timestamp. The pre-existing
--no-optimize one-shot save-gate behavior is unchanged.
2. The corpus-change test now holds --provider constant across both
calls, so the only delta is the appended session line and the test
proves a corpus change is picked up, not a queryKey change.
3. loadStatusSnapshot now mirrors the save gate: a persisted record
whose payload carries stale === true or a hydration block is treated
as a miss and recomputed. Belt-and-braces only; the save gate that
prevents such records is unchanged.
4. The optimize block cannot be re-derived on a snapshot hit without
the parsed corpus. scanAndDetect consumes the full ProjectSummary
trees plus a transcript scan plus ~/.claude state, so re-deriving it
pays the full fresh parse the snapshot exists to avoid on exactly
the cold processes the disk snapshot serves, and the resident serve
child gains nothing because its in-memory output memo already
dedupes a repeated argv. Deriving from warm caches is therefore not
honestly achievable, so this takes the plain exit: optimize-carrying
requests are excluded from reuse (useSnapshot = !queryScope.optimize
everywhere, the main semantics) and the computeOptimizeBlock helper,
the save-time optimize stripping, and the optimize-path snapshot
tests are reverted. Honesty over cleverness.
The CHANGELOG entry this branch added now describes the final
semantics. Full suite green, tsc clean.
* fix(tui): show quit feedback during the cold-start fill and exit on the second q (#1143)
Follow-up to #1142. With the input fix in place, q pressed during the
post-paint background index landed instantly but the exit path drained the
fill first - a deterministic ~16.5s silent wait on a 21k-file corpus.
The first q while the fill is active now arms a confirmation and renders
'Finishing background index so the next launch starts warm - press q or
Ctrl+C again to quit now' in the footer area, styled to match the
StatusBar (DIM border, ORANGE accent on the action key). The second q
takes the abrupt path, which #1109 already proved kill-safe (nothing
marked seen without being parsed, resume converges). Ctrl+C is unchanged:
it always exits through the abrupt path, whether or not the confirmation
is armed. q with no fill active exits immediately as before, with no
status-line flicker. The confirmation auto-clears the moment the fill
lands so a stale flag can never trap a later q.
The fill's indexing signal already flows to the dashboard (the #1109
banner reuses it), so no new global state was introduced. Pinning tests
added to tests/dashboard-exit.test.ts.
BREAKING NOTE FOR 0.9.22: ships in 0.9.22, not Unreleased.
* test: flush the unmount frame before the no-flicker assertion
The progressive cold start introduced by #1109 paints the dashboard from
recent files, then runs a background fill that parses the deferred
history and writes the per-file session cache. On a real 21k-file corpus
that fill's cache save publishes one JSON.stringify + fsync per provider
shard in a tight loop, and the whole save held the event loop long
enough that Ink's stdin reader never ran - a q or Ctrl+C pressed in the
post-paint window was swallowed, not buffered, and the user saw a
frozen terminal (#1139, #1141).
saveCache now yields to the event loop between every shard write, so
the Ink input handler fires between publishes. The yield sits BETWEEN
shards, never inside one, so each shard's temp+rename atomicity and the
lost-fence cleanup that owns the canonical envelope are preserved;
nothing marked seen without being parsed, the kill-safe resume
invariant from #1109 is intact, and the floored-parse memo key suffix
keeps warm reads byte-identical. The fill still parses every deferred
file, so money totals are unchanged.
The dashboard's useInput now exits on Ctrl+C through the same path as
q, so a raw 0x03 from the terminal lands as soon as the loop breathes.
Pinning tests:
- tests/save-cache-yields.test.ts: a setImmediate probe must interleave
with saveCache's shard writes.
- tests/dashboard-exit.test.ts: a raw \x03 exits the dashboard the
same way q does.
- tests/parser.test.ts: the copilot validated-clean re-parse test
tightens its fake-timer scope to only Date, so the new event-loop
yield inside saveCache still resolves under vitest's fake timers.
The #1109 cold-start one-shots (report, sessions, models, json/csv/
markdown, the desktop and menubar payloads) are unchanged.
Wire the SessionLineage field on SessionSummary and CachedFile. The
relationship is captured ONLY when a provider writes it on disk in a
durable form: a child file that names its parent (Claude's
transcript-internal sessionId on a sidechain; Kimi Code's state.json
agents[<id>].parentAgentId for any non-main agent), or a parent file
whose spawn / child manifest names the children (Claude's
agentSpawnLinks; Kimi Code's state.json agents map listing any non-main
entry). The brief forbids inference from time adjacency or directory
layout, so a parent whose only evidence is the child file living under
its directory does not yet qualify, and stays lineage-less.
Per provider:
- claude: child (parentSessionId present) or root (agentSpawnLinks
non-empty). Captured at install time and through the append-merge in
scanProjectDirs, where lineage is sticky and cached-first.
- kimicode: child (parentAgentId === 'main') or root (sibling non-main
entry alongside the main agent). state.json is read by
kimicodeLineageForSource, called from the per-provider install path.
Bump PROVIDER_PARSE_VERSIONS for both providers to session-lineage-capture-v1
so already-cached sessions gain the new field on the next cold parse
(env fingerprint change forces the re-parse; the field is purely
additive, no cost / token / call total moves).
Invariant: byte-identical report totals with and without the field.
The new parser-lineage-capture test asserts this on a Claude corpus
(parent + two children) and covers Claude two-sided linkage, Claude
one-sided (child file names parent while parent's spawn result was
compacted away), Kimi Code nested subagent (child and root variants),
no-evidence (no lineage field at all, not 'unknown'), and the cache
round-trip across a warm-disk reload.
Co-authored-by: codeburn <dev@codeburn.local>
The save gate re-read the module-level hydration global at a point the
codebase itself documents as unsafe: buildMenubarPayloadForRange runs
further parses (its own 365-day history re-parse included) after
capturing the verdict, and the last parse wins. A poll that went
read-only under a held refresh lock could therefore persist its
under-reported payload once a later parse reset the global, pinning
degraded dollars to disk until the corpus next changed. The gate now
requires the payload's own markers (stale !== true, no hydration
block), captured at the one safe read point, with the global kept as a
belt-and-braces conjunct. Regression holds the refresh lock, asserts
no snapshot file is written for the degraded pass, and that the next
clean pass writes one. Post-merge review follow-up to #999.
Round 3 adversarial-panel fixes for PR #999:
- Snapshot writes now go through a per-queryKey file plus a CAS re-read
guard before rename, closing the last-writer-wins race where a slower
recompute against an older corpus could clobber a fresher save, and
where distinct queryKeys used to evict each other's single shared slot
(B-G1).
- cline-cli's discoverSessions now fingerprints the growing
<sessionId>.messages.json sibling instead of the static
<sessionId>.json metadata file, so new turns in a live session are no
longer invisible to computeCorpusFingerprint (C-G1).
- computeCorpusFingerprint folds in computeEnvFingerprint per discovered
provider, so non-discovery env vars like CODEBURN_CURSOR_MAX_BUBBLES
and KIMI_MODEL_NAME can no longer serve a stale-forever snapshot (A-G1).
- collectFilesRecursive now resolves symlinked subdirectories and
recurses into them (with a visited-inode guard against cycles) instead
of misclassifying them as leaf files (C-G2).
- Added tests/session-cache-status-snapshot.test.ts covering concurrent
writers against the snapshot file, mirroring session-cache-shards.test.ts's
existing coverage for the main cache (D-G9).
Ports the menubar's Kimi flow to the Electron quota module: read-only
~/.kimi-code/credentials (KIMI_CODE_HOME honored), api.kimi.com usages
snapshot, terminal expired-login state that never refreshes the CLI's
short-lived token, 5-hour/weekly window mapping with over-limit clamp,
membership plan label, and the standard per-provider Settings toggle.
The renderer memo was an LRU capped at 8-24 entries, so cycling periods
evicted payloads and returning to one forced a skeleton plus a 1.5-6s
recompute. The memo is now one latest payload per key for the app
session: a revisited period paints its cached numbers in the same
commit, skips the CLI entirely when the payload is under 30s old, and
otherwise revalidates behind the painted data and swaps in place.
Failed revalidates keep last-good data with the existing error surface;
first visits keep the loading state. (#1135 part 1)
Version 0.9.21 across CLI, desktop app, and Windows menubar; refreshed
LiteLLM pricing snapshot and fallback (catches the 2026-08-24 OpenAI
GPT-5.6 cut and DeepSeek v4 repricing mid-rollout, #1134 tracks
re-tightening); CHANGELOG stamped for 0.9.21; README release links to
0.9.21. Snapshot-pinned model tests updated to current catalog truth.
Second-round review fixes for the persisted menubar status snapshot
(getagentseal/codeburn#999, ozymandiashh review 2026-08-21):
- queryKey now folds in getPricingGenerationKey() (src/models.ts): the
live LiteLLM cache's freshness, the bundled snapshot's own content
digest, and the existing CACHE_SCHEMA_VERSION pricing-behavior
version. None of these moved the corpus fingerprint before, so a
repricing fetch or pricing-logic fix could keep serving old rendered
costs against an unchanged session corpus.
- The optimize path (default; --no-optimize unaffected) no longer
reads or writes the persisted snapshot at all. Optimize findings
depend on mutable project/config/prompt/hook state with no single
enumerable fingerprint, and computing them already requires the same
full parse the snapshot exists to skip, so excluding them from the
snapshot loses no additional performance versus fingerprinting that
input surface.
Adds two regression tests exercising both fixes end-to-end.
Settings window titles itself after the visible pane like System
Settings; the popover subtitle adopts the website tagline (Your AI
Bill, Itemized); About stops printing dev (dev) when version and build
match; remaining user-facing em-dashes across popover and service
strings become sentence breaks.
Status item returns to the SF flame (the 0101 mark blurs at menubar
size); About hero shows the full-color binary flame and the About
sidebar row its bare white template, no chip; footer credits Resham
Joshi (iamtoruk) / AgentSeal, MIT; About tagline matches the website
(Your AI Bill, Itemized); UI copy drops em-dashes (currency rows use
middots); Settings window is resizable and fullscreen-capable and
recenters when restored off-screen.
Ports the desktop app's Antigravity flow: ps+lsof discovery of the
local language server, Connect-RPC quota/status probes over loopback
HTTPS with the self-signed-cert trust override scoped strictly to
127.0.0.1, disconnected-state UX when the app is not running. Also
swaps all provider sidebar marks to the official LobeHub icon set.
Settings window now replicates CodexBar's System Settings layout
(880x620 fullSizeContentView window, transparent titlebar, fixed 260pt
sidebar, SF Symbol gradient chips, native search field, grouped About
pane with Updates/Links). Status item renders the 0101 flame template
with severity tints. package-app.sh ships the SwiftPM resource bundle,
fixing the Bundle.module launch crash. Sidebar layout modeled on
CodexBar (MIT, steipete/CodexBar).
Ports the desktop app's Copilot flow (editor-plugin OAuth token from
~/.config/github-copilot, copilot_internal/user snapshot) read-only,
with github.com-host preference and UI-string sanitization.
Replaces the tab strip with a NavigationSplitView shell: search field,
General/About tile rows, and a data-driven Providers section whose real
logo marks render gray at rest and accent-tinted on hover, with green
connected dots and an N-on counter. Bundles template icon assets.
Ports the desktop app's Gemini Code Assist quota flow (loadCodeAssist
tier + retrieveUserQuota buckets, ~/.gemini/oauth_creds.json read-only,
in-memory-only refresh) to the menubar. Plan tab and empty-state gating
now treat Gemini as plan-capable.
The #1126 test pinned basename presence; under #1128's fail-closed
provenance the same fixture (no trusted provider-recorded cwd) correctly
omits the field entirely — stronger privacy, same zero-paths guarantee.
The positive basename-on-the-wire case is covered by
sync-project-provenance.
A turn anchored before the range start survives range slicing whole, so
the aggregator can emit a residue day the parse never covered (#1130,
found in #1127's root-cause work). The merge guards already defuse the
overwrite; this stops the residue from reaching durable history at all.
Applied at both ranged call sites (re-derive and gap); the aggregator
and slicer semantics are untouched — whole-corpus callers still
aggregate undated parses freely.
Diagnosed via #1127. Three guards:
- The gap-fill path no longer overwrites cached days wholesale: a
complete parse wins per (date, provider) through the same guarded
merge the re-derive path uses; a partial parse only fills days and
slices the baseline lacks. Previously a parse degraded by the
refresh lock (menubar running) could replace a good day with its
undercount permanently. The gap path also stops dropping
pendingRederive.
- A day whose only content is turn-anchored residue (a midnight-
straddling turn's category counts with zero cost/calls/tokens) is
never sealed: the watermark pulls back so the next launch re-derives
it — self-healing for every broken cache in the wild, no version
bump. Bounded to the settle window and never for the oldest cached
day.
- codeburn doctor gains a read-only Cache health section listing
residue-only dates and failed/empty session-cache entries, so the
next report like #1127 is diagnosable in one command.
Tests: residue-shape pin (day-aggregator), partial-gap cannot shrink a
populated day (fails on the old code), complete-gap parity, watermark
pull-back re-derive.
rolloutFileSessionId sliced 8192 bytes then JSON.parse. Production
session_meta first lines are 22-27 KB, so both trees returned
undefined ids and the overlapping nest source was kept. Read up to
the existing 1 MiB first-line cap and parse payload.session_id from
the complete line. Pin billed-A / nest-A / nest-B at 30 KB on the
exported no-arg singleton.