Conversation
The window-global quick replies (Alt+A → "Yes, approved.", Alt+C → "Continue.") fired on any keydown with altKey set. On a Polish (and many other) layout, AltGr (right Alt) + a/c types ą/ć; on Linux and macOS AltGr sets altKey without ctrlKey, so typing a diacritic in the reply draft sent a canned reply instead of the text — reopening/continuing a session on the wrong prompt. Windows was shielded only because AltGr also sets ctrlKey. Fire the chord only when the keystroke produced the bare Latin letter (event.key === 'a'/'c'), never a composed diacritic, and skip whenever the AltGraph modifier is engaged. Adds a regression test for ć/ą. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Cezar knew what its own runs spent and learned about a plan limit only by walking into one. Neither answers "how much have I burned, and how close am I to the ceiling?". Adds one workspace-level read, `GET /api/v1/workspace/usage`, carrying two halves that are deliberately never summed: - `accounts[]` — tokens read from each agent's OWN home (`~/.claude`, `~/.codex`, and every extra account's relocated home), so sessions started from a terminal count too, plus any quota percentage the vendor itself wrote to disk (Codex's `rate_limits`; Claude publishes none, so nothing is invented); - `runs` — cezar's own run records across every registered project, the only half that can attribute tokens to a project or a dollar figure. Every cezar run appears in both, so adding them would double-count. Correctness details that decide the numbers: Claude writes one reply once per stream flush and copies it again on `--resume`, so samples de-duplicate on `message.id` + `requestId` (measured: 69 usage lines carrying 28 replies); Codex folds cached tokens into `input_tokens` and reports a per-event delta beside a cumulative total, so the delta is summed and the cache split out; run totals are read per STEP because `run.tokensUsed` is already their sum. Cost is bounded by an incremental reader over the append-only transcripts — resume at the last complete line, discard on shrink, never open a file untouched since the retention floor — behind the same stale-while-revalidate shape health uses, and a demand-driven `usage` WS topic so an idle workspace scans nothing. Measured on a real home (63 MB, 66 projects): 740 ms cold, 20 ms warm. Cockpit: a permanent read-out in the sidebar footer (last 5 h, plus the tightest published quota) and a `/usage` page with per-account windows, quota bars, 30-day sparklines and cezar's spend by agent and by project. Both obey the existing token/cost presentation switches; hosted mode serves no account rows, since those homes are on another machine. Spec: .ai/specs/2026-08-14-token-usage-monitor.md Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Self-review of #2 found one live defect, and writing the route test the review asked for found a second, worse one. Refreshing: - `reconcile()` never invalidated the usage query. The `usage` WS topic is local-mode only (a browser WebSocket cannot carry a reverse proxy's credentials), so a remotely served cockpit had NO refresh path at all: the sidebar chip and `/usage` froze on the first snapshot for as long as the tab stayed open, and the chip shows no staleness cue. One invalidation, pinned by the reconcile-doctrine tests. - `readUsage` served its cache even with no socket hub injected. Without a hub nothing keeps that cache warm, so the answer had nothing scheduled to correct it; `readHealth` opens with exactly this branch, for exactly this reason. Counting: - The runs half reported ZERO whenever the boot project was absent from the registry — which is the normal state of a task worktree and of `$HOME`, since `shouldRegisterProject` suppresses registration there. The ⌘K index can shrug that off because the active project's own `GET /runs` still feeds it; this route is its reader's only source, so it printed a confident zero over work that plainly happened. The boot store is in hand either way, so it is now always counted. - The Claude de-duplication key required BOTH `message.id` and `requestId`, so a line carrying only the former got no key at all — counted once per copy, the exact failure the key exists to prevent. Measured on 30 days of real transcripts: 23 of 10 267 usage lines have no `requestId` (all zero-token synthetic entries today, so nothing was double-counted in practice). Housekeeping: - `scanJsonlTree` now returns `{samples, newestPath, droppedFiles}` and takes an `onForget` eviction signal. That removes a second full walk of the Codex sessions tree per refresh, bounds the per-file model map that previously grew for the life of the process as sessions rotate, and makes the file-cap undercount say so instead of passing for a measurement. - The per-account failure reason is a fixed string again: Node's fs errors quote the path they failed on, which the comment above it claimed they never do. - `existsSync` → async `stat`, and a note on `fillDailySeries` about the client/server clock assumption its date keys rest on. Adds `usage-api.test.ts`: both halves on one read, the hosted-mode withholding, the boot-project fallback, the side-effect-free contract (a cold project's `runs/` directory must not appear), fresh-compute without a hub, and the degrade-to-zeros path. Every case above fails without its fix. Gate: typecheck clean, npm test 5718 passed / 315 files, test:unit 36, build + check:pack ok, test:package 12. Re-verified on a real ~/.claude: 729 ms cold, 28 ms warm. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
📦 npm preview dry run —
|
|
🤖 Claiming this PR — starting Note: this repository carries only the default GitHub labels, so every pipeline label this run would normally apply ( |
# Conflicts: # BACKWARD_COMPATIBILITY.md # packages/web/src/api/queries.ts # packages/web/src/routes.tsx
|
🤖 |
sheeerth
left a comment
There was a problem hiding this comment.
🔍 Code Review: feat(usage): monitor token usage across agent accounts and cezar runs
🎯 Summary
Re-review of PR #2 after two changes since the last pass: the review fixes in 5aef4a31, and the base merge in 12e476ef that brought 34 commits of main (including the 0.10.0 release bump, the global Tasks page, and the sidebar-footer fix open-mercato#879) onto this branch. The feature itself is one workspace-level read, GET /api/v1/workspace/usage, carrying two halves that are deliberately never summed — what each agent CLI recorded in its own home, and what cezar's own runs spent — plus a permanent read-out in the sidebar footer and a /usage page.
The scope reviewed is the full merged diff: 37 files, +3411/−20, of which 16 are new. Reviewed with particular attention to the three merge resolutions, since a textual auto-merge can compile and still be semantically wrong.
What this pass confirms as good: the two halves stay separate at the contract level rather than by convention, so nobody can accidentally add them; the incremental JSONL reader handles the three cases that matter (resume at the last complete line, discard on shrink, skip files untouched since the retention floor) and has tests that assert on parse counts rather than only on results, so the incrementality itself is pinned; the de-duplication key addresses a real property of Claude's transcript format that is easy to miss; the route inherits runs-index's side-effect-free contract, and the new usage-api.test.ts pins it by asserting a cold project's runs/ directory never appears.
Verdict
✅ approve — no blockers and no majors. Every finding from the previous pass is fixed and pinned by a test, the base merge is semantically clean, and CI is green on the merged head. The one caveat below is a disclosure, not a finding.
Submitted as a comment review rather than a formal approval only because GitHub does not permit approving one's own pull request. The verdict above is the review's real outcome.
🧪 Validation Gate
Run in configured order on the merged head (12e476ef):
| Command | Status | Notes |
|---|---|---|
npm run typecheck |
✅ PASS | Clean across contract, api-client, server and web. |
npm test |
See the note below — the same six fail on unmodified origin/main in this sandbox, and CI runs the suite green on this exact commit. |
|
npm run test:unit |
✅ PASS | 36 passed. |
npm run build |
✅ PASS | check:pack ok — 491 files, 85 under web/dist. |
npm run test:package |
✅ PASS | 15 passed. |
| GitHub Actions — Unit, build, E2E, and package | ✅ PASS | 4m29s on the merged head. |
| GitHub Actions — Publish npm snapshot | ✅ PASS | 26s. |
On the six local failures. They are getRepoInfo / worktreeShortstat / commitAll / health / projects / automations cases that assert behaviour outside a git repository. In this sandbox cezar points TMPDIR at .ai/cezar/tmp/<taskId> (the open-mercato#785 agent-tmpdir feature), which is inside the checkout — so mkdtempSync(tmpdir()) is not outside a repo at all and the assertions legitimately fail. Two pieces of evidence make this conclusive rather than convenient: the same six fail on an unmodified origin/main worktree in the same environment, and GitHub Actions runs the full suite green on this branch's merged head. Per the CI procedure, failures that also occur on the base branch are out of scope for this PR and are reported, not fixed from here.
Findings
No blockers, majors, minors or nits survived this pass. The previous review's six findings were all fixed in 5aef4a31 and are re-verified here:
reconcile()now invalidatesworkspaceQueryKeys.usage, so a remotely served cockpit — which gets nousageWS topic, because a browser WebSocket cannot carry a reverse proxy's credentials — refreshes on reconnect and visibility instead of freezing on its first snapshot. Pinned by the reconcile-doctrine tests.- The runs half now counts the boot project even when the registry does not list it. This was the worse defect the first review missed: registration is suppressed for task worktrees and for
$HOME, so the panel printed a confident zero in exactly the setup cezar's own task worktrees run in. readUsagecomputes fresh when no socket hub is injected, matchingreadHealth.- The Claude de-duplication key falls back to
message.idalone when a line carries norequestId(measured: 23 of 10 267 real usage lines). scanJsonlTreereturns{samples, newestPath, droppedFiles}and takes anonForgeteviction signal — one tree walk instead of two, a bounded per-file model map, and a file-cap undercount that says so.- The per-account failure reason is a fixed string, since Node's fs errors quote the path they failed on.
Merge resolution review (12e476ef)
Three files conflicted and each was checked for semantic — not merely textual — correctness:
packages/web/src/routes.tsx— both sides added an import (UsageRoutehere,GlobalTasksRouteon main); both kept. The<Route path="usage">registration survived intact.packages/web/src/api/queries.ts— main changeduseRunsIndexto takerefetchIntervalMs; that signature is kept, this branch'suseTokenUsage/useTokenUsageSubscriptionare kept, and the two doc blocks were re-separated so each again precedes its own function (the naive resolution left main's runs-index doc block orphaned above this branch's hook).BACKWARD_COMPATIBILITY.md— main addedreferenceStatusesto theruns-indexshape; that line is taken from main and the newworkspace/usageentry sits beside it.
Every mount point was then verified to survive the merge rather than assumed: the usage route, the usageChip slot in both app-shell and app-shell-container, the nav item, the reconcile invalidation and the root subscription. The merged NAV_ITEMS order is coherent, and the pinned nav/palette/shell tests pass against it.
One adjacent interaction was checked specifically because main had just touched it: open-mercato#879 fixed the sidebar footer's controls row overflowing at nightly-length version strings. This PR adds a separate footer row, so it does not re-enter that row's constraint; at the enforced MIN_SIDEBAR_WIDTH of 264px the new row's worst-case content (999.9k / 5h plus a 40px bar and 100%) measures roughly 166–176px inside a 236px content box, so it has margin and no elastic item is required.
💥 Breaking Changes
- No exported or public symbol removed or renamed without a deprecation path — the
AppShellusageChipprop andNavPropsaddition are optional and additive. - No function signature changed in a breaking way.
- No required type field removed or narrowed.
- No HTTP route removed or renamed;
/api/v1/workspace/usageis new, single-mount, and inventoried in BACKWARD_COMPATIBILITY.md §2 together with its degrade and hosted-mode behaviour. - No field removed or retyped in an existing response shape.
- No event or message name renamed; the WS topic
usageis new and trusted-only by default, which is correct for a payload naming projects and accounts. - No CLI command or flag changed.
- No database schema to change — state is plain files, and no file format was touched.
- No config key renamed, no default changed silently, no new
CEZ_*variable and no new user-authored state. - No contract had to change, so no deprecation window applies.
🧪 Test Coverage
The feature carries 96 test cases across seven files. The arithmetic (windows anchored on local midnight, retention floor, clock-skew tolerance, cache weighting, de-duplication) is covered in samples.test.ts; the incremental reader's append / shrink / partial-tail / cutoff / eviction behaviour in jsonl-scan.test.ts, with assertions on how many lines were actually parsed; both vendor formats — including the stream-flush duplicates, the --resume copies, the Codex cached-token split and the rate_limits shape — in providers.test.ts; step-versus-run attribution and grouping in runs-usage.test.ts; the shared presentation logic in usage-view.test.ts; the chip and page including hidden-metrics, hosted-mode, unreadable-project and empty states in usage.test.tsx; and the route itself — both halves, hosted-mode withholding, the boot-project fallback, the side-effect-free contract and the degrade-to-zeros path — in usage-api.test.ts.
Gaps worth naming, none of them blocking:
- Codex's rollout format is still unverified against a live
~/.codex— there is none on this machine. The parser accepts both the nestedinfo.last_token_usageshape and the older flat one, and an unrecognized line contributes nothing rather than a wrong number, so the failure mode is an undercount rather than a false figure. A Codex user's confirmation would close this. - No before/after bundle measurement. The page adds no browser dependency and draws with CSS bars rather than a chart library, so the growth is source only; the built main chunk is 280 kB / 75 kB gzip. Worth a number if the repo starts tracking one.
🔬 UI QA —
|
| # | Where | What was expected | Result | Evidence |
|---|---|---|---|---|
| 1 | /usage, local mode |
Page renders both halves, labelled and never summed; account cards carry windows, a 30-day sparkline and a model breakdown | ✅ PASS | step-01-usage-page.png |
| 2 | Sidebar footer, every route | The chip is present, shows the rolling-5h total, and falls back to the day's total when no vendor quota exists | ✅ PASS | step-01-usage-page.png (bottom left: 21.4M / 5h · 21.4M today) |
| 3 | Tasks page → chip click | The chip navigates to the usage page | ✅ PASS | step-03-chip-navigates.png |
| 4 | Codex account with no ~/.codex |
Row is present and states why, rather than vanishing or showing a zero | ✅ PASS | step-01-usage-page.png (right card) |
| 5 | Claude account | No quota percentage is shown — Claude publishes none locally | ✅ PASS | step-01-usage-page.png (no % on the Claude card) |
| 6 | Hosted mode (CEZ_REMOTE=1) |
Account rows withheld with an explanation; the cezar-tasks half still answers | ✅ PASS | Text capture below |
| 7 | Live incrementality | Numbers advance as the running agent appends to its transcript | ✅ PASS | 21.4M → 21.8M across two captures ~4 min apart |
| 8 | Mobile viewport | Layout holds at phone width |
Notes worth a reviewer's eye
Hosted mode was verified first, by accident and then on purpose. The sandbox this ran in exports
CEZ_REMOTE=1, so the first boot came up hosted and the page correctly rendered "This cockpit is
served remotely, so the agent homes it would read are on another machine. Only the cezar tasks below
can be counted from here." with capabilities.localHandoff: false and an empty accounts[], and the
sidebar chip correctly did not render. The environment was then rebooted with CEZ_REMOTE unset to
exercise the local-mode surface above. Both branches are therefore verified against a real server
rather than only in unit tests.
The numbers are real. GET /api/v1/workspace/usage answered from this machine's actual
~/.claude: 21.4M weighted tokens in the rolling 5 h window, 247.5M over 7 days, 369.7M over 30,
split across claude-opus-5 / claude-opus-4-8 / claude-haiku-4-5. The Codex row reports
available: false with no sessions recorded for this account yet, which is correct — there is no
~/.codex here. The cezar-tasks half reads zero because the test env boots against a throwaway
CEZ_HOME, so its registry holds no runs; that is the honest answer for that sandbox, not a defect.
One cosmetic nit, not blocking: the account card header reads Claude Code Default and then
default again at the right edge — the account's label and the isDefault badge say the same word
twice. Worth collapsing when the badge and the label agree.
Environment
- App: production build,
http://127.0.0.1:53197,CEZ_DRY_RUN=1(agent CLIs mocked; no login, no network). - Browser: Chrome for Testing 152.0.7977.54 via agent-browser 0.33.2, launched with
--no-sandbox
(this host disables unprivileged user namespaces) and a shortTMPDIR(cezar's own agentTMPDIR
exceeds Chrome's singleton-socket path limit). Neither workaround touches the app under test.
step-01-usage-page.png
step-03-chip-navigates.png
🧪 Follow-up: browser-level test for the usage surfaceThis change ships thorough component tests ( File:
The fixture work is the only real cost: steps 3–5 need a server booted with a controlled Evidence only — no labels were changed by this QA pass. |
🤖
|


🎯 Goal
Give the cockpit a permanent, honest answer to "how many tokens have I burned, and how close am I to the ceiling?" — counting the agents' own sessions, not just the ones cezar started. A read-out sits in the sidebar footer at all times;
/usageholds the breakdown.What Changed
packages/contract/src/usage.ts, exported from the index):GET /api/v1/workspace/usage→{generatedAt, accounts[], runs}. Two halves that are deliberately never summed — every cezar run appears in both, so adding them double-counts.accounts[]is what each agent CLI recorded in its own home;runsis what cezar's own runs spent.packages/cezar/src/usage/):samples.ts— the one definition of a window (rolling5hplustoday/last7d/last30danchored on the host's LOCAL midnight), the cache weighting reused fromcore/usage.ts, and de-duplication. Three readers do not get three notions of "today".jsonl-scan.ts— incremental reader for append-only logs: resume at the last complete line (a half-flushed tail is re-read, not dropped), discard the cache when a file shrank, never open a file untouched since the retention floor.providers.ts— Claude (<home>/projects/**, subagent transcripts included) and Codex (<home>/sessions/**, plus therate_limitstail read).runs-usage.ts— per-STEP attribution over run records, becauserun.tokensUsedis by construction their sum.snapshot.ts— assembly and the per-account degrade policy.server.ts): ausageRoutesfamily chained intoworkspaceV1, plus a demand-drivenusageWS topic, both behind the same stale-while-revalidate cache shapehealthuses (30 s TTL, 5 min staleness ceiling). Project runs are read the wayruns-indexreads them — straight offruns.jsonfor an unowned project, never through a built context, so opening a usage panel cannot prune worktrees or resume agents.UsageChipin the sidebar footer (a shell SLOT, mounted byapp-shell-container, soAppShellstays presentational), the/usageroute and its nav item,lib/usage-view.tsfor the presentation logic the chip and the page share, and one root-leveluseTokenUsageSubscriptionmirroringuseHealthSubscription..ai/specs/2026-08-14-token-usage-monitor.md, the route's entry in BACKWARD_COMPATIBILITY.md §2, and the README view table (7 → 8 views).The details that decide whether the numbers are right
--resumecopies earlier replies into the new session file. A measured transcript held 69 usage lines carrying 28 distinct replies, so samples de-duplicate onmessage.id+requestId; summing lines would roughly double every figure.input_tokensand reports a per-event delta beside a cumulative total. The delta is summed (so a resumed session adds up) and the cache is split out (so the weighting can price it at ~10%).rate_limitsto disk and those are shown verbatim, with the reset resolved; Claude publishes none locally, so itslimits[]is empty rather than filled from a plan ceiling cezar does not know.🧪 Tests
npm run typecheck— clean (contract, api-client, server, web).npm test— 5707 passed / 314 files, including 51 new cases.npm run test:unit— 36 passed.npm run build+check:pack— ok (478 files).npm run test:package— 12 passed.samples.test.ts), the incremental reader's append/shrink/partial-tail/cutoff behavior (jsonl-scan.test.ts), both vendor formats including the stream-flush and--resumeduplicates and therate_limitsshape (providers.test.ts), step-vs-run attribution and grouping (runs-usage.test.ts), the shared view logic (usage-view.test.ts), and the chip/page including the hidden-metrics, hosted-mode, unreadable-project and empty states (usage.test.tsx).nav-items,app-shell,project-groups,command-palette,health-topic).~/.claude(63 MB, 66 projects, 30 days): 11 505 samples, 740 ms cold, 20 ms warm — the incremental cache does what it claims.💥 Breaking Changes
None. The route is additive, no existing shape changed, no new env var, no new user-authored state. Hosted mode serves
accounts: [](those homes are on another machine), andCEZ_HIDE_TOKEN_METRICS=1removes the surfaces entirely.One caveat worth a reviewer's eye
Codex's rollout format could not be verified against a live
~/.codexon this machine — there is none. The parser handles both the nestedinfo.last_token_usageshape and the older flat one, and an unrecognized line contributes nothing rather than a wrong number, but a Codex user's confirmation would be worth having.🤖 Generated with Claude Code