C4: a statement keeps since when its engine is unanswered, and the keeper names each node unanswered past a bound while heard (issue 1966, ADR 0668 §3; review round 1, finding 4)

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
This commit is contained in:
2026-11-10 04:15:01 +01:00
parent c584f4570b
commit c6689bfdb3
2 changed files with 59 additions and 1 deletions
@@ -137,6 +137,9 @@ type Statement struct {
Answered bool `json:"answered"`
Words string `json:"words,omitempty"`
AskAgain *Again `json:"ask-again,omitempty"`
// UnansweredSince is when the engine first did not answer, of the asks since its last answer; zero once answered
// (ADR 0668 §3: a kept call unanswered past five minutes is a finding of its caller).
UnansweredSince time.Time `json:"unanswered-since,omitzero"`
}
// Again is a person's ask again of one node, written by the tool and answered by the part that keeps the state.
@@ -328,6 +331,31 @@ func (k *Keeper) settle(s Statement, why string) {
k.take(s)
}
// Unanswered is, of nodes, each whose engine the kept call has not answered for bound or longer by now while heard says
// its machine is heard, with since when (novox/hq ADR 0668 §3); a node whose machine is away is none. With no Call, none.
func (k *Keeper) Unanswered(nodes []string, heard func(node string) bool, bound time.Duration, now time.Time) map[string]time.Time {
out := map[string]time.Time{}
if k.Call == nil {
return out
}
k.mu.Lock()
defer k.mu.Unlock()
for _, node := range nodes {
s, ok := k.by[node]
if ok && !s.Answered && !s.UnansweredSince.IsZero() && now.Sub(s.UnansweredSince) >= bound && heard(node) {
out[node] = s.UnansweredSince
}
}
return out
}
// Asking says an ask of node runs now.
func (k *Keeper) Asking(node string) bool {
k.mu.Lock()
defer k.mu.Unlock()
return k.asking[node]
}
// take keeps s, unless the node did not answer and an earlier statement stands: an away node keeps its last one (ADR 0624
// §1), marked as not answering.
func (k *Keeper) take(s Statement) {
@@ -335,8 +363,14 @@ func (k *Keeper) take(s Statement) {
if k.by == nil {
k.by = map[string]Statement{}
}
if !s.Answered {
s.UnansweredSince = s.AskedAt
if old, ok := k.by[s.Node]; ok && !old.Answered && !old.UnansweredSince.IsZero() {
s.UnansweredSince = old.UnansweredSince
}
}
if old, ok := k.by[s.Node]; ok && !s.Answered && old.From != FromNone {
old.Answered, old.AskedAt, old.Words, old.AskAgain = false, s.AskedAt, s.Words, nil
old.Answered, old.AskedAt, old.Words, old.AskAgain, old.UnansweredSince = false, s.AskedAt, s.Words, nil, s.UnansweredSince
s = old
}
k.by[s.Node] = s