The node-engine serves its seat's verbs from the bootstrapped definition until it holds the seat list (issue 1966, recovery)
mesh/delivery merged: source.main moved to novox/mesh-host@87bdc95a3d65
mesh/check pass 7b42c94a4d78 on 87bdc95a3d65 | unit 1679 run, 333 kept, 31 skipped | integration 1692 run, 333 kept, 26 skipped | over bound: unit (sl…

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
This commit is contained in:
2026-11-08 18:34:20 +01:00
parent 6dd8e08a84
commit 7b42c94a4d
2 changed files with 43 additions and 1 deletions
+18 -1
View File
@@ -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)
+25
View File
@@ -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)
}
}
}
}