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:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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": [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user