diff --git a/modules/messenger/cmd/messenger/walks.go b/modules/messenger/cmd/messenger/walks.go index 6e51369f..8a96d9c3 100644 --- a/modules/messenger/cmd/messenger/walks.go +++ b/modules/messenger/cmd/messenger/walks.go @@ -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 { diff --git a/modules/messenger/cmd/messenger/walks_test.go b/modules/messenger/cmd/messenger/walks_test.go index 5d6b9f86..2208a75c 100644 --- a/modules/messenger/cmd/messenger/walks_test.go +++ b/modules/messenger/cmd/messenger/walks_test.go @@ -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) }