Skip to content

[Bug]: opencode-plugin 在 OpenCode 2.2.6+ 上自动注入静默失效(context hook 取不到 message.metadata) #7423

Closed

Description

@AtaraxiaZ

现象

OpenCode 2.2.6 起,本插件的自动注入在模型侧完全失效,但链路前半段一切正常,也没有任何报错。

实测(opencode 2.2.7 + @openviking/opencode-plugin 2026.11.10,本地 OpenViking 服务正常):

环节 结果
prompt hook 查询 OpenViking 正常,日志有 Injected OpenViking session context、recall_context_assembled
注入块写入 event.metadata 正常,且宿主把 metadata 持久化到了 session 消息里
context hook 读 message.metadata.openviking 恒为 undefined,静默跳过
模型是否看到 <openviking-context> 看不到(新会话直接问模型,回答 CANARY=no)

影响:session-start 的 profile 注入和每轮的 auto-recall 全部失效,模型不再"默认访问"OpenViking,只能靠自己想起来调 MCP 工具。对下游用户表现为"记忆突然不管用了",且没有任何提示。

根因

上游 anomalyco/opencode#61018("refactor(ai): remove unused metadata fields",2026-11-02 合并,2.2.6 首次发版)删掉了 @opencode/ai 里 Message.metadata,并在 packages/core/src/session/runner/to-llm-message.ts 里不再把 session metadata 带到 hook 消息上:

     case "user":
       return [Message.make({
           id: message.id,
           role: "user",
           content,
-          metadata: {
-            ...message.metadata,
-            ...(message.agents?.length ? { agents: message.agents } : {}),
-          },
       })]

lib/v2-plugin.mjs 的 injectV2Context 恰好依赖这一字段:

const blocks = message?.metadata?.[METADATA_KEY]?.context
if (message?.role !== "user" || !Array.isArray(blocks) || blocks.length === 0) continue

所以 prepareV2Prompt 写进去的 event.metadata[openviking] 永远读不回来。

注意这不是"数据丢了":metadata 仍然存在于存储层,ctx.session.context() 返回的存储消息里 metadata.openviking.context 完好。只是 hook 收到的消息不带它。

建议修复

不建议只把 injectV2Context 改成 async 完事,需要两条路配合(本地验证过的改法):

  1. prepareV2Prompt 写 event.metadata 的同时,按 messageID 在进程内存里留一份(有界 Map)。
  2. context hook 优先查内存;未命中(插件重启、resume、fork)时,每个 session 最多一次调用 ctx.session.context({ sessionID }),把存储消息里的 metadata.openviking.context 灌进同一张 Map,再注入。

这样每轮不额外请求,注入文本也可重复(prompt cache 稳定)。

示意:

export async function injectV2Context(event, repoContext, { getContext } = {}) {
  const repoPrompt = repoContext.getRepoSystemPrompt()
  if (repoPrompt) pushSystem(event, repoPrompt)
  if (!Array.isArray(event?.messages)) return

  const pending = event.messages.filter((m) =>
    m?.role === "user" && typeof m?.id === "string" && !hasInjectedContext(m))
  if (pending.length === 0) return
  if (pending.some((m) => !promptContexts.has(m.id))) await hydratePromptContexts(event, getContext)

  for (const message of pending) {
    const blocks = promptContexts.get(message.id) ?? message?.metadata?.[METADATA_KEY]?.context
    if (!Array.isArray(blocks) || blocks.length === 0) continue
    message.content.unshift({ type: "text", text: blocks.filter(Boolean).join("\n\n"),
      metadata: { [METADATA_KEY]: true } })
  }
}

同时建议保留对 message.metadata 的读取作为第一条分支,兼容 2.2.6 之前的 OpenCode。

验证

本地打了上述补丁后,用两个 canary 会话实测:

  • 新会话问"你的上下文里是否有一个以 <openviking-context 开头的块",模型回答 CANARY=yes 并引出 viking://user/default/memories/profile.md;
  • 第二轮问是否能额外看到召回块,回答 RECALL=yes 并引出一条记忆 URI;
  • 插件重载后(内存清空)再次对话,日志出现 Hydrated prompt contexts from stored session { messages: 9, injectable: 3 },说明回读路径可用。

node --test tests/*.test.mjs 57 项全过,新增了一项覆盖"hook 消息无 metadata"的场景。

关联

上游 issue(英文,已附最小复现插件):anomalyco/opencode#63252

顺带一提,本仓库 #6736 反映的"OpenCode 2.x 双实例加载"在本地也一直存在(每次初始化会打印两条 Registered OpenViking MCP server,dispose 时对全部会话做一次 flushAll),与本 issue 无关,但会放大 metadata/state 的时序问题。

Activity

  1. r266-tech commented on Nov 11, 2026

    @r266-tech
    Contributor

    There is a useful overlap with the existing v2 latency PR #5496, but its current implementation does not cover the stored-message fallback described here.

    I exercised the registered production prompt/context hooks with metadata-free { id, role, content } messages, comparing current main 9879a78d01e7 with #5496 at ce66cadcb947:

    Case main #5496
    New prompt, then context without metadata No injected block Profile + recall injected
    Repeated model step for that message No injected block Same bytes replayed
    New plugin instance, same session and persisted recall ledger No injected block Same bytes replayed
    Message from before the ledger existed, context has no metadata but session.context() can return its stored metadata.openviking.context No injected block No injected block

    The last case makes zero session.context() calls on both heads. #5496's legacy branch still reads message.metadata.openviking.context directly, and its existing legacy regression supplies that metadata on the hook message. That fixture passes, but it does not exercise the post-2.2.6 message shape.

    So the stored-message hydration fallback remains necessary even if #5496's new-message/ledger path is adopted. In particular, a metadata-free old message with the blocks available only through ctx.session.context({ sessionID }) would make a useful migration/resume regression.

    The existing focused tests pass: main tests/v2-plugin.test.mjs 9/9; #5496's v2 plugin + recall files 16/16. AI-only local Node hook verification on macOS / Node 26.13.0, with deterministic recall and host-storage fixtures; no live OpenCode UI, provider request, or model call.

  2. AtaraxiaZ commented on Nov 13, 2026

    @AtaraxiaZ
    ContributorAuthor

    OpenCode那边修复了这个问题,是他们的锅,这条issue就不需要了,anomalyco/opencode#63252

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

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions