From 7b42c94a4d78e1774f30459703a0fd855b41051a Mon Sep 17 00:00:00 2001 From: jochen Date: Sun, 8 Nov 2026 18:34:20 +0100 Subject: [PATCH] The node-engine serves its seat's verbs from the bootstrapped definition until it holds the seat list (issue 1966, recovery) The link serves the seat node-engine when it opens, before the node agent starts the reader of the seat list; since ADR 0635 catalogue holds no seat until then, so the engine registered no verb at all. Nobody answered manifest-reads, compose, composition or the hand acts on novox, shanks and now ace, and the assigner kept ace's old statement, holding its own successor on seats the new engine states. Quick and thorough fix are one here: the only seat-list read at start is this one (the composer waits for the list, the reads statement restates). Claude-Session: https://claude.ai/code/session_01NcbgfFfQ4Z9UE5jK32PmE5 --- internal/link/engineseat.go | 19 ++++++++++++++++++- internal/link/seats_for_tests_test.go | 25 +++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/internal/link/engineseat.go b/internal/link/engineseat.go index ad7ca02..43605eb 100644 --- a/internal/link/engineseat.go +++ b/internal/link/engineseat.go @@ -13,6 +13,7 @@ import ( oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel" stdio "git.novox.be/novox/mesh-sdk/go" "git.novox.be/novox/mesh-sdk/go/catalogue" + "git.novox.be/novox/mesh-sdk/go/seatlist" "github.com/nats-io/nats.go" "github.com/nats-io/nats.go/micro" ) @@ -99,6 +100,22 @@ func AnswerEngineCall(handle EngineFunc, verb, node, subject string, body []byte return say() } +// ServedEngineSeat is the engine's seat as it serves it: the seat list's definition once the engine holds it, and the +// bootstrapped definition before (novox/hq ADR 0636 §2: every reader holds the bootstrapped seats from its start). The +// link serves the seat when it opens, before the node agent starts the reader of the list; read from the list alone, the +// engine served no verb, so nobody answered manifest-reads and the assigner kept an engine's old statement (issue 1966). +func ServedEngineSeat() catalogue.Seat { + if s, ok := catalogue.SeatNamed(EngineSeat); ok && len(s.Serves) > 0 { + return s + } + for _, s := range catalogue.SeatsFromDefinitions(seatlist.Bootstrapped()) { + if s.Name == EngineSeat { + return s + } + } + return catalogue.Seat{Name: EngineSeat} +} + // EngineSeats is a link that serves this node's seat. Optional, as Asked is. type EngineSeats interface { ServeEngineSeat(EngineFunc) @@ -118,7 +135,7 @@ func (l *natsLink) ServeEngineSeat(handle EngineFunc) { return } apart := make(chan struct{}, apartAtOnce) - for _, v := range catalogue.EngineVerbs(EngineSeat) { + for _, v := range ServedEngineSeat().Serves { verb := v.Name answer := EngineAnswer(handle, verb, l.node, apart) meta := EngineEndpointMetadata(v, l.node) diff --git a/internal/link/seats_for_tests_test.go b/internal/link/seats_for_tests_test.go index 7994cc2..0b8d81b 100644 --- a/internal/link/seats_for_tests_test.go +++ b/internal/link/seats_for_tests_test.go @@ -1,6 +1,9 @@ package link import ( + "slices" + "testing" + "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/seatlist/seatlisttest" ) @@ -8,3 +11,25 @@ import ( // The seats the tests judge against are given to catalogue as the node-engine gives the ones it read from the bus // (novox/hq ADR 0635 §1); catalogue compiles none. func init() { catalogue.UseSeats(catalogue.SeatsFromDefinitions(seatlisttest.Definitions())) } + +// Before the engine holds a seat list it serves its seat's verbs from the bootstrapped definition (issue 1966: served from +// the empty list, it answered no verb and the assigner kept an old statement). +func TestServedEngineSeatBeforeTheList(t *testing.T) { + held := catalogue.Seats() + defer catalogue.UseSeats(held) + for _, tc := range []struct { + name string + seats []catalogue.Seat + }{{"no list held", nil}, {"list held", held}} { + catalogue.UseSeats(tc.seats) + var names []string + for _, v := range ServedEngineSeat().Serves { + names = append(names, v.Name) + } + for _, want := range []string{ActManifestReads, ActComposition, ActHandOver} { + if !slices.Contains(names, want) { + t.Errorf("%s: the engine serves %v, without %s", tc.name, names, want) + } + } + } +} -- 2.54.0