一个小字符串是怎么打爆 OMP 自动压缩的

太长不看

OMP 是一个 AI 编程助手。你跟它聊得越久,对话越长。AI 一次能处理的文字有上限,所以到了一定长度,OMP 会自动把旧对话压缩成一段摘要,继续聊。

压缩的过程是让 AI 重新读一遍旧对话,写个总结。问题出在 OMP 怎么排版旧对话:它用了一种叫 Harmony 的格式来序列化,其中 AI 之前的思考内容被包上了 <|channel|>analysis 这样的标记。这个标记是 OMP 自己加的格式,不是 AI 写的,也不是对话里本来就有的。

GitHub Copilot 的 gpt-5.6 系列模型拒绝处理包含这个标记的请求。为什么只有 gpt-5.6 系列拦、其他模型不拦,没有从官方得到确认。从行为看,它可能把这个标记识别为不该出现在用户输入里的内部协议符号。哪怕请求里只有这一个字符串、什么都没有,也会被拒。

结果就是压缩失败,会话卡住,用户得手动换模型或清空历史才能继续。

十几个字符的字符串,直接让 OMP compaction 400 了。

字符串是这个:

1
<|channel|>analysis

场景是 OMP 的自动压缩。会话太长以后,OMP 会把历史对话压成一份摘要,继续塞回上下文里。正常情况下,这件事不该惊动用户。结果这次直接炸了:

1
2
Compaction failed: Summarization failed: 400
{"error":{"message":"Request blocked.","code":"invalid_request_body"}}

一开始看起来像凭证问题。再看 raw request,味道不对。

先定义问题是啥

不是模型不会回答,也不是网络问题。

OMP 在做 compaction summarization 的时候,把历史 transcript 当普通 input_text 发给 Copilot Responses API,里面出现了 Harmony 的 analysis channel marker:

1
<|channel|>analysis

Copilot 的 gpt-5.6-* 三个模型直接拒绝。

要拆三个问题:最小 payload 能不能单独触发?是不是某个环节加了料?这个字符串从哪来的?

最小 payload

1
2
3
4
5
{
"model": "gpt-5.6-terra",
"input": "<|channel|>analysis",
"stream": false
}

直接发请求,返回 400:

1
2
3
4
5
6
{
"error": {
"message": "Request blocked.",
"code": "invalid_request_body"
}
}

不需要完整 transcript,不需要上百 KB 上下文,单独这个字符串就够了。

边界测试的结果很明确。下面这些可以过:

1
2
3
4
5
6
7
<|channel|>final
<|channel|>commentary
<|channel|>confidence
<|channel|>hidden
<|channel|>reasoning
<|channel|>thinking
<|channel|>analysiz

下面这些会被拦:

1
2
3
<|channel|>analysis
<|channel|>Analysis
<|channel|> analysis

这就不像普通内容过滤了。它更像一个针对 Harmony / hidden reasoning channel 的 guard。

推断需要直连上游来坐实。

直连上游验证

如果只在一个环境复现,锅可能在任何一层。于是直接打 Copilot 上游:

1
https://api.githubcopilot.com/v1/responses

结果:

model result
gemini-2.5-pro 400, unsupported Responses API
gemini-3.5-flash 400, unsupported Responses API
gpt-5-mini 200
gpt-5.3-codex 200
gpt-5.4 200
gpt-5.4-mini 200
gpt-5.5 200
gpt-5.6-luna 400, Request blocked
gpt-5.6-sol 400, Request blocked
gpt-5.6-terra 400, Request blocked
mai-code-1-flash-picker 200

Gemini 那两条是另一个问题:它们本来就不支持 Responses API。真正相关的是 gpt-5.6-lunagpt-5.6-solgpt-5.6-terra

这三条直连上游也 400。

这个字符串从哪里来?

<|channel|>analysis 为什么会出现在 OMP payload 里?

它不是用户手写的,也不是 advisor 文本随机带进去的。它来自 OMP 自己的 transcript serialization。

上游源码路径是这些:

  • packages/agent/src/compaction/compaction.ts
  • packages/agent/src/compaction/utils.ts
  • packages/catalog/src/identity/dialect.ts
  • packages/ai/src/dialect/harmony.ts

compaction 里会把历史消息转换成 LLM message,然后序列化成对话文本:

1
2
3
4
const llmMessages = (options?.convertToLlm ?? defaultConvertToLlm)(currentMessages);
const conversationText = serializeConversation(llmMessages, preferredDialect(model.id));

let promptText = `<conversation>\n${conversationText}\n</conversation>\n\n`;

serializeConversation() 拿到 dialect 后调用对应 renderer 把历史消息转成 transcript 文本。

gpt-5.6-* 选哪个 dialect?preferredDialect() 对 OpenAI 和 gpt-oss family 返回 "harmony",其余走 fallback。

再看 Harmony dialect 怎么渲染 assistant thinking:

1
2
3
4
function renderThinking(text: string): string {
if (!text) return "";
return `${START}assistant${CHANNEL}analysis${MESSAGE}${text}${END}`;
}

翻成人话就是:

1
<|start|>assistant<|channel|>analysis<|message|>...<|end|>

所以这不是泄漏了什么奇怪字符串。OMP 有意用目标模型的 native dialect 渲染历史 assistant thinking,目的是保留不同类型的历史内容。

问题在最后一步:这段渲染后的 transcript 是当普通 input_text 发给 Copilot gpt-5.6-* 做 summarization 的。模型看到的不是结构化 transcript,而是普通用户输入里冒出来一个 <|channel|>analysis

Copilot 上游把它拦了。

谁的锅?

返回 400 的是 GitHub Copilot upstream,直连复现过。但让 marker 进入 compaction input 的是 serialization 路径本身。

Harmony marker 在 OMP 里合法,gpt-5.5 也不拦。但拼在一起就炸了。

那应该怎么修?

最小修法在 compaction summarization 这条路径:不要把 Harmony control tokens 原样作为 plain input 发给 Copilot gpt-5.6-*。关掉 Harmony 或禁止 assistant thinking 都没必要。

几种可能的做法:

  1. 对 Copilot gpt-5.6-* 的 compaction input 改用 XML/plain transcript。
  2. 在 compaction serialization 里 escape Harmony control tokens,比如把 <|channel|>analysis 变成不可被上游识别的普通文本。
  3. 把 assistant thinking demote 成普通安全标签,比如 <thinking>...</thinking>,不要用 Harmony channel marker。

这里也有边界:主对话的 tool-calling dialect 可以通过 PI_DIALECT 一类配置影响;但 compaction 这段代码查到的是直接用 preferredDialect(model.id),目前没看到用户级 opt-out。

所以用户侧 workaround 更现实:把 compaction 用的 fallback/smol model 临时换到不会拦的模型,例如前面测过 gpt-5.5 对最小字符串返回 200。

调 agent 的 context compaction,除了看摘要 prompt 写得好不好,还要看摘要输入里有没有把上一轮模型协议的控制 token 当普通文本塞回下一轮。

已把发现补到 OMP issue:https://github.com/can1357/oh-my-pi/issues/5184#issuecomment-4946123954