Wire debounced cached async drift judgment into RecordWindow
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -181,6 +181,10 @@ func New(snapshotPath string) (*Controller, error) {
|
||||
_ = store.PruneOlderThan(c.sessionsDir, sessionRetention, c.clock())
|
||||
if c.runtimeState == domain.RuntimeActive && s.SessionID != "" {
|
||||
c.allowedClasses = s.AllowedWindowClasses
|
||||
// Drift state is not persisted: recompute fresh after restart to avoid
|
||||
// stale interrupts. This also initializes judgedClasses (the pipeline
|
||||
// writes to it) so a restored Active session never panics on a nil map.
|
||||
c.resetDriftLocked()
|
||||
c.replayStats(s.SessionID)
|
||||
}
|
||||
return c, nil
|
||||
@@ -590,10 +594,101 @@ func (c *Controller) RecordWindow(snap evidence.WindowSnapshot) {
|
||||
now := c.clock()
|
||||
_ = store.AppendFocus(c.sessionsDir, c.stats.SessionID, focusEvent(now, snap))
|
||||
c.applyEvent(now, snap)
|
||||
launch := c.evaluateDriftLocked(now, snap)
|
||||
c.mu.Unlock()
|
||||
if launch != nil {
|
||||
go launch()
|
||||
}
|
||||
c.notify()
|
||||
}
|
||||
|
||||
// evaluateDriftLocked runs the local-first drift pipeline for one observation
|
||||
// and updates synchronous drift state. When an async judgment is warranted it
|
||||
// returns the judging closure; the caller runs it in a goroutine after
|
||||
// releasing the mutex (the M2 RequestCoach discipline). Caller holds mu and has
|
||||
// already verified the runtime is Active.
|
||||
func (c *Controller) evaluateDriftLocked(now time.Time, snap evidence.WindowSnapshot) func() {
|
||||
class, title := snap.Class, snap.Title
|
||||
|
||||
// 1. Local match: authoritative on-task, no LLM.
|
||||
ac := domain.AllowedContext{WindowClasses: c.allowedClasses}
|
||||
if evidence.MatchesAllowed(ac, class, title) {
|
||||
c.driftStatus = driftOnTask
|
||||
c.driftReason = ""
|
||||
return nil
|
||||
}
|
||||
|
||||
// 2. Per-class cache.
|
||||
if v, ok := c.judgedClasses[class]; ok {
|
||||
c.applyVerdictLocked(v)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 3. No judge wired: never block; leave idle.
|
||||
if c.judge == nil {
|
||||
c.driftStatus = driftIdle
|
||||
c.driftReason = ""
|
||||
return nil
|
||||
}
|
||||
|
||||
// 4. Debounce: at most one judgment per driftDebounce window.
|
||||
if !c.lastJudgedAt.IsZero() && now.Sub(c.lastJudgedAt) < driftDebounce {
|
||||
return nil
|
||||
}
|
||||
|
||||
prevStatus, prevReason := c.driftStatus, c.driftReason
|
||||
c.driftGen++
|
||||
gen := c.driftGen
|
||||
c.lastJudgedAt = now
|
||||
c.driftStatus = driftPending
|
||||
c.driftReason = ""
|
||||
judge := c.judge
|
||||
commitment := ""
|
||||
if c.commitment != nil {
|
||||
commitment = c.commitment.NextAction + " — " + c.commitment.SuccessCondition
|
||||
}
|
||||
|
||||
return func() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), driftTimeout)
|
||||
defer cancel()
|
||||
v, err := judge.JudgeDrift(ctx, commitment, class, title)
|
||||
|
||||
c.mu.Lock()
|
||||
if gen != c.driftGen || c.runtimeState != domain.RuntimeActive {
|
||||
c.mu.Unlock()
|
||||
return // stale: a newer judgment started or the session ended
|
||||
}
|
||||
if err != nil {
|
||||
// Degrade: never fabricate drift. Revert the optimistic pending.
|
||||
if c.driftStatus == driftPending {
|
||||
c.driftStatus = prevStatus
|
||||
c.driftReason = prevReason
|
||||
}
|
||||
c.mu.Unlock()
|
||||
log.Printf("session: drift judge failed: %v", err)
|
||||
c.notify()
|
||||
return
|
||||
}
|
||||
c.judgedClasses[class] = v
|
||||
if c.stats != nil && c.stats.Current.Class == class {
|
||||
c.applyVerdictLocked(v)
|
||||
}
|
||||
c.mu.Unlock()
|
||||
c.notify()
|
||||
}
|
||||
}
|
||||
|
||||
// applyVerdictLocked maps a verdict onto drift state. Caller holds mu.
|
||||
func (c *Controller) applyVerdictLocked(v ai.Verdict) {
|
||||
if v.OnTask {
|
||||
c.driftStatus = driftOnTask
|
||||
c.driftReason = ""
|
||||
return
|
||||
}
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user