enginereads: the event heard is kept in the node's statement (issue 1975 note 6)
mesh/delivery merged: source.main moved to novox/mesh-contracts@0e12819cc727
mesh/delivery-group group fix/engine-reads-and-compositions merged: every member is merged
mesh/check checking 04c6a1eead98 on 0e12819cc727 | novox/mesh-contracts#8 in group fix/engine-reads-and-compositions

Builds: issue 1975 note 6 (the lead's review round 1; no design line). The engine 'not yet' half is dropped there.

Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
2026-11-12 11:00:33 +01:00
parent e575659187
commit 04c6a1eead
2 changed files with 40 additions and 0 deletions
@@ -143,6 +143,15 @@ type Statement struct {
// EventAt is when the part last heard the seat's event manifest-reads-changed for the node, beside when it asked and // EventAt is when the part last heard the seat's event manifest-reads-changed for the node, beside when it asked and
// what the engine stated then (issue 1966 note 170: was the event heard, and was it asked again after it). // what the engine stated then (issue 1966 note 170: was the event heard, and was it asked again after it).
EventAt time.Time `json:"event-at,omitzero"` EventAt time.Time `json:"event-at,omitzero"`
// Heard is the newest event heard of the node with what it says the engine states, kept so the state shows what the
// rules read while the statement is behind it (issue 1975 note 6), and so a part that starts again holds it.
Heard *HeardEvent `json:"heard,omitempty"`
}
// HeardEvent is an event manifest-reads-changed as the part kept it: when the engine said it and what it said.
type HeardEvent struct {
At time.Time `json:"at"`
ManifestReads []string `json:"manifest-reads,omitempty"`
} }
// Again is a person's ask again of one node, written by the tool and answered by the part that keeps the state. // Again is a person's ask again of one node, written by the tool and answered by the part that keeps the state.
@@ -473,6 +482,11 @@ func (k *Keeper) take(s Statement) {
if at := k.eventAt[s.Node]; at.After(s.EventAt) { if at := k.eventAt[s.Node]; at.After(s.EventAt) {
s.EventAt = at s.EventAt = at
} }
if e, ok := k.said[s.Node]; ok {
if at, err := time.Parse(time.RFC3339Nano, e.At); err == nil {
s.Heard = &HeardEvent{At: at.UTC(), ManifestReads: slices.Clone(e.ManifestReads)}
}
}
if !s.Answered { if !s.Answered {
s.UnansweredSince = s.AskedAt s.UnansweredSince = s.AskedAt
if old, ok := k.by[s.Node]; ok && !old.Answered && !old.UnansweredSince.IsZero() { if old, ok := k.by[s.Node]; ok && !old.Answered && !old.UnansweredSince.IsZero() {
@@ -481,6 +495,7 @@ func (k *Keeper) take(s Statement) {
} }
if old, ok := k.by[s.Node]; ok && !s.Answered && old.From != FromNone { if old, ok := k.by[s.Node]; ok && !s.Answered && old.From != FromNone {
old.Answered, old.AskedAt, old.Words, old.AskAgain, old.UnansweredSince = false, s.AskedAt, s.Words, nil, s.UnansweredSince old.Answered, old.AskedAt, old.Words, old.AskAgain, old.UnansweredSince = false, s.AskedAt, s.Words, nil, s.UnansweredSince
old.Heard = s.Heard
s = old s = old
} }
k.by[s.Node] = s k.by[s.Node] = s
@@ -529,6 +544,15 @@ func (k *Keeper) Heard(node string, raw []byte, deleted bool) {
_, had := k.by[node] _, had := k.by[node]
if !had { if !had {
k.by[node] = s k.by[node] = s
if h := s.Heard; h != nil && !h.At.IsZero() {
if k.said == nil {
k.said, k.catching = map[string]nodeengine.ManifestReadsChangedEvent{}, map[string]int{}
}
if _, ok := k.said[node]; !ok {
k.said[node] = nodeengine.ManifestReadsChangedEvent{V: ContractVersion, Node: node, Why: ReadsStarted,
At: h.At.UTC().Format(time.RFC3339Nano), ManifestReads: slices.Clone(h.ManifestReads)}
}
}
} }
k.mu.Unlock() k.mu.Unlock()
if !had && k.Changed != nil { if !had && k.Changed != nil {
@@ -246,6 +246,22 @@ func TestAStatementOlderThanAHeardEventDoesNotStand(t *testing.T) {
t.Errorf("after the event's ask: %v, %d calls", s.ManifestReads, calls) t.Errorf("after the event's ask: %v, %d calls", s.ManifestReads, calls)
} }
// The event heard is in the node's statement as the part keeps it, and a part that starts again holds it.
var kept []byte
keeper := &Keeper{Sync: true, Put: func(_ string, v []byte) error { kept = v; return nil }, Call: func(string, string, any) (json.RawMessage, error) {
return answer(`"seat:a"`, early), nil
}}
keeper.HeardChanged(event)
keeper.AskNode("ace", "x")
if st, err := Decode(kept); err != nil || st.Heard == nil || len(st.Heard.ManifestReads) != 2 {
t.Errorf("the kept statement shows the event heard as %+v, %v", st.Heard, err)
}
restarted := &Keeper{Sync: true}
restarted.Heard("ace", kept, false)
if got := restarted.Reads([]string{"ace"})["ace"]; len(got) != 2 {
t.Errorf("a part started again reads %v, want the event's", got)
}
// An engine that answers the old statement to every ask: the event's reads stand, the asks are bounded. // An engine that answers the old statement to every ask: the event's reads stand, the asks are bounded.
calls = 0 calls = 0
stuck := &Keeper{Sync: true, Call: func(string, string, any) (json.RawMessage, error) { stuck := &Keeper{Sync: true, Call: func(string, string, any) (json.RawMessage, error) {