Split evidence-stats accounting into stats.go
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -96,21 +96,6 @@ var ErrNotPlanning = errors.New("session: coaching is only available while plann
|
||||
|
||||
var ErrNotActive = errors.New("session: only available while a commitment is active")
|
||||
|
||||
// bucketKey identifies a time bucket; Title is the scrubbed title.
|
||||
type bucketKey struct{ Class, Title string }
|
||||
|
||||
// EvidenceStats is the in-memory accounting for the current session only.
|
||||
type EvidenceStats struct {
|
||||
SessionID string
|
||||
StartedUnix int64
|
||||
Buckets map[bucketKey]time.Duration
|
||||
SwitchCount int
|
||||
Current evidence.WindowSnapshot
|
||||
lastFocusAt time.Time
|
||||
lastKey bucketKey
|
||||
hasLast bool
|
||||
}
|
||||
|
||||
// Controller holds runtime state and the active commitment behind a mutex.
|
||||
type Controller struct {
|
||||
mu sync.Mutex
|
||||
@@ -1031,66 +1016,3 @@ func (c *Controller) applyVerdictLocked(v ai.Verdict) {
|
||||
c.driftStatus = driftDrifting
|
||||
c.driftReason = v.Reason
|
||||
}
|
||||
|
||||
// applyEvent advances stats by one observation: it credits the prior segment to
|
||||
// the prior bucket, counts a context switch on key change, and records the new
|
||||
// current window. Used by both live tracking and crash replay. Caller holds mu.
|
||||
func (c *Controller) applyEvent(now time.Time, snap evidence.WindowSnapshot) {
|
||||
if c.stats.hasLast {
|
||||
c.stats.Buckets[c.stats.lastKey] += now.Sub(c.stats.lastFocusAt)
|
||||
}
|
||||
newKey := keyFor(snap)
|
||||
if c.stats.hasLast && newKey != c.stats.lastKey {
|
||||
c.stats.SwitchCount++
|
||||
}
|
||||
c.stats.lastKey = newKey
|
||||
c.stats.lastFocusAt = now
|
||||
c.stats.hasLast = true
|
||||
c.stats.Current = snap
|
||||
}
|
||||
|
||||
// replayStats rebuilds in-memory stats from the raw session log after a crash.
|
||||
func (c *Controller) replayStats(sessionID string) {
|
||||
events, err := store.ReplaySession(c.sessionsDir, sessionID)
|
||||
if err != nil || len(events) == 0 {
|
||||
c.stats = &EvidenceStats{
|
||||
SessionID: sessionID,
|
||||
StartedUnix: c.clock().Unix(),
|
||||
Buckets: map[bucketKey]time.Duration{},
|
||||
}
|
||||
return
|
||||
}
|
||||
c.stats = &EvidenceStats{
|
||||
SessionID: sessionID,
|
||||
StartedUnix: events[0].AtUnixMillis / 1000,
|
||||
Buckets: map[bucketKey]time.Duration{},
|
||||
}
|
||||
for _, e := range events {
|
||||
c.applyEvent(time.UnixMilli(e.AtUnixMillis), snapFromEvent(e))
|
||||
}
|
||||
}
|
||||
|
||||
func keyFor(snap evidence.WindowSnapshot) bucketKey {
|
||||
if !snap.Health.Available {
|
||||
return bucketKey{Class: "", Title: unavailableTitle}
|
||||
}
|
||||
return bucketKey{Class: snap.Class, Title: evidence.ScrubTitle(snap.Title)}
|
||||
}
|
||||
|
||||
func focusEvent(now time.Time, snap evidence.WindowSnapshot) store.FocusEvent {
|
||||
return store.FocusEvent{
|
||||
AtUnixMillis: now.UnixMilli(),
|
||||
Class: snap.Class,
|
||||
Title: snap.Title,
|
||||
Available: snap.Health.Available,
|
||||
Reason: snap.Health.Reason,
|
||||
}
|
||||
}
|
||||
|
||||
func snapFromEvent(e store.FocusEvent) evidence.WindowSnapshot {
|
||||
return evidence.WindowSnapshot{
|
||||
Title: e.Title,
|
||||
Class: e.Class,
|
||||
Health: evidence.EvidenceHealth{Available: e.Available, Reason: e.Reason},
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user