J4 (A2j finding 1): an owner's read tool answers a key's kept history, so a reader sees what changed before its first view

The view's module history, heard messages and a want's since asked the owners' read tools for a key's history and
were refused: views.ReadTool takes key with history and answers {history: [{revision, at, op, value}]} from the
state's kept values (KeptSource through mesh/state.history; viewstest.Source and Serve from the bucket).

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
This commit is contained in:
2026-11-04 14:48:21 +01:00
parent 712b21bd74
commit fe83d66b62
3 changed files with 130 additions and 11 deletions
+68 -7
View File
@@ -25,6 +25,20 @@ func (k KeptSource) Get(key string) (*Entry, bool, error) {
return &Entry{Value: e.Value, Revision: e.Revision, Written: e.At}, true, nil return &Entry{Value: e.Value, Revision: e.Revision, Written: e.At}, true, nil
} }
// History is every value the state still keeps for key, oldest first (stdio.KeptState.History): as many as the
// state declares it keeps, deletes included.
func (k KeptSource) History(key string) ([]Revision, error) {
kept, err := k.State.History(key)
if err != nil {
return nil, err
}
out := make([]Revision, 0, len(kept))
for _, r := range kept {
out = append(out, Revision{Revision: r.Revision, At: r.At, Op: r.Op, Value: r.Value})
}
return out, nil
}
// ParseCallers is a callers list as a manifest writes it into an env value: comma-separated, each a module (on any // ParseCallers is a callers list as a manifest writes it into an env value: comma-separated, each a module (on any
// machine), <machine>.<module> or person.<name> (stdio.Callers). // machine), <machine>.<module> or person.<name> (stdio.Callers).
func ParseCallers(list string) stdio.Callers { func ParseCallers(list string) stdio.Callers {
@@ -76,22 +90,28 @@ func FirstViewTool(owner, name, state, words string, callers stdio.Callers, valu
} }
// ReadTool is an owner's ordinary read tool of one of its states (design 25 § 12.2): the keys, one key's value and // ReadTool is an owner's ordinary read tool of one of its states (design 25 § 12.2): the keys, one key's value and
// revision, or a page of values in the first view's form, and the state's fill against its cap as its mark last said // revision, one key's kept history (values a Historian), or a page of values in the first view's form, and the state's
// it. Granted, as any tool is, to every reader that invokes it, and callable by a session. // fill against its cap as its mark last said it. Granted, as any tool is, to every reader that invokes it, and callable
// by a session.
func ReadTool(owner, name, state, words string, values, marks Source, now func() time.Time) stdio.Tool { func ReadTool(owner, name, state, words string, values, marks Source, now func() time.Time) stdio.Tool {
return stdio.Tool{ return stdio.Tool{
Name: name, Name: name,
Description: fmt.Sprintf("Read %s's state %s: %s With no argument, its keys and how full it is against its cap (as the "+ Description: fmt.Sprintf("Read %s's state %s: %s With no argument, its keys and how full it is against its cap (as the "+
"owner last marked it); with key, that key's value and revision; with values, a page of values whole by key, at the "+ "owner last marked it); with key, that key's value and revision; with key and history, every value the state still "+
"revision the owner said its views family up to, continued with after (the page's next).", owner, state, words), "keeps for that key, oldest first, each with its revision, time and op; with values, a page of values whole by key, "+
"at the revision the owner said its views family up to, continued with after (the page's next).", owner, state, words),
Input: map[string]any{"type": "object", "properties": map[string]any{ Input: map[string]any{"type": "object", "properties": map[string]any{
"key": map[string]any{"type": "string", "description": "one key: its value and revision"}, "key": map[string]any{"type": "string", "description": "one key: its value and revision"},
"values": map[string]any{"type": "boolean", "description": "a page of values whole by key"}, "history": map[string]any{"type": "boolean", "description": "with key: every value the state keeps for it, oldest first"},
"after": map[string]any{"type": "string", "description": "with values: the key the page before ended at"}}}, "values": map[string]any{"type": "boolean", "description": "a page of values whole by key"},
"after": map[string]any{"type": "string", "description": "with values: the key the page before ended at"}}},
Run: func(args map[string]any) (any, error) { Run: func(args map[string]any) (any, error) {
key, _ := args["key"].(string) key, _ := args["key"].(string)
after, _ := args["after"].(string) after, _ := args["after"].(string)
all, _ := args["values"].(bool) all, _ := args["values"].(bool)
if history, _ := args["history"].(bool); history {
return ReadHistory(owner, state, values, strings.TrimSpace(key))
}
return Read(owner, state, values, marks, strings.TrimSpace(key), all, strings.TrimSpace(after), now()) return Read(owner, state, values, marks, strings.TrimSpace(key), all, strings.TrimSpace(after), now())
}, },
} }
@@ -119,6 +139,47 @@ type KeyAnswer struct {
Revision uint64 `json:"revision,omitempty"` Revision uint64 `json:"revision,omitempty"`
} }
// Revision is one value a state keeps for a key: its revision, when the bus wrote it, its op (put, or delete) and
// its value, null for a delete.
type Revision struct {
Revision uint64 `json:"revision"`
At time.Time `json:"at"`
Op string `json:"op"`
Value json.RawMessage `json:"value"`
}
// Historian is a Source that also keeps each key's history (KeptSource: the state's own kept values).
type Historian interface {
History(key string) ([]Revision, error)
}
// HistoryAnswer is a read tool's answer of one key's history: every value kept, oldest first; none kept is empty.
type HistoryAnswer struct {
Owner string `json:"owner"`
State string `json:"state"`
Key string `json:"key"`
History []Revision `json:"history"`
}
// ReadHistory is a read tool's answer of key's history from values, refused when no key is named or values keeps none.
func ReadHistory(owner, state string, values Source, key string) (HistoryAnswer, error) {
if key == "" {
return HistoryAnswer{}, fmt.Errorf("a history of %s is of one key: name it", state)
}
h, ok := values.(Historian)
if !ok {
return HistoryAnswer{}, fmt.Errorf("%s's state %s keeps no history of its keys here", owner, state)
}
kept, err := h.History(key)
if err != nil {
return HistoryAnswer{}, fmt.Errorf("the history of %s.%s cannot be read: %w", state, key, err)
}
if kept == nil {
kept = []Revision{}
}
return HistoryAnswer{Owner: owner, State: state, Key: key, History: kept}, nil
}
// Read is a read tool's answer: one key, a page of values, or the keys and fill. // Read is a read tool's answer: one key, a page of values, or the keys and fill.
func Read(owner, state string, values, marks Source, key string, all bool, after string, now time.Time) (any, error) { func Read(owner, state string, values, marks Source, key string, all bool, after string, now time.Time) (any, error) {
switch { switch {
+29 -1
View File
@@ -32,6 +32,14 @@ func (m mapSource) Get(key string) (*Entry, bool, error) {
return &e, true, nil return &e, true, nil
} }
// keptSource is a mapSource that keeps each key's history, as a Historian.
type keptSource struct {
mapSource
kept map[string][]Revision
}
func (k keptSource) History(key string) ([]Revision, error) { return k.kept[key], nil }
func markOf(state string, rev uint64, made time.Time) Entry { func markOf(state string, rev uint64, made time.Time) Entry {
raw, _ := json.Marshal(Mark{V: Version, State: state, Revision: rev, Created: made, Fill: Fill{Keys: 2, Bytes: 100, MaxBytes: 1000}}) raw, _ := json.Marshal(Mark{V: Version, State: state, Revision: rev, Created: made, Fill: Fill{Keys: 2, Bytes: 100, MaxBytes: 1000}})
return Entry{Value: raw, Revision: 1} return Entry{Value: raw, Revision: 1}
@@ -97,7 +105,7 @@ func TestViews(t *testing.T) {
t.Errorf("an unproven call naming a caller is answered") t.Errorf("an unproven call naming a caller is answered")
} }
}) })
t.Run("a read tool says the keys and the fill, one key, or a page of values", func(t *testing.T) { t.Run("a read tool says the keys and the fill, one key, one key's kept history, or a page of values", func(t *testing.T) {
r, err := Read("o", "modules", values, marks, "", false, "", now()) r, err := Read("o", "modules", values, marks, "", false, "", now())
ra, ok := r.(ReadAnswer) ra, ok := r.(ReadAnswer)
if err != nil || !ok || len(ra.Keys) != 2 || ra.Fill == nil || ra.Fill.MaxBytes != 1000 || ra.Revision != 9 { if err != nil || !ok || len(ra.Keys) != 2 || ra.Fill == nil || ra.Fill.MaxBytes != 1000 || ra.Revision != 9 {
@@ -111,6 +119,26 @@ func TestViews(t *testing.T) {
if ra := r.(ReadAnswer); ra.Fill != nil || ra.Words == "" { if ra := r.(ReadAnswer); ra.Fill != nil || ra.Words == "" {
t.Errorf("an unmarked state says its fill as %+v", ra) t.Errorf("an unmarked state says its fill as %+v", ra)
} }
kept := []Revision{{Revision: 3, At: written, Op: "put", Value: json.RawMessage(`{"n":0}`)},
{Revision: 5, At: written.Add(time.Second), Op: "delete", Value: json.RawMessage(`null`)}, {Revision: 7, At: written, Op: "put", Value: json.RawMessage(`{"n":1}`)}}
tool := ReadTool("o", "o_read_modules", "modules", "w", keptSource{values, map[string][]Revision{"a": kept}}, marks, now)
r, err = tool.Run(map[string]any{"key": "a", "history": true})
raw, _ := json.Marshal(r)
var h struct {
History []Revision `json:"history"`
}
if err != nil || json.Unmarshal(raw, &h) != nil || len(h.History) != 3 || h.History[1].Op != "delete" || h.History[2].Revision != 7 || !h.History[0].At.Equal(written) {
t.Errorf("a key's history = %s, %v", raw, err)
}
if r, err = tool.Run(map[string]any{"key": "none", "history": true}); err != nil || r.(HistoryAnswer).History == nil {
t.Errorf("a key with nothing kept answers %+v, %v, not an empty history", r, err)
}
if _, err := tool.Run(map[string]any{"history": true}); err == nil {
t.Error("a history naming no key is answered")
}
if _, err := ReadTool("o", "o_read_modules", "modules", "w", values, marks, now).Run(map[string]any{"key": "a", "history": true}); err == nil {
t.Error("a source keeping no history answers one")
}
}) })
// A follower whose owner answers from values and marks. // A follower whose owner answers from values and marks.
+33 -3
View File
@@ -23,7 +23,8 @@ import (
// 1), the owner's marks, its views stream with a durable consumer per reader (`<node>_<module>`), the emitter saying // 1), the owner's marks, its views stream with a durable consumer per reader (`<node>_<module>`), the emitter saying
// each change, and the read tool answering on nc: tool is the owner's read tool as a reader's invokes names it // each change, and the read tool answering on nc: tool is the owner's read tool as a reader's invokes names it
// (`<owner>.<tool>`), stream its views stream as its manifest's `streams` names it, both given as the reader's // (`<owner>.<tool>`), stream its views stream as its manifest's `streams` names it, both given as the reader's
// ProcessReader.Follow takes them. It returns the bucket the test writes. // ProcessReader.Follow takes them. The tool answers a key's history too (key with history), from what the bucket keeps:
// a test that reads history makes the bucket first with the history it needs. It returns the bucket the test writes.
func Serve(t testing.TB, ctx context.Context, js jetstream.JetStream, nc *nats.Conn, owner, state, tool, stream string, readers ...string) jetstream.KeyValue { func Serve(t testing.TB, ctx context.Context, js jetstream.JetStream, nc *nats.Conn, owner, state, tool, stream string, readers ...string) jetstream.KeyValue {
t.Helper() t.Helper()
bucket := owner + "_" + state bucket := owner + "_" + state
@@ -54,10 +55,18 @@ func Serve(t testing.TB, ctx context.Context, js jetstream.JetStream, nc *nats.C
tool = strings.TrimPrefix(tool, owner+".") tool = strings.TrimPrefix(tool, owner+".")
sub, err := nc.Subscribe("mesh.mod."+owner+".call."+tool+".>", func(m *nats.Msg) { sub, err := nc.Subscribe("mesh.mod."+owner+".call."+tool+".>", func(m *nats.Msg) {
var in struct { var in struct {
After string `json:"after"` After string `json:"after"`
Key string `json:"key"`
History bool `json:"history"`
} }
_ = json.Unmarshal(m.Data, &in) _ = json.Unmarshal(m.Data, &in)
a, err := views.FirstView(owner, state, Source{ctx, kv}, Source{ctx, marks}, in.After, time.Now()) var a any
var err error
if in.History {
a, err = views.ReadHistory(owner, state, Source{ctx, kv}, in.Key)
} else {
a, err = views.FirstView(owner, state, Source{ctx, kv}, Source{ctx, marks}, in.After, time.Now())
}
out := map[string]any{"result": a, "node": "n0"} out := map[string]any{"result": a, "node": "n0"}
if err != nil { if err != nil {
out = map[string]any{"error": err.Error()} out = map[string]any{"error": err.Error()}
@@ -108,3 +117,24 @@ func (s Source) Get(key string) (*views.Entry, bool, error) {
} }
return &views.Entry{Value: e.Value(), Revision: e.Revision(), Written: e.Created()}, true, nil return &views.Entry{Value: e.Value(), Revision: e.Revision(), Written: e.Created()}, true, nil
} }
// History is every value the bucket keeps for key, oldest first, as the tool runner's mesh/state.history says it
// (a put, or a delete with a null value); none kept is an empty history.
func (s Source) History(key string) ([]views.Revision, error) {
kept, err := s.KV.History(s.Ctx, key)
if errors.Is(err, jetstream.ErrKeyNotFound) {
return []views.Revision{}, nil
}
if err != nil {
return nil, err
}
out := make([]views.Revision, 0, len(kept))
for _, e := range kept {
r := views.Revision{Revision: e.Revision(), At: e.Created().UTC(), Op: "delete", Value: json.RawMessage("null")}
if e.Operation() == jetstream.KeyValuePut {
r.Op, r.Value = "put", json.RawMessage(e.Value())
}
out = append(out, r)
}
return out, nil
}