环境
- billion-context
0.1.100(npm 全局安装,proxy 模式)
- 客户端:
bili dsh → @deepseek-ai/dsh@0.1.5-rc.1
- 上游:
https://api.deepseek.com/chat/completions(OpenAI 兼容协议)
- 模型:
deepseek-flash(DeepSeek-V41-Flash),reasoningEffort: high,思考模式开启
- 系统:Windows
- 会话标记:
pfa-a723d224e63437ca
- 日志:
~/.local/state/billion-context/bili.log 第 15441–15445 行
现象
compress 之后的重放请求被上游直接拒绝:
{"error":{"message":"The `reasoning_content` in the thinking mode must be passed back to the API.","type":"invalid_request_error","param":null,"code":"invalid_request_error"}}
日志原文:
[acp-proxy: compress requested 2 range(s): m00001–m00077, m00090–m00092]
[acp-proxy: [Compressed m00001–m00077, m00090–m00092 → 2 block(s), ~46753 tokens saved.
Excluded 2 protected message(s) m00003, m00078 from compression range (recent/last-user zone).;
Excluded 3 protected message(s) m00089, m00091, m00093 from compression range (recent/last-user zone).]]
[acp-loop] round 1: re-request view refreshed to post-compress fold
[acp-loop] round 1: proxy tool executed; re-requesting so the model sees the result
[acp-proxy: compress loop upstream error 400: {"error":{"message":"The `reasoning_content` in the thinking mode must be passed back to the API.", ...}}]
时间线:折叠完成 11:47:26.041 → 400 11:47:26.257,间隔 216 ms,中间没有任何用户操作。
同一轮折叠之前的请求是成功的([acp-usage] round 1 input=93330 cached=91136)。
所以触发者是折叠本身,不是客户端送进来的历史。
根因
applySingleRange() 的执行次序有问题:轮次完整性调整在前,保护区过滤在后。
| 次序 |
代码位置(npm dist/index.js, v0.1.100) |
作用 |
| 1 |
46021 applyPairBoundaryAdjustments() |
保证轮次完整:把 reasoning ↔ tool-call ↔ tool-result 补齐成完整的一轮 |
| 2 |
46082–46095 hitProtectedRaw 保护区过滤 |
把「最近 N 条 + 最新用户消息」内的消息从范围中间剔除 |
| 3 |
— |
剔除之后没有再复检完整性 |
一个已经校验合格的连续范围,被挖掉中间几格之后就可能只剩半个轮次。
证据:会话快照
~/.local/share/billion-context/sessions/openai/api.deepseek.com_16ced886da89c98308353b78.json
该快照在报错后约 500 ms 落盘(savedAt = 2026-09-10T11:47:26.763Z),内容就是失败请求所用的视图。
压缩后活跃视图的最后 3 条:
| # |
role |
contentType |
toolName |
toolCallId |
reasoningContent |
| 13 |
system |
text |
— |
— |
摘要块 acp_summary_b2 |
| 14 |
assistant |
tool-call |
pwsh |
call_01_1KCOFhwi1Eb3IQbsMedl8972 |
无 |
| 15 |
tool |
tool-result |
pwsh |
call_01_1KCOFhwi1Eb3IQbsMedl8972 |
— |
注意 call_01_ 这个编号:同轮的第一个调用 call_00_… 连同该轮的 reasoning 一起被折叠了,
只剩第 2 个调用孤零零地活着。
对照组(同一快照内未被折叠的轮次,结构正确):
[assistant reasoning] → [assistant tool-call call_00] → [assistant tool-call call_01] → [tool result ×2]
重建请求时 coreToOpenai() 会把连续的 assistant 消息合并成一条,于是:
- 正常轮次 →
{role:"assistant", tool_calls:[…], reasoning_content:"…"} ✅
- 被打洞的轮次 →
{role:"assistant", tool_calls:[…]} ❌ → 上游 400
机制同族。#539 是 acp-loop 重放时漏掉本轮新生成的 reasoning_content
(src/loop/core.ts 的 signature 门控,已由 PR #540 修复)。
本 issue 是历史消息被折叠切半导致同一后果 —— 同属「re-request 送出的 assistant 消息
带 tool_calls 却没有 reasoning_content」。PR #540 新增的回归测试
tests/issue539-loop-reasoning.test.ts 只覆盖了「本轮新生成」这一路,没有覆盖折叠切半。
另一条独立的丢失路径(#651 / PR #667,0.1.98 起)
dropCompressReasoning()(DEFAULT_COMPRESS_REASONING = { drop: true, threshold: 2048 })
会在 compress 工具调用闭合后,删除该轮 compress 之前的 reasoning 消息。
- 0.1.96 中不存在该函数(
grep -c dropCompressReasoning dist/index.js = 0)
- 0.1.98 起出现
在思考类模型上这会稳定触发同一个 400。也就是说:即使本次的空洞问题侥幸没触发,
一个已闭合的 compress 轮次在下一个请求里也会独立复现同样的报错。
建议修复
- 把保护区过滤提到
applyPairBoundaryAdjustments() 之前(先挖洞、再补完整性);或者
- 挖洞之后再跑一次
applyPairBoundaryAdjustments(),把因空洞而残缺的轮次整体移出压缩集合
- 顺带:
adjustBoundariesForReasoningPairs() 目前向后扩展只走一格(newEndIndex = j2 + 1),
遇到「一轮多个工具调用」根本补不全,建议改为扩展到「该 assistant 轮次的所有 tool-call
及其 tool-result 全部包住」
dropCompressReasoning 建议按 provider 自动判定,思考类 provider 默认 drop: false
可能相关的已有工作
临时规避
billion-context.json:
{
"compress": {
"reasoning": { "drop": false },
"minCompressRangeChars": 20000
}
}
仅缓解(关掉第二条路径 + 抬高压缩门槛让紧贴尾部的小范围被拒),不能根治。
环境
0.1.100(npm 全局安装,proxy 模式)bili dsh→@deepseek-ai/dsh@0.1.5-rc.1https://api.deepseek.com/chat/completions(OpenAI 兼容协议)deepseek-flash(DeepSeek-V41-Flash),reasoningEffort: high,思考模式开启pfa-a723d224e63437ca~/.local/state/billion-context/bili.log第 15441–15445 行现象
compress 之后的重放请求被上游直接拒绝:
{"error":{"message":"The `reasoning_content` in the thinking mode must be passed back to the API.","type":"invalid_request_error","param":null,"code":"invalid_request_error"}}日志原文:
时间线:折叠完成
11:47:26.041→ 40011:47:26.257,间隔 216 ms,中间没有任何用户操作。同一轮折叠之前的请求是成功的(
[acp-usage] round 1 input=93330 cached=91136)。所以触发者是折叠本身,不是客户端送进来的历史。
根因
applySingleRange()的执行次序有问题:轮次完整性调整在前,保护区过滤在后。dist/index.js, v0.1.100)46021applyPairBoundaryAdjustments()46082–46095hitProtectedRaw保护区过滤一个已经校验合格的连续范围,被挖掉中间几格之后就可能只剩半个轮次。
证据:会话快照
~/.local/share/billion-context/sessions/openai/api.deepseek.com_16ced886da89c98308353b78.json该快照在报错后约 500 ms 落盘(
savedAt = 2026-09-10T11:47:26.763Z),内容就是失败请求所用的视图。压缩后活跃视图的最后 3 条:
acp_summary_b2call_01_1KCOFhwi1Eb3IQbsMedl8972call_01_1KCOFhwi1Eb3IQbsMedl8972注意
call_01_这个编号:同轮的第一个调用call_00_…连同该轮的 reasoning 一起被折叠了,只剩第 2 个调用孤零零地活着。
对照组(同一快照内未被折叠的轮次,结构正确):
重建请求时
coreToOpenai()会把连续的 assistant 消息合并成一条,于是:{role:"assistant", tool_calls:[…], reasoning_content:"…"}✅{role:"assistant", tool_calls:[…]}❌ → 上游 400与 #539 / #540 的关系
机制同族。#539 是 acp-loop 重放时漏掉本轮新生成的
reasoning_content(
src/loop/core.ts的 signature 门控,已由 PR #540 修复)。本 issue 是历史消息被折叠切半导致同一后果 —— 同属「re-request 送出的 assistant 消息
带 tool_calls 却没有 reasoning_content」。PR #540 新增的回归测试
tests/issue539-loop-reasoning.test.ts只覆盖了「本轮新生成」这一路,没有覆盖折叠切半。另一条独立的丢失路径(#651 / PR #667,0.1.98 起)
dropCompressReasoning()(DEFAULT_COMPRESS_REASONING = { drop: true, threshold: 2048 })会在 compress 工具调用闭合后,删除该轮 compress 之前的 reasoning 消息。
grep -c dropCompressReasoning dist/index.js= 0)在思考类模型上这会稳定触发同一个 400。也就是说:即使本次的空洞问题侥幸没触发,
一个已闭合的 compress 轮次在下一个请求里也会独立复现同样的报错。
建议修复
applyPairBoundaryAdjustments()之前(先挖洞、再补完整性);或者applyPairBoundaryAdjustments(),把因空洞而残缺的轮次整体移出压缩集合adjustBoundariesForReasoningPairs()目前向后扩展只走一格(newEndIndex = j2 + 1),遇到「一轮多个工具调用」根本补不全,建议改为扩展到「该 assistant 轮次的所有 tool-call
及其 tool-result 全部包住」
dropCompressReasoning建议按 provider 自动判定,思考类 provider 默认drop: false可能相关的已有工作
fix(preflight): preserve complete content when recovering blocked ranges(open)—— 看起来正是处理「blocked range 恢复时内容不完整」,可能与本 issue 重叠
临时规避
billion-context.json:{ "compress": { "reasoning": { "drop": false }, "minCompressRangeChars": 20000 } }仅缓解(关掉第二条路径 + 抬高压缩门槛让紧贴尾部的小范围被拒),不能根治。