Skip to content

feat(desktop): add Agent Usage UI backed by the NIP-AM local archive - #2035

Open
wpfleger96 wants to merge 23 commits into
mainfrom
duncan/agent-usage-archive
Open

feat(desktop): add Agent Usage UI backed by the NIP-AM local archive#2035
wpfleger96 wants to merge 23 commits into
mainfrom
duncan/agent-usage-archive

Conversation

@wpfleger96

@wpfleger96 wpfleger96 commented Jul 17, 2026

Copy link
Copy Markdown
Member

Adds a rolling token/cost usage view for agents, sourced from the local NIP-AM metrics archive. Covers both the Agents overview and a focused per-agent subview in the profile panel, with the per-agent breakdown grouped by (harness, model) so the same model running under two different harnesses (e.g. claude-sonnet-4-5 via goose vs claude-code) renders as two distinct rows.

Backend

  • get_agent_usage_series Tauri command (archive/agent_usage.rs): reads the local SQLite archive over a caller-supplied window of local-midnight bucket boundaries, returns per-agent token and cost totals broken down by (harness, model), with partial/unknown-field flags when evidence is incomplete. Sort tiebreak: harness ascending → model ascending, None last in each.
  • Boundary arity is a bounded range rather than a fixed pair: MIN_BOUNDARIES = 2 (the single-bucket 1d case) through MAX_BOUNDARIES = 367 (366 daily buckets, one leap year). The frontend picker clamps to the same span, so this fail-closed check is defense in depth and never the UX error path. MAX_INTERVAL_SECS still bounds each individual bucket at 48h.
  • agent_metric_index: nullable harness TEXT column, parsed from AgentTurnMetricPayload.harness and written/read through all store paths. Schema migration M1 runs as a single SQLite transaction — schema-guarded ALTER TABLE, full index rebuild from the canonical archived_events store (ingest and backfill share the same from_payload parser), completion marker written last — so a crash mid-migration can never leave a half-built index marked complete.
  • Wired persistedAgentMetrics notifier through the archive sync path so new metric events are picked up without a full resync.

Frontend

  • AgentUsageSection (features/agent-usage/ui/): ranked agent list in AgentsView, rendered below the agent cards and the teams section. Row click-through opens the profile panel focused on usage.
  • AgentUsageDailyBars: each bar carries its date on the x-axis and its compact token total above it, plus a per-bar hover tooltip with the exact total/input/output split. Each tooltip field reports "unknown" independently rather than collapsing an uncounted value to zero. Above 14 buckets the chart drops on-bar value text and thins date ticks instead of overflowing; the tooltip stays complete.
  • AgentUsageRangeTabs: 1d/7d/30d presets plus a Custom tab whose popover takes an arbitrary inclusive start/end date pair. Selecting Custom opens the picker without moving the active range, so an in-progress edit never issues a query. Validation is local — the user sees "Pick a range of 366 days or fewer", not a backend arity error.
  • Custom endpoints are civil dates parsed to local midnight and walked by distinct local midnights, so DST transitions and skipped civil dates cannot produce duplicate or non-increasing bucket boundaries.
  • AgentUsageFocusedView: per-agent totals with a by-(harness, model) breakdown, rendered as a focused profile-panel subview (same pattern as Memories/Diagnostics) rather than a new tab. Each breakdown row shows the harness as a dimmed sub-label next to the model name; null harness (pre-migration or unknown data) renders no label, so single-harness data is visually unchanged.
  • UserProfilePanel/UserProfilePanelSections/UserProfilePanelTabs: threaded canViewUsage/onOpenUsage (owner-only, bot profiles only) and added a BarChart3 ingress row in the Info tab.
  • agentUsage.ts sortModelsByKnownTotal: ordinal compound (harness, model) tiebreak matching the Rust ordering (identifier domain is ASCII; documented).
  • Loading skeleton, query-error retry, empty state, and a collection-off banner (with retained-data coverage copy when historical data still exists) are all covered.

Agent-side first-turn accounting

UsageTracker::seed_zero_baseline() (crates/buzz-acp/src/usage.rs) seeds a zero baseline for sessions this process just spawned, so the first turn of a fresh session reports reliable token deltas instead of failing closed. Sessions the process re-attaches to are deliberately not seeded — their true prior cumulative is unknown, and fabricating a baseline there would misattribute another process's tokens to the first observed turn.

last_cost seeds to Some(0.0) so a first-turn cost delta computes against a real zero; last_total stays None because NIP-AM forbids fabricating a total baseline for providers that never report one. The call sites in pool.rs sit immediately after create_session_and_apply_model() on both the channel and heartbeat paths, keyed off is_new_session — the only authoritative spawned-vs-attached signal. Seeding is idempotent via entry().or_insert().

Tests

  • Rust: unit tests for bucket-boundary math, boundary-arity acceptance and rejection at both edges of the supported range, cost-ladder aggregation (direct/cumulative/decrease-taint), ranking tie-breaks (including harness), and the same-model/two-harness collapse fix; integration tests exercising the command against a real SQLite archive (fresh ingest, pubkey filtering, pre-existing-row backfill); store_migration_tests.rs covering M1 through the real open_archive_db path (legacy-file backfill, idempotent reopen, crash-before-commit recovery).
  • agentUsage.test.mjs / hooks.test.mjs: pure-helper unit tests for formatting, partial/unknown-field detection, compound-key sorting, custom-range validation, and local-midnight boundary construction across DST and skipped civil dates.
  • tests/e2e/agent-usage.spec.ts: loading → resolved, preset and custom window switching, both click-through paths into the focused view, dated axis labels and on-bar values, tooltip contents, error/retry, empty state, collection-off banner (with and without retained-data copy), partial-total badge, harness sub-label rendering, and the usage section's position below the agents and teams sections.
  • crates/buzz-acp/src/usage.rs: regression tests covering the spawned first turn reporting reliable deltas, the re-attached first turn staying fail-closed, both paths going reliable by the second turn, every wire field on the first-turn payload, and the seed being a no-op when a baseline already exists.

@wpfleger96
wpfleger96 requested a review from a team as a code owner July 17, 2026 17:08
@wpfleger96
wpfleger96 marked this pull request as draft July 17, 2026 17:10
@wpfleger96
wpfleger96 force-pushed the duncan/agent-usage-archive branch from 3bdb2a1 to 2451115 Compare July 20, 2026 17:45
npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 and others added 3 commits July 20, 2026 23:20
Adds the Rust backend half of the NIP-AM local agent usage feature
(Rev 3 frozen plan): a rebuildable agent_metric_index parsed from
archived kind-44200 rows, a pure per-field accounting ladder
(agent_usage.rs) computing token/cost deltas with adjacent-cumulative
preference and direct-value fallback, and the get_agent_usage_series
Tauri command wiring backfill, orphan repair, collection-enabled
detection, and A13's hasArchivedEvidence into one series response.

commit_archive now indexes kind-44200 rows in the same transaction as
the canonical event insert, and upsert_archived_event/gc_orphaned_events
return/enforce the new-row and cascade-delete invariants (A5/A6) that
persistedAgentMetrics and the index's rebuildability depend on.

get_agent_usage_series' SQLite core is split into a plain sync fn so
it can be driven directly against an in-memory Connection in tests
without a Tauri AppState.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Completes Phase 2 of the NIP-AM local agent usage feature. tauriArchive
decodes the backend's persistedAgentMetrics count defensively (missing
field -> 0, per A5/M2's backward-compatible wire contract) and exposes
an onAgentMetricsChanged notifier, fired only when a subscription
mutation touches kind 44200 specifically.

ArchiveSyncManager routes both its idle/size-triggered and destroy-time
flushes through one sendBatch helper that notifies usage listeners
only when the backend reports persistedAgentMetrics > 0 — rejections,
duplicate-only batches, and non-metric successes never notify, keeping
the refresh signal backend-authoritative.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Surfaces per-agent NIP-AM turn-metric usage (tokens, cost, model
breakdown, 7d/30d bucket series) in the desktop app, reading from the
Phase 2 get_agent_usage_series archive command:

- agent-usage/lib/agentUsage.ts: pure client-side helpers -- bucket
  boundary construction, bigint-safe token formatting, and the ranked
  overview-row projection shared by both UI surfaces.
- agent-usage/hooks.ts: react-query wrapper around
  get_agent_usage_series with the app's standard retry:1 default.
- agent-usage/ui/AgentUsageSection.tsx: ranked per-agent usage rows on
  the Agents overview, with a 7d/30d window switch, retry-on-error,
  and a collection-off banner (with or without retained-data coverage
  copy) linking to Local Archive settings.
- agent-usage/ui/AgentUsageFocusedView.tsx: per-agent drill-down
  (bucketed series + model breakdown) reached via an overview row or
  the profile panel's Info-tab usage ingress row; wired into
  AgentsView, UserProfilePanel, and ProfilePanelContext.
- Partial badge on rows whose total is a known lower bound
  (has_unknown_usage), surfaced via the accounting engine's per-field
  completeness contract.

Extends the Rust archive test suite (agent_usage_tests.rs) to close
gaps in the pure accounting engine: the f64 cost ladder had no direct
outcome.cost assertions, assign_bucket_index's start-inclusive/
end-exclusive edges were only exercised indirectly, validate_request's
chrono finite-range rejection was unexercised, and the A2 ranking
tiebreak (pubkey ascending on an exact totalTokens tie) had no test
distinguishing it from insertion order.

tests/e2e/agent-usage.spec.ts (wired into playwright.config.ts's smoke
project) exercises both UI surfaces against a mocked
get_agent_usage_series: ranked rows, window switching, click-through
to the focused view from both the overview row and the profile
panel's ingress row, retry recovery from a query error, the
collection-off banner and its settings deep link, and the Partial
badge.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
@wpfleger96
wpfleger96 force-pushed the duncan/agent-usage-archive branch 2 times, most recently from 4576ca6 to b213a00 Compare July 21, 2026 07:00
…chive

* origin/main: (25 commits)
  feat(cli): filter archived instances from --template roster resolution (#2207)
  chore(release): release Buzz Desktop version 0.4.21 (#2209)
  fix(desktop): clarify data deletion action (#2208)
  feat: brand authentication complete page (#2192)
  fix(buzz-acp,buzz-agent): surface stall duration and fate (#2204)
  fix(desktop): resolve activity feed showing channel UUID instead of message content (#2201)
  feat(cli): add agents archive/unarchive/archived subcommands (#2173)
  chore(acp): strip stale finding-number references from comments (#2202)
  chore(release): release Buzz Mobile version 0.4.9 (#2200)
  Simplify first-community onboarding choices (UI only) (#2194)
  fix(mobile): sanitize Android image uploads (#2188)
  fix(cli): paginate channel directory queries (#2181)
  Fix persisted agent defaults display (#2182)
  [codex] Fix missing release tag detection (#2191)
  feat(web): authenticate gated community browsing (#2190)
  fix(timeout): unified turn-timeout fix — cap inheritance, steer renewal, activity-aware requeue, LLM stall surfacing (#2175)
  fix(desktop): scope draft store per workspace relay (#2179)
  chore(release): release Buzz Mobile version 0.4.8 (#2187)
  [codex] create release tags with dedicated App (#2186)
  fix(desktop): avoid forwarding click event as channel callback (#2174)
  ...

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
@wpfleger96
wpfleger96 force-pushed the duncan/agent-usage-archive branch from b213a00 to 122b043 Compare July 21, 2026 08:04
wpfleger96 pushed a commit that referenced this pull request Jul 21, 2026
wpfleger96 pushed a commit that referenced this pull request Jul 23, 2026
Four Playwright tests covering the new Agent Usage UI surfaces:
01 overview card (default/collapsed), 02 focused usage subview,
03 multi-day bars chart, 04 empty state (collection on, no data).

Also adds agent-usage-screenshots.spec.ts to the smoke project
testMatch so CI can discover and run it alongside the other
screenshot specs.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Stack: #2035 → this PR

## What

Group the per-agent model breakdown by `(harness, model)` instead of
`model` alone, so the same model running under two different harnesses
(e.g. `claude-sonnet` via `goose` vs `claude-code`) produces two
distinct rows rather than collapsing into one.

## Changes

**Rust / SQLite**
- `agent_metric_index`: add nullable `harness TEXT` column. Schema
migration M1: `ALTER TABLE ... ADD COLUMN` + delete-then-backfill
rebuild so all existing rows get `harness` populated from retained
`archived_events.raw_json` — no data loss; ingest and backfill share the
same `from_payload` parser (frozen-plan requirement preserved).
- `AgentMetricIndexRow`: parse `harness` from
`AgentTurnMetricPayload.harness` (REQUIRED field per NIP-AM), write/read
through all store paths (`ROW_COLUMNS`, INSERT, `row_from_sql`).
- `AgentScope.models` grouping key widened from `Option<String>` to
`(Option<String>, Option<String>)` i.e. `(harness, model)`. Sort
tiebreak: harness ascending → model ascending, `None` last in each.
- `ModelUsage` wire type: add `harness: Option<String>` field.

**Frontend**
- `tauriArchive.ts` / `bridge.ts`: add `harness` to `AgentUsageModel`
type.
- `AgentUsageFocusedView`: render `harness` as a dimmed sub-label next
to each model name on breakdown rows. `null` harness (pre-migration data
or unknown) renders no label — single-harness data is visually
unchanged.
- `agentUsage.ts` `sortModelsByKnownTotal`: tiebreak updated for
compound `(harness, model)` key.

**Tests**
- Rust: collapse-fix test (same model / two harnesses → two rows),
migration test (old-shape rows get harness populated after rebuild),
harness sort coverage.
- TS: `sortModelsByKnownTotal` harness tiebreak tests, same-model
two-harness sort test.
- E2E: `bridge.ts` fixture type updated, `agent-usage.spec.ts` harness
assertion, `agent-usage-screenshots.spec.ts` shot 02 harness-label
visibility check.

## Gates

All green locally:
- `just desktop-check` ✓
- `just desktop-typecheck` ✓
- `just desktop-build` ✓
- `just desktop-test` (3487 pass, 0 fail) ✓
- `cargo test` in `desktop/src-tauri` (1575 pass, 0 fail) ✓

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
wpfleger96 pushed a commit that referenced this pull request Jul 25, 2026
npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 and others added 3 commits July 25, 2026 15:43
…te formatter

The branch had ratcheted the desktop file-size gate four times. One of those
entries re-declared `src-tauri/src/lib.rs` at 1001 while a pre-existing entry
at line 58 sets 1013 — the override map is a JS `Map`, so the later key won and
the branch silently lowered a ceiling it never needed to touch (lib.rs is 920).

Splitting at existing seams removes the other two: the M1 schema migration
moves to `archive/store_migrations.rs` behind one `pub(super)` entry point, and
the kind-44200 archive tests move to `archive/mod_agent_metric_tests.rs`,
`#[path]`-included from `mod_tests.rs` so they keep reaching the shared
fixtures through `use super::*`. `mod_tests.rs` returns to its pre-branch 1208
ceiling rather than dropping it, since that debt predates this work.

`formatCoverageDate` was duplicated verbatim in both usage components; it now
lives beside the other formatters in `lib/agentUsage.ts`.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…chive

* origin/main: (112 commits)
  docs(contributing): trim to goose-scale minimal intake surface (#2780)
  fix(relay): preserve reconnect backoff (#2759)
  refactor(relay): expose reconnect timing policy (#2310)
  fix(desktop): clear stale working badges on agent stop/restart (#2803)
  fix(desktop): surface agent rename relay profile sync failure as a warning toast (#2279)
  fix(docker): create /data/git so the compose volume inherits buzz ownership (#2840)
  fix(mobile): invalidate DM directory providers at the community boundary (#2842)
  feat(relay): make per-owner community limit configurable via BUZZ_MAX_COMMUNITIES_PER_OWNER (#2599)
  fix(discovery): inject PATH into Codex adapter planning (#2767)
  chore(release): release Buzz Desktop version 0.4.26 (#2808)
  Refine mobile navigation and creation flows (#2810)
  feat(relay): add author-only-unless-shared read gate for kind 30175 (#2768)
  fix(core): block IPv6 transition SSRF targets (#2801)
  Style mobile pairing QR codes (#2775)
  Refine community management flows (#2738)
  fix(workflow): bypass system proxies for webhooks (#2800)
  docs: replace VPN-vendor references with generic wording (#2805)
  docs: point readme at deploy compose bundle (#2363)
  fix(desktop): explain macOS local network access (#2263)
  fix(desktop): clarify CLI runtime setup (#2680)
  ...

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Merging main brought in #2098, which removed the additional-agents gallery and
shrank `UserProfilePanel.tsx` from 1014 to 983 lines. This branch's additions
land it at 1009 — under main's existing 1025 ceiling — so the ratchet to 1040
is no longer load-bearing.

The branch now leaves the override map byte-identical to main: no new entries,
no raised ceilings, no lowered ones.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 and others added 2 commits July 29, 2026 14:07
No real publisher emits totalTokens today (0 of 1,934 prod reports
do). The UI was keying its flagship visuals off that field, producing
'No usage reported' over millions of real tokens.

Four display surfaces fixed:
- Daily bars header: '≈ N tokens' when knownTotal null but i/o known
- Daily bar heights: 'approx' bar kind scaled to i/o sum (bg-primary/70)
  instead of the hatched unknown baseline
- Per-agent row: trailing text '≈ N' + progress bar from i/o approx
- Focused view 'Total tokens' stat: '≈ N,NNN' exact display

PARTIAL badge: no longer keys on totalTokens.incomplete (permanently
true); now keys on inputTokens/outputTokens incompleteness only, so
honest partial rows still badge but the permanently-null-total case
does not.

Caveat paragraph: gated on agent.hasUnknownUsage || invalidReportCount
> 0 only, not on the permanent totals-unknown state. Removed the false
persistent-error banner from every real prod user's focused view.

All approximations use ≈ prefix at every callsite — the wire contract
is unchanged; this is display only.

New lib export: deriveApproxTotal (5 unit tests). sumKnownBucketTotals
now returns approxTotal alongside knownTotal (2 new unit tests + 2
existing tests updated). e2e seeds updated: mockAgentUsage default
now uses {inputTokens, outputTokens, totalTokens: null} — the real
prod shape — so the suite can no longer pass while prod renders empty.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…plit caveat

Fold Thufir's three blocking amendments into cbfbe1e:

1. Centralize display metric as DisplayTotal {kind, value, partial}.
   deriveApproxTotal() replaced by deriveDisplayTotal() returning an
   explicit exact|approximate|unknown discriminant with provenance:
   - exact: totalTokens.value present; partial mirrors wire incomplete flag
   - approximate: totalTokens null, i/o known; value = bigint-safe i/o sum;
     partial = inputTokens.incomplete || outputTokens.incomplete
   - unknown: no token counts at all; value null, partial false
   All surfaces (Section header/bars/rows, DailyBars, FocusedView stat)
   now consume this single result — no surface can drift independently.

2. Re-sort agents/models by display tier.
   sortAgentsByKnownTotal/sortModelsByKnownTotal replaced by
   sortAgentsByDisplayTotal/sortModelsByDisplayTotal using a three-tier
   key (exact=0, approximate=1, unknown=2), descending by value within
   tier, then existing tiebreaks. With all-null prod totals the list
   order and bar widths now agree instead of disagreeing.

3. Split caveat paragraph into per-condition sentences.
   The single agent-usage-focused-partial-explanation block replaced by
   two separately-gated <p> elements:
   - agent-usage-focused-unknown-intervals-caveat: gates on hasUnknownUsage
   - agent-usage-focused-invalid-reports-caveat: gates on invalidReportCount > 0
   Each sentence claims only what its gate proves.

Test updates:
- deriveApproxTotal tests replaced with deriveDisplayTotal tests covering
  all three kinds, partial provenance, and i/o-only combinations.
- Sort tests updated for new export names and assertions.
- Mixed exact/approximate/unknown population tests added for both
  sortAgentsByDisplayTotal and sortModelsByDisplayTotal.
- e2e spec updated from single partial-explanation testid to the two
  per-condition testids.
- 3567 tests pass, tsc --noEmit clean.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 and others added 3 commits July 29, 2026 16:50
1. Gate unknown-intervals caveat on direct i/o incompleteness
   (isPartialField(inputTokens) || isPartialField(outputTokens)) instead
   of hasUnknownUsage, which ORs total/cost incompleteness too and cannot
   prove an i/o interval claim. Narrow copy to 'Some input/output usage
   could not be counted and is omitted rather than shown as zero.' Fix the
   incorrect deltaReliable comment.

2. deriveDisplayTotal fail-closed: approximate only when BOTH input and
   output are non-null. One-sided i/o returns unknown for the total (the
   independent display fields may still show the known value). Replace
   one-sided approximate tests with fail-closed tests.

3. sumKnownBucketTotals returns a single provenance-bearing DisplayTotal
   instead of parallel {knownTotal, approxTotal, partial}. Exact when
   every report-bearing bucket is exact; approximate (summing
   exact+approx display values) when any bucket is approximate; unknown
   when any report-bearing bucket has no display value. Partial derives
   from the union of each bucket's DisplayTotal.partial (i/o and total
   completeness) — not from total absence. Add mixed-bucket test.

4. Partial provenance end-to-end: AgentUsageSection header consumes the
   single DisplayTotal and derives partial from its .partial field.
   deriveBarState carries dt.partial in the approx bar (new
   'approx-partial' kind) so genuinely incomplete i/o is identified in
   daily bar styling and accessible copy. Approximation alone no longer
   triggers Partial.

5. Behavioral e2e guard: new scenario seeds both i/o + totalTokens:null
   and asserts all four surfaces (overview row, header, daily bar,
   focused total) render '≈'. Fix stale 'in 800' assertion to '≈ 1K'.
   Update the coverage-dates partial-explanation fixture to use
   incomplete i/o fields so the new gate fires correctly.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…bar copy, e2e isolation)

Three remaining pass-2 findings fixed:

1. sumKnownBucketTotals aggregation precedence (Thufir's explicit ruling).
   One unknown report-bearing bucket no longer erases the known subtotal.
   Track anyWithValue separately from sawAny; unknown buckets set partial=true
   but do not touch sumValue. Return unknown/null ONLY when no report-bearing
   bucket contributed any numeric value. Added three new tests: exact+unknown
   (preserves exact subtotal, partial=true), approximate+unknown (preserves
   approx subtotal, partial=true), all-unknown (returns unknown/null).

2. approx-partial daily bar trailing copy.
   trailingText for approx-partial now renders '≈N*' (asterisk suffix)
   instead of '≈N', giving sighted users a distinct partial signal beyond
   opacity alone. T2 e2e test extended with an incomplete-i/o bucket and
   an assertion on '≈1K*' to verify the distinguishable copy.

3. e2e guard surface isolation.
   Header and focused-total assertions in the T1b regression guard now
   target dedicated testids (agent-usage-header-value and
   agent-usage-focused-total-value) so each surface fails independently
   rather than passing via a descendant bar's ≈ text. Added testId prop
   to UsageStat (optional, threading to the value <p> only), wired from
   ApproxTokenStat. Added data-testid='agent-usage-header-value' to the
   header value span in AgentUsageSection.

MINOR: fixed stale caveat-gate comment at agent-usage.spec.ts that still
attributed the sentence to hasUnknownUsage=true instead of direct i/o
incompleteness.

Claim reconciliation: the stated 21/21 Playwright count was wrong — the
file defines 17 tests and I ran 17/17. The prior run was against this
file at this head.

Gates: 3571 unit tests pass, tsc --noEmit clean, 17/17 agent-usage
Playwright specs pass under the smoke project.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Union of smoke test list entries from both branches:
- agent-usage.spec.ts (this branch)
- agent-usage-screenshots.spec.ts (this branch)
- harness-management.spec.ts (main)
- harness-catalog-screenshots.spec.ts (main)
- inline-custom-harness.spec.ts (main)

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
wpfleger96 added a commit that referenced this pull request Jul 29, 2026
…ain (#3593)

## What

Wires genuine provider-reported `total_tokens` through the full
buzz-agent → buzz-acp publish chain so kind-44200 events carry real
per-turn and cumulative totals for OpenAI-backed models, while
preserving all existing behaviour for Anthropic and external harnesses
(goose, claude-code).

## Why

Live prod data showed 0 of 1,934 archived reports carry `totalTokens`.
Both hardcoded `total_tokens: None` in `pool.rs` and the absent field in
`buzz-agent`'s parser are root causes. This is the backend half of a
two-track fix; the display-fallback half lands in
[#2035](#2035).

## Changes

**`crates/buzz-agent/src/types.rs`**
- Added `total_tokens: Option<u64>` to `LlmResponse` with an explicit
doc comment that NIP-AM forbids deriving it.
- Added `TurnTotalState` enum (`Unseen | Exact(u64) | Unknown`) with
`fold()` and `exact_value()` — the tri-state accumulator that
distinguishes not-yet-observed from permanently poisoned.

**`crates/buzz-agent/src/llm.rs`**
- `parse_responses` and `parse_openai`: read `usage.total_tokens` from
OpenAI Chat Completions (including Databricks routes) and the Responses
API via `sum_usage`.
- Anthropic: explicit `total_tokens: None` — no genuine total available;
NIP-AM forbids summing categories.

**`crates/buzz-agent/src/agent.rs`**
- Added `turn_total_state: &'a mut TurnTotalState` to `RunCtx`.
- Fold `response.total_tokens` into the accumulator after each
usage-bearing response; non-usage-bearing responses (keepalive/stream
frames) do not poison.

**`crates/buzz-agent/src/lib.rs`**
- Added `accumulated_total_state: TurnTotalState` to `Session` (default
`Unseen`).
- Per-turn state passed to `RunCtx`, folded into session cumulative
after each turn.
- Emits `accumulatedTotalTokens` in `usage_update` only when cumulative
is `Exact(n)`.

**`crates/buzz-acp/src/usage.rs`**
- Added `accumulated_total_tokens: Option<u64>` (serde default) to
`UsageUpdatePayload` — optional for goose compat.
- Added `last_total: Option<u64>` to `SessionState`.
- Added `turn_total_tokens` and `cumulative_total_tokens` to `TurnUsage`
(field-local — never affect `delta_reliable`).
- Derive turn-total delta only when prev and current are both `Some` and
monotonic; absence, decrease, or no baseline leaves only the total delta
null without touching input/output reliability.

**`crates/buzz-acp/src/pool.rs`**
- Replaced both hardcoded `total_tokens: None` in
`publish_agent_turn_metric` with `usage.turn_total_tokens` and
`usage.cumulative_total_tokens`.

## Tests

20 new tests across the four touched files:

| File | Tests |
|------|-------|
| `types.rs` | `TurnTotalState` fold, accumulation, exact_value, default
(7 tests) |
| `llm.rs` | Chat present/absent, Responses present/absent, Anthropic
always-None (5 tests) |
| `usage.rs` | First turn no baseline, second-turn delta, cumulative
decrease (field-local), current absent, goose-shaped deserialization,
baseline absent (6 tests) |
| `pool.rs` | Exact turn+cumulative mapping, null totals never derived
(2 tests) |

`cargo test -p buzz-acp -p buzz-agent` — all passing, 0 failures.

## Scope

Boundary: `crates/buzz-agent/**` + `crates/buzz-acp/**` only. Desktop
unchanged.
`costUsd` explicitly out of scope.

Related: [#2035](#2035)

---------

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: npub1g8493u0xfsjrvflg4n08ezd7vec99mnwzlv0qgwpr9d7gvjwhuzqx59rhw <41ea58f1e64c243627e8acde7c89be667052ee6e17d8f021c1195be4324ebf04@buzz.block.builderlab.xyz>
npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 and others added 7 commits July 29, 2026 18:18
Main's Biome version rewraps lines differently than the version this
branch was written against, so the post-merge tree failed the desktop
lint and format gate. Formatter output only; no logic changes.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
main's file-size ratchet (#3352) replaced the static per-file override map
with a git-diff base comparison, dropping the 1025-line ceiling that
UserProfilePanel.tsx had been living under. The 7-day ingress query was
agent-usage logic sitting in a profile file; moving it home puts the panel
back under the 1000-line limit with no behavior change.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…chive

* origin/main: (22 commits)
  feat(catalog): resolve publisher display name in catalog detail pane (#3640)
  feat(mesh): upgrade embedded mesh to v0.74 and harden shared compute (split 1/2 of #3467) (#3741)
  docs(nips): specify kind:30621 multi-repo projects (NIP-MP) (#3163)
  Refine agent sharing dialog (#3699)
  desktop: enable getUserMedia in the Linux WebKitGTK webview (#3607)
  fix: align responsive agent views (#3688)
  Add macOS agent menu-bar menu (#3565)
  Fix pending message feedback (#3543)
  fix(desktop): remove remaining Projects panel fills (#3742)
  feat(mobile): desktop-parity emoji and thread experience (#3485)
  desktop: restore direct community member adds (#3634)
  fix(desktop): explain open agent access (#2561)
  fix(cli): resolve agents from owner records (#3178)
  fix(desktop): remove Projects overview card fills (#3416)
  feat(replica): portable heartbeat-token fence with snapshot-local reader routing (#3268)
  fix(git): channel binding tooling + author remediation for unbound repos (#3626)
  feat: configure S3 URL addressing style (#3400)
  feat: add first-class OpenRouter provider support (#1975)
  feat(agent,acp): wire provider total_tokens through NIP-AM publish chain (#3593)
  chore(release): release Buzz Desktop version 0.5.2 (#3624)
  ...

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Every session's first turn was systematically uncounted: the publisher
had no prior cumulative snapshot, so it emitted deltaReliable:false and
null turn.* fields.  For sessions that buzz-acp itself created via
session/new, prior usage is zero by definition — the zero baseline IS
the correct starting point.

Add UsageTracker::seed_zero_baseline(), called from pool.rs immediately
after create_session_and_apply_model() succeeds (both channel and
heartbeat paths).  The zero baseline seeds SessionState with
last_input/output=0 and last_cost=Some(0.0), so the first
usage_update notification computes turn.*=cumulative.* and emits
deltaReliable:true.

The re-attach path (session ID already in agent.state.sessions) is
unchanged: buzz-acp did not create that session and its prior usage is
genuinely unknown, so the existing fail-closed behavior is correct.

seed_zero_baseline() is a no-op if a baseline already exists, guarding
against accidental double-seeding across session rotation.

Regression tests added (a)-(d) per task spec:
(a) self-spawned first turn: deltaReliable=true, turn.*==cumulative.*
(b) re-attach first turn: unchanged fail-closed behavior
(c) second turn unaffected in both modes
(d) wire-frame payload field assertions for the spawned path

Gates at tip: cargo test -p buzz-acp 645/645, cargo fmt --check clean,
cargo clippy -p buzz-acp --all-targets -- -D warnings clean.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…-baseline

* origin/main:
  Render mobile agent mention chips (#3702)
  fix(catalog): update Amp description (#3758)
  fix(acp): preserve truncated thread context (#3340)

Signed-off-by: npub1g8493u0xfsjrvflg4n08ezd7vec99mnwzlv0qgwpr9d7gvjwhuzqx59rhw <41ea58f1e64c243627e8acde7c89be667052ee6e17d8f021c1195be4324ebf04@buzz.block.builderlab.xyz>
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
The daily-bars chart labeled each bar with its token total and nothing
identified which day a bar covered, so a multi-day range was unreadable.
Bars now carry the date on the x-axis, the compact token value on the bar
itself, and a per-bar tooltip with the exact total/input/output split.
Each tooltip field reports "unknown" independently rather than collapsing
an uncounted value to zero.

Range selection grows from 7d/30d to 1d/7d/30d plus a custom picker over
arbitrary start/end dates. Custom endpoints are civil dates parsed to
local midnight so DST transitions and skipped civil dates cannot produce
duplicate or non-increasing bucket boundaries. The picker caps a range at
one year, which is why the backend boundary arity now admits up to 367
values (366 days plus the closing boundary).

Above 14 buckets the chart drops on-bar value text and thins date ticks
instead of overflowing; the tooltip stays complete, so no detail is lost.

The usage section moves below the agents and teams sections, which are
the primary reason to open the view.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…chive

* origin/main:
  fix(desktop): reuse profiles when joining communities (#2155)
  Render mobile agent mention chips (#3702)
  fix(catalog): update Amp description (#3758)
  fix(acp): preserve truncated thread context (#3340)

Signed-off-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
wpfleger96 pushed a commit that referenced this pull request Jul 30, 2026
@wpfleger96

Copy link
Copy Markdown
Member Author

Screenshots for the dated-bars chart, the range filter, and the nav reorder.

Overview usage section

The default collapsed card on the Agents page: daily bars plus the per-agent totals row.
01-overview-usage-section

Focused usage view

Per-agent drilldown in the profile panel, with the model/harness breakdown.
02-focused-usage-view

Dated daily bars

Each bar now carries its date on the x-axis and its compact token total above it; a zero-token day renders a labeled 0 and a day with no reports renders no bar at all.
03-daily-bars-multi-day

Empty state

Collection is on but nothing has been archived in the window yet.
04-empty-state

Bar hover tooltip

Hovering a bar shows the exact total/input/output split for that day. Each field reports "unknown" independently rather than collapsing an uncounted value to zero.
05-bar-hover-tooltip

Custom range picker

The Custom tab opens an inclusive start/end date picker. The range is capped at 366 days in the picker itself, so the backend's fail-closed arity check is never the UX error path.
06-custom-range-picker

Usage section last in the nav order

The usage section now renders below the agent cards and the teams section.
07-usage-section-last-in-nav

npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 and others added 2 commits July 30, 2026 14:34
…gent-usage-archive

* commit '3695674c264a6fee70b27f4837bb65d313e57ede':
  fix(buzz-acp): seed zero baseline for self-spawned sessions

Signed-off-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
…chive

* origin/main:
  Fix video reviews in thread replies (#3719)
  feat(release): make desktop releases immutable (#3568)
  Make relay reconnect backoff authoritative (#3774)
  feat(desktop): add password-protected backups in settings (#3701)

Signed-off-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
@wpfleger96
wpfleger96 marked this pull request as ready for review July 30, 2026 21:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant