现象
opencode 2.x TUI 会话正文里能看到原始 render tag 被渲染出来,例如 ``。这些是压缩协议的消息引用元数据,不是会话内容。
根因链(已取证)
请求侧注入 :acp-kernel 的 renderMessage(dist/index.js:1704-1720)在把会话历史发给模型前,给消息文本前插一行 ``——renderTags: "text-only" 策略下 text 消息逐条插。server.ts 每轮 `processTurn` 后重建请求体(`src/server.ts:1853/1980/2159`)原样带标签上发。
模型回显 :标签进入模型上下文后,模型会在输出里照抄(本会话 bili.log 当日 212 条 [tag-echo] stripped model-emitted render tag 记录为证)。
响应侧 strip 是止血不是根治 :tag-echo-filter.ts(偶尔重复输出 <acp tokens="177" type="text">m00155</acp> #206 /[bug] #644 follow-up (post-#646, v0.1.97): typo'd render-tag echo (<acip …) bypasses the strip regex and passes through as a 43-char degenerate end_turn turn — agent stalls mid-orchestration until manual nudge #673 以来的累计修复)在输出流上剥标签,只覆盖 text/content/reasoning_content/reasoning/thinking 字段。凡模型把标签写进工具调用参数、代码块内文本、或命名错误变体 等形态,strip 拦不住(#361 已承认 tool-call XML 只 warn 不剥;#673 已承认 typo 变体漏网)。
可见性 :opencode 的 TUI 渲染的是会话消息的原文。注入发生在请求侧消息文本里,模型回显后若 strip 漏掉,该文本即进入会话存储并被原样渲染——用户看到的就是这个。
为什么这是架构问题而不是单个 bug
renderMessage 的注入策略把「协议元数据」写进了「消息正文」。协议元数据应该只存在于瞬态请求载荷(wire payload),一旦它以文本形式进入消息正文,就有三条暴露路径:模型回显、工具调用参数回带、decompress/压缩块摘要回灌。响应侧 strip 需要穷举所有回显形态才能堵住,而 #206/#361/#468/#673/#717 的迭代史证明穷举不完。
建议
主修复 :把 render tag 从「消息正文前缀」挪出消息文本——例如改用 host 不可见的元数据通道(header/单独字段),或注入只作用于瞬时序列化视图、绝不落回 host 会话存储。
次选 :若必须保留文本注入,给用户一个完全关闭注入的开关(现已有 ACP_RENDER_NONE / renderTags: "none",但默认值仍是 text-only——建议把 opencode 插件模式默认改为 none,因为 opencode 插件自己有 sessionID 绑定能力,模型端不需要靠正文标签识别消息)。
止损 :文档明示 ACP_RENDER_NONE=1 可关闭。
复现
长会话 + 开启压缩(renderTags 默认 text-only)
任意一次压缩后,模型回显历史中的标签
观察 bili.log [tag-echo] stripped 之外仍有漏网标签进入 TUI 正文
环境
opencode 2.0.3(Bun 编译二进制)
billion-context feat/pi-native-plugin 分支(本地构建)
插件模式部署(plugins/billion-context + 代理自持)
现象
opencode 2.x TUI 会话正文里能看到原始 render tag 被渲染出来,例如 ``。这些是压缩协议的消息引用元数据,不是会话内容。
根因链(已取证)
acp-kernel的renderMessage(dist/index.js:1704-1720)在把会话历史发给模型前,给消息文本前插一行 ``——renderTags: "text-only"策略下 text 消息逐条插。server.ts 每轮 `processTurn` 后重建请求体(`src/server.ts:1853/1980/2159`)原样带标签上发。[tag-echo] stripped model-emitted render tag记录为证)。tag-echo-filter.ts(偶尔重复输出 <acp tokens="177" type="text">m00155</acp> #206/[bug] #644 follow-up (post-#646, v0.1.97): typo'd render-tag echo (<acip …) bypasses the strip regex and passes through as a 43-char degenerate end_turn turn — agent stalls mid-orchestration until manual nudge #673 以来的累计修复)在输出流上剥标签,只覆盖 text/content/reasoning_content/reasoning/thinking 字段。凡模型把标签写进工具调用参数、代码块内文本、或命名错误变体等形态,strip 拦不住(#361已承认 tool-call XML 只 warn 不剥;#673已承认 typo 变体漏网)。为什么这是架构问题而不是单个 bug
renderMessage的注入策略把「协议元数据」写进了「消息正文」。协议元数据应该只存在于瞬态请求载荷(wire payload),一旦它以文本形式进入消息正文,就有三条暴露路径:模型回显、工具调用参数回带、decompress/压缩块摘要回灌。响应侧 strip 需要穷举所有回显形态才能堵住,而#206/#361/#468/#673/#717的迭代史证明穷举不完。建议
ACP_RENDER_NONE/renderTags: "none",但默认值仍是 text-only——建议把 opencode 插件模式默认改为 none,因为 opencode 插件自己有 sessionID 绑定能力,模型端不需要靠正文标签识别消息)。ACP_RENDER_NONE=1可关闭。复现
[tag-echo] stripped之外仍有漏网标签进入 TUI 正文环境