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

@ -13,11 +13,15 @@ import (
// Config is the top-level configuration structure, mapping config.example.yaml.
type Config struct {
Accounts []AccountConfig `yaml:"accounts"`
Pool PoolConfig `yaml:"pool"`
Quota QuotaConfig `yaml:"quota"`
Checkpoint CheckpointConfig `yaml:"checkpoint"`
MCPHTTP MCPHTTPConfig `yaml:"mcp_http"`
Accounts []AccountConfig `yaml:"accounts"`
Pool PoolConfig `yaml:"pool"`
Quota QuotaConfig `yaml:"quota"`
Checkpoint CheckpointConfig `yaml:"checkpoint"`
MCPHTTP MCPHTTPConfig `yaml:"mcp_http"`
Notifications NotificationsConfig `yaml:"notifications"`
Dispatcher DispatcherConfig `yaml:"dispatcher"`
Watcher WatcherConfig `yaml:"watcher"`
Janitor JanitorConfig `yaml:"janitor"`
}
// AccountConfig describes a single Anthropic account available to the daemon.
@ -71,9 +75,38 @@ type CheckpointConfig struct {
// MCPHTTPConfig defines the HTTP control-plane endpoint.
type MCPHTTPConfig struct {
Listen string `yaml:"listen"`
BearerTokenEnv string `yaml:"bearer_token_env"`
EnableTrigger bool `yaml:"enable_trigger"`
Listen string `yaml:"listen"`
BearerTokenEnv string `yaml:"bearer_token_env"`
EnableTrigger bool `yaml:"enable_trigger"`
}
// NotificationsConfig holds environment variable names for alert credentials.
// Actual secrets are read from env at runtime — never stored in config files.
type NotificationsConfig struct {
TelegramTokenEnv string `yaml:"telegram_token_env"`
TelegramChatIDEnv string `yaml:"telegram_chat_id_env"`
ResendAPIKeyEnv string `yaml:"resend_api_key_env"`
NotifyEmailEnv string `yaml:"notify_email_env"`
}
// DispatcherConfig controls the task dispatcher behaviour.
type DispatcherConfig struct {
ProjectsDir string `yaml:"projects_dir"`
IdleTimeout Duration `yaml:"idle_timeout"`
PromptTimeout Duration `yaml:"prompt_timeout"`
MaxDispatchPerTask int `yaml:"max_dispatch_per_task"`
}
// WatcherConfig controls the session watcher behaviour.
type WatcherConfig struct {
Interval Duration `yaml:"interval"`
DoneSignalDir string `yaml:"done_signal_dir"`
IdleTimeout Duration `yaml:"idle_timeout"`
}
// JanitorConfig controls the periodic housekeeping goroutine.
type JanitorConfig struct {
Interval Duration `yaml:"interval"`
}
// Duration is a time.Duration that unmarshals from YAML strings like "30s", "1h".
@ -138,6 +171,27 @@ func (c *Config) defaults() {
if c.Pool.Autonomous.Max == 0 {
c.Pool.Autonomous.Max = 10
}
if c.Watcher.Interval.Duration == 0 {
c.Watcher.Interval.Duration = 30 * time.Second
}
if c.Watcher.IdleTimeout.Duration == 0 {
c.Watcher.IdleTimeout.Duration = 60 * time.Minute
}
if c.Watcher.DoneSignalDir == "" {
c.Watcher.DoneSignalDir = "/tmp"
}
if c.Janitor.Interval.Duration == 0 {
c.Janitor.Interval.Duration = 5 * time.Minute
}
if c.Dispatcher.IdleTimeout.Duration == 0 {
c.Dispatcher.IdleTimeout.Duration = 60 * time.Minute
}
if c.Dispatcher.PromptTimeout.Duration == 0 {
c.Dispatcher.PromptTimeout.Duration = 30 * time.Second
}
if c.Dispatcher.MaxDispatchPerTask == 0 {
c.Dispatcher.MaxDispatchPerTask = 3
}
}
// Load reads the YAML file at path, expands home paths, and applies defaults.