Minimize the off-task window on confirmed drift at the block level

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-01 12:43:30 -04:00
parent 41c0a5fbbc
commit 47f1167247
2 changed files with 133 additions and 8 deletions
+48 -8
View File
@@ -17,6 +17,7 @@ import (
"antidrift/internal/ai"
"antidrift/internal/domain"
"antidrift/internal/enforce"
"antidrift/internal/evidence"
"antidrift/internal/knowledge"
"antidrift/internal/statemachine"
@@ -75,10 +76,11 @@ const (
)
const (
driftDebounce = 10 * time.Second
driftTimeout = 30 * time.Second
nudgeDebounce = 5 * time.Minute
nudgeTimeout = 30 * time.Second
driftDebounce = 10 * time.Second
driftTimeout = 30 * time.Second
enforceTimeout = 5 * time.Second
nudgeDebounce = 5 * time.Minute
nudgeTimeout = 30 * time.Second
)
const recentTitlesMax = 10
@@ -149,6 +151,7 @@ type Controller struct {
allowedClasses []string // durable: the active session's allowed window classes
enforcementLevel domain.EnforcementLevel // durable: block enables window-minimize enforcement
guard enforce.Guard
judge ai.DriftJudge
driftStatus string
driftReason string
@@ -184,9 +187,10 @@ type ProposalView struct {
// Status: it is populated precisely when Status is "ontask" (semantic drift
// inside an allowed app), so it cannot be folded into the status enum.
type DriftView struct {
Status string `json:"status"`
Reason string `json:"reason,omitempty"`
Nudge string `json:"nudge,omitempty"`
Status string `json:"status"`
Reason string `json:"reason,omitempty"`
Nudge string `json:"nudge,omitempty"`
Enforced bool `json:"enforced,omitempty"`
}
type CoachView struct {
@@ -407,7 +411,8 @@ func (c *Controller) stateLocked() State {
if status == "" {
status = driftIdle
}
st.Drift = &DriftView{Status: status, Reason: c.driftReason, Nudge: c.nudgeMessage}
enforced := c.enforcementLevel == domain.EnforcementBlock && c.driftStatus == driftDrifting
st.Drift = &DriftView{Status: status, Reason: c.driftReason, Nudge: c.nudgeMessage, Enforced: enforced}
}
return st
}
@@ -720,6 +725,14 @@ func (c *Controller) SetDriftJudge(j ai.DriftJudge) {
c.mu.Unlock()
}
// SetGuard injects the OS enforcement guard. A nil guard disables window-minimize
// enforcement; everything else behaves identically.
func (c *Controller) SetGuard(g enforce.Guard) {
c.mu.Lock()
defer c.mu.Unlock()
c.guard = g
}
// SetNudge injects the AI semantic nudge judge. A nil nudger disables nudging;
// local matching and the drift judge are unaffected.
func (c *Controller) SetNudge(n ai.Nudger) {
@@ -1015,13 +1028,35 @@ func (c *Controller) RecordWindow(snap evidence.WindowSnapshot) {
c.applyEvent(now, snap)
c.recordTitleLocked(snap.Title)
launch := c.evaluateDriftLocked(now, snap)
enforceAct := c.enforceActionLocked()
c.mu.Unlock()
if launch != nil {
go launch()
}
if enforceAct != nil {
go enforceAct()
}
c.notify()
}
// enforceActionLocked returns the minimize thunk when this observation should be
// enforced — guard wired, level is block, and drift is confirmed — else nil. The
// returned func performs blocking X11 I/O and MUST run after the caller releases
// c.mu, following the off-lock async-I/O discipline used by the other roles.
func (c *Controller) enforceActionLocked() func() {
if c.guard == nil || c.enforcementLevel != domain.EnforcementBlock || c.driftStatus != driftDrifting {
return nil
}
guard := c.guard
return func() {
ctx, cancel := context.WithTimeout(context.Background(), enforceTimeout)
defer cancel()
if err := guard.MinimizeActive(ctx); err != nil {
log.Printf("session: enforce minimize failed: %v", err)
}
}
}
// 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
@@ -1096,10 +1131,15 @@ func (c *Controller) evaluateDriftLocked(now time.Time, snap evidence.WindowSnap
return
}
c.judgedClasses[class] = v
var enforceAct func()
if c.stats != nil && c.stats.Current.Class == class {
c.applyVerdictLocked(v)
enforceAct = c.enforceActionLocked()
}
c.mu.Unlock()
if enforceAct != nil {
enforceAct()
}
c.notify()
}
}