Files
mesh-catalog/modules/mesh-knowledge/cmd/mesh-knowledge/operator.go
T
jschoubben c211762813 Hold an ask no channel can carry now and offer it when one can; show held and why (issue 926, ADR 0397)
The router refused such an ask with 'no channel', so the asker waited for an Approve nobody saw. It is now held as an open ask with the per-kind reason, judged again when the kinds or a channel's standing change and every minute, and the knowledge base says held and why.
2026-10-16 17:51:32 +02:00

375 lines
15 KiB
Go

package main
// A record that needs the operator (novox/hq ADR 0290 rule 4, to-be 55 §5): once the knowledge base's own review
// approves it, the operator is asked on the router with Approve and Decline, both at the level approve, so only a channel
// that proves who answered can carry either (ADR 0259, as claude-code's proposals ask it, ADR 0274). Each option
// binds the act it authorises: this record, this version's digest. The knowledge base accepts the record only on
// the router's warrant for Approve, read back from the router's own record of the ask (which only the router
// writes) and held to the ask composed again from the record: the same digest, an option it offered, at its
// level, in time, and the act the option bound. A record changed after it was asked finds the digests differ, and
// nothing is accepted. The warrant is kept with the record.
import (
"context"
"crypto/rand"
"encoding/hex"
"encoding/json"
"fmt"
"regexp"
"strconv"
"strings"
"time"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/asks"
)
// Module is this module's name: the one the bus grants its asks under.
const Module = "mesh-knowledge"
// The two answers, both at the level approve.
const (
OptionApprove = "approve"
OptionDecline = "decline"
)
// AskLasts is how long the operator has to answer: a day less a margin, under the router's bound for an approval.
const AskLasts = asks.ApproveLasts - 10*time.Minute
// The states of an operator's ask, as the record keeps them.
const (
AskWaiting = "waiting for the operator's answer"
AskApproved = "approved by the operator"
AskDeclined = "declined by the operator"
)
// AskBus is the operator channel as this module reaches it: publish an ask under its own name, and read the
// router's record of one of its asks.
type AskBus interface {
Publish(subject string, body any, id string) error
Record(id string) (*stdio.StateEntry, error)
}
type sdkAskBus struct{}
func (sdkAskBus) Publish(subject string, body any, id string) error {
return stdio.SeatPublish(subject, body, id)
}
func (sdkAskBus) Record(id string) (*stdio.StateEntry, error) { return stdio.SeatRecord("", id) }
// act is what an answer binds: this record's version, by its digest, and the answer.
func act(r *Record, a *OperatorAsk, option string) asks.Act {
return asks.Act{"verb": "knowledge-base.accept", "option": option, "ask": a.ID, "kind": r.Kind,
"number": strconv.Itoa(r.Number), "version": strconv.Itoa(a.Version), "digest": r.Digest()}
}
var (
unshownPath = regexp.MustCompile(`[/\\@~$]|\d+\.\d+\.\d+|[A-Za-z0-9+/=_]{24,}|(?i)\b[a-z0-9-]+\.[a-z]{2,12}\b`)
)
// shown is a line the operator's phone may be shown: no path, address, host-like name or long unbroken word —
// narrower than the router's own rule, so the ask is not refused for its words.
func shown(line string) bool { return !unshownPath.MatchString(line) }
// fingerprint is a digest as the operator is shown it: its first 24 hexadecimal digits in groups of four.
func fingerprint(digest string) string {
h := strings.TrimPrefix(digest, "sha256:")
if len(h) < 24 {
return h
}
var g []string
for i := 0; i < 24; i += 4 {
g = append(g, h[i:i+4])
}
return strings.Join(g, " ")
}
// AskGlance is the form of an ask read at a glance (OperatorAsk.Form, novox/hq issue 639).
const AskGlance = 1
// composeAsk is the ask for a record, composed from the record and its ask alone: asked once, and composed
// again to hold the warrant to it — in the form it was asked in, so an ask made before a form changed is still
// held to the words the operator was shown.
func composeAsk(r *Record, a *OperatorAsk) asks.Ask {
if a.Form == AskGlance {
return glanceAsk(r, a)
}
return listedAsk(r, a)
}
// glanceAsk is the ask read at a glance on the phone (novox/hq issue 639): the brief's headline, its summary and
// points; who proposed it, its review, its fingerprint and how to read every clause are its Details, which the
// router notes after the answers. A record without a brief (one proposed before the rule) is asked with its
// title as its summary. The clauses are never listed.
func glanceAsk(r *Record, a *OperatorAsk) asks.Ask {
name := r.Key().String()
headline, explanation := "Approve "+name+"?", name+" needs your approval before it holds."
if t := strings.TrimSuffix(strings.TrimSpace(r.Title), "."); shown(t) && len([]rune(t)) < asks.SummaryLength {
explanation = t + "."
}
if r.Brief != nil && len(briefProblems(r.Brief)) == 0 {
headline, explanation = strings.TrimSpace(r.Brief.Headline), r.Brief.Explanation()
}
author := r.Author
if author == "" {
author = "an agent session"
}
clauses := fmt.Sprintf("Read its %d clauses with the knowledge base's get, number %d.", len(r.Clauses), r.Number)
if len(r.Clauses) == 0 {
clauses = fmt.Sprintf("Read it whole with the knowledge base's get, number %d.", r.Number)
}
details := fmt.Sprintf("%s, proposed by %s; the knowledge base's own review approved it. Fingerprint %s.\n%s",
name, author, fingerprint(r.Digest()), clauses)
return withOptions(r, a, asks.Ask{ID: a.ID, Headline: headline, Explanation: explanation, Details: details})
}
// listedAsk is the ask as it was composed before the brief: every clause's lead listed. Composed only to hold
// the warrant of an ask made then.
func listedAsk(r *Record, a *OperatorAsk) asks.Ask {
name := r.Key().String()
headline := "Accept " + name + "?"
var b strings.Builder
fmt.Fprintf(&b, "%s was proposed by %s and approved by the knowledge base's own review. Its author says it needs your approval before it holds.", name, r.Author)
if shown(r.Title) {
fmt.Fprintf(&b, "\n\nIts title: %s.", strings.TrimSuffix(r.Title, "."))
}
if len(r.Clauses) > 0 {
fmt.Fprintf(&b, "\n\nIts %d clause(s):", len(r.Clauses))
size, hidden := 0, 0
for _, c := range r.Clauses {
lead := c.Lead()
if !shown(lead) || size+len(lead) > 1200 {
hidden++
continue
}
size += len(lead)
fmt.Fprintf(&b, "\n%d. %s", c.N, lead)
}
if hidden > 0 {
fmt.Fprintf(&b, "\n(%d lead(s) not shown here)", hidden)
}
}
fmt.Fprintf(&b, "\n\nRead it whole with the knowledge base's get, number %d; its fingerprint is %s.", r.Number, fingerprint(r.Digest()))
return withOptions(r, a, asks.Ask{ID: a.ID, Headline: headline, Explanation: b.String()})
}
// withOptions is an ask with what every form of it carries: who answers, its expiry, what it is about, and its
// two options, each binding its act.
func withOptions(r *Record, a *OperatorAsk, out asks.Ask) asks.Ask {
out.Who, out.Expires = asks.Operator, a.At.Add(AskLasts)
out.OnExpiry = "the record stays proposed; nothing of it is in force"
out.About = "knowledge." + r.Kind + "." + strconv.Itoa(r.Number)
for _, o := range []struct{ id, label, does string }{
{OptionApprove, "Approve", "the record is accepted and its clauses are in force"},
{OptionDecline, "Decline", "the record stays proposed; nothing of it is in force"},
} {
binds, _ := asks.ActDigest(act(r, a, o.id))
out.Options = append(out.Options, asks.Option{ID: o.id, Label: o.label, Does: o.does, Level: asks.Approve, Binds: binds})
}
return out
}
func newAskID(r *Record) string {
var b [4]byte
_, _ = rand.Read(b[:])
return fmt.Sprintf("kb-%s-%d-%s", r.Kind, r.Number, hex.EncodeToString(b[:]))
}
// askOperator asks the operator to approve a record the knowledge base's review approved, and keeps the ask with
// it. The caller holds the write lock. An ask that could not be made or published is taken back, and the review is
// asked again after a pause, counted with the tries before it (tries): the record stays proposed, and is reviewed
// at most MostReviewTries times.
func (k *KB) askOperator(ctx context.Context, held *Entry, w *Record, by string, tries int) (string, error) {
version := held.CurrentVersion
if held.Pending != nil {
version = held.PendingVersion
}
unasked := func(why string) (string, error) {
w.Ask = nil
w.Judging = failedJudging(tries+1, why, k.now())
said := fmt.Sprintf("approved by the knowledge base's review, and %s (%s", why, w.Judging.owedSays())
e, err := k.Store.Get(ctx, w.Key())
if err != nil || e == nil {
return "", fmt.Errorf("%s: %s, and that could not be kept (%v)", w.Key(), said, err)
}
if err := k.Store.Save(ctx, Version{By: ReviewBy, At: k.now(), Why: said, Record: w, Insight: e.Pending != nil && e.Pending.IsInsight()},
e.Pending == nil, false, e.Revision); err != nil {
return "", fmt.Errorf("%s: %s, and that could not be kept (%v)", w.Key(), said, err)
}
k.emit(EventReviewed, w, ReviewBy, said, e.Versions+1)
return w.Key().String() + ": " + said, nil
}
if k.Asks == nil {
return unasked("the operator cannot be asked from here")
}
oa := &OperatorAsk{ID: newAskID(w), At: k.now(), Version: version, Said: AskWaiting, Form: AskGlance}
ask := composeAsk(w, oa)
if err := ask.Check(k.now()); err != nil {
return unasked(fmt.Sprintf("the ask could not be made (%v)", err))
}
w.Ask = oa
if err := k.Store.Save(ctx, Version{By: by, At: k.now(), Why: "reviewed: approved; the operator is asked", Record: w, Insight: held.Pending != nil && held.Pending.IsInsight()},
held.Pending == nil, false, held.Revision); err != nil {
return "", fmt.Errorf("%s: the review could not be kept (%v); the operator was not asked", w.Key(), err)
}
k.emit(EventReviewed, w, by, "approved; the operator is asked", held.Versions+1)
if err := k.Asks.Publish(asks.AskSubject(Module), ask, ask.ID); err != nil {
k.logf("[mesh-knowledge] the ask for %s could not be published: %v", w.Key(), err)
return unasked(fmt.Sprintf("the operator could not be asked (%v)", err))
}
return fmt.Sprintf("%s needs the operator: asked on a channel that proves who answers (%q). It is accepted only on the operator's Approve; until then nothing of it is in force.", w.Key(), ask.Headline), nil
}
// routerRecord is the router's record of one ask, as far as this module reads it.
type routerRecord struct {
State string `json:"state"`
// Held is why no channel can carry the ask yet, while its state is held (ADR 0397).
Held string `json:"held"`
Ask *asks.Ask `json:"ask"`
Warrant *asks.Warrant `json:"warrant"`
}
// Reconcile decides every record waiting for the operator whose ask the router has closed: accepted on a warrant
// for Approve that holds, left proposed on Decline, an expiry or a warrant that does not hold. It answers a line
// for each decision.
func (k *KB) Reconcile() ([]string, error) {
if k.Asks == nil {
return nil, nil
}
ctx := context.Background()
k.mu.Lock()
defer k.mu.Unlock()
all, err := k.Store.All(ctx)
if err != nil {
return nil, err
}
var said []string
for _, e := range all {
w := e.Working()
if w.Status != StatusProposed || w.Ask == nil || w.Ask.Said != AskWaiting {
continue
}
entry, err := k.Asks.Record(w.Ask.ID)
if err != nil || entry == nil {
if err == nil && k.now().After(w.Ask.At.Add(AskLasts+time.Hour)) {
said = append(said, k.endAsk(ctx, e, w, "ended: the router keeps no record of its ask (refused before it was taken, or forgotten)"))
}
continue
}
var rec routerRecord
if json.Unmarshal(entry.Value, &rec) != nil {
continue
}
// The router holds an ask no channel can carry yet (ADR 0397, issue 926): the record says so, and why,
// until it is offered, rather than saying it waits for an answer nobody was shown.
if held := heldSays(rec); held != w.Ask.Held && (rec.State == "open" || rec.State == "held") {
said = append(said, k.keepHeld(ctx, e, w, held))
}
if rec.State == "open" || rec.State == "held" || rec.Warrant == nil {
continue
}
said = append(said, k.decide(ctx, e, w, *rec.Warrant))
}
return said, nil
}
// heldSays is why the router holds an ask, in words, or "" when it does not.
func heldSays(rec routerRecord) string {
if rec.State != "held" {
return ""
}
if rec.Held == "" {
return "the router holds it; no channel can carry it yet"
}
return rec.Held
}
// keepHeld records, on the record that waits for the operator, that the router holds its ask and why (or that it
// no longer does). The ask stays waiting.
func (k *KB) keepHeld(ctx context.Context, e *Entry, w *Record, held string) string {
w = w.Clone()
w.Ask.Held = held
why := "the operator's ask is offered by the router"
if held != "" {
why = "the operator's ask is held by the router: " + held
}
if err := k.Store.Save(ctx, Version{By: "the knowledge base", At: k.now(), Why: why, Record: w, Insight: e.Pending != nil && e.Pending.IsInsight()},
e.Pending == nil, false, e.Revision); err != nil {
return fmt.Sprintf("%s: what became of its ask could not be kept (%v)", w.Key(), err)
}
return fmt.Sprintf("%s: %s", w.Key(), why)
}
// endAsk records an ask ended without the operator's choice: the record stays proposed.
func (k *KB) endAsk(ctx context.Context, e *Entry, w *Record, why string) string {
w = w.Clone()
w.Ask.Said = why
if err := k.Store.Save(ctx, Version{By: "the knowledge base", At: k.now(), Why: "the operator's ask " + why, Record: w, Insight: e.Pending != nil && e.Pending.IsInsight()},
e.Pending == nil, false, e.Revision); err != nil {
return fmt.Sprintf("%s: the end of its ask could not be kept (%v)", w.Key(), err)
}
k.emit(EventReviewed, w, "the knowledge base", "the operator's ask "+why, e.Versions+1)
return fmt.Sprintf("%s: the operator's ask %s; it stays proposed", w.Key(), why)
}
// decide holds one closed ask's warrant to the record, and accepts the record when it is the operator's Approve.
func (k *KB) decide(ctx context.Context, e *Entry, w *Record, warrant asks.Warrant) string {
w = w.Clone()
w.Ask.Held = "" // an ask that ended is held no longer
if warrant.Outcome != asks.OutcomeChosen {
why := "ended " + string(warrant.Outcome)
if warrant.Words != "" {
why += " (" + warrant.Words + ")"
}
return k.endAsk(ctx, e, w, why)
}
ask := composeAsk(w, w.Ask)
option, err := warrant.For(Module, ask)
if err == nil {
err = option.Performs(act(w, w.Ask, option.ID))
}
if err != nil {
k.logf("[mesh-knowledge] REFUSED the answer to the ask for %s: %v", w.Key(), err)
return k.endAsk(ctx, e, w, "was answered, and the answer does not hold here: "+err.Error())
}
switch option.ID {
case OptionApprove:
w = w.Clone()
w.Ask.Said, w.Ask.Warrant = AskApproved, warrant.Says()
r, err := k.accept(ctx, e, w, "the operator", "accepted on the operator's Approve after the knowledge base's review", "warrant: "+warrant.Says())
if err != nil {
return err.Error()
}
return fmt.Sprintf("%s: %s on the operator's warrant (%s)", r.Key(), r.Status, warrant.Says())
case OptionDecline:
w = w.Clone()
w.Ask.Warrant = warrant.Says()
return k.endAsk(ctx, e, w, "declined by the operator")
}
return k.endAsk(ctx, e, w, "was answered with "+option.ID+", which it does not offer")
}
// HeardEnd takes the router's word that one of this module's asks ended without a choice: nothing is accepted on
// such a word, so it needs no proof beyond its subject. A choice is only ever taken from the router's record.
func (k *KB) HeardEnd(w asks.Warrant) {
if w.Asker != Module || w.Outcome == asks.OutcomeChosen {
if _, err := k.Reconcile(); err != nil {
k.logf("[mesh-knowledge] reading the router's answers: %v", err)
}
return
}
ctx := context.Background()
k.mu.Lock()
defer k.mu.Unlock()
all, err := k.Store.All(ctx)
if err != nil {
return
}
for _, e := range all {
r := e.Working()
if r.Ask != nil && r.Ask.ID == w.Ask && r.Ask.Said == AskWaiting {
k.logf("[mesh-knowledge] %s", k.decide(ctx, e, r, w))
}
}
}