diff --git a/modules/mesh-conditions/cmd/mesh-conditions/compare.go b/modules/mesh-conditions/cmd/mesh-conditions/compare.go index b73e0a85..e1583e7a 100644 --- a/modules/mesh-conditions/cmd/mesh-conditions/compare.go +++ b/modules/mesh-conditions/cmd/mesh-conditions/compare.go @@ -3,8 +3,8 @@ package main // **Built dark, compared, then switched by the operator** (novox/hq ADR 0564 clause 9; issue 1952 C5, C8): `compare` // lists the open keys of the keeper and of the controller, each difference with its reason; the kinds that end with the // controller, the kinds only the keeper raises and the keys held while their module is delivered are listed apart and -// not counted. `take-over` raises an approve ask under the keeper's own name (ADR 0259), refused while compare shows a -// difference, and on the warrant writes the take-over key naming the keeper. `hand-back` is gone (issue 774): nothing +// not counted. `take-over` is refused (issue 1126): the keeper is the one source since the controller ended by its +// unassignment; a yes to an ask raised before still writes the take-over key naming the keeper. `hand-back` is gone (issue 774): nothing // reads the take-over key any more, so it is answered as gone and changes nothing. import ( @@ -91,7 +91,7 @@ func (k *Keeper) Compare() (Comparison, error) { } switch { case homed && (home.Carried || home.Replaced): - why := "ends with the controller: carried from its bucket until the lease write" + why := "ends with the controller: no longer carried (issue 1126)" if home.Replaced { why = "ends with the controller: its fault is observed otherwise" } @@ -125,41 +125,12 @@ type Asks interface { Ask(a asks.Ask) error } -// TakeOver raises the approve ask of a switch to the keeper, refused while compare differs or after the lease write. +// TakeOver is refused: the keeper is the one source of conditions since the controller ended (issue 1126). func (k *Keeper) TakeOver(a Asks, by, why string) (map[string]any, error) { if strings.TrimSpace(why) == "" { return nil, errors.New("a take-over says why") } - if k.LeaseAway() { - return nil, errors.New("the controller's lease is handed away: the keeper is the one source already") - } - cmp, err := k.Compare() - if err != nil { - return nil, err - } - if !cmp.Empty() { - return nil, fmt.Errorf("refused: compare shows %d difference(s), the first %s (%s); the take-over waits for none", - len(cmp.Differences), cmp.Differences[0].Key, cmp.Differences[0].Reason) - } - now := k.Now().UTC() - id := fmt.Sprintf("take-over-%d", now.UnixNano()) - act := asks.Act{"verb": Seat + ".take-over", "module": Module, "ask": id} - bind, err := asks.ActDigest(act) - if err != nil { - return nil, err - } - ask := asks.Ask{ID: id, Headline: "Switch the mesh's alarms to the new keeper?", Who: asks.Operator, - Explanation: fmt.Sprintf("The new condition keeper holds the same %d open alarms as the controller. Your yes makes "+ - "the alarm messages come from the keeper, with the same words. It can be handed back until the controller "+ - "gives up its lease.", cmp.Same), - Options: []asks.Option{{ID: "yes", Label: "Yes, switch", Does: "Alarms come from the keeper.", Level: asks.Approve, Binds: bind}, - {ID: "no", Label: "No, leave it", Does: "Alarms keep coming from the controller.", Level: asks.Acknowledge}}, - Expires: now.Add(asks.ApproveLasts - time.Minute), OnExpiry: "Alarms keep coming from the controller.", - About: Module + "-take-over", Details: "Asked by " + by + ": " + why} - if err := a.Ask(ask); err != nil { - return nil, err - } - return map[string]any{"asked": id, "same": cmp.Same, "apart": len(cmp.Apart)}, nil + return nil, errors.New("the keeper is the one source already: the controller ended by its unassignment (issue 1126)") } // Warranted is the act on the operator's answer: a yes, compare still empty, writes the take-over key naming the keeper. diff --git a/modules/mesh-conditions/cmd/mesh-conditions/hold.go b/modules/mesh-conditions/cmd/mesh-conditions/hold.go index d51e4308..1a26a5ba 100644 --- a/modules/mesh-conditions/cmd/mesh-conditions/hold.go +++ b/modules/mesh-conditions/cmd/mesh-conditions/hold.go @@ -3,7 +3,7 @@ package main // **A module being delivered raises no condition until its delivery ends** (novox/hq ADR 0564 clause 13 and Limits 12; // issue 1952 C7, C8). The keeper reads the assigner's wants and the nodes' haves, and judges a module's delivery on a // node with mesh-sdk's delivery.MotionOf, as the view does. While it is delivered there, a finding about it there, or -// about what it serves, and a condition carried from the controller's bucket on those subjects, is kept in the state +// about what it serves, is kept in the state // `held` and raises nothing. When the delivery ends, healthy at the want or failed, what still stands is raised once, // saying it happened during the delivery; what went is dropped with a log line. The hold lasts at most the delivery's // bound, a key of the state `bounds` whose time to live is the bound: its expiry raises what is held, saying the delivery @@ -98,17 +98,11 @@ func (k *Keeper) motion(node, module string) (delivery.Motion, delivery.Want) { } // holdFor says whether key is held now, and as what. Called with mu held. -func (k *Keeper) holdFor(key string, f *health.Finding, carried *health.Condition, open bool) (held, bool) { - if open || (f == nil && carried == nil) { +func (k *Keeper) holdFor(key string, f *health.Finding, open bool) (held, bool) { + if open || f == nil { return held{}, false } - var kind, source string - var subject health.Subject - if f != nil { - kind, source, subject = f.Kind, f.Source, f.Subject - } else { - kind, source, subject = carried.Kind, carried.Source, carried.Subject - } + kind, source, subject := f.Kind, f.Source, f.Subject if _, raisedOut := k.outOfHold[key]; raisedOut { return held{}, false } diff --git a/modules/mesh-conditions/cmd/mesh-conditions/hold_test.go b/modules/mesh-conditions/cmd/mesh-conditions/hold_test.go index 3a454a94..9beaa64c 100644 --- a/modules/mesh-conditions/cmd/mesh-conditions/hold_test.go +++ b/modules/mesh-conditions/cmd/mesh-conditions/hold_test.go @@ -21,7 +21,7 @@ const ( ) // ADR 0564 clause 13 and Limits 12 (issue 1952 C7, C8): a delivery of mesh-controller on novox replayed want by have. -// Its own findings and the carried controller-deaf raise nothing during it and none after when they went; one still +// Its own findings raise nothing during it and none after when they went; one still // standing at the end is raised once with its during-the-delivery line; one past the bound with its did-not-end line; a // failed delivery raises what stands at once; another module's and the machine's own are raised at once; compare lists a // held key as held. @@ -34,8 +34,6 @@ func TestADeliveryHoldsItsOwnFindingsUntilItEnds(t *testing.T) { raw, _ := json.Marshal(delivery.Have{V: delivery.Version, FP: fp, Composed: composed, Phase: phase, Health: h}) return raw } - deaf, _ := json.Marshal(health.Condition{Key: "core.controller.deaf", Kind: "controller-deaf", Source: "S4", - Subject: health.Subject{Scope: health.ScopeCore, ID: "controller"}, Severity: health.Urgent, Summary: "deaf", Resolver: "self"}) for _, c := range []struct { name string // end is the have the delivery ends with, or nil for a delivery past its bound. @@ -68,9 +66,6 @@ func TestADeliveryHoldsItsOwnFindingsUntilItEnds(t *testing.T) { t.Fatal(err) } } - if err := r.k.Controller("core.controller.deaf", deaf, false); err != nil { - t.Fatal(err) - } // The delivery's bound is its own key's time to live, the module's bound (the default where it names none). if r.st.ttls["bounds/novox.mesh-controller"] != delivery.DefaultDeliveryBound { t.Errorf("%s: the bound key lives %s", c.name, r.st.ttls["bounds/novox.mesh-controller"]) @@ -86,11 +81,10 @@ func TestADeliveryHoldsItsOwnFindingsUntilItEnds(t *testing.T) { held++ } } - if held != 2 { + if held != 1 { t.Errorf("%s: compare lists %d held, %+v", c.name, held, cmp) } - // The carried condition about the controller goes within the delivery; its own finding goes unless it stands. - _ = r.k.Controller("core.controller.deaf", nil, true) + // Its own finding goes unless it stands. if !c.standing { _ = r.k.Finding(watchman, own, nil, true) } @@ -106,8 +100,8 @@ func TestADeliveryHoldsItsOwnFindingsUntilItEnds(t *testing.T) { if co, open := r.open(own); c.standing != open || (open && !strings.Contains(co.Explanation, c.line)) { t.Errorf("%s: its own condition is %v: %q", c.name, open, co.Explanation) } - if _, open := r.open("core.controller.deaf"); open || len(r.k.Held()) != 0 { - t.Errorf("%s: the carried deaf condition was raised, or something is still held: %+v", c.name, r.k.Held()) + if len(r.k.Held()) != 0 { + t.Errorf("%s: something is still held: %+v", c.name, r.k.Held()) } } // A seat's finding is about the module the placement says holds the seat on that node (the lead, 2026-10-29): held diff --git a/modules/mesh-conditions/cmd/mesh-conditions/homes.go b/modules/mesh-conditions/cmd/mesh-conditions/homes.go index 61e5acbd..c900b8cf 100644 --- a/modules/mesh-conditions/cmd/mesh-conditions/homes.go +++ b/modules/mesh-conditions/cmd/mesh-conditions/homes.go @@ -2,8 +2,8 @@ package main // **Where each condition kind lives after the take-over** (novox/hq ADR 0564 clauses 4, 6, 7 and 9, the Context's probe // table; issue 1952 C3, C5): data, so a kind's home is a row and never code. `compare` reads it to tell a difference from -// a kind that ends with the controller or that the controller never raises, and the keeper reads it to carry, until the -// lease write, the controller's conditions of the kinds that end with it. +// a kind that ends with the controller or that the controller never raises, and the keeper reads it to clear, once, what +// it carried of the kinds that ended with the controller (EndCarried, issue 1126). // // The rows are ADR 0564's and the lead's table of 2026-10-29, by the controller's source of each kind. A kind with no // row is named by `compare` as "no home decided". @@ -17,8 +17,8 @@ type Home struct { Controller bool // Observer is the module whose findings raise it after the take-over; empty for a kind that ends. Observer string - // Carried says it ends with the controller's lease and is carried from the controller's bucket until then - // (clause 9: S4, S7, S12, S13, S14, S25, D5, D9, H-controller). + // Carried says it ended with the controller: carried from its bucket while it ran (clause 9: S4, S7, S12, S13, S14, + // S25, D5, D9, H-controller), cleared once by EndCarried and never carried again (issue 1126). Carried bool // Replaced says the kind itself ends and another observes its fault (D1, D10, the stuck probe): listed apart in // compare and not carried. diff --git a/modules/mesh-conditions/cmd/mesh-conditions/keeper.go b/modules/mesh-conditions/cmd/mesh-conditions/keeper.go index 8d5edca3..daabadb3 100644 --- a/modules/mesh-conditions/cmd/mesh-conditions/keeper.go +++ b/modules/mesh-conditions/cmd/mesh-conditions/keeper.go @@ -1,9 +1,10 @@ package main // The condition keeper (novox/hq ADR 0564 clauses 2, 3, 7, 8, 9 and 13; issue 1952 C2, C3, C7): the one writer of the -// mesh's open conditions. It watches every observer's `findings`, the healers' `attempts`, the controller's lease -// alone (its `conditions` are no longer read, issue 774), and the assigner's wants and the nodes' haves for the hold. From -// all of them it derives, per key, the condition that should be open, and writes the difference by compare-and-set, +// mesh's open conditions. It watches every observer's `findings`, the healers' `attempts`, and the assigner's wants and +// the nodes' haves for the hold; nothing of the controller (its `conditions` are no longer read, issue 774, and its lease +// turns nothing, issue 1126: the controller ended by its unassignment). From all of them it derives, per key, the +// condition that should be open, and writes the difference by compare-and-set, // saying each transition as the controller says it (go/health) and keeping it in `history`. Nothing clears a condition // but its finding going (ADR 0231 §14). @@ -46,10 +47,8 @@ var Observers = []string{health.WatchmanModule, health.SelfCheckModule, health.H // The other states it reads, as `.`. const ( - AttemptsRead = health.HealersModule + "." + health.AttemptsState - ControllerLease = "mesh-controller.lease" - ControllerLeaseKey = "holder" - WantsRead = delivery.AssignerModule + ".wants" + AttemptsRead = health.HealersModule + "." + health.AttemptsState + WantsRead = delivery.AssignerModule + ".wants" // PlacementRead is the mesh-placement seat's placement, read for which module holds a seat on a node (hold.go; the // lead, 2026-10-29, for ADR 0564 clause 2's inputs). PlacementRead = delivery.PlacementRead @@ -68,7 +67,7 @@ type Store interface { CreateTTL(state, key string, value any, ttl time.Duration) error } -// held is a finding or a carried condition kept while its module is delivered (clause 13 (b)). +// held is a finding kept while its module is delivered (clause 13 (b)). type held struct { Key string `json:"key"` Node string `json:"node"` @@ -104,8 +103,9 @@ type Keeper struct { findings map[string]map[string]health.Finding blind map[string]string attempts map[string]health.Attempt + // ctrl is the controller's conditions as Controller hands them: compare's other side alone, never a condition's + // input (issue 1126). Nothing feeds it since issue 774; it goes with compare. ctrl map[string]health.Condition - leaseOut bool takeover health.Takeover wants map[string]delivery.Want haves map[string]delivery.Have @@ -209,7 +209,8 @@ func (k *Keeper) Attempt(key string, value []byte, deleted bool) error { return k.Settle(condition) } -// Controller takes one change of the controller's `conditions` bucket (clauses 8, 9): read until the lease write. +// Controller takes one change of the controller's `conditions` bucket, for compare alone: no condition is carried from +// it (issue 1126). func (k *Keeper) Controller(key string, value []byte, deleted bool) error { k.mu.Lock() k.init() @@ -219,38 +220,57 @@ func (k *Keeper) Controller(key string, value []byte, deleted bool) error { var c health.Condition if err := json.Unmarshal(value, &c); err != nil { k.mu.Unlock() - k.Logf("the controller's condition %s is not readable, so nothing is carried from it: %v", key, err) + k.Logf("the controller's condition %s is not readable, so compare does not list it: %v", key, err) return nil } k.ctrl[key] = c } k.mu.Unlock() - return k.Settle(key) + return nil } -// Lease takes the controller's lease key as read (clause 8, Limits 8): once it is handed to a holder that is no -// controller, nothing more is carried from the controller's bucket, and the kinds that ended with it clear. -func (k *Keeper) Lease(away bool) error { - k.mu.Lock() - k.init() - was := k.leaseOut - k.leaseOut = away - keys := make([]string, 0, len(k.ctrl)) - for key := range k.ctrl { - keys = append(keys, key) - } - k.mu.Unlock() - if was == away { - return nil - } - return k.settleAll(keys) -} +// endedWhy is why a condition of a kind that ended with the controller is cleared. +const endedWhy = "its kind ended with the controller, which was unassigned (issue 1126): no finding raises it again" -// LeaseAway says whether the controller's lease is handed away. -func (k *Keeper) LeaseAway() bool { - k.mu.Lock() - defer k.mu.Unlock() - return k.leaseOut +// EndCarried clears, once, every open condition of a kind that ended with the controller (Homes' Carried) that no +// observer's finding names: carried from the controller's bucket while it ran, nothing raises it again (issue 1126). +// It answers how many it cleared. +func (k *Keeper) EndCarried() (int, error) { + open, err := k.Open() + if err != nil { + return 0, err + } + n := 0 + var problems []string + for _, c := range open { + if h, ok := k.homeOf(c.Kind); !ok || !h.Carried { + continue + } + k.mu.Lock() + k.init() + f := k.wanted(c.Key) + k.mu.Unlock() + if f != nil { + continue + } + raw, rev, found, err := k.Store.Get(ConditionsState, c.Key) + if err != nil || !found { + continue + } + var was health.Condition + if json.Unmarshal(raw, &was) != nil { + continue + } + if err := k.clear(c.Key, was, rev, k.Now().UTC(), endedWhy); err != nil { + problems = append(problems, fmt.Sprintf("%s: %v", c.Key, err)) + continue + } + n++ + } + if len(problems) > 0 { + return n, errors.New(strings.Join(problems, "; ")) + } + return n, nil } // Takeover takes the keeper's own take-over key as read. @@ -287,9 +307,9 @@ func (k *Keeper) settleAll(keys []string) error { return nil } -// wanted is what the inputs say key should be: a finding of an observer, or a condition carried from the controller's -// bucket of a kind that ends with the controller, until the lease write (clause 9). Nil when nothing says it. -func (k *Keeper) wanted(key string) (*health.Finding, *health.Condition) { +// wanted is what the inputs say key should be: a finding of an observer. Nil when nothing says it; nothing is carried +// from the controller's bucket (issue 1126). +func (k *Keeper) wanted(key string) *health.Finding { observers := make([]string, 0, len(k.findings)) for o := range k.findings { observers = append(observers, o) @@ -297,18 +317,10 @@ func (k *Keeper) wanted(key string) (*health.Finding, *health.Condition) { sort.Strings(observers) for _, o := range observers { if f, ok := k.findings[o][key]; ok { - return &f, nil + return &f } } - if c, ok := k.ctrl[key]; ok && !k.leaseOut && k.endsWithController(c) { - return nil, &c - } - return nil, nil -} - -func (k *Keeper) endsWithController(c health.Condition) bool { - h, ok := k.homeOf(c.Kind) - return ok && h.Carried + return nil } // settleOnce is one try of Settle. @@ -326,11 +338,11 @@ func (k *Keeper) settleOnce(key string) error { } k.mu.Lock() k.init() - f, carried := k.wanted(key) - hold, holding := k.holdFor(key, f, carried, open) + f := k.wanted(key) + hold, holding := k.holdFor(key, f, open) k.mu.Unlock() - if f == nil && carried == nil { + if f == nil { k.dropHeld(key, "its finding went while its module was delivered") if !open { return nil @@ -341,7 +353,7 @@ func (k *Keeper) settleOnce(key string) error { return k.keepHeld(hold) } k.dropHeld(key, "") - next := k.derive(key, was, open, f, carried, now) + next := k.derive(key, was, open, f, now) events := transitions(was, next, open) if len(events) == 0 && open && sameBody(was, next) { return nil @@ -359,28 +371,16 @@ func (k *Keeper) settleOnce(key string) error { k.mu.Unlock() for _, e := range events { e.Condition, e.At, e.Event = next, now, health.EventFor(e.Change) - e.Show = k.show(next, carried != nil) + e.Show = health.ShowKeeper(next.Key) k.tell(e) } return nil } -func (k *Keeper) show(c health.Condition, carried bool) string { - if carried { - return health.ShowController(c.Key) - } - return health.ShowKeeper(c.Key) -} - // derive is the condition key should be now, from what was open and what the inputs say. -func (k *Keeper) derive(key string, was health.Condition, open bool, f *health.Finding, carried *health.Condition, now time.Time) health.Condition { +func (k *Keeper) derive(key string, was health.Condition, open bool, f *health.Finding, now time.Time) health.Condition { var c health.Condition switch { - case carried != nil: - // Raised and cleared as the controller's bucket says, with its show and actions (Limits 9). - c = *carried - c.Epoch = 0 - c.LooksUnread = "" case open: c = was k.fromFinding(&c, *f, now) @@ -402,9 +402,7 @@ func (k *Keeper) derive(key string, was health.Condition, open bool, f *health.F } k.mu.Unlock() } - if carried == nil { - k.overlay(&c, now) - } + k.overlay(&c, now) if !open { k.mu.Lock() if line := k.outOfHold[key]; line != "" { @@ -447,9 +445,7 @@ func (k *Keeper) fromFinding(c *health.Condition, f health.Finding, now time.Tim _ = now } -// overlay writes the healers' attempts into the condition (clause 7) and, until the lease write, what the controller's -// bucket holds for the same key (clause 8): its tried, its resolver and escalation, and its silence where the keeper has -// none of its own. +// overlay writes the healers' attempts into the condition (clause 7); nothing of the controller's bucket (issue 1126). func (k *Keeper) overlay(c *health.Condition, now time.Time) { k.mu.Lock() var mine []health.Attempt @@ -458,18 +454,8 @@ func (k *Keeper) overlay(c *health.Condition, now time.Time) { mine = append(mine, a) } } - ctrl, carry := k.ctrl[c.Key] - carry = carry && !k.leaseOut k.mu.Unlock() sort.Slice(mine, func(i, j int) bool { return mine[i].Begun.Before(mine[j].Begun) }) - if carry && len(ctrl.Tried) > 0 { - c.Tried = append([]health.Tried(nil), ctrl.Tried...) - if ctrl.Escalated() { - c.Resolver, c.Severity = health.ResolverOperator, health.Urgent - } else if ctrl.Resolver != "" && ctrl.Resolver != health.ResolverSelf { - c.Resolver = ctrl.Resolver - } - } for _, a := range mine { t := a.Tried() if !containsTried(c.Tried, t) { @@ -487,9 +473,6 @@ func (k *Keeper) overlay(c *health.Condition, now time.Time) { if len(c.Tried) > health.KeptTried { c.Tried = c.Tried[len(c.Tried)-health.KeptTried:] } - if carry && c.Silenced == nil && ctrl.Silenced != nil && now.Before(ctrl.Silenced.Until) { - c.Silenced = ctrl.Silenced - } } func containsTried(list []health.Tried, t health.Tried) bool { @@ -556,7 +539,7 @@ func (k *Keeper) clear(key string, was health.Condition, rev uint64, now time.Ti k.mu.Unlock() cleared := now k.tell(health.Event{Condition: was, Event: health.EventCleared, At: now, Change: health.ChangeCleared, Why: why, - Cleared: &cleared, Show: k.show(was, false)}) + Cleared: &cleared, Show: health.ShowKeeper(was.Key)}) return nil } @@ -623,9 +606,6 @@ func (k *Keeper) Resync() (int, error) { seen[key] = true } } - for key := range k.ctrl { - seen[key] = true - } k.mu.Unlock() keys := make([]string, 0, len(seen)) for key := range seen { diff --git a/modules/mesh-conditions/cmd/mesh-conditions/keeper_test.go b/modules/mesh-conditions/cmd/mesh-conditions/keeper_test.go index 21a28ee4..d061f720 100644 --- a/modules/mesh-conditions/cmd/mesh-conditions/keeper_test.go +++ b/modules/mesh-conditions/cmd/mesh-conditions/keeper_test.go @@ -141,7 +141,7 @@ func finding(t *testing.T, observer, scope, id, kind, source string, sev health. } // ADR 0564 clauses 2, 3, 7, 8 (issue 1952 C2, C3): a finding raises, changes and clears its condition; a blind observer -// clears nothing; the healers' attempts and the controller's escalation reach the condition; a reopening within ten +// clears nothing; the healers' attempts reach the condition; a reopening within ten // minutes is the same fault; a silence is a transition in history. func TestTheKeeperRaisesChangesAndClearsFromFindings(t *testing.T) { r := newRig() @@ -206,25 +206,12 @@ func TestTheKeeperRaisesChangesAndClearsFromFindings(t *testing.T) { } return "" }}, - // Until the lease write the controller's escalation for the same key reaches the condition (clause 8). - {name: "the controller's escalation is carried", do: func() error { - ctrl, _ := json.Marshal(health.Condition{Key: key, Kind: "silent", Severity: health.Urgent, Resolver: health.ResolverOperator, - Tried: []health.Tried{{At: r.now, What: "x", Outcome: "escalated", By: "healer H1"}}}) - _ = r.k.Attempt("h3-1", nil, true) - _ = r.k.Attempt("h3-2", nil, true) - return r.k.Controller(key, ctrl, false) - }, check: func(c health.Condition, _ bool) string { - if c.Resolver != health.ResolverOperator || c.Severity != health.Urgent { - return fmt.Sprintf("%+v", c) - } - return "" - }}, } for _, s := range steps { if err := s.do(); err != nil { t.Fatalf("%s: %v", s.name, err) } - if got := r.changes(); s.said != "" && got != s.said || s.said == "" && got != "" && s.name != "the controller's escalation is carried" { + if got := r.changes(); s.said != "" && got != s.said || s.said == "" && got != "" { t.Errorf("%s: said %q, want %q", s.name, got, s.said) } if s.check != nil { @@ -243,28 +230,64 @@ func TestTheKeeperRaisesChangesAndClearsFromFindings(t *testing.T) { if !strings.Contains(string(raw0), `"show":"mesh-conditions.conditions key=machine.g14.silent"`) { t.Errorf("the first transition is %s", raw0) } - // A carried kind (it ends with the controller) is raised and cleared as the controller's bucket says, until the - // lease is handed away, with the controller's show (clause 9, Limits 9). - deaf, _ := json.Marshal(health.Condition{Key: "core.controller.deaf", Kind: "controller-deaf", Source: "S4", - Subject: health.Subject{Scope: health.ScopeCore, ID: "controller"}, Severity: health.Urgent, Summary: "deaf", Resolver: "self", Epoch: 9}) - if err := r.k.Controller("core.controller.deaf", deaf, false); err != nil { +} + +// 1126/A5/AC2: the keeper carries no controller condition: the controller's bucket, as Controller hands it, opens no +// condition and changes none (no escalation, no silence). What it carried while the controller ran, still open, is cleared +// once by EndCarried, said as cleared; a condition an observer's finding holds stays; a second EndCarried clears nothing. +func TestTheKeeperCarriesNoControllerConditionAndClearsWhatItCarriedOnce(t *testing.T) { + r := newRig() + key, raw := finding(t, watchman, health.ScopeMachine, "g14", "silent", "S1", health.Warning, r.now) + if err := r.k.Finding(watchman, key, raw, false); err != nil { t.Fatal(err) } - if c, open := r.open("core.controller.deaf"); !open || c.Epoch != 0 || r.said[0].Show != "mesh-controller.conditions key=core.controller.deaf" { - t.Errorf("a carried kind is %+v, said %+v", c, r.said) - } r.said = nil - if err := r.k.Lease(true); err != nil { - t.Fatal(err) + until := r.now.Add(time.Hour) + for _, c := range []health.Condition{ + {Key: "core.controller.deaf", Kind: "controller-deaf", Source: "S4", Subject: health.Subject{Scope: health.ScopeCore, ID: "controller"}, + Severity: health.Urgent, Summary: "deaf", Resolver: "self"}, + {Key: key, Kind: "silent", Severity: health.Urgent, Resolver: health.ResolverOperator, + Tried: []health.Tried{{At: r.now, What: "x", Outcome: "escalated", By: "healer H1"}}, + Silenced: &health.Silence{Until: until}}, + } { + raw, _ := json.Marshal(c) + if err := r.k.Controller(c.Key, raw, false); err != nil { + t.Fatal(err) + } + if _, err := r.k.Resync(); err != nil { + t.Fatal(err) + } } - if _, open := r.open("core.controller.deaf"); open || r.changes() != "cleared core.controller.deaf" { - t.Error("a carried kind outlived the lease write") + if _, open := r.open("core.controller.deaf"); open || r.changes() != "" { + t.Error("the controller's condition was carried") + } + if c, _ := r.open(key); c.Severity != health.Warning || c.Resolver != health.ResolverSelf || len(c.Tried) != 0 || c.Silenced != nil { + t.Errorf("the controller's escalation reached the keeper's condition: %+v", c) + } + // Carried while the controller ran: two of kinds that ended with it, still open in conditions. + for _, c := range []health.Condition{ + {Key: "core.controller.deaf", Kind: "controller-deaf", Severity: health.Urgent, Resolver: "self", Count: 1}, + {Key: "seat.vault.ace.holder-silent", Kind: "holder-silent", Severity: health.Warning, Resolver: "self", Count: 1}, + } { + if _, err := r.st.Create(ConditionsState, c.Key, c); err != nil { + t.Fatal(err) + } + } + n, err := r.k.EndCarried() + if err != nil || n != 2 || r.changes() != "cleared core.controller.deaf, cleared seat.vault.ace.holder-silent" { + t.Fatalf("EndCarried cleared %d (%v)", n, err) + } + if _, open := r.open(key); !open { + t.Error("a condition a finding holds was cleared") + } + if n, err := r.k.EndCarried(); err != nil || n != 0 || r.changes() != "" { + t.Errorf("a second EndCarried cleared %d (%v)", n, err) } } // ADR 0564 clause 9 (issue 1952 C5, C8; the lead 2026-10-29): compare counts only what both sides can raise. A key held // while its module is delivered, a kind that ends with the controller and a kind the controller never raises are listed -// apart; a kind with no home decided is a difference naming the gap; take-over is refused while compare differs, and +// apart; a kind with no home decided is a difference naming the gap; take-over is refused (issue 1126), and // hand-back is gone (issue 774, review g1): answered as gone, naming its record, and writing nothing. func TestCompareCountsOnlyWhatBothSidesRaise(t *testing.T) { r := newRig() @@ -296,7 +319,7 @@ func TestCompareCountsOnlyWhatBothSidesRaise(t *testing.T) { "delivery.x.mystery": "no home is decided for the kind mystery", "bus.x.consumer-wrong": "mesh-self-check has not written its finding", // A kind of a probe the operator dropped (2026-10-30) is carried, never missing. - "seat.vault.ace.holder-silent": "carried from its bucket until the lease write (dropped 2026-10-30 by the operator", + "seat.vault.ace.holder-silent": "no longer carried (issue 1126) (dropped 2026-10-30 by the operator", // The controller's own bus upgrade ends with its lease (the operator, 2026-10-31; issue 1952). "bus.nats.upgrade-failed": "ends with the controller", } { @@ -332,17 +355,14 @@ func TestCompareCountsOnlyWhatBothSidesRaise(t *testing.T) { if len(Homes) != 84 || carried != 34 { // 19 of ADR 0564, the 63 of the table, plan-failed and stalled t.Errorf("the table holds %d kinds, %d carried", len(Homes), carried) } - if _, err := r.k.TakeOver(nil, "jochen", "now"); err == nil || !strings.Contains(err.Error(), "2 difference") { - t.Errorf("take-over while compare differs: %v", err) + if _, err := r.k.TakeOver(nil, "jochen", "now"); err == nil || !strings.Contains(err.Error(), "one source already") { + t.Errorf("take-over: %v", err) } - for _, away := range []bool{false, true} { - _ = r.k.Lease(away) - if _, err := r.k.HandBack("jochen", "back"); err == nil || !strings.Contains(err.Error(), "hand-back is gone") || - !strings.Contains(err.Error(), "ADR 0564 clause 9") { - t.Errorf("hand-back with the lease away %v: %v, want it answered as gone naming its record", away, err) - } - if _, _, found, _ := r.k.Store.Get(TakeoverState, health.TakeoverKey); found { - t.Errorf("hand-back with the lease away %v wrote the take-over key", away) - } + if _, err := r.k.HandBack("jochen", "back"); err == nil || !strings.Contains(err.Error(), "hand-back is gone") || + !strings.Contains(err.Error(), "ADR 0564 clause 9") { + t.Errorf("hand-back: %v, want it answered as gone naming its record", err) + } + if _, _, found, _ := r.k.Store.Get(TakeoverState, health.TakeoverKey); found { + t.Error("hand-back or take-over wrote the take-over key") } } diff --git a/modules/mesh-conditions/cmd/mesh-conditions/main.go b/modules/mesh-conditions/cmd/mesh-conditions/main.go index fde9df9a..c41a63ee 100644 --- a/modules/mesh-conditions/cmd/mesh-conditions/main.go +++ b/modules/mesh-conditions/cmd/mesh-conditions/main.go @@ -15,7 +15,6 @@ import ( stdio "git.novox.be/novox/mesh-sdk/go" "git.novox.be/novox/mesh-sdk/go/asker" "git.novox.be/novox/mesh-sdk/go/asks" - "git.novox.be/novox/mesh-sdk/go/buscompose" "git.novox.be/novox/mesh-sdk/go/health" ) @@ -106,14 +105,11 @@ func Keep(k *Keeper) { } return nil } - watch(k, ControllerLease, "", func(c stdio.StateChange) error { - if c.Key != ControllerLeaseKey { - return nil - } - away, why := buscompose.LeaseHandedAway(c.Value, c.Op == "put", nil) - logf("the controller's lease: %s", why) - return said(k.Lease(away)) - }) + // Once at start: what was carried from the controller's bucket while it ran is cleared (issue 1126); nothing raises + // those kinds again. + if n, err := k.EndCarried(); n > 0 || err != nil { + logf("cleared %d condition(s) of kinds that ended with the controller: %v", n, err) + } watch(k, WantsRead, "", func(c stdio.StateChange) error { return said(k.Want(c.Key, c.Value, c.Op != "put")) }) watch(k, HavesRead, "", func(c stdio.StateChange) error { return said(k.Have(c.Key, c.Value, c.Op != "put")) }) watch(k, AttemptsRead, health.HealersModule+" attempts", func(c stdio.StateChange) error { diff --git a/modules/mesh-conditions/cmd/mesh-conditions/manifest_test.go b/modules/mesh-conditions/cmd/mesh-conditions/manifest_test.go index 55dcea7f..d22707dd 100644 --- a/modules/mesh-conditions/cmd/mesh-conditions/manifest_test.go +++ b/modules/mesh-conditions/cmd/mesh-conditions/manifest_test.go @@ -29,14 +29,14 @@ func TestTheManifestSaysWhatTheCodeDoes(t *testing.T) { !slices.Equal(Verbs, catalogue.VerbNames(seat.Serves)) { t.Errorf("module %s runs as %s and claims %+v; the seat serves %v", m.Module, m.RunsAs, m.Claims, catalogue.VerbNames(seat.Serves)) } - reads := []string{ControllerLease, WantsRead, HavesRead, AttemptsRead, PlacementRead} + reads := []string{WantsRead, HavesRead, AttemptsRead, PlacementRead} for _, o := range Observers { reads = append(reads, o+"."+health.FindingsState) } slices.Sort(reads) got := slices.Clone(m.Reads) slices.Sort(got) - if slices.Contains(m.Reads, "mesh-controller.conditions") || !slices.Equal(got, reads) || slices.Contains(m.Reads, health.HealersModule+"."+health.WouldState) { + if slices.Contains(m.Reads, "mesh-controller.conditions") || slices.Contains(m.Reads, "mesh-controller.lease") || !slices.Equal(got, reads) || slices.Contains(m.Reads, health.HealersModule+"."+health.WouldState) { t.Errorf("reads %v, want %v and never the healers' would", got, reads) } ttl, keyTTL := map[string]int{}, map[string]bool{} diff --git a/modules/mesh-conditions/cmd/mesh-conditions/tools.go b/modules/mesh-conditions/cmd/mesh-conditions/tools.go index 4ff0ee80..d62b6e11 100644 --- a/modules/mesh-conditions/cmd/mesh-conditions/tools.go +++ b/modules/mesh-conditions/cmd/mesh-conditions/tools.go @@ -73,8 +73,8 @@ func Tools(k *Keeper, a Asks) []stdio.Tool { {Name: Seat + ".compare", Description: "The keeper's open conditions against the controller's, each difference with " + "its reason; what ends with the controller, what only the keeper raises and what is held listed apart.", Run: func(map[string]any) (any, error) { return k.Compare() }}, - {Name: Seat + ".take-over", Description: "Ask the operator's Approve to switch the readers of conditions to the " + - "keeper; refused while compare differs.", + {Name: Seat + ".take-over", Description: "Refused: the keeper is the one source of conditions since the " + + "controller ended by its unassignment (issue 1126); kept as the seat's verb until the seat drops it.", Input: obj(map[string]any{"why": str("why now")}, "why"), Run: func(args map[string]any) (any, error) { return k.TakeOver(a, stdio.CallerOf(args), strArg(args, "why")) @@ -212,7 +212,7 @@ func (k *Keeper) Inputs() map[string]any { findings[o] = len(fs) } return map[string]any{"findings": findings, "blind": k.blind, "attempts": len(k.attempts), - "controller-conditions": len(k.ctrl), "lease-handed-away": k.leaseOut, "follow": k.takeover.Follow, + "controller-conditions": len(k.ctrl), "follow": k.takeover.Follow, "wants": len(k.wants), "haves": len(k.haves)} } diff --git a/modules/mesh-conditions/cmd/mesh-conditions/whole_integration_test.go b/modules/mesh-conditions/cmd/mesh-conditions/whole_integration_test.go index f8881730..b788bafb 100644 --- a/modules/mesh-conditions/cmd/mesh-conditions/whole_integration_test.go +++ b/modules/mesh-conditions/cmd/mesh-conditions/whole_integration_test.go @@ -261,7 +261,7 @@ func TestTheKeeperWholeRaisesAndClearsOnTheBus(t *testing.T) { if err != nil { t.Fatal(err) } - buckets := []string{"mesh-controller_conditions", "mesh-controller_lease", "mesh-delivery-assigner_wants", "node-engine_have", "mesh-delivery-assigner_placement", + buckets := []string{"mesh-controller_conditions", "mesh-delivery-assigner_wants", "node-engine_have", "mesh-delivery-assigner_placement", "mesh-healers_attempts"} for _, o := range Observers { buckets = append(buckets, o+"_findings") diff --git a/modules/mesh-conditions/module.json b/modules/mesh-conditions/module.json index addf5483..7a02f96e 100644 --- a/modules/mesh-conditions/module.json +++ b/modules/mesh-conditions/module.json @@ -28,7 +28,6 @@ "mesh-provisioner.findings", "artifact-store-tools.findings", "mesh-healers.attempts", - "mesh-controller.lease", "mesh-delivery-assigner.wants", "node-engine.have", "mesh-delivery-assigner.placement" diff --git a/modules/mesh-config/README.md b/modules/mesh-config/README.md index 69a9cab4..fb841a1e 100644 --- a/modules/mesh-config/README.md +++ b/modules/mesh-config/README.md @@ -75,6 +75,5 @@ Each directory or archive this module declares says how it treats a file it did controller told by `pull-declares`; the controller's mirrors are no longer read (ADR 0565 §4, issue 774). `config_take_over` asks the operator's Approve, refused while any difference stands and switched on the Approve only if none stands then. `config_hand_back` is gone: it answers so and changes nothing. `config_declared` and `config_resync` read and write again what it keeps. -- From the controller's lease write a trusted value is set only through propose and the operator's Approve. -- Its sets, clears and warrants are recorded in the hand-act log of the seat `mesh-hand-acts`, falling back to the - controller's while that seat has no holder and the controller holds its lease. +- A trusted value is set only through propose and the operator's Approve: no controller relay is trusted (issue 1126). +- Its sets, clears and warrants are recorded in the hand-act log of the seat `mesh-hand-acts` alone. diff --git a/modules/mesh-config/cmd/mesh-config/config.go b/modules/mesh-config/cmd/mesh-config/config.go index 1928a3df..1db9388f 100644 --- a/modules/mesh-config/cmd/mesh-config/config.go +++ b/modules/mesh-config/cmd/mesh-config/config.go @@ -16,9 +16,10 @@ import ( // values it moves; records the change in the history and the hand-act log; raises `config-changed`; and answers // with each module on each node whose value moved. A config change opens no walk and waits at no first-node gate. // -// **Trusted values at the terminal or on the operator's Approve.** A trusted value (ADR 0348 §8) is set only by a -// call the controller carries from its terminal, or on the operator's warrant for a proposal (proposals.go, design -// 57 K4); a set through the verb is refused and names the proposal as the way. +// **Trusted values on the operator's Approve alone.** A trusted value (ADR 0348 §8) is set only on the operator's +// warrant for a proposal (proposals.go, design 57 K4); a set through the verb is refused and names the proposal as the +// way. No controller relay is trusted (ADR 0565 §13, issue 1126): the controller ended by its unassignment, so no lease +// turns its terminal on. // ControllerCaller is the controller's bus user: the one caller that carries a value given at its terminal. const ControllerCaller = "controller" @@ -275,9 +276,9 @@ func (c *Config) change(call Call) (*SetAnswer, error) { return nil, fmt.Errorf("%s is refused for %s at %s: %s", show(value), call.Key, level, strings.Join(problems, "; ")) } - if trusted && call.approval == nil && !c.atTheTerminal(call) { - return nil, fmt.Errorf("%s is trusted (it decides what runs or who can reach what), so it is set only at "+ - "the controller's terminal or on the operator's Approve (novox/hq ADR 0348 §8); %s is not the terminal: "+ + if trusted && call.approval == nil { + return nil, fmt.Errorf("%s is trusted (it decides what runs or who can reach what), so it is set only on "+ + "the operator's Approve (novox/hq ADR 0348 §8; no controller relay is trusted, ADR 0565 §13), not by %s: "+ "propose it instead, with config's action propose", call.Key, call.Caller) } v := Value{Value: compact(raw), By: by, Via: call.Caller, Why: strings.TrimSpace(call.Why), At: now} @@ -293,9 +294,9 @@ func (c *Config) change(call Call) (*SetAnswer, error) { if !had { return nil, fmt.Errorf("nothing is set for %s at %s, so there is nothing to clear", call.Key, level) } - if trusted && call.approval == nil && !c.atTheTerminal(call) { - return nil, fmt.Errorf("%s is trusted, so it is cleared only at the controller's terminal or on the "+ - "operator's Approve (novox/hq ADR 0348 §8); %s is not the terminal: propose the clear instead, with "+ + if trusted && call.approval == nil { + return nil, fmt.Errorf("%s is trusted, so it is cleared only on the operator's Approve (novox/hq ADR 0348 "+ + "§8; no controller relay is trusted, ADR 0565 §13), not by %s: propose the clear instead, with "+ "config's action propose and clear", call.Key, call.Caller) } if err := c.Store.Delete(ValuesState, vk); err != nil { @@ -326,13 +327,6 @@ func (c *Config) change(call Call) (*SetAnswer, error) { return answer, nil } -// atTheTerminal says a call is the controller's relay of its terminal, which holds only until the controller's lease is -// handed away: from the lease write a trusted value is set only through propose and the operator's Approve, whoever -// calls (novox/hq ADR 0565 §13). -func (c *Config) atTheTerminal(call Call) bool { - return call.Caller == ControllerCaller && !c.leaseAway() -} - // declared is the rule a value at a level is held to, and whether it is trusted; refused, naming the declared // keys, for a key the module or the list of predefined keys does not declare (ADR 0348 §10). func (c *Config) declared(s Sources, level Level, key string) (rule, bool, error) { diff --git a/modules/mesh-config/cmd/mesh-config/config_test.go b/modules/mesh-config/cmd/mesh-config/config_test.go index 37270958..7201384a 100644 --- a/modules/mesh-config/cmd/mesh-config/config_test.go +++ b/modules/mesh-config/cmd/mesh-config/config_test.go @@ -404,10 +404,10 @@ func TestTheVerbRefusesWhatADeclarationDoesNotAllow(t *testing.T) { {"a value not in its enum", Call{Key: "position", Value: "middle", Node: "g14", Module: "dunst"}, "is not one of"}, {"a value not matching its pattern", Call{Key: "origin", Value: "Home 1", Node: "g14", Module: "dunst"}, "does not match the pattern"}, - {"a trusted value at the mesh, not from the terminal", Call{Key: "agent-managed-settings", - Value: map[string]any{"a": 1.0}}, "set only at the controller's terminal"}, - {"a trusted module value, not from the terminal", Call{Key: "managed", Value: map[string]any{}, - Node: "g14", Module: "claude-code"}, "set only at the controller's terminal"}, + {"a trusted value at the mesh, not on an Approve", Call{Key: "agent-managed-settings", + Value: map[string]any{"a": 1.0}}, "set only on the operator's Approve"}, + {"a trusted module value, not on an Approve", Call{Key: "managed", Value: map[string]any{}, + Node: "g14", Module: "claude-code"}, "set only on the operator's Approve"}, {"a change without why", Call{Key: "font-size", Value: 12.0, Why: " "}, "say why"}, {"an unproven caller", Call{Key: "font-size", Value: 12.0, Caller: Unproven}, "no proven caller"}, {"by from anyone but the controller", Call{Key: "font-size", Value: 12.0, By: "jochen"}, "only the controller says"}, @@ -438,23 +438,27 @@ func TestTheVerbRefusesWhatADeclarationDoesNotAllow(t *testing.T) { } } -// **A trusted value from the controller's terminal is set**, with who gave it there (ADR 0348 §8). -func TestATrustedValueFromTheTerminalIsSetWithWhoGaveIt(t *testing.T) { - c, m := aMesh(t) - a := set(t, c, Call{Key: "agent-managed-settings", Value: map[string]any{"b": 2.0, "a": 1.0}, - Caller: ControllerCaller, By: "jochen"}) - if string(a.Value) != `{"a":1,"b":2}` { - t.Errorf("the value is written as %s", a.Value) - } - r := m.resolved(t, "g14", "claude-code", "managed") - if r.Source != SourceMesh || !r.Trusted || string(r.Value) != `{"a":1,"b":2}` { - t.Errorf("claude-code's managed settings resolve as %+v", r) - } - e, _ := m.Get(ValuesState, "mesh.agent-managed-settings") - var v Value - _ = json.Unmarshal(e.Value, &v) - if v.By != "jochen" || v.Via != ControllerCaller { - t.Errorf("the value says it was set by %s via %s", v.By, v.Via) +// 1126/A5/AC1: **no controller relay is trusted.** With no lease key (the controller ended by its unassignment), a trusted +// value set or cleared by the controller's account is refused, naming propose; nothing is written (ADR 0565 §13). +func TestNoControllerRelayIsTrusted(t *testing.T) { + for _, call := range []Call{ + {Action: "set", Key: "agent-managed-settings", Value: map[string]any{"a": 1.0}}, + {Action: "set", Key: "managed", Node: "g14", Module: "claude-code", Value: map[string]any{"a": 1.0}}, + {Action: "clear", Key: "agent-managed-settings"}, + } { + c, m := aMesh(t) + m.mirror(t, ValuesState, "mesh.agent-managed-settings", `{"value":{"b":2},"by":"person.jochen","via":"person.jochen"}`) + call.Caller, call.By, call.Why = ControllerCaller, "jochen", "at the terminal" + _, err := c.Change(call) + if err == nil || !strings.Contains(err.Error(), "propose") { + t.Errorf("%s %s from the controller: %v", call.Action, call.Key, err) + } + if v, _ := storedValue(t, m, "mesh.agent-managed-settings"); string(v.Value) != `{"b":2}` { + t.Errorf("%s %s from the controller left %s", call.Action, call.Key, v.Value) + } + if _, held := storedValue(t, m, "module.g14.claude-code.managed"); held { + t.Errorf("%s %s from the controller wrote claude-code's own value", call.Action, call.Key) + } } } diff --git a/modules/mesh-config/cmd/mesh-config/main.go b/modules/mesh-config/cmd/mesh-config/main.go index d799a03b..1311eb3d 100644 --- a/modules/mesh-config/cmd/mesh-config/main.go +++ b/modules/mesh-config/cmd/mesh-config/main.go @@ -19,7 +19,6 @@ import ( stdio "git.novox.be/novox/mesh-sdk/go" "git.novox.be/novox/mesh-sdk/go/asks" - "git.novox.be/novox/mesh-sdk/go/buscompose" "git.novox.be/novox/mesh-sdk/go/handacts" ) @@ -77,15 +76,7 @@ func (runtimeStore) Emit(event string, body any) error { return stdio.Emit(event // recorder records into the hand-act log of the seat mesh-hand-acts alone (novox/hq ADR 0565 §11, issue 1953): the manifest // invokes no verb of the controller and the SDK recorder has no fall back to its log. -var recorder = handacts.Recorder{Ask: stdio.Ask, LeaseAway: func() bool { - e, err := stdio.State(LeaseRead).Get(buscompose.ControllerLeaseKey) - var raw []byte - if e != nil { - raw = e.Value - } - away, _ := buscompose.LeaseHandedAway(raw, e != nil, err) - return away -}} +var recorder = handacts.Recorder{Ask: stdio.Ask} // HandAct records a change in the hand-act log (to-be 45 §7). func (runtimeStore) HandAct(what, why, cause string) error { @@ -238,7 +229,7 @@ func Tools(c *Config) []stdio.Tool { "one level: mesh config (no module, no node) or node config (a node) for a predefined key, or a " + "module's own value on one node (module and node). Each set or clear needs why, is validated against " + "its declaration, names each module on each node whose value moves, and is kept in the history with " + - "who and why. A trusted value is set only at the controller's terminal or on the operator's Approve: " + + "who and why. A trusted value is set only on the operator's Approve: " + "propose asks the operator, under mesh-config's own name on a channel that proves who answered, and sets " + "the value on Approve if what it was shown against still stands; proposals lists them, or one with " + "proposal. A proposal naming the open pull request that introduces its key (pull) is judged against what " + diff --git a/modules/mesh-config/cmd/mesh-config/manifest_test.go b/modules/mesh-config/cmd/mesh-config/manifest_test.go index fb80765e..19e66dab 100644 --- a/modules/mesh-config/cmd/mesh-config/manifest_test.go +++ b/modules/mesh-config/cmd/mesh-config/manifest_test.go @@ -33,7 +33,7 @@ func TestEveryStateAndActHasItsTool(t *testing.T) { ProposalsState: "config proposals", PullsState: "config list", KeptState: "config list", HistoryLog: "config history", DeclaredState: DeclaredTool, TakeoverState: DeclaredTool, PlacementRead: CompareTool, WantsRead: CompareTool, BuildsRead: CompareTool, ModulesRead: DeclaredTool, - ManifestsRead: DeclaredTool, IndexerPredefinedRead: CompareTool, CandidatesRead: CompareTool, LeaseRead: HandBackTool, + ManifestsRead: DeclaredTool, IndexerPredefinedRead: CompareTool, CandidatesRead: CompareTool, "a resolution": ResyncTool, "the indexer's candidates taken": ResyncTool, "a take-over decided": TakeOverTool, } served := map[string]bool{} diff --git a/modules/mesh-config/cmd/mesh-config/proposals_test.go b/modules/mesh-config/cmd/mesh-config/proposals_test.go index 64a23994..65c668bb 100644 --- a/modules/mesh-config/cmd/mesh-config/proposals_test.go +++ b/modules/mesh-config/cmd/mesh-config/proposals_test.go @@ -266,8 +266,8 @@ func TestATrustedValueIsProposedAskedUnderItsOwnNameAndSetOnTheOperatorsApprove( func TestAnApproveIsRefusedWhenTheStoredValueChangedSince(t *testing.T) { c, m, r := aProposingMesh(t) a := propose(t, c, Call{Key: "agent-managed-settings", Value: map[string]any{"a": 1.0}}) - // Set at the terminal meanwhile. - set(t, c, Call{Key: "agent-managed-settings", Value: map[string]any{"b": 2.0}, Caller: ControllerCaller, By: "jochen"}) + // Changed meanwhile. + m.mirror(t, ValuesState, "mesh.agent-managed-settings", `{"value":{"b":2},"by":"person.jochen","via":"person.jochen"}`) m.events = nil r.choose(t, a.Proposal, OptionApprove, nil) said, _ := c.Decide() @@ -501,8 +501,7 @@ func TestAClearIsProposedAndATrustedSetNamesTheProposal(t *testing.T) { if err == nil || !strings.Contains(err.Error(), "propose it instead") { t.Errorf("a trusted set through the verb: %v", err) } - set(t, c, Call{Key: "managed", Value: map[string]any{"a": 1.0}, Node: "g14", Module: "claude-code", - Caller: ControllerCaller, By: "jochen"}) + m.mirror(t, ValuesState, "module.g14.claude-code.managed", `{"value":{"a":1},"by":"person.jochen","via":"person.jochen"}`) a := propose(t, c, Call{Key: "managed", Clear: true, Node: "g14", Module: "claude-code"}) ask := r.asks[a.Proposal] if !strings.HasPrefix(ask.Headline, "Clear managed for claude-code on g14") || diff --git a/modules/mesh-config/cmd/mesh-config/takeover.go b/modules/mesh-config/cmd/mesh-config/takeover.go index 39d09015..eefae9ad 100644 --- a/modules/mesh-config/cmd/mesh-config/takeover.go +++ b/modules/mesh-config/cmd/mesh-config/takeover.go @@ -26,7 +26,6 @@ import ( "time" "git.novox.be/novox/mesh-sdk/go/asks" - "git.novox.be/novox/mesh-sdk/go/buscompose" "git.novox.be/novox/mesh-sdk/go/delivery" ) @@ -41,8 +40,6 @@ const ( // IndexerPredefinedRead and CandidatesRead are the indexer's states (ADR 0565 §2). IndexerPredefinedRead = delivery.IndexerModule + "." + delivery.IndexerPredefinedState CandidatesRead = delivery.IndexerModule + "." + delivery.IndexerConfigCandidatesState - // LeaseRead is the controller's lease: after its write hand-back is gone and the terminal sets nothing (§13). - LeaseRead = buscompose.ControllerLease ) // The sources the switch names. @@ -95,17 +92,6 @@ func (c *Config) source() (Switch, error) { return s, nil } -// leaseAway says the controller's lease is handed away (ADR 0561 §3's reading). -func (c *Config) leaseAway() bool { - e, err := c.Store.Get(LeaseRead, buscompose.ControllerLeaseKey) - var raw []byte - if e != nil { - raw = e.Value - } - away, _ := buscompose.LeaseHandedAway(raw, e != nil, err) - return away -} - // ---- declared -------------------------------------------------------------------------------------------- // DeclaredKept is one key of `declared`: a module's declarations as the manifest the indexer's `modules.` diff --git a/modules/mesh-config/cmd/mesh-config/takeover_test.go b/modules/mesh-config/cmd/mesh-config/takeover_test.go index 4df66d23..91ed0105 100644 --- a/modules/mesh-config/cmd/mesh-config/takeover_test.go +++ b/modules/mesh-config/cmd/mesh-config/takeover_test.go @@ -118,8 +118,7 @@ func TestCompareListsEveryDifferenceAndTheTakeOverWaitsForNone(t *testing.T) { } // The operator's Approve switches config to the indexer: pull-declares is refused, the list and the pull requests' -// config come from the indexer; hand-back is gone, and a switch naming the controller is read as the indexer; from the -// lease write a trusted value is set only on an Approve; declared is written once per manifest (ADR 0565 §3, §4, §13). +// config come from the indexer; hand-back is gone, and a switch naming the controller is read as the indexer; declared is written once per manifest (ADR 0565 §3, §4, §13). func TestTheTakeOverAndTheHandBack(t *testing.T) { c, m, r := anAgreeingMesh(t) steps := []struct { @@ -174,15 +173,6 @@ func TestTheTakeOverAndTheHandBack(t *testing.T) { res, err := c.Reconcile() return res.SourceGone + "|" + string(m.resolved(t, "g14", "dunst", "width").Value), err }, "is gone: config no longer reads its records"}, - {"the lease is written", func() (string, error) { - m.mirror(t, LeaseRead, "holder", `{"handed-to":"the operator's hand-over"}`) - return "lease", nil - }, "lease"}, - {"the terminal sets no trusted value after the lease write", func() (string, error) { - _, err := c.Change(Call{Action: "set", Key: "managed", Module: "claude-code", Node: "g14", Value: map[string]any{}, - Why: "x", Caller: ControllerCaller, By: "person.jochen"}) - return "", err - }, "propose"}, } for _, s := range steps { got, err := s.do() diff --git a/modules/mesh-config/module.json b/modules/mesh-config/module.json index b486b86e..f8ae2d62 100644 --- a/modules/mesh-config/module.json +++ b/modules/mesh-config/module.json @@ -37,8 +37,7 @@ "mesh-delivery-indexer.modules", "mesh-delivery-indexer.manifests", "mesh-delivery-indexer.predefined", - "mesh-delivery-indexer.config-candidates", - "mesh-controller.lease" + "mesh-delivery-indexer.config-candidates" ], "tools": [ "config_compare",