mesh-issues: say in reserve's answer when hq could not be read, and grant the import its records calls (review of #204)
mesh/merge-gate pass: builds mesh-issues → novox; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
mesh/delivery delivered

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.
This commit is contained in:
2026-10-11 04:30:28 +02:00
parent 350a597c47
commit 9d4d9d5cd2
5 changed files with 43 additions and 19 deletions
@@ -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" ||
+25 -13
View File
@@ -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)
}
@@ -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)
}
}