Cancel pending timers when a child is closed (#6)

Co-authored-by: Harry Bayliss <harry@hjb.dev>
Co-committed-by: Harry Bayliss <harry@hjb.dev>
This commit was merged in pull request #6.
This commit is contained in:
2026-05-18 12:46:50 +01:00
committed by harry
parent de60b93bc6
commit 412b1167a2
7 changed files with 278 additions and 4 deletions

View File

@@ -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)
}
}