diff --git a/nestapi.go b/nestapi.go index 7638639..b0a2fca 100644 --- a/nestapi.go +++ b/nestapi.go @@ -146,9 +146,14 @@ func (s *Server) postNestMessage(w http.ResponseWriter, r *http.Request) { } // Claim arbitration (#5, Epic C): first claim wins unless the referent is a - // conspiracy, which expects multiple claims. + // conspiracy, which expects multiple claims. Directed assignment holds: + // while the asked crow has not answered, only it may claim. if kind == "claim" && body.RefersTo != "" { if origin, found := s.msgs.ByMsgID(body.RefersTo); found && origin.Mode != "conspiracy" { + if slices.Contains(nest.Crows, origin.To) && origin.To != body.Author && !s.msgs.Answered(origin.NestID, origin.MsgID(), origin.To) { + httpError(w, http.StatusConflict, origin.To+" was asked to take this; it answers or declines first.") + return + } 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 @@ -188,6 +193,8 @@ func (s *Server) postNestMessage(w http.ResponseWriter, r *http.Request) { go s.respondElder(nest, record) case slices.Contains(nest.Crows, record.To): go s.respondToRequest(nest, record) + default: + go s.respondOpenRequest(nest, record) } } @@ -257,15 +264,39 @@ func (s *Server) inviteCount(nest Nest, requested int) int { return cap } -// respondConspiracy invites N crows; each answers from its own persona lens so -// distinct strengths produce distinct readings. +// respondConspiracy invites N eligible crows; each answers from its own +// persona lens so distinct strengths produce distinct readings. Strangers are +// skipped: unprompted speech is earned (trust ladder, section 10). func (s *Server) respondConspiracy(nest Nest, req Message, invited int) { - crows := s.p.Flock.pick(nest, invited) - for _, crow := range crows { + n := 0 + for _, crow := range nest.Crows { + if n >= invited { + break + } + if !s.maySpeakUnprompted(crow) { + continue + } + n++ go s.respondOne(nest, req, crow, true) } } +// respondOpenRequest surfaces an unaddressed request to one regular-and-up +// crow as a claim opportunity (section 5: a regular may claim open requests). +func (s *Server) respondOpenRequest(nest Nest, req Message) { + for _, crow := range nest.Crows { + if s.maySpeakUnprompted(crow) { + go s.respondOne(nest, req, crow, false) + return + } + } +} + +// maySpeakUnprompted is the trust gate for open (unaddressed) work. +func (s *Server) maySpeakUnprompted(crow string) bool { + return s.p.Ledger.Level(crow, "other") > Stranger +} + // 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." diff --git a/nestlog.go b/nestlog.go index e0201e8..976d165 100644 --- a/nestlog.go +++ b/nestlog.go @@ -119,6 +119,18 @@ func (s *MessageStore) ByMsgID(msgID string) (Message, bool) { return Message{}, false } +// Answered reports whether crow has claimed, delivered or declined the referent. +func (s *MessageStore) Answered(nestID, referentID, crow string) bool { + s.mu.Lock() + defer s.mu.Unlock() + for _, m := range s.log[nestID] { + if m.Author == crow && m.RefersTo == referentID && responsiveKind[m.Kind] { + return true + } + } + return false +} + // FirstClaim returns the author of the first claim against a message, and // whether any claim exists. func (s *MessageStore) FirstClaim(nestID, referentID string) (string, bool) {