messenger: run a walk's bound from its started_at, as the controller's walk events say it (hq ADR 0308 §5)
mesh/merge-gate pass: builds messenger → novox; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
mesh/delivery superseded: a newer head of the same pull request

This commit is contained in:
2026-10-12 11:56:30 +02:00
parent 2927cb2ceb
commit 991ffcfda0
2 changed files with 35 additions and 4 deletions
+22 -3
View File
@@ -14,8 +14,12 @@ package main
// - `bound_seconds`: twice the largest delivery budget among its deliveries, or 900 when none has one (§5),
// on `started`; missing, 900 is taken;
// - `budget_class`: `core`, `leaf` or `none`;
// - `at` when the controller gives it, else the plan's `updated`: when it happened. A start older than its
// bound is history and opens nothing; an end older than FreshFor with nothing held closes quietly.
// - `started_at` on the start and `ended_at` on the end (else `at`, else the plan's `updated`): when it
// happened. A start older than its bound is history and opens nothing; an end older than FreshFor with
// nothing held closes quietly.
//
// A touch may name `carried` beside `modules` (what a moved tool runner or node-engine carries); the
// controller lists those in `modules` too, which is what is read.
//
// An event of its own named `walk`, with the same body, is read the same way, should the controller say it
// there instead.
@@ -87,6 +91,9 @@ type WalkEvent struct {
Touches []Touch
BoundSeconds int
At time.Time
// StartedAt is the walk's own start, on both events (`started_at`): the bound runs from it, so a router
// that hears a start late, or starts again, ends the hold on time.
StartedAt time.Time
}
// HeldMessage is one message a walk holds: when it was first held and what was to be said.
@@ -220,7 +227,16 @@ func DecodeWalk(body []byte) (WalkEvent, bool, error) {
}
}
}
for _, n := range []string{"at", "updated"} {
if s := str("started_at"); s != "" {
if t, err := time.Parse(time.RFC3339Nano, s); err == nil {
ev.StartedAt = t
}
}
when := []string{"started_at", "at", "updated"}
if ev.Phase == WalkEnded {
when = []string{"ended_at", "at", "updated"}
}
for _, n := range when {
if s := str(n); s != "" {
if t, err := time.Parse(time.RFC3339Nano, s); err == nil {
ev.At = t
@@ -297,6 +313,9 @@ func (h *Holder) Walked(ev WalkEvent) {
if w != nil {
delete(h.walks, ev.Walk)
w.Touches = mergeTouches(w.Touches, ev.Touches)
if !ev.StartedAt.IsZero() {
w.Started = ev.StartedAt
}
}
h.mu.Unlock()
if w == nil {
+13 -1
View File
@@ -10,11 +10,16 @@ import (
// The tests of novox/hq ADR 0308 "How it is checked" §2–§6, with the controller's walk events given by a fake
// source: the body the controller's plan-moved carries at a walk's start and end (ADR 0308 §1).
// walkBody is a plan-moved body as the controller says a walk's start or end.
// walkBody is a plan-moved body as the controller says a walk's start or end (mesh-controller pull request 247).
func walkBody(t *testing.T, id, phase, outcome string, at time.Time, bound int, touches ...Touch) []byte {
t.Helper()
b := map[string]any{"id": id, "state": "walking", "walk": id, "phase": phase, "touches": touches,
"updated": at.Format(time.RFC3339Nano), "budget_class": "leaf"}
if phase == WalkStarted {
b["started_at"] = at.Format(time.RFC3339Nano)
} else {
b["ended_at"] = at.Format(time.RFC3339Nano)
}
if outcome != "" {
b["outcome"] = outcome
}
@@ -413,6 +418,13 @@ func TestTheWalkEventsAreReadAsTheControllerSaysThem(t *testing.T) {
len(ev.Touches) != 1 || ev.Touches[0].Machine != "novox" || len(ev.Touches[0].Modules) != 2 {
t.Fatalf("%+v %v %v", ev, ok, err)
}
// As mesh-controller's walk events (its pull request 247) say an end: ended_at, and carried beside modules.
end, ok, err := DecodeWalk([]byte(`{"id":"plan-1","walk":"plan-1","phase":"ended","outcome":"stopped",` +
`"touches":[{"machine":"novox","modules":["node-tools","messenger"],"carried":["messenger"]}],` +
`"started_at":"2026-10-12T11:12:00Z","ended_at":"2026-10-12T11:19:00Z","updated":"2026-10-12T11:19:01Z"}`))
if err != nil || !ok || end.Outcome != WalkStopped || !end.At.Equal(at.Add(7*time.Minute)) || len(end.Touches[0].Modules) != 2 {
t.Fatalf("%+v %v %v", end, ok, err)
}
if _, ok, err := DecodeWalk([]byte(`{"id":"plan-1","state":"walking","tier":1}`)); ok || err != nil {
t.Fatalf("a move inside a walk read as its start or end: %v %v", ok, err)
}