+// Notify accepts root external data without adding it to memory or an active
+// chain. A receipt is durable before success. Exact retries return the same
+// sequence even after completion. New keys fail when the receipt store is full.
+func (m *DaemonMessages) Notify(input *NotifyInput) (Event, error) {
+ m.mutex.Lock()
+ defer m.mutex.Unlock()
+
+ if m.failure != nil {
+ return Event{}, fmt.Errorf("notification queue stopped: %w", m.failure)
+ }
+
+ if input == nil {
+ return Event{}, errors.New("missing notification data")
+ }
+
+ err := notifyValidate(*input)
+ if err != nil {
+ return Event{}, fmt.Errorf("validate external event: %w", err)
+ }
+
+ digest := notifyDigest(input.Text)
+ for _, receipt := range m.state.Notifications {
+ if receipt.Source != input.Source || receipt.ID != input.ID {
+ continue
+ }
+
+ if receipt.Digest != digest {
+ return Event{}, errors.New("notification key conflicts with accepted text")
+ }
+
+ return Event{
+ Kind: "notify",
+ ID: receipt.Number,
+ Status: "duplicate",
+ }, nil
+ }
+
+ if len(m.state.Notifications) >= notifyReceipts {
+ return Event{}, errors.New("notification receipt store is full")
+ }
+
+ root := m.ordered[0]
+ text, err := notifyText(*input, root.clock())
+ if err != nil {
+ return Event{}, fmt.Errorf("prepare external event: %w", err)
+ }
+
+ name := root.agent.config.Name
+ m.add(name, name, "work", text)
+
+ m.state.Pending[len(m.state.Pending)-1].Notification = true
+ m.state.Notifications = append(m.state.Notifications, NotifyReceipt{
+ Source: input.Source,
+ ID: input.ID,
+ Digest: digest,
+ Number: m.state.Next,
+ })
+
+ err = m.save()
+ if err != nil {
+ return Event{}, fmt.Errorf("accept external event: %w", m.failed(err))
+ }
+
+ select {
+ case root.wake <- struct{}{}:
+ default:
+ }
+
+ root.logger.Info("event accepted", "id", m.state.Next)
+
+ return Event{
+ Kind: "notify",
+ ID: m.state.Next,
+ Status: "accepted",
+ }, nil