CAT-P3: the page's model is a tool, its re-read a hand act, an unanswered source is said and asked again on a backoff
knowledge_overview answers the page's model as data (reads); knowledge_overview_reread asks one source's verb again by hand, no other, and records the hand act (acts). A source marked from its verb is asked again alone from 10 seconds, doubling to 5 minutes, its first answer clearing the mark; one marked from its subscription asks nothing while silent and the verb once on its next event. The module's health carries the page's last rebuild and says it is degraded while a source is unanswered, staying healthy. Issue 1968. Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
This commit is contained in:
@@ -208,3 +208,28 @@ func (k *KB) formRefused() int {
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// pageHealth adds the overview page to a verdict: the last rebuild and each source's last answer, and the module is said degraded
|
||||
// while any source is unanswered. The module stays healthy: a source that does not answer is that module's fault, not this one's.
|
||||
func (k *KB) pageHealth(base HealthAnswer) HealthAnswer {
|
||||
if k.Overview == nil {
|
||||
return base
|
||||
}
|
||||
k.Overview.mu.Lock()
|
||||
rebuilt := k.Overview.rebuilt
|
||||
k.Overview.mu.Unlock()
|
||||
if bad := k.Overview.Unanswered(); len(bad) > 0 {
|
||||
var says []string
|
||||
for _, s := range bad {
|
||||
why := "no answer yet"
|
||||
if s.Mark != nil {
|
||||
why = s.Mark.Cause + ": " + s.Mark.Why
|
||||
}
|
||||
says = append(says, fmt.Sprintf("%s (%s; last answer %s)", s.Name, why, s.Last.Local().Format("15:04")))
|
||||
}
|
||||
base.Why += "; the overview page is degraded, it does not answer: " + strings.Join(says, ", ")
|
||||
return base
|
||||
}
|
||||
base.Why += "; the overview page was last rebuilt " + rebuilt.Local().Format("2006-01-02 15:04")
|
||||
return base
|
||||
}
|
||||
|
||||
@@ -628,7 +628,7 @@ func healthTool(status string, now func() time.Time, k *KB) stdio.Tool {
|
||||
Run: func(map[string]any) (any, error) {
|
||||
ans := ReadStatus(status, now())
|
||||
if k != nil {
|
||||
ans = k.ledgerHealth(ans) // once the records are on the ledger, what the ledger form owes (health.go)
|
||||
ans = k.pageHealth(k.ledgerHealth(ans)) // what the ledger form owes once the records are on it, and the page (health.go)
|
||||
}
|
||||
return ans, nil
|
||||
}}
|
||||
|
||||
@@ -84,11 +84,11 @@ func TestItDeclaresAndHoldsTheKnowledgeBaseSeatOnceForTheMesh(t *testing.T) {
|
||||
}
|
||||
if !reflect.DeepEqual(m.Invokes, []string{"seat:issue-tracker.reserve", "claude-code.claude_code_judge", "claude-code.claude_code_judged", "records.records_status",
|
||||
"records.records_list", "records.records_read", "seat:operator-channel.notify", "seat:issue-tracker.list", "seat:issue-tracker.get",
|
||||
"seat:issue-tracker.milestones", "seat:issue-tracker.changes", "seat:mesh-delivery.deliveries", "seat:mesh-delivery.show", "seat:mesh-delivery.groups", "seat:operator-channel.asks"}) {
|
||||
"seat:issue-tracker.milestones", "seat:issue-tracker.changes", "seat:mesh-delivery.deliveries", "seat:mesh-delivery.show", "seat:mesh-delivery.groups", "seat:operator-channel.asks", "seat:mesh-hand-acts.hand-act"}) {
|
||||
t.Fatalf("invokes %v", m.Invokes)
|
||||
}
|
||||
src := ""
|
||||
for _, f := range []string{"main.go", "hq.go", "progress.go", "progress_since.go", "milestones.go", "overview.go"} {
|
||||
for _, f := range []string{"main.go", "hq.go", "progress.go", "progress_since.go", "milestones.go", "overview.go", "tools.go"} {
|
||||
src += readFile(t, f)
|
||||
}
|
||||
for _, inv := range m.Invokes {
|
||||
|
||||
@@ -3,6 +3,8 @@ package main
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
@@ -99,3 +101,117 @@ func TestThePageModelHasThreeBlocksKeptByEventsAndAsksNoVerbAgain(t *testing.T)
|
||||
t.Fatalf("the seven-day block: %+v", m.Accepted)
|
||||
}
|
||||
}
|
||||
|
||||
// A source that does not answer says so, never as an empty list (ADR 0616 §5, §6; AC P3): from its verb, on a bounded backoff
|
||||
// asked again alone, its first answer clearing the mark; from its subscription, asking nothing while it is silent and the verb once on its
|
||||
// next event; the model as a tool equals the page's model; the re-read tool asks one source and records the hand act.
|
||||
func TestASourceThatDoesNotAnswerIsSaidAndAskedAgainOnABackoff(t *testing.T) {
|
||||
w := newOverviewWorld(t)
|
||||
w.fail[DeliveryGroupsVerb] = true
|
||||
w.o.Start()
|
||||
m, _ := w.o.Model()
|
||||
var delivery SourceState
|
||||
for _, s := range m.Sources {
|
||||
if s.Name == SourceDelivery {
|
||||
delivery = s
|
||||
}
|
||||
}
|
||||
if delivery.Answered || delivery.Mark == nil || delivery.Mark.Cause != MarkVerb || len(m.Operator.Groups) != 0 || !strings.Contains(delivery.Mark.Why, "no answer in time") {
|
||||
t.Fatalf("%+v", delivery)
|
||||
}
|
||||
if len(m.Designs) != 1 {
|
||||
t.Fatalf("another source's block was lost: %+v", m.Designs)
|
||||
}
|
||||
// Asked again on the backoff alone: 10 s, then doubling, at most 5 minutes; no other source is asked.
|
||||
calls := w.o.Calls(DeliveryGroupsVerb)
|
||||
if got := w.o.Due(); len(got) != 0 {
|
||||
t.Fatalf("asked before the backoff ran out: %v", got)
|
||||
}
|
||||
var waits []time.Duration
|
||||
for i := 0; i < 8; i++ {
|
||||
before := w.now
|
||||
w.now = w.o.state[SourceDelivery].Mark.Next
|
||||
waits = append(waits, w.now.Sub(before))
|
||||
if got := w.o.Due(); len(got) != 1 || got[0] != SourceDelivery {
|
||||
t.Fatalf("%v", got)
|
||||
}
|
||||
}
|
||||
if waits[0] != BackoffFrom || waits[1] != 2*BackoffFrom || waits[7] != BackoffMost || w.o.Calls(DeliveryGroupsVerb) != calls+8 || w.o.Calls(AsksVerb) != 1 {
|
||||
t.Fatalf("waits %v, delivery asked %d, asks asked %d", waits, w.o.Calls(DeliveryGroupsVerb), w.o.Calls(AsksVerb))
|
||||
}
|
||||
// Its first answer rebuilds the block and clears the mark.
|
||||
w.fail[DeliveryGroupsVerb] = false
|
||||
w.now = w.o.state[SourceDelivery].Mark.Next
|
||||
w.o.Due()
|
||||
m, _ = w.o.Model()
|
||||
if len(w.o.Unanswered()) != 0 || len(m.Operator.Groups) != 1 {
|
||||
t.Fatalf("%+v", m)
|
||||
}
|
||||
// A subscription that goes silent marks the source from the subscription and asks nothing while it is silent.
|
||||
asked := w.o.Calls(AsksVerb)
|
||||
w.now = w.now.Add(HeartbeatWithin + time.Second)
|
||||
if got := w.o.Silent(); len(got) != 3 {
|
||||
t.Fatalf("%v", got)
|
||||
}
|
||||
if got := w.o.Due(); len(got) != 0 || w.o.Calls(AsksVerb) != asked {
|
||||
t.Fatalf("a silent subscription was asked: %v", got)
|
||||
}
|
||||
if u := w.o.Unanswered(); len(u) != 3 || u[0].Mark.Cause != MarkSubscription {
|
||||
t.Fatalf("%+v", u)
|
||||
}
|
||||
// Its next event (or heartbeat) makes the module ask the verb once, and that answer clears the mark.
|
||||
w.o.Heard(SourceAsks, "messenger.offered", json.RawMessage(`{"key":"ask-9","headline":"x"}`))
|
||||
if w.o.Calls(AsksVerb) != asked+1 || w.o.state[SourceAsks].Mark != nil {
|
||||
t.Fatalf("asked %d, mark %+v", w.o.Calls(AsksVerb), w.o.state[SourceAsks].Mark)
|
||||
}
|
||||
// An unanswered verb then turns the mark into one from the verb.
|
||||
w.fail[DeliveryGroupsVerb] = true
|
||||
w.o.Heard(SourceDelivery, "", nil)
|
||||
if mk := w.o.state[SourceDelivery].Mark; mk == nil || mk.Cause != MarkVerb {
|
||||
t.Fatalf("%+v", mk)
|
||||
}
|
||||
}
|
||||
|
||||
func TestThePagesModelIsAToolAndItsRereadIsAHandAct(t *testing.T) {
|
||||
w := newOverviewWorld(t)
|
||||
w.entries = []*Entry{recordEntry(301, StatusAccepted, "2026-11-02", w.now.Add(-24*time.Hour), nil)}
|
||||
w.o.Start()
|
||||
var acts []string
|
||||
k := &KB{Overview: w.o, Seats: func(key string, body any) (json.RawMessage, error) {
|
||||
acts = append(acts, key)
|
||||
return json.RawMessage(`{}`), nil
|
||||
}, Now: func() time.Time { return w.now }}
|
||||
var tools []toolRun
|
||||
for _, tool := range moduleTools(k, callerOf) {
|
||||
tools = append(tools, toolRun{tool.Name, tool.Run})
|
||||
}
|
||||
got, err := callTool(t, tools, OverviewTool, map[string]any{})
|
||||
want, _ := w.o.Model()
|
||||
raw, _ := json.Marshal(want)
|
||||
var wantMap map[string]any
|
||||
_ = json.Unmarshal(raw, &wantMap)
|
||||
if err != nil || !reflect.DeepEqual(got, wantMap) {
|
||||
t.Fatalf("the tool's answer is not the page's model: %v", err)
|
||||
}
|
||||
// The re-read asks the one source and no other, and records itself.
|
||||
before := map[string]int{}
|
||||
for _, v := range []string{TrackerListVerb, DeliveryGroupsVerb, AsksVerb} {
|
||||
before[v] = w.o.Calls(v)
|
||||
}
|
||||
w.fail[AsksVerb] = true
|
||||
res, err := callTool(t, tools, RereadTool, map[string]any{"source": SourceAsks, "why": "the page showed it stale"})
|
||||
if err != nil || res["answered"] != false || w.o.Calls(AsksVerb) != before[AsksVerb]+1 || w.o.Calls(DeliveryGroupsVerb) != before[DeliveryGroupsVerb] || w.o.Calls(TrackerListVerb) != before[TrackerListVerb] {
|
||||
t.Fatalf("%v %v", err, res)
|
||||
}
|
||||
if len(acts) != 1 || acts[0] != "seat:mesh-hand-acts.hand-act" {
|
||||
t.Fatalf("the hand act: %v", acts)
|
||||
}
|
||||
if _, err := callTool(t, tools, RereadTool, map[string]any{"source": "nobody"}); err == nil {
|
||||
t.Fatal("a source the page lacks was re-read")
|
||||
}
|
||||
// The health carries the page: degraded while a source is unanswered, healthy.
|
||||
h := k.pageHealth(HealthAnswer{State: "healthy", Healthy: true, Why: "ok"})
|
||||
if !h.Healthy || !strings.Contains(h.Why, "overview page is degraded") || !strings.Contains(h.Why, SourceAsks) {
|
||||
t.Fatalf("%+v", h)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,11 +29,13 @@ const (
|
||||
IntegrityTool = "knowledge_integrity"
|
||||
MoveTool = "knowledge_move"
|
||||
MoveCheckTool = "knowledge_move_check"
|
||||
OverviewTool = "knowledge_overview"
|
||||
RereadTool = "knowledge_overview_reread"
|
||||
)
|
||||
|
||||
// ModuleTools are the module's own tools beside the health tool, in the order the manifest lists them.
|
||||
var ModuleTools = []string{HealthTool, HighestTool, MarksTool, HeadTool, OperationsTool, SectionTool, DraftTool, PartsTool,
|
||||
AskedTool, FillTool, IndexTool, AgainTool, IntegrityTool, FormTool, MoveTool, MoveCheckTool}
|
||||
AskedTool, FillTool, IndexTool, AgainTool, IntegrityTool, FormTool, MoveTool, MoveCheckTool, OverviewTool, RereadTool}
|
||||
|
||||
var errNotOnLedger = errors.New("the records are still in the database: this tool reads the ledger, which serves them after the move")
|
||||
|
||||
@@ -373,6 +375,53 @@ func moduleTools(k *KB, caller func(map[string]any) string) []stdio.Tool {
|
||||
}
|
||||
return ms.Move(context.Background(), ask, func(s string) { k.logf("[mesh-knowledge] the move: %s", s) })
|
||||
}},
|
||||
{Name: OverviewTool,
|
||||
Description: "The overview page's model as data, the same the page renders: the designs in progress with their milestones and issues, the records " +
|
||||
"accepted in the last seven days and those waiting, what waits on the operator (records for an Approve, open asks, delivery groups), and each source " +
|
||||
"with whether it answered, why it did not, and when it last did. Reads only.",
|
||||
Input: schema(nil, map[string]any{}),
|
||||
Run: func(m map[string]any) (any, error) {
|
||||
if _, err := readArgs(OverviewTool, m); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if k.Overview == nil {
|
||||
return nil, errors.New("the overview page is not running in this process")
|
||||
}
|
||||
return k.Overview.Model()
|
||||
}},
|
||||
{Name: RereadTool,
|
||||
Description: "Ask one source's verb again by hand and rebuild its block of the overview page now, asking no other: issue-tracker, mesh-delivery or " +
|
||||
"operator-channel. A hand act, recorded with the hand-act log. Answers whether the source answered.",
|
||||
Input: schema([]string{"source"}, map[string]any{"source": text("issue-tracker, mesh-delivery or operator-channel"), "why": text("one line: why")}),
|
||||
Run: func(m map[string]any) (any, error) {
|
||||
a, err := readArgs(RereadTool, m, "source", "why")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
source, err := a.str("source")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
why, _ := a.str("why")
|
||||
if k.Overview == nil {
|
||||
return nil, errors.New("the overview page is not running in this process")
|
||||
}
|
||||
if _, ok := k.Overview.state[source]; !ok {
|
||||
return nil, fmt.Errorf("%q is no source of the page: issue-tracker, mesh-delivery or operator-channel", source)
|
||||
}
|
||||
rebuildErr := k.Overview.Rebuild(source)
|
||||
if k.Seats != nil {
|
||||
if _, err := k.Seats("seat:mesh-hand-acts.hand-act", map[string]any{"what": "asked the " + source + " again for the overview page, by hand",
|
||||
"why": firstNonEmpty(why, "the page showed the source unanswered, or stale")}); err != nil {
|
||||
k.logf("[mesh-knowledge] the hand act of the re-read of %s could not be recorded: %v", source, err)
|
||||
}
|
||||
}
|
||||
out := map[string]any{"source": source, "answered": rebuildErr == nil}
|
||||
if rebuildErr != nil {
|
||||
out["why"] = rebuildErr.Error()
|
||||
}
|
||||
return out, nil
|
||||
}},
|
||||
{Name: MoveCheckTool,
|
||||
Description: "Compare the database with the ledger record by record now: the versions, the served and the proposed version whole and every section's text. " +
|
||||
"Answers every difference, at most 50, and where the records are served from; a database that is behind is named.",
|
||||
|
||||
@@ -130,7 +130,7 @@ func TestEveryStateTheModuleKeepsHasAToolThatReadsIt(t *testing.T) {
|
||||
if _, err := callTool(t, tools, MoveCheckTool, map[string]any{}); err == nil {
|
||||
t.Fatal("a move check without a database")
|
||||
}
|
||||
if len(ModuleTools) != 16 {
|
||||
if len(ModuleTools) != 18 {
|
||||
t.Fatalf("%v", ModuleTools)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,7 +179,9 @@
|
||||
"knowledge_integrity",
|
||||
"knowledge_form_only",
|
||||
"knowledge_move",
|
||||
"knowledge_move_check"
|
||||
"knowledge_move_check",
|
||||
"knowledge_overview",
|
||||
"knowledge_overview_reread"
|
||||
],
|
||||
"emits": [
|
||||
"reserved",
|
||||
@@ -233,7 +235,8 @@
|
||||
"seat:mesh-delivery.deliveries",
|
||||
"seat:mesh-delivery.show",
|
||||
"seat:mesh-delivery.groups",
|
||||
"seat:operator-channel.asks"
|
||||
"seat:operator-channel.asks",
|
||||
"seat:mesh-hand-acts.hand-act"
|
||||
],
|
||||
"config": {
|
||||
"readers": {
|
||||
|
||||
Reference in New Issue
Block a user