Reach a seat and a module of one name each: list the seat's verbs, and route a machine's address to the module
mesh/merge-gate pass: builds mesh-tools, node-tools → ace, g14, novox, shanks; no bus step; every machine composes with the change as it did without (0 of …
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request

mesh-delivery is the delivery's seat and the module holding it, which answers the seat's verbs with
tools of the same names. Discovery keyed the seat's verbs and the module's tools in one namespace, so
the module's tool took the key and the seat was listed with no verb; and every address with the name
resolved to the seat, so neither the seat's verbs nor the module's tools could be called. The keys are
apart, and `<node>/<module>.<tool>` reaches the module when the module serves it (novox/hq issue 284).
This commit is contained in:
jochen
2026-10-07 01:37:46 +02:00
parent 01c98db0a4
commit dac4d423a8
3 changed files with 181 additions and 91 deletions
+118 -91
View File
@@ -243,100 +243,17 @@ func indexOn(conn *bus.Conn) (*index, error) {
return nil, err
}
l := &Listing{Tools: []Tool{}, NotAnswering: []string{}}
x := &index{Modules: map[string]*moduleInfo{}, Listing: l, Discovery: d, Recorded: map[string][]string{}}
var endpoints []announce.Endpoint
for _, h := range d.Heard {
endpoints = append(endpoints, announce.Endpoints(h.Info)...)
}
x, announced, machines := indexOfEndpoints(endpoints)
l := x.Listing
x.Discovery = d
for _, s := range d.Silent {
x.Unheard = append(x.Unheard, unheard{Runtime: s.Name, Machine: s.Machine,
Why: fmt.Sprintf("it answered PING; what it serves did not arrive within %s", announce.Patience)})
}
announced := map[string]map[string]bool{} // module → node → announced something
seats := map[string]*seatInfo{}
toolAt := map[string]int{} // <module>.<tool> or <seat>.<verb> → index in l.Tools
machines := map[string]bool{}
for _, h := range d.Heard {
for _, e := range announce.Endpoints(h.Info) {
if e.Node != "" {
machines[e.Node] = true
}
if announced[e.Module] == nil {
announced[e.Module] = map[string]bool{}
}
announced[e.Module][e.Node] = true
switch e.Kind {
case announce.KindSeat:
st := seats[e.Seat]
if st == nil {
st = &seatInfo{Seat: e.Seat, Scope: e.Scope}
seats[e.Seat] = st
}
if st.Scope != "node" && e.Scope == "node" {
st.Scope = "node"
}
h := holder{Module: e.Module, Node: e.Node}
if !containsHolder(st.Holders, h) {
st.Holders = append(st.Holders, h)
}
key := e.Seat + "." + e.Tool
if _, have := toolAt[key]; !have {
toolAt[key] = len(l.Tools)
t := Tool{Module: e.Seat, Name: e.Tool, Description: e.Description, Input: e.Schema, Seat: true, Scope: st.Scope}
l.Tools = append(l.Tools, t)
st.Verbs = append(st.Verbs, t)
}
case announce.KindTool:
m := x.Modules[e.Module]
if m == nil {
m = &moduleInfo{Module: e.Module}
x.Modules[e.Module] = m
}
if e.Node != "" && !contains(m.On, e.Node) {
m.On = append(m.On, e.Node)
}
m.Interchangeable = m.Interchangeable || e.Interchangeable
key := e.Module + "." + e.Tool
i, have := toolAt[key]
if !have {
i = len(l.Tools)
toolAt[key] = i
l.Tools = append(l.Tools, Tool{Module: e.Module, Name: e.Tool, Description: e.Description, Input: e.Schema})
}
if !contains(l.Tools[i].Subjects, e.Subject) {
l.Tools[i].Subjects = append(l.Tools[i].Subjects, e.Subject)
}
}
}
}
// A tool's subjects as a call looks them up: the plain one any instance answers first, then each
// machine's.
for i := range l.Tools {
t := &l.Tools[i]
if t.Seat {
continue
}
plain := "mesh.mod." + t.Module + ".tool." + t.Name
sort.SliceStable(t.Subjects, func(a, b int) bool {
if (t.Subjects[a] == plain) != (t.Subjects[b] == plain) {
return t.Subjects[a] == plain
}
return t.Subjects[a] < t.Subjects[b]
})
}
for _, t := range l.Tools {
if !t.Seat {
x.Modules[t.Module].Tools = append(x.Modules[t.Module].Tools, t)
}
}
for _, m := range x.Modules {
sort.Strings(m.On)
}
for _, st := range seats {
sort.Slice(st.Holders, func(a, b int) bool { return st.Holders[a].Node < st.Holders[b].Node })
x.Seats = append(x.Seats, *st)
}
sort.Slice(x.Seats, func(i, j int) bool { return x.Seats[i].Seat < x.Seats[j].Seat })
sort.SliceStable(l.Tools, func(i, j int) bool {
return l.Tools[i].Module+"."+l.Tools[i].Name < l.Tools[j].Module+"."+l.Tools[j].Name
})
// What should have answered: every assignment of a module that declares tools. Silence is named;
// a module with no tools is never a name here.
@@ -410,6 +327,104 @@ func (s *Surface) indexAsked(fresh bool) (*index, error) {
return x, nil
}
// indexOfEndpoints is what the announced endpoints say: every seat with its verbs and holders, every
// module with its tools and machines, the flat listing, and which module announced on which machine.
func indexOfEndpoints(endpoints []announce.Endpoint) (*index, map[string]map[string]bool, map[string]bool) {
l := &Listing{Tools: []Tool{}, NotAnswering: []string{}}
x := &index{Modules: map[string]*moduleInfo{}, Listing: l, Recorded: map[string][]string{}}
announced := map[string]map[string]bool{} // module → node → announced something
seats := map[string]*seatInfo{}
// <module>.<tool>, or seat:<seat>.<verb>, → index in l.Tools. **Apart**, because a seat and a module may
// share a name — mesh-delivery is the delivery's seat and the module holding it (novox/hq ADR 0239), and
// the module answers the seat's verbs with tools of the same names: in one namespace the module's tool
// took the key first, and the seat was listed with no verb at all (novox/hq issue 284).
toolAt := map[string]int{}
machines := map[string]bool{}
for _, e := range endpoints {
if e.Node != "" {
machines[e.Node] = true
}
if announced[e.Module] == nil {
announced[e.Module] = map[string]bool{}
}
announced[e.Module][e.Node] = true
switch e.Kind {
case announce.KindSeat:
st := seats[e.Seat]
if st == nil {
st = &seatInfo{Seat: e.Seat, Scope: e.Scope}
seats[e.Seat] = st
}
if st.Scope != "node" && e.Scope == "node" {
st.Scope = "node"
}
h := holder{Module: e.Module, Node: e.Node}
if !containsHolder(st.Holders, h) {
st.Holders = append(st.Holders, h)
}
key := "seat:" + e.Seat + "." + e.Tool
if _, have := toolAt[key]; !have {
toolAt[key] = len(l.Tools)
t := Tool{Module: e.Seat, Name: e.Tool, Description: e.Description, Input: e.Schema, Seat: true, Scope: st.Scope}
l.Tools = append(l.Tools, t)
st.Verbs = append(st.Verbs, t)
}
case announce.KindTool:
m := x.Modules[e.Module]
if m == nil {
m = &moduleInfo{Module: e.Module}
x.Modules[e.Module] = m
}
if e.Node != "" && !contains(m.On, e.Node) {
m.On = append(m.On, e.Node)
}
m.Interchangeable = m.Interchangeable || e.Interchangeable
key := e.Module + "." + e.Tool
i, have := toolAt[key]
if !have {
i = len(l.Tools)
toolAt[key] = i
l.Tools = append(l.Tools, Tool{Module: e.Module, Name: e.Tool, Description: e.Description, Input: e.Schema})
}
if !contains(l.Tools[i].Subjects, e.Subject) {
l.Tools[i].Subjects = append(l.Tools[i].Subjects, e.Subject)
}
}
}
// A tool's subjects as a call looks them up: the plain one any instance answers first, then each
// machine's.
for i := range l.Tools {
t := &l.Tools[i]
if t.Seat {
continue
}
plain := "mesh.mod." + t.Module + ".tool." + t.Name
sort.SliceStable(t.Subjects, func(a, b int) bool {
if (t.Subjects[a] == plain) != (t.Subjects[b] == plain) {
return t.Subjects[a] == plain
}
return t.Subjects[a] < t.Subjects[b]
})
}
for _, t := range l.Tools {
if !t.Seat {
x.Modules[t.Module].Tools = append(x.Modules[t.Module].Tools, t)
}
}
for _, m := range x.Modules {
sort.Strings(m.On)
}
for _, st := range seats {
sort.Slice(st.Holders, func(a, b int) bool { return st.Holders[a].Node < st.Holders[b].Node })
x.Seats = append(x.Seats, *st)
}
sort.Slice(x.Seats, func(i, j int) bool { return x.Seats[i].Seat < x.Seats[j].Seat })
sort.SliceStable(l.Tools, func(i, j int) bool {
return l.Tools[i].Module+"."+l.Tools[i].Name < l.Tools[j].Module+"."+l.Tools[j].Name
})
return x, announced, machines
}
// target is what an address resolves to.
type target struct {
Address string
@@ -433,9 +448,21 @@ func resolve(x *index, address string) (target, error) {
}
prefix, name := rest[:dot], rest[dot+1:]
if s := x.seat(prefix); s != nil {
s := x.seat(prefix)
if s != nil && x.Modules[prefix] != nil && findTool(x.Modules[prefix].Tools, name) != nil {
// **A seat and a module of one name** (novox/hq issue 284): mesh-delivery is the delivery's seat and
// the module holding it. The module's tool is meant when the seat has no such verb, or when a machine
// is named for a seat held once for the mesh — `<node>/<module>.<tool>` is a module on one machine.
if findTool(s.Verbs, name) == nil || (node != "" && s.Scope != "node") {
s = nil
}
}
if s != nil {
verb := findTool(s.Verbs, name)
if verb == nil {
if len(s.Verbs) == 0 {
return target{}, fmt.Errorf("the seat %s has no verb %s: its holder announced none", prefix, name)
}
return target{}, fmt.Errorf("the seat %s has no verb %s; it has %s", prefix, name, toolNames(s.Verbs))
}
if s.Scope == "node" {
@@ -0,0 +1,57 @@
package console
import (
"strings"
"testing"
"github.com/novox/mesh-tools/node-tools/internal/announce"
)
// **Issue 284**: mesh-delivery is the delivery's seat, held once for the mesh, and the module holding it,
// which answers the seat's verbs with tools of the same names and has tools of its own. Discovery put the
// module's tool and the seat's verb under one key, so the seat was listed with no verb; and every address
// with the name resolved to the seat — `mesh-delivery.deliveries` and `novox/mesh-delivery.delivery_status`
// alike were refused, the seat's verbs and the module's tools both out of reach.
func TestASeatAndAModuleOfOneNameAreEachReached(t *testing.T) {
var endpoints []announce.Endpoint
for _, tool := range []string{"deliveries", "delivery_status"} {
endpoints = append(endpoints, announce.Endpoint{Kind: announce.KindTool, Module: "mesh-delivery", Tool: tool,
Node: "anchor", Subject: "mesh.mod.mesh-delivery.tool." + tool + ".anchor"})
}
for _, verb := range []string{"deliveries", "stalled"} {
endpoints = append(endpoints, announce.Endpoint{Kind: announce.KindSeat, Module: "mesh-delivery", Seat: "mesh-delivery",
Tool: verb, Scope: "mesh", Subject: "mesh.seat.mesh-delivery.tool." + verb})
}
x, _, _ := indexOfEndpoints(endpoints)
s := x.seat("mesh-delivery")
if s == nil || len(s.Verbs) != 2 {
t.Fatalf("the seat is listed with verbs %v, not its two", s)
}
if m := x.Modules["mesh-delivery"]; m == nil || len(m.Tools) != 2 {
t.Fatalf("the module is listed with tools %v, not its two", m)
}
for address, want := range map[string]struct {
seat bool
key string
}{
"mesh-delivery.deliveries": {true, "seat:mesh-delivery.deliveries"},
"mesh-delivery.stalled": {true, "seat:mesh-delivery.stalled"},
"anchor/mesh-delivery.delivery_status": {false, "mesh-delivery.delivery_status@anchor"},
"anchor/mesh-delivery.deliveries": {false, "mesh-delivery.deliveries@anchor"},
} {
got, err := resolve(x, address)
if err != nil {
t.Errorf("%s: %v", address, err)
continue
}
if got.Seat != want.seat || got.Key != want.key {
t.Errorf("%s resolves to %+v, not the %s", address, got, want.key)
}
}
// A module tool of the name without a machine: the module keeps state on its machine, so it says so.
if _, err := resolve(x, "mesh-delivery.delivery_status"); err == nil || !strings.Contains(err.Error(), "<node>/") {
t.Errorf("the module's tool without a machine: %v", err)
}
}