[Bug] 本地压缩估算90.4万token、上游实际计数113万;context_exceeded且retryable=false后仍循环重发88次,压缩失败无历史裁剪降级,fork无法解除超窗(3.10.1 / OpenAI-compatible)
Nobody has claimed this yet.
- Dominant language
- No language data
- Stars
- 22
- Forks
- 1
- PR merge metrics
- No merged PRs in 30d
Description
类别
对话 / Agent 交互(auto-compact / 上下文压缩)
涉及框架
ZCode Agent,OpenAI-compatible provider
严重程度
阻塞使用 · Blocking
复现频率
必现 · Always(该超长会话触发压缩时)
问题描述
先说明排除项:本机 ZCode 3.10.1(内部版本 0.16.5)的 resources/glm/zcode.cjs 已内置 #93 的修复,OpenAI-compatible 流式请求会携带 stream_options: { include_usage: true },因此本问题不是 #93 所述 usage 缺失。
一个超长会话(第 202 轮、2503 条 message、8638 条 part;message + part 序列化记录合计约 14.2 MB)触发自动压缩(querySource=compact)后,客户端记录的待压缩历史估算为 903,733 / 903,788 token,但上游对实际压缩请求的计数约为 1,129,914 / 1,129,945 token,超过模型明确返回的 1,048,576 token 上限,因而被 HTTP 400 拒绝:
The input (1129914 tokens) is longer than the model's context length (1048576 tokens).
The input (1129945 tokens) is longer than the model's context length (1048576 tokens).
因此,现有证据并不支持“网关实际窗口小于 90.4 万”。相反,上游窗口约为 104.86 万;关键问题是:客户端的 preCompactTokenCount 比上游实际输入少算约 22.6 万 token(约 25%)。可能未计入或低估了系统提示、压缩摘要指令、工具定义、请求封装,或客户端与上游 tokenizer 存在差异。无论具体来源是哪一项,压缩预算没有按最终请求体做足够的安全校验,导致客户端判断可发送、上游实际判定超窗。
随后又暴露出两个恢复问题:
retryable=false后仍循环新建压缩请求:母会话在约 17 分钟内连续发起 88 个压缩请求,均为同一 queryId、不同 requestId、attempt=1,全部以context_exceeded失败;这不是单个请求的常规 retry,而像是外层 compact 调度反复重新入队,未继承连续失败计数或未触发熔断。- 失败后没有观察到能解除超窗的历史减量降级:客户端代码存在对旧工具结果做 microcompaction 的机制,但本次事故中未观察到
context_exceeded后按最终预算裁剪/分块历史;后续请求仍以近似规模失败。 - fork 不能解除超窗:fork 会话为近全量克隆(2493 条 message,母会话 2503 条),第一份压缩输入仍为 1,129,954 token,大于 1,048,576;随后又连续失败 8 次。因此 fork 不是该故障的有效恢复手段。
复现步骤
- 使用 OpenAI-compatible 通道的模型,在同一会话持续对话,并产生大量工具输出,直到上下文接近模型窗口。
- 等待 auto/reactive compact 触发。
- 客户端依据
preCompactTokenCount认为压缩输入尚可发送,但最终请求经上游 tokenizer 计数后超过窗口,收到 400context_exceeded、retryable=false。 - 观察客户端继续以同一 queryId 创建新的 compact 请求,每个请求又从
attempt=1开始,且没有通过裁剪/分块使输入降到窗口内。 - (可选)fork 该会话;近全量历史被复制,新会话仍然超窗。
预期表现
- 在发送压缩请求前,按最终请求组成(历史、系统提示、摘要指令、工具定义等)和保守 tokenizer 预算校验,并为 tokenizer 差异保留安全余量。
- 一旦上游明确返回
context_exceeded,下一次必须减少输入:分块摘要、裁剪较早历史,或使用不会先携带整份超窗上下文的降级路径。 - 尊重
retryable=false;同一 compact query 连续失败达到上限后停止重新入队,展示明确错误和可执行的恢复指引。 - fork 时提示它会近全量复制上下文,不能作为超窗恢复方案;提供“仅保留最近消息/选定消息创建新会话”等真正减量的方式。
实际表现
- 本地压缩前估算约 90.4 万 token,但上游实际收到约 113.0 万 token,超过 104.86 万窗口。
- 母会话连续失败 88 次,持续约 17 分钟;fork 后又失败 8 次。
- 没有观察到失败触发的有效历史裁剪/分块;用户只能另开全新会话并人工迁移必要上下文。
证据
- 日志:
~/.zcode/cli/log/zcode-2026-08-29.jsonl- 母会话 12:02:07Z–12:19:31Z:同一 queryId 连续 88 个 compact 请求;每个均为新 requestId、
attempt=1、HTTP 400、reason=context_exceeded、retryable=false;12:19:33Z 用户取消。 - 上游原始错误明确给出
1129914 > 1048576、1129945 > 1048576。 - fork 后原始错误为
1129954 > 1048576;12:20:43Z–12:22:09Z 又有 8 个 compact 请求失败,12:22:12Z 用户取消。
- 母会话 12:02:07Z–12:19:31Z:同一 queryId 连续 88 个 compact 请求;每个均为新 requestId、
- 会话库 compaction part:
cmp_0f61818e:auto,interrupted,preCompactTokenCount=903733。cmp_e05fd636:reactive,interrupted,preCompactTokenCount=903788。- 同一会话 08-15 / 08-17 曾成功压缩,preCompact 分别为 298,906 / 230,091;其他会话 08-18 / 08-20 也曾在 5,851–6,648 成功,说明压缩机制并非始终失效,而是大输入的预算与失败恢复存在缺陷。
- 8/29 统计:母会话 88 次、fork 8 次,共 96 次
context_exceeded;全天共有 108 条失败事件、107 个唯一 requestId(另含 3 次 auth_failed、6 次 timeout、3 条取消事件对应 2 个唯一 requestId)。 - 本地配置当前声明
limit.context=1000000、limit.output=384000,但该文件修改时间晚于事故,只能说明当前配置,不能据此反推 8/29 的历史根因;本 Issue 以日志中上游返回的 1,048,576 窗口为准。 - 代码核对(本机 3.10.1):
- OpenAI-compatible
include_usage修复已存在。 - 存在
thresholdPercentOverride,默认 preflight-v1 阈值百分比为 100,并预留 reserve / buffer;但用户配置中未见公开的 compact threshold 入口。 - 存在旧工具结果 microcompaction,故不能笼统表述为“完全没有减量机制”;本事故缺少的是
context_exceeded后足以解除超窗的最终请求预算、裁剪/分块和熔断。
- OpenAI-compatible
ZCode 版本
3.10.1.6272(CLI 内部版本 0.16.5)
设备 / 系统
Windows 11 家庭中文版 25H2,x64(客户端记录:win32 10.0.26200 x64;系统 Build 26200.9168)
模型渠道
OpenCode Zen 网关(baseURL opencode.ai/zen/go/v1),OpenAI-compatible,模型 deepseek-v4-pro。上游错误明确返回模型窗口为 1,048,576 token。
关联
- #93(OpenAI-compatible usage 缺失;本机版本已包含该修复)
- #114(同族问题:auto-compact 对 OpenAI-compatible 失效及阈值不可配置)
- #348(上下文压缩可编排)
- #412(compact 命令增强)
建议(Asks)
- 用最终请求组成做压缩预算,并为 tokenizer 差异设置保守安全余量;不要只依据
preCompactTokenCount判断可发送。 - 记录并展示预算构成(历史、system、tools、压缩指令、预留输出),方便定位约 22.6 万 token 的差额来源。
- 收到
context_exceeded后,下一次强制减量(裁剪/分块/分层摘要),禁止原规模重发。 - 外层 compact 调度继承连续失败次数并尊重
retryable=false;达到上限后熔断,避免每次重建请求都显示attempt=1。 - 提供真正减量的新会话迁移能力;不要把近全量 fork 或仍可能走同一路径的
/compact当作已验证的恢复方案。 - 将 compact threshold / safety margin 暴露为可配置项,并给 OpenAI-compatible provider 提供合理默认值。
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with resources/glm/zcode.cjs and the cited JSONL log, tracing compact scheduling, preflight token accounting, and context_exceeded handling. Done means the final request budget includes all components with safety margin, failures reduce input before retrying, and retryable=false stops repeated requeueing after a bounded number of attempts.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript
- Domain
- ai, backend-api-design
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 38/100