Skip to content

Commit ca33db1

Browse files
committed
fix: stabilize snapshot attribution and scheduler retries
Cross-session snapshot selection can take complete token and cost data from a later transcript, but report materialization still credited that later session and equal snapshots depended on scan order. Preserve earliest-session attribution through every report backend and use session identity to settle ranking ties. An exhausted embedding backstop retry could also be restarted by every periodic tick, keeping a detached daemon alive and making its regression timing-dependent. Latch that exhausted reconciliation until a real new mutation arrives.
1 parent 1bef13d commit ca33db1

17 files changed

Lines changed: 232 additions & 40 deletions

cmd/agentsview/embed_scheduler.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,7 @@ func (s *embedScheduler) Run(ctx context.Context) {
166166
// instead of retrying on the debounce interval. It is read and written only
167167
// from this single goroutine, so it needs no synchronization of its own.
168168
var pendingBackstop bool
169+
var backstopRetryExhausted bool
169170
var pendingRelease func()
170171
var buildErrorRetries int
171172
defer func() {
@@ -192,6 +193,7 @@ func (s *embedScheduler) Run(ctx context.Context) {
192193
// A new mutation is a new work item, even if an earlier attempt is
193194
// still waiting to retry.
194195
buildErrorRetries = 0
196+
backstopRetryExhausted = false
195197
if pendingRelease == nil {
196198
pendingRelease = release
197199
} else {
@@ -217,6 +219,7 @@ func (s *embedScheduler) Run(ctx context.Context) {
217219
// retries and release the lease, but let the next fresh
218220
// notification carry Backstop: true instead of deferring the
219221
// full pass until the next periodic tick.
222+
backstopRetryExhausted = pendingBackstop
220223
buildErrorRetries = 0
221224
continue
222225
}
@@ -236,8 +239,15 @@ func (s *embedScheduler) Run(ctx context.Context) {
236239
pendingRelease = nil
237240
}
238241
pendingBackstop = false
242+
backstopRetryExhausted = false
239243
buildErrorRetries = 0
240244
case <-backstopC:
245+
if backstopRetryExhausted {
246+
// This full reconciliation already consumed its bounded retry.
247+
// Keep its intent for the next mutation without letting periodic
248+
// ticks restart the same failed work and retain the daemon forever.
249+
continue
250+
}
241251
if buildErrorRetries > 0 {
242252
// A failed work item already owns the retained lease and
243253
// retry timer. Fold the periodic reconciliation into it
@@ -276,6 +286,7 @@ func (s *embedScheduler) Run(ctx context.Context) {
276286

277287
release()
278288
pendingBackstop = false
289+
backstopRetryExhausted = false
279290
if pendingRelease != nil {
280291
// This successful full reconciliation also satisfies work
281292
// that was already pending before the backstop began. A

cmd/agentsview/embed_scheduler_test.go

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -426,27 +426,15 @@ func TestEmbedSchedulerBackstopTicksDoNotRestartPendingRetry(t *testing.T) {
426426
fake := &fakeEmbedManager{results: []fakeTryBuildResult{
427427
{started: true, err: buildErr},
428428
}}
429-
idled := make(chan struct{})
430-
ctx, cancel := context.WithCancel(t.Context())
431-
tracker := server.NewIdleTracker(5*time.Millisecond, func() {
432-
close(idled)
433-
cancel()
434-
})
435429
s := newEmbedScheduler(
436-
fake, 70*time.Millisecond, 20*time.Millisecond, false, tracker,
430+
fake, 20*time.Millisecond, 5*time.Millisecond, false, nil,
437431
)
438-
go s.Run(ctx)
432+
go s.Run(t.Context())
439433
defer s.Stop()
440434

441-
waitForSchedulerCondition(t, func() bool { return fake.callCount() >= 1 },
442-
"expected the initial backstop attempt")
443-
go tracker.Run(ctx)
444-
select {
445-
case <-idled:
446-
case <-time.After(500 * time.Millisecond):
447-
require.Fail(t,
448-
"frequent backstop ticks restarted the retry lifecycle and retained the idle lease")
449-
}
435+
waitForSchedulerCondition(t, func() bool { return fake.callCount() >= 2 },
436+
"expected the failed backstop and its bounded retry")
437+
time.Sleep(50 * time.Millisecond)
450438
assert.Equal(t, []vector.BuildRequest{
451439
{Backstop: true}, {Backstop: true},
452440
}, fake.callsSnapshot(), "one backstop work item should get one bounded retry")

docs/internal/session-format-sources.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -134,9 +134,11 @@ Grok section and remove the explicit registry exception in the coverage test.
134134
can persist several assistant records with one `(message.id, requestId)` pair
135135
while `usage.output_tokens` grows from an early partial count to the final
136136
billed count (observed examples included `5` then `631` and `6` then `798`).
137-
The session-usage and activity-report paths therefore keep the greatest
138-
output-token snapshot within each session before applying cross-session
139-
replay deduplication; equal snapshots retain their first occurrence.
137+
Usage reporting therefore keeps the greatest output-token snapshot for each
138+
message/request identity across the included sessions, attributes it to the
139+
earliest transcript, and then applies cross-session replay deduplication.
140+
Equal snapshots are selected deterministically by timestamp, session id, and
141+
message ordinal.
140142
Replaying the three captured sessions after this correction matched all
141143
transcript-visible output; each full-wire total remained 15 output tokens
142144
higher because Claude Code's separate session-title request is not persisted.

internal/activity/activity.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -777,10 +777,19 @@ func claudeSnapshotSurvivorSelection(
777777
// report's range, effective-end, complete-snapshot, and cross-session dedup
778778
// filters.
779779
func UsageSurvivorMask(start, end, effEnd time.Time, usage []UsageRow) []bool {
780-
mask, _ := usageSurvivorSelection(start, end, effEnd, usage)
780+
mask, _ := UsageSurvivorSelection(start, end, effEnd, usage)
781781
return mask
782782
}
783783

784+
// UsageSurvivorSelection returns both the survivor mask and the session that
785+
// should receive each surviving row. A complete snapshot can come from a
786+
// later transcript while retaining the earliest transcript's attribution.
787+
func UsageSurvivorSelection(
788+
start, end, effEnd time.Time, usage []UsageRow,
789+
) (mask []bool, attribution []string) {
790+
return usageSurvivorSelection(start, end, effEnd, usage)
791+
}
792+
784793
func usageSurvivorSelection(
785794
start, end, effEnd time.Time, usage []UsageRow,
786795
) (mask []bool, attribution []string) {

internal/activity/parity_pgtest_test.go

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -486,23 +486,25 @@ func assertParityForCase(
486486

487487
// assertDayMinuteFixtureSanity checks the day-minute report actually exercises
488488
// the fixture: a full day with peak concurrency 2, nine sessions, non-zero
489-
// cost, and exactly 14050 output tokens. The token total proves the
489+
// cost, and exactly 22550 output tokens. The token total proves the
490490
// synthetic-model usage row (9999 tokens) is excluded, the dedup pair
491-
// collapses to its earlier 500-token row, the subagent's and unique fork's
492-
// tokens count, and the replaying fork's do not -- not merely that the
493-
// backends agree on a wrong number -- so the deep-compare above extends those
494-
// guarantees, plus the zero-cost primary-model fallback, to PG and DuckDB.
491+
// keeps its complete 9000-token snapshot with earlier attribution, the
492+
// subagent's and unique fork's tokens count, and the replaying fork's do not --
493+
// not merely that the backends agree on a wrong number. The deep-compare above
494+
// extends those guarantees, plus the zero-cost primary-model fallback, to PG
495+
// and DuckDB.
495496
func assertDayMinuteFixtureSanity(t *testing.T, r activity.Report) {
496497
t.Helper()
497498
require.False(t, r.Partial, "fixture day must be a full day")
498499
require.Equal(t, 2, r.Peak.Agents, "fixture must reach peak concurrency 2")
499500
require.Equal(t, 9, r.Totals.Sessions, "fixture session count")
500501
require.Positive(t, r.Totals.Cost.Microdollars, "fixture must exercise cost")
501502
// 2400 (parity-a) + 1600 (parity-b) + 300 (parity-c; synthetic 9999 row
502-
// excluded) + 500 (parity-d wins the dedup) + 0 (parity-e deduped away;
503+
// excluded) + 9000 (parity-d receives parity-e's complete snapshot) +
504+
// 0 (parity-e deduped away;
503505
// parity-f zero-cost) + 250 (parity-sub) + 9000 (parity-fork, unique)
504-
// + 0 (parity-fork-replay deduped away) = 14050.
505-
require.Equal(t, 14050, r.Totals.OutputTokens,
506+
// + 0 (parity-fork-replay deduped away) = 22550.
507+
require.Equal(t, 22550, r.Totals.OutputTokens,
506508
"synthetic row excluded, dedup collapses, subagent and unique fork count")
507509

508510
bySession := map[string]activity.SessionRow{}
@@ -522,8 +524,8 @@ func assertDayMinuteFixtureSanity(t *testing.T, r activity.Report) {
522524
require.Contains(t, bySession, "parity-d")
523525
require.Contains(t, bySession, "parity-e")
524526
require.Contains(t, bySession, "parity-f")
525-
require.Equal(t, 500, bySession["parity-d"].OutputTokens,
526-
"dedup keeps the earlier whole-second duplicate's tokens")
527+
require.Equal(t, 9000, bySession["parity-d"].OutputTokens,
528+
"complete snapshot is attributed to the earlier session")
527529
require.Equal(t, 0, bySession["parity-e"].OutputTokens,
528530
"the later fractional duplicate is dropped")
529531
require.Equal(t, "model-x", bySession["parity-f"].PrimaryModel,

internal/db/activityreport.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -521,11 +521,11 @@ func (db *DB) activityReportUsageFrom(
521521
_, row.OutputTokens, _, _, _ = dailyUsageRowTokens(candidate.scan)
522522
baseRows[i] = row
523523
}
524-
mask := activity.UsageSurvivorMask(
524+
mask, attribution := activity.UsageSurvivorSelection(
525525
q.RangeStart, q.RangeEnd, q.EffectiveEnd, baseRows,
526526
)
527527
return materializeActivityReportUsageCandidates(
528-
candidates, mask, rateResolver,
528+
candidates, mask, attribution, rateResolver,
529529
)
530530
}
531531

@@ -667,13 +667,14 @@ func (db *DB) activityReportUsageCandidatesFrom(
667667
return nil, nil, err
668668
}
669669
return materializeActivityReportUsageCandidates(
670-
candidates, nil, rateResolver,
670+
candidates, nil, nil, rateResolver,
671671
)
672672
}
673673

674674
func materializeActivityReportUsageCandidates(
675675
candidates []activityReportUsageCandidate,
676676
mask []bool,
677+
attribution []string,
677678
rateResolver *export.PricingResolver,
678679
) ([]activity.UsageRow, *export.PricingBlock, error) {
679680
out := make([]activity.UsageRow, 0, len(candidates))
@@ -702,6 +703,9 @@ func materializeActivityReportUsageCandidates(
702703
costSource = export.CostSourceReported
703704
}
704705
row := candidate.row
706+
if attribution != nil {
707+
row.SessionID = attribution[i]
708+
}
705709
row.InputTokens = inputTok
706710
row.OutputTokens = outputTok
707711
row.CacheCreationTokens = cacheCrTok

internal/db/activityreport_test.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,7 @@ func TestGetActivityReport_PricingModelsOnlyIncludeDedupSurvivors(t *testing.T)
367367
s.Agent = "claude"
368368
s.StartedAt = Ptr("2026-06-16T10:31:00Z")
369369
s.EndedAt = Ptr("2026-06-16T10:31:00Z")
370+
s.IsAutomated = true
370371
})
371372
insertMessages(t, d, Message{
372373
SessionID: "later", Ordinal: 0, Role: "assistant", Content: "x",
@@ -380,6 +381,16 @@ func TestGetActivityReport_PricingModelsOnlyIncludeDedupSurvivors(t *testing.T)
380381
dayQuery(t, "2026-06-16", "UTC"))
381382
require.NoError(t, err)
382383
assert.Equal(t, 900, r.Totals.OutputTokens)
384+
assert.Equal(t, r.Totals.Cost, r.Totals.InteractiveCost)
385+
assert.Zero(t, r.Totals.AutomatedCost.Microdollars)
386+
bySession := make(map[string]activity.SessionRow, len(r.BySession))
387+
for _, session := range r.BySession {
388+
bySession[session.SessionID] = session
389+
}
390+
require.Contains(t, bySession, "earlier")
391+
require.Contains(t, bySession, "later")
392+
assert.Equal(t, 900, bySession["earlier"].OutputTokens)
393+
assert.Zero(t, bySession["later"].OutputTokens)
383394
require.NotNil(t, r.Pricing)
384395
assert.Contains(t, r.Pricing.Models, "complete-model")
385396
assert.NotContains(t, r.Pricing.Models, "partial-model")

internal/db/reporting_export.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -422,13 +422,15 @@ func finalizeReportingUsage(
422422
rows []activity.UsageRow,
423423
) ([]activity.UsageRow, error) {
424424
sortReportingUsage(rows)
425-
mask := activity.UsageSurvivorMask(
425+
mask, attribution := activity.UsageSurvivorSelection(
426426
query.RangeStart, query.RangeEnd, query.EffectiveEnd, rows,
427427
)
428428
survivors := make([]activity.UsageRow, 0, len(rows))
429429
for i, keep := range mask {
430430
if keep {
431-
survivors = append(survivors, rows[i])
431+
row := rows[i]
432+
row.SessionID = attribution[i]
433+
survivors = append(survivors, row)
432434
}
433435
}
434436
return allocateReportingUsageCosts(survivors)

internal/db/reporting_export_test.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -869,6 +869,51 @@ func TestFinalizeReportingUsageOrdering(t *testing.T) {
869869
}
870870
}
871871

872+
func TestFinalizeReportingUsageAttributesCompleteSnapshotToEarliestSession(t *testing.T) {
873+
query := activity.Query{
874+
RangeStart: time.Date(2026, 7, 28, 9, 0, 0, 0, time.UTC),
875+
RangeEnd: time.Date(2026, 7, 28, 10, 0, 0, 0, time.UTC),
876+
EffectiveEnd: time.Date(2026, 7, 28, 10, 0, 0, 0, time.UTC),
877+
}
878+
rows := []activity.UsageRow{
879+
{
880+
SessionID: "earlier-parent",
881+
MessageOrdinal: 1,
882+
UsageSource: "message",
883+
Timestamp: "2026-07-28T09:05:00Z",
884+
Model: "model-a",
885+
OutputTokens: 100,
886+
Cost: money.Money{Microdollars: 1000},
887+
CostSource: export.CostSourceReported,
888+
Priced: true,
889+
Contributes: true,
890+
ClaudeMessageID: "shared-message",
891+
ClaudeRequestID: "shared-request",
892+
},
893+
{
894+
SessionID: "later-child",
895+
MessageOrdinal: 1,
896+
UsageSource: "message",
897+
Timestamp: "2026-07-28T09:06:00Z",
898+
Model: "model-a",
899+
OutputTokens: 900,
900+
Cost: money.Money{Microdollars: 9000},
901+
CostSource: export.CostSourceReported,
902+
Priced: true,
903+
Contributes: true,
904+
ClaudeMessageID: "shared-message",
905+
ClaudeRequestID: "shared-request",
906+
},
907+
}
908+
909+
survivors, err := finalizeReportingUsage(query, rows)
910+
require.NoError(t, err)
911+
require.Len(t, survivors, 1)
912+
assert.Equal(t, "earlier-parent", survivors[0].SessionID)
913+
assert.Equal(t, 900, survivors[0].OutputTokens)
914+
assert.Equal(t, money.Money{Microdollars: 9000}, survivors[0].Cost)
915+
}
916+
872917
func reportingUsageBreakdownKeys(
873918
rows []export.ReportingUsageBreakdown,
874919
) []string {

internal/db/usage.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1139,7 +1139,7 @@ func snapshotRankedDailyUsageRowsSQL(
11391139
ROW_NUMBER() OVER (
11401140
PARTITION BY claude_message_id, claude_request_id
11411141
ORDER BY snapshot_output_tokens DESC, ts ASC,
1142-
COALESCE(message_ordinal, -1) ASC
1142+
session_id ASC, COALESCE(message_ordinal, -1) ASC
11431143
) AS snapshot_rank
11441144
FROM usage_snapshot_window
11451145
WHERE claude_message_id != '' AND claude_request_id != ''

0 commit comments

Comments
 (0)