Count a bus refusal by what the bus said, not by how often the controller looked (issue 402)
mesh/merge-gate pass: builds mesh-controller → novox; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered

S9 reads the one remembered advisory again on every look for an hour, and
each look was kept as an observation and a line of evidence, so a single
refusal read as one repeated every 30 seconds. An observation may now say
when what it reads happened and how often; the keeper counts that, and a
look with nothing newer adds nothing. Sources that look at the present
keep counting their looks.
This commit is contained in:
2026-10-11 02:10:48 +02:00
parent f008fab9d8
commit c11222026a
4 changed files with 131 additions and 9 deletions
+12 -1
View File
@@ -152,7 +152,9 @@ type Condition struct {
// reopened after a send was there at the send unless the send fell in one of its gaps.
First time.Time `json:"first,omitzero"`
Gaps []Gap `json:"gaps,omitempty"`
// Observations is how many times it was observed since raised.
// Observations is how many times it was observed since raised: a look that sees it, for a source
// that looks at the present; for one that reads a record of what happened (Observation.Happened),
// how many times it happened, and LastObserved when it last did (novox/hq issue 402).
Observations int `json:"observations"`
// Count is how many times it has been raised, a reopening within ReopenWithin counted.
Count int `json:"count"`
@@ -208,6 +210,15 @@ type Observation struct {
Said string
Source string
Resolver string
// Happened is set by a source that reads a record of what happened rather than looking at the
// present — a bus advisory, remembered for an hour and read again on every look — to when it last
// happened; Times is how many times the record counts it. The condition then counts what happened,
// not the looks (novox/hq issue 402): one refusal looked at every half minute read as "observed 15
// time(s)", with a line of evidence every 30 seconds, and was taken for a refusal going on. A look
// that brings nothing newer than the condition's last observation adds no observation and no
// evidence. Zero for a source that looks at the present, whose every look is an observation.
Happened time.Time
Times int
// Confirm says a single look can be wrong about this finding — a question over the network that
// went unanswered, a time measured once on a loaded machine. The keeper does not read it: the
// source that looks again does, and raises it only when the next look sees it too, or while it is
+20 -7
View File
@@ -191,11 +191,16 @@ func (k *Keeper) Observe(ctx context.Context, o Observation) (Condition, error)
if err != nil {
return Condition{}, fmt.Errorf("reading the condition %s: %w", key, err)
}
// What was observed and when: the look, or what the record says happened (novox/hq issue 402).
at, observations := now, 1
if !o.Happened.IsZero() {
at, observations = o.Happened.UTC(), max(1, o.Times)
}
if !found {
c := Condition{Key: key, Kind: o.Kind, Subject: Subject{Scope: o.Scope, ID: o.ID, Machine: o.Machine, Also: o.Also},
Severity: o.Severity, Summary: o.Summary, Headline: o.Headline, Explanation: o.Explanation,
Resolved: o.Resolved, Needs: o.Needs, Actions: o.Actions, Evidence: []Evidence{{At: now, Said: said}},
Source: o.Source, Raised: now, LastObserved: now, Observations: 1, Count: 1,
Resolved: o.Resolved, Needs: o.Needs, Actions: o.Actions, Evidence: []Evidence{{At: at, Said: said}},
Source: o.Source, Raised: now, LastObserved: at, Observations: observations, Count: 1,
Resolver: orSelf(o.Resolver)}
change := ChangeRaised
k.mu.Lock()
@@ -244,7 +249,10 @@ func (k *Keeper) Observe(ctx context.Context, o Observation) (Condition, error)
}
// The kind as the source says it now: a source that gave the same key a kind of its own since
// (a probe's finding split out for a healer) is read by that kind from its next observation.
c.Kind, c.Summary, c.Source, c.LastObserved = o.Kind, o.Summary, o.Source, now
// A record read again with nothing newer in it is not observed again: its words are refreshed,
// its count, evidence and last observation stay (novox/hq issue 402).
fresh := o.Happened.IsZero() || at.After(c.LastObserved)
c.Kind, c.Summary, c.Source = o.Kind, o.Summary, o.Source
wasNeeds, wasActions := c.Needs, c.Actions
c.Headline, c.Explanation, c.Resolved, c.Needs, c.Actions = o.Headline, o.Explanation, o.Resolved, o.Needs, o.Actions
escalatedWords(&c)
@@ -257,10 +265,15 @@ func (k *Keeper) Observe(ctx context.Context, o Observation) (Condition, error)
if len(o.Also) > 0 {
c.Subject.Also = o.Also
}
c.Observations++
c.Evidence = append([]Evidence{{At: now, Said: said}}, c.Evidence...)
if len(c.Evidence) > KeptEvidence {
c.Evidence = c.Evidence[:KeptEvidence]
if fresh {
// A record counts what happened since it began; the condition never counts down, so a
// record begun again (a controller restarted) still adds the one it shows.
c.Observations = max(c.Observations+1, o.Times)
c.LastObserved = at
c.Evidence = append([]Evidence{{At: at, Said: said}}, c.Evidence...)
if len(c.Evidence) > KeptEvidence {
c.Evidence = c.Evidence[:KeptEvidence]
}
}
if err := k.stamp(&c); err != nil {
return Condition{}, err