fix(core): roll up subagent cost into parent session and fix fork double-counting - #43645
fix(core): roll up subagent cost into parent session and fix fork double-counting#43645AndyS77 wants to merge 5 commits into
Conversation
…ble-counting - Zero out cost/tokens on cloned step-finish parts in fork() to prevent double-counting pre-fork spend in forked sessions (anomalyco#31032, anomalyco#36944) - Add Session.totalCost(sessionID) that BFS-walks descendant sessions via children() and aggregates cost + tokens (anomalyco#39740) - Add GET /session/:id/cost endpoint exposing aggregated cost - Add E2E tests: subagent cost rollup, fork with subagent history, nested child sessions, multiple child sessions Closes anomalyco#39740, anomalyco#31032, anomalyco#36944 Supersedes anomalyco#25712 Co-Authored-By: zai-glm-52 <noreply@ai.local> Agent: @bug-fix Scope: cost-subagent-rollup
- Add totalCost field to SessionData, updated via refreshCost effect - Footer formatUsage uses totalCost when available, falls back to info.cost - Context % remains per-session (info.tokens) — correct for context window - refreshCost fires on message.updated for parent session and on bootstrap - Calls GET /session/:id/cost endpoint (requires SDK regeneration) Co-Authored-By: zai-glm-52 <noreply@ai.local> Agent: @bug-fix Scope: cost-subagent-rollup
- stats: use totalCost() for top-level sessions (no parentID), skip child sessions to avoid double-counting - acp: sendUpdate fetches aggregated cost via session.cost endpoint, falls back to totalSessionCost(messages) if unavailable - messageLoaderFromSDK: add cost() method to MessageLoaderInterface Co-Authored-By: zai-glm-52 <noreply@ai.local> Agent: @bug-fix Scope: cost-subagent-rollup
…tent SDK cost method - Rename totalCost to subagentCost in SessionData (clarity: this is the aggregated child session cost, not the parent's own cost) - Footer cost = info.cost (parent) + data.subagentCost (children) - refreshCost uses existing session.children SDK endpoint instead of non-existent session.cost (requires Protocol registration for SDK gen) - stats: catch NotFoundError from totalCost - acp: revert to totalSessionCost(messages) until SDK supports cost - handler: map NotFoundError to ApiNotFoundError Co-Authored-By: zai-glm-52 <noreply@ai.local> Agent: @bug-fix Scope: cost-subagent-rollup
… layer - Add session.cost and session.children endpoints to protocol session group - Add children() and totalCost() to SessionV2 Interface + implementation - Add handlers in protocol SessionHandler - Regenerate both SDKs (client + legacy js) - Switch TUI refreshCost to SDK session.cost() endpoint - Switch ACP sendUpdate to SDK session.cost() with fallback - Rename SessionData.subagentCost to totalCost (holds server-aggregated total) - Footer uses totalCost when > 0, falls back to info.cost - Add E2E tests: aggregated cost across nested children, 404 for missing session - Update ACP usage test mock with cost method Co-Authored-By: zai-glm-52 <noreply@ai.local> Agent: @bug-fix Scope: cost-subagent-rollup
|
The following comment was made by an LLM, it may be inaccurate: Found Potential Duplicates/Related PRs
These are the most relevant matches. PR #25712 is particularly important as it appears to be the companion UI PR that would consume the cost aggregation API being implemented in #43645. |
|
Thanks for updating your PR! It now meets our contributing guidelines. 👍 |
|
Reviewed against a billing-reconciled session tree (root + 3 subagents, cross-checked against the provider's invoice — context in #45417): the BFS rollup with the visited set is correct, fork zeroing works, and the test coverage (nested / multi-child / fork scenarios) is good. One real issue in the stats aggregation:
Numbers from my reconciled tree (root $0.168 own + children $0.0417 / $0.0395 / $0.0396):
Suggested fix: contribute 0 from child rows to the grand total when their parent is present in the aggregated set (or only aggregate top-level rows). Per-session rows can still display each session's own cost. Two smaller notes:
|
|
Heads-up: this PR's fork-zeroing fixes the over-count (#36944) but doesn't cover the deleted-parent case (#48306). After zeroing, an orphan fork's inherited spend becomes unrecoverable even from message-level data. Consider preserving deleted-session aggregates (rollup) or reattributing inherited cost at fork time. |
Issue for this PR
Closes #39740
Closes #31032
Closes #36944
Type of change
What does this PR do?
Subagent session costs were not rolled up to the parent session (#39740), and forked sessions double-counted pre-fork costs (#31032, #36944). This PR fixes both issues and exposes the aggregated cost through the protocol layer so the TUI, stats, and ACP can display it.
Fork double-counting fix: When a session is forked, the cloned step-finish parts now have their cost and tokens zeroed out. The forked session starts with zero cost and only accumulates cost from its own LLM calls, not the parent's pre-fork history.
Cost rollup: Added
children()andtotalCost()to both V1Session.Serviceand V2SessionV2.Interface.totalCost()does a BFS walk across all descendant sessions, summing cost and tokens.Protocol endpoint: Added
session.costandsession.childrento the protocol session group (/api/session/:sessionID/cost,/api/session/:sessionID/children) with handlers in both the protocol handler (packages/server) and the server handler (packages/opencode). Both SDKs were regenerated.TUI:
refreshCosteffect instream.transport.tscallssdk.session.cost()onmessage.updatedand bootstrap.SessionData.totalCostfield; footerformatUsageusestotalCostwhen > 0, falls back toinfo.cost.Stats + ACP: Stats uses
totalCost()for top-level sessions. ACPsendUpdateusessdk.session.cost()withtotalSessionCost(messages)fallback.How did you verify your code works?
cost-rollup.test.ts): fork zeroing, own cost, child cost, nested children, multiple childrenusage.test.ts): mock updated withcostmethodhttpapi-session.test.ts): aggregated cost across nested children (parent -> child -> grandchild, verifying 0.06 = 0.01 + 0.02 + 0.03), 404 for missing sessionbun typecheckclean (0 source-code errors)bun run generateRelated PRs (used as reference)
Checklist