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

  1. Make deactivate handlers fast, async, and non-blocking; offload heavy cleanup to a separate background path.
  2. Raise totalBudgetMs in LifecycleCapabilityHost options if legitimate cleanup needs more.
  3. Reduce the number of registered deactivate handlers; consolidate.
  4. 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

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


AI-assisted analysis of agalwood/Motrix@1a708ee577 (2026-08-12). Data as JSON: /api/errors/21b453599cd2b0ba. Report an issue: GitHub.