From ed037784e2851d7162e532fc7aaa266a1915d9da Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 12 Oct 2026 11:27:05 +0200 Subject: [PATCH 1/2] Name the hold a waiting walk waits on in plans (issue 529) A walk whose delivery was held for a person said only that it waits for mesh-delivery's word and that plans go starts it by hand, pointing the reader at a hand start instead of the person's decision. Ask mesh-delivery what holds each waiting walk and say the held delivery, its reason and mesh-delivery.release, or the delivery not at its turn. --- cmd/mesh-controller/delivery.go | 23 ++++++ cmd/mesh-controller/delivery_conditions.go | 31 ++++++++ cmd/mesh-controller/issue529_test.go | 84 ++++++++++++++++++++++ cmd/mesh-controller/release_plan.go | 43 +++++++++-- 4 files changed, 177 insertions(+), 4 deletions(-) create mode 100644 cmd/mesh-controller/issue529_test.go diff --git a/cmd/mesh-controller/delivery.go b/cmd/mesh-controller/delivery.go index 56e4a077..4c4df022 100644 --- a/cmd/mesh-controller/delivery.go +++ b/cmd/mesh-controller/delivery.go @@ -82,6 +82,29 @@ func waitingNote(p inventory.Plan) string { p.Delivery.Awaits, p.ID) } +// waitingSaid is what a walk waiting for its word says when the delivery's owner said what holds it (novox/hq +// issue 529): a delivery held for a person, its reason and the verb that releases it — the person's decision, not +// a hand start — or the deliveries not at their turn. Told nothing, it is waitingNote. +func waitingSaid(p inventory.Plan, lines []stalledLine) string { + if len(lines) == 0 { + return waitingNote(p) + } + var held, other []string + for _, l := range lines { + if l.State == "held" { + held = append(held, l.Says) + } else { + other = append(other, l.Says) + } + } + if len(held) > 0 { + return fmt.Sprintf("published; its walk waits for %s's word, and a delivery it carries is held for a person: %s", + p.Delivery.Awaits, strings.Join(held, "; ")) + } + return fmt.Sprintf("published; its walk waits for %s's word: %s — `plans go %s --why …` starts it by hand", + p.Delivery.Awaits, strings.Join(other, "; "), p.ID) +} + // letGo gives a waiting walk its word: by the delivery's owner, or a person. The next advance asks its // first tier. Refused for a walk that does not wait. func letGo(ctx context.Context, inv *inventory.Inventory, id, by, why string) (inventory.Plan, error) { diff --git a/cmd/mesh-controller/delivery_conditions.go b/cmd/mesh-controller/delivery_conditions.go index def12653..21d7ed94 100644 --- a/cmd/mesh-controller/delivery_conditions.go +++ b/cmd/mesh-controller/delivery_conditions.go @@ -37,6 +37,9 @@ type stalledLine struct { // Number is the pull request's, for a line of a head the forge never announced (novox/hq issue 347). Number int `json:"number,omitempty"` State string `json:"state"` + // Walk is the walk a line is about: a waiting walk no live delivery holds, or, asked with `walks`, each walk + // waiting for the owner's word and what holds it (novox/hq issues 362, 529). + Walk string `json:"walk,omitempty"` // Waiting are the merge checks a line of the state `unanswered` waits on, as mesh-delivery says each: // "mesh/merge-gate pending since ", "mesh/repo-check never set" (novox/hq issue 438). Waiting []string `json:"waiting,omitempty"` @@ -125,6 +128,34 @@ func deliveriesStalled(ctx context.Context, conn *nats.Conn, held bool) ([]stall return lines, nil } +// walkWaits are, by walk, the lines mesh-delivery says of a walk waiting for its word: what holds it. +type walkWaits map[string][]stalledLine + +// walksWaiting asks the delivery's owner what holds each walk waiting for its word (novox/hq issue 529): its +// `stalled` asked with `walks`, so a line of `plans` names a held delivery, its reason and the verb that releases +// it, instead of pointing at a hand start. Nil when no holder is on record or none answers: the line then reads as +// before. An owner from before this answers its stalled lines, of which only those naming a walk are kept. +func walksWaiting(ctx context.Context, conn *nats.Conn, held bool) walkWaits { + if !held { + return nil + } + raw, err := askDeliveryOwner(ctx, conn, "stalled", map[string]any{"walks": true}) + if err != nil { + return nil + } + var lines []stalledLine + if json.Unmarshal(raw, &lines) != nil { + return nil + } + out := walkWaits{} + for _, l := range lines { + if l.Walk != "" { + out[l.Walk] = append(out[l.Walk], l) + } + } + return out +} + // stalledObservations are the conditions of what is stalled. func stalledObservations(lines []stalledLine) []conditions.Observation { out := make([]conditions.Observation, 0, len(lines)) diff --git a/cmd/mesh-controller/issue529_test.go b/cmd/mesh-controller/issue529_test.go new file mode 100644 index 00000000..858672db --- /dev/null +++ b/cmd/mesh-controller/issue529_test.go @@ -0,0 +1,84 @@ +package main + +import ( + "context" + "encoding/json" + "strings" + "testing" + "time" + + "github.com/nats-io/nats.go" + + "github.com/novox/mesh-controller/internal/inventory" +) + +// novox/hq issue 529: a walk waiting because its delivery is held for a person said only "its walk waits for +// mesh-delivery's word — `plans go … --why …` starts it by hand", pointing a reader at a hand start instead of at +// the person's decision. The line names what mesh-delivery says holds the walk: the held delivery, its reason and +// `mesh-delivery.release`; or the delivery not at its turn. + +func waitingWalk(now time.Time) inventory.Plan { + return inventory.Plan{ID: "plan-1791793277221379309", Repository: "novox/mesh-sdk", Branch: "main", + Commit: "9747f3e1b1ac", State: inventory.PlanBuilding, Tiers: [][]string{{"mesh-sdk"}}, + Created: now.Add(-9 * time.Minute), Modules: map[string]*inventory.PlanModule{}, + Delivery: &inventory.PlanDelivery{Awaits: "mesh-delivery"}} +} + +func TestAWalkWaitingOnAHeldDeliveryNamesTheHold(t *testing.T) { + now := time.Date(2026, 10, 12, 8, 35, 0, 0, time.UTC) + p := waitingWalk(now) + held := "novox/mesh-sdk@d41d255f28ad held since 2026-10-12T08:23:00Z: its group adr-0307-state-history's composed " + + "check did not pass for the heads that merged — a person decides: `mesh-delivery.release` (id " + + "novox/mesh-sdk@d41d255f28ad, why) lets it go, `mesh-delivery.stop` ends it" + waits := walkWaits{p.ID: {{ID: "novox/mesh-sdk@d41d255f28ad", State: "held", Walk: p.ID, Says: held}}} + line := planLineSaid(p, now, pauseView{}, tierAtLeast, nil, waits) + for _, want := range []string{"held for a person", "composed check did not pass", "mesh-delivery.release"} { + if !strings.Contains(line, want) { + t.Fatalf("the line does not say %q: %q", want, line) + } + } + if strings.Contains(line, "starts it by hand") { + t.Fatalf("a held walk's line points at a hand start: %q", line) + } + // Told nothing — an older mesh-delivery, or none answering — it reads as before. + if line := planLineSaid(p, now, pauseView{}, tierAtLeast, nil, nil); !strings.Contains(line, "starts it by hand") { + t.Fatalf("told nothing, the line reads %q", line) + } +} + +func TestAWalkWaitingOnADeliveryNotAtItsTurnSaysSo(t *testing.T) { + now := time.Date(2026, 10, 12, 8, 57, 0, 0, time.UTC) + p := waitingWalk(now) + waits := walkWaits{p.ID: {{ID: "novox/mesh-sdk@9747f3e1b1ac", State: "published", Walk: p.ID, + Says: "not let go yet: novox/mesh-tools@3f0c1a6cec18 (published) carried by it not at its turn"}}} + if line := planLineSaid(p, now, pauseView{}, tierAtLeast, nil, waits); !strings.Contains(line, "not at its turn") || + !strings.Contains(line, "novox/mesh-tools@3f0c1a6cec18") { + t.Fatalf("the line reads %q", line) + } +} + +// What holds each waiting walk is asked of mesh-delivery's `stalled` with `walks`, a verb the controller's grant +// already names; only lines naming a walk are kept, by walk. +func TestWhatHoldsTheWaitingWalksIsAskedOfTheDeliveryOwner(t *testing.T) { + was := askDeliveryOwner + defer func() { askDeliveryOwner = was }() + var asked map[string]any + askDeliveryOwner = func(_ context.Context, _ *nats.Conn, verb string, args map[string]any) (json.RawMessage, error) { + if verb != "stalled" { + t.Fatalf("asked %s", verb) + } + asked = args + return json.Marshal([]stalledLine{{ID: "novox/a@1", State: "held", Walk: "plan-1", Says: "held: why"}, + {ID: "novox/b@2", State: "held", Says: "a delivery past its bound, no walk"}}) + } + got := walksWaiting(t.Context(), nil, true) + if asked["walks"] != true { + t.Fatalf("stalled was asked with %v", asked) + } + if len(got) != 1 || len(got["plan-1"]) != 1 || got["plan-1"][0].Says != "held: why" { + t.Fatalf("the waiting walks are %+v", got) + } + if got := walksWaiting(t.Context(), nil, false); got != nil { + t.Fatalf("with no holder on record it asked: %+v", got) + } +} diff --git a/cmd/mesh-controller/release_plan.go b/cmd/mesh-controller/release_plan.go index d3a9b260..dcbd9a5d 100644 --- a/cmd/mesh-controller/release_plan.go +++ b/cmd/mesh-controller/release_plan.go @@ -11,6 +11,8 @@ import ( "strings" "time" + "github.com/nats-io/nats.go" + "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/link" ) @@ -1258,6 +1260,13 @@ func planLineWith(p inventory.Plan, now time.Time, pause pauseView, bound time.D // planLineOn is planLineWith naming the plan by what it moves and where (planHeadline), given what runs each // module; nil names no machine. func planLineOn(p inventory.Plan, now time.Time, pause pauseView, bound time.Duration, running func(string) []string) string { + return planLineSaid(p, now, pause, bound, running, nil) +} + +// planLineSaid is planLineOn saying, of a walk waiting for its delivery's word, what the delivery's owner says +// holds it (novox/hq issue 529); nil waits says it as before. +func planLineSaid(p inventory.Plan, now time.Time, pause pauseView, bound time.Duration, running func(string) []string, + waits walkWaits) string { name := planHeadline(p, running) where := fmt.Sprintf("tier %d of %d", min(p.Tier+1, len(p.Tiers)), len(p.Tiers)) switch p.State { @@ -1274,7 +1283,7 @@ func planLineOn(p inventory.Plan, now time.Time, pause pauseView, bound time.Dur since := now.Sub(inTierSince(p)).Round(time.Second) if p.Waiting() { // Waiting for its delivery's word is no lateness of the walk's (novox/hq ADR 0239). - return fmt.Sprintf("%s · %s, %s, for %s", name, where, waitingNote(p), since) + return fmt.Sprintf("%s · %s, %s, for %s", name, where, waitingSaid(p, waits[p.ID]), since) } if deferred(p) { // Nor is waiting for the walk before it to end (one walk at a time, novox/hq ADR 0276). @@ -1479,11 +1488,11 @@ func plansCommand(ctx context.Context, args []string) error { return err } bounds := readTierBounds(ctx, inv, now) - fmt.Printf("%s — %s\n", p.ID, planLineOn(p, now, buildSeatPause(ctx, inv, []inventory.Plan{p}), + fmt.Printf("%s — %s\n", p.ID, planLineSaid(p, now, buildSeatPause(ctx, inv, []inventory.Plan{p}), bounds.of(p.Repository), func(module string) []string { on, _ := inv.Running(ctx, module) return on - })) + }, waitsOf(ctx, inv, []inventory.Plan{p}))) if r := p.Release; r != nil { // A release plan's walk (ADR 0236): machines done, the one judged, those to come. fmt.Printf(" machines in order: %s; done: %s; skipped: %s\n", strings.Join(r.Order, ", "), @@ -1630,15 +1639,41 @@ func plansCommand(ctx context.Context, args []string) error { on, _ := inv.Running(ctx, module) return on } + waits := waitsOf(ctx, inv, plans) for _, p := range plans { if p.Batch() { continue } - fmt.Printf("%-28s %s\n", p.ID, planLineOn(p, now, pause, bounds.of(p.Repository), running)) + fmt.Printf("%-28s %s\n", p.ID, planLineSaid(p, now, pause, bounds.of(p.Repository), running, waits)) } return nil } +// waitsOf asks the delivery's owner what holds the walks among these that wait for its word (novox/hq issue 529), +// once, and only when one waits; nil when none waits, no holder is on record, or the bus or the owner does not +// answer within a few seconds — the lines then read as before. +func waitsOf(ctx context.Context, inv *inventory.Inventory, plans []inventory.Plan) walkWaits { + waiting := false + for _, p := range plans { + waiting = waiting || p.Open() && p.Waiting() + } + if !waiting { + return nil + } + entries, err := inv.Catalogued(ctx) + if err != nil { + return nil + } + ctx, cancel := context.WithTimeout(ctx, 3*time.Second) + defer cancel() + var waits walkWaits + _ = onTheBus(func(conn *nats.Conn) error { + waits = walksWaiting(ctx, conn, deliverySeatHeld(entries)) + return nil + }) + return waits +} + // planWhatIf is the plan a merge would produce, computed the way the merge handler computes one // and saved nowhere: the modules the repository's changed files touch (or the modules named), what // packages their source, everything reachable from them, in tiers. For reading before merging. From b03ec3b64e81f0e8ec7773f08dee0278bdc122a3 Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 12 Oct 2026 11:34:54 +0200 Subject: [PATCH 2/2] Offer no hand start for a walk that is only waiting its turn (issue 529) A hand start there would skip its group's order; point at it only for a walk nothing holds, and say why nothing holds it. --- cmd/mesh-controller/delivery.go | 19 ++++++++++++++----- cmd/mesh-controller/issue529_test.go | 22 ++++++++++++++++++++-- 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/cmd/mesh-controller/delivery.go b/cmd/mesh-controller/delivery.go index 4c4df022..4055e730 100644 --- a/cmd/mesh-controller/delivery.go +++ b/cmd/mesh-controller/delivery.go @@ -89,20 +89,29 @@ func waitingSaid(p inventory.Plan, lines []stalledLine) string { if len(lines) == 0 { return waitingNote(p) } - var held, other []string + var held, unheld, other []string for _, l := range lines { - if l.State == "held" { + switch l.State { + case "held": held = append(held, l.Says) - } else { + case "unheld": + unheld = append(unheld, l.Says) + default: other = append(other, l.Says) } } - if len(held) > 0 { + switch { + case len(held) > 0: return fmt.Sprintf("published; its walk waits for %s's word, and a delivery it carries is held for a person: %s", p.Delivery.Awaits, strings.Join(held, "; ")) + case len(other) > 0: + // Not at its turn: it goes when its turn comes. A hand start here would skip its group's order (review of + // novox/hq issue 529), so none is offered. + return fmt.Sprintf("published; its walk waits for %s's word: %s", p.Delivery.Awaits, strings.Join(other, "; ")) } + // Held by nothing: nothing will give it its word, so the hand start is the way on. return fmt.Sprintf("published; its walk waits for %s's word: %s — `plans go %s --why …` starts it by hand", - p.Delivery.Awaits, strings.Join(other, "; "), p.ID) + p.Delivery.Awaits, strings.Join(unheld, "; "), p.ID) } // letGo gives a waiting walk its word: by the delivery's owner, or a person. The next advance asks its diff --git a/cmd/mesh-controller/issue529_test.go b/cmd/mesh-controller/issue529_test.go index 858672db..d8cc2fb1 100644 --- a/cmd/mesh-controller/issue529_test.go +++ b/cmd/mesh-controller/issue529_test.go @@ -51,8 +51,26 @@ func TestAWalkWaitingOnADeliveryNotAtItsTurnSaysSo(t *testing.T) { p := waitingWalk(now) waits := walkWaits{p.ID: {{ID: "novox/mesh-sdk@9747f3e1b1ac", State: "published", Walk: p.ID, Says: "not let go yet: novox/mesh-tools@3f0c1a6cec18 (published) carried by it not at its turn"}}} - if line := planLineSaid(p, now, pauseView{}, tierAtLeast, nil, waits); !strings.Contains(line, "not at its turn") || - !strings.Contains(line, "novox/mesh-tools@3f0c1a6cec18") { + line := planLineSaid(p, now, pauseView{}, tierAtLeast, nil, waits) + if !strings.Contains(line, "not at its turn") || !strings.Contains(line, "novox/mesh-tools@3f0c1a6cec18") { + t.Fatalf("the line reads %q", line) + } + // No hand start offered: it would skip the group's order (review of issue 529). + if strings.Contains(line, "plans go") { + t.Fatalf("a walk not at its turn points at a hand start: %q", line) + } +} + +// A walk nothing holds — the nzbgeek case, its merges' deliveries ended as nothing-to-move — is said so, and the hand +// start is the way on. +func TestAWalkHeldByNothingSaysWhyAndTheHandStart(t *testing.T) { + now := time.Date(2026, 10, 11, 20, 30, 0, 0, time.UTC) + p := waitingWalk(now) + waits := walkWaits{p.ID: {{ID: "novox/mesh-media-catalog@cdcdcdcd0000", State: "unheld", Walk: p.ID, + Says: "held by nothing: its merges' deliveries ended as nothing-to-move (novox/mesh-media-catalog@abababab0000), " + + "yet the walk moves nzbgeek, nzbget — nothing gives it its word"}}} + line := planLineSaid(p, now, pauseView{}, tierAtLeast, nil, waits) + if !strings.Contains(line, "ended as nothing-to-move") || !strings.Contains(line, "plans go "+p.ID) { t.Fatalf("the line reads %q", line) } }