Skip to content

[BUG]: context hook messages drop metadata, so per-message plugin context never reaches the model #63252

Closed

Description

@AtaraxiaZ

Description

session.hook("context") receives messages that only carry { id, role, content }. Since #61018 ("refactor(ai): remove unused metadata fields", first released in 2.2.6) Message.metadata no longer exists on those objects, so a plugin cannot attach context to a specific stored user message any more.

That is the documented v2 pattern for per-message context injection: query on the prompt hook, store the result on event.metadata, then inject it before that same message on every model step. The host still persists metadata with the stored message and ctx.session.context() still returns it, but the context hook payload no longer has it, so the injection silently no-ops.

The asymmetry is the whole bug: the field is writable on prompt, readable through session.context(), and absent on the one payload where plugins need it.

Plugins

@openviking/opencode-plugin 2026.11.10 (session-start profile + per-prompt recall injected this way). Any v2 plugin using prompt -> context metadata is affected the same way.

OpenCode version

2.2.7. First release with the change: 2.2.6.

Steps to reproduce

Minimal plugin:

export default {
  id: "canary",
  async setup(ctx) {
    await ctx.session.hook("prompt", async (event) => {
      event.metadata = { ...event.metadata, canary: { context: ["CANARY-TOKEN"] } }
    })
    await ctx.session.hook("context", async (event) => {
      for (const message of event.messages) {
        const blocks = message.metadata?.canary?.context
        console.log("hook metadata?", Boolean(blocks))
        if (message.role === "user" && blocks) {
          message.content.unshift({ type: "text", text: blocks.join() })
        }
      }
    })
  },
  async server() { return {} },
}
  1. Load the plugin and ask the model to repeat CANARY-TOKEN if it can see it.
  2. hook metadata? false on every step, and the model never sees the token.
  3. ctx.session.context({ sessionID }) for the same session returns that message with metadata.canary intact, so the data exists and is reachable, just not from the hook.

Expected

Keep metadata on the messages handed to context (a read-only pass-through is enough), or expose another supported way for a plugin to address stored message metadata from the hook. Writing on prompt and reading back through a full session.context() fetch is a costly substitute, and plugging that hole is what every affected plugin now has to reimplement.

Actual

Silently dropped. No error, no warning, no deprecation notice.

Extra context

  • refactor(ai): remove unused metadata fields #61018 documents this as a breaking change for plugins, but only in the PR body; the 2.2.6 and 2.2.7 release notes do not mention it, so downstream plugins had no signal to update.
  • I am not asking to restore @opencode/ai's Message.metadata; the request is about what the hook payload carries.
  • Local workaround used by the plugin: cache the blocks by message id in process memory, and fall back to ctx.session.context() once per session when a message id is unknown (restart/resume/fork). It works, but the storage round trip is exactly what the metadata field used to avoid.

System

  • OpenCode 2.2.7, macOS (Apple Silicon), Ghostty/iTerm2, plugin API v2 only (no v1 hooks in play).

Activity

  1. AtaraxiaZ commented on Nov 11, 2026

    @AtaraxiaZ
    Author

    Downstream report with the consumer side and a verified local workaround: volcengine/OpenViking#7423

    Two data points from that thread that may help triage:

    • The same metadata is still returned by ctx.session.context({ sessionID }). Confirmed on 2.2.7: after a plugin reload (so any in-memory cache is gone), the stored messages came back with metadata.openviking.context intact, while the context hook payload for the same messages had none.
    • Affected plugins can only recover it by fetching the whole session context once per session and rebuilding the mapping themselves. That works, but it means the hook is the only place the data is missing.

    If passing metadata through on hook messages is unwanted, a documented alternative for "which stored message is this" would be enough; the plugin only needs the association, not the host's own bookkeeping.

  2. opencodejr commented on Nov 11, 2026

    @opencodejr
    Contributor

    Thanks for the detailed report and the canary plugin, it makes the gap easy to see.

    You're right about the cause. #61018 (first released in 2.2.6) removed metadata from @opencode/ai messages and stopped core from copying the stored message's metadata onto the messages the context hook receives. That was a deliberate API change, listed as breaking for plugins in the PR. The plugin docs only describe event.metadata on the prompt hook as admission metadata and don't say it reaches context hook messages. So I'm treating this as a request for the plugin API rather than a regression.

    The request: give context hook messages a supported, read-only way to reach the stored message's metadata, so a plugin can tie context to a specific user message without a full ctx.session.context() round trip. That could be a pass-through field or a per-message-id map on the hook event. Maintainers will consider it; I can't promise an outcome.

    Until then, the workaround you describe (cache by message id, and fall back to ctx.session.context() once per session) is the supported route.

    Related, though not the same request:

  3. rekram1-node commented on Nov 12, 2026

    @rekram1-node
    Collaborator

    This was a field that was meant to be purely internal... didn't think anyone depended on it, bad call on my part. Apologies

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions