Repository navigation
fix: anchor heartbeat evaluation on real beats and close stale status-page incidents - #309
Merged
Merged
Conversation
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>
…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
enabled auto-merge (squash)
September 3, 2026 05:37
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
enabled auto-merge (squash)
September 3, 2026 06:51
Merged
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
Closed
Closed
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.
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_2is 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
overdueByof a few milliseconds and alastSignalAtpointing 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_untouchedauto-resolve default behaved exactly likenever. 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 thencone-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
results_raw_signal_idx, appended to this cycle's consolidated018_v0_22_2in both dialects. Without it a silent heartbeat's signal lookup would walk every evaluation row inside raw retention on every tick.GetLastResultForChecksis deliberately untouched — it still serves the API'slastResult, where "newest row of any origin" is the right answer.?stale=trueis added to the org-side status-page incident list, with astaleboolean on each item meaning "the linked incident is resolved"; theoutputfield's description now points at the new docs paragraph.OnIncidentReopenedreachable: 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.check-heartbeat-push,magic-wand,check-heartbeat-evaluation-rows,stale-publication-banner,tv-board) were verified with--listand 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.autoResolvepolicy norhumanTouchedAt. 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 toautoResolve: 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.acmepage 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 onautoCreated && !humanTouched, so an untouched hand-published entry now self-resolves without that notice.?stale=truewith noactive=truefilters 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 sendsactive=true.2026-09-03-01-getting-started-card-modern-redesign,2026-09-03-01-operator-notifications-support-messages-and-signups) arrived inspecs/todos/from another session while this batch was running and were deliberately left for a later batch.🤖 Generated with Claude Code