Ask each machine's engine for the root-free word again: the engines were deaf, not absent (issue 774)

The sweep took the node seat node-engine's root-free for one nothing answers; the engines had been refused their
subscriptions by the bus since 12:10 (fixed in mesh-host), so the address was right. This puts it back and keeps
what the previous commit added: every failed ask said with its error, a fresh word not thrown away, and the failure
as the held ask's reason.
This commit is contained in:
2026-10-30 18:14:42 +01:00
parent 7531c3257d
commit 2d28a52d61
6 changed files with 69 additions and 69 deletions
+43 -40
View File
@@ -242,8 +242,9 @@ func (h *Holder) carriers(level asks.Level) []string {
return out
}
// RootVerdict is the word on one machine (novox/hq ADR 0259 §8): free only when it was measured, recently, that no
// agent can become root there without a person.
// RootVerdict is a machine's engine's word on that machine (the node seat node-engine's root-free, novox/hq ADR 0259
// §8 as ADR 0563 §6 changes it): 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"`
@@ -251,61 +252,63 @@ type RootVerdict struct {
Judged string `json:"judged,omitempty"`
}
// RootFreeVerb is the verb the router asks, as this module, which machines are root-free: the controller's, which
// judges each machine from its node-engine's own verdict (ADR 0259 §8). ADR 0563 §6 moves the word to the node seat
// node-engine and ADR 0571 §11 to the self-check's probe; neither answers on the mesh yet (the engine's acts are left
// out of its declared state, mesh-host #178, and no seat mesh-self-check is held), so asking them held every ask
// that offers an approval (issue 774, the sweep of 2026-10-30). The controller's verb is the one that answers.
const RootFreeVerb = "seat:" + ControllerSeat + ".root-free"
// EngineSeat is the node seat each machine's engine holds, and RootFreeVerb its verb the router asks of a machine, as
// this module: `seat:node-engine.root-free@<machine>`, which that machine's engine alone answers (ADR 0563 §6).
const (
EngineSeat = "node-engine"
RootFreeVerb = "seat:" + EngineSeat + ".root-free"
)
// RootFreeWithin is how long the router waits for the word; RootFreeKept how long a word is used to render a
// RootFreeWithin is how long the router waits for the machines' words; 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 word on machines at once, failing on no answer in time. Only a verdict on a machine asked is
// taken.
// askRootFree asks each machine's own engine about that machine, at once, failing on no answer in time. A machine's
// word is the verdict its engine gives on it; a verdict an engine gives on another machine is not taken.
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")
return nil, errors.New("this router cannot ask a machine's engine")
}
type answer struct {
raw json.RawMessage
err error
machine string
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("%s did not answer within %s", RootFreeVerb, RootFreeWithin)
}
if a.err != nil {
return nil, fmt.Errorf("%s: %w", RootFreeVerb, a.err)
}
var body struct {
Machines []RootVerdict `json:"machines"`
}
if err := json.Unmarshal(a.raw, &body); err != nil {
return nil, fmt.Errorf("%s's answer could not be read: %v", RootFreeVerb, err)
}
asked := map[string]bool{}
done := make(chan answer, len(machines))
for _, m := range machines {
asked[m] = true
go func(m string) {
raw, err := h.Ask(RootFreeVerb+"@"+m, map[string]any{})
done <- answer{m, raw, err}
}(m)
}
out := map[string]RootVerdict{}
for _, v := range body.Machines {
if asked[v.Machine] {
out[v.Machine] = v
deadline := time.After(RootFreeWithin)
for range machines {
var a answer
select {
case a = <-done:
case <-deadline:
return nil, fmt.Errorf("a machine's engine did not answer within %s", RootFreeWithin)
}
if a.err != nil {
return nil, fmt.Errorf("%s's engine: %w", a.machine, a.err)
}
var body struct {
Machines []RootVerdict `json:"machines"`
}
if err := json.Unmarshal(a.raw, &body); err != nil {
return nil, fmt.Errorf("%s's engine's answer could not be read: %v", a.machine, err)
}
for _, v := range body.Machines {
if v.Machine == a.machine {
out[v.Machine] = v
}
}
}
return out, nil
@@ -382,7 +385,7 @@ func (h *Holder) rootFree(k KindInfo, cached bool) string {
v, said := verdicts[m]
switch {
case !said:
return "the controller said nothing of whether an agent can become root on " + m
return "its engine said nothing of whether an agent can become root on " + m
case !v.Free:
why := v.Why
if why == "" {
+2 -8
View File
@@ -256,14 +256,8 @@ func (r *busRuntime) runtime(id json.RawMessage, method string, params json.RawM
case "mesh/seat.publish":
return map[string]any{}, r.nc.Publish(p.Subject, p.Body)
case "mesh/ask":
if p.Key == RootFreeVerb {
var args struct{ Machines []string }
_ = json.Unmarshal(p.Body, &args)
var out []RootVerdict
for _, m := range args.Machines {
out = append(out, RootVerdict{Machine: m, Free: true})
}
return map[string]any{"machines": out}, nil
if m, ok := strings.CutPrefix(p.Key, RootFreeVerb+"@"); ok {
return map[string]any{"machines": []RootVerdict{{Machine: m, Free: true}}}, nil
}
return nil, fmt.Errorf("the test runtime answers nothing for %s", p.Key)
}
+1 -1
View File
@@ -315,7 +315,7 @@ type Holder struct {
Emit func(event string, body any) error
// Ask calls a seat verb through the mesh, as this module: the answers the operator chooses.
Ask Asker
// RootFree asks the root-free word on machines (the controller's verb, RootFreeVerb); nil asks it through Ask
// RootFree asks each machine's engine's root-free (the node seat node-engine's, ADR 0563 §6); nil asks it through Ask
// (askRootFree).
RootFree func(machines []string) (map[string]RootVerdict, error)
// Zone is the operator's time zone, which every time in a message is said in; nil is this machine's.
@@ -88,7 +88,7 @@ func TestItDeclaresTheSeatsOfAnAskAndHoldsTheOperatorChannel(t *testing.T) { //
// It performs nothing: the verbs it calls read the open conditions, whether root is free, and which issues
// track which conditions.
if !reflect.DeepEqual(m.Emits, []string{"refused"}) || !reflect.DeepEqual(m.Invokes, []string{"seat:" + ControllerSeat + ".conditions",
"seat:mesh-conditions.conditions", RootFreeVerb, TrackingVerb}) {
"seat:mesh-conditions.conditions", "seat:" + EngineSeat + ".root-free", TrackingVerb}) {
t.Fatalf("emits %v invokes %v", m.Emits, m.Invokes)
}
var state []string
+21 -18
View File
@@ -9,6 +9,7 @@ import (
"os"
"path/filepath"
"regexp"
"sort"
"strings"
"sync"
"testing"
@@ -73,32 +74,34 @@ func TestApprovalNeedsTheControllersWordThatRootIsFree(t *testing.T) {
}
}
// H3: the word on the router's machine and the channel's is asked of the verb that answers it on the mesh, the
// controller's root-free, once with both machines, as this module; a verdict on a machine not asked is not taken.
// The sweep of 2026-10-30 (issue 774): the node seat node-engine's root-free answered nowhere, so every ask that
// offers an approval was held, silently.
func TestTheRootWordIsAskedOfTheVerbThatAnswers(t *testing.T) {
// H3, as ADR 0563 §6 changes it: the word on each machine is asked of that machine's own engine, the node seat
// node-engine's root-free there, as this module, and read from its answer; a machine's word is that machine's alone.
func TestTheRootWordIsAskedOfEachMachinesEngine(t *testing.T) {
h, _, _, _, _, _ := newRouter(t)
h.RootFree = nil
var asked []string
h.Ask = func(key string, body any) (json.RawMessage, error) {
ms, _ := body.(map[string]any)["machines"].([]string)
asked = append(asked, key+" "+strings.Join(ms, ","))
return json.RawMessage(`{"machines":[{"machine":"anchor","free":true,"why":"judged","judged":"2026-10-09T10:00:00Z"}]}`), nil
asked = append(asked, key)
machine := strings.TrimPrefix(key, "seat:node-engine.root-free@")
if machine == "elsewhere" {
machine = "anchor" // an engine that answers for another machine is not believed
}
return json.RawMessage(`{"machines":[{"machine":"` + machine + `","free":true,"why":"judged","judged":"2026-10-09T10:00:00Z"}]}`), nil
}
info, _ := h.Bench.Kind("telegram")
if why := h.rootFree(info, false); why != "" {
t.Fatalf("free not read: %s", why)
}
if len(asked) != 1 || asked[0] != "seat:mesh-controller.root-free anchor" {
if len(asked) != 1 || asked[0] != "seat:node-engine.root-free@anchor" {
t.Errorf("asked %v", asked)
}
asked = nil
h.Node = "elsewhere"
if why := h.rootFree(info, false); !strings.Contains(why, "said nothing of whether an agent can become root on elsewhere") {
t.Errorf("a machine left out of the answer was a pass: %q", why)
t.Errorf("one machine's word was taken for another's: %q", why)
}
if len(asked) != 1 || asked[0] != "seat:mesh-controller.root-free elsewhere,anchor" {
sort.Strings(asked)
if len(asked) != 2 || asked[0] != "seat:node-engine.root-free@anchor" || asked[1] != "seat:node-engine.root-free@elsewhere" {
t.Errorf("the router's machine and the channel's were asked as %v", asked)
}
h.Node = "anchor"
@@ -113,8 +116,8 @@ func TestTheRootWordIsAskedOfTheVerbThatAnswers(t *testing.T) {
}
// A failed ask of the root word is said with its error, keeps the word still fresh for rendering, never passes an
// answer, and is the reason a held ask carries (the sweep of 2026-10-30: the word was deleted silently and the held
// reason said only "not recent").
// answer, and is the reason a held ask carries (the sweep of 2026-10-30, issue 774: the engines did not answer, the
// word was deleted silently and the held reason said only "not recent").
func TestAFailedRootAskIsSaidAndKeepsAFreshWord(t *testing.T) {
h, _, _, c, store, _ := newRouter(t)
link(h, store, c, "42", c.now().Add(-2*time.Hour))
@@ -125,13 +128,13 @@ func TestAFailedRootAskIsSaidAndKeepsAFreshWord(t *testing.T) {
t.Fatalf("free not read: %s", why)
}
h.RootFree = func([]string) (map[string]RootVerdict, error) {
return nil, errors.New("nothing in the mesh is called node-engine")
return nil, errors.New("anchor's engine: no responders")
}
c.pass(RootFreeKept / 3)
if why := h.rootFree(info, false); !strings.Contains(why, "could not be asked: nothing in the mesh is called node-engine") {
if why := h.rootFree(info, false); !strings.Contains(why, "could not be asked: anchor's engine: no responders") {
t.Errorf("a failed ask judged an answer: %q", why)
}
if len(logs) != 1 || !strings.Contains(logs[0], "nothing in the mesh is called node-engine") || !strings.Contains(logs[0], "kept for rendering") {
if len(logs) != 1 || !strings.Contains(logs[0], "no responders") || !strings.Contains(logs[0], "kept for rendering") {
t.Errorf("the failure was not said with its error: %v", logs)
}
if why := h.rootFree(info, true); why != "" {
@@ -139,10 +142,10 @@ func TestAFailedRootAskIsSaidAndKeepsAFreshWord(t *testing.T) {
}
c.pass(RootFreeKept)
stand(h, c)
if why := h.rootFree(info, true); !strings.Contains(why, "failed: nothing in the mesh is called node-engine") {
if why := h.rootFree(info, true); !strings.Contains(why, "failed: anchor's engine: no responders") {
t.Errorf("a stale word passed, or its reason hid the failure: %q", why)
}
if why := h.holdWhy(heldAsk(c, "d-9")); !strings.Contains(why, "nothing in the mesh is called node-engine") {
if why := h.holdWhy(heldAsk(c, "d-9")); !strings.Contains(why, "no responders") {
t.Errorf("a held ask does not say why: %q", why)
}
}
+1 -1
View File
@@ -94,7 +94,7 @@
"invokes": [
"seat:mesh-controller.conditions",
"seat:mesh-conditions.conditions",
"seat:mesh-controller.root-free",
"seat:node-engine.root-free",
"seat:issue-tracker.tracking"
],
"reads": [