From 9d4d9d5cd2e2339d01273b1b786cae6d18d247e2 Mon Sep 17 00:00:00 2001 From: jochen Date: Sun, 11 Oct 2026 04:30:28 +0200 Subject: [PATCH] mesh-issues: say in reserve's answer when hq could not be read, and grant the import its records calls (review of #204) A number handed out unchecked against hq is a failure that must be loud, so the answer carries a warning, not only the log. The import already calls records_status and records_read; invokes now names them. --- modules/mesh-issues/README.md | 3 +- .../cmd/mesh-issues/manifest_test.go | 4 +- .../mesh-issues/cmd/mesh-issues/reserve.go | 38 ++++++++++++------- .../cmd/mesh-issues/reserve_test.go | 13 ++++++- modules/mesh-issues/module.json | 4 +- 5 files changed, 43 insertions(+), 19 deletions(-) diff --git a/modules/mesh-issues/README.md b/modules/mesh-issues/README.md index a3b056ec..22597ceb 100644 --- a/modules/mesh-issues/README.md +++ b/modules/mesh-issues/README.md @@ -209,7 +209,8 @@ sessions writing a record at once never take one number. reserve of a kind is refused and nothing is spent. - **Never at or below a number hq holds**: each reserve reads hq again, and a counter that stands below the highest number there (one taken by hand) is raised to it first, written only at the revision read. When - hq cannot be read once the counter has started, the counter is used as it stands. + hq cannot be read once the counter has started, the counter is used as it stands, and the answer's + `warning` says the number was not checked against hq. - **Open branches are the floor's job.** The tracker reads hq's checkout of `main` and cannot list a folder on another branch with the forge tools it may call; a number taken on an open branch and not yet merged is covered by the floor. Before the first reserve of a kind, the operator raises `decision-floor` or diff --git a/modules/mesh-issues/cmd/mesh-issues/manifest_test.go b/modules/mesh-issues/cmd/mesh-issues/manifest_test.go index 6c83796b..96ded3bf 100644 --- a/modules/mesh-issues/cmd/mesh-issues/manifest_test.go +++ b/modules/mesh-issues/cmd/mesh-issues/manifest_test.go @@ -97,10 +97,10 @@ func TestItDeclaresAndHoldsTheIssueTrackerSeatOnceForTheMesh(t *testing.T) { t.Fatalf("state %+v", d) } // The duplicate check hears the tracker's own opens and asks the agent module on its node (ADR 0278 §1, §5); - // a reservation reads what numbers the record holds through the records module (issue 424). + // the import and a reservation read the record through the records module (issue 424). if !reflect.DeepEqual(m.Consumes, []string{"mesh-issues." + EventOpened, "mesh-issues." + EventMoved, "mesh-delivery.transition"}) || !reflect.DeepEqual(m.Invokes, []string{"claude-code.claude_code_judge", "seat:mesh-delivery.checks", "seat:operator-channel.notify", - "records.records_list"}) { + "records.records_status", "records.records_list", "records.records_read"}) { t.Fatalf("consumes %v, invokes %v", m.Consumes, m.Invokes) } if len(m.Build.Artifacts) != 1 || m.Build.Artifacts[0].From != "cmd/mesh-issues" || diff --git a/modules/mesh-issues/cmd/mesh-issues/reserve.go b/modules/mesh-issues/cmd/mesh-issues/reserve.go index 60d19a12..0bb57f93 100644 --- a/modules/mesh-issues/cmd/mesh-issues/reserve.go +++ b/modules/mesh-issues/cmd/mesh-issues/reserve.go @@ -117,7 +117,7 @@ func (t *Tracker) Reserve(a ReserveAsk) (*Reserved, error) { records = busRecords{} } highest, rerr := highestIn(records, seq) - n, err := t.nextOf(seq, highest, rerr) + n, unread, err := t.nextOf(seq, highest, rerr) if err != nil { return nil, err } @@ -126,21 +126,29 @@ func (t *Tracker) Reserve(a ReserveAsk) (*Reserved, error) { return nil, err } out := &Reserved{Kind: kind, Number: n, Prefix: prefixOf(kind, n), For: purpose, By: r.By, At: r.At, - Said: fmt.Sprintf("Reserved %s number %s; it is spent, whether or not its record lands.", kind, prefixOf(kind, n))} + Said: fmt.Sprintf("Reserved %s number %s; it is spent, whether or not its record lands.", kind, prefixOf(kind, n)), + Warning: unread} body := map[string]any{"kind": kind, "number": n, "prefix": out.Prefix, "by": r.By, "at": r.At} if purpose != "" { body["for"] = purpose } if err := t.Emit(EventReserved, body); err != nil { t.Logf("[mesh-issues] %s %s was reserved and its reserved event could not be said: %v", kind, out.Prefix, err) - out.Warning = fmt.Sprintf("the reservation is kept, and its reserved event could not be said on the bus (%v): "+ - "what watches the events did not hear of it", err) + out.Warning = joinWarnings(out.Warning, fmt.Sprintf("the reservation is kept, and its reserved event could not be said on the bus (%v): "+ + "what watches the events did not hear of it", err)) } else { out.Announced = true } return out, nil } +func joinWarnings(a, b string) string { + if a == "" { + return b + } + return a + "; and " + b +} + // highestIn is the highest number the record holds in a sequence's folder, sub-folders and documents alike. func highestIn(records Records, seq sequence) (uint64, error) { folders, docs, err := records.List(seq.folder) @@ -161,39 +169,43 @@ func highestIn(records Records, seq sequence) (uint64, error) { // nextOf takes a sequence's next number. A counter not yet started is started once, above the highest the // record holds, the operator's floor and every reservation kept; it is never started without the record // read. A started counter behind the record (a number taken by hand) is raised to it first; a record that -// cannot be read then leaves the counter as it is. -func (t *Tracker) nextOf(seq sequence, highest uint64, recordErr error) (uint64, error) { +// cannot be read then leaves the counter as it is, and the warning answered says so. +func (t *Tracker) nextOf(seq sequence, highest uint64, recordErr error) (uint64, string, error) { + warning := "" e, err := t.Store.Get(CountersState, seq.kind) if err != nil { - return 0, fmt.Errorf("the %s counter cannot be read (%v); no number was taken", seq.kind, err) + return 0, "", fmt.Errorf("the %s counter cannot be read (%v); no number was taken", seq.kind, err) } if e == nil { if recordErr != nil { - return 0, fmt.Errorf("the %s counter has not started, and what numbers hq's %s holds cannot be read through "+ + return 0, "", fmt.Errorf("the %s counter has not started, and what numbers hq's %s holds cannot be read through "+ "the records module (%v): it starts above them, so no number was taken. Ask again when the records "+ "module answers", seq.kind, seq.folder, recordErr) } if err := t.startSequence(seq, highest); err != nil { - return 0, err + return 0, "", err } } else if recordErr != nil { t.Logf("[mesh-issues] the record's %s could not be read (%v); the %s counter is used as it stands", seq.folder, recordErr, seq.kind) + warning = fmt.Sprintf("what numbers hq's %s holds could not be read through the records module (%v): the "+ + "number comes from the %s counter as it stands, unchecked against hq. Check that hq holds no %s at or above it", + seq.folder, recordErr, seq.kind, seq.kind) } else if err := t.raise(seq.kind, highest); err != nil { - return 0, err + return 0, "", err } for round := 0; round < counterRounds; round++ { n, err := t.Store.Increment(CountersState, seq.kind, counterAttempts) if err == nil { - return n, nil + return n, warning, nil } if !errors.Is(err, stdio.ErrChanged) { t.Logf("[mesh-issues] the %s counter could not be advanced: %v", seq.kind, err) - return 0, fmt.Errorf("the %s counter could not be advanced (%v); nothing was reserved. If the bus took the "+ + return 0, "", fmt.Errorf("the %s counter could not be advanced (%v); nothing was reserved. If the bus took the "+ "write before it failed, one number stays unused", seq.kind, err) } t.Sleep(jitter(time.Duration(round+1) * 10 * time.Millisecond)) } - return 0, fmt.Errorf("so many %s numbers are being reserved at this moment that the counter was taken by another "+ + return 0, "", fmt.Errorf("so many %s numbers are being reserved at this moment that the counter was taken by another "+ "caller %d times; no number was taken. Reserve again", seq.kind, counterRounds*counterAttempts) } diff --git a/modules/mesh-issues/cmd/mesh-issues/reserve_test.go b/modules/mesh-issues/cmd/mesh-issues/reserve_test.go index 3dc84af2..1597b593 100644 --- a/modules/mesh-issues/cmd/mesh-issues/reserve_test.go +++ b/modules/mesh-issues/cmd/mesh-issues/reserve_test.go @@ -222,8 +222,17 @@ func TestAnUnreadableRecordRefusesTheFirstNumberAndNotTheNext(t *testing.T) { r.err = nil h.reserve(t, KindDecision) // 292 r.err = errors.New("records did not answer") - if got := h.reserve(t, KindDecision); got != 293 { - t.Fatalf("got %d, want 293 from the started counter", got) + out, err := h.Reserve(ReserveAsk{Kind: KindDecision}) + if err != nil || out.Number != 293 { + t.Fatalf("got %+v (%v), want 293 from the started counter", out, err) + } + // Failure is loud: the answer says the number was not checked against hq. + if !strings.Contains(out.Warning, "records did not answer") || !strings.Contains(out.Warning, "unchecked") { + t.Fatalf("a number unchecked against hq answered warning %q", out.Warning) + } + r.err = nil + if out, err := h.Reserve(ReserveAsk{Kind: KindDecision}); err != nil || out.Warning != "" { + t.Fatalf("a reserve with hq read answered %+v (%v): no warning expected", out, err) } } diff --git a/modules/mesh-issues/module.json b/modules/mesh-issues/module.json index 6f549df7..6ef3b3bc 100644 --- a/modules/mesh-issues/module.json +++ b/modules/mesh-issues/module.json @@ -61,7 +61,9 @@ "claude-code.claude_code_judge", "seat:mesh-delivery.checks", "seat:operator-channel.notify", - "records.records_list" + "records.records_status", + "records.records_list", + "records.records_read" ], "state": [ {