Skip to content

feat: a window says how long a state held (ADR 0088) - #132

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

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

Conversation

@kfeofantov

Copy link
Copy Markdown
Contributor

Slice B of the "when" work, and the answer to "а если упало, но потом
восстановилось". Builds on #131.

The half that was left

ADR 0087 gave the two agent states a start instant, and named what it did not
do. A state field describes the present, collection_coverage supersedes, and
the backend is entitled to keep one row — so a state that ended leaves nothing:

13:47  failing_since: 13:47     the payload says so
14:06  (no field)               recovered — and the row is clean
                                nineteen minutes are now unrecoverable

agent_states is the other half: one record per (window, state, subject),
hour-aligned, superseding by (window start, window length) like the six
window kinds beside it.

Three numbers, and why each is there

  • occupied_nanos — how much of the hour the state held.
  • entries — how many episodes that was. Independent of the duration on
    purpose: one nineteen-minute outage and three six-minute ones are different
    diagnoses that a single number merges.
  • observed_nanos — how much of the hour the agent was running to watch at
    all. Without it, zero occupancy means both "a healthy hour" and "an hour
    nobody watched", which is ADR 0054's founding defect in a new shape.

A record is written for every watched subject, including a clear one — a
subject with no record is one nobody watched.

The relationship to collection_coverage is container_restarts to
restart_counters: one is a reading, the other a window, and the contract now
carries the same sentence keeping them apart.

Where this could have gone silently wrong

Occupancy is an advance, not a reading. The five journals beside this one resume
safely because a sample read twice is the same value; that property does not
transfer. So:

Accrual moves forward from accrued_to and is never recomputed from the
episode's start.

Write now - open_since once and a resumed window counts the whole pre-restart
span a second time — with no error, only a wrong number. open_since is the
marker that tells a resumed process the episode it finds open is the one already
counted. Both directions have tests that fail on the number.

One assumption, measured rather than assumed

Sampling states at the flush pass could in principle miss an episode that began
and ended between two passes. It cannot here: a pass runs every minute
(everyPass) and a new run of watch failures begins only after two minutes of
quiet (watchStreakGap), so two episodes cannot both begin and end between two
passes. The halt cannot flap at all — it latches until the process ends.

What it still cannot say

In ADR 0088 under its own heading, not buried in consequences: a restart
crossing a window boundary adds one entry to a continuing episode (detectable —
since moves at the same moment); an episode that closed between two passes
undercounts by up to one pass, because guessing when it closed would be the
payload's only invented number; and the marker survives a process restart, not a
pod replacement, because the spool is an emptyDir.

Not in this PR

The counter deltas — filter.excluded_*, shipping.deferred, the puller's
outcomes, the intake rejections. They have the same blind spot for the same
reason, but subtracting a previous reading is a different mechanism with its own
failure modes, which ADR 0062 already priced. Named as out of scope in ADR 0088
rather than left to look finished.

Evidence

go test -race ./...            all packages ok
go vet -tags e2e ./...         clean
GOOS=linux golangci-lint run   0 issues
make proto-lint                clean (no schema change: ADR 0074 schemas only
                               the three kinds someone has read)
git archive HEAD | tar -x      builds and tests clean from the extraction

🤖 Generated with Claude Code

https://claude.ai/code/session_01KeZECJXTDHL6Nts95eKMJA

ADR 0087 gave the two agent states a start instant and named what it did not
do: a state that ended is absent from the next payload by design, because a
state field describes the present and the kind supersedes. A watch that failed
for nineteen minutes and recovered left the backend's row clean.

`agent_states` is the other half — one record per (window, state, subject),
hour-aligned, carrying how much of the hour the state held, how many episodes
that was, and how much of the hour the agent was running to watch at all. The
relationship to `collection_coverage` is `container_restarts` to
`restart_counters`: one is a reading, the other a window, and the contract
carries the sentence that keeps them apart.

Occupancy is an advance rather than a reading, so resuming it is where this
could go silently wrong. Accrual moves forward from `accrued_to` and is never
recomputed from the episode's start; `open_since` is what tells a resumed
process the episode it finds open is the one already counted. Both directions
are covered by tests that fail on a number, not on an error.

Sampling at the pass loses nothing, and that is measured rather than assumed: a
pass runs every minute and a new run of watch failures needs two minutes of
quiet to begin, so two episodes cannot both begin and end between two passes.

The counter deltas are deliberately not here — subtracting a previous reading
is a different mechanism with its own failure modes (ADR 0062 priced them).

  go test -race ./...            all packages ok
  go vet -tags e2e ./...         clean
  GOOS=linux golangci-lint run   0 issues

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KeZECJXTDHL6Nts95eKMJA
@kfeofantov
kfeofantov force-pushed the feat/a-state-says-when-it-began branch from 39d1328 to 5c4cb36 Compare September 18, 2026 14:35
@kfeofantov
kfeofantov merged commit d05f83f into main Sep 19, 2026
5 checks passed
@kfeofantov
kfeofantov deleted the feat/a-state-says-when-it-began branch September 19, 2026 08:28
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