D12: the watchman serves the watchdogs seat's presence with its declared machine, answering that machine's online and since in the seat's answer (issue 1966, ADR 0596 §2; review round 3, finding 1)

The builder and the indexer ask it whether a node's machine is heard instead of reading the watchman's state; key and prefix stay for the whole read.

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
This commit is contained in:
2026-11-10 04:55:31 +01:00
parent 0f52bb80ad
commit 8e426b236d
2 changed files with 69 additions and 4 deletions
@@ -13,6 +13,7 @@ import (
"time"
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient"
"git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshwatchdogs"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/views"
@@ -105,6 +106,35 @@ var keyArgs = map[string]any{"type": "object", "properties": map[string]any{
"key": map[string]any{"type": "string", "description": "one key only"},
"prefix": map[string]any{"type": "string", "description": "only the keys that begin with it"}}}
// presenceArgs are the seat's input of presence, machine, beside the watchman's own key and prefix.
var presenceArgs = map[string]any{"type": "object", "properties": map[string]any{
"machine": map[string]any{"type": "string", "description": "one machine: whether the watchman hears it and since when"},
"key": map[string]any{"type": "string", "description": "one key only"},
"prefix": map[string]any{"type": "string", "description": "only the keys that begin with it"}}}
// presenceOf answers the seat's presence of one machine (meshwatchdogs.PresenceAnswer): online and since as the state
// presence holds it under the machine's key; a machine it holds no word of is not online, with no since (novox/hq ADR
// 0596 §2: a part asks this verb, never reads the state).
func presenceOf(st state, machine string) (any, error) {
e, err := st.Get(meshwatchdogs.PresenceByNodeKey(machine))
if err != nil {
return nil, fmt.Errorf("presence %s cannot be read: %w", machine, err)
}
answer := meshwatchdogs.PresenceAnswer{Machine: machine}
if e == nil {
return answer, nil
}
v, err := meshwatchdogs.DecodePresenceByNodeValue(e.Value)
if err != nil {
return nil, fmt.Errorf("presence %s does not read: %w", machine, err)
}
answer.Online = v.Online
if !e.At.IsZero() {
answer.Since = e.At.UTC().Format(time.RFC3339Nano)
}
return answer, nil
}
// Tools are the seat's verbs and the module's tools, over the states states gives and the program asked through ask.
func Tools(states func(string) state, ask func(socket.Ask) (json.RawMessage, error)) []stdio.Tool {
read := func(name string) func(map[string]any) (any, error) {
@@ -141,9 +171,15 @@ func Tools(states func(string) state, ask func(socket.Ask) (json.RawMessage, err
Description: "What stopped moving: every key of the watchman's stuck bucket, <bucket>.<key> with what it waited for since " +
"when, and whether it moved since (never deleted). A key not moved is also the watchman's finding " +
"delivery.<key>.stuck, a node's presence machine.<node>.silent. Give key for one, prefix to narrow. Read-only."},
{Name: verb(VerbPresence), Input: keyArgs, Run: read("presence"),
{Name: verb(VerbPresence), Input: presenceArgs, Run: func(args map[string]any) (any, error) {
if machine := str(args, "machine"); machine != "" {
return presenceOf(states("presence"), machine)
}
return read("presence")(args)
},
Description: "Who is online on the bus as the watchman hears it: each node's agent (<node>) and each delivery part's instance " +
"(part.<part>.<node>), online or not since the entry's time. Give key for one, prefix to narrow. Read-only."},
"(part.<part>.<node>), online or not since the entry's time. Give machine for one machine's online and since, " +
"key for one key, prefix to narrow. Read-only."},
{Name: verb(VerbDeadLetters), Run: dead,
Description: "What a consumer gave up on, kept in DEAD_LETTERS (novox/hq ADR 0564 §5, ADR 0264, ADR 0376): newest first, " +
"for one consumer (its name or <stream>.<consumer>), at most limit; one whole by id (a restricted one's body " +
@@ -4,9 +4,12 @@ import (
"encoding/json"
"errors"
"os"
"reflect"
"slices"
"testing"
"time"
"git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshwatchdogs"
stdio "git.novox.be/novox/mesh-sdk/go"
"mesh-delivery-watchman/internal/socket"
@@ -27,7 +30,7 @@ func (f fakeState) Get(key string) (*stdio.StateEntry, error) {
if !ok {
return nil, nil
}
return &stdio.StateEntry{Key: key, Value: json.RawMessage(v), Revision: 3}, nil
return &stdio.StateEntry{Key: key, Value: json.RawMessage(v), Revision: 3, At: time.Date(2026, 11, 10, 3, 0, 0, 0, time.UTC)}, nil
}
// 1952/WM1, ADR 0564 §12: every state the manifest keeps is read by a tool, every act the program starts by hand is
@@ -51,7 +54,8 @@ func TestEveryStateAndActHasATool(t *testing.T) {
t.Fatal(err)
}
asked := []socket.Ask{}
states := map[string]fakeState{"findings": {"machine.ace.silent": `{"v":1}`, "core.doctor.self-check-silent": `{"v":1}`}}
states := map[string]fakeState{"findings": {"machine.ace.silent": `{"v":1}`, "core.doctor.self-check-silent": `{"v":1}`},
"presence": {"ace": `{"v":1,"online":true}`, "g14": `{"v":1,"online":false}`, "part.mesh-delivery-builder.ace": `{"v":1,"online":true}`}}
tools := Tools(func(name string) state { return states[name] }, func(a socket.Ask) (json.RawMessage, error) {
asked = append(asked, a)
if a.Act != "" {
@@ -126,6 +130,31 @@ func TestEveryStateAndActHasATool(t *testing.T) {
t.Fatal("a key not held is answered")
}
})
// Issue 1966, ADR 0596 §2 (review round 3): presence with the seat's machine answers that machine's online and since in
// the seat's declared answer; one the watchman holds no word of is not online; without it, the keys as before.
t.Run("presence answers one machine as the seat declares", func(t *testing.T) {
for machine, want := range map[string]meshwatchdogs.PresenceAnswer{
"ace": {Machine: "ace", Online: true, Since: "2026-11-10T03:00:00Z"},
"g14": {Machine: "g14", Since: "2026-11-10T03:00:00Z"},
"novox": {Machine: "novox"},
} {
out, err := served[Seat+"."+VerbPresence].Run(map[string]any{"machine": machine})
if err != nil {
t.Fatal(err)
}
b, _ := json.Marshal(out)
var got meshwatchdogs.PresenceAnswer
if err := json.Unmarshal(b, &got); err != nil || !reflect.DeepEqual(got, want) {
t.Errorf("presence of %s = %s (%v), want %+v", machine, b, err, want)
}
}
out, _ := served[Seat+"."+VerbPresence].Run(map[string]any{"prefix": "part."})
b, _ := json.Marshal(out)
var whole meshwatchdogs.PresenceAnswer
if err := json.Unmarshal(b, &whole); err != nil || whole.Holds != 3 || len(whole.Keys) != 1 || whole.State != "presence" {
t.Errorf("presence whole = %s (%v)", b, err)
}
})
t.Run("dead-letters and resync ask the program", func(t *testing.T) {
if _, err := served[Seat+"."+VerbDeadLetters].Run(map[string]any{"consumer": "ace_web", "limit": float64(3), "id": float64(7)}); err != nil {
t.Fatal(err)