Commit graph

22 commits

Author SHA1 Message Date
Resham Joshi
29fe23e6d6
dashboard: fold instead of spread when measuring column widths (#1460) 2026-09-16 18:52:02 +02:00
Aditya Vikram Singh
81f1eb3d6a
perf(tui): show cached periods before refreshing sources (#1159)
* perf: paint Today before indexing TUI history

* perf(tui): serve cached periods before source refresh

* fix(tui): keep cached snapshots out of parse memo

* fix(tui): preserve provider and period truth

* fix(tui): label visible period while indexing

* fix(tui): recover provider switches and empty Today
2026-08-28 13:21:47 -07:00
Aditya Vikram Singh
9a93fdd77d
fix(tui): clear stale panels on settled resize (#1156) 2026-08-28 13:21:03 -07:00
iamtoruk
f060a2d806 Merge origin/main into feat/copilot-session-store-landing
Resolved against main's newer invariants:

- daily-cache: DAILY_CACHE_VERSION/MIN_SUPPORTED -> 21. Main shipped 19
  (#1015 grok); 18 was burned by this branch's earlier public head and 20 is
  claimed by the unmerged #1040, and isMigratableCache would adopt either as
  finalized without re-deriving. Header keeps main's v19 note and adds v21's.
- session-cache: PROVIDER_PARSE_VERSIONS keeps main's grok/dsh entries and
  appends this branch's `-session-store-v2` to main's copilot value.
- parser: durable age-out takes main's orphan-only rule (#992).
  retainWhilePresent is now redundant under it rather than load-bearing.
- audit-report: both imports (isBehavioralCall + sanitizeModelForDisplay).
- tests/parser (e), (l), (sc): re-pinned to the orphan-only age-out. A
  still-discovered >90d copilot events.jsonl now keeps serving its per-turn
  output alongside the store rows; the (sc) age-out subject is now an orphan.
- daily-cache-version-rederivation: seed 20 so the adjacent-draft-version
  case is what the test pins.
- scripts/upgrade-path: expected daily cache filename -> v21.
2026-08-19 12:29:33 -07:00
iamtoruk
6b427d72ce fix(models): honest unpriced guidance, shared ID sanitizing, readable narrow warning
The unpriced-models pointer told every user to run `codeburn model-alias`. A
subscription or flat-rate model is correctly $0, and mapping it onto another
model's per-token rate invents spend that was never billed (#968), so the hint
now states the condition instead of the instruction.

`sanitizeModelForDisplay` guarded only the `--unpriced` path, leaving every
other command and format rendering provider-supplied IDs unfiltered. It moves
to the `formatModel` closures in models-report and audit-report, the two sites
every renderer routes through, covering the raw-ID fallback as well. The
`--unpriced` override stays: it bypasses that path deliberately, because
`model-alias` keys on the raw ID, not the friendly name. README says so.

Below 45 columns of panel the dashboard warning dropped its marker and count
and printed a bare command, so a narrow terminal gave no signal that anything
was wrong. `! N: codeburn models --unpriced` is 31 characters and fits.
2026-08-18 09:58:25 -07:00
iamtoruk
2d4c00a9a8 merge origin/main into pr996
Keep both: #985's --unpriced filter (in main) and #996's top/ordering/sanitize/dashboard changes.
2026-08-18 08:50:38 -07:00
iamtoruk
c5df60e69a merge: main into connector guidance branch
Kept both sides' act-report tests: the PR's partial-action baseline
capture block and main's applied-fix verdict block.
2026-08-18 08:17:25 -07:00
iamtoruk
0aed84f4ca Merge remote-tracking branch 'origin/main' into feat/optimize-finding-class
# Conflicts:
#	tests/dashboard.test.ts
2026-08-18 02:40:53 -07:00
iamtoruk
7c54cf85c2 optimize: classify findings as fix/nudge/keep and mark measured vs estimated
Every finding now resolves to a class (apply-able fix, habit nudge, or
informational keep) and a basis (measured from provider-counted usage, or
estimated from a schema/heuristic model), both from one table next to the
FindingId union. The class follows the plan layer: an id is 'fix' only when
buildPlan routes it, and an instance drops to 'nudge' when it lacks the
payload or cause its builder needs.

CLI, TUI and the desktop app group findings under Fix now / Habits / FYI
with continuous numbering; the CLI header reports 'N measured · M
estimated' in place of the blanket 'Estimates only.' footer. The JSON
report gains class + basis per finding and summary.measuredSavingsUSD;
existing fields are unchanged. The menubar's top three follow the same
order, since every surface reads the sorted findings list.

Sessions whose cost the provider never reported leave the cost-outliers
peer math; when nothing else is priced the comparison falls back to them
and the finding reports itself as estimated instead of disappearing.
2026-08-18 02:26:57 -07:00
iamtoruk
e2007c5e2f test(dashboard): wait for the Optimize scan on real event-loop turns, not fake-timer hops
The Optimize scan does real fs I/O (readdir/stat) that only resolves on a
real event-loop turn, but the wait loop counted 20 vi.advanceTimersByTimeAsync
hops under full fake timers, which flush fake timers + microtasks but never
give real I/O a chance to complete. Under load that read a stale
"Scanning Today..." frame. Scope fake timers to just what the 60s
auto-refresh interval needs, leave setImmediate/Date real, and wait on a
real wall-clock deadline instead of a fixed hop count.
2026-08-18 02:10:24 -07:00
Matthew Kelch
a5a0ddaeac
feat(copilot): read per-request input/cache from session-store.db
The Copilot CLI and the GitHub Copilot desktop app both write
~/.copilot/session-store.db unconditionally; its assistant_usage_events
table holds one row per API request. Until now input/cache tokens for
these surfaces came only from the session.shutdown rollups in
events.jsonl, which are written only on clean shutdown (a crash loses
the whole leg's input/cache accounting) and lump each session leg into
one per-model total. The rollup also RESETS its counters at in-session
compaction (traced on a clean single-process 107-request session whose
sole rollup covered exactly its five post-compaction requests), so even
cleanly-closed long sessions were truncated; on a long-history machine
the store recovered ~35% of real Copilot spend lost to crashes and
compaction resets. The DB rows are per-request, crash-proof, and carry
real timestamps.

The store's input_tokens is cache-INCLUSIVE (input + cache_read +
cache_write), the same convention as the shutdown rollups — verified
against each row's token_details_json and by reconciling per-session
sums against the CLI's own footers and rollups across two machines
(1,380+ rows, 8 models, CLI 1.0.70–1.0.79, schema_version 6): every
divergence was a rollup gap. Emitted calls mirror the shutdown-call
contract: input/cache/reasoning only, output 0 — per-turn output stays
owned by the events.jsonl assistant.message calls.

Rollup-vs-store precedence is RECONCILED at serve time, per
(session, model), and only there. Both representations always parse and
cache; parseProviderSources aggregates the cached calls and, wherever
store rows exist for a (session, model), drops the rollup calls and
serves the rows plus per-leg RESIDUAL calls: each rollup leg subtracts
only the rows in its own interval — rows commit strictly before their
leg's shutdown line, so a leg at time T covers exactly the rows in
(previous leg's T, T] — and any remainder (per token component, floored
at zero) serves once at that leg's own timestamp. A store missing
requests a leg covered — adopted mid-session, rows pruned before ever
being read — therefore still serves that tail exactly once ON THAT
LEG'S DAY, a crash-tail row the rollup never saw can never cancel it,
and a complete store serves pure per-request granularity with every
residual retired to zero. The decision reads only cached contents, never discovery:
deleting or resetting the store changes nothing served, so finalized
daily history can never flip on an absence epoch; cached rows of a
deleted store remain the record until the 90-day orphan age-out (which
exempts still-discovered paths). The serve set is the one coherent
snapshot — nothing a writer does between discovery and a parse can
change what one pass sees — and read-time precedence heals persisted
duplication (stale epochs, runtimes without node:sqlite, restored
files) instead of preserving it, following the buildDurablePeriod
pattern.

Store rows and rollups carry supplementary accounting weight. A rollup
(or its residual) is aggregate accounting, never a request: zero
api-call/model-call/turn weight, tokens and cost fully retained. A
store row is one real request, but when it pairs with a served per-turn
call it is supplementary too; rows pair with same-model per-turn calls
by timestamp adjacency (monotone matching, tight 2-minute window — the
two are written at the same completion moment, and a wide window would
let a crash-only row pair against a neighbor whose own row is missing),
computed once over the FULL serve set so a date-range boundary that
separates a row from its call cannot double the request across adjacent
day queries. Only the unpaired rows — store-only requests, exactly
where crash-lost requests sit — count. Supplementary-only turns fold
into the nearest behavioral turn within 30 minutes; with no behavioral
turn to fold into they stay separate weightless turns, each on its own
day, with apiCalls 0 — and the session emission gate admits
usage-bearing zero-call sessions. The weight
propagates into the daily cache: aggregateProjectsIntoDays applies the
same rule to every calls counter and category-turn count it seals, so
v19 history and live summaries can never disagree about what was a
request.

A changed source whose read defers on the busy shape (locked, EACCES,
corrupt mid-replace — discovery still emits the source; only true
absence or a schema mismatch reads as absent) now marks session
hydration incomplete, so the daily backfill holds its watermark instead
of finalizing a day the deferred rows never reached; an unchanged
unreadable store defers nothing. The verdict travels with its result —
the 180s memo and the serve burst-reuse restore the hydration verdict
their cached data was parsed under, so a memoized partial parse cannot
inherit a later parse's complete — and a discovered source whose
FINGERPRINT cannot be read (EACCES on a present file) defers instead of
silently skipping, while a genuinely deleted file stays a silent skip. Copilot reasoning tokens are no longer
double-billed at the report layer: they are a subset of the output the
per-turn calls already price, and copilot joins claude in the
reasoning-inside-output case of the query-time cost recompute.

Store dedup keys are content-discriminated —
copilot-store:<sid>:<rowId>:<fnv1a64(created_at|tokens|model)> —
because AUTOINCREMENT prevents id reuse only within one database
lifetime: a same-path DB reset reusing row ids now mints new keys
instead of the durable union swallowing the new usage, while a
byte-identical re-insert still collapses (64-bit: 32-bit FNV
collisions between plausible token tuples are constructible). Every
call of a session serves under one project label resolved at serve
time — the session-state-derived label when the serve set knows it,
else the store rows' own — so neither rows cached before events.jsonl
existed nor an events.jsonl orphaned by a session-state prune can
split the session across two grouping keys.

CODEBURN_COPILOT_SESSION_STORE_DB is read but deliberately NOT
fingerprinted, per the #927 ruling (any copilot fingerprint change
drops cached entries whose path still exists, destroying pruned history
only the cache holds); the read is allowlisted in the #927 guard, and
serve-time reconciliation makes repointing safe without a fingerprint —
the new store's rows parse on sight and the old path's entries persist
as durable orphans. The copilot parse version appends session-store-v2
and the daily cache bumps v17 → v19: per-day attribution, call counts
and costs all change against pre-store builds. 19, not 18: an earlier
pushed head of this PR already claimed v18 under different accounting,
and the carry-forward would adopt those days as finalized without
re-deriving them.

Verified by A/B on snapshots of two real stores, a live SIGKILL crash
test (row present, no rollup, tokens recovered exactly), live resumes
whose warm-cache deltas matched new rows to the token, upgrade-healing
at 4,800-session scale, and serve-level regressions pinning every
maintainer finding from six review rounds: the rows-then-shutdown race,
stale-cache healing, age-out exemption, absence-epoch identity,
progressive row landing with residual retirement, behavioral weight
across all four pinned scenarios, the hydration fence, project
unification in both directions, the same-path reset, mixed
coverage (crash tail vs covered-leg gap), multi-leg residual day
attribution, range-invariant pairing, memo-scoped hydration verdicts,
and the fingerprint-failure fence.
2026-08-17 17:52:01 -04:00
Aditya Vikram Singh
02ddc3c413 Complete unpriced model guidance 2026-08-14 04:05:42 +05:30
Aditya Vikram Singh
1ea3d68e71 fix(optimize): label connector actions clearly 2026-08-12 21:51:57 +05:30
ihearttokyo
74e69ba2fd
Stabilize TUI refresh, scrolling, responsive layout, and dashboard data density (#863)
Improve dashboard refresh coordination, scrolling, responsive layout, and data-density behavior, including the Windows-safe resize correction validated on the final head.
2026-08-09 03:39:19 +03:00
AgentSeal
3212b865f7 Merge branch 'main' into fix/post-0918-polish
Resolves tests/dashboard.test.ts (union of both new describe blocks and imports).
2026-08-03 23:08:13 +02:00
Aditya Vikram Singh
47981a50ce fix(dashboard): label the day-count denominator, explain durable carry-forward
Issue #767 item 3 reports two different active-day counts on one dashboard
screen: the Daily Activity panel shows "of 37" (from a bounded live scan of
surviving session files) while the same period's headline can reflect more
days via the durable daily cache, which also counts days whose session files
have since expired. Each count is correct for what it measures - the panel
intentionally scans a fixed six-month window independent of the selected
period tab so scrolling always works, while the headline is scoped to the
active period tab.

Re-deriving the panel's count from the durable series was considered and
rejected: it would need a second buildDurablePeriod call scoped to six months
independent of the period tab, which is a behavior and perf change, not
polish. Kept the count unchanged and instead labelled the denominator -
dailyActivityFooter (src/dashboard.tsx) now renders "of N days scanned"
instead of a bare "of N", so the panel reads as "here's what the live scan
covered" rather than as a contradiction of the headline. Checked the
Optimize view's similar "Showing X-Y of Z" footer (line ~794): it counts
findings, not days, and isn't part of this ambiguity.

Also fixes an adjacent gap found while investigating: overview.ts's
non-interactive report already had a footnote for durable-cache carry-forward
("includes $X preserved from expired session logs") when carriedCostUSD > 0;
the interactive dashboard's Overview panel had no equivalent, so a headline
that included carried-forward cost had no explanation anywhere on screen.
Extracted the wording as carriedCostNote (format.ts) and surfaced
carriedCostUSD through DurableOverview so the TUI shows the same footnote.
This is separate from the active-day-count fix above - it explains cost
carry-forward, not day counts.

Fixes getagentseal/codeburn#767 (item 3).
2026-07-29 23:02:05 +05:30
iamtoruk
fad846622f fix(codex): restore discovery fast path, duration precedence, and dashboard width
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.
2026-07-27 11:36:07 -07:00
ihearttokyo
92b2b5ef39
fix(dashboard): Bound Daily Activity scans (#727)
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.
2026-07-20 05:21:35 -07:00
ihearttokyo
ccd8764e5b
feat(dashboard): make Daily Activity history directly scrollable (#672)
Some checks are pending
CI / semgrep (push) Waiting to run
* feat(dashboard): add inline daily history paging

* refactor(dashboard): make daily activity directly scrollable

* fix(dashboard): keep the empty state for users with no history at all

Scrollable mode kept the dashboard shell up whenever full history exists,
but a truly-new user with zero data anywhere saw a zeroed skeleton instead
of the No usage data message. Extract the decision into showEmptyState so
the boundary is test-pinned: empty period with history renders the shell,
empty everything renders the empty state, and the loading pass never
flashes it.

---------

Co-authored-by: AgentSeal <hello@agentseal.org>
2026-07-16 04:10:08 -07:00
Resham Joshi
3b71650f24
Fix mangled project paths in dashboard (#320)
* 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>
2026-05-11 22:02:38 -07:00
voidborne-d
c16b21ec50 fix(classifier): surface skill name as subCategory for general turns (#203)
Turns whose only assistant tool is `Skill` collapse to category `general`
because `classifyByToolPattern` returns `'general'` and `refineByKeywords`
only operates on `coding`/`exploration`. In environments that lean on Claude
Code skills, the per-activity dashboard column flattens — every `/init`,
`/review`, `/security-review`, `/claude-api`, plus user-defined skills, all
land in `general` with no signal about which workflow ran.

Implements Option A from the issue:

- `ParsedApiCall.skills: string[]` populated in the Anthropic-path parser
  via a new `extractSkillNames` helper that reads `input.skill || input.name`
  from each `Skill` ToolUseBlock (mirrors `detectGhostSkills` extraction at
  optimize.ts:765 so the two stay in sync).
- `ClassifiedTurn.subCategory?: string` set to the first skill name when the
  resolved category is `general` AND any skill identifier was extracted.
  Top-level category stays `general` — existing aggregations, exports, and
  category-keyed code paths unchanged.
- `SessionSummary.skillBreakdown: Record<string, {turns,costUSD,editTurns,
  oneShotTurns}>` populated in the same per-turn loop that builds
  `categoryBreakdown`. Provider sessions (Codex/Cursor/etc.) keep `skills:
  []` — they don't expose the Skill tool surface today.
- Dashboard `ActivityBreakdown` renders top-N skill sub-rows beneath the
  `general` row when present (indented `/skill-name`, dimmed). Other
  categories render exactly as before; if no skills were invoked, the panel
  is byte-identical to current output.

Existing 419 tests still pass. New `tests/classifier.test.ts` adds 8 cases:
single skill via `input.skill`, single via `input.name`, first-wins for
multi-skill turns, aggregation across multiple assistant calls in one turn,
no-name fallback (`subCategory` stays undefined), `Skill+Edit` promoting to
`coding` and dropping subCategory, non-Skill general turns, and a legacy
ParsedApiCall shape with `skills` field absent (forward-compat). Pre-fix
verification by stashing the source change reproduces 4/8 failures with the
exact "expected 'init', received undefined" diff; restoring → 8/8 pass.

Closes #203.

🤖 AI assistance disclosure: assistant-scaffolded by Claude (Opus 4.7);
author of record reviewed every line, ran the full vitest suite locally
(`npm test` → 32 files / 427 tests pass), `npx tsc --noEmit` clean, and
`npm run build` produces a clean ESM bundle.
2026-05-04 06:26:45 +08:00
Ninym
0461193819 fix: handle empty firstTimestamp in TopSessions, add dashboard tests
- TopSessions: show '----------' placeholder when session.firstTimestamp
  is empty (Copilot provider yields '' when timestamp is missing)
- DashboardContent: add comment explaining undefined days tri-state
- tests/dashboard.test.ts: cover top-5 selection (fewer than 5 sessions,
  tied costs, descending sort) and avg/s returning '-' for zero sessions

Authored-by: AgentSeal <hello@agentseal.org>
2026-04-16 15:48:24 +02:00