COND5: the keeper hears a machine away and back from the watchman

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
This commit is contained in:
2026-11-11 12:11:54 +01:00
parent 405729127d
commit 5c8f3b8351
4 changed files with 72 additions and 7 deletions
@@ -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) {
@@ -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 {
@@ -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 {
+3 -1
View File
@@ -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",