Skip to content

autoRestart destroys the plugin and never re-initialises it, then reports it recovering — and, once successThreshold binds, healthy #12032

Description

@os-warren

Blocked-by: #11825

Found while implementing #11955 (making successThreshold bind from every status that
records a failure). Outside that card's surface — it is fenced to the success branch's
status gate — so this is filed rather than fixed.

What was measured

packages/core/src/health-monitor.ts at b797b30566, in attemptRestart:

try{// Call destroy and init to restartif(plugin.destroy){awaitplugin.destroy();}// Note: Full restart would require kernel context// This is a simplified version - actual implementation would need kernel integrationthis.logger.info('Plugin restarted',{plugin: pluginName});// Reset counters on successful restartthis.failureCounters.set(pluginName,0);this.successCounters.set(pluginName,0);this.healthStatus.set(pluginName,'recovering');}catch(error){}

The comment above the call says "Call destroy and init to restart". Only destroy() is
called. init() is never called, and nothing else in the file calls it — init appears in
health-monitor.ts exclusively inside that comment. So the sequence a plugin actually gets
is: destroy, then a log line reading Plugin restarted, then status recovering, then
periodic health checks continue to run against the destroyed instance.

Declared contract

packages/spec/src/kernel/plugin-lifecycle-advanced.zod.ts:

autoRestart: z.boolean().default(false).describe('Automatically restart plugin on health check failure'),

content/docs/references/kernel/plugin-lifecycle-advanced.mdx:114 repeats it verbatim.
Neither says the restart is destroy-only, and the operator-visible signals all say the
opposite: the log line asserts Plugin restarted, restartAttempts is incremented as
though a restart occurred, and maxRestartAttempts / restartBackoff schedule further
"restarts" of a plugin that was never brought back up.

Why the severity moved

Before #11955, a restarted plugin sat at recovering and reached healthy on its next
successful check. After #11955 it reaches healthy after successThreshold consecutive
successful checks. Either way the terminal report for a destroyed, never-re-initialised
plugin is healthy — but the second shape is more convincing, because the plugin now has
to pass a declared number of consecutive checks to get there. The default health check when
no checkMethod resolves is { name: 'plugin-loaded', status: 'passed' }, which a
destroyed plugin passes indefinitely.

What is NOT claimed

Whether the right resolution is to implement a real restart, to stop claiming one, or to
retire the surface is not determined here, and it is downstream of #11825 — which records
that nothing reads .health and that the kernel never constructs PluginHealthMonitor at
all. The kernel context this code says it needs is exactly what #11825 is about, so this
should be triaged after that card settles: if the surface is retired, this is moot; if it
is wired up, the missing init() becomes implementable for the first time.

Back-links: #11955 · #11825 · PR #12031.

Generated by Claude Code

Metadata

Metadata

Assignees

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions