Security audit follow-ups on the DeepSeek Harness provider.
- **Decompression bomb.** Every zstd frame was decoded with no output bound, so
a 16 KB crafted log expanded to ~916 MB of RSS (a 65 KB one declares 2 GB).
Each frame now decodes under a 64 MB per-call cap, and the caps chain into a
running per-file budget of MAX_SESSION_FILE_BYTES: a frame is given only the
bytes the file has left, so node throws ERR_BUFFER_TOO_LARGE without
allocating past the cap. The throw propagates out of the existing skip path,
which discards the whole file rather than counting the frames read before the
bomb, so a crafted tail cannot poison a partial total. The discovery header
read takes the same per-frame cap. Measured on a 65 KB / 2 GB bomb: 916 MB
-> 67 MB peak, zero calls emitted, one notice.
Lines are still materialized eagerly; the byte budget bounds that, and making
the read lazy would change readEventLines' contract for no further bound.
- **Usage type confusion.** Token fields were read with `?? 0` and never
type-checked, so a string or array inputTokens flowed into the global totals
and the persisted cache, where `0 + [1, 2]` becomes "01,2". They now go
through numberOrZero (copilot.ts semantics: finite, positive, else 0).
All-zero calls are still skipped.
- **Snap over-scope.** The personal-files read entry is `$HOME/.dsh/sessions`
rather than all of `$HOME/.dsh`; the provider reads nothing else.
- **Third-party notice.** scanZstdFrames is transcribed from
@deepseek-ai/dsh-session-persistence-jsonl. The published npm package is
BSD-3-Clause (Copyright (c) 2026, DeepSeek) while the monorepo source
declares MIT for the same package; THIRD_PARTY_NOTICES.md reproduces the
stricter of the two and ships via package.json `files`.
zstd landed in node:zlib in 22.15; the package floor and CI pin are 22.13, so
the runtime already degrades with a notice there. The tests compressed
fixtures at runtime and failed outright. Fixture writes now fall back to
plain jsonl below 22.15 so parsing semantics still run, container-specific
tests skip, and the Tests workflow runs on both 22.13.0 and latest 22.x.
Reviewed src/providers/dsh.ts against deepseek-harness @ 99f6f02f and fixed
what the format says but the parser did not:
- A forked session's log replays its parent's events verbatim, and codeburn
parses the parent's own log as its own session, so every inherited call was
billed twice. The header's parentSession + seedLength mark that prefix;
events with seq < seedLength are now skipped.
- The model now comes from the reporting assistant/message's own
message.source, which is what actually served the step. request/header only
describes the request DSH was about to make, and is the fallback.
- user/message also carries agent-injected context (runtime snapshots, skill
bodies) under source.kind 'plugin'; only a typed prompt becomes the preview,
and it is bounded to 500 chars like every other provider rather than holding
a whole injected system prompt per turn.
- A log stamped with a session format version other than 0 is skipped with a
notice. The format is pinned at 0 upstream with no compatibility implied, so
reading a bumped format under today's assumptions would report confident
wrong numbers.
- Timestamps go through the seconds-vs-milliseconds guard and fall back to the
header createdAt, so a call can no longer carry an empty timestamp and land
in the undated cache shard.
- The compressed read buffers the whole log to scan its frames, so it now takes
the same oversize guard readSessionFile applies to the uncompressed variant.
- The zstd-unavailable notice fired once per session log; each distinct notice
is now emitted once.
- Emit workingDirectory beside projectPath, as codex does.
Tests add the upstream examples/acp-agent snapshot as a fixture, covering the
real record shapes: packed reasoning-chunks/tool-call-chunks storage rows, a
plugin-injected user/message beside the typed one, and both the streamed usage
chunk and the final assistant/message usage for the same step. Plus the same
snapshot re-encoded as multi-frame zstd with a torn tail (identical output), a
forked session, an unsupported format version, and unparsable lines.
Two live processes share one cache directory routinely (a one-shot CLI
beside the resident serve child, two menubar polls), and the shard layout
had two ways to lose data there.
- The atomic write used a FIXED temp name, so two writers publishing the
envelope — every save does — shared one `envelope.json.tmp` and
interleaved into a torn payload, or one deleted the shards the other's
envelope named. 39 of 40 rounds ended in a total cache loss. The temp
name carries a nonce again, as it did before the shard layout.
- A save reused a shard filename from its own load snapshot without
checking the file was still there. Another process republishing that
provider unlinks the old shard, so the stale writer published an
envelope naming a deleted file — read back as a corrupt provider and
dropped whole, including PR-linked orphans no re-parse can recover. A
reused shard is now existence-checked, and re-verified once more
immediately before the envelope is published; a vanished one is
rewritten from memory.
Also:
- Progress saves take a 30s floor beside the file counter. Only the
claude scan reports per file; every other provider calls saveProgress
once at its own boundary, so the counter alone never fired there.
- The unreferenced-shard sweep waits an hour (temps still 5 minutes): an
unreferenced shard may belong to a concurrent save whose envelope has
not landed yet.
- The sweep also retires the pre-v8 single-file temps in the parent
directory, which nothing writes anymore.
- The shard directory is created 0o700.
- The claude and provider paths mark the cache dirty where they DELETE a
stale entry, not only where they replace it: an unreadable file skips
the replace, and the deletion would otherwise live only in memory.
- Codex only treats a grown file as an append when the recorded boundary
still lands just after a newline, so a same-inode rewrite that happens
to end up larger re-parses instead of resuming mid-line.
Codex rollout files are append-only and the active ones run to hundreds
of MB, but any growth re-read the file from byte 0 because the cache
keyed only on mtime+size. The parser now records a restart point at every
task_started boundary — the byte offset plus the state the single-pass
decode carries across it — and a grown file with the same dev/ino picks
up from there.
The boundary sits at the task_started line itself, so the task it opens
is re-decoded from the tail; the entry stores how many calls were decoded
before that point so the resumed run starts from exactly those and cannot
double-count the open task. An unusable or absent snapshot falls back to
a full re-parse.
CODEX_CACHE_VERSION is deliberately not bumped: the new fields are
additive and absence-safe both ways, so a bump would discard a warm
cache for nothing.
A warm launch rewrote the entire session cache whenever any provider
appended a few KB: on a 6 GB corpus that is a 155 MB stringify + fsync
every run. The on-disk cache is now a version-suffixed directory holding
one shard per provider plus a small envelope, and a save rewrites only
the providers marked dirty.
- Dirtiness is tracked per provider (markCacheDirty) instead of one
global flag, so an appended Claude session no longer republishes
Codex, Copilot and the rest.
- Shards carry a nonce in their filename and the envelope is renamed
last, so a save is published at a single point: readers never see a
half-updated set, and a writer that loses the refresh ownership fence
leaves the canonical shards untouched.
- A shard that fails validation is treated as an absent provider rather
than rejecting the whole cache, so one malformed turn costs one
provider's re-parse instead of every provider's history.
- v7 migrates losslessly: the blob is re-laid-out into shards and
removed only once that save publishes. Nothing re-parses.
- Cold-parse progress saves now trigger every N files parsed rather than
every 5s, so a slow cold parse no longer rewrites the growing cache on
a wall clock.
Reads DSH sessions from $DSH_HOME/sessions (default ~/.dsh/sessions):
one directory per session holding session.jsonl.zstd (or an
uncompressed session.jsonl when compression=none).
The .zstd log is a concatenation of independent zstd frames (one per
appended event batch), which node:zlib's one-shot API cannot decode
whole; the provider ports the frame-boundary scan from the official
@deepseek-ai/dsh-session-persistence-jsonl package and decompresses
frame by frame. zstd needs Node >= 22.15; older runtimes get a notice
and DSH data is skipped.
Usage follows dsh-token-meter semantics: an assistant/message usage
report is the final value for its (turn, step) and replaces the
earlier assistant/chunk sample instead of double counting. Models come
from the most recent request/header config; reasoning tokens are
billed at the output rate. One parsed call per (turn, step), dedup key
dsh:<sessionId>:<turn>:<step>.
Hermes, Cursor, OpenCode and copilot OTel sources all live in SQLite
databases that their agents keep open in WAL mode for the life of the
process. Committed writes park in <db>-wal until a checkpoint, so the
main file's stat can sit hours or days behind the newest committed data.
fingerprintFile only statted the main file, which broke two ways:
- The date-range mtime pre-filter in parseProviderSources read the stale
mtime as "nothing in range" and skipped the source entirely. Every
Hermes session committed after the last checkpoint vanished from
reports: the today-parse skipped the db (mtime < local midnight) while
the backfill only keeps days through yesterday. Exactly the "17
sessions in the DB, 14 reported, the 3 from today missing" report in
issue #913.
- reconcileFile saw an unchanged fingerprint between checkpoints and kept
serving stale cached turns for sessions that had since grown.
Fold the -wal sibling into the fingerprint: newest mtime wins and sizes
add, so both WAL growth and a checkpoint (db grows, wal truncates) move
the fingerprint. -shm is deliberately ignored (it mutates on reads).
Bare SQLite paths get the fold only when the extension says database, so
JSONL transcript fingerprints (offset-based append detection) are
untouched.
Refs #913
- vscode-cline-parser: entry.ts was truthy-checked but not validity-checked,
so a garbage timestamp made new Date(ts).toISOString() throw RangeError and
abort the whole session parse. Validate the date, fall back to empty.
- models: parseLiteLLMEntry read fields off its argument with no null/type
guard, so a null value in the remote LiteLLM pricing JSON threw and aborted
the entire live pricing load. Return null for a null/non-object entry.
Both mutation-checked: the tests raise RangeError / TypeError before the fix.
parseChatFile estimated input tokens from pendingUserMessage - the last
human turn sliced to 500 chars - while output summed every bot char, so a
multi-turn session or any prompt over 500 chars undercounted input tokens
and therefore costUSD severalfold. Accumulate every human turn's full
length (inputChars), matching the modern-execution path; keep the 500
slice for the display userMessage only. Mutation-checked: a 2400-char
prompt reports 125 tokens before, 600 after.
OpenClaude is a Claude Code fork routing to any LLM; transcripts are
Claude-Code-schema JSONL under ~/.openclaude/projects/<slug>/<uuid>.jsonl
with replay.json siblings skipped. Only usage-bearing assistant lines
become calls; sidechain lines are counted as real spend; costs are always
computed (the transcript reports none) through the shared tables.
Real local testing: sessions generated with the actual CLI against
DeepSeek (deepseek-chat), parsed end to end.
The rollup fallback was gated on the post-dedup emitted counter, so a session
whose per-message calls were all suppressed by the shared dedup (a duplicated
session directory reusing a session_id) fell through to the metadata.usage
rollup and double-counted its cost. Gate on a hadMetrics flag set before the
dedup check instead.
The Cline CLI (npm `cline`, 3.x) stores sessions as
<sessions>/<id>/<id>.json + <id>.messages.json. The existing `cline`
provider only discovers tasks/<id>/ui_messages.json, so every CLI session
was silently reported as $0.00 — no warning, not even under --verbose.
Adds `cline-cli` as its own provider rather than a third root on `cline`,
leaving the shared Cline-family parser (Roo Code, KiloCode, IBM Bob)
untouched. It mirrors the CLI's own root resolution
(CLINE_SESSION_DATA_DIR -> CLINE_DATA_DIR -> CLINE_DIR -> ~/.cline),
implements probeRoots() so `doctor` can tell "not installed" from "wrong
override", emits one call per assistant message's `metrics` block, and
falls back to the session rollup when a session carries none. The
fallback reads `usage`, not `aggregateUsage`, which folds in spawned
subagents that are themselves separate session directories.
Two supporting changes, both required for CLI costs to report correctly:
- parser.ts re-priced cline-cli calls from tokens because the provider
was not on the reported-cost allowlist, inflating a real 12-session
local sample from $1.11 to $3.92.
- session-cache.ts gains the matching PROVIDER_ENV_VARS entry (so a
changed override invalidates) and a `reported-cost-v1` parse version
(so sessions cached before the allowlist fix re-parse once instead of
being re-priced forever).
Cost is treated as metered only when actually present and non-negative,
so a metered $0 stays reported while a missing or negative cost falls
back to token pricing — applied identically on the per-message and
rollup paths. Timestamps promote a seconds-resolution value rather than
silently landing in 1970, matching the guard kiro.ts uses.
CLINE_DIR / CLINE_DATA_DIR / CLINE_SESSION_DATA_DIR are added to the test
env-isolation list so a developer's real sessions cannot bleed into
fixtures.
The VS Code variant discovery bug reported alongside this in #874 is
deliberately NOT fixed here — it shipped in #882.
Verified against 18 real local sessions: 142 calls, 4,934,762 input /
224,561 output tokens, and a cost matching the CLI's own metered total to
the cent. `codeburn doctor` reports "Cline CLI OK".
Refs: #874
Follow-up to #881. Structural discovery admits third-party rollouts whose
schema is unverified. Two unchecked JSON.parse fields still reached string ops
on the parse path: an unparseable timestamp threw RangeError out of the
fork-cutoff Date math, and a non-string model threw TypeError from calculateCost
(.replace). Either sank that session's usage to zero. Skip the fork cutoff for
an unparseable timestamp, and only adopt a string model (falling back to a real
model otherwise) so the session is counted instead of silently reading zero.
Codex session discovery required `payload.originator` to start with
"codex" (case-insensitive). `originator` is a free-form client identity
string, not a format marker: any tool driving `codex app-server` writes
structurally identical rollouts under ~/.codex/sessions with its own
value ("t3code_desktop", "JetBrains.IntelliJ IDEA", ...). Those sessions
were silently dropped from every report, and each past fix only admitted
one more spelling.
Gate on structure instead: a first line that parses as JSON, has
type === "session_meta", and carries a plain-object payload. Foreign and
malformed files are still rejected. Directory ownership decides the
provider — codex.ts is the only provider that reads ~/.codex, and the
walk only visits rollout-*.jsonl under the strict YYYY/MM/DD path or
archived_sessions/ — so no double counting is possible. `originator` is
still parsed onto the meta entry; nothing downstream reads it.
Bump the daily cache to v16. Historical days are served from that cache
(usage-aggregator only recomputes today) and retention is ten years, so
without a bump an upgrading user with a warm cache keeps the pre-fix
rollups forever: discovery reruns, so the session COUNT moves, while
cost and calls stay frozen — a self-contradicting report that reads as
"fixed". Measured on a fixture with two same-day rollouts, one
codex-cli and one t3code_desktop:
pristine main, fresh cache cost 4.55 calls 1 sessions 1
this branch, main's warm cache cost 4.55 calls 1 sessions 2 (was)
this branch, main's warm cache cost 18.2 calls 2 sessions 2 (now)
this branch, fresh cache cost 18.2 calls 2 sessions 2 (truth)
CODEX_CACHE_VERSION and PROVIDER_PARSE_VERSIONS.codex deliberately stay
put: both caches are keyed per file path and are written only after a
successful parse, so a file rejected at discovery has no entry to
invalidate. Verified on the fixture above — main's codex-results.json
and session-cache.v7.json hold only the first-party rollout, and reusing
them unchanged still yields the correct total.
Harden `payload.cwd` while admitting unverified clients. It is declared
`string` but comes straight off JSON.parse, and a number/object/array
threw "cwd.replace is not a function" out of sanitizeProject; the throw
escaped discoverSessions into safeDiscoverSessions, which returns [] for
the WHOLE provider, so one malformed file made every Codex report read
zero. Guarded in discovery (falls back to the `unknown` project) and on
the parse side, where a non-string cwd would otherwise ride into
projectPath/workingDirectory and reach the parser's path helpers.
Closes#873, closes#626.
Cline discovery only looked at the stable VS Code globalStorage root, so
tasks created in VS Code Insiders or VSCodium were never found. The
singular getVSCodeGlobalStoragePath helper returns paths[0], and because
the provider always passed a concrete overrideDir, the 3-variant fallback
inside discoverClineTasks was never reached - unlike the Roo Code and
KiloCode siblings, which pass overrideDir straight through.
Build the default roots from getVSCodeGlobalStoragePaths (stable,
Insiders, VSCodium) plus the ~/.cline/data root and hand them to
discoverClineTasks in one call. The existing dedupe by task id still
collapses a task id seen in more than one root, so totals cannot inflate.
The configuredDirs override used by tests and createClineProvider(dirs)
is unchanged.
The shared Pi/OMP discovery gate only checked the first physical line of a
transcript for a `type: "session"` record. Oh My Pi writes a fixed-width
`type: "title"` metadata line before that header (upstream
can1357/oh-my-pi@0ce330a, 2026-06-27), so valid OMP transcripts were rejected
and omitted from `codeburn sessions --provider omp`.
readFirstEntry now scans up to MAX_HEADER_LINES_SCANNED (20) leading lines via
the existing streaming readSessionLines helper, skipping blank lines and
malformed JSON, until it finds a session record. This keeps discovery bounded
for message-only files (and pathological blank/junk-line runs) instead of
reading the whole file, and Pi and OMP continue to share the exact same
discovery path.
Fixes#845
Active/tool-wait timing used to be back-patched onto calls that had
already been appended to the result list. Buffer the calls decoded since
the last task_started instead, stamp them in place on task_complete, and
append them at the next task_started or at end of stream, so a task is
only emitted once its timing is final. Single-pass and split decodes then
agree instead of depending on where a decode boundary falls.
Output is unchanged: parsing 827 real rollouts (4.2 GB, 51224 calls) with
and without this change produces byte-identical results. The buffer holds
one task's calls; the deepest observed over that corpus was 924 against a
result list of 8196.
Attribution still spans everything since the last task_started, which
matters when a mid-file session_meta re-arms the fork-replay cutoff and
swallows a task_started while its task_complete lands past the cutoff.
The added test pins that case.
Review fixes on top of #805.
Discovery: getCachedCodexProject short-circuits again without opening the
file. The PR read the first line of every rollout to collect session_id,
which cost +129ms on an 827-file session directory with a warm cache.
Archived duplicates are now filtered by basename, which needs no file I/O.
Timing: on oversized mcp_tool_call_end lines a `duration_ms` key inside
invocation.arguments outranked the payload-level duration, inflating tool
wait. The depth-aware payload value wins; the naive scan stays as the
fallback for task_complete.
Layout: MIN_WIDE goes back to 90 so 90-129 column terminals keep the
two-column dashboard. The By Model panel drops the Tok/s column (and its
footnote) when the panel has fewer than 61 inner columns or when no model
recorded active timing, so narrow terminals do not truncate and non-Codex
users get no dead column.
The desktop sessions resolver returned a single per-platform path, so
Microsoft Store (MSIX) installs of Claude Desktop were invisible: their
data lives under %LOCALAPPDATA%\Packages\<Claude package>\LocalCache\Roaming\Claude\local-agent-mode-sessions
and a filesystem junction workaround breaks Cowork's own file access
(reported and verified in #611).
getDesktopSessionsDir() becomes getDesktopSessionsDirs(): an ordered,
deduped candidate list (override, then classic APPDATA, then MSIX packages
matching Claude_* or *.Claude_*, existence-checked, lexicographically
sorted; .config on Linux). Results are memoized per env-input tuple so the
parser's per-file classification never rescans Packages. All call sites
scan every candidate; macOS, Linux and classic Windows behavior unchanged.
Fixes#611
- Resolve k3/k3-agent/k2d6-agent model aliases to canonical Kimi names
- Discover sessions across all Kimi Code homes (CLI + desktop runtime)
- Accept conv-*/ctitle-* session directory naming, not just session_*
- Add Kimi Code provider tab with brand color to the menubar
- Show short model names (Kimi K3, Kimi K2.6) in the menubar payload
Three review leftovers plus one inconsistency found while fixing them.
computePace returns undefined for a non-finite usedFraction instead of
letting NaN sail through the clamp into every pace field.
Daily-cache retention goes from 730 days to 3650: the cache is the only
durable record of carried days and lifetime now reads from it, so the
2-year prune would have replayed the lost-history bug in slow motion at
that horizon. Measured cost stays small (~2.3 MB / ~11 ms parse per 730
dense days).
The vercel-gateway dashboard-range test pins a relative day instead of
2026-06-01, which would have started failing once the rolling 6-month
window moved past it around December.
Reuse one concrete six-month scan for interactive history and selected-period totals. This restores network-backed providers, removes concurrent refresh scans, and keeps non-interactive reports scoped to their requested period.
Adds a provider for Kimi Code CLI (MoonshotAI/kimi-code, the successor of
kimi-cli), reading ~/.kimi-code/sessions/wd_*/session_*/: state.json for
session metadata and per-agent agents/<id>/wire.jsonl event streams.
- Usage from usage.record events: inputOther->input, output->output,
inputCacheRead->cacheRead, inputCacheCreation->cacheWrite. The store has
no cost fields, so cost is computed from tokens and flagged estimated.
- Real model attribution: usage.record.model carries the config ALIAS; the
real model id lives on llm.request events. Rows resolve the alias through
the alias->model map first, with the nearest preceding request as
fallback, so aliases never leak into reports.
- Subagent wire files parse as separate sources under one session without
double counting; multi-turn continuations in one wire.jsonl are one
session with multiple turns; retry-only failed sessions parse to zero
usage; malformed JSONL lines are skipped.
- Tool calls feed the tool breakdown; tool state resets at turn boundaries
so a failed turn cannot bleed its tools into the next row.
- KIMI_CODE_HOME override, eager registration, PROVIDER_ENV_VARS and
PROVIDER_PARSE_VERSIONS entries, probeRoots for doctor, docs page.
Adds an eager provider for Amazon Quick Desktop (aws.amazon.com/quick/desktop),
which stores local data under ~/.quickwork. Usage source of truth is the
per-day EMF metrics JSONL (Model, InputTokens, OutputTokens, CostUSD,
session_id); real CostUSD passes through the cache like kiro/devin/hermes.
sessions.db (SQLite, read-only) enriches calls with titles, first user
message, and tool names, and provides estimated usage (quickdesk-auto,
costIsEstimated) for sessions that predate metrics coverage. Multi-profile
layout via profiles.json entries plus the migrated legacy root; honors
QUICKWORK_HOME; schema-tolerant (sqlite_master and PRAGMA introspection,
per-line JSONL error isolation). The on-disk schema is reverse-engineered and
documented as such in docs/providers/quickdesk.md.
Performance and coherence:
- Settings > General 'Refresh every' (Manual/30s/1m/3m/5m/10m), live via
RefreshCadenceContext; Manual polls only on demand
- usePolled memoKey LRU: provider/period switches paint the last-good
result instantly with a switching hairline while refreshing quietly
- quota TTL 5min + honest rate-limited copy on 429 backoff
- version-suffixed cache files (session-cache.v5.json, daily-cache.v12,
auto-minted on future bumps); legacy files never written or deleted,
adopted once when versions match: old and new binaries coexist
without clobbering (field-observed menubar-vs-desktop ping-pong)
- advisory hydration lock: concurrent cold starts share one scan
(wait-then-read-warm), stale/dead locks self-heal, never a
correctness gate
Telemetry v1 + onboarding (desktop only, per owner decisions):
- first-launch onboarding (3 feature screens + consent); region-split
default (EU/EEA/UK/CH off, elsewhere on, unknown off); nothing sends
before consent completion or while off; dev builds never send
- anonymous install UUID, rotated on opt-out; day-granularity events,
cost buckets only; whitelisted names; 200-event queue, 5min flush
- Settings > Privacy live toggle replaces the static claim
Zero computed-number changes. App 316/316, root 1805.
Wire contract targets api.codeburn.app/v1/telemetry (Worker follows).
* fix(copilot): bill CLI input and cache tokens from session.shutdown
Copilot CLI sessions (~/.copilot/session-state/*/events.jsonl) recorded
only outputTokens, because the parser read assistant.message events and
ignored the session.shutdown rollup. That rollup is the sole on-disk
source of a CLI session's input, cache-read and cache-write tokens, so
cost was underreported (in the reported case 35k+ cache-read tokens were
dropped entirely).
Handle session.shutdown in the CLI (non-transcript) JSONL path and emit
one supplementary call per model from modelMetrics.<model>.usage. The
per-turn assistant.message events keep owning output tokens, tools and
turn structure; the shutdown call contributes only the input and cache
tokens they lack, with output excluded, so no dimension double-counts and
the combined cost equals the full CLI-measured cost. usage.inputTokens is
cache-inclusive (input + cache_read + cache_write), so the cache
components are subtracted to recover the uncached input calculateCost
expects. These are real counts written by the CLI, so costIsEstimated is
false. Sessions without session.shutdown (crashed or still running) keep
the exact previous output-only behavior, and the VS Code, JetBrains and
OTel paths are untouched.
Bump the copilot entry in PROVIDER_PARSE_VERSIONS so already-cached CLI
sessions re-parse and pick up the corrected tokens and cost.
* fix(copilot): never emit an empty-timestamp shutdown call
A session.shutdown without its own timestamp and without sessionStartTime
produced a call with an empty timestamp, which the date-range filters in
parser.ts drop silently, erasing exactly the tokens this feature bills.
Fall back to the last stamped event in the session.
* fix(cache): durable orphans survive provider fingerprint changes
Bumping a durable provider's PROVIDER_PARSE_VERSIONS changed its env
fingerprint, and getOrCreateProviderSection replaced the whole section on
mismatch. For copilot OTel orphans the discarded cache entry is the only
remaining record of pruned spans, so the first post-upgrade run silently
and permanently erased month-to-date history. Carry forward exactly the
entries whose source no longer exists; present sources still drop and
re-parse under the new fingerprint, which is the point of a bump.
Regression test forges a pre-upgrade fingerprint on a pruned-source
orphan and asserts it stays counted and cached across the transition.
* feat(antigravity): add support for Antigravity IDE storage on Windows
CodeBurn previously only detected Antigravity CLI usage (.pb files under
.gemini/antigravity/). Antigravity IDE on Windows stores session state in
VSCode-style storage at %APPDATA%\Antigravity IDE\User\globalStorage\state.vscdb,
which was not detected.
Add support for reading Antigravity IDE sessions from the VSCode-style storage:
- Extend CONVERSATION_ROOTS to include APPDATA Antigravity IDE path
- Refine path classification to properly identify IDE vs CLI sessions
- Handle missing per-call timestamps by stamping file mtime as fallback
- Bump CACHE_VERSION to 4 for cache invalidation
Fixes: CodeBurn reports zero usage when Antigravity IDE is actively used.
* fix(antigravity): stamp mtime fallback at emission, tighten path classification
- Apply the mtime fallback to a copy at emission instead of mutating and
persisting it into the cache, so a later file rewrite can't retro-date a
session's history to the new mtime. SQLite gen_metadata rows have no real
per-call time; the RPC path still uses chatStartMetadata.createdAt.
- Drop the unreachable APPDATA classifier branch (discovery only walks the
~/.gemini roots; APPDATA state.vscdb holds no token usage). This also removes
the misroute of base-antigravity paths under an "Antigravity IDE" profile dir.
- Bump CACHE_VERSION to invalidate caches that persisted a synthesized mtime.
* fix(antigravity): stabilize untimestamped call times across DB rewrites
Extract ChatStartMetadata.created_at from proto-encoded data and decode multiple timestamp formats (ISO string, Timestamp submessage, unix varint). Implement assignStableTimestamps() to preserve first-seen timestamps across file rewrites, preventing retro-dating of sessions when .db files are modified. Make conversation roots dynamic (computed per call) to honor environment overrides in tests. Add comprehensive timestamp stability tests verifying that timestamps remain fixed across file mtime changes while respecting date-range filters.
* test(antigravity): assert today range excludes first-seen timestamps
Adds a test case to verify that the 'today' date range filter correctly excludes sessions based on their first-seen timestamp, not file modification time. This ensures that even if a database file is rewritten with a later mtime, sessions with earlier first-seen dates are properly filtered out.
Refs #411