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, } 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, 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, }) } writeJSON(w, http.StatusOK, out) } 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"` 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) // 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) msg := Message{ NestID: nestID, Kind: kind, Author: body.Author, Text: body.Text, To: body.To, RefersTo: body.RefersTo, TaggedBy: taggedBy, 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: if kind is request and To is a crow, respond. if kind == "request" && record.To != "" && record.To != "owner" { go s.crowResponds(nest, record) } writeJSON(w, http.StatusCreated, map[string]any{ "msg_id": record.MsgID(), "kind": record.Kind, "author": record.Author, "text": record.Text, "to": record.To, "refers_to": record.RefersTo, "tagged_by": record.TaggedBy, "ts": record.Timestamp, }) } // 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) { s.hub.PublishNest(m.NestID, "nest.message", map[string]any{ "nest_id": m.NestID, "msg_id": m.MsgID(), "kind": m.Kind, "author": m.Author, "text": m.Text, "to": m.To, "refers_to": m.RefersTo, "tagged_by": m.TaggedBy, }) } func (s *Server) crowResponds(nest Nest, req Message) { crows := s.p.Flock.pick(nest, 1) if len(crows) == 0 { return } crow := crows[0] // B4 (@-addressing) makes this the addressed crow system := systemFor(crow, crowSystem, true) prompt := "NEST: " + nest.Name + "\nREQUEST FROM: " + req.Author + "\nTEXT: " + req.Text budget := &Budget{name: "nest-message", limit: s.p.G.TokenBudgetPerItem} answer, err := s.p.Flock.ask(s.ctx, nil, crow, system, prompt, budget) if err != nil { return } reply := Message{ NestID: nest.ID, Kind: "say", Author: crow, Text: strings.TrimSpace(answer), To: req.Author, RefersTo: req.MsgID(), TaggedBy: "harness", Timestamp: time.Now().UTC().Format(time.RFC3339), } record, err := s.msgs.Append(reply) if err != nil { return } s.emitNestMessage(record) }