From 38bfacdc021a623faf4ca3d8a2e54c4595af21fd Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 25 Aug 2026 23:46:55 +0000 Subject: [PATCH] docs(protocol): document the recovery half of the health-threshold machinery MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `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 Claude-Session: https://claude.ai/code/session_01UjM2ia8Av1v5NqfqQEQmC6 --- content/docs/protocol/kernel/lifecycle.mdx | 26 ++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/content/docs/protocol/kernel/lifecycle.mdx b/content/docs/protocol/kernel/lifecycle.mdx index 4c2840bd40..11aa2e447e 100644 --- a/content/docs/protocol/kernel/lifecycle.mdx +++ b/content/docs/protocol/kernel/lifecycle.mdx @@ -717,6 +717,32 @@ to `unhealthy` only once `failureThreshold` of them accumulate. A check that **throws** — including one that exceeds `timeout` — is the separate `failed` status, applied immediately with no threshold. +Recovery is the mirror of that half, and `successThreshold` is its counter: the +number of **consecutive** passing rounds a plugin needs before the monitor +reports it `healthy` again. The count is consulted from every status that +records an observed failure — `degraded`, `unhealthy`, `failed` and `recovering` +alike — and while it accumulates the plugin sits in `recovering`, which is +therefore a *reported* status and not merely a vocabulary entry. `healthy` and +`unknown` are the two statuses the count is **not** consulted from: neither +records a failure to recover from, so a passing round promotes straight to +`healthy`. `unknown` is what `registerPlugin` writes before any check has run, +which is why a freshly registered plugin reads `healthy` on its first passing +round however high `successThreshold` is declared. + +"Consecutive" is strict, and it is the failing round that enforces it: any +failure resets the success count to zero — both routes included — so a throw +part-way through a recovery starts the next attempt at one rather than resuming +where it left off. The symmetry holds the other way too: a passing round resets +the failure count, so `failureThreshold` likewise counts only an unbroken run. +A successful auto-restart lands the plugin in `recovering` with **both** +counters cleared, so a restarted plugin still owes a full `successThreshold` of +passing rounds before it reads `healthy`. + +At the default `successThreshold: 1` none of this is observable: the first +passing round satisfies the count from every status, and `recovering` is never +the status a check leaves behind. The distinction appears only once a config +declares a value above `1`. + The monitor keeps one report per plugin rather than one aggregate document. Each round of checks builds a `PluginHealthReport` (`@objectstack/spec/kernel`, constructed in `packages/core/src/health-monitor.ts`) and stores it under the