mesh-hand-acts and mesh-healers stop reading the controller's hand-act log (ADR 0571 §13)

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
This commit is contained in:
2026-11-02 00:17:52 +01:00
parent 2f52f91b5c
commit 432de55186
14 changed files with 143 additions and 363 deletions
@@ -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-<ns>-<n>`), 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
}
@@ -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
}
+19 -121
View File
@@ -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]
})
}
@@ -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)
@@ -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{}) {
@@ -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"))
@@ -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)
}
}
-1
View File
@@ -14,7 +14,6 @@
}
],
"reads": [
"mesh-controller.hand-acts",
"messenger.asks"
],
"logs": [
@@ -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
@@ -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))
}
}
+21 -7
View File
@@ -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) {
@@ -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)
+26 -23
View File
@@ -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 {
+3 -3
View File
@@ -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": [
{