Skip to content

feat: a state says when it began (ADR 0087) - #131

Merged
kfeofantov merged 1 commit into
mainfrom
feat/a-state-says-when-it-began
Sep 18, 2026
Merged

kfeofantov merged 1 commit into
mainfrom
feat/a-state-says-when-it-began

Conversation

@kfeofantov

Copy link
Copy Markdown
Contributor

Slice A of the "when" work: the two fields in collection_coverage that are
states rather than counts now carry the instant they began.

The defect

sources[].failing and shipping.halted were bare booleans in a payload that
supersedes — the backend upserts, last write wins, one row is all it is obliged
to keep. So a state that ended left nothing behind. A watch that failed for
nineteen minutes and recovered produced, on the next flush, bytes identical to a
watch that never failed; an outage shorter than the flush interval was never
stated at all. The agent knew and did not say.

halted was the sharper case: a halted agent ships nothing, so the payload
carrying the fact never travels. Its readers are the spool and the metrics
endpoint, and to both "shipping stopped" without "at 04:31 nine days ago" is
nearly the same as silence.

What changed

  • sources[].failing_since — the instant the run of failures began, not the
    latest failure. watchHealth already recorded it and SourceHealths reduced
    it to a boolean on the way out.
  • shipping.halted_since — the halt had no clock at all, because nothing had
    ever asked it for one.
  • shipping_halted_since_seconds on /metrics. One gauge, not two:
    for a halt the metrics endpoint is the only channel that works, while
    source_failing ships on every pass and a scrape dates itself.
  • synced is deliberately untouched — a latch, not a state that ends, and the
    instant it would carry is the payload's own since.

Why the booleans stay

§6 of backend-requirements.md requires both forms across a deprecation period,
and the schema gate enforces the same independently: buf breaking runs the
FILE category, which refuses the deletion of a field even with its number
reserved. Measured rather than assumed — the attempt and its error are in
ADR 0087 §3. Both booleans are now derived from the instant beside them, in one
place each, so no payload can carry a state with no start or a start with no
state.

Riding along: a promise that had gone stale

security.md said the startup read "is a pure read of five kinds and no others"
and named five. cmd/agent/recover.go has resumed six since ADR 0080 added
goroutine_counts — which is the list ADR 0077 had itself declared closed,
with the instruction that a sixth kind means changing it there. Corrected in the
same diff and named here so it is not mistaken for part of the feature.

What this does not do

A state that ended is still invisible. Recovering that needs occupancy within a
window — how much of an hour the agent spent in a state, and how many times it
entered — which is slice B, a different mechanism with a different cost, and is
neither decided nor pre-judged here.

Evidence

go test -race ./...            all packages ok
GOOS=linux golangci-lint run   0 issues
make proto-lint                buf lint and buf breaking clean
git archive HEAD | tar -x      builds and tests clean from the extraction

🤖 Generated with Claude Code

https://claude.ai/code/session_01KeZECJXTDHL6Nts95eKMJA

`sources[].failing` and `shipping.halted` were bare booleans in a payload that
supersedes, so a state that ended left nothing behind: a watch that failed for
nineteen minutes and recovered produced bytes identical to one that never
failed, and an outage shorter than the flush interval was never stated at all.

Both instants already existed and were discarded on the way out — `watchHealth`
records where each run of failures began, and the halt is now stamped by a clock
the shipper did not have. The booleans stay for the compatibility window of
backend-requirements.md §6 and are derived from the instants, because `buf
breaking` runs the `FILE` category, which refuses a field deletion even with the
number reserved (measured, not assumed).

`synced` is deliberately untouched: it is a latch, and the instant it would
carry is the payload's own `since`.

Also corrects a promise that had gone stale: security.md said the startup read
covers five kinds and named five, while cmd/agent/recover.go has resumed six
since ADR 0080 added `goroutine_counts`.

  go test -race ./...        all packages ok
  GOOS=linux golangci-lint run   0 issues
  make proto-lint            buf lint and buf breaking clean

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KeZECJXTDHL6Nts95eKMJA
@kfeofantov
kfeofantov merged commit 56d81c5 into main Sep 18, 2026
5 checks passed
@kfeofantov
kfeofantov deleted the feat/a-state-says-when-it-began branch September 18, 2026 14:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant