Check stability records (noise-damping stage 2) + the stages 2–4 plan#372
Merged
Conversation
The exploration and staging plan for reducing close-reopen alert noise beyond the static linger: rolling transition aggregates recorded at filing time, adaptive linger and open-grace windows derived from them, Nagios-style flapping state, and operator-in-the-loop pattern suggestions. Includes the off-the-shelf survey (pipeline services vs embedded analysis libraries) and the augurs/changepoint maintenance check. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AEZZ9HMkKAFqrJzTg3FrE7
passcod
force-pushed
the
claude/alerting-noise-damping-plan
branch
from
July 18, 2026 23:58
c8d06c0 to
d9ea7f6
Compare
Every stamped filing now feeds a fixed-size check_stability row (1:1 with the check state) from *observed* results, so policy and any future damping never feed back into the statistics: lifetime observation counters, a 32-entry healthy<->degraded transition ring, and a 168-bucket hour-of-week duty profile whose buckets halve at a cap so it leans towards recent weeks. Skipped observations carry no signal. A data migration backfills the records from the last 30 days of status history (present-in-push checks only; canopy's own checks start cold), so profiles are useful immediately. Read surfaces, no behaviour hooks yet: - the check attention page gains a stability column (flips/24h-7d) and, in the expanded row, the derived stats plus a 7x24 UTC duty-cycle heatmap — a load-dependent check reads at a glance. - the get_check_stability MCP tool returns full records with derived flap statistics for a set of (source, check) pairs, optionally narrowed to a server or group. Spec: CHK gains a Stability section; MCP spec lists the tool. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AEZZ9HMkKAFqrJzTg3FrE7
The status-history backfill moves out of migrations onto the monitor pod: a single fleet-wide INSERT..SELECT would hold FK row locks (FOR KEY SHARE) on most live issues rows until commit, and the filing path's SELECT .. FOR UPDATE conflicts with those — so the whole replay would stall ingestion. Instead the pod replays one server per statement (riding the (server_id, created_at) statuses index, row locks held for milliseconds), gated by a completion-marker table and an advisory lock; a crash mid-run resumes on the next startup and existing rows are never overwritten. The table-creation migration also gains a lock_timeout guard: attaching the FK takes SHARE ROW EXCLUSIVE on issues, which is instant to hold but queues behind long transactions — and stalls ingestion writes queued behind it. Better to fail the migrate run fast and retry. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AEZZ9HMkKAFqrJzTg3FrE7
Everything transitional — the replay code, the monitor-pod startup call, its test, and the marker table — carries TODO(backfill-removal), to be deleted (marker table via a migration) once every deployment has run the backfill. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AEZZ9HMkKAFqrJzTg3FrE7
The per-check page at /healthchecks/:source/:check is really the check's home — policy chips, documentation, and now stability — so name it that: the CheckDetail page, with three sections (About this check, Fleet stability, Needs attention). The needs-attention server list is unchanged; the settings editor at /settings/healthchecks/:check is renamed HealthcheckSettings to free the name. The private API endpoint follows: status_check_attention -> status_check_detail (breaking for API consumers; the UI is the only one). New Fleet stability section answers 'is this the whole fleet or one server': every server's duty profile summed into one heatmap plus aggregate flap counts, computed client-side from the per-server records already in the response — a shared load-dependent pattern reads at a glance even when no single server stands out. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AEZZ9HMkKAFqrJzTg3FrE7
The needs-attention list adopts the grouping and ordering every other
list uses: rank buckets in display order (unranked last, no heading),
groups alphabetical within a bucket with the group name as a linked
section heading, servers by kind then name, ungrouped servers trailing.
Position no longer encodes urgency — the result chip does.
Group- and canopy-scoped states of the check now show too:
check_state_for_check returns all scopes, and the response carries
groups (with their status-page rank bucket) and the canopy-wide state.
A group's own state files under its heading as a 'whole group' row;
canopy's self-monitoring state gets a trailing section. Both carry the
same expandable detail + stability record as server rows, and both
feed the fleet stability rollup ('Across 12 servers, 1 group…').
Server rows gain rank and kind on the wire so the client reuses the
shared ordering helpers rather than reimplementing them.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AEZZ9HMkKAFqrJzTg3FrE7
Each group heading on the check detail list now carries the group's stability aggregate — flip counts summed over every member's record plus the group's own state, untouched by the healthy toggle — and expands to the combined duty-cycle heatmap and statistics: the same rollup the fleet section shows, scoped to one group. With per-row records and the fleet section, stability now reads at all three levels: server, group, fleet. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AEZZ9HMkKAFqrJzTg3FrE7
ReturnType<typeof useTheme> resolves to unknown under project-reference build mode (tsc -b, what CI runs). Row is a component, so it calls useTheme() directly instead of receiving theme as a typed prop. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AEZZ9HMkKAFqrJzTg3FrE7
passcod
marked this pull request as ready for review
July 20, 2026 00:20
Broken shared amber with warning, so the two chips read identically. Give broken amber/charcoal hazard tape instead — instantly distinct from a plain warning, and apt for 'the check itself couldn't run'. The white bold label with a dark halo stays legible over both stripe colours, in light and dark themes; the tooltip and label still carry the meaning (status is never colour-alone). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AEZZ9HMkKAFqrJzTg3FrE7
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
🤖 Stacked on #371 (stage 1, the static linger). Implements stage 2 — transition aggregates of the noise-damping plan, plus the plan document itself (
docs/plans/alerting-hysteresis.md, covering stages 2–4, the off-the-shelf survey, and the augurs maintenance check). No behaviour hooks into the incident pipeline yet — everything here is recording and read surfaces, immediately useful for analysis on its own.The stability record (spec: CHK gains a "Stability" section)
Every stamped filing feeds a fixed-size
check_stabilityrow, 1:1 with the check state, built from observed results — pre-policy, so operator grading and any damping later built on these numbers can never feed back into them. Skipped observations carry no signal. The row holds:Deliberately not a new append-only log (we just deleted
eventsfor being one): the record is a bounded summary per state, updated in place inside the existing filing transaction. Flap statistics — flips in 24h/7d, ring coverage, typical degraded-run and healthy-gap durations — are derived at read time, not stored.Backfill — designed to not block ingestion, and marked for deletion
A one-shot pass on the monitor pod (not a data migration) replays the last 30 days of status history into the records, so duty profiles and rings are useful from day one. Why not a migration: migrations run concurrently with the live workload, and a single fleet-wide
INSERT…SELECTwould hold FK row locks (FOR KEY SHARE) on most liveissuesrows until commit — the filing path'sSELECT … FOR UPDATEconflicts with those, so the whole replay would stall ingestion. Instead the pod replays one server per statement (riding the(server_id, created_at)statuses index; row locks held for milliseconds), gated by a completion-marker table plus an advisory lock so concurrent pods don't duplicate the scan; a crash mid-run resumes on the next startup, and rows that already exist are never overwritten. The table-creation migration carries alock_timeout = 5sguard for the FK's momentarySHARE ROW EXCLUSIVEonissues, failing the migrate run fast instead of queueing ingestion writes behind it.Everything transitional is tagged
TODO(backfill-removal)(replay code, monitor-pod call, its test, marker table) — one grep finds the whole cleanup once every deployment has run it. Approximations vs live recording, noted in the code: only checks present in a push's health array are replayed (recovery-by-omission isn't), and canopy's own checks have no status history so they start cold.The check detail page (renamed from "attention")
The per-check page at
/healthchecks/:source/:checkis now the check detail page — it was already the check's home (policy chips, documentation, now stability), and "attention" only describes one part of it. Three sections:All scopes now show:
check_state_for_checkreturns group- and canopy-scoped states too. A group's own state (backup health and the like) files under its group heading as a whole group row, bucketed by the group's status-page rank; canopy's self-monitoring state gets a trailing canopy section. Both feed the fleet rollup. Server rows gainedrank/kindon the wire so the client reuses the shared ordering helpers.The settings editor at
/settings/healthchecks/:checkis renamedHealthcheckSettingsto free the name; the private endpoint follows (status_check_attention→status_check_detail, breaking only for the UI, which is its sole consumer).MCP
get_check_stability: takes up to 32 (source, check) pairs, optionally narrowed to one server or group, and returns each matching state (all scopes) with its full record and derived stats — the raw material for agents to tell a flap from a load-dependent pattern from a real change.Tests
check_stabilitysuite: counters/ring/duty recording, skipped-carries-no-signal, observed-not-effective, ring bounding, bucket halving, backfill replay + marker gating + no-overwrite, pure derived-stats. MCP tool covered end-to-end. Playwright coverage for the stability column, per-server heatmap, no-record state, fleet rollup, and the group/canopy sections. Full database (167), private-server (242), statuses, and e2e (144) suites green.Plan context
docs/plans/alerting-hysteresis.mdtracks the staging: stage 1 ✅ #371, stage 2 ✅ this PR, stage 3 (adaptive linger + open grace from these aggregates) and stage 4 (flapping state, operator-confirmed pattern suggestions) still to come.🤖 Generated with Claude Code
https://claude.ai/code/session_01AEZZ9HMkKAFqrJzTg3FrE7