messenger: keep what a walk held owed in the state until its message was sent, so a restart says a held warning too (issue 538, review 39)
mesh/merge-gate pass: builds messenger → 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 warning held on purpose and lost with the walk's message at a restart was turned
into history and never told. Name the walk in the told line from its own item.
This commit is contained in:
jochen
2026-10-12 13:35:48 +02:00
parent df94be0e60
commit 424fc5a281
5 changed files with 70 additions and 26 deletions
+2 -2
View File
@@ -186,8 +186,8 @@ The controller's `plan-moved` says each walk's start and end: `walk`, `phase` (`
- **The end is one message:** the outcome, the modules and machines reached, how long; each held message still
open, one line each; the ones that cleared counted. Delivered with nothing held and nothing open: one line —
and nothing on a kind listed in the setting `only-what-needs-me`. What it carries is marked told on a kind
only once the message was sent there: a message lost with a restart before it was sent leaves them untold, and
the reading of the open conditions says them as any other.
only once the message was sent there. Until then each is kept owed in the state (`owed`): a message lost with a
restart before it was sent leaves them untold and owed, and the restarted router says each on its own.
- **The bound:** at its start plus `bound_seconds` (900 when not given), what is held and still open is said,
with "The walk … is still running, since HH:MM", and the cleared counted; the walk holds nothing more. A walk
whose end never comes is forgotten a day after it started. A start heard after its end opens nothing: the
+16 -8
View File
@@ -200,14 +200,17 @@ type Record struct {
// ClearHeld is what was waiting to be said of it when it cleared (a reminder, an escalation): said after
// all if it comes back before its clearing was said. Blips counts those comebacks, never said, so a
// condition that flaps is known as one and its message's "again, Nth time" counts only what was said.
ClearHeld string `json:"clear_held,omitempty"`
Blips int `json:"blips,omitempty"`
Reminded bool `json:"reminded,omitempty"`
Pending string `json:"pending,omitempty"` // what could not be said yet: raised, reminder, …
Folded bool `json:"folded,omitempty"` // held by the cap
Cleared time.Time `json:"cleared,omitempty"`
Count int `json:"count"`
Refused string `json:"refused,omitempty"`
ClearHeld string `json:"clear_held,omitempty"`
Blips int `json:"blips,omitempty"`
Reminded bool `json:"reminded,omitempty"`
Pending string `json:"pending,omitempty"` // what could not be said yet: raised, reminder, …
// Owed is what a walk held of it and has not told yet (ADR 0308 §4, §5): kept in the state until a message
// carrying it was sent, so a router that restarts before says it on its own (issue 538), never history.
Owed string `json:"owed,omitempty"`
Folded bool `json:"folded,omitempty"` // held by the cap
Cleared time.Time `json:"cleared,omitempty"`
Count int `json:"count"`
Refused string `json:"refused,omitempty"`
// History: known from the state or from an old event, and never said — not news.
History bool `json:"history,omitempty"`
// Seen is when this holder last took word of it; the open set read after it is authoritative.
@@ -407,6 +410,11 @@ func (h *Holder) Load() error {
r.History = true
}
}
if r.Owed != "" && r.Cleared.IsZero() {
// Held from the operator on purpose and not told yet: said on its own, as the next tick tries what is
// unsent (issue 538). What the operator could have heard is history; this they could not.
r.Pending, r.History = r.Owed, false
}
h.open[r.Key] = &r
}
if sent, err := h.Store.Recent(); err == nil {
+11 -6
View File
@@ -430,6 +430,7 @@ func (h *Holder) sendEntries(ch Channel, entries []entry, now time.Time) bool {
}
r.Pending, r.Folded, r.History = "", false, false
if e.it.What != "cleared" {
r.Owed = ""
if r.Sent == nil {
r.Sent = map[string]string{}
}
@@ -445,7 +446,11 @@ func (h *Holder) sendEntries(ch Channel, entries []entry, now time.Time) bool {
r.LastSaid = now
}
}
var told []*Record
type carried struct {
r *Record
walk string
}
var told []carried
for _, e := range entries {
for _, key := range e.it.Carries {
r := h.open[key]
@@ -461,18 +466,18 @@ func (h *Holder) sendEntries(ch Channel, entries []entry, now time.Time) bool {
if r.FirstSent.IsZero() {
r.FirstSent = now
}
r.LastSaid, r.Pending, r.History = now, "", false
r.LastSaid, r.Pending, r.History, r.Owed = now, "", false, ""
if !r.Cleared.IsZero() && !r.ClearUnsaid {
// It cleared while the message saying it open waited: its clearing is said too.
r.ClearUnsaid = true
}
told = append(told, r)
told = append(told, carried{r, strings.TrimPrefix(e.it.Key, "walk.")})
}
}
h.mu.Unlock()
for _, r := range told {
h.record(Sent{At: now, Channel: name, Key: r.Key, What: "released", Outcome: "told in the message of walk " + strings.TrimPrefix(entries[0].it.Key, "walk.")})
h.persist(r)
for _, c := range told {
h.record(Sent{At: now, Channel: name, Key: c.r.Key, What: "released", Outcome: "told in the message of walk " + c.walk})
h.persist(c.r)
}
key, what := "(digest)", fmt.Sprintf("digest of %d", len(entries))
if len(entries) == 1 {
+17 -5
View File
@@ -580,6 +580,17 @@ func (h *Holder) tellWalk(w *Walk, m Message, open []*Record, whats map[string]s
m = Message{Title: "A delivery ended", Body: "What it reached could not be said here: the delivery's walks say it.", Urgent: m.Urgent}
alone, open = open, nil
}
// Owed until a message carrying it was sent, in the state: a router that restarts first says it on its own
// (issue 538), whichever path below it takes.
h.mu.Lock()
for _, r := range append(append([]*Record(nil), open...), alone...) {
say := whats[r.Key]
if say == "" {
say = "raised"
}
r.Owed = say
}
h.mu.Unlock()
var to []Channel
if m.Urgent {
for _, ch := range h.channels() {
@@ -625,7 +636,8 @@ func (h *Holder) tellWalk(w *Walk, m Message, open []*Record, whats map[string]s
sev = Urgent
}
// What it carries is marked told on a channel once the message was sent there (outbox.go), not now: a
// message lost with a restart before it was sent leaves them untold, to be said as any other (issue 538 A).
// message lost with a restart before it was sent leaves them untold and owed, and the restarted router says
// each on its own (issue 538 A).
var carries []string
for _, r := range open {
carries = append(carries, r.Key)
@@ -635,11 +647,11 @@ func (h *Holder) tellWalk(w *Walk, m Message, open []*Record, whats map[string]s
h.enqueue(ch.Name(), item{Key: "walk." + w.ID, What: WalkEndWhat, Severity: sev, At: now, Msg: &msg, Carries: carries})
}
for _, r := range open {
if len(kept) == 0 {
break
if len(kept) > 0 {
h.record(Sent{At: now, Channel: "-", Key: r.Key, What: "released",
Outcome: "to be told in the message of walk " + w.ID + " " + what})
}
h.record(Sent{At: now, Channel: "-", Key: r.Key, What: "released",
Outcome: "to be told in the message of walk " + w.ID + " " + what})
h.persist(r) // owed, in the state, until the message was sent
}
h.flush(now, false)
for _, r := range alone {
+24 -5
View File
@@ -623,13 +623,18 @@ func TestWhatAWalksPlainEndCannotCarryIsSaidOnItsOwn(t *testing.T) {
// Issue 538 A: what a walk held is marked told only once the message carrying it was sent. When that message
// could not be sent and the router restarts before its retry, the message is gone with it: what it held is not
// marked told, and the reading of the open conditions says it at once, not its reminder hours later.
// marked told, it stays owed in the state, and the restarted router says it — an urgent one and a warning alike,
// once each, never left as history.
func TestAHeldMessageIsToldOnlyOnceTheWalksMessageWasSent(t *testing.T) {
for _, then := range []string{"sent", "restart"} {
for _, then := range []string{"sent", "restart urgent", "restart warning"} {
t.Run(then, func(t *testing.T) {
h, tg, dt, c, st := newHolder(t)
sev := Urgent
if then == "restart warning" {
sev = Warning
}
walk(t, h, "plan-h", WalkStarted, "", c.now(), 1200, novox("mesh-tools"))
k := cond("machine.novox.tools-silent", Urgent, "the tools on novox are silent")
k := cond("machine.novox.tools-silent", sev, "the tools on novox are silent")
k.Raised = c.now()
h.Condition(EventRaised, k)
runFor(h, c, time.Minute)
@@ -649,9 +654,13 @@ func TestAHeldMessageIsToldOnlyOnceTheWalksMessageWasSent(t *testing.T) {
if !heard("Still open after it:\n- Urgent: "+headlineOf(k.Key), tg, dt) || saidOn(tg, dt) != 2 {
t.Fatalf("not told once in the walk's message: telegram %+v, desk %+v", tg.sends, dt.sends)
}
if r := h.open[k.Key]; r == nil || !r.told() || r.Sent["telegram"] != "" || r.Sent["desktop"] != "" {
if r := h.open[k.Key]; r == nil || !r.told() || r.Owed != "" || r.Sent["telegram"] != "" || r.Sent["desktop"] != "" {
t.Fatalf("not marked told on both channels once sent: %+v", r)
}
runFor(h, c, 10*time.Minute)
if saidOn(tg, dt) != 2 {
t.Fatalf("said again after the walk's message: telegram %+v, desk %+v", tg.sends, dt.sends)
}
return
}
again := &Holder{Away: tg, Desk: dt, Store: st, Now: c.now, Logf: t.Logf}
@@ -660,10 +669,20 @@ func TestAHeldMessageIsToldOnlyOnceTheWalksMessageWasSent(t *testing.T) {
}
again.Start()
again.Sync([]Condition{k}, c.now())
runFor(again, c, time.Minute)
runFor(again, c, 5*time.Minute)
if !heard(headlineOf(k.Key), tg, dt) {
t.Fatalf("what the lost message carried was not said after the restart: %v", outcomes(again, k.Key))
}
said := saidOn(tg, dt)
if r := again.open[k.Key]; r == nil || !r.told() || r.Owed != "" || r.Pending != "" {
t.Fatalf("said, and still owed or not marked told: %+v", r)
}
runFor(again, c, 20*time.Minute) // within its reminder's bound: nothing more is due
again.Sync([]Condition{k}, c.now())
runFor(again, c, time.Minute)
if saidOn(tg, dt) != said {
t.Fatalf("said again: telegram %+v, desk %+v", tg.sends, dt.sends)
}
})
}
}