agalwood/Motrix · error · LifecycleError
plugin.lifecycle.deactivate_timeout
plugin.lifecycle.deactivate_timeout
Error message
plugin "${pluginId}" deactivate budget exhausted before all handlers ran What it means
Deactivate handlers for one plugin run sequentially and share a single total budget (default 2000ms). Before each handler, remaining = totalBudgetMs - elapsed is computed; once it drops to zero the next handler is never run and the error fires. Handlers are also individually raced (_raceWithTimeout) so a slow handler can consume the whole budget.
Source
Thrown at src/core/plugin/capabilities/lifecycle.ts:100
* sharing `totalBudgetMs`. Clears the handler list after the run (whether
* it succeeded or failed). Throws `LifecycleError` on timeout or handler
* throw.
*/
async runDeactivate(pluginId: string): Promise<void> {
const list = this.handlers.get(pluginId) ?? []
// Snapshot so dispose() calls during run don't mutate the in-flight list.
const snapshot = list.slice()
// Clear immediately so subsequent calls start fresh.
this.handlers.delete(pluginId)
const startMs = Date.now()
for (const handler of snapshot) {
const elapsed = Date.now() - startMs
const remaining = this.totalBudgetMs - elapsed
if (remaining <= 0) {
throw new LifecycleError(
'plugin.lifecycle.deactivate_timeout',
`plugin "${pluginId}" deactivate budget exhausted before all handlers ran`
)
}
await this._raceWithTimeout(pluginId, handler, remaining)
}
}
/**
* Clear all registered handlers for `pluginId` without running them.
* Called on full plugin removal / uninstall.
*/
reset(pluginId: string): void {
this.handlers.delete(pluginId)
}
/**View on GitHub (pinned to 1a708ee577)
Solutions
- Make deactivate handlers fast, async, and non-blocking; offload heavy cleanup to a separate background path.
- Raise totalBudgetMs in LifecycleCapabilityHost options if legitimate cleanup needs more.
- Reduce the number of registered deactivate handlers; consolidate.
- Audit handlers for accidental awaits on never-resolving promises.
Example fix
// before
host.onDeactivate('p', async () => { await fs.writeFileSync(...) ; await db.flush(); await db.close() })
// after
host.onDeactivate('p', async () => { db.close(); }) // fire-and-forget heavy flush to a background task Defensive patterns
Strategy: validation
Validate before calling
// profile handlers in dev before shipping
const t0 = Date.now()
await handler()
if (Date.now() - t0 > 200) console.warn('slow deactivate handler', handler.name) Try / catch
try {
await host.deactivate(pluginId)
} catch (e) {
if (e instanceof LifecycleError && e.code === 'plugin.lifecycle.deactivate_timeout') {
// schedule remaining cleanup off the deactivate path (background task)
} else throw e
} Prevention
- Keep deactivate handlers O(few ms): close handles, do not flush large buffers inline.
- Raise totalBudgetMs only if legitimate cleanup needs it; do not paper over a slow handler.
- Consolidate many small handlers into one fast batch teardown.
When it happens
Trigger: A deactivate handler does blocking I/O (sync fs, large writes); many handlers whose cumulative time exceeds 2000ms; a handler awaits a never-resolving promise that gets killed at the per-handler timeout, consuming most of the budget; cleanup logic that awaits external services.
Common situations: Plugin grew cleanup responsibilities over time; plugin closes DB connections or flushes buffers synchronously; third-party SDKs with slow teardown; CI machines slower than production so budget is tighter.
Related errors
- plugin.lifecycle.secrets_seed_missing
- plugin.http.timeout
- PluginActivationCapExceeded
- plugin.fs.task.not_available_outside_hook
- plugin.metadata.not_available_outside_hook
AI-assisted analysis of agalwood/Motrix@1a708ee577 (2026-08-12).
Data as JSON: /api/errors/21b453599cd2b0ba.
Report an issue: GitHub.