From 5c8f3b8351fe28834bb0fad3bab868cac6abe247 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 11 Nov 2026 12:11:54 +0100 Subject: [PATCH] COND5: the keeper hears a machine away and back from the watchman MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An observer on a machine away is not named in findings-unstated; a machine back has its observers asked again (mesh-watchdogs machine-away and machine-back in consumes). Builds: design 47 G §389 Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ --- .../cmd/mesh-conditions/events.go | 53 ++++++++++++++++++- .../cmd/mesh-conditions/keeper_test.go | 20 +++++-- .../cmd/mesh-conditions/manifest_test.go | 2 +- modules/mesh-conditions/module.json | 4 +- 4 files changed, 72 insertions(+), 7 deletions(-) diff --git a/modules/mesh-conditions/cmd/mesh-conditions/events.go b/modules/mesh-conditions/cmd/mesh-conditions/events.go index 830c8be39..4520787dc 100644 --- a/modules/mesh-conditions/cmd/mesh-conditions/events.go +++ b/modules/mesh-conditions/cmd/mesh-conditions/events.go @@ -16,6 +16,7 @@ import ( cc "git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient" seat "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions" + watchdogs "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshwatchdogs" stdio "git.novox.be/novox/mesh-sdk/go" ) @@ -40,6 +41,12 @@ func (k *Keeper) FindingEvent(e stdio.Envelope) error { if observer, ok := emitterOf(e.Key, EventFindingCleared); ok { return k.FindingCleared(observer, e.Node, e.Body) } + if _, ok := emitterOf(e.Key, watchdogs.EventMachineAway); ok && strings.HasPrefix(e.Key, watchdogs.Seat+".") { + return k.MachinePresence(e.Body, true) + } + if _, ok := emitterOf(e.Key, watchdogs.EventMachineBack); ok && strings.HasPrefix(e.Key, watchdogs.Seat+".") { + return k.MachinePresence(e.Body, false) + } if observer, ok := emitterOf(e.Key, EventFindingsStated); ok { return k.FindingsStated(observer, e.Node, e.Body) } @@ -148,10 +155,52 @@ func storedFinding(f seat.Finding) seat.FindingsByKeyValue { Ref: f.Ref, Run: f.Run, WaitingOn: f.WaitingOn, At: f.At} } +// MachinePresence takes the watchman's word that a machine is away or back (design 47 G §389): an observer on a machine +// away is not named in findings-unstated, and an ask is said again to the modules of a machine back. +func (k *Keeper) MachinePresence(body []byte, away bool) error { + var m struct { + Machine string `json:"machine"` + } + if err := json.Unmarshal(body, &m); err != nil || m.Machine == "" { + k.Logf("the watchman's word on a machine's presence is not readable: %v", err) + return nil + } + k.mu.Lock() + k.init() + if k.away == nil { + k.away = map[string]bool{} + } + if away { + k.away[m.Machine] = true + k.mu.Unlock() + return nil + } + delete(k.away, m.Machine) + var modules []string + for _, v := range k.views { + if v.node == m.Machine && v.observer != "" && !slices.Contains(modules, v.observer) { + modules = append(modules, v.observer) + } + } + k.mu.Unlock() + sort.Strings(modules) + var problems []string + for _, module := range modules { + if _, err := k.Ask(Module, module); err != nil { + problems = append(problems, err.Error()) + } + } + if len(problems) > 0 { + return errors.New(strings.Join(problems, "; ")) + } + return nil +} + // hear subscribes to the observers' events once the runtime serves, asked again until it takes each. func hear(k *Keeper) { - for _, event := range []string{EventFindingWritten, EventFindingCleared, EventFindingsStated} { - pattern := "*." + event + patterns := []string{"*." + EventFindingWritten, "*." + EventFindingCleared, "*." + EventFindingsStated, + watchdogs.Seat + "." + watchdogs.EventMachineAway, watchdogs.Seat + "." + watchdogs.EventMachineBack} + for _, pattern := range patterns { go func() { <-stdio.Served() for wait := 2 * time.Second; ; wait = min(wait*2, time.Minute) { diff --git a/modules/mesh-conditions/cmd/mesh-conditions/keeper_test.go b/modules/mesh-conditions/cmd/mesh-conditions/keeper_test.go index 4105d581f..f8206d4d4 100644 --- a/modules/mesh-conditions/cmd/mesh-conditions/keeper_test.go +++ b/modules/mesh-conditions/cmd/mesh-conditions/keeper_test.go @@ -539,12 +539,26 @@ func TestTheKeeperSetsItsViewFromWhatAnObserverStates(t *testing.T) { {"its next findings-stated clears it", func() error { return stated(watchman, "ace", ask, t0.Add(10*time.Minute), nil, f(watchman, "d")) }, []string{"machine.d.silent", "machine.g.silent"}, "its finding went"}, - {"a machine away is not named", func() error { + {"a machine away is not named, and a machine back is asked again", func() error { if _, err := r.k.Ask("jochen", ""); err != nil { return err } - r.k.away = map[string]bool{"g14": true} - return r.k.AskPassed(askedKey(selfCheck, "g14", asked[len(asked)-1]["ask"].(string))) + last := asked[len(asked)-1]["ask"].(string) + away := mustJSON(t, map[string]any{"machine": "g14", "at": r.now}) + if err := r.k.FindingEvent(stdio.Envelope{Key: "mesh-watchdogs.machine-away", Node: "ace", Body: away}); err != nil { + return err + } + if err := r.k.AskPassed(askedKey(selfCheck, "g14", last)); err != nil { + return err + } + before := len(asked) + if err := r.k.FindingEvent(stdio.Envelope{Key: "mesh-watchdogs.machine-back", Node: "ace", Body: away}); err != nil { + return err + } + if len(asked) != before+2 || asked[before]["module"] != watchman || asked[before+1]["module"] != selfCheck { + t.Errorf("a machine back was asked %v", asked[before:]) + } + return nil }, []string{"machine.d.silent", "machine.g.silent"}, ""}, {"resync names one module: its hand act and its ask", func() error { if _, err := r.k.ResyncVerb("person.jochen", selfCheck); err != nil { diff --git a/modules/mesh-conditions/cmd/mesh-conditions/manifest_test.go b/modules/mesh-conditions/cmd/mesh-conditions/manifest_test.go index 98ef5b04b..066f9b6b9 100644 --- a/modules/mesh-conditions/cmd/mesh-conditions/manifest_test.go +++ b/modules/mesh-conditions/cmd/mesh-conditions/manifest_test.go @@ -38,7 +38,7 @@ func TestTheManifestSaysWhatTheCodeDoes(t *testing.T) { reads := []string{HavesRead, AttemptsRead} if !slices.Equal(m.Invokes, []string{WantsTool, AssignmentsTool, "seat:mesh-hand-acts.hand-act"}) || !slices.Equal(m.Consumes, []string{"mesh-delivery-assigner.views.wants", "mesh-delivery-assigner.views.assignments", - "*.finding-written", "*.finding-cleared", "*.findings-stated"}) { + "*.finding-written", "*.finding-cleared", "*.findings-stated", "mesh-watchdogs.machine-away", "mesh-watchdogs.machine-back"}) { t.Errorf("invokes %v, consumes %v", m.Invokes, m.Consumes) } for _, r := range m.Reads { diff --git a/modules/mesh-conditions/module.json b/modules/mesh-conditions/module.json index 7f1a0c112..3414ae96b 100644 --- a/modules/mesh-conditions/module.json +++ b/modules/mesh-conditions/module.json @@ -27,7 +27,9 @@ "mesh-delivery-assigner.views.assignments", "*.finding-written", "*.finding-cleared", - "*.findings-stated" + "*.findings-stated", + "mesh-watchdogs.machine-away", + "mesh-watchdogs.machine-back" ], "state": [ "conditions",