From e79a546eb7ab51d0ee8bfb5c6f86ce1e0e5afb25 Mon Sep 17 00:00:00 2001 From: jochen Date: Sun, 8 Nov 2026 23:29:26 +0100 Subject: [PATCH] Compose what runs from the haves while the wants view is unanswered (issue 1966, recovery) Since 16:39 the running assigner cannot answer its wants page within a call's bound, and the failed wants read held the whole user list, so no new build got its credential. As the assignments view already does, an unanswered wants view now composes what runs from each have's fingerprint, is said as a gap that is no failed read, and keeps the pass to one that only adds (keepsLoaded), so the list is held only where it would take from a loaded user. Claude-Session: https://claude.ai/code/session_01NcbgfFfQ4Z9UE5jK32PmE5 --- modules/mesh-grants/grants_test.go | 8 ++++++++ modules/mesh-grants/internal/grants/inputs.go | 20 +++++++++++++++++-- .../mesh-grants/internal/grants/keeploaded.go | 2 ++ 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/modules/mesh-grants/grants_test.go b/modules/mesh-grants/grants_test.go index 7d57b724..bf3fc9ea 100644 --- a/modules/mesh-grants/grants_test.go +++ b/modules/mesh-grants/grants_test.go @@ -2640,6 +2640,14 @@ func TestGrants(t *testing.T) { b.put(grants.ReadHave, "b.mod-y", runs) b.put(grants.ReadHave, "a.mesh-delivery-indexer", runs) }, ""}, + // The wants view unanswered, the assignments answered (issue 1966, the list held from 2026-11-08 16:39): what runs + // comes from the haves, the wants are no failed read, and the pass only adds. + {"the wants view unanswered", func(b *fakeBus, p *grants.Part) { + b.refused[grants.ReadWants] = true + if in := grants.ReadInputs(b, &grants.Manifests{}); in.WantsUnread == "" || len(in.ReadFailed()) != 0 { + t.Errorf("an unanswered wants view is a failed read, or composes no fallback: %q, %v", in.WantsUnread, in.ReadFailed()) + } + }, ""}, // The ledger of a held module stays with its module's users, whatever the seat list (held.go). {"the ledger of a module whose manifest is refused", func(b *fakeBus, p *grants.Part) { b.refuse("mod-x", ledgerX, "it is held") diff --git a/modules/mesh-grants/internal/grants/inputs.go b/modules/mesh-grants/internal/grants/inputs.go index 4b63aa60..dc8ff14f 100644 --- a/modules/mesh-grants/internal/grants/inputs.go +++ b/modules/mesh-grants/internal/grants/inputs.go @@ -180,6 +180,11 @@ type Inputs struct { // answering (bootstrap only, issue 1966); empty when the assignments placed them. Such a pass retires nothing // (ReadFailures) and seals no list dropping a user of the list in force (holdsBack). ByHaves string + // WantsUnread says why this pass composed what runs from each have's fingerprint, the wants view not answering while + // the assignments view did (bootstrap only, issue 1966: a wants page the running assigner cannot answer in a call's + // bound); empty when the wants were read. As ByHaves, such a pass retires nothing (ReadFailures) and only adds + // (keepsLoaded): the list is held only where it would take a permission from a loaded user (holdsBack). + WantsUnread string // Held is, by module, the manifest a held module's running build was judged on (its have's fingerprint, the builder's // build of it): what the seats it defines, the streams others hear from it and its reads are composed from while its // trunk manifest is refused or waits, so no other principal loses a grant to it (ADR 0601 clause 3). @@ -519,12 +524,20 @@ func ReadInputs(r Reader, cache *Manifests) Inputs { // What each machine is wanted to run: the want's fingerprint, the builder's build of it, the manifest it built. var fromTrunk []string wants := map[string][]byte{} - if in.ByHaves == "" { + _, wantsErr := r.Keys(ReadWants) + switch { + case in.ByHaves == "" && wantsErr == nil: wants = each(r, ReadWants, &in.Gaps) // What each machine runs now is its have's build, which the want may already have left (issue 1966): the // memberships keep what either build reads, calls and consumes. runningByHaves(r, &in, cache, in.Had) - } else { + case in.ByHaves == "": + // **Bootstrap only** (issue 1966), as the assignments' placement by the haves: while the wants view does not answer, + // what runs is each placed module's have's fingerprint, said as a gap that is no failed read, and the pass only adds. + in.WantsUnread = "the wants view did not answer (" + wantsErr.Error() + "), so what runs is composed from each have's fingerprint" + in.Gaps = append(in.Gaps, Gap{Input: ReadWants, Why: "refused or absent: what runs is composed from each have's fingerprint instead, until it answers (" + wantsErr.Error() + ")"}) + fromTrunk = runningByHaves(r, &in, cache, in.Running) + default: // Placed by the haves, what runs is what each have's fingerprint names, not a want: the assigner's wants are its // view too, and not read in such a pass. in.Gaps = append(in.Gaps, Gap{Input: ReadWants, Why: "not read: the modules are placed by the node-engines' haves this pass, so what runs is composed from each have's fingerprint"}) @@ -716,6 +729,9 @@ func (in Inputs) ReadFailures() []string { if in.ByHaves != "" { out = append(out, ReadAssignments+": "+in.ByHaves) } + if in.WantsUnread != "" { + out = append(out, ReadWants+": "+in.WantsUnread) + } for _, g := range in.Refused { out = append(out, g.Input+": "+g.Why) } diff --git a/modules/mesh-grants/internal/grants/keeploaded.go b/modules/mesh-grants/internal/grants/keeploaded.go index dee095b4..23693a87 100644 --- a/modules/mesh-grants/internal/grants/keeploaded.go +++ b/modules/mesh-grants/internal/grants/keeploaded.go @@ -21,6 +21,8 @@ func (in Inputs) keepsLoaded() string { switch { case in.ByHaves != "": return in.ByHaves + case in.WantsUnread != "": + return in.WantsUnread case in.SeatListUnread: return "the seat list is unread, so only the bootstrapped seats compose" }