D2: assigner_ask_again asks one of the assigner's asks of the operator again by hand (issue 1966, ADR 0668 §5)
The recovery of an ask that ended at the channel unheard needed a restart. The tool writes the key again under a new asking by the ask-ended rules (a proposal's authorising ask and a put's record ask are refused), refuses an asking the operator answered, naming the answer, and records the hand act through the seat mesh-hand-acts. One tool added to the manifest. Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient"
|
||||
|
||||
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-assigner/asks"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-teller/tellerevents"
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
|
||||
"mesh-delivery-assigner/internal/assign"
|
||||
"mesh-delivery-assigner/internal/buckets"
|
||||
"mesh-delivery-assigner/internal/puts"
|
||||
)
|
||||
|
||||
// askAgainTool asks one of the assigner's asks again by hand (issue 1966, ADR 0668 §5): the recovery of an ask that ended at
|
||||
// the operator channel without an answer when the event that would have asked it again was not heard.
|
||||
const askAgainTool = "assigner_ask_again"
|
||||
|
||||
// asksState and answersState are the assigner's asks and the teller's answers, as the runtime names them.
|
||||
const (
|
||||
asksState = "asks"
|
||||
answersState = buckets.TellerModule + ".answers"
|
||||
)
|
||||
|
||||
// askAgainByHand writes the ask at key again under a new asking, by the rules the teller's ask-ended writes it by (assign.AskedAgain:
|
||||
// a proposal's authorising ask and a put's record ask are not), so the teller asks it again; an asking the operator answered
|
||||
// is refused, naming the answer. The act is recorded through the seat mesh-hand-acts.
|
||||
func askAgainByHand(st store, ask func(string, any) (json.RawMessage, error), key, why, by string, now time.Time) (any, error) {
|
||||
switch {
|
||||
case key == "":
|
||||
return nil, errors.New("name the ask's key, as assigner_read_asks lists it")
|
||||
case why == "":
|
||||
return nil, errors.New("a hand ask again says why")
|
||||
case by == "" || by == stdio.Unproven:
|
||||
return nil, errors.New("a hand ask again names who asks it, and the runtime proved no caller")
|
||||
}
|
||||
current, _, found, err := st.get(asksState, key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !found {
|
||||
return nil, fmt.Errorf("the assigner holds no ask under %s: assigner_read_asks lists them", key)
|
||||
}
|
||||
_, routerAsk, err := assign.DecodeAsk(key, current)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if raw, _, found, err := st.get(answersState, key); err == nil && found {
|
||||
if a, err := meshdeliveryclient.DecodeAnswer(raw); err == nil && strings.HasPrefix(a.Warrant, routerAsk.ID+"@") {
|
||||
return nil, fmt.Errorf("the asking %s was answered %q (%s): it is not asked again", routerAsk.ID, a.Option, a.Words)
|
||||
}
|
||||
}
|
||||
var put *puts.Put
|
||||
if k, err := asks.ParseAnyAskKey(key); err == nil && k.Kind == asks.AskKindRecord {
|
||||
if pkey, err := puts.Key(k.Node, k.Module); err == nil {
|
||||
if raw, _, found, err := st.get(putsState, pkey); err == nil && found {
|
||||
if p, err := puts.Decode(raw); err == nil {
|
||||
put = &p
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
value, refused := assign.AskedAgain(tellerevents.Ended{Key: key, Ask: routerAsk.ID, Outcome: occ.OutcomeCancelled}, current, put, now)
|
||||
if refused != "" {
|
||||
return nil, errors.New(refused)
|
||||
}
|
||||
if err := st.put(asksState, key, value); err != nil {
|
||||
return nil, fmt.Errorf("the ask %s is not written again: %w", key, err)
|
||||
}
|
||||
_, next, _ := assign.DecodeAsk(key, value)
|
||||
taken, rerr := meshhandactsclient.Recorder{Ask: ask}.Record(meshhandactsclient.Request{What: askAgainTool + " " + key, Why: why + " (" + by + ")", Cause: "ask-again"})
|
||||
recorded := taken.Log
|
||||
if rerr != nil {
|
||||
recorded = "not recorded: " + rerr.Error()
|
||||
}
|
||||
return map[string]any{"key": key, "ended": routerAsk.ID, "asked": next.ID, "by": by, "hand-act": recorded}, nil
|
||||
}
|
||||
|
||||
// askAgainTools is the tool over the runtime's states.
|
||||
func askAgainTools(st store, ask func(string, any) (json.RawMessage, error), now func() time.Time) []stdio.Tool {
|
||||
return []stdio.Tool{{Name: askAgainTool, Description: "Ask one of the assigner's asks of the operator again (issue 1966): the ask " +
|
||||
"under key is written again under a new asking, which the teller asks on the operator channel, as when the channel says it " +
|
||||
"ended without an answer. Refused for an asking the operator answered, a proposal's authorising ask (the seat's verb again " +
|
||||
"asks it) and a record ask a put asked. Recorded as a hand act.",
|
||||
Input: schema(map[string]string{"key": "the ask's key, as assigner_read_asks lists it", "why": "why it is asked again"},
|
||||
[]string{"key", "why"}),
|
||||
Run: func(a map[string]any) (any, error) {
|
||||
key, _ := a["key"].(string)
|
||||
why, _ := a["why"].(string)
|
||||
return askAgainByHand(st, ask, key, why, stdio.CallerOf(a), now())
|
||||
}}}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
"mesh-delivery-assigner/internal/buckets"
|
||||
"mesh-delivery-assigner/internal/puts"
|
||||
"slices"
|
||||
@@ -127,6 +128,31 @@ func TestAHoldAskedAgainAfterALiftAndARestartHasANewID(t *testing.T) {
|
||||
if a2.Ask.ID == a1.Ask.ID {
|
||||
t.Errorf("the ask written again after a lift and a restart has the id %s of the first", a1.Ask.ID)
|
||||
}
|
||||
// Issue 1966, ADR 0668 §5: assigner_ask_again writes the hold again under a new asking and records the hand act; the
|
||||
// asking the operator answered is refused, naming the answer; a proposal's authorising ask is refused by its rule.
|
||||
answersKV := &memKV{values: map[string][]byte{}}
|
||||
st := memStore{asksState: asksKV, answersState: answersKV}
|
||||
var recorded []string
|
||||
record := func(key string, body any) (json.RawMessage, error) {
|
||||
raw, _ := json.Marshal(body)
|
||||
recorded = append(recorded, key+" "+string(raw))
|
||||
return json.RawMessage(`{}`), nil
|
||||
}
|
||||
now := time.Date(2026, 10, 25, 3, 0, 0, 0, time.UTC)
|
||||
out, err := askAgainByHand(st, record, key, "the channel lost it", "lead@shanks", now)
|
||||
a3, derr := decodeAsk(key, asksKV.values[key])
|
||||
if err != nil || derr != nil || a3.Ask.ID == a2.Ask.ID || len(recorded) != 1 || !strings.Contains(recorded[0], askAgainTool+" "+key) {
|
||||
t.Fatalf("ask again: %v, %v; asked %s (was %s), recorded %v", out, err, a3.Ask.ID, a2.Ask.ID, recorded)
|
||||
}
|
||||
answersKV.values[key], _ = mdc.EncodeAnswer(mdc.Answer{Option: "keep", Warrant: a3.Ask.ID + "@2026-10-25T03:01:00Z", Words: "the operator"})
|
||||
if _, err := askAgainByHand(st, record, key, "again", "lead@shanks", now); err == nil || !strings.Contains(err.Error(), "keep") {
|
||||
t.Errorf("an answered asking is asked again: %v", err)
|
||||
}
|
||||
for _, c := range []struct{ key, by, why string }{{key, stdio.Unproven, "a caller"}, {"hold.none", "lead@shanks", "no ask"}} {
|
||||
if _, err := askAgainByHand(st, record, c.key, "w", c.by, now); err == nil {
|
||||
t.Errorf("%s: asked again", c.why)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 1906/H4: an answer counts only for the asking it answered: an answer to an earlier hold of the same fingerprint, or to an ask
|
||||
|
||||
@@ -404,6 +404,7 @@ func serveTools() error {
|
||||
tools = append(tools, viewTools(os.Getenv, keptSource, time.Now)...)
|
||||
// What each node's engine reads, as the assigner uses it (ADR 0618 §3): its own state engine-reads, asked again through it.
|
||||
tools = append(tools, enginereads.Tool(engineReadsTool, "the assigner", enginereads.KeptState(stdio.State(engineReadsState)), false, time.Now))
|
||||
tools = append(tools, askAgainTools(runtimeStore{}, stdio.Ask, time.Now)...)
|
||||
return stdio.Serve("", tools)
|
||||
}
|
||||
|
||||
|
||||
@@ -96,8 +96,8 @@ func TestManifest(t *testing.T) {
|
||||
// which lists and asks again what each node's engine reads.
|
||||
if !slices.Equal(m.Tools, []string{"assigner_put", "assigner_release", "assigner_puts", "assignments", "seat-holders", "wants",
|
||||
"assigner_read_assignments", "assigner_read_seat_holders", "assigner_read_wants", "assigner_read_findings", "assigner_read_asks", "assigner_read_differences", "assigner_read_copies",
|
||||
"assigner_first_views", "assigner_first_view_again", "assigner_engine_reads", "assigner_seat_list", "assigner_seat_list_again"}) {
|
||||
t.Errorf("the assigner serves tools %v, want its three put tools, three first views, six read tools, assigner_engine_reads and the seat list's two (ADR 0636 §7)", m.Tools)
|
||||
"assigner_first_views", "assigner_first_view_again", "assigner_engine_reads", "assigner_seat_list", "assigner_seat_list_again", "assigner_ask_again"}) {
|
||||
t.Errorf("the assigner serves tools %v, want its three put tools, three first views, six read tools, assigner_engine_reads, the seat list's two (ADR 0636 §7) and assigner_ask_again (ADR 0668 §5)", m.Tools)
|
||||
}
|
||||
if m.RunsAs != m.Module || m.SecretsOwner != m.RunsAs {
|
||||
t.Errorf("runs-as = %q: the bundle runs on the assigner's own account (ADR 0621 §1)", m.RunsAs)
|
||||
|
||||
@@ -89,7 +89,8 @@
|
||||
"assigner_first_view_again",
|
||||
"assigner_engine_reads",
|
||||
"assigner_seat_list",
|
||||
"assigner_seat_list_again"
|
||||
"assigner_seat_list_again",
|
||||
"assigner_ask_again"
|
||||
],
|
||||
"state": [
|
||||
{ "name": "assignments", "history": 64 },
|
||||
|
||||
Reference in New Issue
Block a user