dashboard(cli): reset persisted plugin state to stopped before autoload

Prior fix tried to flip started→installed but that transition is
rejected by PluginStore.updatePluginState — only stopped, error, and
the started→error path are allowed from started. The right reset
target is "stopped"; from there loadPlugin's stopped→started is
valid and the autoload completes cleanly.

Also reset error→stopped so a plugin that failed its previous load
(e.g. a transient missing dep that has since been fixed) gets one
more chance at startup instead of staying stuck in error forever.
Failures of the reset are warned, not thrown — autoload still gets
to run on whatever it can.
This commit is contained in:
Semih
2026-05-10 13:25:42 +00:00
parent 170710b136
commit d1233e6684

View File

@@ -1395,20 +1395,31 @@ export async function runDashboard(port: number, opts: { paused?: boolean; dev?:
// path that mirrors how runtime-providing plugins (paperclip etc.)
// expect to be available immediately.
//
// Reset persisted state for enabled plugins back to "installed" before
// loadAllPlugins runs. PluginStore.updatePluginState rejects no-op
// transitions ("started""started"), and the persisted state from
// the previous container generation is "started" for any plugin that
// had been enabled — without this reset, autoload errors on every
// restart for already-enabled plugins.
// Reset persisted state for enabled plugins so loadPlugin can transition
// them back to "started". The state machine in PluginStore is strict:
// started → stopped | error
// stopped → started | error
// installed → started | stopped | error
// error → installed | started | stopped
// Persisted state from a previous container generation is typically
// "started" (a healthy load that didn't get a clean shutdown), and
// started → started is rejected. Walk the registry and flip every
// enabled non-installed plugin to "stopped"; loadPlugin then drives
// it forward to "started" cleanly.
try {
const persisted = await pluginStore.listPlugins();
for (const p of persisted) {
if (p.enabled && p.state === "started") {
if (!p.enabled) continue;
if (p.state === "started" || p.state === "error") {
try {
await pluginStore.updatePluginState(p.id, "installed");
} catch {
// Best effort — loader will surface the real failure if any.
await pluginStore.updatePluginState(p.id, "stopped");
} catch (resetErr) {
logSink.warn(
`Could not reset plugin state for ${p.id}: ${
resetErr instanceof Error ? resetErr.message : String(resetErr)
}`,
"plugin-loader",
);
}
}
}