agalwood/Motrix · critical · Error

plugin.lifecycle.activation_capability_violation

plugin.lifecycle.activation_capability_violation

Error message

effectful call ${capability}.${method} during activation

What it means

During module evaluation (currentPhase === 'activation') only registration-only calls are allowed: hooks.beforeCreate/beforeFinalize/afterComplete/onError, commands.register, and lifecycle.onActivate/onDeactivate. Any effectful call (http, fs.task, fs.storage, storage, notify, ffmpeg, crypto, config, metadata, commands.execute, i18n.t) is fatal — the worker sends a 'fatal' message and throws, killing the plugin. (log.* is an explicit exception and is permitted at top level.)

Source

Thrown at src/core/plugin/host/quick-js-worker.ts:94

const registeredDeactivateHandlers: QuickJSHandle[] = []

function send(msg: WorkerToHost): void {
  port.postMessage(msg)
}

function assertEffectfulAllowed(capability: string, method: string): void {
  if (currentPhase !== 'activation') return
  if (classify(capability, method) !== 'effectful') return
  violationFatal = {
    code: 'plugin.lifecycle.activation_capability_violation',
    message: `effectful call ${capability}.${method} during activation`,
  }
  send({
    type: 'fatal',
    code: violationFatal.code,
    message: violationFatal.message,
  })
  throw new Error(violationFatal.message)
}

async function callHost(
  capability: string,
  method: string,
  args: unknown[]
): Promise<unknown> {
  const id = nextCallId++
  return new Promise((resolve, reject) => {
    pendingCalls.set(id, (resp) => {
      if (resp.type !== 'response') return
      if (resp.ok) {
        resolve(resp.result)
      } else {
        const e: Error & { code?: string } = new Error(resp.error.message)
        e.code = resp.error.code
        reject(e)
      }

View on GitHub (pinned to 1a708ee577)

Solutions

  1. Move effectful work into onActivate or a hook/command handler; keep module top-level to registration only.
  2. If a value is needed at top-level, compute it lazily inside a hook instead.
  3. Use log.* (the permitted exception) only for diagnostics, not for state-changing work.

Example fix

// before — effectful call at module top level
const cfg = await config.get('key')
hooks.beforeCreate(async (ctx) => { /* uses cfg */ })
// after — defer into a hook
hooks.beforeCreate(async (ctx) => {
  const cfg = await config.get('key')
  /* ... */
})
Defensive patterns

Strategy: validation

Validate before calling

import { classify } from '../capabilities/classification'
// at plugin authoring/review time: assert top-level calls are registration-only
function assertTopLevelAllowed(capability: string, method: string): void {
  const c = classify(capability, method)
  if (c !== 'registration-only') {
    throw new Error(`${capability}.${method} is ${c}; move it out of module top level into onActivate or a hook`)
  }
}

Type guard

import { isRegistrationOnly } from '../capabilities/classification'
function isAllowedAtTopLevel(capability: string, method: string): boolean {
  return isRegistrationOnly(capability, method) || capability === 'log'
}

Try / catch

// this error is fatal and kills the plugin; there is no in-process recovery.
// wrap setup work so it never runs at module scope:
hooks.beforeCreate(async () => {
  try { /* effectful setup */ } catch (e) { /* handle inside the hook */ }
})

Prevention

When it happens

Trigger: A plugin calls notify.show, http.get, storage.set, ffmpeg.*, crypto.*, config.get, or i18n.t at top-level module scope rather than inside a registered hook/command handler or onActivate.

Common situations: Plugin runs setup work at import time; reads config during module init; opens a connection in module scope; calls i18n.t to build a top-level constant.

Related errors


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