Skip to content

Commit c3256ac

Browse files
committed
fix(usage): align session totals with streaming snapshots
Claude streaming turns can persist partial and complete usage rows. Subtracting discarded partial output keeps the session total consistent with its cost and breakdown while preserving stored output that has no priceable row.
1 parent 9eaada4 commit c3256ac

6 files changed

Lines changed: 37 additions & 10 deletions

File tree

internal/db/usage.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3232,9 +3232,11 @@ func (db *DB) GetSessionUsage(
32323232
}
32333233
}
32343234
snapshotMask := activity.ClaudeSnapshotSurvivorMask(snapshotRows)
3235+
deduplicatedOutputTokens := 0
32353236
seen := make(map[usageDedupToken]struct{})
32363237
for i, r := range usageRows {
32373238
if !snapshotMask[i] {
3239+
deduplicatedOutputTokens += snapshotRows[i].OutputTokens
32383240
continue
32393241
}
32403242
if key, ok := usageDedupTokenForRow(
@@ -3301,7 +3303,7 @@ func (db *DB) GetSessionUsage(
33013303
SessionID: sess.ID,
33023304
Agent: sess.Agent,
33033305
Project: sess.Project,
3304-
TotalOutputTokens: sess.TotalOutputTokens,
3306+
TotalOutputTokens: max(sess.TotalOutputTokens-deduplicatedOutputTokens, 0),
33053307
PeakContextTokens: sess.PeakContextTokens,
33063308
HasTokenData: sess.HasTotalOutputTokens || sess.HasPeakContextTokens,
33073309
Models: sortedSetKeys(modelsSet),

internal/db/usage_test.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3889,6 +3889,8 @@ func TestGetSessionUsage_PrefersCompleteClaudeSnapshot(t *testing.T) {
38893889
insertSession(t, d, "claude:streamed", "proj", func(s *Session) {
38903890
s.Agent = "claude-code"
38913891
s.StartedAt = new("2026-05-20T10:00:00Z")
3892+
s.TotalOutputTokens = 636
3893+
s.HasTotalOutputTokens = true
38923894
})
38933895
insertMessages(t, d,
38943896
Message{
@@ -3907,6 +3909,7 @@ func TestGetSessionUsage_PrefersCompleteClaudeSnapshot(t *testing.T) {
39073909

39083910
u, err := d.GetSessionUsage(ctx, "claude:streamed", true)
39093911
requireNoError(t, err, "GetSessionUsage")
3912+
assert.Equal(t, 631, u.TotalOutputTokens)
39103913
assert.Equal(t, money.MustParseDollars("0.020775"), u.Cost)
39113914
require.Len(t, u.Breakdown, 1)
39123915
assert.Equal(t, 631, u.Breakdown[0].OutputTokens)

internal/duckdb/analytics_usage.go

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3516,7 +3516,16 @@ func duckUsageCTEFromRaw(
35163516
COALESCE(message_ordinal, -1) ASC
35173517
)
35183518
ELSE 1
3519-
END AS snapshot_rank
3519+
END AS snapshot_rank,
3520+
CASE
3521+
WHEN claude_message_id != '' AND claude_request_id != ''
3522+
THEN SUM(output_tokens_norm) OVER (
3523+
PARTITION BY session_id, claude_message_id, claude_request_id
3524+
) - MAX(output_tokens_norm) OVER (
3525+
PARTITION BY session_id, claude_message_id, claude_request_id
3526+
)
3527+
ELSE 0
3528+
END AS snapshot_deduplicated_output_tokens
35203529
FROM usage_windowed
35213530
),
35223531
usage_snapshot_survivors AS (
@@ -3640,6 +3649,7 @@ type duckUsageAggregateRow struct {
36403649
reportedCostRows int
36413650
authoritativeCost int64
36423651
authoritativeCostRows int
3652+
snapshotDedupOutput int
36433653
}
36443654

36453655
type duckSessionUsageRow struct {
@@ -4279,6 +4289,7 @@ func (s *Store) forEachSessionUsageAggregateRow(
42794289
display_name, started_at,
42804290
input_tokens_norm AS input_tokens,
42814291
output_tokens_norm AS output_tokens,
4292+
snapshot_deduplicated_output_tokens,
42824293
cache_create_norm AS cache_creation_tokens,
42834294
cache_read_norm AS cache_read_tokens,
42844295
CASE WHEN cost_microdollars IS NULL OR cost_source = 'copilot-reported' THEN input_tokens_norm ELSE 0 END AS billable_input_tokens,
@@ -4310,7 +4321,8 @@ func (s *Store) forEachSessionUsageAggregateRow(
43104321
&r.sessionID, &r.project, &r.agent, &r.model,
43114322
&r.priceModel, &r.source, &r.messageOrdinal,
43124323
&r.displayName, &startedAt,
4313-
&r.inputTok, &r.outputTok, &r.cacheCr, &r.cacheRd,
4324+
&r.inputTok, &r.outputTok, &r.snapshotDedupOutput,
4325+
&r.cacheCr, &r.cacheRd,
43144326
&r.billableInput, &r.billableOutput, &r.billableReason,
43154327
&r.billableCacheCr, &r.billableCacheRd,
43164328
&r.billableWebSearch,
@@ -4617,10 +4629,12 @@ func (s *Store) GetSessionUsage(
46174629
var totalCost money.Money
46184630
var authoritativeCost *money.Money
46194631
var hasComputedCost, hasReportedCost bool
4632+
deduplicatedOutputTokens := 0
46204633
hasRows := false
46214634
err = s.forEachSessionUsageAggregateRow(
46224635
ctx, db.UsageFilter{}, sessionID,
46234636
func(r duckUsageAggregateRow) error {
4637+
deduplicatedOutputTokens += r.snapshotDedupOutput
46244638
if r.authoritativeCostRows > 0 {
46254639
v := money.Money{Microdollars: r.authoritativeCost}
46264640
authoritativeCost = &v
@@ -4693,7 +4707,7 @@ func (s *Store) GetSessionUsage(
46934707
}
46944708
out := &db.SessionUsage{
46954709
SessionID: sessionID, Agent: sess.Agent, Project: sess.Project,
4696-
TotalOutputTokens: sess.TotalOutputTokens,
4710+
TotalOutputTokens: max(sess.TotalOutputTokens-deduplicatedOutputTokens, 0),
46974711
PeakContextTokens: sess.PeakContextTokens,
46984712
HasTokenData: hasRows || sess.HasTotalOutputTokens || sess.HasPeakContextTokens,
46994713
Models: sortedBoolKeys(models),

internal/duckdb/store_test.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2688,10 +2688,13 @@ func TestSessionUsagePrefersCompleteClaudeSnapshot(t *testing.T) {
26882688
`{"input_tokens":1000,"output_tokens":631}`)
26892689
second.OutputTokens = 631
26902690

2691+
session := syncSession(
2692+
"duck-streamed", "alpha", "streamed",
2693+
"2026-01-13T00:00:00.000Z", 2)
2694+
session.TotalOutputTokens = 636
2695+
session.HasTotalOutputTokens = true
26912696
_, err := local.WriteSessionBatchAtomic([]db.SessionBatchWrite{{
2692-
Session: syncSession(
2693-
"duck-streamed", "alpha", "streamed",
2694-
"2026-01-13T00:00:00.000Z", 2),
2697+
Session: session,
26952698
Messages: []db.Message{first, second},
26962699
DataVersion: 1,
26972700
ReplaceMessages: true,
@@ -2707,6 +2710,7 @@ func TestSessionUsagePrefersCompleteClaudeSnapshot(t *testing.T) {
27072710
got, err := store.GetSessionUsage(ctx, "duck-streamed", true)
27082711
require.NoError(t, err)
27092712
require.NotNil(t, got)
2713+
assert.Equal(t, 631, got.TotalOutputTokens)
27102714
assert.Equal(t, money.MustParseDollars("0.020775"), got.Cost)
27112715
require.Len(t, got.Breakdown, 1)
27122716
assert.Equal(t, 631, got.Breakdown[0].OutputTokens)

internal/postgres/usage.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1363,9 +1363,11 @@ func (s *Store) GetSessionUsage(
13631363
}
13641364
}
13651365
snapshotMask := activity.ClaudeSnapshotSurvivorMask(snapshotRows)
1366+
deduplicatedOutputTokens := 0
13661367
seen := make(map[pgUsageDedupToken]struct{})
13671368
for i, r := range usageRows {
13681369
if !snapshotMask[i] {
1370+
deduplicatedOutputTokens += snapshotRows[i].OutputTokens
13691371
continue
13701372
}
13711373
if key, ok := pgUsageDedupTokenForRow(
@@ -1432,7 +1434,7 @@ func (s *Store) GetSessionUsage(
14321434
SessionID: sess.ID,
14331435
Agent: sess.Agent,
14341436
Project: sess.Project,
1435-
TotalOutputTokens: sess.TotalOutputTokens,
1437+
TotalOutputTokens: max(sess.TotalOutputTokens-deduplicatedOutputTokens, 0),
14361438
PeakContextTokens: sess.PeakContextTokens,
14371439
HasTokenData: sess.HasTotalOutputTokens ||
14381440
sess.HasPeakContextTokens,

internal/postgres/usage_pgtest_test.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -561,10 +561,11 @@ func TestStoreGetSessionUsagePrefersCompleteClaudeSnapshot(t *testing.T) {
561561
) VALUES ('claude-opus-4-6', 5000000, 25000000, 6250000, 500000, 'seed');
562562
INSERT INTO sessions (
563563
id, machine, project, agent, started_at,
564-
message_count, user_message_count
564+
message_count, user_message_count,
565+
total_output_tokens, has_total_output_tokens
565566
) VALUES (
566567
'claude:streamed', 'test-machine', 'proj', 'claude-code',
567-
'2026-03-12T10:00:00Z'::timestamptz, 2, 1
568+
'2026-03-12T10:00:00Z'::timestamptz, 2, 1, 636, TRUE
568569
);
569570
INSERT INTO messages (
570571
session_id, ordinal, role, content, timestamp, content_length,
@@ -583,6 +584,7 @@ func TestStoreGetSessionUsagePrefersCompleteClaudeSnapshot(t *testing.T) {
583584
got, err := store.GetSessionUsage(ctx, "claude:streamed", true)
584585
require.NoError(t, err)
585586
require.NotNil(t, got)
587+
assert.Equal(t, 631, got.TotalOutputTokens)
586588
assert.Equal(t, money.MustParseDollars("0.020775"), got.Cost)
587589
require.Len(t, got.Breakdown, 1)
588590
assert.Equal(t, 631, got.Breakdown[0].OutputTokens)

0 commit comments

Comments
 (0)