sessions: attribute PR spend per turn instead of per session#790
Merged
Conversation
added 4 commits
July 21, 2026 00:30
The by-PR surfaces attributed a session's full cost to every PR it referenced, so one orchestration session that touched many PRs rendered identical full-session rows. Capture per-turn PR references during transcript parsing and attribute each turn's cost to the PR set active at that turn (split evenly across a multi-PR merge-sweep turn), carrying the most recent PR set forward across turns that reference nothing. Turns before the first reference form an unattributed bucket. A session whose transcript expired before per-turn capture keeps its session-level prLinks but has no per-turn refs; it falls back to an even whole-session split, and any row carrying such a portion is flagged approximate. Rows are now summable: the CLI and app footers report the attributed sum plus the unattributed remainder instead of a distinct-session total. Bumps the session cache version so surviving transcripts re-parse and populate the new per-turn field. Daily-cache versioning is untouched.
Addresses the review findings on the per-turn PR attribution and adds the model and task-category surfaces. Correctness: - Cache migration: the 5 -> 6 session-cache bump now adopts the prior v5 file's expired-source PR entries instead of abandoning them, and the claude scan preserves and surfaces PR-bearing orphans, so a session whose transcript was deleted still appears as a legacy even-split instead of vanishing. The daily cache is untouched. - Date-range carry: the parser captures the PR set active at the start of the in-range turn slice and seeds the state machine with it, so a PR referenced before the window still owns its later, in-range, ref-less turns instead of the session falling back to a whole-session approx split. - Calls are split across a multi-PR turn by largest-remainder, keeping per-PR counts whole (a 1-call, 2-PR turn no longer renders as 2 calls). - The CLI and app footers reconcile to the rounded row values actually shown. - Distinct sessions are keyed by project + sessionId, not sessionId alone. - The app tolerates an older by-reference payload (no attributedCost): it keeps the old non-summable footer and never renders NaN. Features: - Each PR contribution records the models of its calls and the turn's task category (split by the same share on a multi-PR turn); legacy even-split rows carry the model union but no category breakdown. - Payload rows gain models (short names, cost-desc) and categories (label + cost, cost-desc, omitted when empty); the payload gains otherPrCount/otherPrCost for the PRs beyond the sent top 20. - CLI --by-pr gains a Models column. The desktop table is now full-width with a Models column and click-to-expand rows showing a per-category cost breakdown with proportional bars, keyboard accessible, with an "Other (N more PRs)" row when capped. Tests: state-machine seed/models/categories/largest-remainder/dedup, the prRefs round-trip through the real incremental-append path (continuation + straddle), v5 adoption of an expired PR session, payload round-trip, and the desktop expansion/models/old-payload cases.
Round-2 review follow-ups. - Mixed legacy/live category breakdown: a PR row that combines an expired legacy contribution (even-split, no turn data) with a live per-turn contribution now emits an explicit "Legacy estimate (no per-turn detail)" category carrying the legacy share, so the expansion reconciles to the row cost instead of silently dropping it. A legacy-only row still shows no breakdown. - v5 adoption is now per cached file: one malformed entry is skipped instead of rejecting the whole v5 cache and dropping every valid expired PR session. - The desktop "Other (N more PRs)" line moved to the table footer as a muted, separated summary rather than a sorted row. - Row expansion resets when the PR set changes (period/provider/data), so a stale expansion cannot linger on a row that is gone. - Payload models per row are capped to the top 4 by attributed cost, with a name-ascending tie-break; categories get the same stable tie-break. Tests: mixed live+legacy reconciliation, legacy-only omission, model cap and tie-break, corrupt-plus-valid v5 adoption, and the desktop expansion-reset.
…ion; app: reset PR expansion on period switch A v5 entry that was valid except for a malformed optional field (agentType, failed) passed per-file validation and flowed downstream as a non-string. The expansion reset keyed only on the row URL set, so a period switch that returned the same PRs kept a stale expansion open over changed numbers.
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.
Problem
The "Spend by pull request" surfaces (CLI
codeburn sessions --by-pr, thepullRequestspayload, and the desktop PR tab) attributed spend by reference: a session counted its full cost toward every PR it referenced. One long orchestration session that touched many PRs rendered a wall of identical rows (same full-session cost, same call count on every PR), which is useless for seeing where money actually went.Core change: per-turn attribution
Attribution is now per turn. During transcript parsing, PR references are captured per turn (
prRefson the cached turn shape), reusing the existingpr-linkextraction; the session-levelprLinksstays the union. A pure, unit-tested state machine (attributeSessionPrSpend) walks a session's turns in order, carryingcurrentPrSet= the PR set of the most recent turn that referenced any PR. Each turn's cost/calls are attributed to that set, split evenly across a multi-PR merge-sweep turn. Turns before the first reference form an unattributed bucket. Rows are now summable; the CLI and app footers report the attributed sum plus the unattributed remainder.A session whose transcript expired before per-turn capture keeps its session-level
prLinksbut has no per-turn refs; it falls back to an even whole-session split, flaggedapprox(~prefix + tooltip).Range inclusion still keys a turn on its first call timestamp; that is the pre-existing boundary model for date filtering, unchanged by this PR.
Review fixes folded into this PR
NaN.Second-round review follow-ups
New surfaces: models and categories
Each PR contribution records the models of its calls and the turn's task category (split by the same share on a multi-PR turn); legacy even-split rows carry the model union but no category breakdown (none can be honestly assigned). Payload rows gain
models(short names, cost-desc) andcategories(label + cost, cost-desc, omitted when empty). The CLI--by-prgains a Models column. The desktop table is now full-width with a Models column and click-to-expand rows showing a per-category cost breakdown with proportional bars, keyboard accessible, with the PR link still opening independently.Validation
Unit and integration tests cover the state machine (interleaved parallel PRs, multi-ref split, pre-reference overhead, range-carry seed, legacy fallback, largest-remainder calls, project+sessionId dedup, models and categories), the
prRefsround-trip through the real incremental-append path (continuation merge and straddle fallback), the version-bump adoption of an expired PR session, the payload round-trip, and the desktop expansion / models / old-payload cases. Root suite, app suite, andtscare green.Real-data drive on a frozen snapshot of a heavy orchestration session that shipped roughly thirty PRs, comparing the old and new engines from the same transcripts: