M1-M5: the router holds back the asker's oldest ask behind a fourth and offers it again when one ends; the seat operator-channel's ask-again offers it now (milestone 47/G, issue 1966)

Claude-Session: https://claude.ai/code/session_01JG73KK8HD1gufnqyFxMFdW

ask-again is the seat operator-channel's verb (key in; state or error out), not the module tool messenger_ask_again, and records no hand act (G §394, §450).
(Joined from 681975ec.)
Builds: design 47 G §40, §83, §403 (RTR1, RTR2)

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
This commit is contained in:
2026-11-11 14:48:07 +01:00
parent 85023b3b52
commit 6f7d070769
9 changed files with 371 additions and 24 deletions
@@ -592,12 +592,15 @@ func TestAnAskIsRefusedInWordsToItsAsker(t *testing.T) {
}
}
all := strings.Join(refused, "\n")
for _, want := range []string{"d-1: the ask is refused: an ask that destroys lasts at most", "d-2: its words carry",
"d-6: its asker already has 3 open asks"} {
// A fourth offered ask is never refused (ADR 0666 §12): it is taken and the asker's oldest is held back.
for _, want := range []string{"d-1: the ask is refused: an ask that destroys lasts at most", "d-2: its words carry"} {
if !strings.Contains(all, want) {
t.Errorf("not refused: %q in\n%s", want, all)
}
}
if strings.Contains(all, "d-6") || store.asks["mesh-delivery.d-6"].State != StateOpen || store.asks["mesh-delivery.d-3"].State != StateHeldBack {
t.Errorf("the fourth ask was refused or the oldest not held back: d-6 %s, d-3 %s", store.asks["mesh-delivery.d-6"].State, store.asks["mesh-delivery.d-3"].State)
}
if _, kept := store.asks["mesh-delivery.d-1"]; kept {
t.Error("a refused ask was recorded")
}
+34 -13
View File
@@ -65,6 +65,9 @@ type AskRecord struct {
Warrant *oc.Warrant `json:"warrant,omitempty"`
// Told: the warrant, or the end, was published to the asker.
Told bool `json:"told,omitempty"`
// HeldBackSince is, while the ask is held back, since when: kept open behind newer asks of its asker, its tickets
// withdrawn, offered again when one of its asker's offered asks ends (novox/hq ADR 0666 §12).
HeldBackSince time.Time `json:"held-back-since,omitempty"`
// HeldSince and HeldWhy say, while the ask is held, since when and why no channel can carry it: per kind, the
// reason from the judgement that judges an answer. The asker reads them in this record, as it reads
// the state (the asks protocol has no word for a held ask on `decided`, which means an ask ended).
@@ -558,23 +561,24 @@ func (h *Holder) TakeAsk(verb, asker string, body json.RawMessage) error {
h.refuseAsk(asker, a, "its whole words carry "+refusal.String()+", which may not leave the mesh on any channel: withhold it")
return nil
}
// A fourth offered ask is taken, never refused: the asker's oldest offered one is held back once this one is recorded
// (holdBackOldest). An ask with the same `about` replaces the open one, a held-back one too.
open, replaced := 0, ""
h.mu.Lock()
for k, e := range h.asks {
if e.rec.Asker != asker || (e.rec.State != StateOpen && e.rec.State != StateHeld) {
if e.rec.Asker != asker || !liveState(e.rec.State) {
continue
}
if a.About != "" && e.rec.Ask.About == a.About {
replaced = k
continue
}
open++
if e.rec.State != StateHeldBack {
open++
}
}
h.mu.Unlock()
if open >= occ.MostOpen {
h.refuseAsk(asker, a, fmt.Sprintf("its asker already has %d open asks", open))
return nil
}
holdBack := open >= occ.MostOpen
rec := AskRecord{Asker: asker, Ask: a, State: StateOpen, Opened: now, Tickets: h.ticketsFor(a)}
if len(rec.Tickets) == 0 {
// Held, not refused (ADR 0397): the ask breaks no rule, and the operator is asked when a channel can carry it.
@@ -605,6 +609,9 @@ func (h *Holder) TakeAsk(verb, asker string, body json.RawMessage) error {
} else {
h.keepAsk(key, rec, 0)
}
if holdBack {
h.holdBackOldest(asker, key)
}
if rec.State == StateHeld {
h.Logf("[messenger] %s asks %q (%s): held, not shown: %s", asker, a.Headline, a.ID, rec.HeldWhy)
return nil
@@ -897,12 +904,14 @@ func (h *Holder) closeAsk(key string, outcome string, words string, w *oc.Warran
rec, rev = *got, r
}
}
if rec.State != StateOpen && rec.State != StateHeld {
if !liveState(rec.State) {
h.keepAsk(key, rec, rev)
return ErrChanged
}
now := h.Now()
freed := rec.State == StateOpen || rec.State == StateHeld // a slot of the asker's offered asks, unless a newer ask takes it
rec.State = string(outcome)
rec.HeldBackSince = time.Time{}
rec.HeldWhy = ""
rec.Closed = now
if t, ok := rec.Tickets[used]; ok {
@@ -926,6 +935,9 @@ func (h *Holder) closeAsk(key string, outcome string, words string, w *oc.Warran
rev = newRev
}
h.keepAsk(key, rec, rev)
if freed && outcome != occ.OutcomeReplaced {
defer h.offerHeldBack(rec.Asker)
}
h.sayClosed(key, rec, outcome, words)
if rec.Asker == SelfAsker {
// The router's own ask: it does what was decided, and tells nobody else (selfact.go).
@@ -1016,7 +1028,7 @@ func (h *Holder) endLeftovers() {
if r.Origin != "ask" || r.Ask == "" || !r.Cleared.IsZero() {
continue
}
if e := h.asks[r.Ask]; e != nil && e.rec.State != StateOpen && e.rec.Record == r.Key {
if e := h.asks[r.Ask]; e != nil && !liveState(e.rec.State) && e.rec.Record == r.Key {
ended = append(ended, e.rec)
}
}
@@ -1081,9 +1093,9 @@ func (h *Holder) sweepAsks(now time.Time) {
var expired, untold []string
for k, e := range h.asks {
switch {
case (e.rec.State == StateOpen || e.rec.State == StateHeld) && !now.Before(askExpires(e.rec.Ask)):
case liveState(e.rec.State) && !now.Before(askExpires(e.rec.Ask)):
expired = append(expired, k)
case e.rec.State != StateOpen && e.rec.State != StateHeld && !e.rec.Told && e.rec.Warrant != nil:
case !liveState(e.rec.State) && !e.rec.Told && e.rec.Warrant != nil:
untold = append(untold, k)
}
}
@@ -1195,6 +1207,8 @@ func (h *Holder) judge(kind string, c oc.Choice, key string, root func(KindInfo)
return "chosen", nil
}
switch {
case rec.State == StateHeldBack:
return h.refuseChoice(kind, c, "the question waits behind newer questions of its asker, and is offered again when one of them ends")
case !issued:
return h.refuseChoice(kind, c, "it belongs to no ask this router holds")
case t.Kind != kind:
@@ -1386,6 +1400,10 @@ type AskView struct {
// HeldSince and HeldWhy: while the ask is held, since when and why no channel can carry it yet (ADR 0397 §4).
HeldSince string `json:"held_since,omitempty"`
HeldWhy string `json:"held_why,omitempty"`
// HeldBackSince and Behind: while the ask is held back (ADR 0666 §12), since when and the keys of the newer asks of its
// asker it waits behind, in the names of the seat operator-channel's asks answer.
HeldBackSince string `json:"held-since,omitempty"`
Behind []string `json:"behind,omitempty"`
}
// AsksNow is every ask, open first, newest first.
@@ -1400,6 +1418,9 @@ func (h *Holder) AsksNow() []AskView {
if e.rec.State == StateHeld {
v.HeldSince, v.HeldWhy = e.rec.HeldSince.UTC().Format(time.RFC3339), e.rec.HeldWhy
}
if e.rec.State == StateHeldBack {
v.HeldBackSince, v.Behind = e.rec.HeldBackSince.UTC().Format(time.RFC3339), h.behindLocked(k, e.rec)
}
kinds := map[string]bool{}
for _, t := range e.rec.Tickets {
kinds[t.Kind] = true
@@ -1418,7 +1439,7 @@ func (h *Holder) AsksNow() []AskView {
out = append(out, v)
}
sort.Slice(out, func(i, j int) bool {
oi, oj := out[i].State == StateOpen || out[i].State == StateHeld, out[j].State == StateOpen || out[j].State == StateHeld
oi, oj := liveState(out[i].State), liveState(out[j].State)
if oi != oj {
return oi
}
@@ -1479,7 +1500,7 @@ func (h *Holder) sayClosed(key string, rec AskRecord, outcome string, words stri
}
}
// AsksQuery narrows what the tool `operator-channel.asks` answers (issue 595): Key one ask, State open (open and held) or
// AsksQuery narrows what the tool `operator-channel.asks` answers (issue 595): Key one ask, State open (open, held and held-back) or
// ended (all takes both), Since the asks opened at or after that time, Bound the most bytes of the answer's asks. With no
// argument at all the query is the open asks within DefaultAsksBound (issue 1974), and the answer says so.
type AsksQuery struct {
@@ -1543,7 +1564,7 @@ func (q AsksQuery) Narrow(all []AskView) AsksAnswer {
}
var kept []AskView
for _, v := range all {
open := v.State == StateOpen || v.State == StateHeld
open := liveState(v.State)
switch {
case q.Key != "" && v.Key != q.Key,
q.State == "open" && !open,
+5 -5
View File
@@ -105,17 +105,17 @@ func TestAHeldAskExpiresAsAnyOpenAskDoes(t *testing.T) {
}
}
func TestAHeldAskCountsTowardTheOpenAsksOfItsAsker(t *testing.T) {
h, _, _, c, _, w := newRouter(t)
func TestAHeldAskCountsTowardTheOffersOfItsAskerAndTheOldestIsHeldBack(t *testing.T) {
h, _, _, c, store, w := newRouter(t)
withoutVerifiedSender(h)
for i := 1; i <= 4; i++ {
a := heldAsk(c, "d-"+string(rune('0'+i)))
a.About = "about-" + string(rune('0'+i))
submit(t, h, "mesh-delivery", a)
c.pass(time.Second)
}
got := w.of("mesh-delivery")
if len(got) != 1 || got[0].Outcome != occ.OutcomeRefused || !strings.Contains(got[0].Words, "already has 3 open asks") {
t.Fatalf("%+v", got)
if got := w.of("mesh-delivery"); len(got) != 0 || store.asks["mesh-delivery.d-1"].State != StateHeldBack || store.asks["mesh-delivery.d-4"].State != StateHeld {
t.Fatalf("%+v: a fourth is taken, never refused; the oldest is held back (%s)", got, store.asks["mesh-delivery.d-1"].State)
}
}
+197
View File
@@ -0,0 +1,197 @@
package main
import (
"fmt"
oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
"sort"
"time"
occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient"
)
// An ask held back (novox/hq ADR 0666 §12, design 46 § 6): an asker has at most MostOpen asks offered at once; a fourth
// is offered and nothing is refused, so no ask is hidden: the asker's oldest offered ask is held back, its tickets
// withdrawn and its message edited to say it waits behind newer asks, and it is offered again, with new tickets, when
// one of the asker's offered asks ends. `held` is another state, an ask no ready channel can carry (ADR 0397).
// StateHeldBack is an ask kept open behind newer asks of its asker.
const StateHeldBack = "held-back"
// liveState is whether an ask has not ended: offered, held for want of a channel, or held back.
func liveState(state string) bool {
return state == StateOpen || state == StateHeld || state == StateHeldBack
}
// behindLocked are the keys of the newer offered asks of the asker of rec, oldest first; h.mu is held.
func (h *Holder) behindLocked(key string, rec AskRecord) []string {
var behind []string
for k, e := range h.asks {
if k != key && e.rec.Asker == rec.Asker && (e.rec.State == StateOpen || e.rec.State == StateHeld) && e.rec.Opened.After(rec.Opened) {
behind = append(behind, k)
}
}
sort.Slice(behind, func(i, j int) bool {
a, b := h.asks[behind[i]].rec, h.asks[behind[j]].rec
if !a.Opened.Equal(b.Opened) {
return a.Opened.Before(b.Opened)
}
return behind[i] < behind[j]
})
return behind
}
// pickLocked is the key of the asker's asks in one of the states, the oldest first (by opening, then key); "" when
// there is none, never except; h.mu is held.
func (h *Holder) pickLocked(asker, except string, states ...string) string {
pick := ""
for k, e := range h.asks {
if e.rec.Asker != asker || k == except {
continue
}
in := false
for _, s := range states {
in = in || e.rec.State == s
}
if !in {
continue
}
if pick == "" || e.rec.Opened.Before(h.asks[pick].rec.Opened) || (e.rec.Opened.Equal(h.asks[pick].rec.Opened) && k < pick) {
pick = k
}
}
return pick
}
// offeredCount is how many of the asker's asks are offered now: open, or held for want of a channel.
func (h *Holder) offeredCount(asker string) int {
h.mu.Lock()
defer h.mu.Unlock()
n := 0
for _, e := range h.asks {
if e.rec.Asker == asker && (e.rec.State == StateOpen || e.rec.State == StateHeld) {
n++
}
}
return n
}
// freshAsk is an ask's record read again from the store, where there is one.
func (h *Holder) freshAsk(key string) (AskRecord, uint64, bool) {
e := h.askOf(key)
if e == nil {
return AskRecord{}, 0, false
}
rec, rev := e.rec, e.rev
if h.Asks != nil {
if got, r, err := h.Asks.GetAsk(key); err == nil && got != nil {
rec, rev = *got, r
}
}
return rec, rev, true
}
// holdBackOldest holds back the asker's oldest offered ask other than except (the newest, just taken): its tickets
// are withdrawn, it stays open, and its own message is edited to say it waits. One held for want of a channel counts as offered
// and is held back the same way. The work lock is held.
func (h *Holder) holdBackOldest(asker, except string) {
h.mu.Lock()
key := h.pickLocked(asker, except, StateOpen, StateHeld)
h.mu.Unlock()
if key == "" {
return
}
rec, rev, ok := h.freshAsk(key)
if !ok || (rec.State != StateOpen && rec.State != StateHeld) {
return
}
now := h.Now()
rec.State, rec.HeldBackSince, rec.Tickets, rec.HeldSince, rec.HeldWhy = StateHeldBack, now, nil, time.Time{}, ""
if !h.saveHeld(key, rec, rev) {
return
}
h.Logf("[messenger] %s's ask %q (%s) is held back behind its newer asks: it is offered again when one ends", asker, rec.Ask.Headline, rec.Ask.ID)
h.mu.Lock()
r := h.open[rec.Record]
h.mu.Unlock()
if r == nil || r.Origin != "ask" {
return // a condition's message is the newer ask's to show
}
h.mu.Lock()
r.Answered = "Waits behind newer questions of " + asker + ".\nIt is offered again, with its answers, when one of them ends."
h.mu.Unlock()
h.edit(r, "answered", false)
h.persist(r)
}
// offerHeldBack offers the asker's oldest held-back ask again when a slot is free: new tickets, its message edited back
// to the question. The work lock is held.
func (h *Holder) offerHeldBack(asker string) {
if h.offeredCount(asker) >= occ.MostOpen {
return
}
h.mu.Lock()
key := h.pickLocked(asker, "", StateHeldBack)
h.mu.Unlock()
if key != "" {
h.offerOneHeldBack(key)
}
}
// offerOneHeldBack offers one held-back ask again now.
func (h *Holder) offerOneHeldBack(key string) {
rec, rev, ok := h.freshAsk(key)
if !ok || rec.State != StateHeldBack {
return
}
now := h.Now()
rec.HeldBackSince = time.Time{}
rec.Tickets = h.ticketsFor(rec.Ask)
rec.State = StateOpen
if len(rec.Tickets) == 0 {
rec.Tickets, rec.State, rec.HeldSince, rec.HeldWhy = nil, StateHeld, now, h.holdWhy(rec.Ask)
}
if !h.saveHeld(key, rec, rev) {
return
}
h.Logf("[messenger] %s's ask %q (%s) is offered again", rec.Asker, rec.Ask.Headline, rec.Ask.ID)
if rec.State != StateOpen {
return
}
h.mu.Lock()
r := h.open[rec.Record]
h.mu.Unlock()
if r != nil && r.Origin == "ask" {
h.mu.Lock()
r.Answered, r.Ask = "", key
h.mu.Unlock()
h.offerAsk(r)
h.persist(r)
return
}
rec.Record = h.recordFor(rec.Asker, key, rec.Ask)
h.showAsk(key, rec, now)
}
// AskAgain is the seat operator-channel's verb ask-again (G §394, §450): offer a held-back ask now, holding back the
// asker's oldest offered ask in its place when the asker already has the most offered. It answers the ask's state after it,
// or the error that says why it was not offered.
func (h *Holder) AskAgain(key string) oc.AskAgainAnswer {
h.work.Lock()
defer h.work.Unlock()
h.mu.Lock()
h.init()
h.mu.Unlock()
rec, _, ok := h.freshAsk(key)
switch {
case !ok:
return oc.AskAgainAnswer{Error: fmt.Sprintf("this router holds no ask %q: its key is <asker>.<id>, as operator-channel.asks lists", key)}
case rec.State != StateHeldBack:
return oc.AskAgainAnswer{Error: fmt.Sprintf("the ask %s is %s, not held back: only a held-back ask is offered again", key, rec.State)}
}
if h.offeredCount(rec.Asker) >= occ.MostOpen {
h.holdBackOldest(rec.Asker, key)
}
h.offerOneHeldBack(key)
got, _, _ := h.freshAsk(key)
return oc.AskAgainAnswer{State: string(got.State)}
}
@@ -0,0 +1,113 @@
package main
import (
"encoding/json"
"strings"
"testing"
"time"
occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient"
)
// ADR 0666 §12, design 46 §6 and test 71: an asker has at most three asks offered at once; a fourth is offered and the
// oldest is held back (its tickets withdrawn, its message edited, its record open), offered again with new tickets
// when one of the three ends, offered now by messenger_ask_again; an ask with the same `about` replaces a held-back
// one, and a Decline ends an ask whose asker may ask again at once.
func TestNoAskIsHiddenTheOldestOfFourIsHeldBackAndOfferedAgain(t *testing.T) {
h, tg, _, c, store, w := newRouter(t)
link(h, store, c, "42", c.now().Add(-2*time.Hour))
ask := func(id, about string) {
a := ackAsk(c, id)
a.About = about
submit(t, h, "mesh-delivery", a)
c.pass(time.Second)
settle(h, c)
}
state := func(id string) string { return store.asks["mesh-delivery."+id].State }
for i, id := range []string{"d-1", "d-2", "d-3"} {
ask(id, "about-"+string(rune('1'+i)))
}
oldTicket := ""
for tk := range store.asks["mesh-delivery.d-1"].Tickets {
oldTicket = tk
}
ask("d-4", "about-4")
// M1: the fourth reaches the channel, the oldest is held back, none is refused.
if len(w.of("mesh-delivery")) != 0 || state("d-4") != StateOpen || state("d-1") != StateHeldBack {
t.Fatalf("refused %+v; d-4 %s, d-1 %s", w.of("mesh-delivery"), state("d-4"), state("d-1"))
}
if rec := store.asks["mesh-delivery.d-1"]; len(rec.Tickets) != 0 || rec.HeldBackSince.IsZero() {
t.Errorf("held back %+v: tickets withdrawn, since set", rec)
}
if m, ok := tg.edits["1"]; !ok || len(m.Actions) != 0 || !strings.Contains(m.Note, "Waits behind newer questions of mesh-delivery") {
t.Errorf("its message was not edited to say it waits: %v %+v", ok, m)
}
if _, err := h.Chosen("telegram", choiceOf(oldTicket, "42", c.now())); err == nil || !strings.Contains(err.Error(), "waits behind newer questions") || len(w.of("mesh-delivery")) != 0 {
t.Errorf("a tap of a withdrawn ticket: %v %+v", err, w.of("mesh-delivery"))
}
// M3: asks lists it with since when and the newer asks it waits behind; open includes it.
q, _ := AsksQueryOf(map[string]any{"key": "mesh-delivery.d-1"})
raw, _ := json.Marshal(q.Narrow(h.AsksNow()))
for _, want := range []string{`"state":"held-back"`, `"held-since":"`, `"behind":["mesh-delivery.d-2","mesh-delivery.d-3","mesh-delivery.d-4"]`} {
if !strings.Contains(string(raw), want) {
t.Errorf("asks lacks %s: %s", want, raw)
}
}
if q, _ = AsksQueryOf(nil); !strings.Contains(string(mustJSON(q.Narrow(h.AsksNow()))), "mesh-delivery.d-1") {
t.Error("the open asks leave out the held-back one")
}
// M5: an ask with the same about replaces a held-back one; the three offered stay three, so the oldest of them is held back.
ask("d-5", "about-1")
if state("d-1") != string(occ.OutcomeReplaced) || state("d-2") != StateHeldBack || state("d-5") != StateOpen || state("d-3") != StateOpen {
t.Fatalf("after d-5 about the held-back ask's thing: d-1 %s d-2 %s d-3 %s d-5 %s", state("d-1"), state("d-2"), state("d-3"), state("d-5"))
}
// M2: when one of the offered ends, the oldest held back is offered again with new tickets and its message asks again.
if a := h.AskAgain("mesh-delivery.d-3"); !strings.Contains(a.Error, "not held back") {
t.Errorf("ask-again of an offered ask: %+v", a)
}
if err := h.TakeAsk("cancel", "mesh-delivery", []byte(`{"id":"d-3"}`)); err != nil {
t.Fatal(err)
}
settle(h, c)
if rec := store.asks["mesh-delivery.d-2"]; rec.State != StateOpen || len(rec.Tickets) == 0 || !rec.HeldBackSince.IsZero() {
t.Fatalf("d-2 after d-3 ended: %+v", rec)
}
if m := tg.edits["2"]; len(m.Actions) == 0 || strings.Contains(m.Note, "Waits behind") {
t.Errorf("its message does not ask again: %+v", m)
}
// M4: ask-again by hand offers a held-back ask now, holding back the oldest offered one; the seat operator-channel's ask-again (G §450).
ask("d-6", "about-6")
ask("d-7", "about-7")
held := ""
for _, id := range []string{"d-2", "d-4", "d-5", "d-6", "d-7"} {
if state(id) == StateHeldBack {
held = id
}
}
if held == "" {
t.Fatal("no ask is held back after two more")
}
if a := h.AskAgain("mesh-delivery.d-none"); a.Error == "" || a.State != "" {
t.Errorf("ask-again of an ask the router holds none of: %+v", a)
}
if a := h.AskAgain("mesh-delivery." + held); a.Error != "" || a.State != string(StateOpen) || state(held) != StateOpen {
t.Fatalf("%s is %s; ask-again answered %+v", held, state(held), a)
}
n := 0
for _, id := range []string{"d-2", "d-4", "d-5", "d-6", "d-7"} {
if state(id) == StateHeldBack {
n++
}
}
if n != 2 || state("d-5") != StateHeldBack {
t.Errorf("%d asks held back after ask-again (d-5 %s): the oldest offered one took its place", n, state("d-5"))
}
// A Decline ends an ask; its asker may ask again at once, and nothing is refused.
for _, wt := range w.of("mesh-delivery") {
if wt.Outcome == occ.OutcomeRefused {
t.Errorf("refused: %+v", wt)
}
}
}
func mustJSON(v any) []byte { raw, _ := json.Marshal(v); return raw }
+10 -1
View File
@@ -741,7 +741,7 @@ func tools(h *Holder, l *listening) []stdio.Tool {
"most bytes of the answer.",
Input: map[string]any{
"key": str("one ask, by its key (asker.id); none for the open asks"),
"state": str("open (open and held), ended or all; none is open when no key or since is given, else both"),
"state": str("open (open, held and held-back), ended or all; none is open when no key or since is given, else both"),
"since": str("only the asks opened at or after this time, RFC 3339"),
"bound": map[string]any{"type": "integer", "description": "the most bytes of the asks in the answer; said when it cuts the answer short"}},
Run: func(a map[string]any) (any, error) {
@@ -751,6 +751,15 @@ func tools(h *Holder, l *listening) []stdio.Tool {
}
return q.Narrow(h.AsksNow()), nil
}},
{Name: "operator-channel." + oc.VerbAskAgain,
Description: "Offer a held-back ask again now (G §394, §450): an asker has at most three asks offered at once, and a fourth " +
"holds its oldest back behind it (operator-channel.asks lists it with state held-back, since when and the newer asks it waits behind). " +
"It is offered again by itself when one of the asker's asks ends; this offers it now, holding back the asker's oldest offered ask in its " +
"place. Answers the ask's state, or why it was not offered (an ask that is not held back).",
Input: map[string]any{"key": str("the held-back ask, by its key (asker.id)")},
Run: func(a map[string]any) (any, error) {
return h.AskAgain(strArg(a, "key")), nil
}},
{Name: "messenger_identities",
Description: "The accounts linked as the operator, one per channel kind: the account, the name its " +
"service shows, when it was linked, and from when it can approve (an hour after linking).",
@@ -68,7 +68,7 @@ func readManifest(t *testing.T) (manifest, string) {
func TestItDeclaresTheSeatsOfAnAskAndHoldsTheOperatorChannel(t *testing.T) { // 1217/AC6: condition beside ask and cancel
m, _ := readManifest(t)
verbs := []string{"open", "history", "notify", "tell", "asks"}
verbs := []string{"open", "history", "notify", "tell", "asks", oc.VerbAskAgain}
want := []seat{
{Name: oc.Seat, Scope: "mesh", Serves: verbs, Accepts: []string{"ask", "cancel", "condition"}, Emits: []string{"decided"},
ByCaller: []string{"ask", "cancel", "condition", "decided"}, Records: []string{"asks"}},
+2
View File
@@ -3,6 +3,7 @@ module messenger
go 1.26.0
require (
git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel v0.0.0-00010101000000-000000000000
@@ -12,6 +13,7 @@ require (
)
require (
git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshhandacts v0.0.0-00010101000000-000000000000 // indirect
github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op // indirect
github.com/google/go-tpm v0.9.8 // indirect
github.com/klauspost/compress v1.20.1 // indirect
+4 -2
View File
@@ -12,7 +12,8 @@
"history",
"notify",
"tell",
"asks"
"asks",
"ask-again"
],
"accepts": [
"ask",
@@ -66,7 +67,8 @@
"history",
"notify",
"tell",
"asks"
"asks",
"ask-again"
]
}
],