Files
mesh-catalog/modules/messenger/cmd/messenger/asks.go
T
jochen 35c4de7d08
mesh/merge-gate pass: builds messenger, telegram → novox; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
mesh/delivery superseded: a newer head of the same pull request
messenger, telegram: a second tap on the recorded answer is said as recorded, and the fallback claims nothing (issue 372 review)
The review found that a double tap while the phone's spinner turned would be
answered 'That answer was not taken', reading as if the approval failed; it is
now 'You already chose Approve at 13:30: recorded.' and does nothing more. The
fallback toast no longer promises a change that may not come (a stranger's
tap), the wait outlasts the router's 10 s wait for the controller's root word,
the channel's own refusals stay in the chat too, and the store's error words
stay in the journal.
2026-10-10 14:05:41 +02:00

1108 lines
39 KiB
Go

package main
// Asks and warrants (novox/hq ADR 0259): the router holds every ask, judges each answer and issues a
// warrant; the asker performs the option with its own grants. The router performs nothing.
//
// - **An ask arrives on the seat's work queue under its asker's name** (`accept.ask.<asker>`), which the
// bus grants that asker alone; the name is read from the subject, never from the body. It is recorded
// in the bucket `asks` under `<asker>.<id>` before the work is acknowledged, so a restart loses none.
// - **It is refused at once, in words, on its asker's `decided` subject** when its words break a rule,
// when no ready channel can carry any of its answers, or when its asker already has three open. An
// option no channel can carry is named in the words, with where it can still be given.
// - **Each option gets a one-time ticket per channel kind that can carry it**: a button carries only the
// ticket. Bound to the ask, the option and the kind; dead when the ask closes or expires.
// - **An answer is judged in order, refusing at the first failure and saying so on the channel**: the ticket
// is known, unused and for this kind; the ask is open and unexpired; the sender is the operator's
// linked account where the kind verifies its sender; the kind's capabilities, from the controller's
// record of its claim, meet the option's level; for approve, the account was linked an hour ago or more.
// Then the ask is closed by compare-and-set — a second answer loses — and the warrant published.
// - **A warrant not yet heard by its asker is published again** every minute until the bus takes it.
import (
"crypto/rand"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"sort"
"strings"
"time"
"git.novox.be/novox/mesh-sdk/go/asks"
)
// ErrChanged is a compare-and-set that lost: another writer was first.
var ErrChanged = errors.New("written by another writer first")
// LinkedApproveAfter is how long a newly linked account waits before it can approve (decided by the
// operator on 2026-10-08, until an authenticator-app code exists).
const LinkedApproveAfter = time.Hour
// StateOpen is an ask not yet ended; every other state is the asks.Outcome it ended with.
const StateOpen = "open"
// Ticket is one option's one-time ticket on one channel kind.
type Ticket struct {
Option string `json:"option"`
Kind string `json:"kind"`
Used bool `json:"used,omitempty"`
}
// AskRecord is one ask as the router keeps it, under `<asker>.<id>` in the bucket `asks`. Its asker reads
// it there under its own name.
type AskRecord struct {
Asker string `json:"asker"`
Ask asks.Ask `json:"ask"`
State string `json:"state"`
Opened time.Time `json:"opened"`
Closed time.Time `json:"closed,omitempty"`
Tickets map[string]Ticket `json:"tickets,omitempty"`
Warrant *asks.Warrant `json:"warrant,omitempty"`
// Told: the warrant, or the end, was published to the asker.
Told bool `json:"told,omitempty"`
// Record is the message the ask is shown on: a condition's key, or its own.
Record string `json:"record,omitempty"`
// Only is the one account that may answer it, on its kind: an ask of the router's own, put to the account
// an unlink or a relink concerns (novox/hq ADR 0259 §7).
Only *Identity `json:"only,omitempty"`
// Act is what the router does on its own ask's warrant: unlink, or link (selfact.go).
Act *SelfAct `json:"act,omitempty"`
}
// Identity is one account linked as the operator on a channel kind.
type Identity struct {
Kind string `json:"kind"`
Identity string `json:"identity"`
Display string `json:"display,omitempty"`
Linked time.Time `json:"linked"`
ApproveFrom time.Time `json:"approve-from"`
}
// AskStore is the router's own state for asks and the operator's identities (ADR 0201).
type AskStore interface {
GetAsk(key string) (*AskRecord, uint64, error)
CreateAsk(key string, r AskRecord) (uint64, error)
UpdateAsk(key string, r AskRecord, revision uint64) (uint64, error)
AskKeys() ([]string, error)
GetIdentity(key string) (*Identity, error)
PutIdentity(key string, id Identity) error
DeleteIdentity(key string) error
IdentityKeys() ([]string, error)
GetLink(kind string) (*PendingLink, error)
PutLink(kind string, l PendingLink) error
DeleteLink(kind string) error
GetUnlinked(kind string) (*Unlinked, error)
PutUnlinked(kind string, u Unlinked) error
// GetLinksAsked and PutLinksAsked keep when links were asked, by account and by kind (allowLink).
GetLinksAsked(key string) ([]time.Time, error)
PutLinksAsked(key string, times []time.Time) error
}
type askEntry struct {
rec AskRecord
rev uint64
}
func askKey(asker, id string) string { return asker + "." + id }
// loadAsks reads back every ask, and indexes the tickets of the open ones.
func (h *Holder) loadAsks() error {
if h.Asks == nil {
return nil
}
keys, err := h.Asks.AskKeys()
if err != nil {
return err
}
for _, k := range keys {
rec, rev, err := h.Asks.GetAsk(k)
if err != nil {
return err
}
if rec == nil {
continue
}
h.keepAsk(k, *rec, rev)
}
return nil
}
func (h *Holder) keepAsk(key string, rec AskRecord, rev uint64) {
h.mu.Lock()
defer h.mu.Unlock()
if h.asks == nil {
h.asks = map[string]*askEntry{}
h.tickets = map[string]string{}
}
h.asks[key] = &askEntry{rec: rec, rev: rev}
// Kept after the ask ends, so a late answer is told why it was not taken.
for t := range rec.Tickets {
h.tickets[t] = key
}
}
func (h *Holder) askOf(key string) *askEntry {
h.mu.Lock()
defer h.mu.Unlock()
if h.asks == nil {
return nil
}
return h.asks[key]
}
// openAsk is the ask a record shows, when it is still open.
func (h *Holder) openAsk(r *Record) *AskRecord {
if r == nil || r.Ask == "" {
return nil
}
e := h.askOf(r.Ask)
if e == nil || e.rec.State != StateOpen {
return nil
}
rec := e.rec
return &rec
}
// carriers are the kinds that can carry an answer of a level now: ready, offering a choice, and for
// approve verifying their sender with an account of the operator's linked, where the controller says now
// that no agent can become root (rootFree, asked live).
func (h *Holder) carriers(level asks.Level) []string {
if h.Bench == nil {
return nil
}
var out []string
for _, k := range h.Bench.Kinds() {
// Every level needs a choice made in one act, shown exactly as the router rendered it (ADR 0234 §8).
if h.Bench.Ready(k.Kind) != nil || !h.Bench.Promises(k.Kind, "choice") || !h.Bench.Promises(k.Kind, "exact-render") {
continue
}
switch level {
case asks.Acknowledge:
out = append(out, k.Kind)
case asks.Approve:
if h.Bench.Promises(k.Kind, "verified-sender") && h.linked(k.Kind) && h.rootFree(k, true) == "" {
out = append(out, k.Kind)
}
}
// Destroy needs a code, which no channel carries yet.
}
return out
}
// RootVerdict is the controller's word on one machine (its verb root-free, novox/hq ADR 0259 §8): free only
// when it measured, recently, that no agent can become root there without a person.
type RootVerdict struct {
Machine string `json:"machine"`
Free bool `json:"free"`
Why string `json:"why"`
Judged string `json:"judged,omitempty"`
}
// RootFreeVerb is the controller's verb the router asks, as this module, which machines are root-free.
const RootFreeVerb = "seat:" + ControllerSeat + ".root-free"
// RootFreeWithin is how long the router waits for the controller's word; RootFreeKept how long a word is
// used to render a message's buttons. An answer is always judged on a word asked then.
const (
RootFreeWithin = 10 * time.Second
RootFreeKept = 30 * time.Second
)
// askRootFree asks the controller about machines, failing on no answer in time.
func (h *Holder) askRootFree(machines []string) (map[string]RootVerdict, error) {
if h.RootFree != nil {
return h.RootFree(machines)
}
if h.Ask == nil {
return nil, errors.New("this router cannot ask the controller")
}
type answer struct {
raw json.RawMessage
err error
}
done := make(chan answer, 1)
go func() {
raw, err := h.Ask(RootFreeVerb, map[string]any{"machines": machines})
done <- answer{raw, err}
}()
var a answer
select {
case a = <-done:
case <-time.After(RootFreeWithin):
return nil, fmt.Errorf("the controller did not answer within %s", RootFreeWithin)
}
if a.err != nil {
return nil, a.err
}
var body struct {
Machines []RootVerdict `json:"machines"`
}
if err := json.Unmarshal(a.raw, &body); err != nil {
return nil, fmt.Errorf("the controller's answer could not be read: %v", err)
}
out := map[string]RootVerdict{}
for _, v := range body.Machines {
out[v.Machine] = v
}
return out, nil
}
// rootFree is why a kind's proof is not believed now, or "" when the controller says, now, that no agent can
// become root without a person on the router's machine and on the kind's (novox/hq ADR 0259 §8, rule 2, as
// the review of 2026-10-09 asked: a positive, recent pass, never the absence of a condition). Anything else
// fails: a router that does not know its machine, a kind whose machine is not said, a controller that does
// not answer, a machine missing from its answer, a word that is not free. cached allows a word up to
// RootFreeKept old, for rendering; an answer is always judged with cached false.
func (h *Holder) rootFree(k KindInfo, cached bool) string {
if h.Node == "" {
return "this router does not know which machine it runs on, so it cannot ask whether an agent can become root there"
}
if k.Node == "" {
return "the mesh has not said which machine " + Display(k.Kind) + " runs on"
}
machines := []string{h.Node}
if k.Node != h.Node {
machines = append(machines, k.Node)
}
now := h.Now()
key := strings.Join(machines, ",")
h.mu.Lock()
kept, ok := h.rootWord[key]
h.mu.Unlock()
var verdicts map[string]RootVerdict
switch {
case cached && ok && now.Sub(kept.at) < RootFreeKept && now.Sub(kept.at) >= 0:
verdicts = kept.verdicts
case cached:
// Read inside the work, which never waits on the controller: no recent word is no pass. warmRootFree
// asks outside it.
return "the controller's word on whether an agent can become root there is not recent"
default:
got, err := h.askRootFree(machines)
if err != nil {
// A word that could not be asked again is not kept from before: no recent word, no pass.
h.mu.Lock()
delete(h.rootWord, key)
h.mu.Unlock()
return "whether an agent can become root where it runs could not be asked: " + err.Error()
}
verdicts = got
h.mu.Lock()
if h.rootWord == nil {
h.rootWord = map[string]rootWord{}
}
h.rootWord[key] = rootWord{at: now, verdicts: got}
h.mu.Unlock()
}
for _, m := range machines {
v, said := verdicts[m]
switch {
case !said:
return "the controller said nothing of whether an agent can become root on " + m
case !v.Free:
why := v.Why
if why == "" {
why = "it is not measured that it cannot"
}
return "a program working for you may become root on " + m + " without asking you (" + why +
"), so an answer proven there proves nothing until that changes"
}
}
return ""
}
// warmRootFree asks the controller's root-free word for every kind that proves its sender, outside the work,
// so what is rendered and offered inside it reads a recent word.
// force asks even a recent word again: taking an ask, whose refusal its asker keeps until something changes.
func (h *Holder) warmRootFree(force bool) {
if h.Bench == nil {
return
}
now := h.Now()
for _, k := range h.Bench.Kinds() {
if !h.Bench.Promises(k.Kind, "verified-sender") || !h.linked(k.Kind) || h.Node == "" || k.Node == "" {
continue
}
key := h.Node
if k.Node != h.Node {
key += "," + k.Node
}
h.mu.Lock()
kept, ok := h.rootWord[key]
h.mu.Unlock()
if !force && ok && now.Sub(kept.at) >= 0 && now.Sub(kept.at) < RootFreeKept/2 {
continue // recent enough: asked again when half its time has gone
}
_ = h.rootFree(k, false)
}
}
// rootWordFor asks the controller's root-free word for a kind fresh, before the work is taken, and answers
// what judging an answer of that kind then reads: the word for that very kind's placement, or why there is none.
// A kind that does not prove its sender is asked nothing: its answers never approve.
func (h *Holder) rootWordFor(kind string) func(KindInfo) string {
if h.Bench == nil {
return func(KindInfo) string { return "the router does not know the channels yet" }
}
asked, held := h.Bench.Kind(kind)
if !held || !h.Bench.Promises(kind, "verified-sender") {
return func(KindInfo) string { return Display(kind) + " cannot prove who answered" }
}
why := h.rootFree(asked, false)
return func(info KindInfo) string {
if info.Kind != asked.Kind || info.Node != asked.Node || info.Module != asked.Module {
return "where " + Display(kind) + " runs changed while the answer was judged; answer again"
}
return why
}
}
// rootWord is the controller's last word on a set of machines, and when it was asked.
type rootWord struct {
at time.Time
verdicts map[string]RootVerdict
}
// approveHolds is why an answer on a kind could not approve now, or "": the kind verifies its sender, an
// account is linked there, and the controller says no agent can become root where it runs.
func (h *Holder) approveHolds(kind string, cached bool) string {
info, held := h.Bench.Kind(kind)
switch {
case !held:
return "this channel is not one the mesh holds for answers"
case !h.Bench.Promises(kind, "verified-sender"):
return Display(kind) + " cannot prove who answered"
case !h.linked(kind):
return "no " + Display(kind) + " account is linked as you"
}
return h.rootFree(info, cached)
}
// answerable is whether an option of an ask is answered on a kind, by the ask's highest level: an ask that
// offers an approval is answered only where it could be approved, whichever of its options is chosen, so an
// acknowledgement from where approval is not proven never ends a question that waits for one (the review of
// 2026-10-09).
func answerLevel(a asks.Ask) asks.Level {
if a.Highest().Rank() > asks.Acknowledge.Rank() {
return a.Highest()
}
return asks.Acknowledge
}
// ConditionOwner is the module whose asks may be shown on a condition's message: the controller, which
// raises the conditions.
const ConditionOwner = "mesh-controller"
// TakeAsk takes one ask or cancel from the seat's queue. asker is the subject's last token, which the bus
// grants that module alone. An error is returned only when the ask could not be recorded, so the queue
// offers it again; a refusal is said to the asker and the work is taken.
func (h *Holder) TakeAsk(verb, asker string, body json.RawMessage) error {
h.warmRootFree(true) // fresh, before the work: an ask refused for no channel stays refused with its asker
h.work.Lock()
defer h.work.Unlock()
h.mu.Lock()
h.init()
h.mu.Unlock()
if !asks.UsableID(strings.ReplaceAll(asker, "-", "_")) {
h.Logf("[messenger] an ask from %q was taken and dropped: not a module's name", asker)
return nil
}
if verb == "cancel" {
var c struct {
ID string `json:"id"`
}
if json.Unmarshal(body, &c) != nil || !asks.UsableID(c.ID) {
h.Logf("[messenger] a cancel from %s names no ask", asker)
return nil
}
return h.closeAsk(askKey(asker, c.ID), asks.OutcomeCancelled, "its asker took it back", nil, "", nil)
}
var a asks.Ask
if err := json.Unmarshal(body, &a); err != nil {
h.refuseAsk(asker, asks.Ask{ID: "unreadable"}, "it could not be read: "+err.Error())
return nil
}
key := askKey(asker, a.ID)
if h.askOf(key) != nil {
return nil // a redelivery: already recorded
}
now := h.Now()
if err := a.Check(now); err != nil {
h.refuseAsk(asker, a, err.Error())
return nil
}
words := []string{a.Headline, a.Explanation, a.OnExpiry}
for _, o := range a.Options {
words = append(words, o.Label, o.Does)
}
if refusal, ok := Check(strings.Join(words, "\n")); !ok {
h.refuseAsk(asker, a, "its words carry "+refusal.String()+", which may not leave the mesh: move it into a reference or say it in words")
return nil
}
open, replaced := 0, ""
h.mu.Lock()
for k, e := range h.asks {
if e.rec.Asker != asker || e.rec.State != StateOpen {
continue
}
if a.About != "" && e.rec.Ask.About == a.About {
replaced = k
continue
}
open++
}
h.mu.Unlock()
if open >= asks.MostOpen {
h.refuseAsk(asker, a, fmt.Sprintf("its asker already has %d open asks", open))
return nil
}
rec := AskRecord{Asker: asker, Ask: a, State: StateOpen, Opened: now, Tickets: map[string]Ticket{}}
// Every option on the kinds that carry the ask's highest level: an ask with an approval is answered only
// where it could be approved (answerLevel).
for _, kind := range h.carriers(answerLevel(a)) {
for _, o := range a.Options {
rec.Tickets[ticket()] = Ticket{Option: o.ID, Kind: kind}
}
}
if len(rec.Tickets) == 0 {
h.refuseAsk(asker, a, "no channel can carry any of its answers now: "+h.whyNot())
return nil
}
if replaced != "" {
if err := h.closeAsk(replaced, asks.OutcomeReplaced, "a newer ask about the same thing replaced it", nil, "", nil); err != nil && !errors.Is(err, ErrChanged) {
return err
}
}
// Shown on the condition it is about, when that is open here; otherwise as a message of its own.
h.mu.Lock()
target := h.open["ask."+key]
// Shown on a condition's message only when asked by the condition's owner: another asker naming a
// condition's key does not take its message over.
if a.About != "" && asker == ConditionOwner {
if r := h.open[a.About]; r != nil && r.Cleared.IsZero() && r.Origin == "condition" {
target = r
}
}
h.mu.Unlock()
if target != nil {
rec.Record = target.Key
} else {
rec.Record = "ask." + key
}
if h.Asks != nil {
rev, err := h.Asks.CreateAsk(key, rec)
if errors.Is(err, ErrChanged) {
got, rev, err := h.Asks.GetAsk(key)
if err != nil || got == nil {
return fmt.Errorf("the ask %s is on record and cannot be read: %v", key, err)
}
h.keepAsk(key, *got, rev)
return nil
}
if err != nil {
return fmt.Errorf("the ask %s could not be recorded: %w", key, err)
}
h.keepAsk(key, rec, rev)
} else {
h.keepAsk(key, rec, 0)
}
h.Logf("[messenger] %s asks %q (%s), shown on %s", asker, a.Headline, a.ID, rec.Record)
if target != nil {
h.mu.Lock()
target.Ask = key
h.mu.Unlock()
if !target.silenced(now) {
h.offerAsk(target)
}
h.persist(target)
return nil
}
severity := Warning
if a.Urgent {
severity = Urgent
}
r := Record{Key: "ask." + key, Kind: "ask", Subject: asker, Severity: severity, Summary: a.Headline,
Headline: a.Headline, Explanation: a.Explanation, Origin: "ask", More: "operator-channel.asks",
Raised: now, Sent: map[string]string{}, Seen: now, Count: 1, Ask: key}
h.mu.Lock()
h.open[r.Key] = &r
h.mu.Unlock()
h.deliver(&r, "raised")
h.persist(&r)
return nil
}
// askKinds are the kinds an open ask about a record is offered on.
func (h *Holder) askKinds(r *Record) map[string]bool {
rec := h.openAsk(r)
if rec == nil {
return nil
}
out := map[string]bool{}
for _, t := range rec.Tickets {
out[t.Kind] = true
}
return out
}
// offerAsk shows a record's new ask: edited in place where the record was shown, a message of its own on a
// channel that carries the ask and has not shown the record.
func (h *Holder) offerAsk(r *Record) {
kinds := h.askKinds(r)
h.mu.Lock()
sent := map[string]string{}
for k, v := range r.Sent {
sent[k] = v
}
h.mu.Unlock()
now := h.Now()
for _, ch := range h.channels() {
id, shown := sent[ch.Name()]
switch {
case shown && id != "" && ch.CanEdit():
err := ch.Edit(id, h.sayOn(r, "asked", ch.Name()))
h.noteChannel(ch.Name(), err)
if err == nil {
h.record(Sent{At: now, Channel: ch.Name(), Key: r.Key, What: "asked", Outcome: "edited"})
continue
}
h.enqueue(ch.Name(), item{Key: r.Key, What: "asked", Severity: r.Severity, At: now})
case kinds[ch.Name()] && ch.Ready() == nil:
h.enqueue(ch.Name(), item{Key: r.Key, What: "asked", Severity: r.Severity, At: now})
}
}
h.flush(now, false)
}
// refuseAsk says to its asker that an ask was not taken, and why.
func (h *Holder) refuseAsk(asker string, a asks.Ask, why string) {
h.Logf("[messenger] refused an ask from %s (%s): %s", asker, a.ID, why)
h.tell(asks.Warrant{Ask: a.ID, Asker: asker, About: a.About, Outcome: asks.OutcomeRefused, Words: why, At: h.Now()})
}
// tell publishes a warrant, or an ask's end, to its asker alone, deduplicated by the ask.
func (h *Holder) tell(w asks.Warrant) error {
if h.Publish == nil {
return errors.New("this router cannot reach the bus")
}
// A new message id each time (the review of 2026-10-09): an id anyone could foresee lets a publisher on
// the same stream take it first, and the bus would drop the warrant as a duplicate. The asker acts on its
// own record of the ask once, however many times it hears a warrant.
err := h.Publish(asks.DecidedSubject(w.Asker), w, "warrant."+ticket())
if err != nil {
h.Logf("[messenger] could not tell %s how its ask %s ended (%v); told again in a minute", w.Asker, w.Ask, err)
}
return err
}
// closeAsk ends an open ask by compare-and-set, tells its asker, and edits every copy. A second close
// loses: ErrChanged when another outcome was first. confirm, when given, is called once the end is recorded
// and the asker told or not (told), before any copy is edited: the answer's confirmation where it was given
// comes first, and never before the record stands (issue 372).
func (h *Holder) closeAsk(key string, outcome asks.Outcome, words string, w *asks.Warrant, used string, confirm func(told bool)) error {
e := h.askOf(key)
if e == nil {
return nil
}
rec, rev := e.rec, e.rev
if h.Asks != nil {
got, r, err := h.Asks.GetAsk(key)
if err != nil {
return err
}
if got != nil {
rec, rev = *got, r
}
}
if rec.State != StateOpen {
h.keepAsk(key, rec, rev)
return ErrChanged
}
now := h.Now()
rec.State = string(outcome)
rec.Closed = now
if t, ok := rec.Tickets[used]; ok {
t.Used = true
rec.Tickets[used] = t
}
if w == nil {
w = &asks.Warrant{Ask: rec.Ask.ID, Asker: rec.Asker, About: rec.Ask.About, Outcome: outcome, Words: words, At: now}
}
rec.Warrant = w
if h.Asks != nil {
newRev, err := h.Asks.UpdateAsk(key, rec, rev)
if err != nil {
if errors.Is(err, ErrChanged) {
if got, r, gerr := h.Asks.GetAsk(key); gerr == nil && got != nil {
h.keepAsk(key, *got, r)
}
}
return err
}
rev = newRev
}
h.keepAsk(key, rec, rev)
if rec.Asker == SelfAsker {
// The router's own ask: it does what was decided, and tells nobody else (selfact.go).
h.markTold(key)
if confirm != nil {
confirm(true)
}
h.showClosed(rec)
h.selfAct(rec, *w)
return nil
}
told := h.tell(*w) == nil
if told {
h.markTold(key)
}
if confirm != nil {
confirm(told)
}
h.showClosed(rec)
return nil
}
// markTold records that the asker was told.
func (h *Holder) markTold(key string) {
e := h.askOf(key)
if e == nil {
return
}
rec, rev := e.rec, e.rev
rec.Told = true
if h.Asks != nil {
newRev, err := h.Asks.UpdateAsk(key, rec, rev)
if err != nil {
return // read again and told again by the sweep
}
rev = newRev
}
h.keepAsk(key, rec, rev)
}
// showClosed edits every copy of an ask's message to say how it ended, its buttons gone.
func (h *Holder) showClosed(rec AskRecord) {
h.mu.Lock()
r := h.open[rec.Record]
h.mu.Unlock()
if r == nil {
return
}
told := h.toldLine(rec) // read before the lock: it reads the ask's record
h.mu.Lock()
// The first line is how it ended, which an ask's own message shows as its title (sayOn); the rest follows.
switch {
case rec.Warrant != nil && rec.Warrant.Outcome == asks.OutcomeChosen:
r.Answered = chosenLine(rec.Warrant, rec.Closed.In(h.zone())) + "\n" + told
case rec.State == string(asks.OutcomeExpired):
r.Answered = "No longer answerable: nobody answered in time.\n" + strings.TrimSuffix(capital(rec.Ask.OnExpiry), ".") + "."
case rec.State == string(asks.OutcomeReplaced):
r.Answered = ""
default:
r.Answered = "No longer needed."
}
ownMessage := r.Origin == "ask"
h.mu.Unlock()
if r.told() {
h.edit(r, "answered", false)
}
if ownMessage && rec.State != string(asks.OutcomeReplaced) {
h.mu.Lock()
r.Cleared = h.Now()
h.mu.Unlock()
}
h.persist(r)
}
// chosenLine is how a chosen ask ended, in the operator's words: "You chose Approve at 13:30 on Telegram."
func chosenLine(w *asks.Warrant, at time.Time) string {
who, where := "Somebody", ""
if w.By != nil {
if w.By.Who == asks.Operator {
who = "You"
}
where = " on " + Display(w.By.Kind)
}
return who + " chose " + w.Label + " at " + at.Format("15:04") + where + "."
}
// toldLine is what the router knows of the asker after an answer: whether it was told. The asker says nothing
// back of what it then did (the asks protocol carries no result to the router), so nothing more is claimed.
func (h *Holder) toldLine(rec AskRecord) string {
if rec.Asker == SelfAsker {
return "The router does it itself."
}
if e := h.askOf(askKey(rec.Asker, rec.Ask.ID)); e != nil && e.rec.Told {
return "Told to " + rec.Asker + ", which acts on it itself."
}
return "Told to " + rec.Asker + " as soon as the bus takes it; it acts on it itself."
}
// confirmChoice says, where an answer was given and in reply to it, that it was recorded: on Telegram the
// tap's own acknowledgement (issue 372). Called only once the ask's end is recorded.
func (h *Holder) confirmChoice(kind string, c asks.Choice, label, asker string, told bool) {
words := "You chose " + label + ": recorded"
switch {
case asker == SelfAsker:
words += "."
case told:
words += ", and " + asker + " was told."
default:
words += "; " + asker + " is told as soon as the bus takes it."
}
err := h.showOn(kind, Message{Title: words, Quiet: true, Reply: c.ID})
h.record(Sent{At: h.Now(), Channel: kind, Key: "(answer)", What: "confirmed " + label, Outcome: outcomeOf(err)})
}
// confirmAgain says, in reply to the same answer given again, that it was already recorded.
func (h *Holder) confirmAgain(kind string, c asks.Choice, label string, at time.Time) {
err := h.showOn(kind, Message{Title: "You already chose " + label + " at " + at.In(h.zone()).Format("15:04") + ": recorded.",
Quiet: true, Reply: c.ID})
h.record(Sent{At: h.Now(), Channel: kind, Key: "(answer)", What: "confirmed again " + label, Outcome: outcomeOf(err)})
}
// sweepAsks ends asks past their expiry, and tells again an asker not yet told. Called every minute.
func (h *Holder) sweepAsks(now time.Time) {
h.mu.Lock()
var expired, untold []string
for k, e := range h.asks {
switch {
case e.rec.State == StateOpen && !now.Before(e.rec.Ask.Expires):
expired = append(expired, k)
case e.rec.State != StateOpen && !e.rec.Told && e.rec.Warrant != nil:
untold = append(untold, k)
}
}
h.mu.Unlock()
sort.Strings(expired)
for _, k := range expired {
e := h.askOf(k)
if err := h.closeAsk(k, asks.OutcomeExpired, "nobody answered in time: "+e.rec.Ask.OnExpiry, nil, "", nil); err != nil && !errors.Is(err, ErrChanged) {
h.Logf("[messenger] could not end the expired ask %s: %v", k, err)
}
}
for _, k := range untold {
e := h.askOf(k)
if e != nil && h.tell(*e.rec.Warrant) == nil {
h.markTold(k)
}
}
}
// offers are an open ask's buttons on one kind: each option offered there, by its ticket.
func (h *Holder) offers(rec *AskRecord, kind string) []Choice {
var out []Choice
if answerLevel(rec.Ask) != asks.Acknowledge && rec.Asker != SelfAsker {
// Rendered only while approval holds there now, by the controller's recent word.
if h.approveHolds(kind, true) != "" {
return nil
}
} else if rec.Asker == SelfAsker && answerLevel(rec.Ask) != asks.Acknowledge {
if info, held := h.Bench.Kind(kind); !held || h.rootFree(info, true) != "" {
return nil
}
}
for _, o := range rec.Ask.Options {
for t, tk := range rec.Tickets {
if tk.Kind == kind && tk.Option == o.ID && !tk.Used {
out = append(out, Choice{Token: t, Label: o.Label})
}
}
}
return out
}
// elsewhere is the line an ask's message carries on a kind for the options it cannot carry there: where
// else each can be given — "Release or Stop: answer on Telegram, or from the mesh MCP server."
func (h *Holder) elsewhere(rec *AskRecord, kind string) string {
var labels []string
where := map[string]bool{}
for _, o := range rec.Ask.Options {
here := false
for _, tk := range rec.Tickets {
if tk.Option == o.ID && tk.Kind == kind {
here = true
}
}
if here {
continue
}
labels = append(labels, o.Label)
for _, tk := range rec.Tickets {
if tk.Option == o.ID && tk.Kind != kind {
where[tk.Kind] = true
}
}
}
if len(labels) == 0 {
return ""
}
var kinds []string
for k := range where {
kinds = append(kinds, Display(k))
}
sort.Strings(kinds)
answer := "from the mesh MCP server"
if len(kinds) > 0 {
answer = "answer on " + strings.Join(kinds, " or ") + ", or from the mesh MCP server"
}
return joinOr(labels) + ": " + answer + "."
}
func joinOr(labels []string) string {
if len(labels) <= 1 {
return strings.Join(labels, "")
}
return strings.Join(labels[:len(labels)-1], ", ") + " or " + labels[len(labels)-1]
}
// judge is an answer to an ask's option, chosen on a kind: refused at the first failure, in words said on
// the kind; otherwise the ask closed and the warrant published (ADR 0259 §5).
func (h *Holder) judge(kind string, c asks.Choice, key string, root func(KindInfo) string) (string, error) {
e := h.askOf(key)
if e == nil {
return h.refuseChoice(kind, c, "it belongs to no ask this router holds")
}
rec := e.rec
if h.Asks != nil {
if got, _, err := h.Asks.GetAsk(key); err == nil && got != nil {
rec = *got
}
}
now := h.Now()
t, issued := rec.Tickets[c.Ticket]
if w := rec.Warrant; issued && rec.State == string(asks.OutcomeChosen) && w != nil && w.Option == t.Option &&
w.By != nil && w.By.Kind == kind && w.By.Identity == c.Sender.Identity && (c.Sender.Authenticated || !h.Bench.Promises(kind, "verified-sender")) {
// The same answer again from the same account — a second tap while the first was being confirmed, or one
// redelivered after a restart: it is what was recorded, so it is said so, not refused as if it failed
// (issue 372). Nothing more is done.
h.confirmAgain(kind, c, w.Label, rec.Closed)
return "chosen", nil
}
switch {
case !issued:
return h.refuseChoice(kind, c, "it belongs to no ask this router holds")
case t.Kind != kind:
return h.refuseChoice(kind, c, "it was not offered here")
case rec.State != StateOpen:
return h.refuseChoice(kind, c, "the question was already answered or has ended")
case t.Used:
return h.refuseChoice(kind, c, "it was already chosen once")
case !now.Before(rec.Ask.Expires):
return h.refuseChoice(kind, c, "the question expired at "+rec.Ask.Expires.In(h.zone()).Format("15:04"))
}
o, offered := rec.Ask.Option(t.Option)
if !offered {
return h.refuseChoice(kind, c, "it names an answer the question does not offer")
}
info, held := h.Bench.Kind(kind)
if !held || !h.Bench.Promises(kind, "choice") || !h.Bench.Promises(kind, "exact-render") {
return h.refuseChoice(kind, c, "this channel is not one the mesh holds for answers")
}
person := asks.Person{Who: asks.Operator, Kind: kind, Identity: c.Sender.Identity, Display: c.Sender.Display}
verifies := h.Bench.Promises(kind, "verified-sender")
var id *Identity
if verifies {
if !c.Sender.Authenticated {
return h.refuseChoice(kind, c, "the service did not authenticate who sent it")
}
id = h.identity(kind, c.Sender.Identity)
if rec.Only != nil {
// An ask put to one account is answered by that account alone, linked or not any more.
if rec.Only.Kind != kind || rec.Only.Identity != c.Sender.Identity {
return h.refuseChoice(kind, c, "this question was put to another account")
}
only := *rec.Only
id = &only
}
if id == nil {
return h.refuseStranger(kind, c)
}
person.Verified = "user id verified"
} else {
if rec.Only != nil && rec.Only.Kind != kind {
return h.refuseChoice(kind, c, "this question was put to another channel")
}
person.Verified = "a desk click: whoever was at the operator's session on " + c.Sender.Identity
}
// Judged at the ask's highest level, whichever option was chosen (answerLevel): an acknowledgement on an
// ask that offers an approval needs what the approval needs, so it never ends that ask from where an
// approval is not proven.
level := o.Level
if answerLevel(rec.Ask).Rank() > level.Rank() {
level = answerLevel(rec.Ask)
}
proofs := []string{}
switch level {
case asks.Acknowledge:
case asks.Approve:
if !verifies {
return h.refuseChoice(kind, c, Display(kind)+" cannot prove who answered, so it cannot answer a question "+
"that asks for an approval")
}
if now.Before(id.ApproveFrom) {
return h.refuseChoice(kind, c, "this account was linked at "+id.Linked.In(h.zone()).Format("15:04")+
" and can answer this question from "+id.ApproveFrom.In(h.zone()).Format("15:04"))
}
if why := root(info); why != "" {
return h.refuseChoice(kind, c, why)
}
if o.Level == asks.Approve {
proofs = append(proofs, "P1")
}
default:
return h.refuseChoice(kind, c, "it needs a code from the authenticator app, which the mesh does not take yet")
}
w := &asks.Warrant{Ask: rec.Ask.ID, Asker: rec.Asker, About: rec.Ask.About, Outcome: asks.OutcomeChosen,
Option: o.ID, Label: o.Label, Level: o.Level, By: &person, Channel: info.Module, Proofs: proofs, At: now,
// The warrant answers this ask as it was taken and shown, with what each option binds (ADR 0259 §6).
AskDigest: rec.Ask.Digest()}
confirm := func(told bool) { h.confirmChoice(kind, c, o.Label, rec.Asker, told) }
if err := h.closeAsk(key, asks.OutcomeChosen, "", w, c.Ticket, confirm); err != nil {
if errors.Is(err, ErrChanged) {
return h.refuseChoice(kind, c, "the question was already answered")
}
// The store's own words stay in the journal: they are not the operator's to read on the phone.
h.Logf("[messenger] the answer to %s's ask %s could not be recorded: %v", rec.Asker, rec.Ask.ID, err)
return h.refuseChoice(kind, c, "it could not be recorded")
}
h.record(Sent{At: now, Channel: kind, Key: rec.Record, What: "answer " + o.Label, Outcome: "warrant to " + rec.Asker})
h.Logf("[messenger] %s: a warrant for %s's ask %s", w.Says(), rec.Asker, rec.Ask.ID)
return "chosen", nil
}
// refuseStranger refuses an answer from an account that is not the operator's, and tells the operator, by name
// and number, on the account that is linked: a question's buttons reached somebody else (a forwarded message,
// or a phone in other hands). Nothing is said to the other account.
func (h *Holder) refuseStranger(kind string, c asks.Choice) (string, error) {
why := "this account is not linked as the operator"
h.Logf("[messenger] an answer on %s from %s was refused: %s", kind, c.Sender.Identity, why)
h.record(Sent{At: h.Now(), Channel: kind, Key: "(answer)", What: "answer", Outcome: "refused: " + why})
who := c.Sender.Display
if who == "" {
who = "an account"
}
_ = h.showOn(kind, Message{Title: "Another account tried to answer for you",
Body: fmt.Sprintf("The %s account %s (number %s) chose an answer on a question meant for you. It was refused "+
"and nothing was done. If you did not pass the question on, somebody else can read your messages.",
Display(kind), who, c.Sender.Identity), Urgent: true})
return "refused", errorf("%s", why)
}
// refuseChoice says on the kind that an answer was not taken, and why.
func (h *Holder) refuseChoice(kind string, c asks.Choice, why string) (string, error) {
h.Logf("[messenger] an answer on %s from %s was refused: %s", kind, c.Sender.Identity, why)
h.record(Sent{At: h.Now(), Channel: kind, Key: "(answer)", What: "answer", Outcome: "refused: " + why})
h.showOn(kind, Message{Title: "That answer was not taken", Body: capital(why) + ". Nothing was done.",
Quiet: true, Reply: c.ID})
return "refused", errorf("%s", why)
}
// showOnHandle is showOn, answering the handle the message is known by.
func (h *Holder) showOnHandle(kind string, m Message) (string, error) {
for _, ch := range h.channels() {
if ch.Name() == kind {
id, err := ch.Send(m)
h.noteChannel(kind, err)
return id, err
}
}
if h.Publish == nil {
return "", errors.New("this router cannot reach the bus")
}
out := asks.Message{Handle: newHandle(), Title: m.Title, Body: m.Body, Urgent: m.Urgent, Silent: m.Quiet, Reply: m.Reply, To: m.To,
Secret: m.Secret}
if out.To == "" && h.Bench != nil && h.Bench.To != nil {
out.To = h.Bench.To(kind)
}
for _, a := range m.Actions {
out.Buttons = append(out.Buttons, asks.Button{Label: a.Label, Ticket: a.Token})
}
err := h.Publish(asks.ChannelSubject(asks.Show, kind), out, out.Handle)
h.noteChannel(kind, err)
return out.Handle, err
}
// showOn says a message of the router's own on one kind, at once: an answer's refusal, details, a link's code.
func (h *Holder) showOn(kind string, m Message) error {
for _, ch := range h.channels() {
if ch.Name() == kind {
_, err := ch.Send(m)
h.noteChannel(kind, err)
return err
}
}
if h.Publish == nil {
return errors.New("this router cannot reach the bus")
}
out := asks.Message{Handle: newHandle(), Title: m.Title, Body: m.Body, Urgent: m.Urgent, Silent: m.Quiet, Reply: m.Reply}
if h.Bench != nil && h.Bench.To != nil {
out.To = h.Bench.To(kind)
}
if m.To != "" {
out.To = m.To
}
out.Secret = m.Secret
for _, a := range m.Actions {
out.Buttons = append(out.Buttons, asks.Button{Label: a.Label, Ticket: a.Token})
}
err := h.Publish(asks.ChannelSubject(asks.Send, kind), out, out.Handle)
h.noteChannel(kind, err)
return err
}
func ticket() string {
var b [16]byte
_, _ = rand.Read(b[:])
return hex.EncodeToString(b[:])
}
// AskView is one ask as the tool `asks` answers it.
type AskView struct {
Key string `json:"key"`
Asker string `json:"asker"`
Headline string `json:"headline"`
State string `json:"state"`
Opened string `json:"opened"`
Expires string `json:"expires"`
Offered []string `json:"offered_on,omitempty"`
Answer string `json:"answer,omitempty"`
Told bool `json:"asker_told"`
}
// AsksNow is every ask, open first, newest first.
func (h *Holder) AsksNow() []AskView {
h.mu.Lock()
defer h.mu.Unlock()
var out []AskView
for k, e := range h.asks {
v := AskView{Key: k, Asker: e.rec.Asker, Headline: e.rec.Ask.Headline, State: e.rec.State,
Opened: e.rec.Opened.UTC().Format(time.RFC3339), Expires: e.rec.Ask.Expires.UTC().Format(time.RFC3339),
Told: e.rec.Told}
kinds := map[string]bool{}
for _, t := range e.rec.Tickets {
kinds[t.Kind] = true
}
for k := range kinds {
v.Offered = append(v.Offered, k)
}
sort.Strings(v.Offered)
if w := e.rec.Warrant; w != nil {
if w.Outcome == asks.OutcomeChosen {
v.Answer = w.Says()
} else {
v.Answer = string(w.Outcome) + ": " + w.Words
}
}
out = append(out, v)
}
sort.Slice(out, func(i, j int) bool {
if (out[i].State == StateOpen) != (out[j].State == StateOpen) {
return out[i].State == StateOpen
}
return out[i].Opened > out[j].Opened
})
return out
}