# `LifecycleDriver` _(no submodules)_ ## Purpose `LifecycleDriver` is the runtime executor for the per-plugin lifecycle FSM defined in `@suaveplan/api-lifecycle`. It owns one record per registered plugin (manifest hash, plugin module from `definePlugin`, schemaVersion, current FSM state, retry trackers, hook log) and translates the author-facing verbs `install` / `start` / `stop` / `uninstall` / `crash` / `retryInstall` / `retryStart` / `clearRollback` / `forceUninstall` / `repair` into the trigger sequences that the `createPluginFsm` interpreter applies against the legal-transition table. Each high-level operation dispatches the entry trigger, executes the side-effect descriptors captured on the FSM (the discriminated-union `SideEffectOp` from `@suaveplan/api-lifecycle/side-effects`), then dispatches the completion trigger. Failures along the way auto-dispatch the matching `*Failed` trigger and the FSM ends in the documented parked state (`install_failed`, `start_failed`, `uninstall_failed`). Auto-retry of `start_failed` follows the `@suaveplan/retry` exponential-backoff policy defined in `RETRY_POLICIES.startFailed`; crashed plugins auto-restart up to `RETRY_POLICIES.crashed.crashRestartMax` (default 3) with cooldown reset through `CrashBudgetTracker`. ## Features - Full coverage of every `(state, trigger)` edge in `@suaveplan/api-lifecycle`'s `PLUGIN_TRANSITIONS` table. - Per-plugin mutex enforcement inherited from `createPluginFsm` — concurrent triggers queue FIFO; tests assert this dedup. - `SideEffectOp` execution against pluggable adapter surfaces (`RouterAdapter`, `JobSchedulerAdapter`, `TopicSubscriberAdapter`, `HealthMarkerAdapter`, `LedgerAdapter`, `MigrationRunner`, `RbacAdapter`, `CapabilityInjector`, `InflightTracker`). - Emits `plugin.lifecycle_transitions_total{plugin, from, to, trigger, outcome}` on every transition via Chunk-A's `HostInstruments`. - Emits typed events onto the host `EventBus` — `plugin.installed`, `plugin.started`, `plugin.stopped`, `plugin.uninstalled`, `plugin.rolled_back`, `plugin.crashed`. - `snapshot(plugin)` renders an admin-friendly view with the FSM state, the most recent hook outcomes, and any captured `lastError`. - `rollback(plugin, cause)` is invoked by the `ReloadDriver` (see `../hot-reload/`) on a failed reload — drives `starting → rolled_back` without operator intervention. ## Usage: Basic ```ts import { EventBus } from "@suaveplan/events"; import { createHostInstruments, LifecycleDriver, } from "@suaveplan/api-host"; const events = new EventBus(); const instruments = createHostInstruments({ hostId: "h1", hostVersion: "0.1.0", }); const driver = new LifecycleDriver({ adapters: { router: { mount() {}, unmount() {} }, jobs: { register() {}, pause() {}, unpause() {} }, topics: { subscribe() {}, unsubscribe() {} }, health: { mark() {} }, ledger: { async write() {}, async drop() {}, async read() { return null; }, }, migrations: { async runUpTo() {} }, rbac: { registerScopes() {}, dropScopes() {} }, capabilities: { inject: () => ({}) as never, dispose: async () => {}, }, inflight: { async drain() { return 0; }, forceCancel: () => 0, }, }, events, instruments, }); driver.register({ module: pluginModule, manifest, manifestHash: "sha256:...", specifier: "file:///opt/plugin/dist/index.js?v=1", schemaVersion: 1, }); await driver.install(pluginModule.name); await driver.start(pluginModule.name); ``` ## Usage: Advanced Customise the per-plugin mutex (e.g. backed by a distributed lock) and the crash-restart cap: ```ts import { LifecycleDriver } from "@suaveplan/api-host/lifecycle"; import type { PluginMutex } from "@suaveplan/api-lifecycle"; class RedisPluginMutex implements PluginMutex { async acquire(plugin: string) { /* … */ } tryAcquire(plugin: string) { /* … */ } } const driver = new LifecycleDriver({ adapters, events, instruments, mutex: new RedisPluginMutex(), crashRestartMax: 5, sleep: () => Promise.resolve(), }); driver.events.on("plugin.rolled_back", ({ plugin, from, to }) => { operator.notify(`${plugin} rolled back ${from} → ${to}`); }); await driver.crash(plugin.name, new Error("worker exit code 137")); const snap = driver.snapshot(plugin.name); if (snap.lastError !== null) operator.escalate(snap); ``` ## API Reference - `class LifecycleDriver` - `constructor(options: LifecycleDriverOptions)` - `register(record: RegisteredPlugin): void` - `update(record: RegisteredPlugin): void` - `install(plugin: string): Promise` - `start(plugin: string): Promise` - `stop(plugin: string): Promise` - `uninstall(plugin: string): Promise` - `crash(plugin: string, cause: unknown): Promise` - `retryInstall(plugin: string): Promise` - `retryStart(plugin: string): Promise` - `clearRollback(plugin: string): Promise` - `forceUninstall(plugin: string): Promise` - `repair(plugin: string): Promise` - `rollback(plugin: string, cause: unknown): Promise` - `snapshot(plugin: string): LifecycleSnapshot` - `names(): readonly string[]` - `interface LifecycleDriverOptions` — `adapters`, `events`, `instruments`, `mutex?`, `crashRestartMax?`, `sleep?` - `interface LifecycleEventMap` — `plugin.installed`, `plugin.started`, `plugin.stopped`, `plugin.uninstalled`, `plugin.rolled_back`, `plugin.crashed`. - Adapter surfaces from `lifecycle.types.ts` — `RouterAdapter`, `JobSchedulerAdapter`, `TopicSubscriberAdapter`, `HealthMarkerAdapter`, `LedgerAdapter`, `MigrationRunner`, `RbacAdapter`, `CapabilityInjector`, `InflightTracker`, `LifecycleAdapters`. ## Implementation Notes The driver intentionally delegates illegal-transition detection to the underlying `PluginFsm` — every operator verb maps to one entry trigger; attempting `start()` from `crashed` therefore raises a `LifecycleError(ILLEGAL_TRANSITION)` from the FSM rather than a duplicate guard in the driver. This keeps the source-of-truth for legal transitions in The contract package. Side-effect ops `validate-manifest`, `resolve-deps`, `acquire-db-connection`, `complete-otel-span`, `apply-data-policy`, and `rollback-to-prior-module` are documented but execute as no-ops inside the driver: manifest validation ran at registration time, dep-graph resolution is computed by `api-manifest/dep-graph` before registration, the ledger adapter holds the DB connection, OTel span completion is captured via the lifecycle counter, the data policy is applied by the next `drop-ledger` op, and the rollback module switch is performed by the `ReloadDriver` before the `rolled_back` transition fires. Keeping the no-op branches lets Stage-3 wire concrete behaviour without revising the FSM definition. Auto-retry of `start_failed` uses `backoffDelay(base, factor, attempt, max)` locally rather than `@suaveplan/retry`'s `retry()` engine because the driver needs to inter-leave the FSM transitions between attempts — `@suaveplan/retry`'s engine is single-call-with-retries semantics. The underlying delay strategy is identical to `RETRY_POLICIES.startFailed.delay`.