14 // openDaemonMessages connects the root and its children. It restores queued
15 // messages before the loops start. A queue entry stays until its recipient
16 // finishes the turn that takes it, so a stop can interrupt delivery or a turn.
17 func openDaemonMessages(loops []*TurnLoop) (*DaemonMessages, error) {
19 path: filepath.Join(loops[0].agent.dir, "messages.json"),
20 loops: make(map[string]*TurnLoop),
22 for _, loop := range loops {
23 m.loops[loop.agent.config.Name] = loop
25 if loop.agent.config.Parent == "" {
26 loop.logger = loop.logger.With("name", loop.agent.config.Name)
30 data, err := os.ReadFile(m.path)
31 if err != nil && !errors.Is(err, os.ErrNotExist) {
32 return nil, fmt.Errorf("read agent messages: %w", err)
36 err = json.Unmarshal(data, &m.state)
38 return nil, fmt.Errorf("decode agent messages: %w", err)
43 return nil, errors.New("invalid agent message sequence")
47 for _, message := range m.state.Pending {
48 source := m.loops[message.Source]
49 target := m.loops[message.Target]
50 if source == nil || target == nil || message.ID <= previous || message.ID > m.state.Next || (message.LogID != nil && *message.LogID < 0) {
51 return nil, errors.New("invalid queued agent message")
54 if (message.Kind != "user" || target.agent.config.Parent != message.Source) && (message.Kind != "work" || source.agent.config.Parent != message.Target) {
55 return nil, errors.New("queued agents are not parent and child")
61 for i := range m.state.Pending {
62 err = m.deliver(&m.state.Pending[i])
64 return nil, fmt.Errorf("restore agent message: %w", err)
71 // DaemonMessages owns the durable queue between the root and its children. The
72 // mutex serializes queue writes and delivery. Backups take it before the locks
73 // of the loops, memory, and subagents. A stored log ID makes delivery
74 // idempotent, including a stop between the log append and the wake.
75 type DaemonMessages struct {
78 loops map[string]*TurnLoop
79 state DaemonMessageFile
83 // deliver records one queued message and wakes its recipient. The caller holds
84 // the mutex, or owns the queue before the loops start.
85 func (m *DaemonMessages) deliver(message *DaemonMessage) error {
86 loop := m.loops[message.Target]
87 logged, err := loop.memory.AppendOnce(message.Kind, message.Text, message.LogID, func(id int) error {
92 return fmt.Errorf("record agent message: %w", err)
96 if message.Kind == "work" {
97 status = subagentFinished
104 Source: message.Source,
108 loop.logger.Info("message received", "from", message.Source, "id", message.ID)
113 // save replaces the queue file under the mutex, or before the loops start.
114 func (m *DaemonMessages) save() error {
115 data, err := json.Marshal(m.state)
117 return fmt.Errorf("encode agent messages: %w", err)
120 err = writeFileAtomic(m.path, append(data, '\n'))
122 return fmt.Errorf("save agent messages: %w", err)
128 // Tell queues a user message from parent to its own child, then wakes the
129 // child. It is the daemon entry point for parent-to-child messages.
130 func (m *DaemonMessages) Tell(parent, child, text string) error {
132 defer m.mutex.Unlock()
134 source := m.loops[parent]
135 target := m.loops[child]
136 if source == nil || target == nil || target.agent.config.Parent != parent {
137 return errors.New("the recipient is not a child of the sender")
140 if m.failure != nil {
141 return fmt.Errorf("agent message queue stopped: %w", m.failure)
144 m.add(parent, child, "user", text)
148 err = m.deliver(&m.state.Pending[len(m.state.Pending)-1])
152 return fmt.Errorf("send agent message: %w", m.failed(err))
158 // add appends a queue entry under the mutex. Its ID is never reused.
159 func (m *DaemonMessages) add(source, target, kind, text string) {
161 m.state.Pending = append(m.state.Pending, DaemonMessage{
170 // failed stops all loops after a queue failure. Further sends must not change
171 // the queue, because the outcome of the failed write is not known.
172 func (m *DaemonMessages) failed(err error) error {
174 for _, loop := range m.loops {
181 // Snapshot holds the queue lock while snapshot locks and copies agent files.
182 // Its callback must take the memory locks after this lock, as delivery does.
183 func (m *DaemonMessages) Snapshot(snapshot func() error) error {
185 defer m.mutex.Unlock()
189 return fmt.Errorf("copy agent message snapshot: %w", err)
195 // Finish acknowledges the queued messages taken by a turn and queues its final
196 // report in the same file replacement. A child with no final text sends
197 // nothing. Call after a final reply, a failure, or a user interrupt, but not
198 // when a daemon stop interrupts a model request.
199 func (m *DaemonMessages) Finish(loop *TurnLoop, active []Message, reply string) error {
201 defer m.mutex.Unlock()
203 if m.failure != nil {
204 return fmt.Errorf("agent message queue stopped: %w", m.failure)
207 ids := make(map[int]bool, len(active))
208 for _, message := range active {
209 ids[message.ID] = true
212 name := loop.agent.config.Name
213 before := len(m.state.Pending)
214 m.state.Pending = slices.DeleteFunc(m.state.Pending, func(message DaemonMessage) bool {
215 return message.Target == name && message.LogID != nil && ids[*message.LogID]
218 parent := loop.agent.config.Parent
219 report := parent != "" && strings.TrimSpace(reply) != ""
221 m.add(name, parent, "work", "["+name+"] "+reply)
224 if !report && len(m.state.Pending) == before {
229 if err == nil && report {
230 err = m.deliver(&m.state.Pending[len(m.state.Pending)-1])
234 return fmt.Errorf("finish agent messages: %w", m.failed(err))
240 // DaemonMessageFile holds the sequence and the unacknowledged deliveries.
241 type DaemonMessageFile struct {
242 Next int `json:"next"`
243 Pending []DaemonMessage `json:"pending"`
246 // DaemonMessage is one queued delivery. LogID is set before the log append. A
247 // nil LogID means that delivery has not started.
248 type DaemonMessage struct {
250 Source string `json:"source"`
251 Target string `json:"target"`
252 Kind string `json:"kind"`
253 Text string `json:"text"`
254 LogID *int `json:"log_id,omitempty"`