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
}
// 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
// machine), <machine>.<module> or person.<name> (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
// 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
// it. Granted, as any tool is, to every reader that invokes it, and callable by a session.
// revision, one key's kept history (values a Historian), or a page of values in the first view's form, and the state's
// 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 {
return stdio.Tool{
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 "+
"owner last marked it); with key, that key's value and revision; 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),
"owner last marked it); with key, that key's value and revision; with key and history, every value the state still "+
"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{
"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"},
"after": map[string]any{"type": "string", "description": "with values: the key the page before ended at"}}},
"key": map[string]any{"type": "string", "description": "one key: its value and revision"},
"history": map[string]any{"type": "boolean", "description": "with key: every value the state keeps for it, oldest first"},
"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) {
key, _ := args["key"].(string)
after, _ := args["after"].(string)
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())
},
}
@@ -119,6 +139,47 @@ type KeyAnswer struct {
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.
func Read(owner, state string, values, marks Source, key string, all bool, after string, now time.Time) (any, error) {
switch {
+29 -1
View File
@@ -32,6 +32,14 @@ func (m mapSource) Get(key string) (*Entry, bool, error) {
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 {
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}
@@ -97,7 +105,7 @@ func TestViews(t *testing.T) {
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())
ra, ok := r.(ReadAnswer)
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 == "" {
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.
+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
// 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
// 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 {
t.Helper()
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+".")
sub, err := nc.Subscribe("mesh.mod."+owner+".call."+tool+".>", func(m *nats.Msg) {
var in struct {
After string `json:"after"`
After string `json:"after"`
Key string `json:"key"`
History bool `json:"history"`
}
_ = 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"}
if err != nil {
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
}
// 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
}