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" }