From 432de55186441b83853c5c43736fd6a0587dac47 Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 2 Nov 2026 00:17:52 +0100 Subject: [PATCH] =?UTF-8?q?mesh-hand-acts=20and=20mesh-healers=20stop=20re?= =?UTF-8?q?ading=20the=20controller's=20hand-act=20log=20(ADR=200571=20?= =?UTF-8?q?=C2=A713)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit mesh-hand-acts lists acts from its own log alone and no longer copies: its copy tools answer that the copy is over, with the marker of the copy that was made (the live top-up runs before this delivery). mesh-healers drops the watch of mesh-controller_hand-acts and counts the seat's acts: it asks the seat's `hand-acts` once at its start for the fortnight (new invoke) and hears act-recorded after, an act listed and heard counting once, so a restart does not forget the fortnight. Tests: the two-log listing and the copy table became one listing and one copy-is-over test; the healers' count test lost its controller-bucket helper and gained the seat listing; the integration test now proves the old bucket is not copied. (issue 774, 774/S10) Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL --- .../mesh-hand-acts/cmd/mesh-hand-acts/acts.go | 88 +++-------- .../cmd/mesh-hand-acts/acts_test.go | 124 ++++------------ .../mesh-hand-acts/cmd/mesh-hand-acts/copy.go | 140 +++--------------- .../mesh-hand-acts/cmd/mesh-hand-acts/main.go | 13 +- .../cmd/mesh-hand-acts/manifest_test.go | 4 +- .../cmd/mesh-hand-acts/tools.go | 15 +- .../mesh-hand-acts/whole_integration_test.go | 21 +-- modules/mesh-hand-acts/module.json | 1 - .../mesh-healers/cmd/mesh-healers/healers.go | 1 - .../cmd/mesh-healers/healers_test.go | 12 +- modules/mesh-healers/cmd/mesh-healers/main.go | 28 +++- .../cmd/mesh-healers/manifest_test.go | 4 +- .../mesh-healers/cmd/mesh-healers/wanted.go | 49 +++--- modules/mesh-healers/module.json | 6 +- 14 files changed, 143 insertions(+), 363 deletions(-) diff --git a/modules/mesh-hand-acts/cmd/mesh-hand-acts/acts.go b/modules/mesh-hand-acts/cmd/mesh-hand-acts/acts.go index 120a736f..858faf67 100644 --- a/modules/mesh-hand-acts/cmd/mesh-hand-acts/acts.go +++ b/modules/mesh-hand-acts/cmd/mesh-hand-acts/acts.go @@ -10,15 +10,14 @@ package main // **Who did it is the runtime's word, never the caller's**: `by` is the bus user the runtime proved. A warrant is the // router's word, never the caller's: `warranted` reads the router's own record of the asker's ask and records who // chose, through which channel, with which proofs and which answer, once per ask. The controller's bucket -// `mesh-controller_hand-acts` stays its own until it loses its lease and is read, never written: `hand-acts` answers -// over both logs, and no act is written to both (ADR 0565 §11). +// `mesh-controller_hand-acts` is no longer read (issue 774, ADR 0571 §13): its acts were copied into this log once, and +// `hand-acts` answers from this log alone. import ( "encoding/json" "errors" "fmt" "regexp" - "slices" "sort" "strconv" "strings" @@ -282,33 +281,16 @@ type Listing struct { Fill *Fill `json:"fill,omitempty"` } -// HandActs reads both logs. +// HandActs reads this seat's log alone: what was copied from the controller's is in it (copy.go). func (l *Log) HandActs(days int, cause, who string) Listing { now := l.Now() since := now.Add(-time.Duration(days) * 24 * time.Hour) out := Listing{Acts: []Act{}, Repeated: map[string]int{}, Days: days, Kept: map[string]int{}} - var all []Act - // What was copied from the controller's log is in this one: it is read from this one only (copy.go). - copied := map[string]bool{} - for _, log := range []string{ActsLog, ControllerActs} { - acts, kept, err := l.read(log, now.Add(-max(time.Duration(days)*24*time.Hour, RepeatedWithin))) - if log == ActsLog && err == nil { - for _, a := range acts { - copied[a.ID] = a.ID != "" - } - } - if log == ControllerActs { - acts = slices.DeleteFunc(acts, func(a Act) bool { return a.ID != "" && copied[a.ID] }) - } - if err != nil { - if out.Unread == nil { - out.Unread = map[string]string{} - } - out.Unread[log] = err.Error() - continue - } - out.Kept[log] = kept - all = append(all, acts...) + all, kept, err := l.read(now.Add(-max(time.Duration(days)*24*time.Hour, RepeatedWithin))) + if err != nil { + out.Unread = map[string]string{ActsLog: err.Error()} + } else { + out.Kept[ActsLog] = kept } for _, a := range all { if now.Sub(a.At) <= RepeatedWithin && a.Repair() { @@ -331,56 +313,20 @@ func (l *Log) HandActs(days int, cause, who string) Listing { return out } -// read is every act of one log since a moment, and how many it keeps: this seat's log entry by entry, the -// controller's bucket by its keys, each of which names its time, so only those within are read whole. -func (l *Log) read(log string, since time.Time) ([]Act, int, error) { +// read is every act of this seat's log since a moment, entry by entry, and how many it keeps. +func (l *Log) read(since time.Time) ([]Act, int, error) { var out []Act - keep := func(raw []byte, from string) { - var a Act - if json.Unmarshal(raw, &a) != nil || a.At.Before(since) { - return - } - a.Log = from - out = append(out, a) - } - if log == ActsLog { - entries, err := l.Store.Entries(ActsLog) - if err != nil { - return nil, 0, err - } - for _, raw := range entries { - keep(raw, handacts.LogSeat) - } - return out, len(entries), nil - } - keys, err := l.Store.Keys(log) + entries, err := l.Store.Entries(ActsLog) if err != nil { return nil, 0, err } - for _, k := range keys { - if at, ok := keyTime(k); ok && at.Before(since) { + for _, raw := range entries { + var a Act + if json.Unmarshal(raw, &a) != nil || a.At.Before(since) { continue } - raw, found, err := l.Store.Get(log, k) - if err != nil { - return nil, len(keys), err - } - if found { - keep(raw, handacts.LogController) - } + a.Log = handacts.LogSeat + out = append(out, a) } - return out, len(keys), nil -} - -// keyTime is the time an act's key names (`act--`), when it names one. -func keyTime(key string) (time.Time, bool) { - parts := strings.Split(key, "-") - if len(parts) != 3 || parts[0] != "act" { - return time.Time{}, false - } - ns, err := strconv.ParseInt(parts[1], 10, 64) - if err != nil { - return time.Time{}, false - } - return time.Unix(0, ns), true + return out, len(entries), nil } diff --git a/modules/mesh-hand-acts/cmd/mesh-hand-acts/acts_test.go b/modules/mesh-hand-acts/cmd/mesh-hand-acts/acts_test.go index ea501dad..e0add639 100644 --- a/modules/mesh-hand-acts/cmd/mesh-hand-acts/acts_test.go +++ b/modules/mesh-hand-acts/cmd/mesh-hand-acts/acts_test.go @@ -3,7 +3,6 @@ package main import ( "encoding/json" "errors" - "fmt" "sort" "strings" "sync" @@ -229,9 +228,10 @@ func TestWarrantedRecordsTheRoutersWordOnce(t *testing.T) { } } -// hand-acts answers over both logs, newest first, narrowed by days, cause and who, and counts each cause repaired by -// hand twice in a fortnight across both, never a person's decision; a log it cannot read is said, never read as empty. -func TestHandActsReadsBothLogs(t *testing.T) { +// hand-acts answers from this seat's log alone, newest first, narrowed by days, cause and who, and counts each cause +// repaired by hand twice in a fortnight, never a person's decision; a log it cannot read is said, never read as empty. +// The controller's bucket holds acts here and is made to fail: it is not read (774/S10/AC1). +func TestHandActsReadsItsOwnLogAlone(t *testing.T) { l, store, _, now := newLog() put := func(log, verb, cause, by string, ago time.Duration) { at := now.Add(-ago) @@ -243,21 +243,22 @@ func TestHandActsReadsBothLogs(t *testing.T) { _ = store.Put(log, actKey(at), a) } put(ActsLog, VerbRecord, "relogin", "person.jochen", time.Hour) - put(ControllerActs, VerbRecord, "relogin", "shanks.claude-code", 3*24*time.Hour) - put(ControllerActs, VerbDrill, CauseDrill, "person.jochen", time.Hour) + put(ActsLog, VerbRecord, "relogin", "shanks.claude-code", 3*24*time.Hour) put(ActsLog, VerbDrill, CauseDrill, "person.jochen", 2*time.Hour) put(ActsLog, VerbWarrant, CauseOperatorAnswer, "the operator", time.Hour) put(ActsLog, VerbWarrant, CauseOperatorAnswer, "the operator", 2*time.Hour) - put(ControllerActs, VerbRecord, "config", "ace.mesh-config", 30*24*time.Hour) + put(ActsLog, VerbRecord, "config", "ace.mesh-config", 30*24*time.Hour) + put(ControllerActs, VerbRecord, "never-read", "ace.old", time.Hour) + store.keysErr[ControllerActs] = errors.New("the controller's bucket must not be read") cases := []struct { name string days int cause, who string want int }{ - {"a fortnight", 14, "", "", 6}, - {"a day", 1, "", "", 5}, - {"forty days", 40, "", "", 7}, + {"a fortnight", 14, "", "", 5}, + {"a day", 1, "", "", 4}, + {"forty days", 40, "", "", 6}, {"by cause", 14, "relogin", "", 2}, {"by who", 14, "", "claude-code", 1}, } @@ -272,12 +273,12 @@ func TestHandActsReadsBothLogs(t *testing.T) { } } } - if got := l.HandActs(14, "", ""); got.Acts[0].Log != Seat || got.Kept[ControllerActs] != 3 || got.Fill == nil || - got.Fill.Entries != 4 { + if got := l.HandActs(14, "", ""); got.Acts[0].Log != Seat || got.Kept[ActsLog] != 6 || len(got.Kept) != 1 || got.Fill == nil || + got.Fill.Entries != 6 { t.Errorf("the log of an act or the kept counts are not said: %+v", got.Kept) } - store.keysErr[ControllerActs] = errors.New("the bus is away") - if got := l.HandActs(14, "", ""); got.Unread[ControllerActs] == "" || len(got.Acts) != 4 { + store.keysErr[ActsLog] = errors.New("the bus is away") + if got := l.HandActs(14, "", ""); got.Unread[ActsLog] == "" || len(got.Acts) != 0 { t.Errorf("an unreadable log: %+v", got.Unread) } w, err := l.Warrants() @@ -286,86 +287,21 @@ func TestHandActsReadsBothLogs(t *testing.T) { } } -// The controller's hand acts are copied into this seat's log once, each with its own key, time and author, and -// never twice: a marker makes it once, a key already in the log is not appended again, a log that cannot be read or -// written leaves no marker so the next call copies again, and hand-acts counts a copied act once (research 072 row 26). -func TestTheControllersHandActsAreCopiedOnce(t *testing.T) { - base := time.Date(2026, 10, 28, 0, 0, 0, 0, time.UTC) - old := []string{fmt.Sprintf("act-%d-1", base.UnixNano()), fmt.Sprintf("act-%d-2", base.UnixNano()+1), - fmt.Sprintf("act-%d-3", base.UnixNano()+2), "warrant-x-y"} - cases := []struct { - name string - already []string // keys in the new log before the copy - oldErr, newErr error - junk bool - wantErr string - wantCopied, wantAlready int - wantMarker bool - }{ - {"all", nil, nil, nil, false, "", 4, 0, true}, - {"finishes a half copy", []string{old[0], old[1]}, nil, nil, false, "", 2, 2, true}, - {"an entry that is no act is copied unchanged", nil, nil, nil, true, "", 5, 0, true}, - {"the old log cannot be read", nil, errors.New("away"), nil, false, "cannot be read", 0, 0, false}, - {"the new log cannot be written", nil, nil, errors.New("full"), false, "could not be copied", 0, 0, false}, +// The copy of the controller's hand acts is over: the tools answer so, with the marker of the copy that was made, and +// read and write nothing of the controller's bucket nor of the log (774/S10; research 072 row 26). +func TestTheCopyOfTheControllersHandActsIsOver(t *testing.T) { + l, store, _, now := newLog() + store.keysErr[ControllerActs] = errors.New("the controller's bucket must not be read") + st, err := l.Copy("test", true) + if err != nil || st.Done || !strings.Contains(st.Said, "no longer read") { + t.Errorf("without a marker: %+v, %v", st, err) } - for _, tc := range cases { - l, store, _, _ := newLog() - for _, k := range old { - _ = store.Put(ControllerActs, k, Act{ID: k, At: base, By: "ace.mesh-config", Verb: VerbRecord, Why: "w", Cause: "c"}) - } - if tc.junk { - _ = store.Put(ControllerActs, fmt.Sprintf("act-%d-4", base.UnixNano()+3), "not an act") - } - for _, k := range tc.already { - _ = store.Append(ActsLog, k, Act{ID: k}) - } - store.keysErr[ControllerActs], store.keysErr[ActsLog] = tc.oldErr, tc.newErr - st, err := l.Copy("test", false) - if tc.wantErr != "" { - if err == nil || !strings.Contains(err.Error(), tc.wantErr) { - t.Errorf("%s: %v", tc.name, err) - } - } else if err != nil || !st.Done || st.Marker.Copied != tc.wantCopied || st.Marker.Already != tc.wantAlready { - t.Fatalf("%s: %+v, %v", tc.name, st, err) - } - _, marked, _ := store.Get(CopiedState, CopyKey) - if marked != tc.wantMarker || len(store.logKeys(ActsLog)) != map[bool]int{true: tc.wantCopied + tc.wantAlready, false: len(tc.already)}[tc.wantMarker] { - t.Errorf("%s: marker %v, log %v", tc.name, marked, store.logKeys(ActsLog)) - } - if !tc.wantMarker { - continue - } - // Each act is kept with its own time and author, as the controller wrote it. - raw, _, _ := store.Last(ActsLog, old[2]) - var kept Act - _ = json.Unmarshal(raw, &kept) - if kept.By != "ace.mesh-config" || !kept.At.Equal(base) { - t.Errorf("%s: kept %+v", tc.name, kept) - } - // Asked again: nothing more is appended, and hand-acts counts each act once. - before := len(store.logKeys(ActsLog)) - if again, err := l.Copy("test", false); err != nil || !again.Done || !strings.Contains(again.Said, "already copied") || - len(store.logKeys(ActsLog)) != before { - t.Errorf("%s: a second copy: %+v, %v", tc.name, again, err) - } - if got := l.HandActs(30, "", ""); got.Kept[ControllerActs] != len(old)+boolInt(tc.junk) || len(got.Acts) != len(old) { - t.Errorf("%s: %d acts listed from %d kept: %+v", tc.name, len(got.Acts), before, got.Kept) - } - // An act the controller wrote since is appended by a top-up, and only that one. - late := fmt.Sprintf("act-%d-9", base.UnixNano()+9) - _ = store.Put(ControllerActs, late, Act{ID: late, At: base, By: "ace.late", Verb: VerbRecord, Why: "w", Cause: "c"}) - if up, err := l.Copy("test", true); err != nil || up.Marker.Copied != 1 || len(store.logKeys(ActsLog)) != before+1 { - t.Errorf("%s: a top-up: %+v, %v", tc.name, up, err) - } - if s, err := l.Copied(); err != nil || !s.Done || s.Marker == nil { - t.Errorf("%s: copied says %+v, %v", tc.name, s, err) - } + _ = store.Put(CopiedState, CopyKey, CopyMarker{At: *now, By: "start", Source: ControllerActs, Found: 606, Copied: 606}) + st, err = l.Copy("test", false) + if err != nil || !st.Done || st.Marker.Copied != 606 || !strings.Contains(st.Said, "no longer read") || len(store.logKeys(ActsLog)) != 0 { + t.Errorf("with a marker: %+v, %v", st, err) + } + if s, err := l.Copied(); err != nil || !s.Done || s.Marker == nil { + t.Errorf("copied says %+v, %v", s, err) } } - -func boolInt(b bool) int { - if b { - return 1 - } - return 0 -} diff --git a/modules/mesh-hand-acts/cmd/mesh-hand-acts/copy.go b/modules/mesh-hand-acts/cmd/mesh-hand-acts/copy.go index 0c289388..65a492c6 100644 --- a/modules/mesh-hand-acts/cmd/mesh-hand-acts/copy.go +++ b/modules/mesh-hand-acts/cmd/mesh-hand-acts/copy.go @@ -1,22 +1,14 @@ package main -// The one-time copy of the controller's hand-act log into this seat's log (novox/hq research 072 row 26, ADR 0571 -// §13). The controller kept every act of its life in its bucket `mesh-controller_hand-acts` (574 on 2026-10-31); from -// the lease write only this seat reads them, so they are copied once into `acts`, each as the controller wrote it: the -// same key, the same bytes, so its time and its author are what they were. -// -// **When it runs.** The lease write has no hook to run from (it is a person's step, ADR 0561 §6), so the copy runs at -// this module's start while the old log is readable and no marker says it was copied, and on the tool -// `hand_acts_copy`. Both do the same thing, and neither does it twice (a `top_up` of the tool appends only what is new): the marker `controller-hand-acts` of the state -// `copied` is created once, after every act is in the log; an act already in the log (by its key) is never appended -// again, so a copy that stopped half way is finished by the next one. Until the marker exists `hand-acts` hides an -// old act that is already copied, so the two logs never count one act twice. `hand_acts_copied` says where it stands. +// The one-time copy of the controller's hand-act log into this seat's log is over (novox/hq research 072 row 26, ADR 0571 +// §13, issue 774). The controller kept every act of its life in its bucket `mesh-controller_hand-acts`; they were copied +// once into `acts`, each as the controller wrote it, by the earlier builds of this module, which left the marker +// `controller-hand-acts` of the state `copied`. This module no longer reads that bucket, so the copy cannot run: the +// tools `hand_acts_copy` and `hand_acts_copied` stay and say so, with the marker's figures when it exists. import ( "encoding/json" - "errors" "fmt" - "sort" "time" ) @@ -47,92 +39,32 @@ type CopyStatus struct { Marker *CopyMarker `json:"marker,omitempty"` // Said is a sentence for a person: what this call did or found. Said string `json:"said"` - // Pending are the old acts not yet in this seat's log (counted only while the copy is not whole). - Pending *int `json:"pending,omitempty"` - Old *int `json:"old-log-keys,omitempty"` - New *int `json:"new-log-entries,omitempty"` + // New is how many acts this seat's log holds. + New *int `json:"new-log-entries,omitempty"` } -// Copy copies the old log into this one, once. by says what asked (`start` or a caller). With topUp it also runs after -// the marker exists, appending only the acts the log does not hold yet (the controller's, written since the copy), and -// leaves the marker as it was. An old log that cannot be -// read, or an act that cannot be appended, is an error and leaves no marker, so the next call copies again. +// gone is what a call to copy answers. +const gone = "the controller's hand-act log is no longer read, so nothing can be copied (novox/hq ADR 0571 §13, issue 774); " + + "the acts copied before are in the log `acts`" + +// Copy answers that the copy is over: it reads and writes nothing of the controller's. The marker, when the copy once +// ran to its end, is returned with the answer. func (l *Log) Copy(by string, topUp bool) (CopyStatus, error) { - marked := false - if raw, found, err := l.Store.Get(CopiedState, CopyKey); err != nil { - return CopyStatus{}, fmt.Errorf("whether the controller's hand acts were copied cannot be read: %w", err) - } else if found { - var m CopyMarker - if err := json.Unmarshal(raw, &m); err != nil { - return CopyStatus{}, fmt.Errorf("the copy's marker is not readable: %w", err) - } - if !topUp { - return CopyStatus{Done: true, Marker: &m, Said: fmt.Sprintf("already copied at %s: %d acts of %s, nothing done", - m.At.Format(time.RFC3339), m.Copied+m.Already, m.Source)}, nil - } - marked = true - } - keys, err := l.Store.Keys(ControllerActs) + st, err := l.Copied() if err != nil { - return CopyStatus{}, fmt.Errorf("the controller's hand acts cannot be read, so none were copied: %w", err) + return st, err } - sortByTime(keys) - m := CopyMarker{By: by, Source: ControllerActs, Found: len(keys)} - l.Logf("[%s] copying the controller's hand acts into %s: %d found in %s", Module, ActsLog, len(keys), ControllerActs) - for i, k := range keys { - raw, found, err := l.Store.Get(ControllerActs, k) - if err != nil { - return CopyStatus{}, fmt.Errorf("%s of %s cannot be read after %d of %d acts were copied: %w", k, ControllerActs, m.Copied, len(keys), err) - } - if !found { - continue // removed since it was listed - } - if _, there, err := l.Store.Last(ActsLog, k); err != nil { - return CopyStatus{}, fmt.Errorf("whether %s is in %s cannot be read, after %d acts were copied: %w", k, ActsLog, m.Copied, err) - } else if there { - m.Already++ - continue - } - var a Act - if json.Unmarshal(raw, &a) != nil { - m.Unreadable = append(m.Unreadable, k) - l.Logf("[%s] %s of %s is not an act; copied unchanged", Module, k, ControllerActs) - } - if err := l.Store.Append(ActsLog, k, json.RawMessage(raw)); err != nil { - return CopyStatus{}, fmt.Errorf("%s could not be copied into %s after %d acts: %w", k, ActsLog, m.Copied, err) - } - m.Copied++ - if m.FirstKey == "" { - m.FirstKey = k - } - m.LastKey = k - if (i+1)%100 == 0 { - l.Logf("[%s] copied %d of %d", Module, i+1, len(keys)) - } - } - m.At = l.Now().UTC() - if marked { - l.Logf("[%s] topped up the copy: %d acts copied, %d already there, of %d found", Module, m.Copied, m.Already, m.Found) - return CopyStatus{Done: true, Marker: &m, Said: fmt.Sprintf("topped up: %d acts copied that the first copy did not hold (%d already there)", m.Copied, m.Already)}, nil - } - if err := l.Store.Create(CopiedState, CopyKey, m); errors.Is(err, ErrExists) { - return CopyStatus{Said: "another instance finished the copy first; nothing more done"}, nil - } else if err != nil { - return CopyStatus{}, fmt.Errorf("every act was copied but the marker could not be written, so the next call checks them again: %w", err) - } - l.Logf("[%s] copied the controller's hand acts: %d copied, %d already there, %d not acts, of %d found (%s .. %s)", - Module, m.Copied, m.Already, len(m.Unreadable), m.Found, m.FirstKey, m.LastKey) - return CopyStatus{Done: true, Marker: &m, Said: fmt.Sprintf("copied %d acts (%d were already there) of %d found in %s", - m.Copied, m.Already, m.Found, ControllerActs)}, nil + st.Said = gone + "; " + st.Said + return st, nil } -// Copied says where the copy stands without doing anything: the marker, or how many old acts wait. +// Copied says where the copy ended: the marker with its figures, or that none was left, and how many acts the log holds. func (l *Log) Copied() (CopyStatus, error) { raw, found, err := l.Store.Get(CopiedState, CopyKey) if err != nil { return CopyStatus{}, fmt.Errorf("the copy's marker cannot be read: %w", err) } - var st CopyStatus + st := CopyStatus{Said: "no marker of a finished copy is left"} if found { var m CopyMarker if err := json.Unmarshal(raw, &m); err != nil { @@ -145,39 +77,5 @@ func (l *Log) Copied() (CopyStatus, error) { n := int(f.Entries) st.New = &n } - keys, err := l.Store.Keys(ControllerActs) - if err != nil { - if !st.Done { - st.Said = "not copied yet, and the controller's log cannot be read: " + err.Error() - } - return st, nil - } - n := len(keys) - st.Old = &n - if !st.Done { - pending := 0 - for _, k := range keys { - if _, there, err := l.Store.Last(ActsLog, k); err != nil || !there { - pending++ - } - } - st.Pending = &pending - st.Said = fmt.Sprintf("not copied yet: %d of %d old acts wait; hand_acts_copy copies them", pending, n) - } return st, nil } - -// sortByTime orders keys oldest first by the time they name, those that name none after, by key. -func sortByTime(keys []string) { - sort.SliceStable(keys, func(i, j int) bool { - ti, oi := keyTime(keys[i]) - tj, oj := keyTime(keys[j]) - switch { - case oi && oj && !ti.Equal(tj): - return ti.Before(tj) - case oi != oj: - return oi - } - return keys[i] < keys[j] - }) -} diff --git a/modules/mesh-hand-acts/cmd/mesh-hand-acts/main.go b/modules/mesh-hand-acts/cmd/mesh-hand-acts/main.go index 7c1a07c0..f8e6e5f9 100644 --- a/modules/mesh-hand-acts/cmd/mesh-hand-acts/main.go +++ b/modules/mesh-hand-acts/cmd/mesh-hand-acts/main.go @@ -1,7 +1,7 @@ // mesh-hand-acts: the holder of the seat mesh-hand-acts (novox/hq ADR 0565 §10). A Go bundle served by a runtime of // its own, on its own bus account (`runs-as`), so that its account alone writes the hand-act log (`acts`, its module -// log: a stream that keeps every act in order) and `warrants`. It reads the router's record of each ask for a warrant and the controller's log for -// `hand-acts`. stdout is the MCP channel; what this module says, it says on stderr. +// log: a stream that keeps every act in order) and `warrants`. It reads the router's record of each ask for a warrant; the controller's log is no longer read +// (issue 774). stdout is the MCP channel; what this module says, it says on stderr. package main import ( @@ -77,15 +77,6 @@ func (runtimeStore) Fill(log string) (Fill, error) { func main() { l := &Log{Store: runtimeStore{}, Now: time.Now, Logf: logf, Publish: stdio.SeatPublish} - // No hook of the lease write runs this module, so the one-time copy of the controller's hand acts runs at start - // and on the tool hand_acts_copy; the marker makes it once (copy.go). - go func() { - if st, err := l.Copy("start", false); err != nil { - logf("[%s] the copy of the controller's hand acts is NOT done: %v", Module, err) - } else { - logf("[%s] %s", Module, st.Said) - } - }() if err := stdio.Serve("", Tools(l)); err != nil { logf("%v", err) os.Exit(1) diff --git a/modules/mesh-hand-acts/cmd/mesh-hand-acts/manifest_test.go b/modules/mesh-hand-acts/cmd/mesh-hand-acts/manifest_test.go index 05c9d53b..564033cc 100644 --- a/modules/mesh-hand-acts/cmd/mesh-hand-acts/manifest_test.go +++ b/modules/mesh-hand-acts/cmd/mesh-hand-acts/manifest_test.go @@ -43,10 +43,10 @@ func TestTheManifestSaysWhatTheCodeDoes(t *testing.T) { t.Errorf("claims %+v", m.Claims) } // act-recorded is the seat's event, said on the seat's subject: a module lists no event of a seat it claims. - if len(m.Emits) != 0 || !slices.Equal(m.Reads, []string{ControllerActs, RouterRecords}) { + if len(m.Emits) != 0 || !slices.Equal(m.Reads, []string{RouterRecords}) { t.Errorf("emits %v, reads %v", m.Emits, m.Reads) } - readBy := map[string]string{ActsLog: HandActsTool, WarrantsState: HandActsTool, ControllerActs: HandActsTool, + readBy := map[string]string{ActsLog: HandActsTool, WarrantsState: HandActsTool, RouterRecords: WarrantedTool, CopiedState: CopiedTool} tools := map[string]bool{} for _, tool := range Tools(&Log{}) { diff --git a/modules/mesh-hand-acts/cmd/mesh-hand-acts/tools.go b/modules/mesh-hand-acts/cmd/mesh-hand-acts/tools.go index ad51f7ed..3944137d 100644 --- a/modules/mesh-hand-acts/cmd/mesh-hand-acts/tools.go +++ b/modules/mesh-hand-acts/cmd/mesh-hand-acts/tools.go @@ -52,8 +52,7 @@ func Tools(l *Log) []stdio.Tool { return l.HandAct(stdio.CallerOf(a), arg(a, "verb"), arg(a, "what"), arg(a, "why"), arg(a, "cause"), arg(a, "condition")) }}, {Name: HandActsTool, - Description: "What was done by hand lately, over both logs (this seat's and the controller's until its lease " + - "ends), newest first: each act with who, why, its cause and which log holds it, and the causes repaired by " + + Description: "What was done by hand lately, from this seat's log (the controller's acts were copied into it), newest first: each act with who, why, its cause and which log holds it, and the causes repaired by " + "hand twice or more within a fortnight (a healer is wanted). Narrowed by cause and who; warrants=true lists " + "every warrant recorded instead.", Input: obj(map[string]any{"days": map[string]any{"type": "number", "description": "how many days back (default 14)"}, @@ -74,16 +73,14 @@ func Tools(l *Log) []stdio.Tool { return l.Warranted(stdio.CallerOf(a), arg(a, "asker"), arg(a, "ask")) }}, {Name: CopiedTool, - Description: "Where the one-time copy of the controller's hand acts into this seat's log stands (novox/hq research 072 " + - "row 26): done, with when, by what and how many acts were copied, or how many old acts still wait, and how " + - "full both logs are. Changes nothing.", + Description: "Where the one-time copy of the controller's hand acts into this seat's log ended (novox/hq research 072 " + + "row 26): the marker with when, by what and how many acts were copied, and how full the log is. The copy " + + "cannot run any more: the controller's log is no longer read. Changes nothing.", Input: obj(map[string]any{}), Run: func(map[string]any) (any, error) { return l.Copied() }}, {Name: CopyTool, - Description: "Copy the controller's hand acts (its bucket mesh-controller_hand-acts) into this seat's log once, each " + - "with its own key, time and author. Does nothing when the copy is already done; an act already in the log is " + - "never appended again, so a copy that stopped is finished by asking again. With top_up=true it also runs after the " + - "copy is done and appends only the acts written since. This also runs at the module's start.", + Description: "Answers that the copy is over (the controller's hand-act log, its bucket mesh-controller_hand-acts, " + + "is no longer read, novox/hq ADR 0571 §13) with the marker of the copy that was made; copies nothing.", Input: obj(map[string]any{"top_up": str("\"true\": after the copy is done, append the acts written since")}), Run: func(a map[string]any) (any, error) { return l.Copy("tool, called by "+stdio.CallerOf(a), a["top_up"] == true || strings.EqualFold(arg(a, "top_up"), "true")) diff --git a/modules/mesh-hand-acts/cmd/mesh-hand-acts/whole_integration_test.go b/modules/mesh-hand-acts/cmd/mesh-hand-acts/whole_integration_test.go index 8977994d..555208eb 100644 --- a/modules/mesh-hand-acts/cmd/mesh-hand-acts/whole_integration_test.go +++ b/modules/mesh-hand-acts/cmd/mesh-hand-acts/whole_integration_test.go @@ -354,7 +354,7 @@ func TestTheHandActLogWholeRecordsOnTheBus(t *testing.T) { if err != nil { t.Fatal(err) } - // One act of the controller's, kept before the module starts: the start copies it into the log. + // One act of the controller's, kept before the module starts: the module no longer reads that bucket, so it never copies it. oldActs, _ := js.KeyValue(ctx, "mesh-controller_hand-acts") oldKey := fmt.Sprintf("act-%d-1", time.Now().Add(-time.Hour).UnixNano()) if _, err := oldActs.Put(ctx, oldKey, []byte(`{"id":"`+oldKey+`","at":"`+time.Now().Add(-time.Hour).UTC().Format(time.RFC3339)+ @@ -402,22 +402,17 @@ func TestTheHandActLogWholeRecordsOnTheBus(t *testing.T) { t.Fatal(err) } } - for i := 0; ; i++ { - if c, err := p.call(CopiedTool, "person.jochen", map[string]any{}); err == nil && strings.Contains(string(c), `"done":true`) { - break - } else if i > 50 { - t.Fatalf("the controller's hand acts were not copied at the start: %s, %v", c, err) - } - time.Sleep(200 * time.Millisecond) + if c, err := p.call(CopyTool, "person.jochen", map[string]any{}); err != nil || !strings.Contains(string(c), "no longer read") || strings.Contains(string(c), `"done":true`) { + t.Errorf("the copy's tool answered %s, %v", c, err) } - if e, err := logStream.GetLastMsgForSubject(ctx, "mesh.log."+Module+"."+ActsLog+"."+oldKey); err != nil || !strings.Contains(string(e.Data), `"by":"ace.old"`) { - t.Errorf("the controller's act is not copied with its author: %v", err) + if _, err := logStream.GetLastMsgForSubject(ctx, "mesh.log."+Module+"."+ActsLog+"."+oldKey); err == nil { + t.Errorf("the controller's act was copied although its bucket is no longer read") } - if info, err := logStream.Info(ctx); err != nil || info.State.Msgs != 3 { - t.Errorf("the log keeps %v, want the copied act, the act and one warrant (%v)", info, err) + if info, err := logStream.Info(ctx); err != nil || info.State.Msgs != 2 { + t.Errorf("the log keeps %v, want the act and one warrant (%v)", info, err) } listed, err := p.call(HandActsTool, "person.jochen", map[string]any{}) - if err != nil || !strings.Contains(string(listed), "warrant-claude-code-a1") || !strings.Contains(string(listed), r.Act.ID) { + if err != nil || !strings.Contains(string(listed), "warrant-claude-code-a1") || !strings.Contains(string(listed), r.Act.ID) || strings.Contains(string(listed), oldKey) { t.Errorf("hand-acts answered %s, %v", listed, err) } } diff --git a/modules/mesh-hand-acts/module.json b/modules/mesh-hand-acts/module.json index 443ea9d3..0386ee8c 100644 --- a/modules/mesh-hand-acts/module.json +++ b/modules/mesh-hand-acts/module.json @@ -14,7 +14,6 @@ } ], "reads": [ - "mesh-controller.hand-acts", "messenger.asks" ], "logs": [ diff --git a/modules/mesh-healers/cmd/mesh-healers/healers.go b/modules/mesh-healers/cmd/mesh-healers/healers.go index 3d7d5ff4..31bc2606 100644 --- a/modules/mesh-healers/cmd/mesh-healers/healers.go +++ b/modules/mesh-healers/cmd/mesh-healers/healers.go @@ -45,7 +45,6 @@ const ( LeaseRead = buscompose.ControllerLease LeaseKey = buscompose.ControllerLeaseKey ConditionsRead = "mesh-conditions.conditions" - HandActsRead = "mesh-controller.hand-acts" ActedEvent = health.EventHealerActed // ActedSubject is where the seat says it, which its consumers hear (catalogue.SeatSays). ActedSubject = "mesh.seat." + Seat + ".event." + ActedEvent diff --git a/modules/mesh-healers/cmd/mesh-healers/healers_test.go b/modules/mesh-healers/cmd/mesh-healers/healers_test.go index ad2de049..dcc42ae0 100644 --- a/modules/mesh-healers/cmd/mesh-healers/healers_test.go +++ b/modules/mesh-healers/cmd/mesh-healers/healers_test.go @@ -374,16 +374,19 @@ func TestACauseRepairedByHandTwiceIsAHealerWanted(t *testing.T) { w := newWorld() act := func(key, verb, cause string, ago time.Duration) { raw, _ := json.Marshal(HandAct{ID: key, At: w.now.Add(-ago), By: "person.jochen", Verb: verb, Why: "x", Cause: cause}) - w.h.HandActHeard(key, "put", raw) + w.h.ActRecorded(raw) } act("a1", "broker consumer-reset", "consumer-behind", 10*24*time.Hour) if keys := w.store.keys(FindingsState); len(keys) != 0 { t.Fatalf("one act is a finding: %v", keys) } - // The second of the cause is the seat's own act, heard as act-recorded: one in each log counts two (ADR 0565 §11). + // The second of the cause is heard as act-recorded; an act heard again and listed by the seat counts once (774/S10/AC2). seatAct, _ := json.Marshal(HandAct{ID: "a2", At: w.now.Add(-24 * time.Hour), By: "person.jochen", Verb: "broker consumer-reset", Why: "x", Cause: "consumer-behind"}) w.h.ActRecorded(seatAct) + if n, err := w.h.Seeded([]byte(`{"acts":[` + string(seatAct) + `]}`)); err != nil || n != 1 { + t.Fatalf("the seat's listing was not taken: %d, %v", n, err) + } // A drill in each log counts none. act("a3", "hand-act drill", "drill", time.Hour) drill, _ := json.Marshal(HandAct{ID: "a4", At: w.now.Add(-2 * time.Hour), Verb: "hand-act drill", Why: "x", Cause: "drill"}) @@ -408,9 +411,8 @@ func TestACauseRepairedByHandTwiceIsAHealerWanted(t *testing.T) { if keys := w.store.keys(FindingsState); len(keys) != 0 { t.Errorf("past the fortnight the finding stays: %v", keys) } - w.h.HandActHeard("a5", "delete", nil) - if len(w.h.acts) != 7 { - t.Errorf("a deleted entry is kept: %d", len(w.h.acts)) + if len(w.h.acts) != 8 { + t.Errorf("an act heard twice or listed after it was heard is kept twice: %d acts", len(w.h.acts)) } } diff --git a/modules/mesh-healers/cmd/mesh-healers/main.go b/modules/mesh-healers/cmd/mesh-healers/main.go index 70fad411..90b482af 100644 --- a/modules/mesh-healers/cmd/mesh-healers/main.go +++ b/modules/mesh-healers/cmd/mesh-healers/main.go @@ -2,8 +2,8 @@ // own, on its own bus account (`runs-as`), so that its account alone writes its buckets: `attempts` (every act begun, // made and finished, and every escalation, kept a week), `would` (what it would have done while the controller holds // its lease, kept a week) and `findings` (the brake, its blindness, and S15). It hears the condition keeper's -// condition-raised and condition-changed on a durable consumer, watches the controller's lease, and watches the -// controller's hand-act log for S15. stdout is the MCP channel; what this module says, it says on stderr. +// condition-raised and condition-changed on a durable consumer, watches the controller's lease, and counts the seat's hand acts +// for S15. stdout is the MCP channel; what this module says, it says on stderr. package main import ( @@ -68,11 +68,25 @@ func main() { h.WakeHeard(c.Key, c.Op) return nil }) - go stdio.State(HandActsRead).Follow("", func(c stdio.StateChange) error { - h.HandActHeard(c.Key, c.Op, c.Value) - return nil - }) - // S15 over both logs: the seat's act-recorded on a durable consumer, beside the controller's bucket's watch. + // S15 over the seat's log: the fortnight's acts asked of the seat once at the start, then act-recorded on a + // durable consumer (the controller's bucket is no longer read, issue 774). + go func() { + <-stdio.Served() + for wait := 2 * time.Second; ; wait = min(wait*2, time.Minute) { + answer, err := stdio.Ask("seat:mesh-hand-acts.hand-acts", map[string]any{"days": int(Within.Hours() / 24)}) + if err == nil { + n, err := h.Seeded(answer) + if err == nil { + logf("[%s] took %d hand acts of the last fortnight from the seat", Module, n) + return + } + logf("[%s] %v", Module, err) + } else { + logf("[%s] the seat's hand acts cannot be asked yet (%v); asking again in %s", Module, err, wait) + } + time.Sleep(wait) + } + }() go func() { <-stdio.Served() // subscribed as soon as the runtime serves, never after a guessed wait: an event sent meanwhile is heard for wait := 2 * time.Second; ; wait = min(wait*2, time.Minute) { diff --git a/modules/mesh-healers/cmd/mesh-healers/manifest_test.go b/modules/mesh-healers/cmd/mesh-healers/manifest_test.go index bbbe8d49..f9ccac51 100644 --- a/modules/mesh-healers/cmd/mesh-healers/manifest_test.go +++ b/modules/mesh-healers/cmd/mesh-healers/manifest_test.go @@ -49,8 +49,8 @@ func TestTheManifestSaysWhatTheCodeDoes(t *testing.T) { }{ {"emits (healer-acted is the seat's, said on its subject)", m.Emits, nil}, {"consumes", m.Consumes, []string{RaisedEvent, ChangedEvent, RecordedEvent}}, - {"invokes", m.Invokes, []string{AssertTool, "seat:mesh-hand-acts.hand-act"}}, - {"reads", m.Reads, []string{LeaseRead, ConditionsRead, HandActsRead}}, + {"invokes", m.Invokes, []string{AssertTool, "seat:mesh-hand-acts.hand-act", "seat:mesh-hand-acts.hand-acts"}}, + {"reads", m.Reads, []string{LeaseRead, ConditionsRead}}, } { if !slices.Equal(c.got, c.want) { t.Errorf("%s %v, want %v", c.name, c.got, c.want) diff --git a/modules/mesh-healers/cmd/mesh-healers/wanted.go b/modules/mesh-healers/cmd/mesh-healers/wanted.go index 84e30161..9d111b5c 100644 --- a/modules/mesh-healers/cmd/mesh-healers/wanted.go +++ b/modules/mesh-healers/cmd/mesh-healers/wanted.go @@ -1,12 +1,12 @@ package main // S15: a cause repaired by hand twice within a fortnight is a healer wanted (ADR 0231 §27, §28; ADR 0564 Limits 2). -// The hand-act log is the controller's bucket mesh-controller_hand-acts, a key-value bucket with no events: mesh-healers -// watches it, the one of the four holders granted its read, counts each cause again on every put, and writes the -// finding `healer-wanted` per cause. **A heal is never a hand act**, and an act that is a person's decision by design -// (a drill above all) never counts. The finding clears when fewer than two acts of its cause remain within the -// fortnight: the key `fortnight` of the healers' wakes expires when the oldest counted act leaves it. From the log's home (issue 1953) they -// watch that instead. +// The hand-act log is the seat mesh-hand-acts' (ADR 0565 §10, §11), which says each act as act-recorded: mesh-healers +// hears them, counts each cause again on every act, and writes the finding `healer-wanted` per cause. At its start it +// asks the seat for the fortnight's acts once, so a restart does not forget them. The controller's bucket of hand acts +// is no longer read (issue 774, ADR 0571 §13). **A heal is never a hand act**, and an act that is a person's decision by +// design (a drill above all) never counts. The finding clears when fewer than two acts of its cause remain within the +// fortnight: the key `fortnight` of the healers' wakes expires when the oldest counted act leaves it. import ( "encoding/json" @@ -22,7 +22,7 @@ import ( // Within is how far back a repeated cause counts. const Within = 14 * 24 * time.Hour -// HandAct is what S15 reads of one entry of the controller's hand-act log (mesh-controller internal/link HandAct). +// HandAct is what S15 reads of one act of the hand-act log (mesh-sdk handacts.Act). type HandAct struct { ID string `json:"id"` At time.Time `json:"at"` @@ -35,7 +35,7 @@ type HandAct struct { } // decided are the verbs whose entries are a person's decision by design, with the causes they hold for (every cause -// when none): the controller's handActVerbs, read as S15 reads them. A verb not listed counts as a repair. +// when none): the controller's handActVerbs as the log still names them, read as S15 reads them. A verb not listed counts as a repair. var decided = map[string][]string{ "warrant": nil, "seat": {"seat-passed"}, @@ -68,34 +68,37 @@ func personsDecision(a HandAct) bool { return len(causes) == 0 || slices.Contains(causes, a.Cause) } -// ActRecorded is one act the seat mesh-hand-acts recorded, heard as act-recorded: counted beside the controller's -// log's, under a key of its own log, so an act is counted once whichever log holds it (ADR 0565 §11). +// ActRecorded is one act the seat mesh-hand-acts recorded, heard as act-recorded or listed by its verb `hand-acts`: +// kept under a key of its own log, so an act heard and listed is counted once (ADR 0565 §11). func (h *Healers) ActRecorded(body []byte) { var a HandAct if json.Unmarshal(body, &a) != nil || a.ID == "" { return } - h.HandActHeard(handacts.LogSeat+":"+a.ID, "put", body) -} - -// HandActHeard is one change of the hand-act log: kept or forgotten, and every cause counted again. -func (h *Healers) HandActHeard(key, op string, value []byte) { h.mu.Lock() if h.acts == nil { h.acts = map[string]HandAct{} } - if op != "put" { - delete(h.acts, key) - } else { - var a HandAct - if json.Unmarshal(value, &a) == nil { - h.acts[key] = a - } - } + h.acts[handacts.LogSeat+":"+a.ID] = a h.mu.Unlock() h.Wanted() } +// Seeded takes the acts of the seat's answer to `hand-acts` (its `acts` list) as heard, at the healers' start; it +// answers how many it took. +func (h *Healers) Seeded(answer []byte) (int, error) { + var l struct { + Acts []json.RawMessage `json:"acts"` + } + if err := json.Unmarshal(answer, &l); err != nil { + return 0, fmt.Errorf("the seat's hand-acts answer is not a list of acts: %w", err) + } + for _, raw := range l.Acts { + h.ActRecorded(raw) + } + return len(l.Acts), nil +} + // Wanted counts every cause repaired by hand within the fortnight and writes a finding for each counted twice or more, // deleting the rest; its wait `fortnight` expires when the oldest counted act of a wanted cause leaves the fortnight. func (h *Healers) Wanted() []Observation { diff --git a/modules/mesh-healers/module.json b/modules/mesh-healers/module.json index 6f2f0688..fc05d31a 100644 --- a/modules/mesh-healers/module.json +++ b/modules/mesh-healers/module.json @@ -20,12 +20,12 @@ ], "invokes": [ "mesh-grants.grants_assert", - "seat:mesh-hand-acts.hand-act" + "seat:mesh-hand-acts.hand-act", + "seat:mesh-hand-acts.hand-acts" ], "reads": [ "mesh-controller.lease", - "mesh-conditions.conditions", - "mesh-controller.hand-acts" + "mesh-conditions.conditions" ], "state": [ {