Skip to content

Fix subagent accounting and expose native-agent spend in existing Burn views - #2

Merged
kalindudc merged 7 commits into
kalindudc:mainfrom
architawr:fix/claude-subagent-usage
Oct 30, 2026
Merged

kalindudc merged 7 commits into
kalindudc:mainfrom
architawr:fix/claude-subagent-usage

Conversation

@architawr

@architawr architawr commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Claude Code's event-only report omits native subagent transcripts. Native Codex child events can reuse the parent's provider session ID, mixing independent cumulative counters. BB workflow workers use lifecycle ownership rather than parentage, so their spend can appear in global Spend while being absent from the origin thread's family total.

This PR fixes those accounting paths and extends the existing popover/details UI so users can see the native-agent subset, including agents inside child BB threads, without adding their spend twice.

Accounting

  • Read Claude transcripts on the execution host, including recursive subagent/workflow files. Repeated API-message snapshots replace one another; transcript totals replace BB events.
  • Exclude copied fork history using source-session message IDs and fork creation time, including rewritten session IDs.
  • Separate native Codex cumulative counters by scoped child turns and persist the mapping. Parent 100, child 80, parent 220 totals 300 rather than 320.
  • Include lifecycle-owned workflow workers in family reports, with deduplication and cycle protection.
  • Expose optional per-agent cost parts and coverage through reports/RPC, with discounts applied to those parts. Native-agent spend remains a subset of its owning thread.

Existing UI, with additions

  • Keep the chip's appearance and own-thread headline.
  • Add one Subagents line to the existing popover, plus a hover/focus/click calculation on its large amount. The large amount has a muted dotted underline; the With children calculation section is omitted when there are no reported BB child threads. Opening the popover focuses its dialog container so the calculation stays closed until the user hovers, clicks or tabs to the amount.
  • Add a third With agents KPI after With children. This is the native-agent subset already in those totals, not an extra charge.
  • Add expandable native-agent rows under both the root and child threads in the existing family table.
  • Refresh parent views on descendant-only changes; include latest-prompt spawned workers in the existing + without replaying each old child's last turn.

The demo shows $4.82 in this thread, $6.64 with children and $1.80 of that amount in native agents, including $0.40 inside a child.

Popover

Popover with one added Subagents line

Calculation on the large amount

Calculation tooltip separating own-thread and family scopes

Details and a child's own native agent

Existing details layout with third KPI and nested agents

Calculation without children

Only the own-thread calculation is shown when no BB child threads exist

Screenshots render the implemented React components in an isolated host harness with demonstration data; no private chat content is published. UI semantics, source coverage and review notes.

Validation

  • Full test suite, typecheck, diff check and BB plugin build pass (521 tests).
  • Installed locally and exercised through Burn's actual usage RPC.
  • Root → child → lifecycle-owned grandchild with native agents at every level: 6 unique calls, no duplicate/fork-history charges; repeated reads unchanged.
  • Four frozen Claude corpora retain the same costs, tokens and call counts after the new native-attribution fields.
  • Component regressions cover tooltip interactions, per-kind discounts, child-native rows, child-only refresh and preventing old worker deltas from replaying.
  • Latest UI follow-up: 43 focused chip/tooltip tests and typecheck pass; browser checks confirm the dotted underline and absence/presence of the child section. Native agents alone do not create a child section; a reported zero-cost child still does. Build/reload succeeded with settings preserved.
  • Follow-up autofocus regression: reproduced the unintended open tooltip with the real Radix popover; 40 focused chip/tooltip tests pass after the fix, including opening, explicit hover/focus and reopening. Typecheck and plugin build pass.
  • Earlier live Codex workflow/native-child smoke checks confirmed ownership/counter separation. Frozen Claude totals were cross-checked with CodexBar 0.56.3; three scenarios matched, and a fourth difference was reproduced as its metadata provider-filter false positive.
  • OpenCode and Pi/API regression tests pass; no new live OpenCode or paid API calls were made. Private transcripts/audit inputs are excluded.

Limits reviewers should know

Per-turn worker attribution uses the originating ancestor's creation time. Later independent tasks sent to an old worker remain in family totals but cannot safely be assigned to a new parent prompt. Claude native background calls retain timestamp grouping across prompts. The delta is therefore not an exact causal trace for arbitrary reused/concurrent agents.

Old mixed Codex ledger records are not rewritten; child-model/service-tier pricing depends on BB telemetry. Missing native breakdown shows Unavailable or a partial lower bound, never an invented zero. Deleted/external logs cannot be recovered. OpenCode depth/message caps and Pi custom-directory discovery remain unchanged. Direct API calls outside supported telemetry are not intercepted. Existing SDK-version/retired-slot build warnings are outside this change. See docs/usage-coverage.md and docs/native-agent-ui.md.

@architawr architawr changed the title Fix Claude/Codex subagent usage and BB workflow attribution Fix subagent accounting and expose native-agent spend in existing Burn views Oct 28, 2026
@kalindudc
kalindudc self-requested a review October 29, 2026 21:40

@kalindudc kalindudc left a comment •

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the contribution. This is a great change. A few things. This moves Claude Code spend to a host transcript reader, separates Codex child counters, counts lifecycle-owned workflow workers, and shows the native-agent subset in the existing popover, KPIs and table. The accounting model is sound. Beyond two code issues, this needs a UI-scope pass: the popover has gained a calculation that belongs in the side panel, and provider names leak into generic UI copy.

I reviewed statically at bf8841d and ran no tests (no dependencies installed in my checkout), and I did not review the screenshots.

Praise: the incremental JSONL reader, the persisted Codex child-turn separation with its reload test, and the fallback that always carries a coverage note instead of an invented zero.

Drafted and reviewed with Shuttle

const children = new Map<string, number>();
for (const batch of chunks(stale, SDK_BATCH)) {
if (deps.descendantsOf) {
for (const [id, ids] of await deps.descendantsOf(batch)) children.set(id, ids.length);

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This now hides a failed descendant listing: descendantsOf catches the error and returns an empty map, threadMeta caches {at, meta: null} for the batch, and spend() drops every thread without meta for 60 seconds. Before this change the same failure failed the request instead of showing lower spend. Could this keep the previous meta or rethrow here, plus a test for the injected branch? The spend tests only cover the SDK path.

const state = await read(join(root, parent + ".jsonl"), parent, false);
if (!state) continue;
found = true;
for (const key of state.entries.keys()) inherited.add(key);

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

inherited reads only the parent's main transcript, but the report also counts the child's subagents/**/agent-*.jsonl, so copied subagent snapshots are excluded by timestamp only. If Claude copies that directory on fork, those calls are charged in both threads; the fork test covers only the main file. Feeding the parent's subagent trees into inherited, or a test proving they are not copied, would close this.

Comment thread extensions/bb/card.tsx
<div className="px-3 py-2.5">
<div className="flex items-baseline gap-2.5">
<span className="text-[26px] leading-tight font-semibold tracking-tight tabular-nums">{usageLabel(report)}</span>
<CostBreakdown answer={answer!}><span className="text-[26px] leading-tight font-semibold tracking-tight tabular-nums">{usageLabel(report)}</span></CostBreakdown>

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The popover is meant as a quick overview, and this calculation tooltip turns it into a second details view; let's keep the detailed calculation in the sidepanel. One scope in the added line also looks wrong: native agents are part of this thread and already count in its total, and child threads already have the with-children figure, so the Subagents line on line 287 should read report.agents rather than family.agents, which adds the children's agents on top.

Comment thread extensions/bb/server.ts Outdated
);
return { total, calls, estimated, children };
const all = [report, ...reported.map(r => r.report)];
const agentRows = all.flatMap((r, i) => r?.agents?.rows.map(a => ({...a, id: i + ":" + a.id})) ?? []);

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

agentRows is uncapped while the child list is capped at 50, and it crosses the usage RPC on every refresh although the client only sums it; hundreds of workflow workers means every worker's rows each time. Sending the family aggregate would bound it. Nit: the positional i + ":" ids change as costs reorder the list.

Comment thread extensions/bb/host/claude-sessions.ts Outdated
report.coverageNote =
missing || missingParent
? "Partial Claude transcript history: some session identities are unavailable."
: "Claude transcripts include saved subagents. Background calls are grouped by time; deleted or external API logs cannot be recovered.";

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Burn is provider-agnostic, but this note is rendered in the generic panel (panel.tsx:315) and names Claude. Saying what is missing in plain terms ("some saved subagent history is unavailable") and leaving provider names to the coverage doc would keep the UI neutral; the fallback note at sources.ts:440 ("BB events can omit subagents") has the same issue.

Comment thread docs/native-agent-ui.md Outdated

The details page gains the third KPI after With children, plus expandable native-agent rows under both the root thread and any child with identifiable native usage. Agent numbers are neutral labels; identifiers are available in the row title.

![Existing popover with one new native-agent line](../assets/screenshots/bb-agents-popover.jpg)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove the screenshots and the screenshot references. They are renders from a demo harness rather than the real UI, they add about 215 KB, and the surrounding text already describes the change. If a screenshot is wanted later, it would be better taken from the installed plugin.

Comment thread docs/native-agent-ui.md Outdated

`UsageReport.agents` is optional and backward compatible. It carries per-agent calls, cost parts, estimation flags and coverage. It survives validation, saved reports and RPC boundaries; discounts apply to the agent's actual cost parts. Family aggregation includes every reported descendant before the existing 50-child display cap.

- Claude: saved recursive subagent files supply native identity; copied fork history and repeated message snapshots are excluded. `available` describes the available session files, not a guarantee about deleted or external history.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This duplicates docs/usage-coverage.md, nothing links here, and lines 47-49 are PR validation notes. The provider source details on lines 34-37 also belong in usage-coverage.md so the UI doc stays provider-agnostic; one linked coverage doc with validation left in the PR would be easier to keep true.

Comment thread docs/usage-coverage.md Outdated

Native subagents can run inside one provider session. BB workflows instead spawn hidden worker threads with `lifecycleOwnerThreadId`; this is distinct from `parentThreadId`. BB's descendants API currently traverses parentage only. Burn extends the returned family over both edges, including nested workers and workers' ordinary children, with deduplication and cycle protection. The global Spend report continues to sum each thread's own report once. Native transcript calls belong to that own report and are not added again as BB worker reports.

A replayed workflow call does not create another worker or another usage record. Real retry attempts that create additional workers remain separate spend. This patch does not change workflow execution or retry policy.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This claim is about BB's workflow execution, which nothing in this repository verifies; citing BB's docs or dropping it avoids a stale statement about billing.

Comment thread docs/usage-coverage.md Outdated

The test suite covers the new reader, recursive workflow paths, duplicate snapshots, appended partial JSONL, unknown models, one-hour cache writes, copied fork history, rewritten files, host/event fallback, preserving saved history, lifecycle ownership, and interleaved Codex child counters. Existing OpenCode, Pi/API usage, budget, report and frontend suites remain in the full run.

On Node 26, run browser tests with `NODE_OPTIONS=--no-experimental-webstorage npm test`; otherwise Node's global Web Storage conflicts with jsdom in the existing suite. Typecheck uses the repository's pinned SDK. `bb plugin build` also checks the installed BB build pipeline; existing SDK-version and retired-slot warnings are outside this patch.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Node 26 test-runner flags and the validation summary are developer notes in a page linked as user-facing coverage; CONTRIBUTING fits better.

@architawr

Copy link
Copy Markdown
Contributor Author

Thanks for the review. The accounting/error-handling fixes are in ba5c40d; I've pushed a UI follow-up in 9548857.

I'd like to retain a compact, on-demand calculation on the amount in the chip popover. It answers “what makes up this cost, and how much do child threads add?” directly where the number is shown, without opening the side panel and interrupting the current task. It adds no permanently visible calculation rows: it opens only on hover, focus or click, and the trigger disappears entirely when there is nothing to break down. The side panel remains the place for per-agent rows and deeper exploration.

The scope is now explicit:

  • The chip calculation shows Main agent + Its subagents = This thread, followed by Child threads and Total with children when children exist.
  • In the side panel, This thread explains only that thread's main-agent/native-agent split. With children separately explains This thread + Child threads = the family total.
  • Child-thread amounts already include their own native subagents; those agents are never added again or shown as this thread's subagents. The popover's Subagents line uses report.agents, as you requested.
  • Only the amount is interactive/underlined, not the KPI title or budget. Missing native breakdowns show no Unavailable row or invented main-agent remainder. Partial known usage stays marked as a lower bound. With no relevant breakdown, the amount is plain text. A reported zero-cost child still counts as a child.

Validation: 45 combinations of own-agent coverage, child presence, child-native agents and calculation scope; full suite passes (586 tests), typecheck and plugin build pass. Installed/reloaded locally with settings preserved.

Would you be open to keeping this smaller conditional calculation in the chip popover?

@kalindudc kalindudc left a comment •

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Second round at 9548857, after the merge of main 0.8.3 and the review fixes. I am willing to accept the additional popover, it much cleaner than the previous. This will go out on the next release.

@kalindudc
kalindudc merged commit d9c2c21 into kalindudc:main Oct 30, 2026
kalindudc added a commit that referenced this pull request Oct 30, 2026
Burn now counts subagent spend correctly and is hardened across its readers, storage and report endpoint. Subagent accounting is by Artur Karapetyan (@architawr) in #2; OpenCode support remains by Nikolai Berezovskii (@brzvsk) in #1.

- Claude Code subagents are counted from host transcripts; Codex child counters stay separate; bb workflow workers count in their origin thread; native-agent spend is shown in the card, details and family table without being counted twice.
- Token and cost fields are coerced to finite numbers, each host report is validated on its own, and agent rows fold past 50, so one malformed or oversized report cannot break a batch or bb's KV cap.
- Session-log memory is bounded per log and across logs, with a cap on an unterminated line; host reads that a price change overtook are not saved or cached.
- A stale stored pi report no longer disables the server-side budget stop; POST /usage requires a per-thread token and rejects a missing one before reading the body.
- Price overrides price router aliases and contract models for Claude Code and Codex, and reprice held and saved reports.
- New threads retry a provisional session lookup after five seconds instead of waiting up to five minutes, with the retry capped after about a minute.
- The Where it went bar normalises parts to shares (it under-filled at costs below a dollar), shows an Unsplit slice for a total with no kinds, and stays hidden until a thread has a priced call.
- Burn page storage reads are bounded; OpenCode read failures are logged; the README's local install says to rebuild better-sqlite3 before tests.

610 tests pass in 41 files and typecheck is clean. Checked with a plugin build and a live install from the checkout: a report without a token was rejected while the session-log reader kept cost current, and a reloaded plugin restored its settings. Settings, stored keys and the plugin database's tables are the same as in 0.8.3; Price overrides defaults to empty, and reports gain optional native-agent detail that older versions ignore.
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.

2 participants