Work through TODO fixes

This commit is contained in:
2026-05-21 15:45:01 +01:00
parent c1b66f9f8a
commit f61788eff2
12 changed files with 540 additions and 33 deletions

View File

@@ -55,9 +55,10 @@ 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
changes chan struct{}
// fireFn is the callback used to deliver the body to the owning
// process. Decoupled so tests can substitute a recorder. Defaults
@@ -67,13 +68,25 @@ type timerManager struct {
func newTimerManager(sess *Session) *timerManager {
m := &timerManager{
sess: sess,
timers: make(map[string]*pendingTimer),
sess: sess,
timers: make(map[string]*pendingTimer),
changes: make(chan struct{}, 1),
}
m.fireFn = defaultFireFn
return m
}
func (m *timerManager) changeEvents() <-chan struct{} {
return m.changes
}
func (m *timerManager) notifyChanged() {
select {
case m.changes <- struct{}{}:
default:
}
}
func defaultFireFn(owner *Child, body, label string) {
if owner == nil || !owner.IsLive() {
return
@@ -121,6 +134,7 @@ func (m *timerManager) TimerSet(ownerID string, body, label string, seconds floa
m.timers[id] = t
m.mu.Unlock()
t.rt = time.AfterFunc(d, func() { m.fireDelay(id) })
m.notifyChanged()
return id, nil
}
@@ -136,6 +150,7 @@ func (m *timerManager) fireDelay(id string) {
body, label := t.body, t.label
delete(m.timers, id)
m.mu.Unlock()
m.notifyChanged()
m.fireFn(owner, body, label)
}
@@ -214,6 +229,7 @@ func (m *timerManager) registerIdleTimer(kind pendingTimerKind, ownerID, body, l
}
m.timers[id] = t
m.mu.Unlock()
m.notifyChanged()
resp.ID = id
resp.Status = "pending"
return resp, nil
@@ -231,6 +247,7 @@ func (m *timerManager) fireIdleMaxWait(id string) {
body, label := t.body, t.label
delete(m.timers, id)
m.mu.Unlock()
m.notifyChanged()
m.fireFn(owner, body, label)
}
@@ -291,6 +308,9 @@ func (m *timerManager) onChildStateChanged(childID string, state IdleState) {
delete(m.timers, id)
}
m.mu.Unlock()
if len(firedIDs) > 0 {
m.notifyChanged()
}
for _, f := range fires {
m.fireFn(f.owner, f.body, f.label)
}
@@ -320,7 +340,7 @@ func (m *timerManager) onChildStateChanged(childID string, state IdleState) {
// legitimate fire and leave the parent never notified.
func (m *timerManager) onChildClosed(childID string) {
m.mu.Lock()
defer m.mu.Unlock()
changed := false
for id, t := range m.timers {
if t.ownerID == childID {
if t.rt != nil {
@@ -329,6 +349,7 @@ func (m *timerManager) onChildClosed(childID string) {
}
t.status = timerStatusCanceled
delete(m.timers, id)
changed = true
continue
}
if !contains(t.watched, childID) {
@@ -344,6 +365,7 @@ func (m *timerManager) onChildClosed(childID string) {
if t.idleBaseline != nil {
delete(t.idleBaseline, childID)
}
changed = true
if len(t.watched) == 0 {
if t.rt != nil {
t.rt.Stop()
@@ -353,6 +375,10 @@ func (m *timerManager) onChildClosed(childID string) {
delete(m.timers, id)
}
}
m.mu.Unlock()
if changed {
m.notifyChanged()
}
}
// allWatchedIdleLocked reports whether every watched child is now
@@ -374,19 +400,21 @@ func (m *timerManager) allWatchedIdleLocked(t *pendingTimer) bool {
// TimerCancel removes a pending or paused timer owned by ownerID.
func (m *timerManager) TimerCancel(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 == timerStatusFired || t.status == timerStatusCanceled {
// Cancelling a fired/cancelled timer is idempotent.
m.mu.Unlock()
return nil
}
if t.rt != nil {
@@ -395,6 +423,8 @@ func (m *timerManager) TimerCancel(ownerID, id string) error {
}
t.status = timerStatusCanceled
delete(m.timers, id)
m.mu.Unlock()
m.notifyChanged()
return nil
}
@@ -402,18 +432,20 @@ func (m *timerManager) TimerCancel(ownerID, id string) error {
// keeps the timer in the registry.
func (m *timerManager) TimerPause(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 != timerStatusPending {
m.mu.Unlock()
return mcp.Errorf(mcp.ErrorKindInvalidArgs, "timer %q is not pending", id)
}
if t.rt != nil {
@@ -429,6 +461,8 @@ func (m *timerManager) TimerPause(ownerID, id string) error {
t.pausedWasMaxWait = t.kind != timerKindDelay
}
t.status = timerStatusPaused
m.mu.Unlock()
m.notifyChanged()
return nil
}
@@ -507,6 +541,7 @@ func (m *timerManager) TimerResume(ownerID, id string) error {
delete(m.timers, id)
}
m.mu.Unlock()
m.notifyChanged()
if fireNow {
m.fireFn(owner, body, label)
}
@@ -587,6 +622,56 @@ func (m *timerManager) activeForChild(id string) *mcp.TimerInfo {
return &info
}
const (
timerSidebarMinRefresh = 50 * time.Millisecond
timerSidebarSubsecondRefresh = 100 * time.Millisecond
)
func nextTimerSidebarLabelChange(d time.Duration) time.Duration {
if d <= 0 {
return 0
}
if d < time.Second {
if d < timerSidebarSubsecondRefresh {
return d
}
return timerSidebarSubsecondRefresh
}
step := time.Second
if d >= time.Hour {
step = time.Hour
} else if d >= time.Minute {
step = time.Minute
}
wait := d % step
if wait <= 0 || wait < timerSidebarMinRefresh {
return timerSidebarMinRefresh
}
return wait
}
func (m *timerManager) nextSidebarRefreshAfter(now time.Time) (time.Duration, bool) {
m.mu.Lock()
defer m.mu.Unlock()
var best time.Duration
found := false
for _, t := range m.timers {
if t.status != timerStatusPending || t.firesAt.IsZero() {
continue
}
wait := nextTimerSidebarLabelChange(t.firesAt.Sub(now))
if wait <= 0 {
wait = timerSidebarMinRefresh
}
if !found || wait < best {
best = wait
found = true
}
}
return best, found
}
func isIdleState(s IdleState) bool {
return s == StateIdle
}