From 3ec05dda1f41af7a4177403da37d99165c8408ad Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 12 Nov 2026 10:48:08 +0100 Subject: [PATCH] assigner: a reads statement older than a reads-changed heard does not stand (issue 1975) The assigner keeps each reads-changed event it hears beside its statement (enginereads.Keeper.HeardChanged), reads a node as the event says while its statement is behind it, and asks again until the statement is as new. Builds: issue 1975 note 4 (the lead's handover; no design line) Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h --- .../cmd/mesh-delivery-assigner/main.go | 4 ++++ .../cmd/mesh-delivery-assigner/pass_test.go | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/main.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/main.go index 3ec09822..cb12c55a 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/main.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/main.go @@ -569,6 +569,10 @@ func hearReadsChanged(ctx context.Context, js jetstream.JetStream, hub *process. logf("%s holds what is not %s, and %s is asked again all the same: %v", m.Subject(), enginereads.Event, node, err) reads.AskNode(node, enginereads.Event+" that does not read") default: + // What the event says is kept first: a statement older than it does not stand while the node is asked again. + if e, derr := enginereads.DecodeChanged(m.Data()); derr == nil { + reads.HeardChanged(e) + } reads.AskNode(node, enginereads.Event) } } diff --git a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass_test.go b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass_test.go index 5b077f78..e2a019c4 100644 --- a/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass_test.go +++ b/modules/mesh-delivery-assigner/cmd/mesh-delivery-assigner/pass_test.go @@ -317,6 +317,25 @@ func TestAnAnswerCountsOnlyForTheAskingItAnswered(t *testing.T) { if asked["ace"] != 2 { t.Errorf("ace, back online, was asked %d times, want 2", asked["ace"]) } + // Issue 1975: ace's engine answered its bootstrapped seats, then stated the full list and said so; an answer older + // than that event does not stand: the rules read what the event says, and the node is asked until it is as new. + stale := true + bootstrapped := func(node, _ string, _ any) (json.RawMessage, error) { + if stale { + stale = false + return json.Marshal(nodeengine.ManifestReadsAnswer{V: 1, Node: node, ManifestReads: []string{"seat:a"}, StatedAt: "2026-11-12T01:14:36.498Z"}) + } + return json.Marshal(nodeengine.ManifestReadsAnswer{V: 1, Node: node, ManifestReads: []string{"seat:a", "seat:b"}, StatedAt: "2026-11-12T01:14:39.001Z"}) + } + rk := &enginereads.Keeper{Call: bootstrapped, Sync: true} + rk.AskNode("ace", "the assigner started") + rk.HeardChanged(nodeengine.ManifestReadsChangedEvent{V: 1, Node: "ace", Why: enginereads.ReadsStarted, At: "2026-11-12T01:14:39.002Z", ManifestReads: []string{"seat:a", "seat:b"}}) + rp := &passer{w: w, reads: rk, online: map[string]bool{"ace": true}} + rn := w.nodes(nil, nil) + rp.engineReads(rn) + if !slices.Equal(rn.Reads["ace"], []string{"seat:a", "seat:b"}) { + t.Errorf("the rules read the statement older than the event heard: %v", rn.Reads["ace"]) + } // ADR 0668 ยง3 (review round 1): a node whose engine answers nothing while its machine is heard is asked again at each // pass once its ask ended, and past five minutes is the finding call-unanswered, cleared once it answers; none is // written while its machine is away.