Heal what is known, under a brake, and say every repair (hq to-be 45 Phase 3)

Research 031 counted the repairs people made by hand: a push to unstick a
plan waiting on a report, a controller restarted to make an object again, a
plan closed, a consumer re-made from now. Each was the ordinary path taken
again by someone who noticed. The healer registry makes each a registered
response to one condition kind, with a budget, a settle and its event:

- H1 sent-not-reported: ask the machine's node-engine to report again
  (mesh.node.<n>.ask.report); if it does not report what it was sent, send
  it again, never moving a build a policy or a plan holds back
- H2 stalled: close a plan whose wait is superseded or finished
- H3 holder-silent / consumer-lost: the send's own assertion of the bus's
  objects (issue 208's note)
- H4 consumer-behind: consumer-reset, only for a consumer the stream table
  marks resettable (the controller's own events consumer)
- H5 is the identity provider's own repair (ADR 0224 §5), registered only

Success is the observation clearing the condition, never the healer; a spent
budget hands the condition to the operator, urgent, with what was tried, and
no healer touches it again. Every act is begun in the store before it is made
(migration 0070), kept in the condition's tried as "healer Hn" and said as
the seat event healer-acted; a heal is never a hand act. More than twelve acts
in an hour stop every healer until an hour after the last, said urgently.
Only the lease holder heals.

S15 is live: a cause repaired by hand twice in a fortnight raises
healer-wanted, naming the healer that was not enough where one exists. D6's
far-behind finding has its own kind, consumer-behind. Nodes are granted the
question; the controller's grant gains healer-acted (genesis lock in
mesh-host). `healers` lists the registry, the acts and the brake; status
counts the week's heals.
This commit is contained in:
jochen
2026-10-06 14:26:26 +02:00
parent 20c147ffdb
commit 751e39186c
31 changed files with 2154 additions and 21 deletions
+14 -1
View File
@@ -79,13 +79,21 @@ type Evidence struct {
Said string `json:"said"`
}
// Attempt is one healer's try at a condition (to-be 45 §7; written from Phase 3).
// Attempt is one healer's try at a condition (to-be 45 §7): when, what it did, what came of it, and
// which healer — so a person reading `conditions show` sees the mesh repairing itself, by whom.
type Attempt struct {
At time.Time `json:"at"`
What string `json:"what"`
Outcome string `json:"outcome"`
// By is the healer, as a person reads it: `healer H1`. Never a person: an act by hand is the
// hand-act log's, not a condition's attempt.
By string `json:"by"`
}
// KeptAttempts is how many attempts a condition keeps, newest last: a healer's budget is a few, and
// a condition reopened again and again carries its tries forward only so far.
const KeptAttempts = 10
// Silence is a person saying they know: no messages until it ends (to-be 45 §2). Recorded as a hand
// act; the condition stays open, and `status` still says it.
type Silence struct {
@@ -139,6 +147,11 @@ func (c Condition) SilencedAt(now time.Time) bool {
return c.Silenced != nil && now.Before(c.Silenced.Until)
}
// Escalated says a healer tried and its budget is spent: the operator resolves it now (to-be 45 §2,
// "budget spent ──► OPEN, resolver: operator, severity: urgent"). An observation does not lower its
// severity again: the watchdog that sees it every half minute would otherwise undo the escalation.
func (c Condition) Escalated() bool { return c.Resolver == ResolverOperator && len(c.Tried) > 0 }
// Show is the verb that shows more about a condition, as a message carries it.
func (c Condition) Show() string { return "mesh-controller.conditions key=" + c.Key }
+97 -4
View File
@@ -76,6 +76,9 @@ type clearing struct {
at time.Time
count int
silenced *Silence
// tried is what healers tried before it cleared: a reopening is the same fault, and what was
// tried on it is still what was tried.
tried []Attempt
}
// Options are what a Keeper is made with.
@@ -113,7 +116,8 @@ func NewKeeper(ctx context.Context, o Options) *Keeper {
if recent, err := k.history.Since(ctx, k.now().Add(-ReopenWithin)); err == nil {
for _, e := range recent {
if e.Change == ChangeCleared {
k.cleared[e.Key] = clearing{at: e.At, count: e.Condition.Count, silenced: e.Condition.Silenced}
k.cleared[e.Key] = clearing{at: e.At, count: e.Condition.Count, silenced: e.Condition.Silenced,
tried: e.Condition.Tried}
}
}
} else {
@@ -191,6 +195,7 @@ func (k *Keeper) Observe(ctx context.Context, o Observation) (Condition, error)
if before.silenced != nil && now.Before(before.silenced.Until) {
c.Silenced = before.silenced
}
c.Tried = before.tried
}
k.mu.Unlock()
if err := k.stamp(&c); err != nil {
@@ -216,7 +221,8 @@ func (k *Keeper) Observe(ctx context.Context, o Observation) (Condition, error)
return Condition{}, fmt.Errorf("the condition %s on the bus cannot be read: %w", key, err)
}
var changes []Event
if o.Severity != c.Severity {
// A healer's budget spent made it urgent; the watchdog seeing it again does not undo that.
if o.Severity != c.Severity && !(c.Escalated() && o.Severity != Urgent) {
changes = append(changes, Event{Change: ChangeSeverity, Was: string(c.Severity)})
c.Severity = o.Severity
}
@@ -224,7 +230,9 @@ func (k *Keeper) Observe(ctx context.Context, o Observation) (Condition, error)
changes = append(changes, Event{Change: ChangeResolver, Was: c.Resolver})
c.Resolver = r
}
c.Summary, c.Source, c.LastObserved = o.Summary, o.Source, now
// The kind as the source says it now: a source that gave the same key a kind of its own since
// (a probe's finding split out for a healer) is read by that kind from its next observation.
c.Kind, c.Summary, c.Source, c.LastObserved = o.Kind, o.Summary, o.Source, now
if o.Machine != "" {
c.Subject.Machine = o.Machine
}
@@ -282,7 +290,7 @@ func (k *Keeper) Clear(ctx context.Context, key, why string) (bool, error) {
}
now := k.now().UTC()
k.mu.Lock()
k.cleared[key] = clearing{at: now, count: c.Count, silenced: c.Silenced}
k.cleared[key] = clearing{at: now, count: c.Count, silenced: c.Silenced, tried: c.Tried}
k.mu.Unlock()
k.tell(Event{Condition: c, At: now, Change: ChangeCleared, Why: why, Cleared: &now})
return true, nil
@@ -290,6 +298,91 @@ func (k *Keeper) Clear(ctx context.Context, key, why string) (bool, error) {
return false, fmt.Errorf("the condition %s kept moving under this clearing; %d tries", key, tries)
}
// Tried records a healer's attempt on an open condition (to-be 45 §7) and makes the healer its
// resolver: said as `condition-changed` when the resolver changes, kept in `tried` either way. **It
// never clears the condition**: a repair that worked is seen by the observation that raised it, which
// clears it — a healer marking its own work done would be a second opinion of the fact. False when no
// condition is open under the key: it cleared meanwhile, and there is nothing to record against.
func (k *Keeper) Tried(ctx context.Context, key string, a Attempt, resolver string) (Condition, bool, error) {
return k.amend(ctx, key, func(c *Condition, now time.Time) []Event {
c.Tried = appendAttempt(c.Tried, a, now)
var changes []Event
if resolver != "" && resolver != c.Resolver && !c.Escalated() {
changes = append(changes, Event{Change: ChangeResolver, Was: c.Resolver, Why: a.Outcome})
c.Resolver = resolver
}
return changes
})
}
// Escalate is a healer's budget spent, or a repair it may not make (to-be 45 §2, §7): the attempt that
// says so is kept in `tried`, the resolver becomes the operator and the severity urgent, each said as
// `condition-changed`. Observation still clears it when the fault goes; nothing else does.
func (k *Keeper) Escalate(ctx context.Context, key string, a Attempt) (Condition, bool, error) {
return k.amend(ctx, key, func(c *Condition, now time.Time) []Event {
c.Tried = appendAttempt(c.Tried, a, now)
var changes []Event
if c.Severity != Urgent {
changes = append(changes, Event{Change: ChangeSeverity, Was: string(c.Severity), Why: a.Outcome})
c.Severity = Urgent
}
if c.Resolver != ResolverOperator {
changes = append(changes, Event{Change: ChangeResolver, Was: c.Resolver, Why: a.Outcome})
c.Resolver = ResolverOperator
}
return changes
})
}
// appendAttempt adds an attempt, stamped when it has no time, keeping the newest KeptAttempts.
func appendAttempt(tried []Attempt, a Attempt, now time.Time) []Attempt {
if a.At.IsZero() {
a.At = now
}
tried = append(tried, a)
if len(tried) > KeptAttempts {
tried = tried[len(tried)-KeptAttempts:]
}
return tried
}
// amend changes one open condition by compare-and-set and says what changed. False when none is open.
func (k *Keeper) amend(ctx context.Context, key string, change func(*Condition, time.Time) []Event) (Condition, bool, error) {
for i := 0; i < tries; i++ {
entry, found, err := k.store.Get(ctx, key)
if err != nil {
return Condition{}, false, fmt.Errorf("reading the condition %s: %w", key, err)
}
if !found {
return Condition{}, false, nil
}
var c Condition
if err := json.Unmarshal(entry.Value, &c); err != nil {
return Condition{}, false, fmt.Errorf("the condition %s on the bus cannot be read: %w", key, err)
}
now := k.now().UTC()
changes := change(&c, now)
if err := k.stamp(&c); err != nil {
return Condition{}, false, err
}
body, err := json.Marshal(c)
if err != nil {
return Condition{}, false, err
}
if err := k.store.Update(ctx, key, body, entry.Revision); errors.Is(err, ErrMoved) {
continue
} else if err != nil {
return Condition{}, false, fmt.Errorf("writing the condition %s: %w", key, err)
}
for _, e := range changes {
e.At, e.Condition = now, c
k.tell(e)
}
return c, true, nil
}
return Condition{}, false, fmt.Errorf("the condition %s kept moving under this write; %d tries", key, tries)
}
// Reconcile is one source's whole observation: every condition it observes is observed, and every
// condition it raised before and no longer observes is cleared — the observation says it is
// resolved. A source that could not observe must not call this: an empty observation clears all it
+105
View File
@@ -0,0 +1,105 @@
package conditions
import (
"testing"
"time"
)
// **A healer's attempt is said and kept, and never clears the condition** (to-be 45 §7): the resolver
// becomes the healer, `tried` says what it did, and only an observation clears it.
func TestAHealersAttemptIsKeptAndClearsNothing(t *testing.T) {
k, _, told, c := keeper(t)
ctx := t.Context()
if _, err := k.Observe(ctx, silent("ace")); err != nil {
t.Fatal(err)
}
held, open, err := k.Tried(ctx, "machine.ace.silent", Attempt{What: "asked ace to report", Outcome: "acted",
By: "healer H1"}, ResolverHealer("H1"))
if err != nil || !open {
t.Fatalf("tried: %v %v", open, err)
}
if held.Resolver != "healer:H1" || len(held.Tried) != 1 || held.Tried[0].By != "healer H1" || held.Tried[0].At.IsZero() {
t.Fatalf("after one attempt: %+v", held)
}
said := settled(t, told, 2)
if said[1].Event != EventChanged || said[1].Change != ChangeResolver || said[1].Was != ResolverSelf {
t.Errorf("the healer taking it is not said as a change of resolver: %+v", said[1])
}
// A second attempt by the same healer is kept and says nothing new.
if _, _, err := k.Tried(ctx, "machine.ace.silent", Attempt{What: "sent again", Outcome: "acted", By: "healer H1"},
ResolverHealer("H1")); err != nil {
t.Fatal(err)
}
if got, _, _ := k.Get(ctx, "machine.ace.silent"); len(got.Tried) != 2 {
t.Errorf("tried %+v", got.Tried)
}
time.Sleep(50 * time.Millisecond)
if n := len(told.Said()); n != 2 {
t.Errorf("a second attempt said %d events in all, want 2", n)
}
// Nothing open: nothing recorded, and no error.
if _, open, err := k.Tried(ctx, "machine.nobody.silent", Attempt{What: "x"}, ResolverHealer("H1")); open || err != nil {
t.Errorf("an attempt on nothing open: %v %v", open, err)
}
c.pass(time.Minute)
}
// **A spent budget is the operator's, urgent, and the watchdog seeing it again does not undo that.**
func TestAnEscalationHoldsAgainstTheNextObservation(t *testing.T) {
k, _, told, _ := keeper(t)
ctx := t.Context()
if _, err := k.Observe(ctx, silent("ace")); err != nil {
t.Fatal(err)
}
if _, _, err := k.Tried(ctx, "machine.ace.silent", Attempt{What: "asked", Outcome: "acted", By: "healer H1"},
ResolverHealer("H1")); err != nil {
t.Fatal(err)
}
held, _, err := k.Escalate(ctx, "machine.ace.silent", Attempt{What: "budget spent", Outcome: "escalated", By: "healer H1"})
if err != nil {
t.Fatal(err)
}
if held.Severity != Urgent || held.Resolver != ResolverOperator || !held.Escalated() {
t.Fatalf("escalated: %+v", held)
}
said := settled(t, told, 4)
if said[2].Change != ChangeSeverity || said[3].Change != ChangeResolver || said[3].Was != "healer:H1" {
t.Errorf("the escalation is not said as severity then resolver: %+v %+v", said[2], said[3])
}
again, err := k.Observe(ctx, silent("ace")) // a warning, as the watchdog says it
if err != nil {
t.Fatal(err)
}
if again.Severity != Urgent || again.Resolver != ResolverOperator {
t.Errorf("the next observation undid the escalation: %+v", again)
}
// A healer trying later does not take it back from the operator.
after, _, _ := k.Tried(ctx, "machine.ace.silent", Attempt{What: "x", By: "healer H1"}, ResolverHealer("H1"))
if after.Resolver != ResolverOperator {
t.Errorf("a later attempt took the condition back from the operator: %s", after.Resolver)
}
}
// **What was tried is carried into a reopening**: the same fault again within ten minutes is the
// same condition, and what the healers tried on it is still what they tried.
func TestAReopeningCarriesWhatWasTried(t *testing.T) {
k, _, _, c := keeper(t)
ctx := t.Context()
if _, err := k.Observe(ctx, silent("ace")); err != nil {
t.Fatal(err)
}
if _, _, err := k.Tried(ctx, "machine.ace.silent", Attempt{What: "asked", By: "healer H1"}, ResolverHealer("H1")); err != nil {
t.Fatal(err)
}
if _, err := k.Clear(ctx, "machine.ace.silent", "heard again"); err != nil {
t.Fatal(err)
}
c.pass(5 * time.Minute)
again, err := k.Observe(ctx, silent("ace"))
if err != nil {
t.Fatal(err)
}
if again.Count != 2 || len(again.Tried) != 1 || again.Tried[0].What != "asked" {
t.Errorf("reopened without what was tried: %+v", again)
}
}