Skip to content

Commit ffe2827

Browse files
committed
Fix 7 critical bugs from sweep
- Fix data race on collector.cancel between Start/Close (add cancelMu) - Guard TopSessions against negative n (panic on slice bounds) - Default to sonnet pricing when model string is empty - Fix Codex parser dedup: use two-generation set (prevents double-counting) - Fix parseTimestamp: return time.Now() on malformed input (prevents zero-time cascade into false stall alerts) - Fix token attribution: track tokensAssigned flag so malformed first content block doesn't shift tokens to wrong event - Handle ExportDoneMsg in Update switch (was falling through silently)
1 parent 37f6a06 commit ffe2827

5 files changed

Lines changed: 45 additions & 18 deletions

File tree

‎internal/app/model.go‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,9 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
8787
m.ready = true
8888
return m, nil
8989

90+
case model.ExportDoneMsg:
91+
return m, nil
92+
9093
case model.TickMsg:
9194
data := m.collector.Snapshot()
9295
m.sessions = data.Sessions

‎internal/metrics/session.go‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,11 @@ func (ss *SessionStore) ProcessEvent(sessionID string, event *parser.ParsedEvent
109109
}
110110

111111
if event.Tokens.Total() > 0 {
112-
cost := ss.costCalc.Calculate(event.Tokens, s.Model)
112+
model := s.Model
113+
if model == "" {
114+
model = "claude-sonnet-4-5"
115+
}
116+
cost := ss.costCalc.Calculate(event.Tokens, model)
113117
s.TotalCost += cost
114118
s.TotalInput += event.Tokens.InputTokens
115119
s.TotalOutput += event.Tokens.OutputTokens
@@ -186,6 +190,9 @@ func (ss *SessionStore) DailyTotal() float64 {
186190
}
187191

188192
func (ss *SessionStore) TopSessions(n int) []CostEntry {
193+
if n <= 0 {
194+
return nil
195+
}
189196
ss.mu.RLock()
190197
defer ss.mu.RUnlock()
191198

‎internal/parser/claude.go‎

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ func (p *ClaudeParser) parseAssistant(entry *claudeEntry) ([]*ParsedEvent, error
9797
}
9898

9999
var events []*ParsedEvent
100+
tokensAssigned := false
100101
for _, raw := range entry.Message.Content {
101102
var c claudeContent
102103
if err := json.Unmarshal(raw, &c); err != nil {
@@ -110,10 +111,10 @@ func (p *ClaudeParser) parseAssistant(entry *claudeEntry) ([]*ParsedEvent, error
110111
Timestamp: ts,
111112
EntryType: "assistant",
112113
Model: entry.Message.Model,
113-
Tokens: tokens,
114114
}
115-
if len(events) > 0 {
116-
ev.Tokens = TokenUsage{}
115+
if !tokensAssigned {
116+
ev.Tokens = tokens
117+
tokensAssigned = true
117118
}
118119

119120
switch c.Type {
@@ -215,7 +216,13 @@ func IsTestCommand(toolInput string) bool {
215216
}
216217

217218
func parseTimestamp(s string) time.Time {
218-
t, _ := time.Parse(time.RFC3339Nano, s)
219+
if s == "" {
220+
return time.Now()
221+
}
222+
t, err := time.Parse(time.RFC3339Nano, s)
223+
if err != nil {
224+
return time.Now()
225+
}
219226
return t
220227
}
221228

‎internal/parser/codex.go‎

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,15 @@ import (
66
)
77

88
type CodexParser struct {
9-
seenRequests map[string]bool
10-
seenCount int
9+
currentGen map[string]bool
10+
previousGen map[string]bool
11+
currentCount int
1112
}
1213

1314
func NewCodexParser() *CodexParser {
1415
return &CodexParser{
15-
seenRequests: make(map[string]bool, 256),
16+
currentGen: make(map[string]bool, 256),
17+
previousGen: make(map[string]bool),
1618
}
1719
}
1820

@@ -93,15 +95,16 @@ func (p *CodexParser) Parse(line []byte) ([]*ParsedEvent, error) {
9395
}
9496

9597
rid := entry.ID
96-
if rid != "" && p.seenRequests[rid] {
98+
if rid != "" && (p.currentGen[rid] || p.previousGen[rid]) {
9799
return nil, nil
98100
}
99101
if rid != "" {
100-
p.seenRequests[rid] = true
101-
p.seenCount++
102-
if p.seenCount > maxSeenRequests {
103-
p.seenRequests = make(map[string]bool, 256)
104-
p.seenCount = 0
102+
p.currentGen[rid] = true
103+
p.currentCount++
104+
if p.currentCount > maxSeenRequests {
105+
p.previousGen = p.currentGen
106+
p.currentGen = make(map[string]bool, 256)
107+
p.currentCount = 0
105108
}
106109
}
107110

‎internal/source/collector.go‎

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ type Collector struct {
2525
mu sync.Mutex
2626
alerts []model.Alert
2727

28-
cancel context.CancelFunc
28+
cancelMu sync.Mutex
29+
cancel context.CancelFunc
2930
}
3031

3132
func NewCollector(logger *slog.Logger, cfg *config.Config) *Collector {
@@ -48,7 +49,10 @@ func NewCollector(logger *slog.Logger, cfg *config.Config) *Collector {
4849
}
4950

5051
func (c *Collector) Start(ctx context.Context) {
51-
ctx, c.cancel = context.WithCancel(ctx)
52+
ctx, cancel := context.WithCancel(ctx)
53+
c.cancelMu.Lock()
54+
c.cancel = cancel
55+
c.cancelMu.Unlock()
5256

5357
c.runDiscovery()
5458
c.processAllTails()
@@ -57,8 +61,11 @@ func (c *Collector) Start(ctx context.Context) {
5761
}
5862

5963
func (c *Collector) Close() {
60-
if c.cancel != nil {
61-
c.cancel()
64+
c.cancelMu.Lock()
65+
cancel := c.cancel
66+
c.cancelMu.Unlock()
67+
if cancel != nil {
68+
cancel()
6269
}
6370
}
6471

0 commit comments

Comments
 (0)