From b7159d71a67bddb5a351e29796cc03aa6a12e3cc Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 28 Oct 2026 22:15:24 +0100 Subject: [PATCH 1/6] Node agent: say what it holds today in its answer, so a seed that holds the same is the same; pin mesh-sdk b03bf4c (issue 1126) --- go.mod | 2 +- go.sum | 6 ++-- internal/selfcompose/selfcompose.go | 15 ++++++++++ internal/selfcompose/selfcompose_test.go | 15 ++++++++-- internal/selfcompose/shadow.go | 37 ++++++++++++++++++++++-- 5 files changed, 64 insertions(+), 11 deletions(-) diff --git a/go.mod b/go.mod index cb56e7d..b40b9cf 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/novox/mesh-host go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028174713-38dbe1d3a388 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028211337-b03bf4c118d9 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 golang.org/x/crypto v0.57.0 diff --git a/go.sum b/go.sum index 8a76d42..26aa362 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028162235-0de30064156a h1:BYJrXqPfeNSF0eypX3UnczLq/geXNkfJbiUK9JHlHNA= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028162235-0de30064156a/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028174713-38dbe1d3a388 h1:ycu/sNieTdLpD3bzhZOCXdzcoqXUOxPRmqWV019REpo= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028174713-38dbe1d3a388/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028211337-b03bf4c118d9 h1:Zy31cb1lVyFn1j1T7qT8cXJgnO3pyEr5SKCFERVM1CE= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028211337-b03bf4c118d9/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= diff --git a/internal/selfcompose/selfcompose.go b/internal/selfcompose/selfcompose.go index f3167c7..fb14265 100644 --- a/internal/selfcompose/selfcompose.go +++ b/internal/selfcompose/selfcompose.go @@ -17,6 +17,7 @@ import ( "encoding/json" "errors" "fmt" + "slices" "sort" "strings" "sync" @@ -100,6 +101,9 @@ type Composer struct { sealedOwn map[string]ownSealed said string refusal string + // held is what the composer's last pass held: each module refused alone, and the node's own ("node") when it could + // compose none (heldNow). + held []string // doing is what the composer waits on now (waiting), for a running answer to say (issue 1126, T2); its own lock, so // it is read while c.mu is held. doingMu sync.Mutex @@ -621,12 +625,23 @@ func (c *Composer) refused(why string, held map[string]string) { c.mu.Lock() same := c.refusal == said c.refusal = said + c.held = heldModules(held) + if why != "" { + c.held = append(c.held, delivery.HaveNodeWord) + } c.mu.Unlock() if !same && c.env.Refused != nil { c.env.Refused(why, held) } } +// heldNow is what the composer's last pass held, sorted. +func (c *Composer) heldNow() []string { + c.mu.Lock() + defer c.mu.Unlock() + return slices.Clone(c.held) +} + // keepRunning is comp with each sealed value the node already runs at the same slot kept, where it opens to the same // value (delivery.KeepSeals, issue 1126, C1): of what runs (running, the agent's), else of what its module's have says // it runs (Held). A have written before slots names no slot: its boxes are reused within their own module alone, at a diff --git a/internal/selfcompose/selfcompose_test.go b/internal/selfcompose/selfcompose_test.go index f588f0c..155efc8 100644 --- a/internal/selfcompose/selfcompose_test.go +++ b/internal/selfcompose/selfcompose_test.go @@ -1401,12 +1401,16 @@ func TestAHaveSaysWhetherItsModuleRunsInsideTheNodesOwn(t *testing.T) { name string have delivery.Have in bool + // heldToday says the shadow counts the module held today (issue 1126: a seed holding it too holds it as today). + heldToday bool }{ {"failed, naming nothing it runs", delivery.Have{Phase: delivery.PhaseFailed, Health: delivery.HealthFailed, - Detail: "the node cannot compose it, and keeps running what it ran: no build"}, false}, + Detail: "the node cannot compose it, and keeps running what it ran: no build"}, false, false}, {"failed at a build fingerprint", delivery.Have{FP: "build-0123abcd", Composed: "sha256:" + strings.Repeat("c", 64), Phase: delivery.PhaseFailed, - Health: delivery.HealthFailed, Detail: "the node cannot compose it, and keeps running what it ran: no build"}, true}, - {"running with no fingerprint (mesh-wireguard)", delivery.Have{Phase: delivery.PhaseRunning, Health: delivery.HealthOK}, true}, + Health: delivery.HealthFailed, Detail: "the node cannot compose it, and keeps running what it ran: no build"}, true, false}, + {"running with no fingerprint (mesh-wireguard)", delivery.Have{Phase: delivery.PhaseRunning, Health: delivery.HealthOK}, true, false}, + {"held today", delivery.Have{FP: "build-0123abcd", Composed: "sha256:" + strings.Repeat("c", 64), Phase: delivery.PhaseHeld, + Health: delivery.HealthWaits, Detail: "held: a required config value is unset"}, true, true}, } { t.Run(row.name, func(t *testing.T) { c := New(Env{Node: "n1"}) @@ -1422,6 +1426,11 @@ func TestAHaveSaysWhetherItsModuleRunsInsideTheNodesOwn(t *testing.T) { if c.inOwn["web"] != row.in { t.Errorf("web counted inside the node's own composition: %v, want %v", c.inOwn["web"], row.in) } + s := &Shadow{c: c, ran: map[string]string{}} + s.takeRan(agent.Update{Key: key, Value: value}) + if got := slices.Contains(s.heldToday(), "web"); got != row.heldToday { + t.Errorf("web held today: %v, want %v", got, row.heldToday) + } }) } } diff --git a/internal/selfcompose/shadow.go b/internal/selfcompose/shadow.go index 820cd0d..bdc7422 100644 --- a/internal/selfcompose/shadow.go +++ b/internal/selfcompose/shadow.go @@ -40,6 +40,9 @@ type Shadow struct { // ran is, by module, the digest the module's have says runs: what runs, where the agent's list holds only the copy // from the declaration kept before the switch (issue 1126, probe #532 on shanks). ran map[string]string + // heldNow is, by module, whether its have says the node holds it today (phase held): a candidate holding it too holds + // it as today (frombus.Scope.HeldToday, issue 1126). + heldNow map[string]bool // answered is, by proposal, the candidate revisions and the input changes (moved) each answer was composed at: a proposal // is answered again only when either moved (issue 1126, 2026-10-28: shanks answered probe/1126-seed-7 at revision 84 // every 13-17 s, woken by its own modules' haves whose health words changed at every look). @@ -614,7 +617,7 @@ func (s *Shadow) answerProposal(ctx context.Context, proposal string) (delivery. c.wants = held c.mu.Unlock() a := frombus.Answer(ctx, reader{c: c}, c.env.Mesh, frombus.Scope{Node: node, KeptAt: kept, Placements: given, InOwn: inOwn, - SameValue: c.sameValue}, fps, running, proposal, rev) + SameValue: c.sameValue, HeldToday: s.heldToday()}, fps, running, proposal, rev) s.mu.Lock() ran := make(map[string]string, len(s.ran)) for m, d := range s.ran { @@ -624,6 +627,28 @@ func (s *Shadow) answerProposal(ctx context.Context, proposal string) (delivery. return asHavesSay(a, ran), true } +// heldToday is what the node holds today: each module whose have's phase is held, and what its own composer holds, the +// node's own composition ("node") among them when it composes none of its own. +func (s *Shadow) heldToday() []string { + set := map[string]bool{} + s.mu.Lock() + for m, held := range s.heldNow { + if held { + set[m] = true + } + } + s.mu.Unlock() + for _, m := range s.c.heldNow() { + set[m] = true + } + out := make([]string, 0, len(set)) + for m := range set { + out = append(out, m) + } + sort.Strings(out) + return out +} + // takeRan keeps the digest one of this node's module haves says runs, and says whether it changed: a have whose health // or words alone moved runs the same. func (s *Shadow) takeRan(u agent.Update) bool { @@ -635,12 +660,18 @@ func (s *Shadow) takeRan(u agent.Update) bool { defer s.mu.Unlock() before, had := s.ran[module] h, err := delivery.DecodeHave(u.Value) + if s.heldNow == nil { + s.heldNow = map[string]bool{} + } + wasHeld := s.heldNow[module] + s.heldNow[module] = !u.Deleted && err == nil && h.Phase == delivery.PhaseHeld + holdMoved := wasHeld != s.heldNow[module] // a hold's start or end changes the answer too if u.Deleted || err != nil || h.Composed == "" || h.Phase == delivery.PhaseRemoved { delete(s.ran, module) - return had + return had || holdMoved } s.ran[module] = h.Composed - return !had || before != h.Composed + return !had || before != h.Composed || holdMoved } // asHavesSay is a with each module that differs only from the copy the agent keeps of the declaration before the switch From 9914459830d72e16d7d54c17bebadd238ef39b98 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 28 Oct 2026 22:18:26 +0100 Subject: [PATCH 2/6] Pin mesh-sdk 06b13ec, the group's head (issue 1126) --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index b40b9cf..8d0c72d 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/novox/mesh-host go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028211337-b03bf4c118d9 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028211757-06b13ec1cc07 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 golang.org/x/crypto v0.57.0 diff --git a/go.sum b/go.sum index 26aa362..95a0a47 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028211337-b03bf4c118d9 h1:Zy31cb1lVyFn1j1T7qT8cXJgnO3pyEr5SKCFERVM1CE= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028211337-b03bf4c118d9/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028211757-06b13ec1cc07 h1:TFcgfKLUNH7LY5osqdxEdp2VC4bT2Q/TeC913/JkrsU= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028211757-06b13ec1cc07/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 a143678fd898a885b10a5413db81f2e93f8e81ba Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 28 Oct 2026 22:41:14 +0100 Subject: [PATCH 3/6] Pin mesh-sdk 77b48a4, the group's head (issue 1126) --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 8d0c72d..21d0bd6 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/novox/mesh-host go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028211757-06b13ec1cc07 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028212605-77b48a4f1aff github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 golang.org/x/crypto v0.57.0 diff --git a/go.sum b/go.sum index 95a0a47..3c3267b 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028211757-06b13ec1cc07 h1:TFcgfKLUNH7LY5osqdxEdp2VC4bT2Q/TeC913/JkrsU= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028211757-06b13ec1cc07/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028212605-77b48a4f1aff h1:EHuM+ed+/M9woMZsMou6/2xm6hIdb1YCFINprrP54Ys= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028212605-77b48a4f1aff/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 91686fdb81a25d7c769afa641f7b919e8d6d690b Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 28 Oct 2026 23:45:28 +0100 Subject: [PATCH 4/6] Node agent: count a module running a build of the old form as held today, so the seed keeps it as the controller does (issue 1126) --- internal/selfcompose/selfcompose_test.go | 6 +++++- internal/selfcompose/shadow.go | 9 +++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/internal/selfcompose/selfcompose_test.go b/internal/selfcompose/selfcompose_test.go index 155efc8..09db36f 100644 --- a/internal/selfcompose/selfcompose_test.go +++ b/internal/selfcompose/selfcompose_test.go @@ -1407,7 +1407,11 @@ func TestAHaveSaysWhetherItsModuleRunsInsideTheNodesOwn(t *testing.T) { {"failed, naming nothing it runs", delivery.Have{Phase: delivery.PhaseFailed, Health: delivery.HealthFailed, Detail: "the node cannot compose it, and keeps running what it ran: no build"}, false, false}, {"failed at a build fingerprint", delivery.Have{FP: "build-0123abcd", Composed: "sha256:" + strings.Repeat("c", 64), Phase: delivery.PhaseFailed, - Health: delivery.HealthFailed, Detail: "the node cannot compose it, and keeps running what it ran: no build"}, true, false}, + Health: delivery.HealthFailed, Detail: "the node cannot compose it, and keeps running what it ran: no build"}, true, true}, + {"running a build of the old form", delivery.Have{FP: "build-1790368817085312877", Composed: "sha256:" + strings.Repeat("c", 64), + Phase: delivery.PhaseRunning, Health: delivery.HealthOK}, true, true}, + {"running a build of the new form", delivery.Have{FP: delivery.SourcePrefix2 + strings.Repeat("a", 64), Composed: "sha256:" + strings.Repeat("c", 64), + Phase: delivery.PhaseRunning, Health: delivery.HealthOK}, false, false}, {"running with no fingerprint (mesh-wireguard)", delivery.Have{Phase: delivery.PhaseRunning, Health: delivery.HealthOK}, true, false}, {"held today", delivery.Have{FP: "build-0123abcd", Composed: "sha256:" + strings.Repeat("c", 64), Phase: delivery.PhaseHeld, Health: delivery.HealthWaits, Detail: "held: a required config value is unset"}, true, true}, diff --git a/internal/selfcompose/shadow.go b/internal/selfcompose/shadow.go index bdc7422..91bf349 100644 --- a/internal/selfcompose/shadow.go +++ b/internal/selfcompose/shadow.go @@ -40,7 +40,8 @@ type Shadow struct { // ran is, by module, the digest the module's have says runs: what runs, where the agent's list holds only the copy // from the declaration kept before the switch (issue 1126, probe #532 on shanks). ran map[string]string - // heldNow is, by module, whether its have says the node holds it today (phase held): a candidate holding it too holds + // heldNow is, by module, whether its have says the node holds it today (phase held, or a build of the old form it runs + // as the controller keeps it): a candidate holding it too holds // it as today (frombus.Scope.HeldToday, issue 1126). heldNow map[string]bool // answered is, by proposal, the candidate revisions and the input changes (moved) each answer was composed at: a proposal @@ -664,7 +665,11 @@ func (s *Shadow) takeRan(u agent.Update) bool { s.heldNow = map[string]bool{} } wasHeld := s.heldNow[module] - s.heldNow[module] = !u.Deleted && err == nil && h.Phase == delivery.PhaseHeld + // Held today: its have's phase is held, or it runs a build of the old form (not src2:), which no registered build of + // the trunk names, so the controller keeps it as it runs (issue 1126: novox's de-spiegel at build-…, its newer build + // not registered); a seed keeps it so too. + s.heldNow[module] = !u.Deleted && err == nil && h.Phase != delivery.PhaseRemoved && + (h.Phase == delivery.PhaseHeld || (h.FP != "" && !strings.HasPrefix(h.FP, delivery.SourcePrefix2))) holdMoved := wasHeld != s.heldNow[module] // a hold's start or end changes the answer too if u.Deleted || err != nil || h.Composed == "" || h.Phase == delivery.PhaseRemoved { delete(s.ran, module) From abdf85aa4f5681fb3a211cd28f7dc15d3b35a680 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 28 Oct 2026 23:50:19 +0100 Subject: [PATCH 5/6] Node agent: say how it keeps each module today, held, failed or at a build of the old form named by its have's build; pin mesh-sdk f609be7 (issue 1126) --- go.mod | 2 +- go.sum | 4 +- internal/selfcompose/selfcompose_test.go | 8 ++-- internal/selfcompose/shadow.go | 48 +++++++++++++----------- 4 files changed, 34 insertions(+), 28 deletions(-) diff --git a/go.mod b/go.mod index 21d0bd6..53af774 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/novox/mesh-host go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028212605-77b48a4f1aff + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028224922-f609be786463 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 golang.org/x/crypto v0.57.0 diff --git a/go.sum b/go.sum index 3c3267b..f5a6d2c 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028212605-77b48a4f1aff h1:EHuM+ed+/M9woMZsMou6/2xm6hIdb1YCFINprrP54Ys= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028212605-77b48a4f1aff/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028224922-f609be786463 h1:Q0XohSVvVtUpamUj+H7Bgha82RDy/EKp/zzhHJGA74A= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028224922-f609be786463/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= diff --git a/internal/selfcompose/selfcompose_test.go b/internal/selfcompose/selfcompose_test.go index 09db36f..06d1ce8 100644 --- a/internal/selfcompose/selfcompose_test.go +++ b/internal/selfcompose/selfcompose_test.go @@ -1405,11 +1405,11 @@ func TestAHaveSaysWhetherItsModuleRunsInsideTheNodesOwn(t *testing.T) { heldToday bool }{ {"failed, naming nothing it runs", delivery.Have{Phase: delivery.PhaseFailed, Health: delivery.HealthFailed, - Detail: "the node cannot compose it, and keeps running what it ran: no build"}, false, false}, + Detail: "the node cannot compose it, and keeps running what it ran: no build"}, false, true}, {"failed at a build fingerprint", delivery.Have{FP: "build-0123abcd", Composed: "sha256:" + strings.Repeat("c", 64), Phase: delivery.PhaseFailed, Health: delivery.HealthFailed, Detail: "the node cannot compose it, and keeps running what it ran: no build"}, true, true}, - {"running a build of the old form", delivery.Have{FP: "build-1790368817085312877", Composed: "sha256:" + strings.Repeat("c", 64), - Phase: delivery.PhaseRunning, Health: delivery.HealthOK}, true, true}, + // The live have of novox's de-spiegel (2026-10-28): a build of the old form, named by `build`, and no `fp`. + {"running a build of the old form", delivery.Have{Build: "build-1790368817085312877", Phase: delivery.PhaseRunning, Health: delivery.HealthOK}, true, true}, {"running a build of the new form", delivery.Have{FP: delivery.SourcePrefix2 + strings.Repeat("a", 64), Composed: "sha256:" + strings.Repeat("c", 64), Phase: delivery.PhaseRunning, Health: delivery.HealthOK}, false, false}, {"running with no fingerprint (mesh-wireguard)", delivery.Have{Phase: delivery.PhaseRunning, Health: delivery.HealthOK}, true, false}, @@ -1432,7 +1432,7 @@ func TestAHaveSaysWhetherItsModuleRunsInsideTheNodesOwn(t *testing.T) { } s := &Shadow{c: c, ran: map[string]string{}} s.takeRan(agent.Update{Key: key, Value: value}) - if got := slices.Contains(s.heldToday(), "web"); got != row.heldToday { + if _, got := s.heldToday()["web"]; got != row.heldToday { t.Errorf("web held today: %v, want %v", got, row.heldToday) } }) diff --git a/internal/selfcompose/shadow.go b/internal/selfcompose/shadow.go index 91bf349..5a66209 100644 --- a/internal/selfcompose/shadow.go +++ b/internal/selfcompose/shadow.go @@ -40,10 +40,11 @@ type Shadow struct { // ran is, by module, the digest the module's have says runs: what runs, where the agent's list holds only the copy // from the declaration kept before the switch (issue 1126, probe #532 on shanks). ran map[string]string - // heldNow is, by module, whether its have says the node holds it today (phase held, or a build of the old form it runs - // as the controller keeps it): a candidate holding it too holds - // it as today (frombus.Scope.HeldToday, issue 1126). - heldNow map[string]bool + // heldNow is, by module, how its have says the node keeps it today as it runs: "held", "failed", or "runs a build of + // the old form" (a `build` and no `fp`, which no registered build of the trunk names); "" when it is composed as any + // other. A candidate that cannot compose it, or holds it only behind one of these, keeps it as today + // (frombus.Scope.HeldToday, issue 1126). + heldNow map[string]string // answered is, by proposal, the candidate revisions and the input changes (moved) each answer was composed at: a proposal // is answered again only when either moved (issue 1126, 2026-10-28: shanks answered probe/1126-seed-7 at revision 84 // every 13-17 s, woken by its own modules' haves whose health words changed at every look). @@ -630,23 +631,20 @@ func (s *Shadow) answerProposal(ctx context.Context, proposal string) (delivery. // heldToday is what the node holds today: each module whose have's phase is held, and what its own composer holds, the // node's own composition ("node") among them when it composes none of its own. -func (s *Shadow) heldToday() []string { - set := map[string]bool{} +func (s *Shadow) heldToday() map[string]string { + out := map[string]string{} s.mu.Lock() - for m, held := range s.heldNow { - if held { - set[m] = true + for m, how := range s.heldNow { + if how != "" { + out[m] = how } } s.mu.Unlock() for _, m := range s.c.heldNow() { - set[m] = true + if _, said := out[m]; !said { + out[m] = "held" + } } - out := make([]string, 0, len(set)) - for m := range set { - out = append(out, m) - } - sort.Strings(out) return out } @@ -662,14 +660,22 @@ func (s *Shadow) takeRan(u agent.Update) bool { before, had := s.ran[module] h, err := delivery.DecodeHave(u.Value) if s.heldNow == nil { - s.heldNow = map[string]bool{} + s.heldNow = map[string]string{} } wasHeld := s.heldNow[module] - // Held today: its have's phase is held, or it runs a build of the old form (not src2:), which no registered build of - // the trunk names, so the controller keeps it as it runs (issue 1126: novox's de-spiegel at build-…, its newer build - // not registered); a seed keeps it so too. - s.heldNow[module] = !u.Deleted && err == nil && h.Phase != delivery.PhaseRemoved && - (h.Phase == delivery.PhaseHeld || (h.FP != "" && !strings.HasPrefix(h.FP, delivery.SourcePrefix2))) + s.heldNow[module] = "" + if !u.Deleted && err == nil { + switch { + case h.Phase == delivery.PhaseHeld: + s.heldNow[module] = "held" + case h.Phase == delivery.PhaseFailed: + s.heldNow[module] = "failed" + case h.Phase != delivery.PhaseRemoved && h.FP == "" && h.Build != "": + // A build of the old form: its have names a `build` and no `fp`, which no registered build of the trunk + // names, so the controller keeps it as it runs (issue 1126: novox's de-spiegel at build-…); a seed keeps it so. + s.heldNow[module] = "runs a build of the old form" + } + } holdMoved := wasHeld != s.heldNow[module] // a hold's start or end changes the answer too if u.Deleted || err != nil || h.Composed == "" || h.Phase == delivery.PhaseRemoved { delete(s.ran, module) From 93f33e2305de38ce00d0ba1eb3a02791e82aad48 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 28 Oct 2026 23:56:51 +0100 Subject: [PATCH 6/6] Pin mesh-sdk 76f7c4d: a held module keeps its items (issue 1126) --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 53af774..1543bd1 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/novox/mesh-host go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028224922-f609be786463 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028225633-76f7c4d2238f github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 golang.org/x/crypto v0.57.0 diff --git a/go.sum b/go.sum index f5a6d2c..235c60c 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028224922-f609be786463 h1:Q0XohSVvVtUpamUj+H7Bgha82RDy/EKp/zzhHJGA74A= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028224922-f609be786463/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028225633-76f7c4d2238f h1:jCd8WN+Olo4nlaB1cz1HYpEV4xc6Mzw7viXdwU+0fic= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261028225633-76f7c4d2238f/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=