[codex] collect claude usage events#30
Conversation
578e143 to
0192d87
Compare
- add native Claude UsageEvent collection from assistant message usage records - cover assistant timestamps, cross-day events, streaming dedupe, and subagent discovery - close EBTA-004 with workspace review and verification evidence
0192d87 to
bac32aa
Compare
There was a problem hiding this comment.
Pull request overview
Adds native Claude UsageEvent collection so event-based token aggregation can use assistant-message timestamps and per-message usage deltas, while preserving existing Claude session aggregation behavior.
Changes:
- Implement
(*claude.Provider).CollectUsageEventsplusparseUsageEventsto emit deduped, timestamped usage events from Claude JSONL logs. - Add focused tests covering assistant timestamps, cross-day events, streaming dedupe semantics, no-ID uniqueness, and subagent path discovery.
- Add EBTA-004 workspace artifacts and mark EBTA-004 as done in the task plan.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
provider/claude/parser.go |
Refactors path discovery into a shared helper and adds native usage-event parsing + collection for Claude JSONL files. |
provider/claude/parser_test.go |
Adds unit tests for Claude usage-event parsing, dedupe rules, and subagent path inclusion. |
docs/plans/2026-04-16-event-based-token-aggregation-task.md |
Updates the EBTA task board entry to mark EBTA-004 as done and logs timeline notes. |
workspace/EBTA-004/test_strategy.md |
Captures the test plan used for EBTA-004 implementation/verification. |
workspace/EBTA-004/review.md |
Records review outcome and follow-up test hardening for file-order dedupe semantics. |
workspace/EBTA-004/research_review.md |
Summarizes research/review decisions and accepted constraints for the implementation. |
workspace/EBTA-004/original_impl.md |
Documents pre-change Claude parsing behavior and constraints relevant to EBTA-004. |
workspace/EBTA-004/new_impl.md |
Documents the implementation plan for adding Claude native usage events. |
workspace/EBTA-004/final_impl_v1.md |
Records the intended final design (v1) for Claude native usage events. |
workspace/EBTA-004/final_impl.md |
Records the finalized intended behavior/constraints for EBTA-004. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| func tokenUsageFromClaudeUsage(usage *claudeUsage) provider.TokenUsage { | ||
| if usage == nil { | ||
| return provider.TokenUsage{} | ||
| } | ||
| return provider.TokenUsage{ | ||
| InputOther: usage.InputTokens, | ||
| InputCacheRead: usage.CacheReadInputTokens, | ||
| InputCacheCreate: usage.CacheCreationInputTokens, | ||
| Output: usage.OutputTokens, | ||
| } |
There was a problem hiding this comment.
tokenUsageFromClaudeUsage centralizes the Claude->TokenUsage field mapping, but parseSession still duplicates that mapping inline. To prevent the two code paths from drifting over time (sessions vs. native events), consider reusing this helper in parseSession as well.
| func tokenUsageFromClaudeUsage(usage *claudeUsage) provider.TokenUsage { | |
| if usage == nil { | |
| return provider.TokenUsage{} | |
| } | |
| return provider.TokenUsage{ | |
| InputOther: usage.InputTokens, | |
| InputCacheRead: usage.CacheReadInputTokens, | |
| InputCacheCreate: usage.CacheCreationInputTokens, | |
| Output: usage.OutputTokens, | |
| } | |
| func tokenUsage(inputOther, inputCacheRead, inputCacheCreate, output int) provider.TokenUsage { | |
| return provider.TokenUsage{ | |
| InputOther: inputOther, | |
| InputCacheRead: inputCacheRead, | |
| InputCacheCreate: inputCacheCreate, | |
| Output: output, | |
| } | |
| } | |
| func tokenUsageFromClaudeUsage(usage *claudeUsage) provider.TokenUsage { | |
| if usage == nil { | |
| return provider.TokenUsage{} | |
| } | |
| return tokenUsage( | |
| usage.InputTokens, | |
| usage.CacheReadInputTokens, | |
| usage.CacheCreationInputTokens, | |
| usage.OutputTokens, | |
| ) |
| continue | ||
| } | ||
| events = append(events, parsed...) | ||
| } |
There was a problem hiding this comment.
CollectUsageEvents appends per-file event slices without sorting the combined result. Because discovery order can vary (e.g., across baseDirs or future changes in collectPaths), this can make the returned event list non-deterministic even though parseUsageEvents sorts within a file. Consider sorting the final events slice (e.g., by Timestamp/SourcePath/EventID) before returning, similar to cursor's CollectUsageEvents.
| } | |
| } | |
| sort.Slice(events, func(i, j int) bool { | |
| if !events[i].Timestamp.Equal(events[j].Timestamp) { | |
| return events[i].Timestamp.Before(events[j].Timestamp) | |
| } | |
| if events[i].SourcePath != events[j].SourcePath { | |
| return events[i].SourcePath < events[j].SourcePath | |
| } | |
| return events[i].EventID < events[j].EventID | |
| }) |
Summary
UsageEventcollection from local assistantmessage.usageJSONL records.CollectSessionsbehavior while reusing Claude path discovery for top-level and subagent files.Behavior
Claude native events now keep assistant-message timestamps, source paths, model/session/title metadata, and the same token-field mapping used by session aggregation. Streaming duplicate records are deduplicated by
message.id + requestId, with the latest file record winning; records without either ID remain unique.Verification
make fmtgo test -count=1 ./provider/claude -run 'Test(ParseClaudeUsageEvents|CollectClaudeUsageEvents)'go test -count=1 -race -cover ./provider/claudemake testmake vetmake buildmake lintNotes
make testwas wrapped with heartbeat output because the e2e phase is quiet for several minutes in this environment; the underlying command remained the repositorymake testtarget.