{"record":{"id":"abeeb0bed28ad436","repo":"agalwood/Motrix","slug":"plugin-metadata-not-available-outside-hook","errorCode":"plugin.metadata.not_available_outside_hook","errorMessage":"metadata is only available inside a hook invocation; no task context is active","messagePattern":"metadata is only available inside a hook invocation; no task context is active","errorType":"exception","errorClass":"PluginCodedError","httpStatus":null,"severity":"error","filePath":"src/core/plugin/host/capability-bridge.ts","lineNumber":849,"sourceCode":"          ? (args.data[0] as string | undefined)\n          : undefined\n        return host.keys(pluginId, prefix)\n      }\n      default:\n        throw new PluginCodedError(\n          'plugin.capability.unavailable',\n          `unknown storage method: ${msg.method}`\n        )\n    }\n  }\n\n  // -------------------------------------------------------------------------\n  // metadata — requires hook context (taskId). Task 19 throws when none.\n  // -------------------------------------------------------------------------\n\n  private async dispatchMetadata(msg: BridgeCallMessage): Promise<unknown> {\n    if (!this.currentTaskId) {\n      throw new PluginCodedError(\n        'plugin.metadata.not_available_outside_hook',\n        'metadata is only available inside a hook invocation; no task context is active'\n      )\n    }\n    const host = this.opts.capabilityHost.metadata\n    const pluginId = this.opts.pluginId\n    const taskId = this.currentTaskId\n    switch (msg.method) {\n      case 'get': {\n        const [key] = metadataGetSchema.parse(msg.args)\n        return host.get(taskId, pluginId, key)\n      }\n      case 'has': {\n        const [key] = metadataGetSchema.parse(msg.args)\n        return host.has(taskId, pluginId, key)\n      }\n      case 'getAll':\n        return host.getAll(taskId, pluginId)","sourceCodeStart":831,"sourceCodeEnd":867,"githubUrl":"https://github.com/agalwood/Motrix/blob/1a708ee57746c434e2c67a44bbf0906a976afea4/src/core/plugin/host/capability-bridge.ts#L831-L867","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Call metadata.* only inside beforeCreate/beforeFinalize hook handlers where the task context is active.","For cross-task plugin state (not bound to one task), use the storage KV capability instead.","In tests, drive the bridge through setHookContext({taskId, fsTaskHost}) before invoking metadata."],"exampleFix":"// before — onActivate\nasync function onActivate() { await metadata.get('config') }\n// after — inside a hook\nexport function beforeCreate(ctx) { const cfg = await ctx.metadata.get('config') }","handlingStrategy":"type-guard","validationCode":"// Track hook entry/exit in the plugin.\nlet inHook = false\nexport function beforeCreate(ctx) { inHook = true; try { /* ... */ } finally { inHook = false } }\nfunction assertTaskContext() { if (!inHook) throw new Error('metadata requires a hook context') }","typeGuard":"function hasTaskContext(ctx: unknown): ctx is { metadata: object } {\n  return !!ctx && typeof (ctx as any)?.metadata === 'object'\n}","tryCatchPattern":"try { return await metadata.get(key) }\ncatch (e) {\n  if (e instanceof Error && e.code === 'plugin.metadata.not_available_outside_hook') {\n    // read from persistent storage instead, or defer to next hook\n    return undefined\n  }\n  throw e\n}","preventionTips":["Use storage (KV) for cross-task plugin state; reserve metadata for task-bound data inside hooks.","Do not capture metadata at activation time for later use.","In tests, setHookContext({taskId, fsTaskHost}) before exercising metadata."],"tags":["plugin","capability","metadata","hook-context","lifecycle"],"backgroundTag":null,"analyzedSha":"1a708ee57746c434e2c67a44bbf0906a976afea4","analyzedAt":"2026-08-12T16:18:09.346Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}