An ask reads at a glance on the phone: a headline, a summary and its points, the rest in a note (issues 383 and 639)

The operator would not read the Telegram ask for ADR 0343: thirty plain lines, every clause on one, no bold,
no summary. A settings proposal was the same wall of text, its values masked so the approval was blind.

- messenger: an ask's message is the asker's headline and explanation alone; who asked and when, what each
  answer does, the expiry, where else it is answered and the asker's Details travel as the message's note
  (asks.Message.Note). Answered, the message is one line, "You chose Approve at 16:06 on Telegram ·
  <headline>". An explanation that does not read at a glance (asks.Glance) is shown and logged. On a kind
  that proves who answers, an ask's values are shown whole (asks.Ask.Whole), held to the secret rule alone
  (from the earlier work on issue 383, #178).
- telegram: every message in HTML, never reworded: the title bold, a key line monospace, the note apart in
  a collapsed blockquote; markup escaped. A tap is compared by its words, whitespace aside, and a message
  shown before is still compared by the digest it was kept under.
- desk-channel: the note follows the body.
- mesh-knowledge: a record that needs the operator carries a brief (headline, summary, at most three
  points), the 30th per-record rule; its ask shows the brief and never the clauses. An ask made before is
  composed again as it was asked (form), so its warrant still holds.
- claude-code: an instructions proposal's explanation is one summary line (the proposer's, or the
  module's), its text in the Details; a question's context is at most 300 characters and its recommendation
  one point. Older proposals and questions keep their form.

The SDK is pinned at mesh-sdk 9588871 (fix/639-an-ask-reads-at-a-glance), which adds Message.Note and
asks.Glance.
This commit is contained in:
jochen
2026-10-13 17:16:50 +02:00
parent 9c166398f2
commit eadf301f9f
46 changed files with 1276 additions and 162 deletions
@@ -84,9 +84,48 @@ func fingerprint(digest string) string {
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.
// 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
@@ -111,9 +150,15 @@ func composeAsk(r *Record, a *OperatorAsk) asks.Ask {
}
}
fmt.Fprintf(&b, "\n\nRead it whole with the knowledge base's get, number %d; its fingerprint is %s.", r.Number, fingerprint(r.Digest()))
out := asks.Ask{ID: a.ID, Headline: headline, Explanation: b.String(), Who: asks.Operator,
Expires: a.At.Add(AskLasts), OnExpiry: "the record stays proposed; nothing of it is in force",
About: "knowledge." + r.Kind + "." + strconv.Itoa(r.Number)}
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"},
@@ -157,7 +202,7 @@ func (k *KB) askOperator(ctx context.Context, held *Entry, w *Record, by string,
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}
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))