package main import ( "context" "errors" "fmt" "sync" ) // DaemonChildren starts children made by the root during a daemon run. The root // calls Create in its turn goroutine. Close runs only after that loop stops. // The daemon context, not the calling turn, owns each child's lifetime. type DaemonChildren struct { ctx context.Context cancel context.CancelFunc root *TurnLoop messages *DaemonMessages opened DaemonAgents workers sync.WaitGroup mutex sync.Mutex failure error } // Create makes and opens a child, registers its queue, and starts its loop. // Registration holds the queue lock through file creation, so a backup cannot // copy a child before its open files are part of the snapshot. func (c *DaemonChildren) Create(name, responsibility string) (string, error) { if c.ctx.Err() != nil { return "", c.ctx.Err() } if c.root.openChild == nil { return "", errors.New("child creation is not available") } var opened *DaemonAgent loop, err := c.messages.AddChild(c.root.agent.config.Name, name, func() (*TurnLoop, error) { agent, err := c.root.agent.CreateChild(name, responsibility, c.root.runs.subagents) if err != nil { return nil, fmt.Errorf("create child config: %w", err) } opened, err = c.root.openChild(c.ctx, agent) if err != nil { return nil, fmt.Errorf("open child: %w", err) } loop, err := opened.Loop(nil, func(Event) { }) if err != nil { return nil, errors.Join(fmt.Errorf("open child loop: %w", err), opened.Close()) } return loop, nil }) if err != nil { return "", fmt.Errorf("start child %q: %w", name, err) } c.opened = append(c.opened, opened) c.workers.Add(1) go func() { defer c.workers.Done() err := daemonRunChild(c.ctx, loop, opened.schedules) if err != nil { opened.logger.Error("child stopped", "err", err) c.mutex.Lock() c.failure = errors.Join(c.failure, err) c.mutex.Unlock() } c.cancel() }() c.root.logger.Info("child started", "name", name) return "Started " + name + ". Use tell to send it a task.", nil } // Close waits for the runtime children, closes their files, and returns their // failures. The caller stops the daemon context before this call. func (c *DaemonChildren) Close() error { c.workers.Wait() return errors.Join(c.failure, c.opened.Close()) }