From 4f636b70807f23e3f72c293d5de7bb2275a63a3a Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 2 Nov 2026 11:23:14 +0100 Subject: [PATCH 1/4] Name no controller verb in hints and drop its handover and paging reads, since the controller ended (issue 1126 B19) Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL --- README.md | 6 +- node-tools/README.md | 2 +- node-tools/internal/alive/alive.go | 2 +- node-tools/internal/bus/bus.go | 78 ++++----------- node-tools/internal/bus/caller.go | 22 ++--- node-tools/internal/bus/caller_bus_test.go | 2 +- node-tools/internal/bus/handover_test.go | 73 -------------- node-tools/internal/bus/log.go | 4 +- node-tools/internal/bus/pages.go | 99 +------------------ node-tools/internal/bus/pages_test.go | 42 +------- node-tools/internal/bus/provision.go | 6 +- node-tools/internal/bus/seat.go | 8 +- node-tools/internal/bus/seat_test.go | 4 +- node-tools/internal/bus/state.go | 4 +- node-tools/internal/bus/state_watch_test.go | 2 +- node-tools/internal/bus/streams.go | 8 +- node-tools/internal/console/address.go | 10 +- node-tools/internal/console/address_test.go | 32 +++--- node-tools/internal/console/client.go | 9 -- node-tools/internal/console/mcp.go | 6 +- .../internal/console/node_argument_test.go | 4 +- node-tools/internal/console/refusal_test.go | 21 ++-- node-tools/internal/console/runtimes_test.go | 4 +- node-tools/internal/console/search_test.go | 37 ++++--- node-tools/internal/console/wait.go | 2 +- node-tools/internal/meshtest/meshtest.go | 22 ++--- node-tools/internal/provide/provide.go | 4 +- node-tools/internal/provide/provide_test.go | 14 +-- .../internal/runtime/seat_traffic_test.go | 2 +- node-tools/src/broker-nats.ts | 12 +-- node-tools/src/client.ts | 2 +- node-tools/src/mcp.ts | 2 +- node-tools/test/client.test.ts | 20 ++-- node-tools/test/mcp.test.ts | 14 +-- node-tools/test/patient-connect.test.ts | 6 +- node-tools/test/roundtrip.mjs | 2 +- 36 files changed, 171 insertions(+), 416 deletions(-) delete mode 100644 node-tools/internal/bus/handover_test.go diff --git a/README.md b/README.md index 22b50b7..f5bb790 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ MESH_OPERATOR_ACCOUNT whose machine this is, and MESH_OPERATOR_HOME where thei MESH_BROKER_URL a plain URL instead of the credential, for the bootstrap case ``` -`node dist/main.js`. On a node the controller composes the variables and the host supervises the +`node dist/main.js`. On a node the mesh composes the variables and the node-engine supervises the process like any other host-side workload (novox/hq to-be 38). As `node-tools` the same process is the console: MCP on `127.0.0.1:4270` (or `MESH_CONSOLE_LISTEN`). @@ -66,14 +66,14 @@ answer took, its size in bytes, how many modules and tools it announced, whether fit the bus, when it was last heard — and who was expected and not heard). `mesh_search` also finds a tool by the shell command it replaces (novox/hq ADR 0245): each seat verb and -module tool says what it replaces in the controller's records (`replaces`, read from the controller's +module tool says what it replaces in the indexer's records (`replaces`, read from its `tools` and `modules` answers), so `journalctl -u x`, `systemctl status x` or `docker ps` puts the verb for it first, and says the command it matched as `instead_of`. A word also matches through a short table of the words agents use for the mesh's (`logs` finds the journal). Best first: a replaced command, then words in a name or a replaced command, then words in a description; seats before modules on a tie. Discovery asks the bus (ADR 0197): every runtime answers the NATS services protocol's `$SRV.PING` and -`$SRV.INFO` with what it serves at that moment, and the console reads where the controller's records +`$SRV.INFO` with what it serves at that moment, and the console reads where the placement seat's records place each module. The console waits at least 750 ms, and up to 5 s for every runtime that answered PING to send what it serves — a large runtime's answer crossing the bus to a broker on another machine can arrive well after the rest. A runtime that said it is there and did not say what it serves, or diff --git a/node-tools/README.md b/node-tools/README.md index 1c0c57f..0478def 100644 --- a/node-tools/README.md +++ b/node-tools/README.md @@ -3,7 +3,7 @@ The node's tool runtime as a module (novox/hq ADR 0175, to-be 38 WP3). Assigned to a machine, it is one process the host runs from this bundle, as the operator's account: it serves every assigned module's tools and every held seat's verbs on the bus, and answers MCP on the machine's loopback — -the console (design 34). The controller composes the process (which bundles to load, where the +the console (design 34). The mesh composes the process (which bundles to load, where the credential is, whose machine it is); this manifest says only what the machine must have for it: the interpreter, a place for the credential, the loopback port, and leave to call every tool. diff --git a/node-tools/internal/alive/alive.go b/node-tools/internal/alive/alive.go index f333457..843b57f 100644 --- a/node-tools/internal/alive/alive.go +++ b/node-tools/internal/alive/alive.go @@ -3,7 +3,7 @@ // **A machine heard and its runtime gone is a machine nobody can ask anything.** The node-engine says // every minute that the machine is there; nothing said whether the runtime serving every module's // tools and every held seat's verbs was. Now it says so too, on its own subject, every interval, with -// the interval — the controller's watchdog raises `tools-silent` after three missed — and its build. +// the interval — the mesh's watchdog raises `tools-silent` after three missed — and its build. // Core NATS, kept by nobody, as the host's heartbeat: a lost one is the next one. package alive diff --git a/node-tools/internal/bus/bus.go b/node-tools/internal/bus/bus.go index 55d73ce..fa1a7c3 100644 --- a/node-tools/internal/bus/bus.go +++ b/node-tools/internal/bus/bus.go @@ -32,9 +32,7 @@ import ( "github.com/novox/mesh-tools/node-tools/internal/wire" ) -// RequestTimeout is how long a call waits for its answer — what modules already expect. The -// controller answers its verbs well inside it (its AnswerWithin, ten seconds), with a call id while a -// verb still runs, so a caller is never left guessing past this (novox/hq issue 265). +// RequestTimeout is how long a call waits for its answer — what modules already expect. const RequestTimeout = 30 * time.Second // Credential is a broker credential as the mesh delivers it (novox/hq ADR 0120). @@ -344,7 +342,7 @@ func (c *Conn) Credential() Credential { return c.cred } // Follow reads a module's membership on this machine once and follows it live, so its tools are // served where the mesh issued them (ADR 0175). It is read from the grants part's state memberships, its key // `.` (novox/hq ADR 0571 clause 7 and 10, mesh-sdk buscompose.GrantsMembershipsBucket), and nowhere else: -// the controller's assignments stream is not read and its subject is not heard. +// the old assignments stream is not read and its subject is not heard. func (c *Conn) Follow(module string) { c.mu.Lock() if c.node == "" { @@ -790,83 +788,47 @@ func (c *Conn) Ask(key string, body any, on string) (Answered, error) { // account publish with its own name alone; asked again on the subject that names no caller only when the // first reached nobody — nothing serves it yet, or the bus does not grant it yet — so no call is done twice. if called := CalledSubject(subject, c.cred.User); called != "" { - answered, handingOver, fallBack, err := c.askCalled(called, data) + answered, fallBack, err := c.askCalled(called, data) if !fallBack { - if !handingOver { - return answered, err - } - time.Sleep(HandoverPause) - answered, again, _, err := c.askCalled(called, data) - if err != nil && again { - return Answered{}, fmt.Errorf("asked twice, %s apart, and both times refused as a handover: %w", HandoverPause, err) - } return answered, err } } - answered, handingOver, err := c.askOnce(subject, data) - if !handingOver { - return answered, err - } - // **Refused because the controller is handing over: asked once more** (novox/hq issue 289). The - // controller being replaced did nothing and said so; the one after it answers the same call. Once: - // a second refusal is a controller that is not coming back, and the caller is told. - time.Sleep(HandoverPause) - answered, again, err := c.askOnce(subject, data) - if err != nil && again { - return Answered{}, fmt.Errorf("asked twice, %s apart, and both times refused as a handover: %w", HandoverPause, err) - } - return answered, err + return c.askOnce(subject, data) } -// HandoverPause is how long a call refused as a handover waits before it is asked once more: the -// supervisor's restart of the controller, which stops the one being replaced and starts the next. A -// variable so a test need not wait. -var HandoverPause = 3 * time.Second - -// RetryHandingOver is the mark the controller puts on a call it refused because it is being replaced -// (mesh-controller internal/link, `"retry": "handing-over"`). -const RetryHandingOver = "handing-over" - // ErrNoResponders is the bus's own refusal: nothing subscribes to the subject asked. It is told apart // from a tool's own error by errors.Is, never by its text (a tool may say "503" itself, issue 1529). var ErrNoResponders = errors.New("503 no responders") -// askOnce asks once, and says whether the answer was a refusal marked to be asked again. -func (c *Conn) askOnce(subject string, data []byte) (Answered, bool, error) { +// askOnce asks once. +func (c *Conn) askOnce(subject string, data []byte) (Answered, error) { msg, err := c.nc.Request(subject, data, RequestTimeout) if err != nil { if errors.Is(err, nats.ErrNoResponders) { - return Answered{}, false, ErrNoResponders + return Answered{}, ErrNoResponders } if errors.Is(err, nats.ErrTimeout) { - return Answered{}, false, errors.New("timeout") + return Answered{}, errors.New("timeout") } - return Answered{}, false, err + return Answered{}, err } - return c.answerOf(subject, msg.Data) + return answerOf(msg.Data) } -// answerOf reads one answer, asking the rest of a paged one on the subject it was asked on, and says whether it -// was a refusal marked to be asked again. -func (c *Conn) answerOf(subject string, data []byte) (Answered, bool, error) { - // An answer larger than one message comes in parts (pages.go, novox/hq issue 314). - body, err := c.whole(subject, data) - if err != nil { - return Answered{}, false, err - } +// answerOf reads one answer. +func answerOf(data []byte) (Answered, error) { var r struct { Result json.RawMessage `json:"result"` Error string `json:"error"` Node string `json:"node"` - Retry string `json:"retry"` } - if err := json.Unmarshal(body, &r); err != nil { - return Answered{}, false, err + if err := json.Unmarshal(data, &r); err != nil { + return Answered{}, err } if r.Error != "" { - return Answered{}, r.Retry == RetryHandingOver, errors.New(r.Error) + return Answered{}, errors.New(r.Error) } - return Answered{Result: r.Result, Node: r.Node}, false, nil + return Answered{Result: r.Result, Node: r.Node}, nil } // PublishAs emits an event as a module: published into JetStream and awaited, de-duplicated by its @@ -1070,7 +1032,7 @@ func (c *Conn) AskAs(module, key string, body any) (Answered, error) { } // EventsStream is where every module's events land, and ConsumerOf the durable consumer the -// controller makes for a module on a machine: the same names the module's own runtime bound +// mesh makes for a module on a machine: the same names the module's own runtime bound // (node-tools/src/broker-nats.ts), so moving the module into the node's runtime neither loses an // event nor sees one twice. const EventsStream = "EVENTS" @@ -1085,7 +1047,7 @@ func ConsumerOf(node, module string) string { // NakDelay is how long an event a handler failed waits before it is offered again: a transient cause // gets another attempt, a permanent one exhausts the consumer's max-deliver rather than spinning. The -// consumer then gives the event up, and the controller keeps it in DEAD_LETTERS until a person delivers +// consumer then gives the event up, and the mesh keeps it in DEAD_LETTERS until a person delivers // it again — under `mesh.again..…`, from which keyOf reads the same key — or drops it // (novox/hq issue 330). var NakDelay = 5 * time.Second @@ -1099,7 +1061,7 @@ var EventHeartbeat = 10 * time.Second // ConsumeAs reads a module's durable consumer on EVENTS, and each one on a module's stream its // membership names (novox/hq ADR 0363 §8), and hands each event to deliver one at a time (novox/hq // ADR 0198): acknowledged when deliver returns nil, negatively acknowledged after NakDelay when it -// returns an error, terminated when it is not an event at all. The consumers are the controller's to +// returns an error, terminated when it is not an event at all. The consumers are the mesh's to // create; this binds to them and never makes one. Runs until stopped. func (c *Conn) ConsumeAs(module string, deliver func(Envelope) error) (func(), error) { durable := ConsumerOf(c.node, module) @@ -1109,7 +1071,7 @@ func (c *Conn) ConsumeAs(module string, deliver func(Envelope) error) (func(), e } h := newHearing(c, js, module, deliver) // Bound by name, never created and never matched against a subject: the consumer and its - // filters are the controller's (design 29 §3), exactly as the module's own runtime bound it. + // filters are the mesh's (design 29 §3), exactly as the module's own runtime bound it. events, _, doing, err := h.bind(EventsStream, durable, nil) if err != nil { return nil, fmt.Errorf("%s %s's consumer %s: %w", doing, module, durable, err) diff --git a/node-tools/internal/bus/caller.go b/node-tools/internal/bus/caller.go index 5a817bb..b9fa39e 100644 --- a/node-tools/internal/bus/caller.go +++ b/node-tools/internal/bus/caller.go @@ -18,7 +18,7 @@ import ( // mesh.seat..call.. a mesh seat's verb // mesh.seat..call... a node seat's verb on one machine // -// The last token is the bus user that published the call, and the controller grants each user the publish of +// The last token is the bus user that published the call, and the mesh grants each user the publish of // these subjects with its own name there and no other — so the caller is a fact the bus server enforces, and // the runtime hands it to the module from the subject a call arrived on, never from the payload. // @@ -32,7 +32,7 @@ import ( const CallKind = "call" // A bus user's name carries dots (`.`, `person.`); a subject token cannot. In the token -// each dot is written `~`, which no part of a bus user's name may hold (the controller refuses any part that +// each dot is written `~`, which no part of a bus user's name may hold (the mesh refuses any part that // is not letters, digits, `_` or `-`), so the token is the user's name and nothing else. const userDot = "~" @@ -118,7 +118,7 @@ type callers struct { pending map[string]map[uint64]context.CancelCauseFunc } -// errCallRefused is the bus refusing a caller-named publish: this user is not granted it yet (the controller's +// errCallRefused is the bus refusing a caller-named publish: this user is not granted it yet (the mesh's // grant not delivered to the bus), so the call is asked again on the subject that names no caller. var errCallRefused = errors.New("the bus refused the caller-named subject") @@ -150,7 +150,7 @@ func (c *Conn) publishRefused(err error) bool { // askCalled asks once on a caller-named subject. fallBack says the call reached nobody — nothing serves that // subject yet, or the bus refused this user its publish — so it may be asked on the subject that names no // caller without being done twice. -func (c *Conn) askCalled(subject string, data []byte) (answered Answered, handingOver, fallBack bool, err error) { +func (c *Conn) askCalled(subject string, data []byte) (answered Answered, fallBack bool, err error) { ctx, cancel := context.WithCancelCause(context.Background()) ctx, stop := context.WithTimeout(ctx, RequestTimeout) defer stop() @@ -180,22 +180,22 @@ func (c *Conn) askCalled(subject string, data []byte) (answered Answered, handin if err != nil { switch { case errors.Is(context.Cause(ctx), errCallRefused): - return Answered{}, false, true, nil + return Answered{}, true, nil case errors.Is(err, nats.ErrNoResponders): c.saidFallBack(subject) - return Answered{}, false, true, nil + return Answered{}, true, nil case errors.Is(err, context.DeadlineExceeded) || errors.Is(err, nats.ErrTimeout): - return Answered{}, false, false, errors.New("timeout") + return Answered{}, false, errors.New("timeout") } - return Answered{}, false, false, err + return Answered{}, false, err } - answered, handingOver, err = c.answerOf(subject, msg.Data) - return answered, handingOver, false, err + answered, err = answerOf(msg.Data) + return answered, false, err } // saidFallBack says, once per subject, that a caller-named call reached nobody and is asked on the subject that // names no caller: a runtime or holder not yet serving it, or a grant that drifted, is seen in the log rather -// than read later as a quiet "unproven" (novox/hq issue 365; expected for the controller's seat until hq issue +// than read later as a quiet "unproven" (novox/hq issue 365; expected until hq issue // 464). func (c *Conn) saidFallBack(subject string) { c.calls.mu.Lock() diff --git a/node-tools/internal/bus/caller_bus_test.go b/node-tools/internal/bus/caller_bus_test.go index 625c7fd..502a3dd 100644 --- a/node-tools/internal/bus/caller_bus_test.go +++ b/node-tools/internal/bus/caller_bus_test.go @@ -16,7 +16,7 @@ import ( mt "github.com/novox/mesh-tools/node-tools/internal/meshtest" ) -// A bus whose users are granted as the controller grants them (mesh-controller internal/broker): each named +// A bus whose users are granted as the controller grants them (mesh-placement internal/broker): each named // user publishes the subjects listed for it. func grantedBus(t *testing.T, publish map[string][]string) string { t.Helper() diff --git a/node-tools/internal/bus/handover_test.go b/node-tools/internal/bus/handover_test.go deleted file mode 100644 index a3478f4..0000000 --- a/node-tools/internal/bus/handover_test.go +++ /dev/null @@ -1,73 +0,0 @@ -package bus_test - -import ( - "encoding/json" - "strings" - "sync/atomic" - "testing" - "time" - - "github.com/nats-io/nats.go" - - "github.com/novox/mesh-tools/node-tools/internal/bus" - "github.com/novox/mesh-tools/node-tools/internal/meshtest" -) - -// **A call the controller refused because it is handing over is asked once more** (novox/hq issue 289): -// the controller being replaced did nothing and marked its refusal, and the one after it answers. A -// refusal not so marked is final, and a second handover refusal is said, not asked a third time. -func TestAHandoverRefusalIsAskedOnceMore(t *testing.T) { - was := bus.HandoverPause - bus.HandoverPause = 10 * time.Millisecond - t.Cleanup(func() { bus.HandoverPause = was }) - - url := meshtest.URL(t) - seat, err := nats.Connect(url) - if err != nil { - t.Fatal(err) - } - defer seat.Close() - c, err := bus.Connect(bus.Credential{URL: url, Module: "console", Node: "anchor"}) - if err != nil { - t.Fatal(err) - } - defer c.Close() - - serve := func(t *testing.T, verb string, answers ...map[string]any) *atomic.Int32 { - t.Helper() - var asked atomic.Int32 - sub, err := seat.Subscribe(bus.SeatToolSubject("mesh-controller", verb, "", ""), func(m *nats.Msg) { - n := int(asked.Add(1)) - 1 - if n >= len(answers) { - n = len(answers) - 1 - } - body, _ := json.Marshal(answers[n]) - _ = m.Respond(body) - }) - if err != nil { - t.Fatal(err) - } - t.Cleanup(func() { _ = sub.Unsubscribe() }) - _ = seat.Flush() - return &asked - } - handingOver := map[string]any{"error": "the controller is handing over to the next one; nothing was done, ask again", - "retry": bus.RetryHandingOver} - - asked := serve(t, "rotate", handingOver, map[string]any{"result": "rotated"}) - got, err := c.Ask("seat:mesh-controller.rotate", map[string]any{}, "") - if err != nil || string(got.Result) != `"rotated"` || asked.Load() != 2 { - t.Fatalf("after a handover refusal: %s %v, asked %d times", got.Result, err, asked.Load()) - } - - asked = serve(t, "push", map[string]any{"error": "refused for its own reason"}) - if _, err := c.Ask("seat:mesh-controller.push", map[string]any{}, ""); err == nil || asked.Load() != 1 { - t.Fatalf("an unmarked refusal was asked again: %v, asked %d times", err, asked.Load()) - } - - asked = serve(t, "status", handingOver) - _, err = c.Ask("seat:mesh-controller.status", map[string]any{}, "") - if err == nil || asked.Load() != 2 || !strings.Contains(err.Error(), "both times refused as a handover") { - t.Fatalf("two handover refusals: %v, asked %d times", err, asked.Load()) - } -} diff --git a/node-tools/internal/bus/log.go b/node-tools/internal/bus/log.go index f7a49af..4997a7a 100644 --- a/node-tools/internal/bus/log.go +++ b/node-tools/internal/bus/log.go @@ -17,7 +17,7 @@ import ( ) // A module's log on the bus (novox/hq ADR 0297): a stream of operations, one subject per key, that the -// controller creates from what the module declared under `logs` and issues to the module's own instances +// mesh creates from what the module declared under `logs` and issues to the module's own instances // in its membership. It mirrors a bucket (ADR 0201) in how it is reached: what a module may reach is // decided here, from its membership, and refused with the reason before anything is sent. // @@ -139,7 +139,7 @@ func (c *Conn) issuedLog(module, name string) (LogIssued, error) { // notOnTheBus says a log is issued and its stream is not there yet. func notOnTheBus(l LogIssued) error { - return fmt.Errorf("the log %q is issued and its stream %s is not on the bus yet: the controller creates "+ + return fmt.Errorf("the log %q is issued and its stream %s is not on the bus yet: the mesh creates "+ "it from the catalogue on its next raise (novox/hq ADR 0297)", l.Name, l.Stream) } diff --git a/node-tools/internal/bus/pages.go b/node-tools/internal/bus/pages.go index 6351f4d..6fb6869 100644 --- a/node-tools/internal/bus/pages.go +++ b/node-tools/internal/bus/pages.go @@ -1,13 +1,7 @@ package bus import ( - "bytes" - "encoding/json" - "errors" "fmt" - "time" - - "github.com/nats-io/nats.go" "github.com/novox/mesh-tools/node-tools/internal/wire" ) @@ -15,97 +9,8 @@ import ( // An answer larger than the bus carries in one message (novox/hq issue 314). // // The bus carries one message of at most its max_payload, and the client library refuses a larger one -// before it leaves the process that sent it. Until this, a holder whose answer was too large wrote one -// line to its own log and its caller waited out RequestTimeout and read "did not answer" — the -// controller's `conditions` and `status` for hours on 2026-10-08, and the operator's channel with them. -// -// The controller pages such an answer (mesh-controller internal/link, pages.go): it answers an error -// carrying `paged` — the call it holds the answer under, its size and its parts — and a caller asks the -// same subject again once per part with PageHeader, each part answered raw with PartHeader. Asking the -// same subject means no grant beyond the one the caller already had. A module's answer too large is -// refused here, loudly, in a message that fits: its instances may be several, and a part asked again -// could reach another. - -const ( - // PageHeader asks for one part of a paged answer: ` `, from zero. - PageHeader = "Mesh-Page" - // PartHeader is on a part: `/`. - PartHeader = "Mesh-Part" - // partTries is how often one part is asked: during a handover a part can reach the controller that - // did not hold it. - partTries = 3 -) - -// PartTimeout bounds the asking of one part. -var PartTimeout = 10 * time.Second - -// paged is what a too-large answer says instead of itself. -type paged struct { - Call string `json:"call"` - Bytes int `json:"bytes"` - Parts int `json:"parts"` -} - -func pagedIn(data []byte) (paged, bool) { - if !bytes.Contains(data, []byte(`"paged"`)) { - return paged{}, false - } - var r struct { - Paged *paged `json:"paged"` - } - if json.Unmarshal(data, &r) != nil || r.Paged == nil || r.Paged.Call == "" || r.Paged.Parts < 1 { - return paged{}, false - } - return *r.Paged, true -} - -// whole is a reply's whole answer: the reply itself, or every part of a paged one, joined. A part that -// cannot be had is an error naming it, never a shorter answer. -func (c *Conn) whole(subject string, data []byte) ([]byte, error) { - p, ok := pagedIn(data) - if !ok { - return data, nil - } - out := make([]byte, 0, p.Bytes) - for n := 0; n < p.Parts; n++ { - var part []byte - var err error - for try := 0; try < partTries; try++ { - if part, err = c.askPart(subject, p, n); err == nil { - break - } - } - if err != nil { - return nil, fmt.Errorf("the answer was %d bytes, more than the bus carries in one message, paged as %s "+ - "in %d parts, and part %d could not be had: %w", p.Bytes, p.Call, p.Parts, n, err) - } - out = append(out, part...) - } - if len(out) != p.Bytes { - return nil, fmt.Errorf("the answer paged as %s was %d bytes, and its parts joined are %d", p.Call, p.Bytes, len(out)) - } - return out, nil -} - -func (c *Conn) askPart(subject string, p paged, n int) ([]byte, error) { - ask := nats.NewMsg(subject) - ask.Header.Set(PageHeader, fmt.Sprintf("%s %d", p.Call, n)) - ask.Data = []byte(`{}`) - reply, err := c.nc.RequestMsg(ask, PartTimeout) - if err != nil { - return nil, err - } - if got := reply.Header.Get(PartHeader); got != fmt.Sprintf("%d/%d", n, p.Parts) { - var r struct { - Error string `json:"error"` - } - if json.Unmarshal(reply.Data, &r) == nil && r.Error != "" { - return nil, errors.New(r.Error) - } - return nil, fmt.Errorf("asked part %d/%d, answered %q", n, p.Parts, got) - } - return reply.Data, nil -} +// before it leaves the process that sent it. A module's answer too large is refused here, loudly, in a +// message that fits, so its caller reads why instead of waiting out RequestTimeout. // fits is an answer the bus can carry: the answer itself, or — when it is larger than one message — // an error that says so, which is. diff --git a/node-tools/internal/bus/pages_test.go b/node-tools/internal/bus/pages_test.go index b06b1c1..181e8d5 100644 --- a/node-tools/internal/bus/pages_test.go +++ b/node-tools/internal/bus/pages_test.go @@ -2,7 +2,6 @@ package bus_test import ( "encoding/json" - "fmt" "strings" "testing" "time" @@ -13,8 +12,8 @@ import ( "github.com/novox/mesh-tools/node-tools/internal/meshtest" ) -// novox/hq issue 314: an answer larger than the bus carries in one message arrives whole when the -// controller pages it, and a module's is said to be too large — neither is a 30-second silence. +// novox/hq issue 314: a module's answer larger than the bus carries in one message is said to be too large, +// at once, not a 30-second silence. (The controller's paging ended with the controller, issue 1126.) func TestAnAnswerLargerThanOneMessage(t *testing.T) { url := meshtest.URL(t) seat, err := nats.Connect(url) @@ -27,43 +26,8 @@ func TestAnAnswerLargerThanOneMessage(t *testing.T) { t.Fatal(err) } defer c.Close() - limit := int(seat.MaxPayload()) + large := strings.Repeat("a condition held past its bound; ", 3*int(seat.MaxPayload())/32) - // The controller as it pages (mesh-controller internal/link, pages.go): the first answer an error - // with `paged`, each part asked again on the same subject with the page header. - large := strings.Repeat("a condition held past its bound; ", 3*limit/32) - whole, _ := json.Marshal(map[string]any{"result": large}) - partSize := limit - 1024 - parts := (len(whole) + partSize - 1) / partSize - sub, err := seat.Subscribe(bus.SeatToolSubject("mesh-controller", "conditions", "", ""), func(m *nats.Msg) { - if page := m.Header.Get(bus.PageHeader); page != "" { - var id string - var n int - fmt.Sscanf(page, "%s %d", &id, &n) - end := min((n+1)*partSize, len(whole)) - part := nats.NewMsg(m.Reply) - part.Header.Set(bus.PartHeader, fmt.Sprintf("%d/%d", n, parts)) - part.Data = whole[n*partSize : end] - _ = m.RespondMsg(part) - return - } - body, _ := json.Marshal(map[string]any{"error": "too large for one message", - "paged": map[string]any{"call": "call-1-1", "bytes": len(whole), "parts": parts}}) - _ = m.Respond(body) - }) - if err != nil { - t.Fatal(err) - } - defer sub.Unsubscribe() - _ = seat.Flush() - - got, err := c.Ask("seat:mesh-controller.conditions", map[string]any{}, "") - var said string - if err != nil || json.Unmarshal(got.Result, &said) != nil || said != large { - t.Fatalf("an answer of %d bytes in %d parts arrived as %d bytes: %v", len(whole), parts, len(got.Result), err) - } - - // A module's answer too large is refused here in words, at once. stop, err := c.HandleSubject("mesh.mod.journal.tool.lines", func(json.RawMessage) (any, error) { return large, nil }) if err != nil { t.Fatal(err) diff --git a/node-tools/internal/bus/provision.go b/node-tools/internal/bus/provision.go index 2e37576..e5f42ff 100644 --- a/node-tools/internal/bus/provision.go +++ b/node-tools/internal/bus/provision.go @@ -14,16 +14,16 @@ import ( // ADR 0370 changes it, issue 685). // // While a module's code runs here, its asks reach the bus under this runtime's account, and the bus proves only -// that account. So the runtime asks its own machine's client on ProvisionAskSubject — which the controller grants +// that account. So the runtime asks its own machine's client on ProvisionAskSubject — which the mesh grants // this machine's tool runner alone to publish (ADR 0370 §3) — naming the module itself, from the bundle that asks // and never from the module's code or arguments (§4). The client answers that module's config values, for what its // manifest declares, and no secret: the vault hands a secret only to a caller the bus proves (§2). -// ProvisionAskSubject is where this machine's tool runner asks its provisioner client (mesh-controller +// ProvisionAskSubject is where this machine's tool runner asks its provisioner client (the node-engine // internal/broker ProvisionerAskSubject): `prov.ask.`. func ProvisionAskSubject(node string) string { return "prov.ask." + node } -// ProvisionChangedSubjects is every module's word of what moved on a machine (mesh-controller internal/broker +// ProvisionChangedSubjects is every module's word of what moved on a machine (the node-engine // ProvisionerChangedSubject): `prov.changed..`, a module's name dots and all. func ProvisionChangedSubjects(node string) string { return "prov.changed." + node + ".>" } diff --git a/node-tools/internal/bus/seat.go b/node-tools/internal/bus/seat.go index dc5713d..5a84212 100644 --- a/node-tools/internal/bus/seat.go +++ b/node-tools/internal/bus/seat.go @@ -24,7 +24,7 @@ import ( // and anything else is refused here, with the reason, before it is sent. A bundle reaches this only through // its own standard input and output: no tool call does. -// SeatTraffic is what a membership lists of a module's seat traffic (the controller's broker.SeatTraffic). +// SeatTraffic is what a membership lists of a module's seat traffic (the grants part's record). type SeatTraffic struct { Publish []string `json:"publish,omitempty"` Subscribe []string `json:"subscribe,omitempty"` @@ -32,7 +32,7 @@ type SeatTraffic struct { Workers []SeatWorker `json:"workers,omitempty"` Records []string `json:"records,omitempty"` // Kinds are the holders of every kinded bench the module uses or watches, with what each promises: the - // controller's record of their claims (novox/hq ADR 0259 §5). + // record of their claims (novox/hq ADR 0259 §5). Kinds []KindHeld `json:"kinds,omitempty"` } @@ -45,7 +45,7 @@ type KindHeld struct { Capabilities []string `json:"capabilities,omitempty"` } -// SeatKinds is the kinds the module's membership names, as the controller issued them. +// SeatKinds is the kinds the module's membership names, as the grants part issued them. func (c *Conn) SeatKinds(module string) ([]KindHeld, error) { t, err := c.trafficOf(module) if err != nil { @@ -249,7 +249,7 @@ type Work struct { // SeatTake reads a worker the module's seat traffic lists and hands each message to deliver: acknowledged // when deliver returns nil, offered again after NakDelay when it returns an error, ended when it is not -// JSON. The worker is the controller's to create; this binds to it by name and never makes one. +// JSON. The worker is the mesh's to create; this binds to it by name and never makes one. func (c *Conn) SeatTake(module, consumer string, deliver func(Work) error) (func(), error) { t, err := c.trafficOf(module) if err != nil { diff --git a/node-tools/internal/bus/seat_test.go b/node-tools/internal/bus/seat_test.go index 6da91d3..fe49a19 100644 --- a/node-tools/internal/bus/seat_test.go +++ b/node-tools/internal/bus/seat_test.go @@ -98,7 +98,7 @@ func TestAModuleSubmitsAndSaysOnlyUnderItsOwnNameAndKind(t *testing.T) { t.Fatalf("an asker could not ask under its own name: %v", err) } for module, subject := range map[string]string{ - "mesh-delivery": "mesh.seat.operator-channel.accept.ask.mesh-controller", + "mesh-delivery": "mesh.seat.operator-channel.accept.ask.mesh-placement", "telegram": "mesh.seat.intake.event.choice.desktop", "messenger": "mesh.seat.intake.event.choice.telegram", } { @@ -206,7 +206,7 @@ func TestAnAskerReadsItsOwnRecordAndNoOther(t *testing.T) { if _, err := kv.Put("mesh-delivery.a1", []byte(`{"state":"open"}`)); err != nil { t.Fatal(err) } - if _, err := kv.Put("mesh-controller.a1", []byte(`{"state":"open"}`)); err != nil { + if _, err := kv.Put("mesh-placement.a1", []byte(`{"state":"open"}`)); err != nil { t.Fatal(err) } got, err := conn.SeatRecord("mesh-delivery", "messenger_asks", "a1") diff --git a/node-tools/internal/bus/state.go b/node-tools/internal/bus/state.go index 1f80af5..f396cc6 100644 --- a/node-tools/internal/bus/state.go +++ b/node-tools/internal/bus/state.go @@ -14,7 +14,7 @@ import ( "github.com/nats-io/nats.go/jetstream" ) -// A module's state on the bus (novox/hq ADR 0201): key-value buckets the controller creates from what +// A module's state on the bus (novox/hq ADR 0201): key-value buckets the mesh creates from what // the module declared, and issues to each assignment in its membership by the name the module uses — // its own state by the local name, another module's as `.`. // @@ -89,7 +89,7 @@ func (c *Conn) bucket(ctx context.Context, s StateIssued) (jetstream.KeyValue, e kv, err := js.KeyValue(ctx, s.Bucket) if err != nil { if errors.Is(err, jetstream.ErrBucketNotFound) { - return nil, fmt.Errorf("the state %q is issued and its bucket is not on the bus yet: the controller "+ + return nil, fmt.Errorf("the state %q is issued and its bucket is not on the bus yet: the mesh "+ "creates it from the catalogue on its next raise", s.Name) } return nil, err diff --git a/node-tools/internal/bus/state_watch_test.go b/node-tools/internal/bus/state_watch_test.go index 3743d0e..cb69445 100644 --- a/node-tools/internal/bus/state_watch_test.go +++ b/node-tools/internal/bus/state_watch_test.go @@ -33,7 +33,7 @@ func TestAWatchHoldsWhileItsCurrentValuesKeepArriving(t *testing.T) { for i := 0; i < older; i++ { keys = append(keys, fmt.Sprintf("novox/mesh-catalog.%d.%040x.status=mesh/check", 400+i, i)) } - for _, member := range []string{"novox/mesh-sdk.76.34f210e5", "novox/mesh-controller.453.c06f75d0", "novox/mesh-delivery.33.28ee22ef"} { + for _, member := range []string{"novox/mesh-sdk.76.34f210e5", "novox/mesh-placement.453.c06f75d0", "novox/mesh-delivery.33.28ee22ef"} { keys = append(keys, member+strings.Repeat("0", 32)+".status=mesh/check") } for _, k := range keys { diff --git a/node-tools/internal/bus/streams.go b/node-tools/internal/bus/streams.go index fe94e92..33e25e8 100644 --- a/node-tools/internal/bus/streams.go +++ b/node-tools/internal/bus/streams.go @@ -14,7 +14,7 @@ import ( // A module's own stream, as the runtime publishes to it and hears it (novox/hq ADR 0363 §8, issue 725). // -// A module may declare a stream of its own; the controller creates it and says, in the module's +// A module may declare a stream of its own; the mesh creates it and says, in the module's // membership, which of its events go there and on which subject (`streams`), and, in the membership of // each module granted to hear it, which durable consumer on it to bind (`hears`). The runtime publishes and // binds exactly as the membership says. A membership naming neither is what every membership was before: @@ -30,7 +30,7 @@ type StreamIssued struct { } // HeardIssued is one module stream an assignment hears: the stream and the durable consumer on it the -// runtime binds. The consumer is the controller's to make, to filter and to delete. +// runtime binds. The consumer is the mesh's to make, to filter and to delete. type HeardIssued struct { Stream string `json:"stream"` Consumer string `json:"consumer"` @@ -73,7 +73,7 @@ func eventMatches(pattern, event string) bool { } // RebindEvery is how often a consumer the membership names and the bus did not let the runtime bind is -// tried again: the controller makes the consumer and sends the user list carrying its grant in the +// tried again: the mesh makes the consumer and sends the user list carrying its grant in the // composition that issues the membership, so one may arrive before the other (ADR 0363 §6). var RebindEvery = 30 * time.Second @@ -294,7 +294,7 @@ func (h *hearing) reconcile() { } } // **A loop the bus ended is no longer reading** (nats.go ends a pull loop on its own when its consumer is - // deleted): the controller deletes and makes again under the same name a restricted consumer it cannot + // deleted): the mesh deletes and makes again under the same name a restricted consumer it cannot // narrow (ADR 0363 §6), and the membership, unchanged, says nothing — so it is bound again here. // **A loop the bus did not end is asked about**: nats.go learns of a deleted consumer only from the answer // to a pull it has pending, and a consumer deleted and made again between two pulls, or while none is diff --git a/node-tools/internal/console/address.go b/node-tools/internal/console/address.go index 95954b0..e3f8aaf 100644 --- a/node-tools/internal/console/address.go +++ b/node-tools/internal/console/address.go @@ -50,7 +50,7 @@ const ( // searchCap is how many matches a search answers before it says how many more there were. const searchCap = 25 -const grammar = "Addresses: `.` for a seat held once for the mesh (e.g. `mesh-controller.nodes`); " + +const grammar = "Addresses: `.` for a seat held once for the mesh (e.g. `mesh-placement.placement`); " + "`/.` for a seat every machine holds (e.g. `ace/node-packet-filter.rules`); " + "`/.` for a module on one machine (e.g. `novox/postgres.postgres_list_databases`); " + "and `.` also for a module whose instances are interchangeable." @@ -242,7 +242,7 @@ type recordedModule struct { } // indexOn asks the mesh what it holds (novox/hq ADR 0197): what answers, from every runtime's own -// announcement on the bus — one `$SRV.INFO` request — and what should, from the controller's records +// announcement on the bus — one `$SRV.INFO` request — and what should, from the indexer's records // read as JSON. Nothing is inferred from a roster and nothing is parsed from print. // // `held` is what the console holds already, so an instance whose announcement is unchanged is not @@ -257,9 +257,9 @@ func indexOn(conn *bus.Conn, held announce.Known, before func(announce.Discovery var recordsWhy []string wg.Add(2) // Which modules there are and where they are placed, and which machines the mesh has, from the bus (ADR 0571 - // clauses 4 and 5): the indexer's modules and manifests and the placement seat's placements, never the controller. + // clauses 4 and 5): the indexer's modules and manifests and the placement seat's placements, never any other record. go func() { defer wg.Done(); recorded, known, recordsWhy = recordsOn(conn) }() - // Who holds which seat on which node, from placement (ADR 0571 clause 4): no call of the controller. + // Who holds which seat on which node, from placement (ADR 0571 clause 4): no other call. go func() { defer wg.Done(); placed, placedErr = seatHoldersOn(conn) }() d, err := announce.Gather(conn, held) wg.Wait() @@ -815,7 +815,7 @@ func firstLine(s string) string { // the machine, `node` is taken out: the machine is in the address. Where it does not — a seat held // once for the mesh, or an interchangeable module — `node` is the tool's own argument, and is kept. // -// **Taking it out everywhere made the mesh's own verbs lie** (novox/hq issue 244): the controller's +// **Taking it out everywhere made the mesh's own verbs lie** (novox/hq issue 244): the old controller verbs // `push`, `plan`, `assign`, `pin` and `settings` take the machine they act on as `node`, and were // described without it — `push` as taking nothing at all, so a push naming one machine arrived as a // push of every machine behind. diff --git a/node-tools/internal/console/address_test.go b/node-tools/internal/console/address_test.go index 883589e..fec01f4 100644 --- a/node-tools/internal/console/address_test.go +++ b/node-tools/internal/console/address_test.go @@ -71,9 +71,9 @@ func TestTheMeshsToolsAreFoundByAddress(t *testing.T) { } // The controller: its own seat's verbs announced on the bus like every runtime's; its records are asked by no one. - controller := connect(t, "mesh-controller", "bench") + controller := connect(t, "mesh-placement", "bench") // Its verb nodes, called through the console as any seat's verb is. - stopNodes, err := controller.HandleSubject("mesh.seat.mesh-controller.tool.nodes", func(json.RawMessage) (any, error) { + stopNodes, err := controller.HandleSubject("mesh.seat.mesh-placement.tool.nodes", func(json.RawMessage) (any, error) { return map[string]any{"ok": true, "output": `[{"name":"bench","mode":"converged"},{"name":"desk","mode":"converged"}]`}, nil }) if err != nil { @@ -82,8 +82,8 @@ func TestTheMeshsToolsAreFoundByAddress(t *testing.T) { t.Cleanup(stopNodes) // What the mesh holds, as the indexer and placement write it (ADR 0571 clauses 4, 5): never the controller's records. tools := map[string][]string{"alpha": {"one", "two"}, "beta": {"three"}, "gamma": {"given"}, "delta": nil, - "epsilon": {"seven"}, "mesh-controller": {"nodes"}} - mesh.Placed(t, tools, map[string][]string{"desk": {"alpha", "beta", "delta"}, "bench": {"epsilon", "mesh-controller"}}) + "epsilon": {"seven"}, "mesh-placement": {"nodes"}} + mesh.Placed(t, tools, map[string][]string{"desk": {"alpha", "beta", "delta"}, "bench": {"epsilon", "mesh-placement"}}) console := mt.MembershipOf("node-tools", "desk", false, nil) console.State = mt.ConsoleReads() mesh.Issue(t, console) @@ -93,11 +93,11 @@ func TestTheMeshsToolsAreFoundByAddress(t *testing.T) { } return nil }) - stopAnn, err := announce.Serve(controller, announce.Service{Name: "mesh-controller", ID: "bench"}, func() []announce.Endpoint { - return []announce.Endpoint{{Kind: announce.KindSeat, Module: "mesh-controller", Seat: "mesh-controller", Scope: "mesh", + stopAnn, err := announce.Serve(controller, announce.Service{Name: "mesh-placement", ID: "bench"}, func() []announce.Endpoint { + return []announce.Endpoint{{Kind: announce.KindSeat, Module: "mesh-placement", Seat: "mesh-placement", Scope: "mesh", // As the live controller announces: a mesh seat's holder names no machine. Tool: "nodes", Node: "", Description: "Every machine the mesh knows.", Schema: json.RawMessage(`{}`), - Subject: "mesh.seat.mesh-controller.tool.nodes"}} + Subject: "mesh.seat.mesh-placement.tool.nodes"}} }) if err != nil { t.Fatal(err) @@ -131,13 +131,13 @@ func TestTheMeshsToolsAreFoundByAddress(t *testing.T) { // The overview names the mesh's seats, the machines' seats and the machines. overview, isErr := call(t, endpoint, "mesh_overview", nil) - if isErr || !strings.Contains(overview, "mesh-controller.nodes") || !strings.Contains(overview, "/node-shelf.list") || + if isErr || !strings.Contains(overview, "mesh-placement.nodes") || !strings.Contains(overview, "/node-shelf.list") || !strings.Contains(overview, `"bench"`) || !strings.Contains(overview, `"desk"`) { t.Errorf("overview: %s", overview) } // Silence is named only where tools should have answered: epsilon declares tools on bench and // nothing there announced it; delta declares none and is never a name. - if strings.Contains(overview, "mesh-controller on bench") { + if strings.Contains(overview, "mesh-placement on bench") { t.Errorf("a mesh seat's holder that announced no machine is called silent on its machine:\n%s", overview) } if !strings.Contains(overview, "epsilon on bench") || strings.Contains(overview, "delta") { @@ -150,7 +150,7 @@ func TestTheMeshsToolsAreFoundByAddress(t *testing.T) { } // A mesh seat, a node seat, an assignment and an interchangeable module, each by address. - if got, isErr := call(t, endpoint, "mesh_call", map[string]any{"address": "mesh-controller.nodes"}); isErr || !strings.Contains(got, "converged") { + if got, isErr := call(t, endpoint, "mesh_call", map[string]any{"address": "mesh-placement.nodes"}); isErr || !strings.Contains(got, "converged") { t.Errorf("mesh seat: %s", got) } if got, isErr := call(t, endpoint, "mesh_call", map[string]any{"address": "desk/node-shelf.list"}); isErr || !strings.Contains(got, `"a"`) { @@ -166,11 +166,11 @@ func TestTheMeshsToolsAreFoundByAddress(t *testing.T) { // Refused, by name, where the address does not say enough or says the wrong thing. for address, want := range map[string]string{ - "beta.three": "keeps state on each machine it runs on, so a call names the machine: write /beta.three — it runs on desk", - "node-shelf.list": "held once per machine: write /node-shelf.list — it is held on desk", - "desk/mesh-controller.nodes": "held once for the whole mesh", - "bench/beta.three": "beta does not run on bench; it runs on desk", - "nonsense": "is not an address", + "beta.three": "keeps state on each machine it runs on, so a call names the machine: write /beta.three — it runs on desk", + "node-shelf.list": "held once per machine: write /node-shelf.list — it is held on desk", + "desk/mesh-placement.nodes": "held once for the whole mesh", + "bench/beta.three": "beta does not run on bench; it runs on desk", + "nonsense": "is not an address", } { got, isErr := call(t, endpoint, "mesh_call", map[string]any{"address": address}) if !isErr || !strings.Contains(got, want) { @@ -201,7 +201,7 @@ func TestTheMeshsToolsAreFoundByAddress(t *testing.T) { t.Fatalf("gamma was found before it served: %s", got) } mesh.Issue(t, mt.MembershipOf("gamma", "desk", false, nil)) - mesh.Placed(t, tools, map[string][]string{"desk": {"alpha", "beta", "delta", "gamma"}, "bench": {"epsilon", "mesh-controller"}}) + mesh.Placed(t, tools, map[string][]string{"desk": {"alpha", "beta", "delta", "gamma"}, "bench": {"epsilon", "mesh-placement"}}) late := connect(t, "node-tools", "desk") stopLate, err := runtime.Run(late, []runtime.Served{{Module: "gamma", Entrypoints: []string{mt.Fixture("env-gamma.serve.mjs")}}}, nil, (&mt.Logs{}).Logf) diff --git a/node-tools/internal/console/client.go b/node-tools/internal/console/client.go index 41fdeb5..751731b 100644 --- a/node-tools/internal/console/client.go +++ b/node-tools/internal/console/client.go @@ -118,9 +118,6 @@ var ( timedOut = regexp.MustCompile(`(?i)timeout`) ) -// controllerSeat is the seat whose holder keeps what came of every call (novox/hq issue 265). -const controllerSeat = "mesh-controller" - // whyItFailed says why a call failed, so the remedy is in the words. func whyItFailed(key string, err error) string { if err == nil { @@ -145,12 +142,6 @@ func whyItFailed(key string, err error) string { said := key + " did not answer within " + bus.RequestTimeout.String() + ". That is not a failure: " + "something is serving it, so it may still be running and may already have done what was " + "asked — check before repeating a call that changes the mesh." - if strings.HasPrefix(strings.TrimPrefix(key, "seat:"), controllerSeat+".") { - // The controller answers every call within seconds, with an id while it runs, so silence - // from it is an answer lost on the way — which it keeps. - said += " The controller answers every call within seconds, so its answer was lost on the " + - "way; " + controllerSeat + ".calls lists its recent calls and what came of each." - } return said } return key + " failed: " + msg diff --git a/node-tools/internal/console/mcp.go b/node-tools/internal/console/mcp.go index b7d5e80..66a196a 100644 --- a/node-tools/internal/console/mcp.go +++ b/node-tools/internal/console/mcp.go @@ -63,7 +63,7 @@ type Surface struct { } // MethodSeats is the console's JSON-RPC method beside MCP's that answers every seat's verbs and holders, in the shape -// the controller's `tools` answered: `{seats: [{seat, scope, tools: [{name, description, input, replaces}], holders: +// the old `tools` answered: `{seats: [{seat, scope, tools: [{name, description, input, replaces}], holders: // [{module, node}]}], holders_why}`. const MethodSeats = "mesh/seats" @@ -157,7 +157,7 @@ func (s *Surface) HandleContext(ctx context.Context, r Request) *Reply { return answer(r.ID, map[string]any{}) case MethodSeats: // The seats' contract and who holds each, for a client on this machine that has no seat definitions of its own - // (the TypeScript `mesh`): read in-process from mesh-sdk and from placement, never asked of the controller + // (the TypeScript `mesh`): read in-process from mesh-sdk and from placement, never asked of anything else // (novox/hq ADR 0571 clauses 4 and 5). return answer(r.ID, s.seatsAnswer()) case "tools/list": @@ -338,7 +338,7 @@ func (s *Surface) instructions() string { "that serves it; what may be called was fixed when this account was issued, so a " + "refusal means the account, not the tool. The list is what the running modules " + "answered, plus every role's tools from the mesh's records — the mesh's own verbs " + - "(mesh-controller.status, .push, .assign …) among them; a module that did not answer " + + "(mesh-delivery.deliveries, mesh-placement.propose …) among them; a module that did not answer " + "is named in the list's _meta and can still be called by .." } return "The tools of a Novox mesh, reached as " + s.who + ", found by address rather than listed " + diff --git a/node-tools/internal/console/node_argument_test.go b/node-tools/internal/console/node_argument_test.go index 23f0030..2f00e51 100644 --- a/node-tools/internal/console/node_argument_test.go +++ b/node-tools/internal/console/node_argument_test.go @@ -11,9 +11,9 @@ import ( // 2026-10-05 the controller's push was described as taking nothing, and a push naming one machine // ran as a push of every machine behind. func TestAMeshSeatsNodeIsItsOwnArgument(t *testing.T) { - push := Tool{Module: "mesh-controller", Name: "push", Seat: true, Scope: "mesh", + push := Tool{Module: "mesh-placement", Name: "push", Seat: true, Scope: "mesh", Input: json.RawMessage(`{"type":"object","properties":{"node":{"type":"string"},"behind":{"type":"string"}}}`)} - mesh := target{Address: "mesh-controller.push", Tool: push, Seat: true} + mesh := target{Address: "mesh-placement.push", Tool: push, Seat: true} props := schemaAsPassed(mesh)["properties"].(map[string]any) if _, has := props["node"]; !has { diff --git a/node-tools/internal/console/refusal_test.go b/node-tools/internal/console/refusal_test.go index 6e6a87d..288cb12 100644 --- a/node-tools/internal/console/refusal_test.go +++ b/node-tools/internal/console/refusal_test.go @@ -22,16 +22,23 @@ func TestARefusalIsSaidOnlyForTheBussOwn(t *testing.T) { } // **A timeout says it is not a failure** (novox/hq issue 265): a push that outlasted the wait had -// pushed, and the caller read "did not answer in time". And from the controller, where its answer is. +// pushed, and the caller read "did not answer in time". No hint names a seat that is gone (novox/hq issue 1126): +// the grammar's example, the handshake's instructions and a timeout's remedy named the controller's verbs. func TestATimeoutIsNotAFailure(t *testing.T) { - got := whyItFailed("seat:mesh-controller.push", errors.New("timeout")) - for _, want := range []string{"not a failure", "may already have done", "mesh-controller.calls"} { + got := whyItFailed("seat:mesh-delivery.deliveries", errors.New("timeout")) + for _, want := range []string{"not a failure", "may already have done"} { if !strings.Contains(got, want) { - t.Errorf("a controller timeout does not say %q: %s", want, got) + t.Errorf("a seat's timeout does not say %q: %s", want, got) } } - if other := whyItFailed("novox/postgres.postgres_list_databases", errors.New("timeout")); !strings.Contains(other, "not a failure") || - strings.Contains(other, "calls") { - t.Errorf("a module's timeout: %s", other) + for name, text := range map[string]string{ + "grammar": grammar, + "flat instructions": (&Surface{Flat: true, who: "x"}).instructions(), + "deep instructions": (&Surface{who: "x"}).instructions(), + "timeout": got, + } { + if strings.Contains(text, "controller") { + t.Errorf("%s names the controller: %s", name, text) + } } } diff --git a/node-tools/internal/console/runtimes_test.go b/node-tools/internal/console/runtimes_test.go index 720e271..5287279 100644 --- a/node-tools/internal/console/runtimes_test.go +++ b/node-tools/internal/console/runtimes_test.go @@ -95,7 +95,7 @@ func TestARuntimeThatAnswersLateIsFoundAndOneThatNeverDoesIsNamed(t *testing.T) Subject: "mesh.mod.nextcloud-client.tool.nextcloud_check.desktop"}, }) - controller := connect(t, "mesh-controller", "bench") + controller := connect(t, "mesh-placement", "bench") var toolsAsked atomic.Int32 for verb, output := range map[string]string{ // The controller's records are served here only to see that none is asked (ADR 0571 clauses 4 and 5). @@ -104,7 +104,7 @@ func TestARuntimeThatAnswersLateIsFoundAndOneThatNeverDoesIsNamed(t *testing.T) "tools": `{"seats":[]}`, } { output, verb := output, verb - stop, err := controller.HandleSubject("mesh.seat.mesh-controller.tool."+verb, func(json.RawMessage) (any, error) { + stop, err := controller.HandleSubject("mesh.seat.mesh-placement.tool."+verb, func(json.RawMessage) (any, error) { toolsAsked.Add(1) return map[string]any{"output": output, "ok": true}, nil }) diff --git a/node-tools/internal/console/search_test.go b/node-tools/internal/console/search_test.go index 4236d03..c624a07 100644 --- a/node-tools/internal/console/search_test.go +++ b/node-tools/internal/console/search_test.go @@ -33,8 +33,8 @@ func meshToSearch(t *testing.T) *index { "node": "What one machine reported it can do, what it is assigned, and why.", "nodes": "Every machine the mesh knows, with whether it is converged or adopted.", } { - endpoints = append(endpoints, announce.Endpoint{Kind: announce.KindSeat, Module: "mesh-controller", Seat: "mesh-controller", - Tool: verb, Scope: "mesh", Description: does, Subject: "mesh.seat.mesh-controller.tool." + verb}) + endpoints = append(endpoints, announce.Endpoint{Kind: announce.KindSeat, Module: "mesh-placement", Seat: "mesh-placement", + Tool: verb, Scope: "mesh", Description: does, Subject: "mesh.seat.mesh-placement.tool." + verb}) } for tool, does := range map[string]string{ "docker_list": "Every container on this machine: name, image, state, ports.", @@ -56,22 +56,21 @@ func meshToSearch(t *testing.T) *index { func TestSearchFindsTheToolThatReplacesACommandFirst(t *testing.T) { x := meshToSearch(t) for query, want := range map[string]string{ - "journalctl": "anchor/node-service-manager.journal", - "journalctl -u mesh-controller -n 50": "anchor/node-service-manager.journal", - "sudo journalctl -fu sshd": "anchor/node-service-manager.journal", - "/usr/bin/journalctl": "anchor/node-service-manager.journal", - "logs": "anchor/node-service-manager.journal", - "systemctl status": "anchor/node-service-manager.status", - "systemctl status nats.service": "anchor/node-service-manager.status", - "systemctl restart mesh-controller": "anchor/node-service-manager.restart", - "systemctl --failed": "anchor/node-service-manager.failed", - "docker ps": "anchor/docker.docker_list", - "docker ps -a": "anchor/docker.docker_list", - "docker logs": "anchor/docker.docker_logs", - "docker logs --tail 100 nats": "anchor/docker.docker_logs", - "docker restart nats": "anchor/docker.docker_restart", - "container logs": "anchor/docker.docker_logs", - "uptime": "mesh-controller.node", + "journalctl": "anchor/node-service-manager.journal", + "journalctl -u mesh-placement -n 50": "anchor/node-service-manager.journal", + "sudo journalctl -fu sshd": "anchor/node-service-manager.journal", + "/usr/bin/journalctl": "anchor/node-service-manager.journal", + "logs": "anchor/node-service-manager.journal", + "systemctl status": "anchor/node-service-manager.status", + "systemctl status nats.service": "anchor/node-service-manager.status", + "systemctl restart mesh-placement": "anchor/node-service-manager.restart", + "systemctl --failed": "anchor/node-service-manager.failed", + "docker ps": "anchor/docker.docker_list", + "docker ps -a": "anchor/docker.docker_list", + "docker logs": "anchor/docker.docker_logs", + "docker logs --tail 100 nats": "anchor/docker.docker_logs", + "docker restart nats": "anchor/docker.docker_restart", + "container logs": "anchor/docker.docker_logs", } { hits := x.search(query) if len(hits) == 0 { @@ -112,7 +111,7 @@ func TestASearchByCommandSaysWhatItReplaces(t *testing.T) { // A synonym is a word, never part of one: `logs` matches through `log`, and finds no `login`. func TestSearchStillMatchesEveryWord(t *testing.T) { x := meshToSearch(t) - if hits := x.search("machine converged"); len(hits) != 1 || hits[0].Address != "mesh-controller.nodes" { + if hits := x.search("machine converged"); len(hits) != 1 || hits[0].Address != "mesh-placement.nodes" { t.Errorf("words in a description: %+v", hits) } if hits := x.search("journalctl kubernetes"); len(hits) != 1 || hits[0].Address != "anchor/node-service-manager.journal" { diff --git a/node-tools/internal/console/wait.go b/node-tools/internal/console/wait.go index 40449ec..9ee1a41 100644 --- a/node-tools/internal/console/wait.go +++ b/node-tools/internal/console/wait.go @@ -3,7 +3,7 @@ package console // mesh_wait: a session waits on the bus for one event instead of polling (novox/hq ADR 0367). // // **Only the families the tool runner was issued** (§2, §6): its membership names them, composed by the -// controller from the tool runner's manifest together with the grant that lets this account subscribe them. +// mesh from the tool runner's manifest together with the grant that lets this account subscribe them. // A name not listed there is refused here, before the bus would refuse it; no argument or setting adds one. // **An event only wakes the session** (§4): what it says is read again from the verb that owns it. diff --git a/node-tools/internal/meshtest/meshtest.go b/node-tools/internal/meshtest/meshtest.go index 448e27a..9ca7cbf 100644 --- a/node-tools/internal/meshtest/meshtest.go +++ b/node-tools/internal/meshtest/meshtest.go @@ -1,7 +1,7 @@ -// Package meshtest raises what the controller would, for tests against a real bus: the ASSIGNMENTS +// Package meshtest raises what the mesh would, for tests against a real bus: the ASSIGNMENTS // and EVENTS streams, memberships issued by hand, and a fixture's path. // -// **Every test has a bus of its own** (novox/hq ADR 0237, the controller's internal/testbus): a server +// **Every test has a bus of its own** (novox/hq ADR 0237, the old testbus): a server // linked in at the release go.mod pins — the release the mesh runs, held so by a test beside this one — // started for the one test and gone after it. The packages shared one bus and had to run one at a time: // each test raised the streams afresh, and the console discovers every runtime that announces itself on @@ -72,7 +72,7 @@ func URL(t *testing.T) string { // buses are the running tests' buses, by test. var buses sync.Map -// Mesh is the controller's job, done by hand. +// Mesh is the mesh's job, done by hand. type Mesh struct { nc *nats.Conn js nats.JetStreamContext @@ -230,7 +230,7 @@ func (m *Mesh) NextEvent(t *testing.T, pattern string) <-chan string { return ch } -// MembershipOf is a membership as the controller issues one on a machine. +// MembershipOf is a membership as the mesh issues one on a machine. func MembershipOf(module, node string, plain bool, seats map[string][]string) bus.Membership { own := "mesh.mod." + module serves := []bus.Served{{Subject: own + ".tool.{tool}." + node}} @@ -291,7 +291,7 @@ func (l *Logs) Has(fragments ...string) bool { // All is every line. func (l *Logs) All() string { return strings.Join(l.lines, "\n") } -// Consumer makes a module's durable consumer on a machine as the controller does: pull, on the +// Consumer makes a module's durable consumer on a machine as the mesh does: pull, on the // EVENTS stream, filtered to what the module consumes, with a short ack wait so a test sees a // redelivery in seconds rather than the mesh's minutes. func (m *Mesh) Consumer(t *testing.T, node, module string, filters []string, ackWait time.Duration) { @@ -351,7 +351,7 @@ func (m *Mesh) BucketKeeping(t *testing.T, name string, history uint8) nats.KeyV return kv } -// Bucket makes a module's state afresh as the controller does from the catalogue (novox/hq ADR 0201), +// Bucket makes a module's state afresh as the mesh does from the catalogue (novox/hq ADR 0201), // and answers it for writing what is there before a bundle starts. func (m *Mesh) Bucket(t *testing.T, name string) nats.KeyValue { t.Helper() @@ -363,7 +363,7 @@ func (m *Mesh) Bucket(t *testing.T, name string) nats.KeyValue { return kv } -// BucketKeyTTL makes a module's state afresh as the controller does for one declared `key-ttl` (mesh-sdk buscompose: +// BucketKeyTTL makes a module's state afresh as the mesh does for one declared `key-ttl` (mesh-sdk buscompose: // a time to live per key allowed, its end leaving a marker kept a minute; issue 1952). func (m *Mesh) BucketKeyTTL(t *testing.T, name string) { t.Helper() @@ -380,10 +380,10 @@ func (m *Mesh) BucketKeyTTL(t *testing.T, name string) { } } -// Log makes a module's log afresh as the controller does from the catalogue (novox/hq ADR 0297): a file +// Log makes a module's log afresh as the mesh does from the catalogue (novox/hq ADR 0297): a file // stream named LOG__ on mesh.log...>, limits retention, no age, no cap per // key, the declared cap, 260 KiB a message (a 256 KiB value and its headers), refusing new entries when full, direct get, no delete or -// purge. It answers the membership entry the controller issues for it. +// purge. It answers the membership entry the mesh issues for it. func (m *Mesh) Log(t *testing.T, module, name string, maxBytes int64) bus.LogIssued { t.Helper() stream := "LOG_" + module + "_" + name @@ -409,7 +409,7 @@ func (m *Mesh) Consumers(t *testing.T, stream string) int { return info.State.Consumers } -// ModuleStream makes a module's own stream afresh as the controller does from the catalogue (novox/hq ADR +// ModuleStream makes a module's own stream afresh as the mesh does from the catalogue (novox/hq ADR // 0363 §1): named in capitals, taking `mesh.stream..event.>`, a subject space EVENTS does not take. func (m *Mesh) ModuleStream(t *testing.T, module, name string) { t.Helper() @@ -455,7 +455,7 @@ func (m *Mesh) Messages(t *testing.T, stream string) uint64 { return info.State.Msgs } -// RemoveConsumer deletes a module's consumer on a machine on a stream, as the controller does when a grant +// RemoveConsumer deletes a module's consumer on a machine on a stream, as the mesh does when a grant // it cannot narrow is withdrawn (novox/hq ADR 0363 §6). func (m *Mesh) RemoveConsumer(t *testing.T, stream, node, module string) { t.Helper() diff --git a/node-tools/internal/provide/provide.go b/node-tools/internal/provide/provide.go index e51e5c7..c7a400c 100644 --- a/node-tools/internal/provide/provide.go +++ b/node-tools/internal/provide/provide.go @@ -1,6 +1,6 @@ // Package provide is the runtime's half of node-tools being a provider of `mcp-endpoint`: it runs the mesh SDK's // provider helper, so a consumer handed to the provisioner (claude-code's `${bound:mcp-endpoint:port}`) is answered -// from an entry node-tools writes in its own bucket, not from the controller's binding file (novox/hq ADR 0349 +// from an entry node-tools writes in its own bucket, not from the old binding file (novox/hq ADR 0349 // clause 12, issue 791). // // The offer carries no credential: the console's authority is the machine's login (ADR 0152, 0175), so the helper @@ -40,7 +40,7 @@ func Offers() map[string]provider.Offer { // busOf is the helper's bus over the runtime's own state: the provider's bucket is its state `bound`, and the // provisions and plans of the seat mesh-provisioner are the states it reads, by mesh-sdk's names (novox/hq ADR 0570, -// issue 1954 L6-T1). The controller's assignments are read no more (mesh-sdk 692dbf9, L6-P3). +// issue 1954 L6-T1). The old assignments are read no more (mesh-sdk 692dbf9, L6-P3). type busOf struct{ conn *bus.Conn } func (b busOf) state(bucket string) (string, error) { diff --git a/node-tools/internal/provide/provide_test.go b/node-tools/internal/provide/provide_test.go index 216e1ef..a6aa784 100644 --- a/node-tools/internal/provide/provide_test.go +++ b/node-tools/internal/provide/provide_test.go @@ -160,8 +160,8 @@ func TestTheOfferServesThePortTheManifestServes(t *testing.T) { } } for _, r := range m.Reads { - if r == "mesh-controller.assignments" { - t.Error("the manifest still reads mesh-controller.assignments, which the sdk's helper no longer reads (issue 1954)") + if r == "mesh-placement.assignments" { + t.Error("the manifest still reads mesh-placement.assignments, which the sdk's helper no longer reads (issue 1954)") } } for _, a := range m.Build.Artifacts { @@ -195,9 +195,9 @@ func TestOnlyTheMachinesRuntimeStartsTheHelper(t *testing.T) { } } -// 774/S33 (ADR 0571 clause 13): this manifest, read as the lease write's check reads it, is not refused at the write: it -// reads and invokes nothing of the controller and consumes nothing of it. -func TestTheManifestIsNotRefusedAtTheLeaseWrite(t *testing.T) { +// 1126/B19 (ADR 0590): this manifest is not refused by the check that refuses any manifest naming the controller: +// it reads, invokes, claims and requires nothing of it. +func TestTheManifestNamesNothingOfTheController(t *testing.T) { raw, err := os.ReadFile("../../module.json") if err != nil { t.Fatal(err) @@ -206,7 +206,7 @@ func TestTheManifestIsNotRefusedAtTheLeaseWrite(t *testing.T) { if err != nil { t.Fatal(err) } - if refused := catalogue.ManifestRefusals(map[string]catalogue.Manifest{Module: m}, catalogue.PhaseWrite); len(refused) != 0 { - t.Errorf("the lease write's check refuses node-tools' manifest: %v", refused) + if refused := catalogue.ManifestRefusals(map[string]catalogue.Manifest{Module: m}); len(refused) != 0 { + t.Errorf("the controller-dependence check refuses node-tools' manifest: %v", refused) } } diff --git a/node-tools/internal/runtime/seat_traffic_test.go b/node-tools/internal/runtime/seat_traffic_test.go index bd80b5d..00a4723 100644 --- a/node-tools/internal/runtime/seat_traffic_test.go +++ b/node-tools/internal/runtime/seat_traffic_test.go @@ -70,7 +70,7 @@ func TestABundlesSeatTrafficGoesThroughItsModulesBus(t *testing.T) { if !strings.Contains(string(first), `"duplicate":false`) || !strings.Contains(string(again), `"duplicate":true`) { t.Errorf("the same id twice: %s, then %s", first, again) } - if _, err := asking.Seat("publish", json.RawMessage(`{"subject":"mesh.seat.operator-channel.accept.ask.mesh-controller","body":{}}`), nil); err == nil { + if _, err := asking.Seat("publish", json.RawMessage(`{"subject":"mesh.seat.operator-channel.accept.ask.mesh-placement","body":{}}`), nil); err == nil { t.Error("a module submitted under another's name") } got, err := asking.Seat("record", json.RawMessage(`{"key":"d-1"}`), nil) diff --git a/node-tools/src/broker-nats.ts b/node-tools/src/broker-nats.ts index 155ff64..a413a3b 100644 --- a/node-tools/src/broker-nats.ts +++ b/node-tools/src/broker-nats.ts @@ -433,7 +433,7 @@ export async function connectNats( /** * React to events. * - * The durable consumer is the **controller's** to create, from what this module declared it + * The durable consumer is the **mesh's** to create, from what this module declared it * consumes (design 29 §3) — this binds to it and never creates one. A runtime that created * its own would be a module deciding its own delivery semantics, and its account cannot * reach the JetStream API to do it anyway. @@ -499,14 +499,14 @@ async function deliver( env = toEnvelope(msg); } catch { // Unparseable: terminate it. Redelivering a message no version of this code can read is - // an infinite loop, and what the controller keeps of a message given up on (DEAD_LETTERS, + // an infinite loop, and what the mesh keeps of a message given up on (DEAD_LETTERS, // novox/hq issue 330) is for handlers that fail, not for bytes that were never an envelope. msg.term(); return; } const forThis = listeners.filter((l) => topicMatches(l.pattern, env.key)); if (forThis.length === 0) { - // The consumer's filters are the controller's, derived from what the module declared it + // The consumer's filters are the mesh's, derived from what the module declared it // consumes, and may be wider than anything it registered a handler for. Acknowledge it, or it // would be redelivered until it expired. msg.ack(); @@ -518,10 +518,10 @@ async function deliver( } catch { // Negative-acknowledge with a delay, so a handler failing on a transient cause gets another // attempt, and one failing permanently exhausts max-deliver rather than spinning: the consumer - // gives the event up, and the controller keeps it in DEAD_LETTERS until a person delivers it + // gives the event up, and the mesh keeps it in DEAD_LETTERS until a person delivers it // again or drops it (novox/hq issue 330). Delivered again, it arrives under // `mesh.again..…`, and keyFromSubject reads the same key from it. The consumer's - // limits are the controller's; this only says "not done". + // limits are the mesh's; this only says "not done". msg.nak(5_000); } } @@ -592,7 +592,7 @@ function toolSubject(key: string, self: string): string { } /** A seat's verb, as its holder serves it: flat for a mesh seat, carrying the machine for a - * node-scoped one (design 33 §4) — the same shape the controller grants. */ + * node-scoped one (design 33 §4) — the same shape the mesh grants. */ export function seatToolSubject(seat: string, verb: string, scope: string | undefined, node: string | undefined): string { const base = `mesh.seat.${seat}.tool.${verb}`; return scope === "node" && node ? `${base}.${node}` : base; diff --git a/node-tools/src/client.ts b/node-tools/src/client.ts index f9663cd..53826f6 100644 --- a/node-tools/src/client.ts +++ b/node-tools/src/client.ts @@ -28,7 +28,7 @@ const CATALOGUE_MODULES = "mesh-catalog.catalog_modules"; /** Where every role's tools are answered: the console on this machine, the Go runtime, which holds mesh-sdk's seat * definitions and reads who holds each seat from placement (novox/hq ADR 0571 clauses 4 and 5). Asked over the loopback - * channel `mesh --console` already uses, by the method `mesh/seats` beside MCP's; never the controller. */ + * channel `mesh --console` already uses, by the method `mesh/seats` beside MCP's; never another record. */ export const SEATS_METHOD = "mesh/seats"; const DEFAULT_CONSOLE = "http://127.0.0.1:4270"; diff --git a/node-tools/src/mcp.ts b/node-tools/src/mcp.ts index 0ba4497..7ee754d 100644 --- a/node-tools/src/mcp.ts +++ b/node-tools/src/mcp.ts @@ -97,7 +97,7 @@ export function mcpSurface(bus: Broker, who: string): Surface { `that serves it; what may be called was fixed when this account was issued, so a ` + `refusal means the account, not the tool. The list is what the running modules ` + `answered, plus every role's tools from the mesh's records — the mesh's own verbs ` + - `(mesh-controller.status, .push, .assign …) among them; a module that did not answer ` + + `(mesh-delivery.deliveries, mesh-placement.propose …) among them; a module that did not answer ` + `is named in the list's _meta and can still be called by ..`, }); diff --git a/node-tools/test/client.test.ts b/node-tools/test/client.test.ts index 9ff8401..6e0af27 100644 --- a/node-tools/test/client.test.ts +++ b/node-tools/test/client.test.ts @@ -40,15 +40,15 @@ async function aMeshWithTools() { await shop.handle("price", async (body: { of?: string }) => ({ of: body.of ?? "nothing", cost: 12 })); // Every role's tools, from the console on this machine (ADR 0571), the method beside MCP's; the controller's // verb answers a call of its seat and is never asked for the roles. - const controller = await connectNats({ url: url!, module: "mesh-controller" }); + const controller = await connectNats({ url: url!, module: "mesh-placement" }); let controllerToolsAsked = 0; - await controller.handle("seat:mesh-controller.tools", async () => { + await controller.handle("seat:mesh-placement.tools", async () => { controllerToolsAsked++; return { seats: [] }; }); - await controller.handle("seat:mesh-controller.status", async () => ({ output: "all quiet", ok: true })); + await controller.handle("seat:mesh-placement.status", async () => ({ output: "all quiet", ok: true })); const local = await aConsoleWithSeats([ - { seat: "mesh-controller", scope: "mesh", tools: [{ name: "status", description: "what is wrong", input: {} }] }, + { seat: "mesh-placement", scope: "mesh", tools: [{ name: "status", description: "what is wrong", input: {} }] }, { seat: "node-dns-resolver", scope: "node", tools: [{ name: "lookup", description: "one machine's", input: {} }] }, ]); return { @@ -71,12 +71,12 @@ test("a person sees what the running modules answer, sorted, and who did not ans const have = await toolsOn(person); assert.deepEqual( have.tools.map((x) => `${x.module}.${x.name}`), - ["mesh-catalog.catalog_modules", "mesh-controller.status", "node-dns-resolver.lookup", "shop.price"], + ["mesh-catalog.catalog_modules", "mesh-placement.status", "node-dns-resolver.lookup", "shop.price"], "the list is what the modules answered plus every role's tools, in a stable order", ); // A role's tool is marked as one; a node-scoped seat's carries its scope, so a caller names // the machine and the verb resolves as the seat's (design 33 §4, ADR 0170). - assert.ok(have.tools.find((x) => x.module === "mesh-controller")!.seat); + assert.ok(have.tools.find((x) => x.module === "mesh-placement")!.seat); const lookup = have.tools.find((x) => x.module === "node-dns-resolver")!; assert.ok(lookup.seat && lookup.scope === "node"); assert.equal(toolKey("node-dns-resolver.lookup", seatsIn(have)), "seat:node-dns-resolver.lookup"); @@ -146,12 +146,12 @@ test("a role's tool is reached through the seat, and a module's own name is neve const person = await connectNats({ url, module: "person.ada" }); try { const roles = seatsIn(await toolsOn(person)); - assert.equal(toolKey("mesh-controller.status", roles), "seat:mesh-controller.status"); - assert.equal(toolKey("mesh-controller.other", roles), "mesh-controller.other", "a verb the seat does not declare is a module's"); + assert.equal(toolKey("mesh-placement.status", roles), "seat:mesh-placement.status"); + assert.equal(toolKey("mesh-placement.other", roles), "mesh-placement.other", "a verb the seat does not declare is a module's"); assert.equal(toolKey("shop.price", roles), "shop.price"); - const answer = await callTool(person, "mesh-controller.status", {}, roles); + const answer = await callTool(person, "mesh-placement.status", {}, roles); assert.deepEqual(answer.result, { output: "all quiet", ok: true }); - const direct = await callTool(person, "seat:mesh-controller.status", {}); + const direct = await callTool(person, "seat:mesh-placement.status", {}); assert.deepEqual(direct.result, { output: "all quiet", ok: true }); } finally { await person.close(); diff --git a/node-tools/test/mcp.test.ts b/node-tools/test/mcp.test.ts index 7b44f13..289a089 100644 --- a/node-tools/test/mcp.test.ts +++ b/node-tools/test/mcp.test.ts @@ -30,10 +30,10 @@ async function aMeshAndACredential(t: { after: (fn: () => Promise | void) tools: [{ name: "price", description: "what something costs", input: { of: { type: "string" } } }], })); await shop.handle("price", async (body: { of?: string }) => ({ of: body.of ?? "nothing", cost: 12 })); - const controller = await connectNats({ url: url!, module: "mesh-controller" }); + const controller = await connectNats({ url: url!, module: "mesh-placement" }); // Every role's tools, from the console on this machine (ADR 0571), which the spawned `mesh` asks through MESH_CONSOLE. const local = await aConsoleWithSeats([ - { seat: "mesh-controller", scope: "mesh", tools: [ + { seat: "mesh-placement", scope: "mesh", tools: [ { name: "status", description: "what is wrong", input: {} }, { name: "push", description: "tell a machine", input: { node: { type: "string" } } }, ] }, @@ -41,8 +41,8 @@ async function aMeshAndACredential(t: { after: (fn: () => Promise | void) { seat: "node-dns-resolver", scope: "node", tools: [{ name: "lookup", description: "one machine's", input: {} }] }, ]); await controller.handle("seat:node-dns-resolver.lookup@anchor", async () => ({ machine: "anchor", answered: true })); - await controller.handle("seat:mesh-controller.status", async () => ({ output: "all quiet", ok: true })); - await controller.handle("seat:mesh-controller.push", async (body: { node?: string }) => ({ told: body.node ?? "nobody" })); + await controller.handle("seat:mesh-placement.status", async () => ({ output: "all quiet", ok: true })); + await controller.handle("seat:mesh-placement.push", async (body: { node?: string }) => ({ told: body.node ?? "nobody" })); t.after(async () => { await catalogue.close(); await shop.close(); @@ -107,7 +107,7 @@ test("a host initialises, lists the mesh's tools and calls one", async (t) => { const listed = byId.get(2)!.result.tools; assert.deepEqual(listed.map((x: { name: string }) => x.name), - ["mesh-controller.push", "mesh-controller.status", "node-dns-resolver.lookup", "shop.price"], + ["mesh-placement.push", "mesh-placement.status", "node-dns-resolver.lookup", "shop.price"], "the modules' tools and the roles', named the way a person names them"); // A node-scoped seat's verb takes the machine, and requires it (ADR 0170). const lookup = listed[2]; @@ -164,7 +164,7 @@ test("a seat's verb keeps a node of its own; only a module's tool gives it to th if (!url) return t.skip("MESH_TEST_NATS unset"); const credential = await aMeshAndACredential(t); const replies = await driving(credential, [ - { jsonrpc: "2.0", id: 1, method: "tools/call", params: { name: "mesh-controller.push", arguments: { node: "anchor" } } }, + { jsonrpc: "2.0", id: 1, method: "tools/call", params: { name: "mesh-placement.push", arguments: { node: "anchor" } } }, ]); const result = replies[0].result; assert.ok(!result.isError, JSON.stringify(replies[0])); @@ -175,7 +175,7 @@ test("a host calls the mesh's own verb through the seat", async (t) => { if (!url) return t.skip("MESH_TEST_NATS unset"); const credential = await aMeshAndACredential(t); const replies = await driving(credential, [ - { jsonrpc: "2.0", id: 1, method: "tools/call", params: { name: "mesh-controller.status", arguments: {} } }, + { jsonrpc: "2.0", id: 1, method: "tools/call", params: { name: "mesh-placement.status", arguments: {} } }, ]); const result = replies[0].result; assert.ok(!result.isError, JSON.stringify(replies[0])); diff --git a/node-tools/test/patient-connect.test.ts b/node-tools/test/patient-connect.test.ts index 81f4c5d..0f2b162 100644 --- a/node-tools/test/patient-connect.test.ts +++ b/node-tools/test/patient-connect.test.ts @@ -69,12 +69,12 @@ test("preparing with nothing named fails rather than passing quietly", async () // therefore pure and tested as such: what a message is for is decided by the patterns registered, not // by which reader happened to fetch it. test("a message is for every pattern that matches it, and nothing else", () => { - const registered = ["mesh-build-machine.built", "mesh-controller.built-before"]; + const registered = ["mesh-build-machine.built", "mesh-placement.built-before"]; const matched = (key: string) => registered.filter((p) => topicMatches(p, key)); assert.deepEqual(matched("mesh-build-machine.built"), ["mesh-build-machine.built"]); - assert.deepEqual(matched("mesh-controller.built-before"), ["mesh-controller.built-before"]); + assert.deepEqual(matched("mesh-placement.built-before"), ["mesh-placement.built-before"]); // Nothing registered for it: the consumer's filter is the controller's and may be wider. assert.deepEqual(matched("mesh-catalog.upgraded"), []); // And a handler that asked for everything gets both, which is what the audit logger does. - assert.deepEqual(["#"].filter((p) => topicMatches(p, "mesh-controller.built-before")), ["#"]); + assert.deepEqual(["#"].filter((p) => topicMatches(p, "mesh-placement.built-before")), ["#"]); }); diff --git a/node-tools/test/roundtrip.mjs b/node-tools/test/roundtrip.mjs index 702fef8..24ad971 100644 --- a/node-tools/test/roundtrip.mjs +++ b/node-tools/test/roundtrip.mjs @@ -5,7 +5,7 @@ import { connectNats } from "../dist/broker-nats.js"; const URL = "nats://127.0.0.1:14222"; -// The controller's job, done by hand here: the stream and the module's durable consumer. +// The mesh's job, done by hand here: the stream and the module's durable consumer. const admin = await connect({ servers: URL }); const jsm = await admin.jetstreamManager(); await jsm.streams.add({ name: "EVENTS", subjects: ["mesh.mod.*.event.>", "mesh.seat.*.event.>"] }); From c6785118714a4dfa0e0f1c49a7b65c1dc1ed6f6f Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 2 Nov 2026 11:56:55 +0100 Subject: [PATCH 2/4] Pin mesh-sdk to the group head of issue 1126 Every member builds against mesh-sdk 729a86b, the head of feat/1126-removal, so the group is checked as one contract. Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL --- node-tools/go.mod | 2 +- node-tools/go.sum | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/node-tools/go.mod b/node-tools/go.mod index d0e4637..bbd7634 100644 --- a/node-tools/go.mod +++ b/node-tools/go.mod @@ -8,7 +8,7 @@ require ( ) require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101235037-c21d8fd9692c + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102104825-729a86b62343 github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op // indirect github.com/google/go-tpm v0.9.8 // indirect github.com/klauspost/compress v1.20.0 // indirect diff --git a/node-tools/go.sum b/node-tools/go.sum index 60e9ab1..ea0f85a 100644 --- a/node-tools/go.sum +++ b/node-tools/go.sum @@ -1,7 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101085317-790d1c82d027 h1:a33dsavoaqllh40eVICbhxi6Kero1EGSjt1V+2fa2Hk= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101085317-790d1c82d027/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101235037-c21d8fd9692c h1:/YCxh+NYfqry3PxZYY1nQXaccupobxpsFt8c9BYuGBg= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101235037-c21d8fd9692c/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102104825-729a86b62343 h1:+n2kwWJoSP30Jp/0dNaNqHt7A0dsfy2YWxep2inVueo= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102104825-729a86b62343/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= From e6f27a5f74de3b4c04bbcc86bc644f90d2d33e28 Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 2 Nov 2026 12:20:14 +0100 Subject: [PATCH 3/4] Pin mesh-sdk to the group head of issue 1126 Every member builds against mesh-sdk f569924, the head of feat/1126-removal, so the group is checked as one contract. Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL --- node-tools/go.mod | 2 +- node-tools/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/node-tools/go.mod b/node-tools/go.mod index bbd7634..a74c35e 100644 --- a/node-tools/go.mod +++ b/node-tools/go.mod @@ -8,7 +8,7 @@ require ( ) require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102104825-729a86b62343 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102111821-f56992403794 github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op // indirect github.com/google/go-tpm v0.9.8 // indirect github.com/klauspost/compress v1.20.0 // indirect diff --git a/node-tools/go.sum b/node-tools/go.sum index ea0f85a..d37e598 100644 --- a/node-tools/go.sum +++ b/node-tools/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102104825-729a86b62343 h1:+n2kwWJoSP30Jp/0dNaNqHt7A0dsfy2YWxep2inVueo= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102104825-729a86b62343/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102111821-f56992403794 h1:/yChE1zgvQsglpBEIl0j550cr8b4m6DfYjddWHjlQK8= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102111821-f56992403794/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= From e475d9f849ca30a8201715b5dae03dad1cfd726b Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 2 Nov 2026 12:27:34 +0100 Subject: [PATCH 4/4] Pin mesh-sdk to the group head of issue 1126 Every member builds against mesh-sdk abf5eac, the head of feat/1126-removal, so the group is checked as one contract. Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL --- node-tools/go.mod | 2 +- node-tools/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/node-tools/go.mod b/node-tools/go.mod index a74c35e..717ea86 100644 --- a/node-tools/go.mod +++ b/node-tools/go.mod @@ -8,7 +8,7 @@ require ( ) require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102111821-f56992403794 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791 github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op // indirect github.com/google/go-tpm v0.9.8 // indirect github.com/klauspost/compress v1.20.0 // indirect diff --git a/node-tools/go.sum b/node-tools/go.sum index d37e598..ebd4e0f 100644 --- a/node-tools/go.sum +++ b/node-tools/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102111821-f56992403794 h1:/yChE1zgvQsglpBEIl0j550cr8b4m6DfYjddWHjlQK8= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102111821-f56992403794/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791 h1:7SYIDPMxbPBr8dMB6k9fYsgVt4ZyWVL41q4LMFsfAvs= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102112556-abf5eace7791/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=