From b04aace26b2c01b47926929064187aaacaf65ea1 Mon Sep 17 00:00:00 2001 From: Smokey <150986568+jacisjake@users.noreply.github.com> Date: Wed, 28 Oct 2026 05:37:19 -0600 Subject: [PATCH 1/2] =?UTF-8?q?test(divergence):=20red=20for=20#25=20?= =?UTF-8?q?=E2=80=94=20restated=20deliver=20nudged,=20distinct=20not,=20no?= =?UTF-8?q?=20model=20spend?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- divergence_test.go | 131 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 divergence_test.go diff --git a/divergence_test.go b/divergence_test.go new file mode 100644 index 0000000..2f0f7be --- /dev/null +++ b/divergence_test.go @@ -0,0 +1,131 @@ +package main + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + "time" +) + +// fakeNoulJev answers every question in a request with a fixed noul probability. +func fakeNoulJev(t *testing.T, noul float64) *JevClient { + t.Helper() + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var body struct { + Questions map[string]any `json:"questions"` + } + json.NewDecoder(r.Body).Decode(&body) + out := map[string]any{} + for q := range body.Questions { + out[q] = map[string]any{"type": "noul", "noul": noul} + } + json.NewEncoder(w).Encode(map[string]any{"model": "jev-1.14.0", "answers": out, + "usage": map[string]int{"input_tokens": 40, "output_tokens": 0}}) + })) + t.Cleanup(srv.Close) + return &JevClient{url: srv.URL, key: "tk", model: "jev-latest", threshold: 0.7, client: srv.Client()} +} + +func postDeliver(t *testing.T, ts *httptest.Server, id, author, refersTo, text string) string { + t.Helper() + code, out := post(t, ts.URL+"/api/nests/"+id+"/messages", + `{"author":"`+author+`","kind":"deliver","refers_to":"`+refersTo+`","text":"`+text+`"}`) + if code != http.StatusCreated { + t.Fatalf("deliver %s: %d %v", author, code, out) + } + return out["msg_id"].(string) +} + +func findNudge(ts *httptest.Server, id, target string) map[string]any { + for _, m := range nestMessagesNoT(ts, id) { + if m["kind"] == "nudge" && m["refers_to"] == target { + return m + } + } + return nil +} + +func nestMessagesNoT(ts *httptest.Server, id string) []map[string]any { + resp, err := http.Get(ts.URL + "/api/nests/" + id + "/messages") + if err != nil { + return nil + } + defer resp.Body.Close() + var msgs []map[string]any + json.NewDecoder(resp.Body).Decode(&msgs) + return msgs +} + +func waitForNudge(ts *httptest.Server, id, target string) map[string]any { + deadline := time.Now().Add(2 * time.Second) + for { + if n := findNudge(ts, id, target); n != nil { + return n + } + if time.Now().After(deadline) { + return nil + } + time.Sleep(10 * time.Millisecond) + } +} + +func TestRestatementGetsNudge(t *testing.T) { + ts, s := newTestNestServer(t) + s.p.Flock.SetClassifier(fakeNoulJev(t, 0.8)) // above band 0.7 + id := newNest(t, ts, "Lab", `"Hugin","Munin"`) + r := postRequest(t, ts, id, "conspiracy", "is this risky") + postDeliver(t, ts, id, "Hugin", r, "ship it") + d2 := postDeliver(t, ts, id, "Munin", r, "also ship it") + + n := waitForNudge(ts, id, d2) + if n == nil { + t.Fatalf("restated deliver must get a nudge, log: %+v", s.msgs.All(id)) + } + if n["to"] != "Munin" || n["author"] != "harness" { + t.Fatalf("nudge must address the restating crow from the harness, got %v", n) + } + if !containsMsg(s.msgs.All(id), d2) { + t.Fatal("the restated deliver itself must stay in the log") + } +} + +func TestDistinctDeliversNoNudge(t *testing.T) { + ts, s := newTestNestServer(t) + s.p.Flock.SetClassifier(fakeNoulJev(t, 0.2)) // below band + id := newNest(t, ts, "Lab", `"Hugin","Munin"`) + r := postRequest(t, ts, id, "conspiracy", "is this risky") + postDeliver(t, ts, id, "Hugin", r, "ship it") + d2 := postDeliver(t, ts, id, "Munin", r, "wait for the audit") + time.Sleep(300 * time.Millisecond) + if n := findNudge(ts, id, d2); n != nil { + t.Fatalf("a distinct deliver must not be nudged, got %v", n) + } +} + +func TestNudgeSpendsNoModelTokens(t *testing.T) { + ts, s := newTestNestServer(t) + s.p.Flock.SetClassifier(fakeNoulJev(t, 0.8)) + id := newNest(t, ts, "Lab", `"Hugin","Munin"`) + r := postRequest(t, ts, id, "conspiracy", "is this risky") + postDeliver(t, ts, id, "Hugin", r, "ship it") + + d2, err := s.msgs.Append(Message{NestID: id, Kind: "deliver", Author: "Munin", Text: "ship", RefersTo: r, Timestamp: time.Now().UTC().Format(time.RFC3339)}) + if err != nil { + t.Fatal(err) + } + b := &Budget{name: "nest", limit: 1} + s.checkDivergence(nestOf(s, id), d2, b) + if b.Spent() != 0 { + t.Fatalf("a review nudge must spend no model tokens, spent=%d", b.Spent()) + } +} + +func containsMsg(msgs []Message, msgID string) bool { + for _, m := range msgs { + if m.MsgID() == msgID { + return true + } + } + return false +} -- 2.54.0 From dfe50da320eaa60bf0283d0bb0a221bb2e4c5831 Mon Sep 17 00:00:00 2001 From: Smokey <150986568+jacisjake@users.noreply.github.com> Date: Wed, 28 Oct 2026 05:37:19 -0600 Subject: [PATCH 2/2] feat(divergence): Jev noul restatement check -> harness review nudge (#25) --- jev.go | 55 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ nestapi.go | 40 +++++++++++++++++++++++++++++++++++++++ nestlog.go | 26 ++++++++++++++++++++++++++ 3 files changed, 121 insertions(+) diff --git a/jev.go b/jev.go index 0f4b2d7..ea7343d 100644 --- a/jev.go +++ b/jev.go @@ -252,6 +252,61 @@ func (j *JevClient) ClassifyKind(ctx context.Context, text string) (string, map[ return a.Choice, a.Probabilities, true } +// divergenceQuestions asks whether a message restates an earlier point. It is +// a noul: JEV_STRATEGIES strategy 3 (crisp yes/no) and noul has no confidence, +// so the probability itself is thresholded (band = threshold). +var divergenceQuestions = map[string]any{ + "restates_earlier": map[string]any{ + "type": "noul", + "instructions": "Does `this` make a point already made in `earlier`, adding no new angle?", + "criteria": map[string]string{ + "true": "It repeats an earlier point without a genuinely new angle.", + "false": "It adds something different from what `earlier` already said.", + }, + }, +} + +// ClassifyDivergence returns the probability that a message restates earlier +// ones, and ok when a value came back (noul always carries one; ok is false +// only on transport error or a missing answer). +func (j *JevClient) ClassifyDivergence(ctx context.Context, state any) (float64, bool) { + if j == nil { + return 0, false + } + answers, _, _, err := j.SystemOne(ctx, state, divergenceQuestions) + if err != nil { + return 0, false + } + a := answers["restates_earlier"] + if a.Noul == nil { + return 0, false + } + return *a.Noul, true +} + +// ClassifyDivergence runs the restatement check under the flock guardrails +// (cap, timeout, flight log with the probability recorded). +func (f *Flock) ClassifyDivergence(ctx context.Context, state any, budgets ...*Budget) (float64, bool) { + if f.jev == nil { + return 0, false + } + for _, b := range budgets { + if err := b.Check(); err != nil { + return 0, false + } + } + f.sem <- struct{}{} + defer func() { <-f.sem }() + cctx, cancel := context.WithTimeout(ctx, f.g.CrowTimeout) + defer cancel() + start := time.Now() + p, ok := f.jev.ClassifyDivergence(cctx, state) + if f.log != nil { + f.log.Write(Event{Kind: "divergence", Crow: "Sentry", Model: f.jev.model, Ms: time.Since(start).Milliseconds(), Prob: map[string]float64{"restates_earlier": p}, Note: fmt.Sprint(ok && p >= f.jev.threshold)}) + } + return p, ok +} + // ClassifyKind tags a nest message under the same guardrails as any call: // flock cap, timeout and flight log, with the probabilities recorded. func (f *Flock) ClassifyKind(ctx context.Context, text string, budgets ...*Budget) (string, map[string]float64, bool) { diff --git a/nestapi.go b/nestapi.go index 8affd54..5229393 100644 --- a/nestapi.go +++ b/nestapi.go @@ -184,6 +184,10 @@ func (s *Server) postNestMessage(w http.ResponseWriter, r *http.Request) { } } + if kind == "deliver" { + go s.checkDivergence(nest, record) + } + writeJSON(w, http.StatusCreated, nestRecord(record)) } @@ -298,6 +302,42 @@ func (s *Server) respondOne(nest Nest, req Message, crow string, lens bool) { } if record, err := s.msgs.Append(reply); err == nil { s.emitNestMessage(record) + if record.Kind == "deliver" { + s.checkDivergence(nest, 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). +func (s *Server) checkDivergence(nest Nest, m Message, budgets ...*Budget) { + if m.Kind != "deliver" { + return + } + earlier := s.msgs.EarlierDelivers(nest.ID, m.RefersTo, m.ID) + if len(earlier) == 0 { + return + } + if _, nudged := s.msgs.NudgeFor(nest.ID, m.MsgID()); nudged { + return // one nudge per deliver; no loop + } + prob, ok := s.p.Flock.ClassifyDivergence(s.ctx, map[string]any{"this": m.Text, "earlier": earlier}, budgets...) + if !ok || prob < s.p.Flock.jev.threshold { + return + } + nudge := Message{ + NestID: nest.ID, + Kind: "nudge", + Author: "harness", + Text: "This reads like a point already made in the thread; review the earlier delivers and add your distinct angle or let it rest.", + To: m.Author, + RefersTo: m.MsgID(), + TaggedBy: "harness", + Timestamp: time.Now().UTC().Format(time.RFC3339), + } + if record, err := s.msgs.Append(nudge); err == nil { + s.emitNestMessage(record) } } diff --git a/nestlog.go b/nestlog.go index 82ea705..a06070f 100644 --- a/nestlog.go +++ b/nestlog.go @@ -132,6 +132,32 @@ func (s *MessageStore) FirstClaim(nestID, referentID string) (string, bool) { return "", false } +// EarlierDelivers returns the texts of deliver messages answering the same +// referent, oldest first, excluding the one with id excludeID. +func (s *MessageStore) EarlierDelivers(nestID, referentID string, excludeID int) []string { + s.mu.Lock() + defer s.mu.Unlock() + var out []string + for _, m := range s.log[nestID] { + if m.Kind == "deliver" && m.RefersTo == referentID && m.ID != excludeID { + out = append(out, m.Text) + } + } + return out +} + +// NudgeFor reports whether a nudge already references a given message. +func (s *MessageStore) NudgeFor(nestID, targetMsgID string) (Message, bool) { + s.mu.Lock() + defer s.mu.Unlock() + for _, m := range s.log[nestID] { + if m.Kind == "nudge" && m.RefersTo == targetMsgID { + return m, true + } + } + return Message{}, false +} + func (s *MessageStore) save(nestID string) error { if err := os.MkdirAll(s.dir, 0o755); err != nil { return err -- 2.54.0