HEAL1 (attempt-changed): the healers say each attempt they write, and every attempt kept at their start

The seat mesh-healers' attempt-changed carries the attempt whole with until (a week from its write), on the subject keyed by
the condition and the attempt, each escaped as a key's name is. The event is the seat definition's; the manifest's emits stays
empty as for healer-acted.

Builds: design 47 G §541, §665

Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
2026-11-11 22:54:18 +01:00
parent 265d3b83b2
commit 22b50c88a9
2 changed files with 72 additions and 1 deletions
@@ -30,6 +30,7 @@ import (
handacts "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshhandacts"
meshhealers "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshhealers"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/keys"
"git.novox.be/novox/mesh-sdk/go/views"
)
@@ -287,6 +288,7 @@ func (h *Healers) act(row Healer, c Condition, budget string, spent int, by stri
}
return "", fmt.Errorf("healer %s did not act on %s: its attempt could not be begun: %w", row.ID, c.Key, err)
}
h.sayAttempt(key, a, begun)
act, said, err := row.repair(h, c)
a.Outcome = OutcomeActed
if err != nil {
@@ -302,6 +304,8 @@ func (h *Healers) act(row Healer, c Condition, budget string, spent int, by stri
a.Said, a.Finished = said+" ("+window+")", &finished
if err := h.Store.PutTTL(AttemptsState, key, a, KeptFor); err != nil {
h.Logf("[%s] healer %s %s %s and could not record what came of it: %v", Module, row.ID, a.Outcome, c.Key, err)
} else {
h.sayAttempt(key, a, finished)
}
h.tell(Acted{Healer: row.ID, Condition: c.Key, Kind: c.Kind, Act: a.Act, Outcome: a.Outcome, Said: said, Budget: window})
h.wake(budget, begun.Add(row.Settle), c.Key)
@@ -321,14 +325,55 @@ func (h *Healers) escalate(row Healer, c Condition, budget string, spent []Attem
window := fmt.Sprintf("%d of %d within %s", len(spent), row.Budget, row.Window)
a := Attempt{Healer: row.ID, Condition: c.Key, Kind: c.Kind, BudgetKey: budget, Act: "handed the condition to the operator",
Outcome: OutcomeEscalated, Said: said, Begun: now, Finished: &now}
if err := h.Store.CreateTTL(AttemptsState, attemptKey(row.ID, c.Key, now), a, KeptFor); err != nil && !errors.Is(err, ErrExists) {
ekey := attemptKey(row.ID, c.Key, now)
if err := h.Store.CreateTTL(AttemptsState, ekey, a, KeptFor); err != nil && !errors.Is(err, ErrExists) {
return "", fmt.Errorf("healer %s could not record handing %s to the operator, and does not: %w", row.ID, c.Key, err)
} else if err == nil {
h.sayAttempt(ekey, a, now)
}
h.tell(Acted{Healer: row.ID, Condition: c.Key, Kind: c.Kind, Act: "handed to the operator", Outcome: OutcomeEscalated,
Said: said, Budget: window})
return fmt.Sprintf("healer %s handed %s to the operator: %s", row.ID, c.Key, said), nil
}
// sayAttempt says an attempt as the seat's attempt-changed (design 47 G §541, §665): the attempt whole as its key in `attempts`
// holds it, with `until`, the end of that key's lifetime (KeptFor from its write at wrote), on the subject keyed by the
// condition and the attempt, each escaped as a key's name is (§73). Not said is said in the journal: the attempt holds it.
func (h *Healers) sayAttempt(key string, a Attempt, wrote time.Time) {
if h.Publish == nil {
return
}
until := wrote.Add(KeptFor)
e := meshhealers.AttemptChangedEvent{Act: a.Act, Begun: &a.Begun, BudgetKey: a.BudgetKey, Condition: a.Condition, Finished: a.Finished,
Healer: a.Healer, Kind: a.Kind, Outcome: a.Outcome, Said: a.Said, Until: &until}
subject := meshhealers.SubjectAttemptChanged(keys.EscapeName(a.Condition), keys.EscapeName(key))
if err := h.Publish(subject, e, key+"."+strconv.FormatInt(wrote.UnixNano(), 10)); err != nil {
h.Logf("[%s] the attempt %s of %s could NOT be said on the bus: %v", Module, key, a.Condition, err)
}
}
// sayOpen says every attempt still kept, at the healers' start, so a keeper that missed one hears it (design 47 G §541). The
// end of an attempt's lifetime is a week from its last write: its finish, or its beginning while it is not finished.
func (h *Healers) sayOpen() {
names, err := h.Store.Keys(AttemptsState)
if err != nil {
h.Logf("[%s] the attempts kept cannot be read, so none is said again at the start: %v", Module, err)
return
}
for _, key := range names {
raw, found, err := h.Store.Get(AttemptsState, key)
var a Attempt
if err != nil || !found || json.Unmarshal(raw, &a) != nil {
continue
}
wrote := a.Begun
if a.Finished != nil {
wrote = *a.Finished
}
h.sayAttempt(key, a, wrote)
}
}
// tell says a heal as healer-acted. Not said is said in the log: the attempt holds it.
func (h *Healers) tell(e Acted) {
e.Event, e.At, e.By = ActedEvent, h.Now().UTC(), "healer "+e.Healer
@@ -345,6 +390,7 @@ func (h *Healers) tell(e Acted) {
// no expiry left to hear, so each newest act whose settle ended within its window is judged at once. A settle still
// running keeps its key on the bus and needs nothing.
func (h *Healers) Rearm() {
h.sayOpen()
all, err := h.attempts()
if err != nil {
h.Logf("[%s] what the healers did cannot be read, so no ended settle is judged: %v", Module, err)
@@ -16,6 +16,7 @@ import (
meshconditions "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions"
handacts "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshhandacts"
meshhealers "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshhealers"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/views"
)
@@ -136,6 +137,8 @@ type world struct {
said []Acted
// events are the subjects the finding writer said its events on.
events []string
// attempted are the attempt-changed events said: subject, outcome and until.
attempted []string
}
func newWorld() *world {
@@ -157,6 +160,9 @@ func (w *world) fresh() {
if a, ok := body.(Acted); ok && subject == "mesh.seat.mesh-healers.event.healer-acted" {
w.said = append(w.said, a)
}
if a, ok := body.(meshhealers.AttemptChangedEvent); ok {
w.attempted = append(w.attempted, subject+" "+a.Outcome+" "+a.Until.Format(time.RFC3339))
}
return nil
},
}
@@ -232,6 +238,25 @@ func TestTheHealersActWithNoLease(t *testing.T) {
}
}
// Design 47 G §541, §665: each attempt written is said as attempt-changed on the subject keyed by the condition and the attempt,
// escaped as a key's name is, with its until a week from its write; and said again for every attempt kept at the healers' start.
func TestAnAttemptIsSaidAsAttemptChangedAndAgainAtTheStart(t *testing.T) {
w := newWorld()
if err := w.h.Heard(body(lost("records"))); err != nil {
t.Fatal(err)
}
if len(w.attempted) != 2 || !strings.Contains(w.attempted[0], "mesh.seat.mesh-healers.event.attempt-changed.bus") ||
!strings.HasSuffix(w.attempted[0], " acting "+w.now.Add(KeptFor).Format(time.RFC3339)) || !strings.Contains(w.attempted[1], " acted ") {
t.Fatalf("said %v: the begun attempt, then the finished one", w.attempted)
}
w.attempted = nil
w.fresh()
w.h.sayOpen()
if len(w.attempted) != 1 || !strings.Contains(w.attempted[0], " acted ") {
t.Errorf("at the start every attempt kept is said again: %v", w.attempted)
}
}
// An act waits out its settle, then its spent budget hands the condition to the operator once; an escalated condition
// is passed over; a second instance cannot begin an act already begun; the brake stops every healer past twelve acts
// in an hour and lets go an hour after the last (ADR 0231 §21, ADR 0564 §7).