Skip to content

fix: anchor heartbeat evaluation on real beats and close stale status-page incidents - #309

Merged
fclairamb merged 48 commits into
mainfrom
batch/2026-09-02
Sep 3, 2026
Merged

fclairamb merged 48 commits into
mainfrom
batch/2026-09-02

Conversation

@fclairamb

Copy link
Copy Markdown
Owner

Five specs from specs/todos/, implemented sequentially on a batch branch, each gated by an independent completeness audit before being archived. Two of the five needed a fix round; the audit caught a regression one of them introduced.

Targeted at the v0.22.2 patch release — the new migration 018_v0_22_2 is named accordingly.

What changed for the user

Heartbeat and email checks now detect a missing beat reliably. The scheduler's periodic evaluation was reading back the newest result row of any origin — which, from the second period after a beat, was its own previous evaluation rather than the beat. A heartbeat that stopped beating therefore stayed Up for a random number of extra periods (a ~50% coin flip per tick, with an unbounded tail), the row that finally opened the incident reported a nonsense overdueBy of a few milliseconds and a lastSignalAt pointing at another evaluation, and the "run started but never completed" branch was unreachable in production because every evaluation re-anchored the run on itself. The evaluation now reads the newest inbound signal specifically, so overdue detection fires on schedule, the diagnostics carried into incidents and notifications name the real beat, and stale runs time out after two periods as designed.

A result row written by the scheduler no longer looks like a beat. Both kinds of row read "Heartbeat received" with status Up, so opening the evaluation row written seconds after a ping showed no Caller card and a bare JSON dump — and people reasonably concluded their caller had not been recorded. Evaluation rows now say so: the on-time message is "Heartbeat on time" (the beat keeps "Heartbeat received"), the recent-results table tags them with a muted Evaluation badge, and the detail page renders a Scheduler evaluation card explaining what wrote the row, when the last real signal arrived, how long before this evaluation it was, and a link straight to that beat. A row whose newest signal was a failed beat now says "Last heartbeat reported failure" instead of the flatly untrue "No heartbeat received". Beat rows are stored byte-for-byte as before, with tests pinning that.

A status-page incident can no longer outlive the outage it describes. Publishing an already-resolved incident minted a fresh publication in the investigating state, as though the outage were live — and because publishing also stamped the entry as human-touched, the if_untouched auto-resolve default behaved exactly like never. One such publication sat open on a public page for ten days, flooring the TV wallboard at "Some Systems Degraded" while every check was up and the ordinary status page said "All Systems Operational". Publishing a resolved incident now produces a resolved publication with the incident's own resolution time and a retroactive post-mortem timeline; linking an incident to a page no longer counts as taking over the narrative; and auto-resolve now covers any publication linked to the resolving incident, hand-published or not (free-form publications with no linked incident stay untouched, as promised). Where the state can still arise, it is visible: dash0 warns on the checks list and the status-page detail page when an incident is open on a public page while every linked check is up, and the TV board now says when its amber comes from an incident rather than a check.

The embedded-devices panel on a heartbeat check is collapsed by default. The TCP/UDP block was fully expanded on every heartbeat check, doubling the height of the endpoint card for the majority of users who only ever hit the HTTPS URL. It now collapses behind a summary line that still names what is enabled (TCP 4001 · UDP 4001 · signed only), with the nc one-liners as their own disclosures. The token-rotation warning deliberately stays outside the collapsed section — it is a security notice and hiding it behind a chevron would defeat it. The 40-line Arduino/ESP sketch it carried inline is gone in favour of a link to the docs page that already published it next to the counter recipe and security discussion, so the two copies can no longer drift.

The "create a weekly uptime report for me" wand produces a report with checks in it. It previously created a schedule with an empty scope, which the backend correctly treats as org-wide but which shows up as two blank pickers and nothing the operator can point at. It now attaches the 10 most recently created checks; an org with no checks yet still gets the org-wide scope.

Notes for review

  • Migration: one new partial index, results_raw_signal_idx, appended to this cycle's consolidated 018_v0_22_2 in both dialects. Without it a silent heartbeat's signal lookup would walk every evaluation row inside raw retention on every tick. GetLastResultForChecks is deliberately untouched — it still serves the API's lastResult, where "newest row of any origin" is the right answer.
  • API: no schema changes. ?stale=true is added to the org-side status-page incident list, with a stale boolean on each item meaning "the linked incident is resolved"; the output field's description now points at the new docs paragraph.
  • The audit caught a regression this batch introduced. Widening auto-resolve to hand-published entries made a pre-existing asymmetry in OnIncidentReopened reachable: such an entry could be auto-resolved on recovery and then silently fail to reopen on relapse, leaving the publication resolved through a live outage and the wallboard green — strictly worse than the bug being fixed. Both directions now gate on the same condition, with a test asserting the public feed is non-empty after a relapse.
  • E2E is authored but was not run locally. Playwright's global setup tears down and recreates the shared dev Postgres, which would disrupt the running devloop, so the new specs (check-heartbeat-push, magic-wand, check-heartbeat-evaluation-rows, stale-publication-banner, tv-board) were verified with --list and eslint only. CI is the real gate for them — worth confirming the E2E job actually ran rather than being skipped behind a red upstream job.
  • Behaviour change worth a second opinion: incident reopen consults neither the page's autoResolve policy nor humanTouchedAt. That was already true for auto-created entries, and making the two directions symmetric was the point, but it now means a hand-published entry an operator resolved by hand can be reopened by a machine-written relapse update even on a page set to autoResolve: never. The direction of error is safe (the page shows amber during a live outage) and it is out of this spec's scope, but it is not something the spec spelled out.
  • Not done, deliberately: the reported acme page still has its stale publication open. That is a one-click ops action on the live instance, not code, and this run was unattended.

Follow-ups spotted, not addressed

  • dash0's "This incident is on autopilot" alert is still gated on autoCreated && !humanTouched, so an untouched hand-published entry now self-resolves without that notice.
  • A bare ?stale=true with no active=true filters within the newest 200 publications, so an older open+stale row could be invisible on a page with a long history. The only caller always sends active=true.
  • Two specs (2026-09-03-01-getting-started-card-modern-redesign, 2026-09-03-01-operator-notifications-support-messages-and-signups) arrived in specs/todos/ from another session while this batch was running and were deliberately left for a later batch.

🤖 Generated with Claude Code

fclairamb and others added 30 commits September 2, 2026 23:51
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The embedded TCP/UDP heartbeat block was always fully expanded and
carried a duplicate of the Arduino/ESP sketch that already lives in
the docs. Wrap it in the existing CollapsibleSection primitive
(collapsed by default, summary line shows enabled transports and
"signed only"), turn the TCP/UDP one-liners into collapsed
CollapsibleCode blocks, drop the inline arduinoSketch() generator,
and point at the docs page instead. The rotate-token security nudge
stays outside the collapsible body so it is visible even collapsed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Add summaryTcp/summaryUdp/summarySignedOnly (collapsed section
summary line) and sketchDocsHint (docs pointer) to all four locales;
drop the now-unused sketchLabel/sketchHint keys the removed inline
Arduino sketch used.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The dash0 heartbeat push panel no longer renders a copy of this
sketch filled in with the check's real values (spec
2026-09-02-01) — it links here instead. Add a sentence above the
sketch naming the four placeholders and pointing at the check's own
Embedded devices panel, so the docs copy is self-sufficient without
the page's substitution.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Assert the Embedded devices section is collapsed on load (summary
line visible, one-liners not), expand it via the new
heartbeat-push-toggle trigger before reading the TCP/UDP commands
(each now sits behind its own CollapsibleCode disclosure), and
assert the docs link (scoped to the section, to avoid ambiguity with
the check-type docs link in the page header) instead of the removed
inline Arduino sketch. Also assert the rotate-token nudge stays
visible after a reload even though the section collapses again.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… always fully expanded

tsc -b, scoped eslint (no new errors/warnings in touched files),
make build-dash0, bun run lint (baseline 42 pre-existing errors,
none in touched files), bun run test:unit (74 files / 1260 tests
green, including locale-key parity), make build-docs, and a
node/github-slugger + built-HTML check confirming the
a-minimal-arduino--esp-sketch anchor resolves. E2E
(check-heartbeat-push.spec.ts) authored and playwright --list
verified syntactically valid; not executed — the local :4000
devloop is not SP_RUNMODE=test (test-org login 401s) and a side-car
test server was out of scope for this pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…always fully expanded

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
buildWeeklyReportWandPayload now takes the fetched checks and caps
checkUids at the first 10 (newest-created-first). The report-schedules
index page fetches useChecks(org, { limit: 10 }) and keeps the wand
disabled/spinning while that query loads, same as during the create
mutation. checkGroupUids stays empty; a zero-check org still falls back
to the empty org-wide scope.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Covers: caps at 10 keeping order, passes through fewer-than-10 checks
unchanged, falls back to the empty/org-wide scope on zero checks, and
checkGroupUids always empty. Locale-parity assertions unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Renamed from "creates a weekly org-wide report" — seeds 12 checks,
asserts the created schedule's checkUids equals GET /checks?limit=10
in the same order, the Scope column reads "10 checks, 0 groups", the
edit page's checks picker lists exactly those 10, and the
onboarding-step-report flip still holds.

Authored and playwright --list / eslint verified; not executed against
a live server this pass — see the gate commit for why.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…me" wand creates a report with an empty check list

tsc -b clean, scoped eslint (0 new errors/warnings in touched files;
baseline unchanged at 42 pre-existing errors), make build-dash0,
bun run test:unit (74 files / 1263 tests green, incl. locale-key
parity and the 4 new onboarding-wand.test.ts cases). E2E
(magic-wand.spec.ts, renamed test) authored and playwright --list /
eslint verified syntactically valid; not executed this pass — running
it once against :4000 triggered the suite's own docker-compose
down/up cycle and briefly took out the shared dev Postgres container,
so a second run was aborted rather than risk repeating that against
the shared devloop. Environment confirmed recovered (admin login,
existing org data intact) before continuing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…report with an empty check list

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…evious row

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A passive check's raw history interleaves inbound-signal rows (heartbeat
POST / incoming email, no worker_uid) with the passive evaluator's own
per-period rows (always worker_uid). GetLastResultForChecks returns the
newest row of ANY origin, which is right for the API's lastResult but
wrong for the evaluator.

GetLastSignalForChecks is the same per-check index descent narrowed by
worker_uid IS NULL, in both dialects, backed by a new partial index
results_raw_signal_idx (migration 018_v0_22_0) so a silent heartbeat's
descent does not walk every evaluation row inside raw retention.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…wn last row

executePassiveJob called backend.LastResults -> GetLastResultForChecks,
the newest raw row of any origin. Since the evaluation itself writes a raw
row every period, from the second tick after a beat onwards it read its
own predecessor: elapsed measured the gap between two evaluations
(period +/- claim jitter) instead of the gap since the beat.

Consequences: overdue detection was a per-tick coin flip with an unbounded
tail, the row that finally opened the incident reported overdueBy in
milliseconds and lastSignalAt pointing at the previous evaluation, and the
stale-run branch (Running beyond 2x period) was unreachable in production
because each Running evaluation re-anchored runStarted on itself.

LastResults becomes LastSignals across the backend interface (WS keeps
returning ErrPassiveUnsupported), and the default branch now carries
lastSignalAt when a non-Up/non-Running signal exists.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
worker_test-level regressions (checkworker/passive_signal_test.go): each
seeds BOTH a beat and a newer worker-written evaluation row, so the three
core cases fail on the pre-fix code — overdue detection, the email
variant, and the stale-run timeout that was unreachable in production.

DB-level contract in both dialects: worker rows skipped, the creation
marker still not a signal, an evaluation-only check absent from the map,
one entry per requested check, org isolation, and an EXPLAIN assertion
that both dialects ride results_raw_signal_idx.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ases

014's SQLite half rebuilds the results table and recreates the indexes it
knows about, so the byte-identity assertions have to name the indexes that
LATER migrations add rather than hard-code one exception. 018's
results_raw_signal_idx is now listed alongside 014's reaper index, and the
replay assertion excludes only what postdates 014.

Also fixes lint (line length, misspell, testifylint) in the new files.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… on its own previous row

make build-backend + lint-back (0 issues) + test all green.

Also verified out of band:
- the two new regression tests fail on pre-fix code (LastSignals pointed
  back at GetLastResultForChecks), and pass again once restored;
- both EXPLAIN assertions fail when results_raw_signal_idx is removed
  from migration 018 (positive control);
- the full non-short postgres package passes apart from
  TestIncrementUsageCounter_Concurrent_Postgres, which fails identically
  on the base commit 8cd1af9 in a clean worktree (embedded-Postgres
  "too many clients" under concurrent load on this machine).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ed v0.22.0

018 was written as 018_v0_22_0 on the spec's stale premise that v0.22.0 was
still pending. v0.22.0 was tagged 2026-09-02 and is an ancestor of this
branch: git ls-tree v0.22.0 shows its highest migration is 017_v0_21_0, so
the file was named after a release that had already shipped without it, and
its header asserted the opposite.

Renamed to 018_v0_23_0 in both dialects (unreleased, number free and
unique, so a rename is allowed per wiki/conventions/database.md) and the
headers now state the real relationship: v0.22.0 is the last released
version and it shipped with 017_v0_21_0, because a file is named after the
release it ships IN and v0.22.0 needed no DDL. v0.23.0 rather than v0.22.1
because v0.22.1 is already in an open release-please PR and this cycle
carries feat: commits.

wiki/conventions/database.md's unreleased-target pointer was two releases
stale (016_v0_19_0); it now names 018_v0_23_0, records this as the third
occurrence of the mistake it exists to prevent, and spells out that the
number and the version do not advance in lockstep.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… on its own previous row

make build-backend + lint-back (0 issues) + test all green after the
migration rename to 018_v0_23_0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…on its own previous row

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…rom beats

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…shable from beats

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…lliding messages

Evaluation rows now declare themselves with evaluation: true and point at
the beat they were computed from (lastSignalAt, lastSignalResultUid) on
every branch. The Up-on-time message becomes "Heartbeat on time" and a
deliberate down/error beat now reads "Last heartbeat reported failure /
error" instead of the false "No heartbeat received".

recordBeat, buildHeartbeatOutput, defaultOutputMessage and the email ingest
are untouched: a beat is stored byte-for-byte as before.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…JSON

Adds EvaluationCard (result detail), an Evaluation badge in the header and
in the Recent Results table for passive checks, and filters the evaluation
bookkeeping keys out of the check header's raw Output block. "Open the
signal" drops the region search param on purpose: the beat has no region.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… reference

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
TestExecutePassiveJob_MarksEvaluationRows tables heartbeat and email, and
its positive control re-reads the seeded beat and deep-compares its output
to what was inserted. TestExecutePassiveJob_MarksEveryBranch walks the rest
of the switch. The heartbeat ingest test now pins that recordBeat never
stamps the marker — both controls were verified to fail when the marker is
deliberately introduced on the ingest side.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
fclairamb and others added 16 commits September 3, 2026 01:39
…signal link

Moves createHeartbeatCheck/getAuthToken from live-updates.spec.ts into
fixtures.ts now that a second file needs them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Keeps the message table and the marking rules in one readable function and
puts executePassiveJob back under the funlen ceiling.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…uishable from beats

Gate: make build-backend lint-back test (green, 0 lint issues), make
build-dash0, dash0 eslint (42 errors, all pre-existing debt in files this
spec does not touch), bun run test:unit (1263 passed), make build-docs.

The new e2e file (check-heartbeat-evaluation-rows.spec.ts) is authored and
verified with --list + eslint but deliberately NOT run: Playwright's global
setup recycles the shared dev Postgres and would disrupt the running
devloop. The coordinator runs the full suite as the batch gate.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ishable from beats

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ncident

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…solved incident

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Publishing an incident that has already resolved minted a fresh
`investigating` entry that nothing would ever close: the only auto-close
trigger is OnIncidentResolved, which had already fired. Such a publication
is now born resolved, with resolved_at taken from the incident and the
templated investigating + resolved updates posted back to back, so the
public timeline reads as a retroactive post-mortem entry.

PublishIncident also stops stamping human_touched_at — linking an incident
to a page is not taking over its narrative — and OnIncidentResolved now
scopes on "linked to this incident" rather than "created by a machine", so
if_untouched applies to hand-published entries instead of behaving like
never. Free-form publications (no incidentUid) stay untouched under every
policy.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A publication is stale when it is still open on the public page while the
incident it tracks has already resolved — the state that leaves a TV board
amber with every check green. Each item now carries `stale`, and
?stale=true narrows the listing to exactly those, so dash0 can warn an
operator without polling every incident itself.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…not a check

A board that escalates past its own rollup because a publication is open is
correct and deliberate, but with nothing saying so the room reads it as
broken monitoring — which is how the reported page stayed amber for ten days
with every check up. The headline now carries a one-line cause when, and
only when, resolveTvState lands strictly worse than the server rollup.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…very

The checks list is where an operator lands when the wallboard goes amber.
When the amber comes from a publication rather than a check, that list is
all green and says nothing — so an entry claiming trouble can sit on a
public page indefinitely. An amber warning now names the page and links
straight to the entry, on the checks list and on the status page's own
detail route.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every new case fails against the pre-fix code: the retroactive publish, the
if_untouched auto-resolve of a hand-published entry, the public page's
active feed, and the stale flag. Paired with positive controls (publishing a
LIVE incident is unchanged) and the negative control the spec names by hand
— auto_resolve=never still leaves everything alone, and a free-form entry
sitting next to a linked one on the same page is untouched while the linked
one closes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…outlive its resolved incident

Lint fixes from the final gate: a query-flag constant, a spelling, and
CreatePublication split into requestedState + linkedIncident to stay under
the statement cap.

Backend: make build-backend lint-back test — green (0 lint issues).
dash0: build, bun run test:unit (1274 pass), eslint 42 pre-existing errors,
none in the files this spec touches.
status0: build, bun test ./src (97 pass), eslint 0 errors.
E2E authored but not run here (shared dev Postgres): 2 dash0 specs, 2 status0
specs, both verified with --list.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…apses

Widening auto-resolve to incident-linked publications opened a hole in the
other direction: OnIncidentReopened still gated on auto_created, so an entry
a recovery had just closed stayed resolved through the relapse. The active
feed excludes a resolved entry, so the public page and the TV board would
have read GREEN during a live outage — the reported bug's mirror image, and
one the stale flag cannot see. Both directions now scope on incidentUid, as
does the group-sibling walk, so a hand-published entry owning a consolidated
outage closes with the last member too.

Also from the audit: stale filtering happens before pagination; a retroactive
Create with a body posts the operator's words once, as the resolved entry;
the client re-applies the stale rule so a backend that ignores the filter
cannot light the banner during a real outage; countStale is deleted rather
than shipped dead; the TV subtitle stops promising 'all monitored services
are passing' over an already-impaired rollup; and the openapi/model/wiki
comments now agree that auto_created gates group consolidation only.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…outlive its resolved incident

Re-gate after the completeness audit's seven items.

Backend: make build-backend lint-back test — 0 lint issues, 193 packages ok,
no failures.
dash0: make build-dash0, bun run test:unit (1274 pass), eslint 42 errors —
the exact pre-existing baseline, none in the files this spec touches.
status0: make build-status0, bun test ./src (100 pass), eslint 0 errors.
E2E authored, not run here: 3 dash0 specs + 4 status0 specs, verified with
--list.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…utlive its resolved incident

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The batch PR is titled fix:, and this repo squash-merges, so release-please
sees only that title: the next release is the v0.22.2 patch, not the minor
v0.23.0 the previous rename predicted. v0.22.1 is already in an open
release-please PR and this batch lands after it.

Renamed 018_v0_23_0.{up,down}.sql to 018_v0_22_2.{up,down}.sql in both
dialects and corrected the headers. The substance is unchanged: v0.22.0 is
the last released version and it shipped with 017_v0_21_0 as its final
migration (a file is named after the release it ships IN, and v0.22.0
needed no DDL), 017 is frozen, 018 is unreleased and free.

wiki/conventions/database.md now points at 018_v0_22_2, keeps the
number-vs-version note, and extends the history to record this file's two
renames — the lesson being that under squash-merge the batch PR title is
what decides patch versus minor, so the suffix is resolved from the latest
tag plus what is in flight, never by incrementing the previous file's.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@fclairamb
fclairamb enabled auto-merge (squash) September 3, 2026 05:37
@fclairamb
fclairamb disabled auto-merge September 3, 2026 06:03
Measured on the real databases: prod has exactly one passive check with
signals and a walk depth of 0 (0.12 ms), so results_raw_signal_idx buys
prod nothing today. Dev's worst silent check does show the effect the
index was meant to remove (1118 rows, 10.5 ms, 477 page reads), but an
extra index on results — the hottest insert path in the product — is not
worth paying for a speculative read benefit on one check.

The query fix stays: GetLastSignalForChecks with worker_uid IS NULL in
both dialects, LastSignals, the executePassiveJob change and every
behavioural test. The coin-flip overdue detection and the unreachable
stale-run branch remain fixed; only the index and its scaffolding go.

Migration 018 is deleted rather than emptied — it was never applied
anywhere (dev and prod both run v0.22.0, highest applied migration 017),
so the number simply returns to being free, and an empty numbered
migration is worse than none.

The lastSignalForChecksQuery doc comments in both dialects now describe
what actually happens (results_raw_idx supplies the equality and the
ordering, worker_uid IS NULL is applied while walking, so the cost is one
row per evaluation row since the last signal) and record the measurements
plus the exact DDL to add when passive checks become numerous or
routinely silent. The archived spec carries the same note, and
wiki/conventions/database.md's unreleased-target paragraph is corrected —
018's write/rename/rename/withdraw cycle is now its worked example of why
unreleased numbers are free and released ones are frozen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@fclairamb
fclairamb enabled auto-merge (squash) September 3, 2026 06:51
@fclairamb
fclairamb merged commit b6a80e2 into main Sep 3, 2026
12 checks passed
@fclairamb
fclairamb deleted the batch/2026-09-02 branch September 3, 2026 08:24
@fclairamb fclairamb mentioned this pull request Sep 3, 2026
fclairamb added a commit that referenced this pull request Sep 3, 2026
🤖 I have created a release *beep* *boop*
---


##
[0.22.1](v0.22.0...v0.22.1)
(2026-09-03)


### Bug Fixes

* anchor heartbeat evaluation on real beats and close stale status-page
incidents ([#309](#309))
([b6a80e2](b6a80e2))
* **deps:** update dependency @simplewebauthn/browser to v14
([#308](#308))
([3b11351](3b11351))
* **deps:** update dependency docusaurus-plugin-llms to ^0.6.0
([#303](#303))
([8be59d5](8be59d5))
* **deps:** update go dependencies (non-major)
([#306](#306))
([91084ca](91084ca))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This was referenced Sep 8, 2026
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