feat(web): per-backend spend on the fleet rail - #334
Open
saucam wants to merge 3 commits into
Open
Conversation
First buildable slice of P5.4 (docs/conductor-frontends-design.md §9, §7). The rail's cost figure now opens into a per-backend split — which backend is most of the bill, how many of its sessions are working, and what it has spent. §7 calls this a metaharness differentiator rather than a nicety, and the reason is structural: no single-vendor tool needs a per-backend breakdown because it only ever has one backend. §9 asks for exactly this zoom above a single session. The rollup is pure (`lib/fleet-economics.ts`), so the part worth testing needs no reactive root. Three decisions worth naming: **The daemon's `agg.totalCostUsd` stays the headline number** rather than a sum of these rows. It counts tasks that may have aged off the capped client board, so a locally derived total would drift low exactly on the long-lived fleets where spend matters. This table answers a different question — how the spend SPLITS — and is scoped to the sessions the board actually describes. **A session with no `providerId` is reported as `unknown`**, never folded into the default backend. Attributing spend to a provider that may not have incurred it is worse than admitting the gap, in a view whose entire purpose is comparing backends against each other. **Share is zero when nothing has been spent, never an even split.** Dividing by a zero total to show four backends at 25% would invent a fact from an absence. Sessions with no usage still COUNT while contributing no spend, so a fleet of ten idle sessions reads as ten sessions at $0 rather than as an empty fleet. `active` counts only genuinely in-flight turns — a session at `waiting_approval` is stopped, and belongs in the attention queue, not the concurrency number. Collapsed by default: it answers a question you ask occasionally, and the lanes answer the one you ask constantly. 9 economics tests; 252 web tests; typecheck, lint and build clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review follow-up on #334 (Oracle suggestion 2). `"unknown"` is a sentinel that reaches the UI, and it was a bare literal in two places — the rollup that produces it and the test that asserts on it. Exported rather than file-local so a consumer that wants to style or filter the bucket compares against the constant instead of re-typing the string; the test now imports it, which is what makes this more than ceremonial. 266 web tests; typecheck, lint and build clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
| row.costUsd += u.totalCostUsd; | ||
| } | ||
| byId.set(id, row); | ||
| } |
There was a problem hiding this comment.
Consider using Intl.NumberFormat for share percentages
While the current implementation returns a 0-1 ratio which is correct for the data layer, consider adding a helper for UI formatting that uses Intl.NumberFormat with percentage style. This would provide consistent localization (e.g., 25% vs 25,00%) across the application.
Suggested fix:
Suggested change
| } | |
| export function formatShare(share: number): string { | |
| return new Intl.NumberFormat(undefined, { | |
| style: 'percent', | |
| minimumFractionDigits: 1, | |
| maximumFractionDigits: 1, | |
| }).format(share); | |
| } |
rsharath
approved these changes
Sep 13, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
First buildable slice of P5.4 (§9, §7). The rail's cost figure now opens into a per-backend split — which backend is most of the bill, how many of its sessions are working, what it has spent.
§7 calls this a metaharness differentiator rather than a nicety, and the reason is structural: no single-vendor tool needs a per-backend breakdown, because it only ever has one backend.
Three decisions worth reviewing
The daemon's
agg.totalCostUsdstays the headline number, not a sum of these rows. It counts tasks that may have aged off the capped client board, so a locally derived total would drift low exactly on the long-lived fleets where spend matters. This table answers a different question — how the spend splits — scoped to the sessions the board actually describes.A session with no
providerIdis reported asunknown, never folded into the default backend. Attributing spend to a provider that may not have incurred it is worse than admitting the gap, in a view whose entire purpose is comparing backends against each other.Share is zero when nothing has been spent — never an even split. Dividing by a zero total to show four backends at 25% would invent a fact from an absence.
Two smaller ones: sessions with no usage still count while contributing no spend (ten idle sessions read as ten sessions at $0, not an empty fleet), and
activecounts only genuinely in-flight turns — a session atwaiting_approvalis stopped and belongs in the attention queue, not the concurrency number.Collapsed by default: it answers a question you ask occasionally; the lanes answer the one you ask constantly.
P5.4 triage — why this piece first
I checked the wire before picking. Of §12's five P5.4 items:
Buildable today (pure client):
providerId+SessionUsageare both onSessionInforoleplus the task→session joinFleetEventWirecarriestype+createdAtBlocked on daemon/protocol work:
FleetTaskWirehas onlycreatedAt, noupdatedAtor transition history, so state transitions can't be reconstructed client-side. An event timeline is buildable; the scrubber §4 describes is not.Happy to take the tree/map lens next, or to spec the protocol additions the blocked three would need.
Verification: 9 economics tests, 252 web tests total, typecheck / lint / build clean.
🤖 Generated with Claude Code