Keep the builder's history as its own log that readers declare and follow, the memberships as the grants part's state, and refuse a module's own stream, so every module's data is in the bus's predefined forms and readable by its tools (issue 1955, ADR 0571 clauses 1, 4, 5, 7)

This commit is contained in:
2026-10-29 23:28:21 +01:00
parent 196c5b6af2
commit bb993dd606
16 changed files with 695 additions and 51 deletions
+51 -5
View File
@@ -58,12 +58,13 @@ func (r *logRuntime) answer(m message) map[string]any {
From uint64 `json:"from"`
Keys string `json:"keys"`
Limit int `json:"limit"`
Since time.Time `json:"since"`
}
_ = json.Unmarshal(m.Params, &p)
r.mu.Lock()
defer r.mu.Unlock()
r.asked = append(r.asked, m)
if p.Log != "changes" {
if p.Log != "changes" && p.Log != "builder.build-log" {
return map[string]any{"error": map[string]any{"code": -32000, "message": "issues was issued no log " + p.Log}}
}
switch strings.TrimPrefix(m.Method, "mesh/log.") {
@@ -83,7 +84,7 @@ func (r *logRuntime) answer(m message) map[string]any {
page := map[string]any{"entries": []LogEntry{}, "next": 0, "last": len(r.entries)}
var got []LogEntry
for _, e := range r.entries {
if e.Sequence < p.From || !matches(p.Keys, e.Key) {
if e.Sequence < p.From || !matches(p.Keys, e.Key) || e.At.Before(p.Since) {
continue
}
if len(got) == limit {
@@ -96,6 +97,16 @@ func (r *logRuntime) answer(m message) map[string]any {
page["entries"] = got
}
return map[string]any{"result": page}
case "newest":
var out []LogEntry
for _, e := range r.entries {
if matches(p.Keys, e.Key) && r.last(e.Key) == e.Sequence {
out = append(out, e)
}
}
return map[string]any{"result": out}
case "watch":
return map[string]any{"result": map[string]any{}}
case "last":
seq := r.last(p.Key)
if seq == 0 {
@@ -148,6 +159,13 @@ func logging(t *testing.T, r *logRuntime, run func() (any, error)) map[string]an
a := r.answer(m)
a["id"] = m.ID
send(a)
// A watch's entries follow its answer, each named by the watch's id (ADR 0571 clause 1).
if m.Method == "mesh/log.watch" {
r.mu.Lock()
e := r.entries[len(r.entries)-1]
r.mu.Unlock()
send(map[string]any{"id": "entry-1", "method": "mesh/log", "params": map[string]any{"watch": m.ID, "entry": e}})
}
}(m)
continue
}
@@ -193,7 +211,9 @@ func TestABundleKeepsALogThroughTheRuntime(t *testing.T) {
pages []*LogPage
last, none *LogEntry
info *LogInfo
narrowed *LogPage
narrowed, since *LogPage
newest []LogEntry
watched *LogEntry
)
got := logging(t, r, func() (any, error) {
changes := Log("changes")
@@ -232,8 +252,27 @@ func TestABundleKeepsALogThroughTheRuntime(t *testing.T) {
if none, err = changes.Last("999"); err != nil {
return nil, err
}
info, err = changes.Info()
return "done", err
if info, err = changes.Info(); err != nil {
return nil, err
}
// ADR 0571 clause 1: another module's log, newest per key, from a time, and followed.
if newest, err = changes.Newest(""); err != nil {
return nil, err
}
if since, err = changes.Since(time.Date(2026, 10, 11, 18, 0, 5, 0, time.UTC), "", 0); err != nil {
return nil, err
}
heard := make(chan LogEntry, 1)
if err = Log("builder.build-log").Watch("letta", LogFromSince(time.Date(2026, 10, 11, 18, 0, 0, 0, time.UTC)),
func(e LogEntry) error { heard <- e; return nil }); err != nil {
return nil, err
}
select {
case e := <-heard:
watched = &e
case <-time.After(5 * time.Second):
}
return "done", nil
})
if out, isError := text(t, got); isError || out != `"done"` {
t.Fatalf("the tool answered %s", out)
@@ -296,6 +335,13 @@ func TestABundleKeepsALogThroughTheRuntime(t *testing.T) {
if none != nil {
t.Fatalf("a key with no entry has a last: %+v", none)
}
if len(newest) != 5 || newest[0].Key != "402" || newest[1].Sequence != 3 || len(since.Entries) != 2 || since.Entries[0].Sequence != 5 {
t.Fatalf("the newest per key are %+v, since a time %+v", newest, since)
}
if method, p = r.params(t, 15); method != "mesh/log.watch" || p["log"] != "builder.build-log" || p["keys"] != "letta" ||
p["from"] != "since" || p["since"] != "2026-10-11T18:00:00Z" || watched == nil || watched.Sequence != 6 {
t.Fatalf("a watch asked %s %v and heard %+v", method, p, watched)
}
if info == nil || info.Entries != 6 || info.MaxBytes != 1<<30 || info.First != 1 || info.Last != 6 || info.Bytes == 0 {
t.Fatalf("the log's info is %+v", info)
}