Skip to content

docs(protocol): document the recovery half of the health-threshold machinery - #12383

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-12033-lifecycle-recovery-half
Aug 26, 2026
Merged

docs(protocol): document the recovery half of the health-threshold machinery#12383
yinlianghui merged 1 commit into
mainfrom
claude/issue-12033-lifecycle-recovery-half

Conversation

@yinlianghui

@yinlianghuiyinlianghui commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

Fixes#12033

What this is

content/docs/protocol/kernel/lifecycle.mdx ("Custom Health Checks") documented the
failure half of PluginHealthMonitor in full and the recovery half not at all.
successThreshold appeared exactly once on the page — inside a code comment listing the
parsed defaults — and nothing said which statuses the count is consulted from, that
recovering is where a plugin sits while the count accumulates, or what a
never-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/main at 7986d973fa, all in packages/core/src/health-monitor.ts
unless 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.)

Claim on the pageSource
successThreshold is a recovery counter — consecutive passing rounds needed before the monitor reports healthyPluginHealthCheckSchema.successThreshold, packages/spec/src/kernel/plugin-lifecycle-advanced.zod.ts:57-59z.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"
The count is consulted from degraded, unhealthy, failed and recoveringRECOVERY_IS_THRESHOLD_GATED (:36-43) — a Record keyed by PluginHealthStatus with boolean values, in which those four map to true
healthy and unknown promote on the first passing roundthe same map — both false — consumed at :175, whose else branch is this.healthStatus.set(pluginName, 'healthy') (:183-185)
While the count accumulates the plugin sits in recovering:180-182 — the else of if (successCount >= config.successThreshold) writes this.healthStatus.set(pluginName, 'recovering')
…so recovering is a reported status, not just a vocabulary entrythe report is built with status: this.healthStatus.get(pluginName) (:216-217)
unknown is what registerPlugin writes before any check has run:70this.healthStatus.set(pluginName, 'unknown')
Any failing round resets the success count to zero, both routesrecordFailedRound (:248-256) — this.successCounters.set(pluginName, 0); both the returned-failure and thrown routes reach it through the single if (failureRoute) call site at :211-213
A passing round resets the failure count:169this.failureCounters.set(pluginName, 0)
A successful auto-restart lands the plugin in recovering with both counters clearedattemptRestart (:320-323) — both counters set to 0, then this.healthStatus.set(pluginName, 'recovering')
At the default successThreshold: 1 none of it is observable:177successCount >= config.successThreshold is satisfied by the first passing round from every status, so recovering is never the status a check leaves behind

The behaviour itself is pinned by packages/core/src/health-monitor.test.ts — its
describe block for successThreshold binding 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

  • The generated reference page content/docs/references/kernel/plugin-lifecycle-advanced.mdx
    is deliberately untouched — it is produced from the Zod .describe() text and can carry
    keys, never transitions.
  • No os:check markers were added to fences on this page — that question is its own card.
  • No new headings: the prose is added inside the existing "Custom Health Checks" section,
    which keeps this change clear of the docs-site epic's declared content/docs/** slice
    (frontmatter title/description and body headings).
  • No changeset — docs-only, releasing nothing, matching the precedent of the recent
    docs-only commits to this same page. skip-changeset is applied.

Verification

Gate union re-derived at the final commit with
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (the script reads
its 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.

1 EXIT=0 pnpm check:cross-package-test-inputs
2 EXIT=0 pnpm check:doc-anchors
3 EXIT=0 pnpm check:doc-authoring
4 EXIT=0 pnpm --filter @objectstack/lint run check:doc-formula-expressions
5 EXIT=0 pnpm --filter @objectstack/lint run check:doc-security-posture
6 EXIT=0 pnpm --filter @objectstack/spec run check:docs
7 EXIT=0 pnpm check:docs-audit-scope
8 EXIT=0 pnpm check:docs-redirects
9 EXIT=0 pnpm check:docs-single-h1
10 EXIT=0 pnpm --filter @objectstack/spec run check:empty-state
11 EXIT=0 pnpm --filter @objectstack/spec run check:liveness
12 EXIT=0 pnpm check:published-readme-links
13 EXIT=0 pnpm check:react-page-adapter-contract
14 EXIT=0 pnpm check:role-word
15 EXIT=0 pnpm --filter @objectstack/spec run check:skill-examples
16 EXIT=0 pnpm --filter @objectstack/spec run check:strictness-ledger
17 EXIT=0 pnpm --filter @objectstack/spec run check:variant-docs
18 EXIT=0 node scripts/check-ci-filter-parity.mjs
19 EXIT=0 node scripts/check-cross-package-test-inputs.mjs
20 EXIT=0 node scripts/check-doc-frontmatter.mjs
21 EXIT=0 node scripts/check-doc-route-spelling.mjs
22 EXIT=0 node scripts/check-docs-section-name.mjs
23 EXIT=0 node scripts/check-section-landing-index.mjs

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. After
turbo run build --filter=@objectstack/formula --filter=@objectstack/lint --filter=@objectstack/client-react all 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

…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_01UjM2ia8Av1v5NqfqQEQmC6
@github-actionsgithub-actionsBot added size/s documentation Improvements or additions to documentation labels Aug 25, 2026
@yinlianghuiyinlianghui added skip-changeset PR has no user-facing published change; bypasses the changeset gate and removed documentation Improvements or additions to documentation size/s labels Aug 25, 2026 — with Claude
@yinlianghui
yinlianghui marked this pull request as ready for review August 26, 2026 01:20
@yinlianghui
yinlianghui added this pull request to the merge queueAug 26, 2026
@yinlianghuiClaude

Copy link
Copy Markdown
CollaboratorAuthor

ACCEPT — devx@objectstack seat (#6023), R9

Lint & Repo Gates (id 98009258904) read by name: completed + success. Ready + auto-merge SQUASH.

Zone 1.2 required every recovery-side claim to be traceable to a named file and symbol. I checked all seven at origin/main rather than reading for plausibility

the doc's claimmeasured
successThreshold = consecutive passing rounds before healthyhealth-monitor.ts:171-177"Recover only once successThreshold consecutive successes have", successCount >= config.successThreshold
consulted from degraded / unhealthy / failed / recoveringRECOVERY_IS_THRESHOLD_GATED — all four true
not consulted from healthy / unknownboth 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: 1plugin-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

  • recovering is 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 failureCounters is stated in the same breath, so failureThreshold is 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: 1 none of this is observable, because the first passing round satisfies the count from every status and recovering is 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

Merged via the queue into main with commit 2a845daAug 26, 2026
44 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-12033-lifecycle-recovery-half branch August 26, 2026 01:42
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

skip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The kernel lifecycle protocol doc describes the failure half of the health threshold machinery and never the recovery half

2 participants

@yinlianghui@claude