Merge b6/registry into the registry milestone's join: the console reads through the assigner's client, with the seat name mesh-module-assigner (issue 1966)

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
This commit is contained in:
2026-11-05 15:39:17 +01:00
12 changed files with 134 additions and 71 deletions
+40 -22
View File
@@ -17,8 +17,10 @@ package console
// session — so a tool that arrived a minute ago is found without the client reconnecting.
import (
assigner "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
meshcatalogclient "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery"
"context"
"encoding/json"
@@ -30,6 +32,7 @@ import (
"github.com/novox/mesh-tools/node-tools/internal/announce"
"github.com/novox/mesh-tools/node-tools/internal/bus"
"github.com/novox/mesh-tools/node-tools/internal/firstview"
)
// IndexKept is how long what the mesh answered is kept before it is asked again: long enough that
@@ -149,7 +152,7 @@ type index struct {
// called missing while it is here (2026-10-05).
Unheard []unheard
// Placed is the state seat-holders as the seat mesh-module-assigner's holder wrote it (novox/hq ADR 0571 clause 4), by its
// keys `<seat>` and `<seat>.<node>`, read with delivery.HoldersOf; PlacedWhy says why it could not be read.
// keys `<seat>` and `<seat>.<node>`, read with assigner.HoldersOf; PlacedWhy says why it could not be read.
Placed map[string][]byte
PlacedWhy string
// Replaces is what each verb and tool replaces (novox/hq ADR 0245):
@@ -337,7 +340,7 @@ func indexOn(conn *bus.Conn, src Source, held announce.Known, before func(announ
}
l.NotAnswering = append(l.NotAnswering, recordsWhy...)
if x.PlacedWhy != "" {
l.NotAnswering = append(l.NotAnswering, delivery.SeatHoldersRead+" (who holds which seat by placement could not be read: "+
l.NotAnswering = append(l.NotAnswering, assigner.SeatHoldersRead+" (who holds which seat by placement could not be read: "+
x.PlacedWhy+")")
}
for _, u := range x.Unheard {
@@ -357,7 +360,7 @@ func indexOn(conn *bus.Conn, src Source, held announce.Known, before func(announ
}
// Source is where the console reads the four states of the indexer and the assigner it indexes the mesh by: the assigner's
// placement and seat-holders, the indexer's modules and manifests, by `<owner>.<state>` (delivery.PlacementRead and the
// placement and seat-holders, the indexer's modules and manifests, by `<owner>.<state>` (assigner.PlacementRead and the
// rest). The tool runner's is its first views (firstview.Views), never the owners' state (novox/hq ADR 0615 §1).
type Source interface {
// State is every current value of a state by key; an error says why it cannot be told yet.
@@ -366,16 +369,16 @@ type Source interface {
Entry(name, key string) ([]byte, error)
}
// seatHoldersOn reads the state seat-holders whole (novox/hq ADR 0571 clause 4), as delivery.SeatHoldersRead.
// seatHoldersOn reads the state seat-holders whole (novox/hq ADR 0571 clause 4), as assigner.SeatHoldersRead.
func seatHoldersOn(src Source) (map[string][]byte, error) {
return src.State(delivery.SeatHoldersRead)
return src.State(assigner.SeatHoldersRead)
}
// The indexer's states the console reads (novox/hq ADR 0571 clauses 4 and 5): every module, and each manifest by its
// digest, by the names a reader's manifest declares.
var (
modulesRead = delivery.IndexerModule + ".modules"
manifestsRead = delivery.IndexerModule + ".manifests"
modulesRead = firstview.ModulesRead
manifestsRead = firstview.ManifestsRead
)
// manifestFacts is what the console takes of a manifest: the tools it declares and what each replaces. A manifest's
@@ -388,20 +391,20 @@ type manifestFacts struct {
var manifestsKept sync.Map // digest → manifestFacts
// recordsOn is every module the indexer holds, with where the placement seat places it and whether it declares tools,
// and every machine placement names (delivery.ModuleNames, delivery.MachineNames). What cannot be read is said, one line
// and every machine placement names (moduleNames, assigner.MachineNames). What cannot be read is said, one line
// per state, for the listing's not answering.
func recordsOn(src Source) (recorded []recordedModule, machines []string, why []string) {
placements, err := src.State(delivery.PlacementRead)
placements, err := src.State(assigner.PlacementRead)
if err != nil {
why = append(why, delivery.PlacementRead+" (where modules are placed could not be read, so what is missing cannot "+
why = append(why, assigner.PlacementRead+" (where modules are placed could not be read, so what is missing cannot "+
"be said: "+err.Error()+")")
}
machines = delivery.MachineNames(placements)
machines = assigner.MachineNames(placements)
on := map[string][]string{}
// The placement's keys in either form, a node key and its module keys or the form before (research 072 row 37).
assembled, err := delivery.PlacementsOf(placements)
assembled, err := assigner.PlacementsOf(placements)
if err != nil {
why = append(why, delivery.PlacementRead+" (not every key is a placement: "+err.Error()+")")
why = append(why, assigner.PlacementRead+" (not every key is a placement: "+err.Error()+")")
}
for _, node := range machines {
for _, m := range assembled[node].Modules {
@@ -412,13 +415,13 @@ func recordsOn(src Source) (recorded []recordedModule, machines []string, why []
if err != nil {
return nil, machines, append(why, modulesRead+" (which modules there are could not be read: "+err.Error()+")")
}
names, err := delivery.ModuleNames(modules)
names, err := moduleNames(modules)
if err != nil {
return nil, machines, append(why, modulesRead+" ("+err.Error()+")")
}
for _, name := range names {
r := recordedModule{Module: name, On: on[name]}
if m, err := delivery.DecodeModule(modules[name]); err == nil && m.Manifest != "" {
if m, err := meshcatalogclient.DecodeModule(modules[name]); err == nil && m.Manifest != "" {
facts, err := manifestOf(src, m.Manifest)
if err != nil {
why = append(why, name+" (its manifest could not be read, so whether it answers tools is not known: "+err.Error()+")")
@@ -431,6 +434,21 @@ func recordsOn(src Source) (recorded []recordedModule, machines []string, why []
return recorded, machines, why
}
// moduleNames is every module the indexer holds, sorted, from its state `modules` as a reader holds its keys (key to
// value; a deleted key absent). A key that is not a module's is refused, naming it.
func moduleNames(keys map[string][]byte) ([]string, error) {
out := make([]string, 0, len(keys))
for k := range keys {
module, err := meshcatalogclient.ParseModulesKey(k)
if err != nil {
return nil, err
}
out = append(out, module)
}
sort.Strings(out)
return out, nil
}
// manifestOf is what the console takes of the manifest of a digest, read from the indexer's manifests once.
func manifestOf(src Source, digest string) (manifestFacts, error) {
if kept, ok := manifestsKept.Load(digest); ok {
@@ -443,12 +461,12 @@ func manifestOf(src Source, digest string) (manifestFacts, error) {
if entry == nil {
return manifestFacts{}, fmt.Errorf("the indexer holds no manifest %s", digest)
}
m, err := delivery.DecodeManifest(entry)
m, err := meshcatalogclient.DecodeManifest(entry)
if err != nil {
return manifestFacts{}, err
}
var facts manifestFacts
if err := json.Unmarshal([]byte(m.JSON), &facts); err != nil {
if err := json.Unmarshal([]byte(m.Manifest), &facts); err != nil {
return manifestFacts{}, err
}
manifestsKept.Store(digest, facts)
@@ -456,19 +474,19 @@ func manifestOf(src Source, digest string) (manifestFacts, error) {
}
// placedHolders is who holds seat (on node, for a seat held per machine) by placement, or none.
func (x *index) placedHolders(seat, node string) []delivery.SeatHolder {
func (x *index) placedHolders(seat, node string) []meshmoduleassigner.SeatHoldersHoldersItem {
if len(x.Placed) == 0 {
return nil
}
hs, err := delivery.HoldersOf(x.Placed, seat, node)
hs, err := assigner.HoldersOf(x.Placed, seat, node)
if err != nil || len(hs) > 0 || node != "" {
return hs
}
// A seat held per machine, asked without one: every node's key of it.
var all []delivery.SeatHolder
var all []meshmoduleassigner.SeatHoldersHoldersItem
for key := range x.Placed {
if s, n, ok := strings.Cut(key, "."); ok && s == seat {
if more, err := delivery.HoldersOf(x.Placed, seat, n); err == nil {
if more, err := assigner.HoldersOf(x.Placed, seat, n); err == nil {
all = append(all, more...)
}
}
+2 -2
View File
@@ -6,8 +6,8 @@ package console
import (
"context"
"encoding/json"
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery"
"os"
"strings"
"sync"
@@ -78,7 +78,7 @@ func (s *Surface) seatsAnswer() map[string]any {
for _, st := range catalogue.Seats() {
holders := x.placedHolders(st.Name, "")
if holders == nil {
holders = []delivery.SeatHolder{}
holders = []meshmoduleassigner.SeatHoldersHoldersItem{}
}
tools := st.Serves
if tools == nil {
+11 -8
View File
@@ -8,7 +8,8 @@ import (
"testing"
"time"
"git.novox.be/novox/mesh-sdk/go/delivery"
assigner "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
seatplacement "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
"github.com/nats-io/nats.go/micro"
"github.com/novox/mesh-tools/node-tools/internal/announce"
@@ -61,13 +62,15 @@ func TestARuntimeThatAnswersLateIsFoundAndOneThatNeverDoesIsNamed(t *testing.T)
map[string][]string{"laptop": {"slack", "nextcloud-client"}, "sleeper": {"nap", "nextcloud-client"},
"desktop": {"nextcloud-client"}, "bench": nil})
// Who holds which seat, by placement (ADR 0571 clause 4), as the seat mesh-module-assigner's holder composes it.
placedKV := mesh.Bucket(t, delivery.SeatHoldersBucket)
for key, sh := range delivery.SeatHoldersOf(map[string]delivery.Placement{"bench": {Node: "bench",
Holds: []delivery.Holding{{Claim: "mesh-wants", Scope: "mesh", Module: "mesh-delivery-assigner"}}}}) {
raw, _ := json.Marshal(sh)
if _, err := placedKV.Put(key, raw); err != nil {
t.Fatal(err)
}
placedKV := mesh.Bucket(t, assigner.SeatHoldersBucket)
holdersKey, err := assigner.SeatHoldersKey("mesh-wants", "")
if err != nil {
t.Fatal(err)
}
raw, _ := json.Marshal(seatplacement.SeatHolders{V: assigner.Version, Seat: "mesh-wants", Scope: "mesh",
Holders: []seatplacement.SeatHoldersHoldersItem{{Module: "mesh-delivery-assigner", Node: "bench"}}})
if _, err := placedKV.Put(holdersKey, raw); err != nil {
t.Fatal(err)
}
consoleMembership := mt.MembershipOf("node-tools", "laptop-console", false, nil)
consoleMembership.State = mt.ConsoleReads()