feat(watcher): Phase 2.1 — SessionWatcher goroutine

- internal/watcher: detecte fin de tache via signal file, prompt ❯, idle timeout
- state: ForEachWorking, SetStalled, SetActiveAccount, ActiveAccount
- config: WatcherConfig, DispatcherConfig, JanitorConfig, NotificationsConfig + defaults
- 5 tests unitaires, go test ./... -race OK

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Ubuntu 2026-04-14 20:27:51 +00:00
parent 978b60ccf7
commit c87145ea0b
7 changed files with 989 additions and 8 deletions

View file

@ -133,6 +133,51 @@ func (s *State) SetWorking(name, task string) {
s.touch()
}
// SetStalled marks the named session as stalled (working but heartbeat too old).
func (s *State) SetStalled(name string) {
s.mu.Lock()
defer s.mu.Unlock()
sess, ok := s.Sessions[name]
if !ok {
sess = &SessionState{}
s.Sessions[name] = sess
}
sess.State = "stalled"
s.touch()
}
// ForEachWorking calls f for each session currently in "working" state.
// A snapshot is taken under the read lock; f is called without any lock held.
func (s *State) ForEachWorking(f func(name string, sess *SessionState)) {
s.mu.RLock()
working := make(map[string]SessionState, len(s.Sessions))
for name, sess := range s.Sessions {
if sess.State == "working" {
working[name] = *sess
}
}
s.mu.RUnlock()
for name, snap := range working {
snap := snap
f(name, &snap)
}
}
// SetActiveAccount updates the active account in the quota state.
func (s *State) SetActiveAccount(name string) {
s.mu.Lock()
defer s.mu.Unlock()
s.Quota.ActiveAccount = name
s.touch()
}
// ActiveAccount returns the current active account name.
func (s *State) ActiveAccount() string {
s.mu.RLock()
defer s.mu.RUnlock()
return s.Quota.ActiveAccount
}
// SetFailed marks the named session as failed and records the failure timestamp.
// The task is preserved for potential requeue by the caller.
func (s *State) SetFailed(name string) {