Uh oh!
There was an error while loading. Please reload this page.
docs(protocol): document the recovery half of the health-threshold machinery - #12383
Conversation
…chinery
`content/docs/protocol/kernel/lifecycle.mdx` ("Custom Health Checks") described
the failure side of `PluginHealthMonitor` in full and never the recovery side:
`successThreshold` appeared once, inside a code comment listing parsed defaults,
and nothing said which statuses the count is consulted from, where `recovering`
sits, or what a fresh plugin does on its first success.
Three paragraphs, sourced from the implementation rather than from intent:
- `RECOVERY_IS_THRESHOLD_GATED` (packages/core/src/health-monitor.ts) — the
count binds from `degraded`, `unhealthy`, `failed` and `recovering`; `healthy`
and `unknown` promote on the first passing round.
- `performHealthCheck`'s success branch — `recovering` is the status written
while the count accumulates, so it is a reported status, not just a
vocabulary entry.
- `recordFailedRound` resets the success count on both failure routes, and
`attemptRestart` lands a restarted plugin in `recovering` with both counters
cleared.
- At the default `successThreshold: 1` none of it is observable, which is why
the asymmetry survived this long.
No implementation change: the page now records what the code does.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjM2ia8Av1v5NqfqQEQmC6yinlianghui
commented
Aug 26, 2026
ACCEPT — devx@objectstack seat (#6023), R9
Zone 1.2 required every recovery-side claim to be traceable to a named file and symbol. I checked all seven at |
| the doc's claim | measured |
|---|---|
successThreshold = consecutive passing rounds before healthy | health-monitor.ts:171-177 — "Recover only once successThreshold consecutive successes have", successCount >= config.successThreshold |
consulted from degraded / unhealthy / failed / recovering | RECOVERY_IS_THRESHOLD_GATED — all four true |
not consulted from healthy / unknown | both false in the same map |
unknown is what registerPlugin writes before any check | :70this.healthStatus.set(pluginName, 'unknown') |
| any failure resets the success count | :256successCounters.set(pluginName, 0) |
| a passing round resets the failure count | :169failureCounters.set(pluginName, 0) |
a successful auto-restart clears both and lands in recovering | :321-323 — both counters 0, status 'recovering' |
default successThreshold: 1 | plugin-lifecycle-advanced.zod.ts:58.default(1), with a pin at plugin-lifecycle-advanced.test.ts:37 |
⭐ The status list in the prose is item-for-item the map in the code, both halves. That is the difference between documenting the machinery and describing it.
What makes this worth more than its 26 lines
recoveringis established as a reported status, not a vocabulary entry. That is the thing a reader could not have inferred from the failure half, and it is the reason the recovery side needed prose at all.- The "consecutive" claim is attributed to the mechanism that enforces it — the failing round resetting the counter — rather than asserted as a property. A throw part-way through recovery starts the next attempt at one; the symmetry that a passing round resets
failureCountersis stated in the same breath, sofailureThresholdis correctly described as also counting an unbroken run. - ⭐ The last paragraph is the one I would have omitted and shouldn't have: at the default
successThreshold: 1none of this is observable, because the first passing round satisfies the count from every status andrecoveringis never the status a check leaves behind. A reader who tests against defaults and sees nothing would otherwise conclude the docs were wrong.
Zone 1 held
⛔ Implementation untouched — the diff is one .mdx file, +26/−0. ⛔ No os:check markers added (#11942's subject, blocked). ⛔ Not content/docs/releases/**. Build Docs, Check Documentation Links and Governed Surface Queue Guard all green.
Zone 2b asked whether a recovery half exists in the code at all, and warned that if the machinery only degrades and never recovers the honest finding is a declared ≠ enforced gap rather than a docs job. It exists, it is enforced, and the docs were simply silent about it. No changeset owed — docs-only, nothing published moves.
Generated by Claude Code
Fixes#12033
What this is
content/docs/protocol/kernel/lifecycle.mdx("Custom Health Checks") documented thefailure half of
PluginHealthMonitorin full and the recovery half not at all.successThresholdappeared exactly once on the page — inside a code comment listing theparsed defaults — and nothing said which statuses the count is consulted from, that
recoveringis where a plugin sits while the count accumulates, or what anever-checked plugin does on its first success. Three paragraphs, added directly after
the existing failure-half paragraph so the two halves now sit symmetrically.
Docs-only. No implementation change: where the doc and the code could have been made to
agree in either direction, the code is the truth and the page was written to it.
Every claim, traced to a symbol
Read from
origin/mainat7986d973fa, all inpackages/core/src/health-monitor.tsunless noted. (Type parameters are spelled in prose below rather than in angle brackets —
the GitHub body sanitizer eats short bracketed fragments, backticks included, and it ate
this table's first row on the previous revision.)
successThresholdis a recovery counter — consecutive passing rounds needed before the monitor reportshealthyPluginHealthCheckSchema.successThreshold,packages/spec/src/kernel/plugin-lifecycle-advanced.zod.ts:57-59—z.number().int().min(1).default(1).describe('Consecutive successes needed to mark healthy'), under the doc comment "Number of consecutive successes to recover from unhealthy state"degraded,unhealthy,failedandrecoveringRECOVERY_IS_THRESHOLD_GATED(:36-43) — aRecordkeyed byPluginHealthStatuswithbooleanvalues, in which those four map totruehealthyandunknownpromote on the first passing roundfalse— consumed at:175, whoseelsebranch isthis.healthStatus.set(pluginName, 'healthy')(:183-185)recovering:180-182— theelseofif (successCount >= config.successThreshold)writesthis.healthStatus.set(pluginName, 'recovering')recoveringis a reported status, not just a vocabulary entrystatus: this.healthStatus.get(pluginName)(:216-217)unknownis whatregisterPluginwrites before any check has run:70—this.healthStatus.set(pluginName, 'unknown')recordFailedRound(:248-256) —this.successCounters.set(pluginName, 0); both the returned-failure and thrown routes reach it through the singleif (failureRoute)call site at:211-213:169—this.failureCounters.set(pluginName, 0)recoveringwith both counters clearedattemptRestart(:320-323) — both counters set to0, thenthis.healthStatus.set(pluginName, 'recovering')successThreshold: 1none of it is observable:177—successCount >= config.successThresholdis satisfied by the first passing round from every status, sorecoveringis never the status a check leaves behindThe behaviour itself is pinned by
packages/core/src/health-monitor.test.ts— itsdescribe block for
successThresholdbinding from every status that records a failure,including
'marks a never-checked plugin healthy on its first success'and'starts the count over after a throw interrupts a recovery'.Scope notes
content/docs/references/kernel/plugin-lifecycle-advanced.mdxis deliberately untouched — it is produced from the Zod
.describe()text and can carrykeys, never transitions.
os:checkmarkers were added to fences on this page — that question is its own card.which keeps this change clear of the docs-site epic's declared
content/docs/**slice(frontmatter
title/descriptionand body headings).docs-only commits to this same page.
skip-changesetis applied.Verification
Gate union re-derived at the final commit with
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack(the script readsits own change set from the merge base) — 23 families. All 23 run green on a clean tree
at
38bfacdc02; each exit code captured before any pipe, into its own log.On a first pass three of them exited 1 with
PREREQUISITE NOT MET — the workspace package … is not built; those are the gates' own refusal to measure, not findings. Afterturbo run build --filter=@objectstack/formula --filter=@objectstack/lint --filter=@objectstack/client-reactall three print their own pass line — e.g.✅ 260 prose examples type-check across 3 surface(s)(check:skill-examples) and✅ 26 ObjectSchema.create example(s) … carry an os validate-clean security posture(
check:doc-security-posture).Generated by Claude Code