Fix idle timer review issues
This commit is contained in:
@@ -12,9 +12,9 @@ import (
|
||||
type pendingTimerKind string
|
||||
|
||||
const (
|
||||
timerKindDelay pendingTimerKind = "delay"
|
||||
timerKindIdleAny pendingTimerKind = "idle_any"
|
||||
timerKindIdleAll pendingTimerKind = "idle_all"
|
||||
timerKindDelay pendingTimerKind = "delay"
|
||||
timerKindIdleAny pendingTimerKind = "idle_any"
|
||||
timerKindIdleAll pendingTimerKind = "idle_all"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -39,7 +39,7 @@ type pendingTimer struct {
|
||||
kind pendingTimerKind
|
||||
status string
|
||||
|
||||
watched []string
|
||||
watched []string
|
||||
idleBaseline map[string]bool // for idle_any: ids already idle at registration (excluded from satisfaction)
|
||||
|
||||
firesAt time.Time
|
||||
@@ -55,9 +55,9 @@ type pendingTimer struct {
|
||||
type timerManager struct {
|
||||
sess *Session
|
||||
|
||||
mu sync.Mutex
|
||||
nextID int
|
||||
timers map[string]*pendingTimer
|
||||
mu sync.Mutex
|
||||
nextID int
|
||||
timers map[string]*pendingTimer
|
||||
|
||||
// fireFn is the callback used to deliver the body to the owning
|
||||
// process. Decoupled so tests can substitute a recorder. Defaults
|
||||
@@ -134,6 +134,7 @@ func (m *timerManager) fireDelay(id string) {
|
||||
t.status = timerStatusFired
|
||||
owner := m.sess.FindChild(t.ownerID)
|
||||
body, label := t.body, t.label
|
||||
delete(m.timers, id)
|
||||
m.mu.Unlock()
|
||||
m.fireFn(owner, body, label)
|
||||
}
|
||||
@@ -228,6 +229,7 @@ func (m *timerManager) fireIdleMaxWait(id string) {
|
||||
t.status = timerStatusFired
|
||||
owner := m.sess.FindChild(t.ownerID)
|
||||
body, label := t.body, t.label
|
||||
delete(m.timers, id)
|
||||
m.mu.Unlock()
|
||||
m.fireFn(owner, body, label)
|
||||
}
|
||||
@@ -247,6 +249,7 @@ func (m *timerManager) onChildStateChanged(childID string, state IdleState) {
|
||||
label string
|
||||
}
|
||||
var fires []firing
|
||||
var firedIDs []string
|
||||
for _, t := range m.timers {
|
||||
if t.status != timerStatusPending {
|
||||
continue
|
||||
@@ -268,6 +271,7 @@ func (m *timerManager) onChildStateChanged(childID string, state IdleState) {
|
||||
body: t.body,
|
||||
label: t.label,
|
||||
})
|
||||
firedIDs = append(firedIDs, t.id)
|
||||
case timerKindIdleAll:
|
||||
if m.allWatchedIdleLocked(t) {
|
||||
t.status = timerStatusFired
|
||||
@@ -279,9 +283,13 @@ func (m *timerManager) onChildStateChanged(childID string, state IdleState) {
|
||||
body: t.body,
|
||||
label: t.label,
|
||||
})
|
||||
firedIDs = append(firedIDs, t.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, id := range firedIDs {
|
||||
delete(m.timers, id)
|
||||
}
|
||||
m.mu.Unlock()
|
||||
for _, f := range fires {
|
||||
m.fireFn(f.owner, f.body, f.label)
|
||||
@@ -327,6 +335,7 @@ func (m *timerManager) TimerCancel(ownerID, id string) error {
|
||||
t.rt = nil
|
||||
}
|
||||
t.status = timerStatusCanceled
|
||||
delete(m.timers, id)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -367,20 +376,29 @@ func (m *timerManager) TimerPause(ownerID, id string) error {
|
||||
// TimerResume re-arms a paused timer. For delay timers the remaining
|
||||
// duration is restored; idle-* timers re-attach to the state-change
|
||||
// watch list, and any remaining max-wait clock resumes.
|
||||
//
|
||||
// Idle-* timers also re-check their satisfaction condition immediately
|
||||
// on resume: idle transitions that occurred while paused are otherwise
|
||||
// missed (onChildStateChanged only sees future flips), so a child that
|
||||
// went idle during the pause window would never fire the timer. For
|
||||
// idle_any we look for any non-baseline watched child currently idle;
|
||||
// for idle_all we check whether every watched child is now idle.
|
||||
func (m *timerManager) TimerResume(ownerID, id string) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
t, ok := m.timers[id]
|
||||
if !ok {
|
||||
m.mu.Unlock()
|
||||
return mcp.Errorf(mcp.ErrorKindNotFound, "no such timer %q", id)
|
||||
}
|
||||
// Empty ownerID = top-level orchestrator caller (e.g. a non-agent
|
||||
// MCP client); allow it to manage every timer in the session.
|
||||
// Otherwise the caller's own id must match the timer's owner.
|
||||
if ownerID != "" && t.ownerID != ownerID {
|
||||
m.mu.Unlock()
|
||||
return mcp.Errorf(mcp.ErrorKindRoleForbidden, "timer %q is not owned by caller", id)
|
||||
}
|
||||
if t.status != timerStatusPaused {
|
||||
m.mu.Unlock()
|
||||
return mcp.Errorf(mcp.ErrorKindInvalidArgs, "timer %q is not paused", id)
|
||||
}
|
||||
t.status = timerStatusPending
|
||||
@@ -397,6 +415,42 @@ func (m *timerManager) TimerResume(ownerID, id string) error {
|
||||
t.pausedRemaining = 0
|
||||
t.pausedWasMaxWait = false
|
||||
}
|
||||
// For idle-* timers, evaluate the condition right now in case a
|
||||
// watched child went idle while paused.
|
||||
var fireNow bool
|
||||
var owner *Child
|
||||
var body, label string
|
||||
switch t.kind {
|
||||
case timerKindIdleAny:
|
||||
for _, wid := range t.watched {
|
||||
if t.idleBaseline[wid] {
|
||||
continue
|
||||
}
|
||||
c := m.sess.FindChild(wid)
|
||||
if c != nil && isIdleState(c.IdleState()) {
|
||||
fireNow = true
|
||||
break
|
||||
}
|
||||
}
|
||||
case timerKindIdleAll:
|
||||
if m.allWatchedIdleLocked(t) {
|
||||
fireNow = true
|
||||
}
|
||||
}
|
||||
if fireNow {
|
||||
t.status = timerStatusFired
|
||||
if t.rt != nil {
|
||||
t.rt.Stop()
|
||||
t.rt = nil
|
||||
}
|
||||
owner = m.sess.FindChild(t.ownerID)
|
||||
body, label = t.body, t.label
|
||||
delete(m.timers, id)
|
||||
}
|
||||
m.mu.Unlock()
|
||||
if fireNow {
|
||||
m.fireFn(owner, body, label)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user