From da0f8f1cfdb4612e9dbae8a909b03717ceab535f Mon Sep 17 00:00:00 2001 From: jochen Date: Sat, 31 Oct 2026 01:59:58 +0100 Subject: [PATCH] The node composes again only when what it composes from changed, so a want, placement, module or value written again asks the vault nothing (issue 774) Every put under want..>, every module's take-in and every placement write made a pass, and each pass asked the vault for the node's own secrets. The shadow's change check moves onto the composer, gains the want (by its fingerprint, the only part composed) and serves both loops. --- internal/selfcompose/selfcompose.go | 63 +++++++++++++++++++++++- internal/selfcompose/selfcompose_test.go | 36 +++++++++++++- internal/selfcompose/shadow.go | 50 +------------------ 3 files changed, 97 insertions(+), 52 deletions(-) diff --git a/internal/selfcompose/selfcompose.go b/internal/selfcompose/selfcompose.go index aeb2730..2fe37d3 100644 --- a/internal/selfcompose/selfcompose.go +++ b/internal/selfcompose/selfcompose.go @@ -19,6 +19,7 @@ import ( "encoding/json" "errors" "fmt" + "reflect" "slices" "sort" "strings" @@ -399,8 +400,7 @@ func (c *Composer) Watch(ctx context.Context, _ string) (<-chan agent.Update, er case ins[t.in].changed != nil: dirty = ins[t.in].changed(c, t.u) || dirty default: - ins[t.in].take(c, t.u) - dirty = true + dirty = c.takeChanged(ins[t.in], t.u) || dirty } if len(merged) > 0 { continue @@ -468,6 +468,65 @@ func (c *Composer) caughtSent() bool { return sent } +// takeChanged takes u through in.take and says whether the entry it names changed: a want, a placement, a module or a +// config value written again with the value it held changes nothing composed, so it makes no pass (issue 774, +// 2026-10-31: every put under want..> made a pass, and each pass asked the vault for the node's own secrets). +func (c *Composer) takeChanged(in input, u agent.Update) bool { + before, had, known := c.entry(in.bucket, u.Key) + in.take(c, u) + if !known { + return true + } + after, has, _ := c.entry(in.bucket, u.Key) + return had != has || !reflect.DeepEqual(before, after) +} + +// entry is what the composer holds of one key of bucket, whether it holds it, and whether the bucket is one it knows. +func (c *Composer) entry(bucket, key string) (v any, has, known bool) { + c.mu.Lock() + defer c.mu.Unlock() + switch bucket { + case delivery.AssignerWantsBucket: + // A want is composed by its fingerprint alone (fingerprints); its words, rule and newest change nothing composed. + module := agent.NodeModule + if _, err := delivery.ParseWantNodeKey(key); err != nil { + node, m, err := delivery.ParseWantKey(key) + if err != nil || node != c.env.Node { + return nil, false, true + } + module = m + } + w, ok := c.wants[module] + return w.FP, ok, true + case delivery.PlacementBucket: + node, err := delivery.ParsePlacementKey(key) + if err != nil { + return nil, false, true + } + p, ok := c.placements[node] + return p, ok, true + case delivery.ModulesBucket: + name, err := delivery.ParseModulesKey(key) + if err != nil { + return nil, false, true + } + m, ok := c.modules[name] + return m, ok, true + case ResolvedBucket: + rest, ok := strings.CutPrefix(key, c.env.Node+".") + if !ok { + return nil, false, true + } + module, k, ok := strings.Cut(rest, ".") + if !ok { + return nil, false, true + } + val, ok := c.values[module][k] + return val, ok, true + } + return nil, false, false +} + func (c *Composer) takeWant(u agent.Update) { c.mu.Lock() defer c.mu.Unlock() diff --git a/internal/selfcompose/selfcompose_test.go b/internal/selfcompose/selfcompose_test.go index 01647d9..0fc00eb 100644 --- a/internal/selfcompose/selfcompose_test.go +++ b/internal/selfcompose/selfcompose_test.go @@ -546,6 +546,40 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) { t.Errorf("%s: a pass made %v, want %v", row.name, got, row.pass) } } + // Issue 774, 2026-10-31: every put under want.n1.> made a pass, and each pass asked the vault for the node's own + // secrets. A key written again with what it held, or a want whose words moved and not its build, makes none. + worded, _ := delivery.EncodeWant(delivery.Want{FP: fp, Self: true, Words: "lemurs is held at aaaaaaaaaaaa"}) + elsewhere, _ := delivery.EncodeWant(delivery.Want{FP: delivery.SourcePrefix2 + strings.Repeat("e", 64), Self: true}) + for _, row := range []struct { + name, bucket, key string + value []byte + pass bool + }{ + {"lemurs's want again", delivery.AssignerWantsBucket, "want.n1.lemurs", want, false}, + {"the node's own want again", delivery.AssignerWantsBucket, "want.n1.node", nodeWant, false}, + {"lemurs's want with other words", delivery.AssignerWantsBucket, "want.n1.lemurs", worded, false}, + {"the placement again", delivery.PlacementBucket, "placement.n1", place, false}, + {"lemurs's module again", delivery.ModulesBucket, "lemurs", mod, false}, + {"lemurs's want at another build", delivery.AssignerWantsBucket, "want.n1.lemurs", elsewhere, true}, + {"lemurs's want back at its build", delivery.AssignerWantsBucket, "want.n1.lemurs", want, true}, + } { + before := passes() + b.put(t, row.bucket, row.key, row.value) + if !row.pass { + quiet(t, out) + } else { + for done := false; !done; { + select { + case <-out: + case <-time.After(200 * time.Millisecond): + done = true + } + } + } + if got := passes() > before; got != row.pass { + t.Errorf("%s: a pass made %v, want %v", row.name, got, row.pass) + } + } // And today's sequence whole: tool runs at its old build; its want and the trunk move to a build not yet done, and // the node holds tool, keeping what runs, naming why; the build lands, and with no other change tool is handed at it. { @@ -629,7 +663,7 @@ func TestTheNodeComposesItsWantsFromTheBus(t *testing.T) { b.mu.Lock() n := 0 for _, s := range b.said { - if strings.Contains(s, "lemurs is wanted at") { + if strings.Contains(s, "lemurs is wanted at "+gone) { n++ } } diff --git a/internal/selfcompose/shadow.go b/internal/selfcompose/shadow.go index 5a66209..5627274 100644 --- a/internal/selfcompose/shadow.go +++ b/internal/selfcompose/shadow.go @@ -4,7 +4,6 @@ import ( "bytes" "context" "fmt" - "reflect" "sort" "strings" "sync" @@ -231,7 +230,7 @@ func (s *Shadow) Run(ctx context.Context) error { case t.in == 6: changed = s.takeWant(t.u) default: - changed = s.takeChanged(ins[t.in], t.u) + changed = c.takeChanged(ins[t.in], t.u) } if changed { if t.in != 0 { @@ -264,53 +263,6 @@ func (s *Shadow) Run(ctx context.Context) error { } } -// takeChanged takes u through in.take and says whether the entry it names changed: a placement, a module or a config -// value written again with the value it held changes nothing composed. -func (s *Shadow) takeChanged(in input, u agent.Update) bool { - before, had, known := s.entry(in.bucket, u.Key) - in.take(s.c, u) - if !known { - return true - } - after, has, _ := s.entry(in.bucket, u.Key) - return had != has || !reflect.DeepEqual(before, after) -} - -// entry is what the composer holds of one key of bucket, whether it holds it, and whether the bucket is one it knows. -func (s *Shadow) entry(bucket, key string) (v any, has, known bool) { - c := s.c - c.mu.Lock() - defer c.mu.Unlock() - switch bucket { - case delivery.PlacementBucket: - node, err := delivery.ParsePlacementKey(key) - if err != nil { - return nil, false, true - } - p, ok := c.placements[node] - return p, ok, true - case delivery.ModulesBucket: - name, err := delivery.ParseModulesKey(key) - if err != nil { - return nil, false, true - } - m, ok := c.modules[name] - return m, ok, true - case ResolvedBucket: - rest, ok := strings.CutPrefix(key, c.env.Node+".") - if !ok { - return nil, false, true - } - module, k, ok := strings.Cut(rest, ".") - if !ok { - return nil, false, true - } - val, ok := c.values[module][k] - return val, ok, true - } - return nil, false, false -} - // revsOf is a proposal's keys' revisions, by node, as one word. func revsOf(keys map[string]candidateKey) string { nodes := make([]string, 0, len(keys))