Cancel pending timers when a child is closed
Stale timer bodies were re-delivered to the orchestrator pane after the parent had already processed the sub-agent's reply and called close_process. The timer registry held no link to the child lifecycle, so timers owned by or watching the closed child lingered until something triggered a fire — e.g. a trailing classifier tick for the now-removed child. Add an OnChildClosed hook to ChildEventListener, emit it from Session.Close (and the terminal-corpse path in reapChild), and have the timer manager prune the registry: cancel timers owned by the closed child; remove the closed child from each timer's watched list (cancel the timer outright when watched empties). Natural exit deliberately does not route through this hook — the classifier already emits an idle transition on exit which delivers any legitimate "fire when sub-agent finishes" semantics exactly once; cancelling on exit would swallow that.
This commit is contained in:
@@ -91,6 +91,12 @@ type ChildEventListener interface {
|
||||
// updates a child's IdleState. Listeners use this to repaint the
|
||||
// sidebar badge and to evaluate idle-aware timers.
|
||||
OnChildStateChanged(childID string, state IdleState)
|
||||
// OnChildClosed fires when a child is being removed from the
|
||||
// session (either via close_process, or — for agent/terminal
|
||||
// kinds — when the PTY exits and the entry will never be
|
||||
// restarted). It signals that any pending references to childID
|
||||
// (e.g. timers owned by or watching it) should be dropped.
|
||||
OnChildClosed(childID string)
|
||||
}
|
||||
|
||||
func NewSession(projectDir, projectKey string) *Session {
|
||||
@@ -167,6 +173,12 @@ func (s *Session) emitStateChanged(id string, state IdleState) {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Session) emitClosed(id string) {
|
||||
for _, l := range s.listenersSnapshot() {
|
||||
l.OnChildClosed(id)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Session) ChildEnv() []string {
|
||||
env := os.Environ()
|
||||
// Mark patterm-owned PTYs so a recursive `patterm` invocation can
|
||||
@@ -374,6 +386,11 @@ func (s *Session) Close(id string, sig syscall.Signal) error {
|
||||
}
|
||||
}
|
||||
s.mu.Unlock()
|
||||
// Notify listeners outside s.mu so they can take their own locks
|
||||
// without inversion. Timer manager uses this to drop pending
|
||||
// timers owned by or watching the closed child — otherwise the
|
||||
// next classifier tick can deliver a stale fire to the parent.
|
||||
s.emitClosed(id)
|
||||
s.forgetPersisted(id)
|
||||
return nil
|
||||
}
|
||||
@@ -486,6 +503,7 @@ func (s *Session) reapChild(c *Child, runID uint64) {
|
||||
}
|
||||
}
|
||||
s.mu.Unlock()
|
||||
s.emitClosed(c.ID)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user