agalwood/Motrix · error · PluginCodedError

plugin.metadata.not_available_outside_hook

plugin.metadata.not_available_outside_hook

Error message

metadata is only available inside a hook invocation; no task context is active

What it means

Thrown by dispatchMetadata when this.currentTaskId is falsy. metadata is scoped to the current task and is only available while a hook invocation is active (the host sets currentTaskId via BridgeHookEnter). Calling metadata.* outside a hook throws plugin.metadata.not_available_outside_hook.

Source

Thrown at src/core/plugin/host/capability-bridge.ts:849

          ? (args.data[0] as string | undefined)
          : undefined
        return host.keys(pluginId, prefix)
      }
      default:
        throw new PluginCodedError(
          'plugin.capability.unavailable',
          `unknown storage method: ${msg.method}`
        )
    }
  }

  // -------------------------------------------------------------------------
  // metadata — requires hook context (taskId). Task 19 throws when none.
  // -------------------------------------------------------------------------

  private async dispatchMetadata(msg: BridgeCallMessage): Promise<unknown> {
    if (!this.currentTaskId) {
      throw new PluginCodedError(
        'plugin.metadata.not_available_outside_hook',
        'metadata is only available inside a hook invocation; no task context is active'
      )
    }
    const host = this.opts.capabilityHost.metadata
    const pluginId = this.opts.pluginId
    const taskId = this.currentTaskId
    switch (msg.method) {
      case 'get': {
        const [key] = metadataGetSchema.parse(msg.args)
        return host.get(taskId, pluginId, key)
      }
      case 'has': {
        const [key] = metadataGetSchema.parse(msg.args)
        return host.has(taskId, pluginId, key)
      }
      case 'getAll':
        return host.getAll(taskId, pluginId)

View on GitHub (pinned to 1a708ee577)

Solutions

  1. Call metadata.* only inside beforeCreate/beforeFinalize hook handlers where the task context is active.
  2. For cross-task plugin state (not bound to one task), use the storage KV capability instead.
  3. In tests, drive the bridge through setHookContext({taskId, fsTaskHost}) before invoking metadata.

Example fix

// before — onActivate
async function onActivate() { await metadata.get('config') }
// after — inside a hook
export function beforeCreate(ctx) { const cfg = await ctx.metadata.get('config') }
Defensive patterns

Strategy: type-guard

Validate before calling

// Track hook entry/exit in the plugin.
let inHook = false
export function beforeCreate(ctx) { inHook = true; try { /* ... */ } finally { inHook = false } }
function assertTaskContext() { if (!inHook) throw new Error('metadata requires a hook context') }

Type guard

function hasTaskContext(ctx: unknown): ctx is { metadata: object } {
  return !!ctx && typeof (ctx as any)?.metadata === 'object'
}

Try / catch

try { return await metadata.get(key) }
catch (e) {
  if (e instanceof Error && e.code === 'plugin.metadata.not_available_outside_hook') {
    // read from persistent storage instead, or defer to next hook
    return undefined
  }
  throw e
}

Prevention

When it happens

Trigger: Plugin calls metadata.get/set/has/getAll/keys/delete from module top-level, onActivate, onStartup, or after the hook has returned. The guard at capability-bridge.ts:848-853 fires before the method switch.

Common situations: Plugin caches a metadata reference at activation and calls it lazily later; plugin reads metadata during initialize(); hook context not wired in a test harness.

Related errors


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