From 77b33cbe6d2920564a51b1a9b9eef5cb4aee8704 Mon Sep 17 00:00:00 2001 From: Smokey <150986568+jacisjake@users.noreply.github.com> Date: Wed, 28 Oct 2026 05:40:22 -0600 Subject: [PATCH] feat(elder): @Elder merges a thread's delivers when asked (#26) --- nestapi.go | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++-- nestlog.go | 19 +++++++++++++++++++ 2 files changed, 70 insertions(+), 2 deletions(-) diff --git a/nestapi.go b/nestapi.go index 5229393..7638639 100644 --- a/nestapi.go +++ b/nestapi.go @@ -28,9 +28,14 @@ KIND: deliver (here is the result) KIND: decline (with a reason) Then, on the next line, your message text.` -// nestMembers lists who may be addressed in a nest: its crows plus the owner. +// serviceRoles are addressable in a nest but never members: they only act when +// a crow's or the owner's message asks them to. +var serviceRoles = []string{"Elder"} + +// nestMembers lists who may be addressed in a nest: its crows, the owner, and +// the service roles. func nestMembers(nest Nest) []string { - return append(append([]string{}, nest.Crows...), "owner") + return append(append(append([]string{}, nest.Crows...), "owner"), serviceRoles...) } func (s *Server) getNestMessages(w http.ResponseWriter, r *http.Request) { @@ -179,6 +184,8 @@ func (s *Server) postNestMessage(w http.ResponseWriter, r *http.Request) { switch { case mode == "conspiracy" && invited > 0: go s.respondConspiracy(nest, record, invited) + case record.To == "Elder": + go s.respondElder(nest, record) case slices.Contains(nest.Crows, record.To): go s.respondToRequest(nest, record) } @@ -308,6 +315,48 @@ func (s *Server) respondOne(nest Nest, req Message, crow string, lens bool) { } } +// respondElder merges the reports on a thread: the Elder only speaks when a +// message asks it to (CHATROOM_SPEC section 6 — synthesis is requested, not a +// fixed step). It answers whoever opened the thread it merges. +func (s *Server) respondElder(nest Nest, req Message) { + if req.RefersTo == "" { + return + } + target, found := s.msgs.ByMsgID(req.RefersTo) + if !found { + return + } + var reports []string + for _, m := range s.msgs.ReportsTo(nest.ID, target.MsgID(), "deliver") { + if slices.Contains(nest.Crows, m.Author) { + reports = append(reports, "["+m.Author+"] "+m.Text) + } + } + if len(reports) == 0 { + return + } + budget := &Budget{name: "elder-merge", limit: s.p.G.TokenBudgetPerItem} + system := systemFor("Elder", elderSystem, true) + prompt := "REQUEST: " + target.Text + "\n--- reports to merge ---\n" + strings.Join(reports, "\n--- \n") + answer, err := s.p.Flock.ask(s.ctx, nil, "Elder", system, prompt, budget) + if err != nil { + return + } + reply := Message{ + NestID: nest.ID, + Kind: "deliver", + Author: "Elder", + Text: strings.TrimSpace(answer), + To: target.Author, + RefersTo: target.MsgID(), + TaggedBy: "harness", + Timestamp: time.Now().UTC().Format(time.RFC3339), + } + if record, err := s.msgs.Append(reply); err == nil { + s.emitNestMessage(record) + } +} + // checkDivergence asks Jev whether this deliver restates earlier delivers that // answer the same request. Above the band the crow gets a review nudge; the // deliver itself stays in the log (nudges never silence). diff --git a/nestlog.go b/nestlog.go index a06070f..e0201e8 100644 --- a/nestlog.go +++ b/nestlog.go @@ -158,6 +158,25 @@ func (s *MessageStore) NudgeFor(nestID, targetMsgID string) (Message, bool) { return Message{}, false } +// ReportsTo lists messages that answer a given referent, oldest first. +func (s *MessageStore) ReportsTo(nestID, referentID string, kinds ...string) []Message { + s.mu.Lock() + defer s.mu.Unlock() + var out []Message + for _, m := range s.log[nestID] { + if m.RefersTo != referentID { + continue + } + for _, k := range kinds { + if m.Kind == k { + out = append(out, m) + break + } + } + } + return out +} + func (s *MessageStore) save(nestID string) error { if err := os.MkdirAll(s.dir, 0o755); err != nil { return err