package main import ( "fmt" "net/http" "slices" "strings" "time" "unicode/utf8" ) var crowPostable = map[string]bool{ "say": true, "request": true, "claim": true, "deliver": true, "refer": true, "decline": true, } // nestReplySystem tells an addressed crow the response contract: its reply // must open with a KIND line the harness reads (must-respond). const markerNestReply = "This is a chatroom request." const nestReplySystem = crowSystem + ` ` + markerNestReply + ` Start your reply on its own first line with exactly: KIND: claim (you will do it) 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. func nestMembers(nest Nest) []string { return append(append([]string{}, nest.Crows...), "owner") } func (s *Server) getNestMessages(w http.ResponseWriter, r *http.Request) { nestID := r.PathValue("nest_id") if _, found := s.nests.Get(nestID); !found { httpError(w, http.StatusNotFound, fmt.Sprintf("There is no nest called %q.", nestID)) return } all := s.msgs.All(nestID) out := make([]map[string]any, 0, len(all)) for _, m := range all { out = append(out, nestRecord(m)) } writeJSON(w, http.StatusOK, out) } // nestRecord is the EVENTS.md wire shape for one message. func nestRecord(m Message) map[string]any { rec := map[string]any{ "msg_id": m.MsgID(), "kind": m.Kind, "author": m.Author, "text": m.Text, "to": m.To, "refers_to": m.RefersTo, "tagged_by": m.TaggedBy, "ts": m.Timestamp, } if m.Mode != "" { rec["mode"] = m.Mode rec["mode_tagged_by"] = m.ModeTagged } if m.Invited > 0 { rec["invited"] = m.Invited } return rec } func (s *Server) postNestMessage(w http.ResponseWriter, r *http.Request) { nestID := r.PathValue("nest_id") nest, found := s.nests.Get(nestID) if !found { httpError(w, http.StatusNotFound, fmt.Sprintf("There is no nest called %q.", nestID)) return } var body struct { Author string `json:"author"` Text string `json:"text"` Kind string `json:"kind"` Mode string `json:"mode"` Invited int `json:"invited"` To string `json:"to"` RefersTo string `json:"refers_to"` } if !decode(w, r, &body) { return } body.Text = strings.TrimSpace(body.Text) body.To = strings.TrimSpace(body.To) if body.To == "" { body.To = parseAddressing(body.Text, nestMembers(nest)) } // Guardrails (EVENTS.md: Nest messages → Guardrails) if n := utf8.RuneCountInString(body.Text); n < 1 || n > s.p.G.MaxItemChars { httpError(w, http.StatusBadRequest, fmt.Sprintf("A message is 1 to %d characters.", s.p.G.MaxItemChars)) return } if body.Author == "owner" { // owner can always post } else if !slices.Contains(nest.Crows, body.Author) { httpError(w, http.StatusBadRequest, fmt.Sprintf("%q is not a member of %s.", body.Author, nestID)) return } if body.Author != "owner" { if ok, msg, wait := s.p.Ledger.Admit(body.Author, time.Now(), s.p.G); !ok { cooldown := int(wait.Seconds()) writeJSON(w, http.StatusTooManyRequests, map[string]any{"message": msg, "cooldown_s": cooldown}) return } } if body.Kind != "" && body.Author != "owner" && !crowPostable[body.Kind] { httpError(w, http.StatusBadRequest, fmt.Sprintf("%q is not a kind crows may post.", body.Kind)) return } // Tagging (A1, EVENTS.md): Jev first, header fallback, say as last resort. kind, taggedBy := s.tagKind(body.Kind, body.Text) // claim/deliver/decline answer a request: with no explicit address they go // back to whoever posted the message they refer to (EVENTS.md deliver rule). if body.To == "" && (kind == "claim" || kind == "deliver" || kind == "decline") && body.RefersTo != "" { if origin, found := s.msgs.ByMsgID(body.RefersTo); found { body.To = origin.Author } } // Mode (Epic C, same rule shape as kind): Jev first, header escape, "" when unsure. mode, modeBy := s.tagMode(kind, body.Mode, body.Text) // A conspiracy expects several crows on the one task; the invite count is // capped by MurderSize and the flock's in-flight limit. invited := 0 if mode == "conspiracy" { invited = s.inviteCount(nest, body.Invited) } // Claim arbitration (#5, Epic C): first claim wins unless the referent is a // conspiracy, which expects multiple claims. if kind == "claim" && body.RefersTo != "" { if origin, found := s.msgs.ByMsgID(body.RefersTo); found && origin.Mode != "conspiracy" { if owner, claimed := s.msgs.FirstClaim(origin.NestID, origin.MsgID()); claimed { httpError(w, http.StatusConflict, origin.MsgID()+" is claimed by "+owner+"; review it instead of redoing it.") return } } } msg := Message{ NestID: nestID, Kind: kind, Author: body.Author, Text: body.Text, To: body.To, RefersTo: body.RefersTo, TaggedBy: taggedBy, Mode: mode, ModeTagged: modeBy, Invited: invited, Timestamp: time.Now().UTC().Format(time.RFC3339), } record, err := s.msgs.Append(msg) if err != nil { httpError(w, http.StatusInternalServerError, "could not save the message: "+err.Error()) return } s.emitNestMessage(record) // Event-driven trigger (no idle loops): // conspiracy -> several invited crows, each from its own lens // any other request addressed to a member crow -> one must-respond reply if kind == "request" { switch { case mode == "conspiracy" && invited > 0: go s.respondConspiracy(nest, record, invited) case slices.Contains(nest.Crows, record.To): go s.respondToRequest(nest, record) } } writeJSON(w, http.StatusCreated, nestRecord(record)) } var modePostable = map[string]bool{"solo": true, "conspiracy": true, "split": true} // tagMode classifies a request's mode (Jev first, header escape). Only requests // carry a mode; other kinds get "". func (s *Server) tagMode(kind, header, text string) (string, string) { if kind != "request" { return "", "" } if m, ok := s.p.Flock.ClassifyMode(s.ctx, text); ok { if modePostable[header] && header != m { return m, "disagreement" } return m, "jev" } if modePostable[header] { return header, "header" } return "", "default" } // tagKind runs the A1 rule: Jev first; Jev unsure -> the poster's header; // neither -> say. tagged_by records which source won, and a Jev override of a // header logs as a disagreement (fuel for calibration). func (s *Server) tagKind(header, text string) (string, string) { kind, _, ok := s.p.Flock.ClassifyKind(s.ctx, text) if ok { if crowPostable[header] && header != kind { return kind, "disagreement" } return kind, "jev" } if crowPostable[header] { return header, "header" } return "say", "default" } func (s *Server) emitNestMessage(m Message) { rec := nestRecord(m) rec["nest_id"] = m.NestID s.hub.PublishNest(m.NestID, "nest.message", rec) } // inviteCount is how many crows a conspiracy asks to take the one task, capped // by MurderSize, the flock's in-flight limit, and the nest's own size. func (s *Server) inviteCount(nest Nest, requested int) int { cap := s.p.G.MurderSize if s.p.G.MaxCrowsInFlight < cap { cap = s.p.G.MaxCrowsInFlight } if requested > 0 && requested < cap { cap = requested } if len(nest.Crows) < cap { cap = len(nest.Crows) } return cap } // respondConspiracy invites N crows; each answers from its own persona lens so // distinct strengths produce distinct readings. func (s *Server) respondConspiracy(nest Nest, req Message, invited int) { crows := s.p.Flock.pick(nest, invited) for _, crow := range crows { go s.respondOne(nest, req, crow, true) } } // conspiracyLine is the lens directive added to every invited crow's prompt. const conspiracyLine = "You are one lens among several on this request; answer from your own strength, not the flock's consensus." func (s *Server) respondToRequest(nest Nest, req Message) { s.respondOne(nest, req, req.To, false) } // respondOne is the must-respond rule for a single crow: answer with claim, // deliver or decline. A non-responsive reply is retried once with a stricter // ask; still nothing responsive and the crow gets a Sentry flag (no reply). func (s *Server) respondOne(nest Nest, req Message, crow string, lens bool) { budget := &Budget{name: "nest-reply", limit: s.p.G.TokenBudgetPerItem} system := systemFor(crow, nestReplySystem, false) prompt := "NEST: " + nest.Name + "\nREQUEST FROM: " + req.Author + "\nRespond to " + req.Author + ".\nTASK: " + req.Text if lens { strength := "" if p, ok := personaOf(crow); ok { strength = p.Strength } prompt += "\n" + conspiracyLine + "\nYour strength: " + strength + " (" + crow + ")." } kind, body, ok := s.nestReply(system, prompt, crow, budget) if !ok { nudge := prompt + "\nYour last reply did not claim, deliver or decline this request. Answer with a KIND line now." kind, body, ok = s.nestReply(system, nudge, crow, budget) } if !ok { s.postFlag(nest, crow, req) return } reply := Message{ NestID: nest.ID, Kind: kind, Author: crow, Text: body, To: req.Author, // responses go back to whoever requested RefersTo: req.MsgID(), TaggedBy: "header", Timestamp: time.Now().UTC().Format(time.RFC3339), } if record, err := s.msgs.Append(reply); err == nil { s.emitNestMessage(record) } } // nestReply asks one crow once and reads its KIND header. ok means the reply // was responsive (claim, deliver or decline). func (s *Server) nestReply(system, prompt, crow string, budget *Budget) (kind, body string, ok bool) { answer, err := s.p.Flock.ask(s.ctx, nil, crow, system, prompt, budget) if err != nil { return "", "", false } kind, body = splitKind(answer) if !responsiveKind[kind] || strings.TrimSpace(body) == "" { return "", "", false } return kind, strings.TrimSpace(body), true } // postFlag records the Sentry's breach note: an addressed crow that would not // answer. The quarantine/ask_owner loop lands with the watchdog epic. func (s *Server) postFlag(nest Nest, crow string, req Message) { flag := Message{ NestID: nest.ID, Kind: "flag", Author: "Sentry", Text: fmt.Sprintf("%s ignored a request from %s and did not answer after one retry. Flagged for contract breach.", crow, req.Author), To: crow, RefersTo: req.MsgID(), TaggedBy: "harness", Timestamp: time.Now().UTC().Format(time.RFC3339), } if record, err := s.msgs.Append(flag); err == nil { s.emitNestMessage(record) } }