Read what a node runs inside its own composition from its haves, not from what the agent applied, so a restarted agent's first composition keeps the bus (issue 1126, review of the fold round 1)
mesh/delivery merged: source.main moved to novox/mesh-host@50c229f90568
mesh/delivery-group group fix/1126-fold merged: every member is merged
mesh/check checking 69b845c0f7b2 on 0d22776b20ef | novox/mesh-host#152 in group fix/1126-fold

A fresh agent composes before it has applied anything (Running empty), so
InOwn of Running folded only mesh-wireguard and the switch would drop nats.
The composer now watches the node's haves: a placed module whose have names no
new-form fingerprint is folded; one with a new-form have whose want is removed
is not folded. The shadow uses the same set, over the candidate's placement.
This commit is contained in:
2026-10-27 09:03:59 +01:00
parent caafd9306e
commit 69b845c0f7
3 changed files with 118 additions and 38 deletions
+18 -13
View File
@@ -89,13 +89,13 @@ func (s *Shadow) Run(ctx context.Context) error {
c := s.c
node := c.env.Node
ins := []input{
{delivery.PlacementCandidatesBucket, ">", nil},
{delivery.PlacementBucket, ">", (*Composer).takePlacement},
{delivery.HaveBucket, "*." + delivery.HaveNodeWord, nil},
{delivery.ModulesBucket, ">", (*Composer).takeModule},
{ResolvedBucket, node + ".>", (*Composer).takeValue},
{delivery.PlacementCandidatesBucket, ">", nil, nil},
{delivery.PlacementBucket, ">", (*Composer).takePlacement, nil},
{delivery.HaveBucket, "*." + delivery.HaveNodeWord, nil, nil},
{delivery.ModulesBucket, ">", (*Composer).takeModule, nil},
{ResolvedBucket, node + ".>", (*Composer).takeValue, nil},
// What this node runs moved: an apply ends in its module's have.
{delivery.HaveBucket, node + ".*", nil},
{delivery.HaveBucket, node + ".*", nil, nil},
}
type tagged struct {
in int
@@ -140,6 +140,7 @@ func (s *Shadow) Run(ctx context.Context) error {
case t.in == 2:
dirty = s.takeRecord(t.u) || dirty
case t.in == 5:
c.takeModuleHave(t.u)
dirty = !isNodeKey(t.u.Key) || dirty
default:
ins[t.in].take(c, t.u)
@@ -312,7 +313,11 @@ func (s *Shadow) answerGroup(ctx context.Context, group string) (delivery.Candid
c.mu.Unlock()
placement = &p
}
fps, inOwn := shadowBuilds(running, *placement, modules)
// What it runs inside its own composition, of what the candidate places (the seed's trunk places nothing).
c.mu.Lock()
inOwn := c.inOwnOf(*placement)
c.mu.Unlock()
fps := shadowBuilds(running, *placement, modules, inOwn)
kept := map[string]string{}
for module, fp := range fps {
if mod, ok := modules[module]; ok && mod.FP2 == fp {
@@ -339,17 +344,17 @@ func (s *Shadow) answerGroup(ctx context.Context, group string) (delivery.Candid
}
// shadowBuilds is the build each module placed is composed at in shadow: the one it runs, and for one the candidate
// adds, the trunk's; and inOwn, the modules it runs inside its own composition (mesh-wireguard, one at a build of the
// old form), which are composed there again rather than at the trunk's build as if the candidate added them (issue
// 1126: nats and photos have a trunk build that no want names, and the seed's answer named them added).
func shadowBuilds(running []delivery.Composed, placement delivery.Placement, modules map[string]delivery.Module) (map[string]string, []string) {
// adds, the trunk's. A module it runs inside its own composition (inOwn, from its haves: mesh-wireguard, one at a build
// of the old form) is given none, so it is folded there again, as the node's own composer folds it, rather than
// composed at the trunk's build as if the candidate added it (issue 1126: nats and photos have a trunk build that no
// want names).
func shadowBuilds(running []delivery.Composed, placement delivery.Placement, modules map[string]delivery.Module, inOwn []string) map[string]string {
fps := map[string]string{}
for _, r := range running {
if r.Module != "" && r.FP != "" {
fps[r.Module] = r.FP
}
}
inOwn := frombus.InOwn(running, placement)
folded := map[string]bool{}
for _, m := range inOwn {
folded[m] = true
@@ -361,7 +366,7 @@ func shadowBuilds(running []delivery.Composed, placement delivery.Placement, mod
}
}
}
return fps, inOwn
return fps
}
func sameAnswer(a, b delivery.CandidateAnswer) bool {