Commit graph

483 commits

Author SHA1 Message Date
ozymandiashh
eece4cf005 fix(codex): validate rollouts structurally instead of by originator
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.
2026-08-04 00:15:24 +03:00
Resham Joshi
18e5011fc3
Merge pull request #860 from avs-io/fix/post-0918-polish
post-0.9.18 polish: compare flags in the TUI, context id prefix, active-days label
2026-08-03 14:11:05 -07: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
ozymandiashh
43410e88a7 fix(cline): scan all VS Code variants for task storage
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.
2026-08-04 00:01:57 +03:00
AgentSeal
80f5ed6cf7 Merge branch 'main' into fix/opencode-session-model 2026-08-03 22:56:49 +02:00
AgentSeal
dbdbf466a0 fix(aggregator): attribute a straddling turn's category to today on the all-provider view
buildDurablePeriod derived the today slice of the multi-day, all-provider
headline from the unsliced whole-range parse, so a turn spanning local midnight
kept its category and turn count anchored on its yesterday start. The per-call
cost and calls bucketed onto today correctly, but By Activity and the JSON
daily turn count lost the post-midnight half — categories summed to only the
pre-midnight cost while the headline, By Model and By Project were right.

Slice the today parse with filterProjectsByDays first, which re-anchors the
straddling turn to its surviving today calls, so today's category cost lands on
today. Category cost is the sum of the slice's own calls, so day-N + day-N+1
still equals the whole-range total (no over-count); the per-day turn-count
split matches the cache side and the documented per-day semantics.

Adds a regression test in the straddling-turn conservation suite
(mutation-checked: fails on the pre-fix code). Also fills in the CHANGELOG
Unreleased entries for the batch (#853, #856, #872, #846/#859, #866/#867, #833).
2026-08-03 22:46:54 +02:00
ozymandiashh
eb90b29422 fix(menubar): retry transient release-asset download failures
`codeburn menubar` aborted the install on the first bad response from
GitHub release-asset delivery. A transient HTTP 500 on the checksum
fetch (issue #876) killed an otherwise healthy install even though the
asset was published correctly and the next request succeeded.

Retry the zip and checksum downloads up to 3 times with a short
exponential backoff (0.5s, 1s) on 5xx responses and network-level
errors. 4xx is never retried: 404/410 still falls through to the
release-API discovery path unchanged, and a 403/429 rate limit cannot
clear inside the backoff window so it surfaces immediately with its
retry-after hint. The checksum comparison stays outside the retry loop
so a genuine digest mismatch still aborts on the first look.

Thrown errors now name the requested URL so a failure is actionable.
Retry parameters and the fetch/sleep/log seams are injectable, matching
the options pattern in src/sync/push.ts and src/cache-refresh-lock.ts.
2026-08-03 23:08:59 +03:00
AgentSeal
62f6eb27c7 Merge branch 'main' into fix/omp-title-slot-discovery
# Conflicts:
#	src/providers/pi.ts
2026-08-03 21:59:19 +02:00
AgentSeal
2c65764c0b Merge branch 'main' into fix/omp-session-discovery 2026-08-03 21:44:19 +02:00
Resham Joshi
458ce1b2d7
Merge pull request #848 from Enclavet/feat/sync-yield-attribution
feat(sync): opt-in git attribution spans on `sync push --attribution`
2026-08-03 12:05:45 -07:00
Resham Joshi
201b289ada
Merge pull request #872 from ozymandiashh/fix/provider-filter-claude-orphan-leak
fix(parser): stop --provider filters from re-surfacing cached claude sessions
2026-08-03 11:54:06 -07:00
chengchuan.zhou
15962202bd test(project-filter): make the durable-headline filter tests time-of-day proof
The tests added in #864 seeded today's live session at a fixed wall-clock
hour (12:00 local). The periods they build end at `new Date()`, and the
suite runs under TZ=UTC, so for any run before 12:00 UTC that timestamp
is in the FUTURE and the range filter correctly drops it. The live half
of the cache/live union then contributes nothing, and the one assertion
that needs a non-zero headline — the unattributed-cost footnote — fell
into renderOverview's "No usage found" early return and went red. Half
of every day was a failing window; a769b50 fixed the start-of-month
flake but this one survived it.

Verified by bisecting the fixture on the unpatched test: moving the
seeded hour from 12:00 to 01:00 (past, at a 03:38 UTC run) turns the
same 12 tests green, so the timestamp's position relative to `now` is
the whole cause.

- Seed the session a few minutes BEFORE now, clamped to today's
  midnight, so it is always both inside today and already in the past.
- Stop the footnote test depending on the live parse at all: seed a
  second, attributable cached day so the headline is non-zero from the
  cache alone. The test now exercises the footnote instead of the
  fixture's timing.
2026-08-03 11:42:32 +08:00
ozymandiashh
6b903a7777 fix(parser): stop --provider filters from re-surfacing cached claude sessions
Claude is scanned via scanProjectDirs instead of parseProviderSources, and
that call had no provider-filter guard. On a --provider <other> run
discoverAllSessions correctly returns no claude sources, so claudeDirs is
empty, but scanProjectDirs still ran: its orphan pass reads the whole cached
claude section and treats every file as no-longer-discovered, re-injecting
PR-bearing entries (and in read-only mode every cached entry) into the result.

The headline stayed correct because it comes from the provider-sliced daily
cache, so only the live-parse panels were wrong. By Model then listed
Anthropic models under --provider cursor while the total showed cursor alone.

Guard the scan with claudeInScope, mirroring the guard the durable-orphan
loop already applies. Deliberately not a claudeDirs.length check: when claude
is in scope but every transcript has been pruned, the orphan pass is what
keeps PR-attributed spend from vanishing.
2026-08-02 19:45:16 +03:00
Andrew Lee
50c8251719 fix(sync): close credential-leak paths; session retraction; span/key/CLI hardening
Review rounds 2-3 + self-review on --attribution:

Credential egress (round 2):
- normalizeRemoteUrl: scp userinfo expressed as an optional regex group
  let backtracking re-parse a credential prefix as host:path
  (x-access-token:ghp_...@host/repo -> token in git.repo). Userinfo is
  now split off at the first @ BEFORE any host matching.
- Positive validation (allow-list) as the final gate on EVERY branch:
  host must be hostname-shaped, every path segment repo-shaped, total
  identity <= 200 chars. Kills transport-helper remotes (ext:: leaks
  local SSH key paths, codecommit:: leaks AWS profile names), residual
  @, spaces/colons, and unbounded strings.
- sanitizePrLinks: links are rebuilt from origin + pathname — userinfo,
  query strings, and fragments are dropped instead of passed through;
  collapsed duplicates dedupe.

Attribution correctness (round 3 + self-review):
- Double-count fix with precise retraction semantics: when a commit
  migrates to a later-parsed tighter-window session, the loser re-emits
  git.commit_count=0. Empty records are emitted ONLY on a true loss in
  THIS computation (lostCandidacy) — a commit that merely aged out of
  the --since range was lost to nobody, and retracting it would
  permanently zero a still-correct server-side count. The sync layer
  additionally requires a prior ledgered state for the session.
- Session dedup key includes project + both window timestamps, so
  ongoing sessions re-emit with corrected span times.
- Span end times clamped like the usage builder (never 0, never
  earlier than start + 1ms).
- CLI mirrors the usage path on attribution push failures instead of
  claiming success.
- Identity normalization: case-insensitive .git strip, doubled path
  slashes collapse.

AI-Origin: human
2026-08-02 12:56:59 +00:00
Andrew Lee
ccee28ae82 fix(sync): address attribution review — cwd-fallback egress, Windows paths, PR-link validation
Review findings on the --attribution PR:

- Privacy: sessions whose project path no longer resolves inherited the
  cwd-fallback repo identity, egressing whatever (possibly confidential)
  repo the user pushes from and falsely attributing its commits.
  buildRepoGroups now tracks per-session identity provenance; the
  attribution path excludes fallback sessions from commit attribution
  entirely (no repo, no commits, PR links only) — they also can no
  longer steal a commit from a genuine session's window.
- Privacy: Windows drive-letter paths (C:/..., C:\..., drive-relative)
  parsed as scp-like remotes, emitting local filesystem paths as repo
  identities. normalizeRemoteUrl rejects drive letters and
  single-character hosts (dotless intranet hosts still accepted).
- Hardening: PR links are shape-checked before sending (https,
  /org/repo/pull/N path, <=256 chars, max 20 per session) — upstream
  parsers only truthiness-check them.
- Safety valve: MAX_ATTRIBUTION_PER_PUSH (10k) caps a first
  --since all --attribution push; dry-run reports the cap.
- Tests: adversarial normalize corpus, cwd-fallback egress repro,
  commit-stealing prevention, PR-link sanitization, and CLI-level tests
  (mock IdP + collector): dry-run sends nothing to the traces endpoint,
  flag-off emits no attribution span names on the wire.
- Docs: reconciled the 'never sent' wording with reality (PR links ride
  even when repo is null; device_id/methodology/timestamps disclosed).
  CHANGELOG Unreleased entry added.

AI-Origin: human
2026-08-02 12:29:18 +00:00
Andrew Lee
1bf7206842 feat(sync): push git attribution spans with --attribution
Expose the yield session-to-commit correlation through codeburn sync so
backends can join AI usage to git activity without local git hooks.

- yield: export normalizeRemoteUrl (host/org/repo; credentials, ports,
  and .git stripped) and computeAttributionRecords, which reuses the
  exact repo-grouping + tightest-window attribution from computeYield
  (extracted into a shared buildRepoGroups) and joins in the normalized
  origin remote and session prLinks.
- otlp: two new span types sharing the session traceId —
  codeburn.session.attribution (git.repo, git.pr_links, git.commit_count)
  and codeburn.commit (git.sha, git.in_main, git.was_reverted). Resource
  attribute codeburn.attribution_methodology=timestamp-window marks the
  attribution as inferred.
- push: generic send core reused by usage and attribution batches. Dedup
  keys encode mutable state (inMain/wasReverted), so a state transition
  re-sends the updated fact while identical states dedupe via the
  existing sent-ledger.
- cli: opt-in --attribution flag on sync push (dry-run aware); commits
  in repos with no network remote are never sent.

AI-Origin: human
2026-08-02 12:28:13 +00:00
AgentSeal
3432a07bec Merge branch 'main' into fix/project-filter-durable-headline 2026-08-02 01:18:13 +02:00
AgentSeal
a769b5008a fix(daily-cache): keep prototype-named project keys; make filter tests date-deterministic
Two issues on top of the --project/--exclude durable-headline fix:

- sanitizeProjects dropped any project whose key is an Object.prototype member
  name (constructor, valueOf, __proto__, ...). A project key is a directory
  basename, so such a name is legitimate, and dropping it left the day's
  per-project split summing to less than the day cost — so the sliced,
  project-filtered headline silently lost that project's spend with no footnote.
  The keys are written via setOwn (defineProperty), so keeping them is
  pollution-safe; only the redundant `name in Object.prototype` guard is removed.
  Regression test added (mutation-checked: fails without the guard removed).

- The new project-filter tests seeded a carried day 10 days ago but ranged over
  the calendar month, so within the first 10 days of a month that day fell out
  of range and the tests went red. Replaced with a fixed 20-day window that
  always spans the seeded day.
2026-08-02 01:16:35 +02:00
Resham Joshi
b94d0ac67c
Merge pull request #853 from KENSHI601/fix/turn-range-per-call-filter
fix(parser): range-filter calls inside turns instead of dropping day-spanning turns
2026-08-01 15:37:24 -07:00
AgentSeal
6c411ad96d Merge branch 'main' into fix/turn-range-per-call-filter 2026-08-02 00:36:34 +02:00
AgentSeal
5a12dccc70 fix(parser): classify range-sliced turns from the whole turn on the provider path
The Codex/OTel/network provider path classified a date-sliced turn from only
its in-range calls (cachedTurnToClassified(slicedTurn)), while the Claude path
kept the full-turn classification. So a midnight-straddling turn was
categorized and edit-counted differently by provider — a Read at 23:59 plus an
Edit at 00:05, sliced to the 23:59 side, read as coding/hasEdits on Claude but
exploration/no-edit on Codex. This contradicted the documented intent that
category/hasEdits/retries are whole-exchange judgments, not per-call sums.

Classify the full turn, then trim its calls to the range (mirroring the Claude
path's classifiedTurnSlicedToRange). Cost/calls still come from the retained
calls, so conservation is unchanged; only the turn-level judgments are now
provider-independent.
2026-08-02 00:35:30 +02:00
Resham Joshi
39441de8b2
Merge pull request #856 from avs-io/fix/daily-cache-degraded-completeness
daily-cache: never finalize history against a degraded session parse
2026-08-01 14:10:17 -07:00
AgentSeal
1579eb0899 Merge branch 'main' into fix/daily-cache-degraded-completeness 2026-08-01 23:09:27 +02:00
AgentSeal
90dffcddc2 fix(daily-cache): trust a stamped watermark so an idle tail is not re-derived every launch
The watermark pull-back could not tell a legitimately-finalized idle tail
(recent days had no activity, so they are absent from the cache) from the
corrupt cache it heals (a degraded parse finalized past days it never read).
Both look like lastComputedDate > newest populated day, so an idle user
re-parsed the tail on every launch, escalating to a full re-derive under
sustained lock contention where before it did nothing.

A degraded parse can no longer set complete, so the corrupt state can only
come from pre-fix code. Stamp watermarkTrusted whenever a COMPLETE parse
finalizes, and pull the watermark back only for unstamped caches. Pre-fix
caches heal once, then are trusted; caches the fixed code writes are trusted
from the first finalize. The heal still recovers genuinely missing days.
2026-08-01 23:07:22 +02:00
Resham Joshi
807b1f422f
Merge pull request #855 from avs-io/fix/refresh-lock-malformed-recovery
Some checks are pending
CI / semgrep (push) Waiting to run
cache: recover a corrupt session-refresh lock instead of freezing ingestion
2026-08-01 13:05:39 -07:00
chengchuan.zhou
4bfcc020f6 fix(aggregator): apply --project/--exclude to the durable headline
The durable headline unions the carry-forward daily cache with today's
live parse. Cached days were sliced to the requested provider
(sliceDayToProvider) but never to the requested project, so a
--project/--exclude query counted every historical day WHOLE while the
detail panels — By Project / By Activity / By Model, all built from the
name-filtered live parse — left the filtered projects out. The Overview
total and the panels below it could not be reconciled; on a real
one-week corpus the headline ran $2,428.85 over the By Project sum,
which is exactly the excluded projects' spend.

Add sliceDayToProject, the project-level counterpart of
sliceDayToProvider, and apply it to the cache-sourced days. Cost, calls,
sessions and savings come out of the per-project day stats the cache has
carried since v15, so they stay exact and still include days whose
session files have expired. Provider slices carry their own project
split, so --provider on top of a project filter stays consistent.

Fields the cache has no per-project split for are handled explicitly
rather than reported as the filtered projects' own:

- tokens/models/categories come from the (project-filtered) live parse
  when a project filter is active, which is exact for every surviving
  session and matches the panels that read the same parse;
- days, or provider slices, carried from before v15 have no project
  split at all and cannot be attributed, so they contribute nothing and
  their cost is returned as unattributedCostUSD and footnoted by the
  terminal overview instead of being folded into a filtered total.
2026-07-31 19:32:02 +08:00
KENSHI601
7591e68851 fix(parser): make day-spanning turn slices conservation-correct
Extends the per-call range filter so multi-day periods stop losing
usage (review on #857):

- Re-anchor a sliced turn's timestamp to its first surviving call in
  parseProviderSources, so every turn slicer shares one split rule.
- scanProjectDirs (Claude Code path): slice per call instead of
  dropping the whole turn on its first assistant timestamp; category/
  subCategory/retries/hasEdits stay classified from the full turn.
- aggregateProjectsIntoDays: bucket cost/calls/tokens (and the model,
  project, provider-slice rollups built from them) under each call's
  own day; turn-level stats (categories, editTurns, oneShotTurns) stay
  turn-anchored. This is the conservation fix: cache (<= yesterday) +
  live (today) unions now sum to the whole range for straddling turns.
- buildJsonReport's dailyMap fallback follows the same per-call rule
  so the no-durable path can't diverge from durable.days.
- filterProjectsByDateRange (dashboard) and filterProjectsByDays
  (menubar/history) slice per call instead of dropping whole turns.

Adds the straddling-turn buildDurablePeriod case to the durable-totals
parity suite (day-N + day-N+1 == whole-range calls/cost/tokens,
verified to fail without the fix), covers the today view and the
surface filters, and makes the suite hermetic on machines with real
provider data.
2026-07-31 00:37:20 +08:00
Aditya Vikram Singh
d5144593f3 fix(cache): recover a corrupt session-refresh lock instead of freezing ingestion
observe() classified a stable unparseable session-refresh.lock body as the
terminal 'unavailable'. parseAllSessions routes that to a read-only parse, so a
zero-byte or truncated lock froze warm-cache ingestion permanently across every
later run while each command still exited successfully.

A corrupt body is now a recoverable observation carrying a real mtime, and is
recovered only through the UNMODIFIED staleness gate — tryTakeover and the age
check are byte-identical to main. sameObservation gains an explicit null/non-null
boundary and a sha1 of the raw bytes, because two corrupt bodies have no tokens
to compare and mtime granularity is coarse on some filesystems.

The heartbeat deliberately does NOT rewrite a body it cannot prove is its own.
An owner that cannot prove ownership ends its ownership: mtime stops advancing,
the publication fence refuses, and a successor recovers the lock one staleMs
later. Losing that parse is the price of never having two owners.
2026-07-30 13:06:25 +05:30
AVSR Pavan Kumar
77b41ce63b feat(act): measure realized savings for defer-* actions in act report
The defer-enable / defer-alwaysload / defer-threshold plan kinds (part 2
of the deferral-coverage work) applied and undid correctly but were
invisible to `act report` — it returned "not measurable: no baseline
captured at apply time", because report.ts never captured a baseline for
them or knew how to compute their realized delta.

Wire them in, mirroring the mcp-remove path since deferral has the same
effect (MCP tool-def schema leaves the upfront prefix):

- needsConfigBaseline + captureBaseline now cover the defer-* kinds.
  Servers are the named set for defer-alwaysload, or the observed MCP
  surface for defer-enable / defer-threshold (which re-enable deferral
  across everything). Per-session tokens use observed tool counts, or the
  5-tools x 400 fallback, exactly like mcp-remove.
- A new deferRow computes realized savings as per-session prefix tokens
  times the post-apply sessions where deferral actually became active —
  detected by the same deferred-tools-inventory signal the
  mcp-deferral-off detector uses. Sessions begun before the client
  restarted still run deferral-off and are excluded; if none benefited,
  the row reports "not yet in effect" with zero realized rather than
  claiming a saving that has not taken hold.

Records applied before this change (no baseline) keep the existing
"no baseline captured at apply time" note, so nothing regresses.

11 tests cover the measured, partial, not-yet-in-effect, no-sessions,
empty-baseline, and missing-baseline paths, plus baseline capture for
each kind. Verified live end to end: apply captures the baseline, and
act report reports realized savings scaled to the sessions that adopted
deferral.
2026-07-30 10:13:06 +05:30
Aditya Vikram Singh
213f4cbe0e fix(daily-cache): never finalize daily history off a degraded parse
A read-only session parse (served when the cache refresh lock times out or
is unavailable) reported itself as a complete hydration, so the daily
backfill published `complete: true` and advanced `lastComputedDate` to
yesterday over days the parse never covered. Because gapStart is
lastComputedDate + 1, those days were never looked at again — observed as a
cache marked complete at 2026-07-28 whose newest entry was 2026-07-25.

- parser: a read-only run reports a complete hydration only when nothing
  changed under the snapshot it served (a skipped or staled file makes it
  partial).
- daily-cache: only a complete parse may advance `lastComputedDate`, on both
  the gap and the full re-derive paths.
- daily-cache: a cache whose watermark outruns its newest populated day has
  its watermark pulled back to that day, so the ordinary gap parse re-derives
  the tail instead of trusting the marker.

Days are only ever added or re-derived, never dropped; the preservation bias
is unchanged and covered by test.
2026-07-29 17:37:44 -07: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
Aditya Vikram Singh
7a200915b4 fix(web): accept a session id prefix in /api/context/tree
`codeburn context <session>` accepts an 8-char session id prefix (findClaudeSession/
findCodexSession resolve it via startsWith), but /api/context/tree additionally
required the resolved ref's sessionId to equal the id passed in verbatim - so
the same prefix that works on the CLI 404s through the API.

findClaudeSession/findCodexSession are already the single source of truth for
prefix resolution used by the CLI; the API now trusts that resolution instead
of re-validating for an exact match afterwards, so both surfaces behave the
same way for a prefix.

Fixes getagentseal/codeburn#767 (item 2).
2026-07-29 22:58:10 +05:30
Aditya Vikram Singh
5ba787346b fix(compare): honor --model-a/--model-b in the TUI and resolve display names
The TUI comparison picker silently ignored --model-a/--model-b (only the
--format json path used them), and both paths required the canonical model
id verbatim with no way to pass the display name the picker itself shows
(e.g. "Opus 4.8" for claude-opus-4-8).

Adds findModelStat, a shared lookup that matches the canonical id first and
falls back to getShortModelName's existing canonical -> display mapping
(case-insensitive) rather than a new alias table. Both the JSON path and
renderCompare/CompareView now use it; renderCompare resolves --model-a/
--model-b up front and seeds CompareView's picked-models state so results
load immediately instead of showing the picker. Also validates "both or
neither" for the TUI (default) format, matching the JSON path.

Fixes getagentseal/codeburn#767 (item 1).
2026-07-29 22:58:05 +05:30
Aditya Vikram Singh
a4627a1a5a fix(providers): scan bounded leading lines for Pi/OMP session discovery
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
2026-07-29 22:43:01 +05:30
KENSHI601
8b83ded657 fix(parser): range-filter calls inside turns instead of dropping day-spanning turns
parseProviderSources keyed the dateRange check on a turn's first call
timestamp, so a long autonomous turn starting before midnight was
excluded from the next day's view entirely and every post-midnight
call in it was lost. Filter calls inside the turn instead and keep
the turn when any call falls in range.

Fixes #852
2026-07-29 02:08:10 +08:00
ihearttokyo
a056d22e78
Merge branch 'main' into agent/fix-daily-history-scan 2026-07-27 19:59:08 -04:00
iamtoruk
7fe432094d refactor(codex): buffer a task's calls until its timing is known
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.
2026-07-27 11:36:21 -07:00
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
Jan Brennenstuhl
36e5ef0a46
fix(omp): discover title-first session transcripts
Co-authored-by: openai/gpt-5.6-terra <noreply@openai.com>
2026-07-27 19:58:50 +02:00
iamtoruk
d4f85ced71 chore(sync): HTML-escape callback page inputs 2026-07-26 07:05:44 -07:00
Resham Joshi
264e8e1b50
Merge pull request #800 from getagentseal/fix/611-desktop-msix-sessions
fix(claude): discover Claude Desktop/Cowork sessions in Windows MSIX installs
2026-07-26 06:47:18 -07:00
ihearttokyo
a6bf81f756 feat(codex): add tool-excluded active throughput metrics 2026-07-24 22:53:43 -04:00
EuanTop
dd6147e529 fix(opencode): read session fallback model from real schema 2026-07-24 13:03:22 +08:00
ozymandiashh
d92b9fea43 fix(claude): discover Claude Desktop/Cowork sessions in Windows MSIX installs
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
2026-07-24 00:30:30 +03:00
reviewer
be5c0c00aa kimicode: discover desktop-runtime sessions and fix menubar visibility
- 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
2026-07-23 21:31:25 +02:00
Resham Joshi
6e3c57a9ff
feat(cli): add interactive sessions browser (#793)
Some checks failed
CI / semgrep (push) Has been cancelled
2026-07-21 13:03:47 +02:00
Resham Joshi
2eb4718122
sessions: correlate cross-provider work with pull requests (#792) 2026-07-21 11:50:11 +02:00
Resham Joshi
8e1caae33a
sessions: fold subagent runs into PR attribution (#791)
Some checks are pending
CI / semgrep (push) Waiting to run
* sessions: fold subagent runs into PR attribution

Sidechain (subagent) session cost never reached the by-PR view, so a
session orchestrated on one model with subagent lanes on another showed
only the parent model on every PR row. Fold each sidechain's cost, calls,
models, and categories into the parent turn that spawned it, so it
inherits that turn's PR set under the existing turn-level state machine.

Linkage, in priority order: the spawn result's toolUseResult.agentId
pairs the child's agent id with the Agent/Task tool_use id that launched
it (recorded per turn), which is the true launch point and wins even when
the child's first activity landed during a later turn; else the child's
first-activity timestamp is bucketed into the containing turn span; else
the child folds into the parent's unattributed spend. Children of parents
that referenced no PR, and orphans whose parent is absent from the scan,
contribute nothing, unchanged.

Cache v6 to v7 (neither shipped, so one combined bump from v5): per-turn
spawnToolUseIds, per-file parentSessionId and agentSpawnLinks; the
validator and the append/compact paths thread them like prRefs. By-PR
footers now count parents plus folded subagent runs and the payload gains
an additive subagentSessions field. distinctCost now includes folded
subagent spend, documented in the payload comment.

* sessions: address adversarial review of subagent PR attribution

Rework child attribution to resolve each subagent to the PR its launching
turn was working on, using the parent's UNFILTERED turn data, and enforce
that every dollar is counted exactly once.

- Mutual exclusion: a child that referenced its own PR attributes
  standalone and is never folded; a child with no links is folded only.
  Fixes a double-charge where a self-linking child was both folded and
  self-attributed.
- Recursion: a fold aggregates a child plus its non-self-linking
  descendants (depth-first, cycle-guarded), so grandchildren spawned by
  subagents reach the PR report.
- Global linkage: the subagent index keys by parentSessionId alone
  (UUIDs are globally unique), so a child whose worktree resolves to a
  different project still links.
- Date-range correctness: spawn-to-PR sets are built at assembly from the
  full turn list, so a spawn in a pre-range turn attributes to the right
  PR; a PR-linked parent whose own turns fall out of range is kept as a
  0-cost fold anchor so its in-range child is not lost.
- Timestamp fallback compares epoch ms (mixed UTC offsets order right) and
  is end-bounded: a child active after the parent's last turn is unlinked
  (contributes nothing), matching orphan semantics.
- Cache adoption tries the newest prior versioned file (v6 then v5) so the
  preceding build's expired-PR history survives the v7 bump; an invariant
  note requires the list to cover every version that can exist on disk.
- Spawn-result pairing matches the tool_result block that carries the
  agentId, not the first block, when a record batches several results.
- resolveSubagentAttribution is computed once and shared by aggregateByPr
  and prLinkedTotals.

subagentSessions now counts folded subtrees (children plus descendants).
Verified on real data: attributed + unattributed reconciles to cost, and
parent-only cost plus folded-children cost equals the folded total to the
cent (no double-count).

* sessions: round-2 hardening of subagent PR attribution

Address a second adversarial review of the new machinery.

- ID collision: parents and a child's parent reference are keyed by
  provider + sessionId, not bare sessionId. When two distinct parents
  still share a key (true duplicate/imported data), the child folds into
  NEITHER (deterministic skip, stays standalone): correctness over
  coverage.
- Recursion dedup is global: one claimed-set spans all of a parent's
  direct children, so a descendant reachable through two paths (a diamond
  or duplicate id) folds exactly once and a parent-link cycle terminates.
- Cache adoption migrates every prior version oldest-to-newest and MERGES
  per source path (newer wins per entry), so a sparse or partial newer
  file no longer masks older-only expired-PR orphans.
- Fold anchors (0-cost PR-linked parents kept only for attribution) live
  in a new ProjectSummary.subagentAnchors, never in `sessions`, so they no
  longer contaminate session counts, averages, or any per-session report.
  Folded PR rows take their date span from the contributing child activity
  rather than the anchor's empty timestamps.
- One-pass buildPrAttribution computes rows and totals together; the
  payload builder and CLI call it once. Drops the identity-keyed
  memoization, which could return stale folds if the array was mutated.
- Ambiguous multi-block spawn-result pairing leaves the spawn link unset
  on purpose; the child then folds via the timestamp fallback rather than
  pairing with the wrong id or disappearing.

Every fix is mutation-verified. A fresh real-data drive re-proves the
no-double-count identity to the cent (parent-only cost plus folded cost
equals the folded total) and that the PR rows sum to attributedCost.

* sessions: round-3 hardening of subagent PR attribution

Third adversarial review pass.

- Ambiguity counts ALL candidate parents (and anchors) sharing a
  provider+sessionId key, not just PR-bearing ones, and uses a
  per-record fingerprint: a key carried by more than one DISTINCT record
  folds its child/subtree into NEITHER (identical duplicates still fold
  once). This unifies the parent-collision and duplicate-descendant rules
  and is deterministic across input order.
- Project-rebuilding filters (by day, by date range, by config source)
  now carry subagentAnchors through, and a date filter CONVERTS a spawn
  parent whose in-range turns are all filtered out into an anchor so a
  surviving in-range child still folds. Rebuilt sessions also keep their
  PR + subagent-linkage metadata (prLinks, parentSessionId, spawnPrSets,
  ...), which buildSessionSummary otherwise drops, so by-PR and folding
  work on a filtered slice (menubar/dashboard flow).
- Fold anchors leave ProjectSummary.sessions entirely and folded PR rows
  take their span from the child, so 0-cost anchors never touch session
  counts or averages.
- Ambiguous spawn pairing (parent named the agent but its exact launching
  tool_use could not be paired) is recorded per parent; a late child of
  such a pairing folds to the parent's last turn within a 30 minute grace
  window, else stays unlinked. A truly-absent pairing gets no grace.
- Row session key is NUL-delimited and provider-prefixed, so a project
  name or session id containing a space no longer collides and
  undercounts distinct sessions.

Every fix mutation-verified. A fresh real-data drive re-proves the
no-double-count identity to the cent, and a day-filtered drive proves the
filter fix end to end (anchors created, subagents fold, identity holds).

* sessions: round-4 hardening of subagent PR attribution

Fourth adversarial review pass.

- sessionFingerprint now covers the COMPLETE linkage-relevant payload,
  not just headline stats: a canonical (sorted-key) serialization of
  agentSpawnLinks, spawnPrSets, prRefsAtRangeStart, ambiguousSpawnAgentIds,
  parent/agent identity, and the per-turn prRefs timeline. Two records
  that share an id and headline stats but map the child to different
  spawns/PRs now fingerprint DISTINCT, so the ambiguity rule fires and
  they fold into neither, deterministically rather than order-dependent
  first-wins.
- A date/day filter recomputes prRefsAtRangeStart at the new slice
  boundary by replaying the original full turn sequence, instead of
  copying the wide range's value. A PR switch between the wide start and
  the slice start (July 1 A, July 10 B, slice July 20) now carries B, not
  a stale A; a turn exactly on the boundary stays in-slice and applies its
  own refs. The recompute selects by timestamp, so it is order-independent.
  Non-contiguous day selections are documented as treated contiguous from
  the earliest selected day (a single session-level seed cannot represent
  multiple segments; the menubar selection is a single day or a run).
- The anchor-carry path drops an anchor that duplicates a surviving
  session id, so malformed merged input cannot double-count.

Also: rebuilt filtered sessions were losing their PR/subagent-linkage
metadata (a child its parentSessionId, a parent its prLinks), which
carryLinkageFields now restores, so by-PR and folding work on any filtered
slice.

Every fix mutation-verified. A fresh real-data drive re-proves the
no-double-count identity and reconciliation to the cent for both a
lifetime scan and a day-filtered slice (anchors created, subagents fold
through the filter).

* sessions: round-5 hardening of subagent PR attribution

Fifth adversarial review pass; closed-form fixes.

- sessionFingerprint serializes the COMPLETE fold-determining state via a
  real recursive canonical encoder: session-level linkage AND, per turn in
  sequence, timestamp, prRefs, cost, calls, savings, and per-model cost.
  Object keys are sorted recursively and set-semantic arrays (PR-ref lists,
  ambiguous ids, spawnPrSets values) are sorted, while the turn list keeps
  order; the structure is emitted through JSON.stringify (no delimiter
  concatenation). Two same-id parents that differ only in a turn timestamp
  now fingerprint DISTINCT (fold neither), and records differing only in
  set-array order fingerprint EQUAL (no false ambiguity).
- recomputeRangeStartPrRefs breaks an exact-same-millisecond tie
  deterministically by the lexicographically-last sorted-ref key, so the
  recomputed seed is stable regardless of turn order.
- A day filter seeds EACH selected day's first ref-less turn by replaying
  the original full turn sequence up to that day's start (per-day seeding),
  so a PR switch on an UNSELECTED day between two selected days carries to
  the later day. Contiguous and non-contiguous selections are both correct.
- The anchor dedupe drops an anchor only when a surviving session shares
  the full provider-aware, fingerprint-qualified identity (a proven
  duplicate): a different-provider or different-record same-id session no
  longer wrongly drops the anchor.

Also fixed a double-count the fingerprint test exposed: two duplicate
parent sessions share a key and the SAME resolved children, so folding is
now done once per parent key.

Every fix mutation-verified. Fresh real-data drives (lifetime, single-day,
and a NON-CONTIGUOUS day selection) re-prove no-double-count and
reconciliation to the cent.

---------

Co-authored-by: reviewer <review@local>
2026-07-20 20:47:23 -07:00
Resham Joshi
45e93129f2
sessions: attribute PR spend per turn instead of per session (#790)
* sessions: attribute PR spend per turn instead of per session

The by-PR surfaces attributed a session's full cost to every PR it
referenced, so one orchestration session that touched many PRs rendered
identical full-session rows. Capture per-turn PR references during
transcript parsing and attribute each turn's cost to the PR set active at
that turn (split evenly across a multi-PR merge-sweep turn), carrying the
most recent PR set forward across turns that reference nothing. Turns
before the first reference form an unattributed bucket.

A session whose transcript expired before per-turn capture keeps its
session-level prLinks but has no per-turn refs; it falls back to an even
whole-session split, and any row carrying such a portion is flagged
approximate.

Rows are now summable: the CLI and app footers report the attributed sum
plus the unattributed remainder instead of a distinct-session total.
Bumps the session cache version so surviving transcripts re-parse and
populate the new per-turn field. Daily-cache versioning is untouched.

* sessions: harden PR attribution and add models + category breakdown

Addresses the review findings on the per-turn PR attribution and adds the
model and task-category surfaces.

Correctness:
- Cache migration: the 5 -> 6 session-cache bump now adopts the prior v5
  file's expired-source PR entries instead of abandoning them, and the claude
  scan preserves and surfaces PR-bearing orphans, so a session whose transcript
  was deleted still appears as a legacy even-split instead of vanishing. The
  daily cache is untouched.
- Date-range carry: the parser captures the PR set active at the start of the
  in-range turn slice and seeds the state machine with it, so a PR referenced
  before the window still owns its later, in-range, ref-less turns instead of
  the session falling back to a whole-session approx split.
- Calls are split across a multi-PR turn by largest-remainder, keeping per-PR
  counts whole (a 1-call, 2-PR turn no longer renders as 2 calls).
- The CLI and app footers reconcile to the rounded row values actually shown.
- Distinct sessions are keyed by project + sessionId, not sessionId alone.
- The app tolerates an older by-reference payload (no attributedCost): it keeps
  the old non-summable footer and never renders NaN.

Features:
- Each PR contribution records the models of its calls and the turn's task
  category (split by the same share on a multi-PR turn); legacy even-split rows
  carry the model union but no category breakdown.
- Payload rows gain models (short names, cost-desc) and categories (label + cost,
  cost-desc, omitted when empty); the payload gains otherPrCount/otherPrCost for
  the PRs beyond the sent top 20.
- CLI --by-pr gains a Models column. The desktop table is now full-width with a
  Models column and click-to-expand rows showing a per-category cost breakdown
  with proportional bars, keyboard accessible, with an "Other (N more PRs)" row
  when capped.

Tests: state-machine seed/models/categories/largest-remainder/dedup, the prRefs
round-trip through the real incremental-append path (continuation + straddle),
v5 adoption of an expired PR session, payload round-trip, and the desktop
expansion/models/old-payload cases.

* sessions: reconcile mixed PR rows, harden v5 adoption, bound models

Round-2 review follow-ups.

- Mixed legacy/live category breakdown: a PR row that combines an expired
  legacy contribution (even-split, no turn data) with a live per-turn
  contribution now emits an explicit "Legacy estimate (no per-turn detail)"
  category carrying the legacy share, so the expansion reconciles to the row
  cost instead of silently dropping it. A legacy-only row still shows no
  breakdown.
- v5 adoption is now per cached file: one malformed entry is skipped instead
  of rejecting the whole v5 cache and dropping every valid expired PR session.
- The desktop "Other (N more PRs)" line moved to the table footer as a muted,
  separated summary rather than a sorted row.
- Row expansion resets when the PR set changes (period/provider/data), so a
  stale expansion cannot linger on a row that is gone.
- Payload models per row are capped to the top 4 by attributed cost, with a
  name-ascending tie-break; categories get the same stable tie-break.

Tests: mixed live+legacy reconciliation, legacy-only omission, model cap and
tie-break, corrupt-plus-valid v5 adoption, and the desktop expansion-reset.

* session-cache: validate optional agentType and failed during v5 adoption; app: reset PR expansion on period switch

A v5 entry that was valid except for a malformed optional field (agentType,
failed) passed per-file validation and flowed downstream as a non-string.
The expansion reset keyed only on the row URL set, so a period switch that
returned the same PRs kept a stale expansion open over changed numbers.

---------

Co-authored-by: reviewer <review@local>
2026-07-20 20:44:29 -07:00
Resham Joshi
a9d4471d17
payload: spend by pull request and by branch; desktop Pull requests tab (#788)
Some checks are pending
CI / semgrep (push) Waiting to run
Part 1 (src): expose two add-only, optional aggregations on the menubar
payload's `current`, computed on the unscoped all-provider path from the
surviving-session parse (carried history cannot contribute, as expected).

- current.pullRequests: aggregateByPr rows (top 20 by cost) plus the
  multi-link-safe distinctCost/distinctSessions. Rows are by-reference, so
  they are never summed.
- current.byBranch: new aggregateByBranch, per-branch spend (top 15 by cost)
  that carries each session's last-seen git branch forward across its turns.
  The cache stores a turn's branch only when it changes, so the parser now
  resolves the branch at reconstruction (before the date slice) and records
  SessionSummary.everHadBranch from the full transcript. That lets the report
  keep a branch-bearing session's pre-branch spend in an explicit null row
  even when the range clipped the anchor turn, while a provider that never
  captures a branch still contributes nothing.

Part 2 (app): new "Pull requests" sidebar entry between Sessions and Spend
(shortcuts reflow to number 8 for Plans). It renders the pullRequests rows as
a refined table with the PR label linking out via openExternal, cost,
sessions, calls and an active-date span, plus a footnote stating the distinct
total and the by-reference attribution. A quiet explanatory line shows when no
PR links exist, never a fake table.

Tests: aggregateByBranch unit tests (carry-forward, null bucket, clipped
anchor, by-reference session counts) and the Pull requests component tests
(table, external link, footnote, both empty states). Nav-reflow assertions in
Sidebar and App tests updated.

Co-authored-by: reviewer <review@local>
2026-07-20 14:36:53 -07:00