Merge pull request 'mesh-delivery: give a waiting walk its word or say what holds it (issues 529, 362)' (#229) from fix/529-a-waiting-walk-is-answered-or-named into main

This commit was merged in pull request #229.
This commit is contained in:
2026-10-12 09:55:20 +00:00
5 changed files with 279 additions and 4 deletions
@@ -366,6 +366,7 @@ func (h *Holder) words() []func() {
}
sort.Strings(walks)
var asks []func()
h.waitsWhy = map[string]string{}
for _, walk := range walks {
want := h.turning[walk]
var waiting []string
@@ -377,14 +378,16 @@ func (h *Holder) words() []func() {
}
if len(waiting) > 0 {
sort.Strings(waiting)
h.Logf("[mesh-delivery] the walk %s is not let go yet: %s carried by it not at its turn", walk,
strings.Join(waiting, ", "))
h.waitsWhy[walk] = strings.Join(waiting, ", ") + " carried by it not at its turn"
h.Logf("[mesh-delivery] the walk %s is not let go yet: %s", walk, h.waitsWhy[walk])
continue
}
ids, why := append([]string(nil), want.ids...), strings.Join(want.whys, "; ")
// **Never the word for a walk that carries a stopped change** (ADR 0299, review of #221): it is asked to
// leave the merge out first (leaveStopped).
if stopped, _ := h.carriesStopped(walk); len(stopped) > 0 {
h.waitsWhy[walk] = "it carries " + strings.Join(stopped, ", ") + ", whose delivery was stopped: it is " +
"asked to leave that merge out first"
h.Logf("[mesh-delivery] the walk %s is not let go: it carries %s, whose delivery was stopped", walk,
strings.Join(stopped, ", "))
continue
@@ -419,3 +422,94 @@ func (h *Holder) words() []func() {
h.turning = nil
return asks
}
// endedMovingNothing is whether a delivery ended delivered with nothing for a walk to move: merged, its plan
// moving no module the mesh holds, and no walk followed.
func endedMovingNothing(d *Delivery) bool {
return d.State == Delivered && d.MergedAs != "" && d.Walk == nil && d.MovesNothing()
}
// endedAsNothingToMove are the deliveries a walk answers that ended delivered as nothing for a walk to move (novox/hq
// issues 362, 529: the nzbgeek case); empty when it answers any other. Such a walk is held by nothing: said, never
// let go by this owner. Under the lock.
func (h *Holder) endedAsNothingToMove(w Walk) []string {
var out []string
for _, d := range h.deliveries {
_, answers := walkAnswers(w, d)
if !answers && (d.Walk == nil || d.Walk.ID != w.ID) {
continue
}
if !endedMovingNothing(d) {
return nil
}
out = append(out, d.ID)
}
sort.Strings(out)
return out
}
// WalksWaiting is `stalled` asked with `walks` (novox/hq issue 529): for each walk waiting for this owner's word,
// what holds it, in the shape of a stalled line naming the walk — each held delivery it carries (State held, its
// reason and the verb that releases it), else why its word was not given at the last tick (State published), else
// that no live delivery holds it (State unheld). A walk whose word is being given is not listed. The controller's
// `plans` says these instead of pointing at a hand start; the self-check never raises a condition for them.
func (h *Holder) WalksWaiting() []StalledLine {
h.mu.Lock()
defer h.mu.Unlock()
now := h.Now()
var out []StalledLine
for id, s := range h.walks {
w := s.walk
if w.Release != nil || w.IsBatch() || !w.Open() || !w.Seen().Waits || h.asking[id] {
continue
}
waited := now.Sub(w.Created).Round(time.Second).String()
var held []StalledLine
live := false
for _, d := range h.deliveries {
if d.State.Final() || d.Walk == nil || d.Walk.ID != id {
continue
}
live = true
if d.State == Held {
held = append(held, StalledLine{ID: d.ID, Number: d.Number, State: Held, Walk: id, For: waited,
Bound: "0s", H2: "none: held for a person — the operator's", Says: HeldLine(d)})
}
}
switch {
case len(held) > 0:
out = append(out, held...)
case h.waitsWhy[id] != "":
out = append(out, StalledLine{ID: IDOf(w.Repository, w.Commit), State: Published, Walk: id, For: waited,
Bound: "0s", H2: "none: it goes at its turn", Says: "not let go yet: " + h.waitsWhy[id]})
case !live:
says := "no delivery that has not ended follows it or is answered by it, so nothing gives it its word: " +
"`mesh-delivery.show` for the deliveries of " + w.Repository
if ended := h.endedAsNothingToMove(w); len(ended) > 0 {
// The nzbgeek case (novox/hq issues 362, 529): its merges' deliveries ended as nothing for a walk to
// move, yet the walk moves modules — nothing judged those, so nothing here lets it go.
says = "held by nothing: its merges' deliveries ended as nothing-to-move (" + strings.Join(ended, ", ") +
"), yet the walk moves " + strings.Join(walkModules(w), ", ") + " — nothing gives it its word"
}
out = append(out, StalledLine{ID: IDOf(w.Repository, w.Commit), State: Unheld, Walk: id, For: waited,
Bound: "0s", H2: "none: no delivery holds the walk — the operator's", Says: says})
}
}
sort.Slice(out, func(i, j int) bool {
if out[i].Walk != out[j].Walk {
return out[i].Walk < out[j].Walk
}
return out[i].ID < out[j].ID
})
return out
}
// walkModules are the modules a walk's record names, sorted.
func walkModules(w Walk) []string {
out := make([]string, 0, len(w.Modules))
for m := range w.Modules {
out = append(out, m)
}
sort.Strings(out)
return out
}
@@ -55,6 +55,9 @@ type Holder struct {
walks map[string]seenWalk
orphans map[string]time.Time
turning map[string]*turnWant
// waitsWhy is, by walk, why the last tick did not give a waiting walk its word: a delivery it carries not at
// its turn, or a stopped change it carries (novox/hq issue 529). Said by WalksWaiting.
waitsWhy map[string]string
// asking are the walks whose word is being asked right now, outside the lock: not asked twice meanwhile.
asking map[string]bool
// leaving are, by walk, the merges a stop gathered to leave out of it, asked once (novox/hq ADR 0299).
@@ -103,6 +106,7 @@ func (h *Holder) init() {
h.written = map[string][]byte{}
h.dirtyGroup = map[string]bool{}
h.unmatched = map[string]unmatched{}
h.waitsWhy = map[string]string{}
h.askTried = map[string]time.Time{}
h.walkAsked = map[string]time.Time{}
h.unchecked = map[string]unchecked{}
@@ -0,0 +1,142 @@
package main
import (
"strings"
"testing"
"time"
)
// novox/hq issue 362's note of 2026-10-11 (the nzbgeek case) and issue 529: mesh-media-catalog #18 added a module
// whose delivery plan said "sent nowhere". This owner ended its delivery at once, "nothing for a walk to move", but
// the controller opened a walk for the merge that moved modules to a machine and waited for this owner's word.
// With its delivery ended, nothing gave the walk its word: it held 39 minutes and two walks queued behind it.
const (
media = "novox/mesh-media-catalog"
headNzb = "abababab00001818"
mergeNzb = "cdcdcdcd00001818"
nzbWalkID = "plan-1791748193836841649"
)
var atNzb = time.Date(2026, 10, 11, 19, 43, 0, 0, time.UTC)
// nzbgeekMerged is #18 checked on a plan that builds a new module and sends it nowhere, and merged: its delivery
// ends at once, nothing for a walk to move.
func nzbgeekMerged(t *testing.T) *world {
t.Helper()
w := newWorldOver(t, newMemStore(), newFakeController(), newFakeForge(), atNzb.Add(-time.Minute))
p := pr(media, 18, headNzb, "feat/nzbgeek", "modules/nzbgeek/module.json")
w.h.PullUpdated(p)
v := verdict(media, 18, headNzb, "pass")
v.Plan = &DeliveryPlan{Repository: media, Base: "main", New: []string{"modules/nzbgeek"},
Summary: "builds new: modules/nzbgeek, sent nowhere; no bus step"}
w.h.Checked(v)
w.later(time.Minute)
w.h.PullMerged(mergedAt(p, mergeNzb, atNzb))
w.settleAll()
if d := w.delivery(IDOf(media, headNzb)); d.State != Delivered {
t.Fatalf("#18 is %s, not delivered as nothing for a walk to move", d.State)
}
return w
}
// nzbWalk is the walk the controller cut for the merge's batch: it names the merge and moves nzbgeek and nzbget.
func nzbWalk(created time.Time, merges ...WalkMerge) Walk {
w := Walk{ID: nzbWalkID, Repository: media, Branch: "main", Commit: mergeNzb, Created: created, State: walkBuilding,
Tiers: [][]string{{"nzbgeek", "nzbget"}}, Modules: map[string]*WalkModule{"nzbgeek": {}, "nzbget": {}},
Delivery: &WalkDelivery{Awaits: "mesh-delivery", Merges: merges}}
return w
}
// **A walk answering only merges that ended as nothing for a walk to move is named as held by nothing** (novox/hq
// issues 362 and 529): never let go by this owner — nothing judged what it moves — and said, by `stalled` with
// `walks`, with why, so the controller's `plans` says so instead of pointing nowhere.
func TestAWalkOfNothingToMoveMergesIsNamedAsHeldByNothing(t *testing.T) {
w := nzbgeekMerged(t)
w.later(90 * time.Second)
w.h.WalkMoved(w.ctl.put(nzbWalk(w.now, WalkMerge{Repository: media, Commit: mergeNzb})))
w.settleAll()
if len(w.ctl.delivers) != 0 {
t.Fatalf("a walk no live delivery holds was let go: %v", w.ctl.delivers)
}
lines := w.h.WalksWaiting()
if len(lines) != 1 || lines[0].Walk != nzbWalkID || lines[0].State != Unheld ||
!strings.Contains(lines[0].Says, "ended as nothing-to-move") || !strings.Contains(lines[0].Says, IDOf(media, headNzb)) ||
!strings.Contains(lines[0].Says, "nzbget") {
t.Fatalf("the walk is said as %+v", lines)
}
}
// **The status names each held delivery, why, and the verb that releases it** (novox/hq issue 529): "held: 1"
// alone pointed a reader nowhere.
func TestTheStatusNamesAHeldDeliveryAndWhy(t *testing.T) {
w := newWorldOver(t, newMemStore(), newFakeController(), newFakeForge(), atNzb)
p := pr(media, 22, headNzb, "adr-0307-state-history", "modules/nzbgeek/module.json")
w.h.PullUpdated(p)
w.h.PullMerged(mergedAt(p, mergeNzb, atNzb)) // merged before any verdict: held for a person
w.settleAll()
id := IDOf(media, headNzb)
d := w.delivery(id)
if d.State != Held {
t.Fatalf("#22 is %s", d.State)
}
held, _ := w.h.Status("listening")["held"].([]string)
if len(held) != 1 || !strings.Contains(held[0], id) || !strings.Contains(held[0], d.HeldWhy) ||
!strings.Contains(held[0], "mesh-delivery.release") {
t.Fatalf("the status says held %q", held)
}
}
// **A walk waiting for this owner's word is told why** (novox/hq issue 529): asked with `walks`, `stalled` says of
// each walk waiting for its word what holds it — a held delivery, naming it, its reason and `mesh-delivery.release`,
// or the deliveries not at their turn — so the controller's `plans` points at the person's decision, not at a hand
// start.
func TestAWaitingWalkIsToldWhatHoldsIt(t *testing.T) {
w := newWorldOver(t, newMemStore(), newFakeController(), newFakeForge(), atDu.Add(5*time.Second))
cc := pr(cat, 101, headCC, "feat/claude-code", "modules/claude-code/module.json")
du := pr(cat, 102, headDu, "feat/dunst", "modules/dunst/module.json")
w.h.PullUpdated(cc)
w.h.PullUpdated(du)
w.h.Checked(verdictMoving(cat, 101, headCC, "claude-code"))
w.h.PullMerged(mergedAt(cc, mergeCC, atCC))
w.h.PullMerged(mergedAt(du, mergeDu, atDu)) // unchecked: held for a person
walk := catWalk("plan-9", mergeDu, atDu.Add(5*time.Second), "claude-code", "dunst")
walk.Delivery.Merges = []WalkMerge{{Repository: cat, Commit: mergeCC, Carried: mergeDu}, {Repository: cat, Commit: mergeDu}}
w.h.WalkMoved(w.ctl.put(walk))
w.settleAll()
idDu := IDOf(cat, headDu)
lines := w.h.WalksWaiting()
if len(lines) != 1 || lines[0].Walk != "plan-9" || lines[0].State != Held || lines[0].ID != idDu ||
!strings.Contains(lines[0].Says, w.delivery(idDu).HeldWhy) || !strings.Contains(lines[0].Says, "mesh-delivery.release") {
t.Fatalf("the waiting walk is said as %+v", lines)
}
// Not by default: `stalled` stays what the self-check raises conditions for.
for _, l := range w.h.Stalled() {
if l.Walk == "plan-9" {
t.Fatalf("stalled lists the waiting walk without being asked: %+v", l)
}
}
// Released, its word is asked: nothing holds it any more.
if _, err := w.h.Release(idDu, "checked by hand", "jochen"); err != nil {
t.Fatal(err)
}
w.h.WalkMoved(w.ctl.walk("plan-9"))
w.settleAll()
if lines := w.h.WalksWaiting(); len(lines) != 0 {
t.Fatalf("a walk let go is still said waiting: %+v", lines)
}
}
// A walk waiting while a delivery it carries is not at its turn says so, naming the delivery — what was said only
// in this owner's journal (novox/hq issue 529, note 2).
func TestAWaitingWalkSaysWhichDeliveryIsNotAtItsTurn(t *testing.T) {
w := newWorldOver(t, newMemStore(), newFakeController(), newFakeForge(), atNzb)
w.h.mu.Lock()
w.h.waitsWhy = map[string]string{"plan-7": "novox/x@1 (published) carried by it not at its turn"}
w.h.walks["plan-7"] = seenWalk{walk: aWalk("plan-7", "novox/x", "1111", true), at: atNzb}
w.h.mu.Unlock()
lines := w.h.WalksWaiting()
if len(lines) != 1 || lines[0].Walk != "plan-7" || !strings.Contains(lines[0].Says, "not at its turn") {
t.Fatalf("the waiting walk is said as %+v", lines)
}
}
@@ -268,8 +268,17 @@ func tools(h *Holder, l *listening) []stdio.Tool {
"and what healer H2 may do once it has passed; and the machine steps' table.",
Run: func(map[string]any) (any, error) { return TableText(), nil }},
{Name: seat + "stalled",
Description: "Every delivery held past its state's bound, with the transition the table lets healer H2 take.",
Run: func(map[string]any) (any, error) { return h.Stalled(), nil }},
Description: "Every delivery held past its state's bound, with the transition the table lets healer H2 take. " +
"With walks: true, instead each walk waiting for this owner's word and what holds it — a held delivery, " +
"its reason and the verb that releases it, or the deliveries not at their turn (novox/hq issue 529).",
Input: map[string]any{"type": "object", "properties": map[string]any{
"walks": map[string]any{"type": "boolean", "description": "the waiting walks and what holds each, instead"}}},
Run: func(a map[string]any) (any, error) {
if walks, _ := a["walks"].(bool); walks {
return h.WalksWaiting(), nil
}
return h.Stalled(), nil
}},
{Name: seat + "recheck",
Description: "Check a rejected or ready delivery again: its merge check statuses put back to pending on the forge, " +
"so nothing merges on the old verdict; proposed again, and its check asked. With why.",
@@ -635,6 +635,18 @@ func (h *Holder) Status(listening string) map[string]any {
out := map[string]any{"deliveries": byState, "groups": len(h.groups), "owed": owed, "unkept": len(h.dirty),
"walks-unmatched": len(h.unmatched), "events": listening, "started": h.started.Format(time.RFC3339),
"seat-on-record": h.held}
// Each held delivery by name, with why and the verb that releases it (novox/hq issue 529): a count alone
// pointed a reader at nothing.
var held []string
for _, d := range h.deliveries {
if d.State == Held {
held = append(held, HeldLine(d))
}
}
if len(held) > 0 {
sort.Strings(held)
out["held"] = held
}
if !h.reconciled.IsZero() {
out["walks-read"] = h.reconciled.Format(time.RFC3339)
}
@@ -743,3 +755,17 @@ func withoutKinds(owed []Effect, kinds ...string) []Effect {
}
return out
}
// HeldLine says a held delivery in one line: which, since when, why, its walk when one waits, and the verb that
// releases it (novox/hq issue 529).
func HeldLine(d *Delivery) string {
why := d.HeldWhy
if why == "" {
why = lastWhyOf(d)
}
s := fmt.Sprintf("%s held since %s: %s", d.ID, d.Since.UTC().Format(time.RFC3339), why)
if d.Walk != nil && d.Walk.Waits {
s += "; its walk " + d.Walk.ID + " waits for it"
}
return s + " — a person decides: `mesh-delivery.release` (id " + d.ID + ", why) lets it go, `mesh-delivery.stop` ends it"
}