From b43cb8c2cc065136ff17a5df9c37cff078d7f860 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 1/2] =?UTF-8?q?test(elder):=20red=20for=20#26=20=E2=80=94?= =?UTF-8?q?=20addressable=20service=20role,=20merge=20on=20request=20only?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- elder_test.go | 91 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 elder_test.go diff --git a/elder_test.go b/elder_test.go new file mode 100644 index 0000000..2325ff2 --- /dev/null +++ b/elder_test.go @@ -0,0 +1,91 @@ +package main + +import ( + "context" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" +) + +// echoBrain returns the prompt itself, so an Elder merge contains the reports. +type echoBrain struct{} + +func (echoBrain) Name() string { return "echo" } +func (echoBrain) Ask(ctx context.Context, system, prompt string) (string, int, error) { + return prompt, 10, nil +} + +func newEchoServer(t *testing.T) (*httptest.Server, *Server) { + t.Helper() + ts, s := newTestNestServer(t) + s.p.Flock.brain = echoBrain{} + return ts, s +} + +func TestElderAddressableInNest(t *testing.T) { + ts, s := newEchoServer(t) + id := newNest(t, ts, "Lab", `"Hugin","Munin"`) + r := postRequest(t, ts, id, "conspiracy", "is this plan risky") + postDeliver(t, ts, id, "Hugin", r, "the mint Condition is fragile") + postDeliver(t, ts, id, "Munin", r, "the audit trail says otherwise") + + code, _ := postMsg(t, ts, id, `{"author":"owner","kind":"request","to":"Elder","text":"merge this","refers_to":"`+r+`"}`) + if code != http.StatusCreated { + t.Fatalf("elder request: %d", code) + } + + elders := awaitMsgsBy(t, ts, id, "Elder") + if len(elders) != 1 { + t.Fatalf("want exactly one Elder message, got %v", elders) + } + m := elders[0] + text, _ := m["text"].(string) + for _, want := range []string{"fragile", "otherwise"} { + if !strings.Contains(text, want) { + t.Fatalf("Elder merge must carry both reports (missing %q): %q", want, text) + } + } + if m["refers_to"] != r || m["to"] != "owner" || m["kind"] != "deliver" { + t.Fatalf("Elder reply shape wrong: %v", m) + } + _ = s +} + +func TestElderOnlyMergesOnRequest(t *testing.T) { + ts, s := newEchoServer(t) + id := newNest(t, ts, "Lab", `"Hugin","Munin"`) + r := postRequest(t, ts, id, "conspiracy", "is this plan risky") + postDeliver(t, ts, id, "Hugin", r, "ship it") + postDeliver(t, ts, id, "Munin", r, "ship it too") + time.Sleep(300 * time.Millisecond) + if got := awaitMsgsBy(t, ts, id, "Elder"); len(got) != 0 { + t.Fatalf("Elder must stay silent unless asked, got %v", got) + } + _ = s +} + +func TestParseAddressingServiceRole(t *testing.T) { + ts, s := newTestNestServer(t) + id := newNest(t, ts, "Lab", `"Hugin"`) + if got := parseAddressing("please @Elder merge this", nestMembers(nestOf(s, id))); got != "Elder" { + t.Fatalf("Elder must be an addressable service handle, got %q", got) + } +} +func awaitMsgsBy(t *testing.T, ts *httptest.Server, id, author string) []map[string]any { + t.Helper() + deadline := time.Now().Add(2 * time.Second) + for { + var out []map[string]any + for _, m := range nestMessages(t, ts, id) { + if m["author"] == author { + out = append(out, m) + } + } + if len(out) > 0 || time.Now().After(deadline) { + return out + } + time.Sleep(10 * time.Millisecond) + } +} 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 2/2] 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