// Package harness is the generic host: it owns shared services and exactly one // active mode, and routes evidence, expiry, and commands to it. package harness import ( "context" "sync" "time" ) // Async runs generation-guarded background work for a mode. It preserves the // lock discipline of the original session.runFetchAsync: fetch runs with no // lock held; stale and apply run under the mode's mutex; notify fires once, // after the lock is released. type Async struct { mu *sync.Mutex notify func() } // NewAsync binds the helper to a mode's mutex and the harness change-notify. func NewAsync(mu *sync.Mutex, notify func()) Async { return Async{mu: mu, notify: notify} } // Run launches a background fetch. stale returns true to DISCARD the result // (a newer generation superseded it). apply records a non-stale result under // the mutex and must NOT call notify — Run owns the post-unlock notify. func (a Async) Run(timeout time.Duration, fetch func(ctx context.Context), stale func() bool, apply func()) { go func() { ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() fetch(ctx) a.mu.Lock() if stale() { a.mu.Unlock() return } apply() a.mu.Unlock() if a.notify != nil { a.notify() } }() }