Repository navigation
fix: overflow-arm stored above the headroom-reserved judging limit → unbounded CONTEXT_WINDOW_EXCEEDED loop #3738
Description
Summary
A session whose model declares a large output budget can fail to converge on a context overflow and loop the client into an unbounded 400/400/400 cycle. bili detects the overflow, arms its emergency-shrink baseline, and then judges the next request against a smaller window than the one it armed at — so the arm can never bring the payload under the limit it exists to enforce. This is a self-heal defect, not a provider problem.
Version-first triage
Reporter runs bili v0.1.200 (installed under DSH Desktop's plugin profile; the running proxy reports 0.1.201). Code at that tag confirms the defect is present, not a version-lag artifact:
$ git grep -n "s.stats.lastInputTokens = info.window" v0.1.200 -- src/server/relay.ts
v0.1.200:src/server/relay.ts:831: s.stats.lastInputTokens = info.window;
$ git show v0.1.200:src/util.ts | grep -n "cap \* window\|DEFAULT_OUTPUT_HEADROOM_MAX_PCT = "
327:const DEFAULT_OUTPUT_HEADROOM_MAX_PCT = 0.25;
358: const reserved = Math.min(maxOutput, cap * window);
Reproduction (real limb, verbatim)
[acp-proxy: upstream HTTP 400: {"error":{"message":"This model's maximum context length is
1048576 tokens. However, you requested 1097304 tokens (841304 in the messages, 256000 in
the completion). Please reduce the length of the messages or completion.
(request_id: 201b2156-8f9f-46d2-b96a-b1485e297ac3)","type":"invalid_request_error",
"param":null,"code":"invalid_request_error"}}]
Surface effect in the client: 本轮运行失败 / 处理失败, tagged CONTEXT_WINDOW_EXCEEDED, and every queued 继续执行目标 entry flips to 已停止. The session never recovers on its own.
Root cause — the arm is stored above the limit the kernel judges against
Three values are in play and they disagree:
| value | where | this limb |
|---|---|---|
| stated window | parsed from the upstream rejection (src/util.ts:291) |
1,048,576 |
| reserved judging limit | reserveOutputHeadroom(window, maxOutput, 0.25) (src/util.ts:355), stored at src/server/handle.ts:2137 as session.metadata.effectiveContextLimit |
792,576 |
| arm | src/server/relay.ts:831 s.stats.lastInputTokens = info.window |
1,048,576 ← bug |
reserveOutputHeadroom computes reserved = min(maxOutput, capPct × window):
capPct × window = 0.25 × 1,048,576 = 262,144
reserved = min(256,000, 262,144) = 256,000
effective = 1,048,576 − 256,000 = 792,576
bili already knew the payload did not fit: server.ts:3143 payloadFitsWindow = 841,304 < 792,576 is false, and server.ts:3067 triggerFires is true. It forwarded anyway, which is correct in isolation — one probe is fine (server.ts:3102 probeForEvidence forwards once to acquire upstream evidence, and the #2490 doomed-probe guard correctly lets this payload through since it is under the 2 MB byte floor).
What breaks is the recovery. armOverflowShrink stores the raw stated window as the meter preflight later consults (preflight.ts:1527 currentTokens = baselineKnown ? deps.session.stats.lastInputTokens : …), while the fit check and every nudge band run against the reserved effectiveContextLimit. So the meter reads 1,048,576 while the bands enforce 792,576:
- over-limit band (
≥ 75%of 792,576 = 594,432) — never read as satisfied by a 1,048,576 meter; - emergency band (
emergencyThresholdPct = 0.95,kernel/src/config.ts:20→ 752,947) — same.
The client re-sends its full history next turn, bili forwards it verbatim again, the upstream rejects it again, the arm is re-set to 1,048,576. Unbounded, and no fold is ever demanded that the client's re-sent payload can satisfy.
Note the arm's value is upstream truth and must stay that way — the defect is only that it is never reconciled with the reservation the kernel is actually enforcing.
Fix
Clamp the arm to min(stated window, reserved judging limit) at src/server/relay.ts:831:
const judgingLimit = typeof s.metadata.effectiveContextLimit === "number" && s.metadata.effectiveContextLimit > 0
? s.metadata.effectiveContextLimit
: 0;
const armedWindow = judgingLimit > 0 && judgingLimit < info.window ? judgingLimit : info.window;
s.stats.lastInputTokens = armedWindow;
s.stats.overflowArmTokens = armedWindow; // #1110: the side-request guard reads thisThe clamp is deliberately conditional and is a no-op in the common case:
- No output budget declared →
reserveOutputHeadroomreturns the window unchanged →effectiveContextLimit === info.window→ the arm stays at the exact stated window. Verified against the real code path: arm1,048,576, limit1,048,576. - Output budget declared → the arm becomes
792,576, which is≤the judging limit, so the bands can fire on the very next turn and the fold converges.
overflowArmTokens is updated in lockstep because the side-request guard (relay.ts:150-155) reads it and would otherwise keep using the un-clamped value.
The fail-fast 413 path and the #2490 doomed-probe guard are untouched.
Verification
tests/fix-arm-headroom-clamp.test.ts (new, 2 cases, pinned against the limb's real numbers):
- Clamp applies — model with
max_tokens: 256,000against a1,048,576window, upstream answers with the verbatim rejection body. AssertslastInputTokensSource === "overflow-arm",lastInputTokens === 792,576(the reservation),lastInputTokens <= effectiveContextLimit, andoverflowArmTokens === lastInputTokens. - Control: no reservation — same session, no
max_tokens. AssertseffectiveContextLimit === 1,048,576(no reservation recorded) and the arm stays at the exact stated window1,048,576.
Red/green: case 1 fails without the fix (arm 1048576 must sit BELOW the stated window 1048576), case 2 passes either way — proving the clamp is conditional rather than a blanket shrink.
Pre-flight results:
$ npx tsc --noEmit --project tsconfig.json # clean
$ node --import tsx --test tests/fix-arm-headroom-clamp.test.ts
ok 1 - the stated-window arm is clamped to the headroom-reserved judging limit
ok 2 - the stated-window arm is the upstream truth when no headroom reservation applies
# tests 2 # pass 2 # fail 0
Adjacent suites, no regressions:
context-overflow.test.ts -> # pass 25 # fail 0
issue2313-probe-forward.test.ts -> # pass 4 # fail 0
issue496-forward-once-learn.test.ts -> # pass 1 # fail 0
headroom-output-fallback.test.ts -> # pass 6 # fail 0
fallback-window-floor.test.ts -> # pass 2 # fail 0
e2e-overflow-selfheal.test.ts -> # pass 3 # fail 0
e2e-relay-5xx-selfheal.test.ts -> # pass 3 # fail 0
issue3004-nudge-decide-selfheal.test.ts -> # pass 7 # fail 0
native-intercept-selfheal.test.ts -> # pass 22 # fail 0
Blast radius
armOverflowShrink has exactly two arming branches, both now writing the clamped / original value as before. No persisted state, no learner, no declared window — the site comment ("declared window unchanged, nothing persisted/learned") still holds. The change is confined to one session-lifetime in-memory meter.
Environment
- billion-context
0.1.200(reporter) /0.1.201(running proxy) - Client: DeepSeek Desktop (dsh), single long-running session
- Upstream: OpenAI-compatible,
1,048,576-token window
Activity
poboll commentedon Oct 18, 2026
PR opened: #3739 —
fix: clamp the overflow-arm to the headroom-reserved judging limitBranch
poboll:2026-10-18_arm-headroom-clamp, basemaster, currentlyMERGEABLE.Diff is deliberately minimal (
src/server/relay.ts+23/−3, new test +270):Red/green pinned in
tests/fix-arm-headroom-clamp.test.ts— case 1 fails without the fix (arm 1048576 must sit BELOW the stated window 1048576), case 2 (no output budget → arm stays at the exact stated window) passes either way, which is what proves the clamp is conditional rather than a blanket shrink of the arm.Pre-flight:
tsc --noEmitclean; adjacent suites green (context-overflow25,issue2313-probe-forward4,issue496-forward-once-learn1,headroom-output-fallback6,fallback-window-floor2,e2e-overflow-selfheal3,e2e-relay-5xx-selfheal3,issue3004-nudge-decide-selfheal7,native-intercept-selfheal22 — all 0 failures).Merging stays yours per AGENTS.md §4.
ranxianglei commentedon Oct 18, 2026
(ai: vllm-qwen/qwen3.8-27b)
查重: 不重复 —— 但同作者的 PR #3739 已在 issue 后 4 分钟提交,实现的就是本 issue 提出的 clamp 修复(
Closes #3738)。因此不再另开并行 PR;我的工作改为:①独立复核 bug 在 v0.1.200/master 的代码证据,②按 PR Review 规范独立审 #3739(不复读 issue 结论),③定向跑测试后给出八问结论。标签:
bugdshscope/kernelp1(缺陷;dsh Desktop 客户端;上下文管理核心自恢复逻辑;p1——overflow 后的会话永不收敛、无限 400 循环、队列全部停止,用户无法继续该会话)开始验证,稍后在本线程与 #3739 各给结论。
docs: cite ranxianglei#3738 in the overflow-arm clamp comment (was an…
ranxianglei commentedon Oct 18, 2026
(ai: vllm-qwen/qwen3.8-27b)
修复 PR #3739 的独立审核已完成,结论:可合(八问全文见该 PR 线程)。要点:bug 真实性已在 master 代码上红绿复现钉死;arm 写入点 clamp 到预留判定线是根因层修复(恢复 #1839 的 arm ≤ 判定窗口契约),消费者回归清点无破坏;远程 CI 全绿(含我推的一个 docs-only 小修
728b15f69:注释里未填的#N+1:占位符改为#3738)。合并留给人工:#3739ranxianglei commentedon Oct 18, 2026
[bot] 🏷
(ai: vllm-qwen/qwen3.8-27b)
独立验证完成,PR #3739 审核进行中(本 issue 与 #3739 是同一件事:你报的问题 + 你的修复 PR,按「一个 issue 一个 PR」由 PR 承载收工)。
1. 缺陷在 v0.1.200 tag 上确认存在(不是版本滞后):
armOverflowShrink把上游声明的原始窗口(1,048,576)原样存进lastInputTokens/overflowArmTokens,而 preflight 的判定目标是 headroom 预留后的 792,576——arm 高于它要服务的判定上限。2. 机制我独立走了一遍代码(不复述你的分析,结论一致且补了一层):meter 在每次 fold 提交后会递减(stream/absorb 路径),所以 pre-fix 的 fold walk 要收敛必须攒够「真实缺口(≈49K) + 整个预留额(256K)」的节省量——foldable 质量不够时每一轮都 exhausted → fail-fast → 死循环;够时也会多折叠最多 256K 的可恢复上下文。另外查到一个相关事实:#453 的 max_tokens clamp 在 v0.1.200/v0.1.201 就已存在,事故那次 400 能带着完整 256K 预算发出去,正是因为 planner 侧对输入的计量低于预留上限(估算偏差类),clamp 判定「放得下」而放行——arm 是这种场景下唯一的上游实证锚点,它存高了后续每轮判定都要为幻影缺口买单。
3. PR #3739 审核进展(已直接在你的分支上补了东西,均为独立标注 commit):
#N+1:占位符 → 改为引用#3738;完整的结构化八问审核结论会贴到 #3739(含回归清点、行为变更披露、可合判定)。合并仍归你(AGENTS.md §4)。
test: pin next-turn convergence the clamp restores (ranxianglei#3738)
ranxianglei commentedon Oct 18, 2026
[bot] 🏷
(ai: vllm-qwen/qwen3.8-27b)
收工判定:缺陷确认、修复经独立审核通过,由 PR #3739 承载,待 CI 绿后人工合并。
结构化八问(triage 结论)
effectiveContextLimit对齐。独立代码走读进一步钉死两点:meter 随 fold 提交递减,所以 pre-fix 收敛条件是 s > 真实缺口+整个预留额;且 使用pi 压缩后就提示prompt is too long #1195 请求内 refold 的门也会被虚高 arm 顶开(实测 deficit=0 时仍强制折叠销毁上下文)。另查实:plugin mode: no hard backstop when the agent ignores the compress nudge — context grows past the window and upstream 400s #453 max_tokens clamp 在 v0.1.200/201 已存在,那次 400 能带着完整预算发出是因为 planner 侧计量低于真实 payload(估算偏差类)而放行——arm 是唯一的上游实证锚点,存高即全线失真。min(stated, reserved)——条件式钳制,无预留时逐字节同旧行为;overflowArmTokens锁步更新(「BUG」sideRequestGuard 会永久拦截进程内第三方插件的 LLM 调用,导致与MemOS冲突 #1110 guard)。标签:bug / dsh(事故客户端 DeepSeek Desktop)/ scope/kernel(上下文管理核心逻辑;kernel 源码未动)/ p1(触发后会话不可用直至重启)。
交付:PR #3739(Closes 本 issue)。审核中已在该分支追加一个测试 commit:turn-2 收敛的行为级回归(mock 强制声明窗口,红绿对照本地实测:pre-fix case1/case3 FAIL、case2 PASS;post-fix 3/3 PASS),并修正了测试头部的机制叙述;
#N+1:占位符你已自行修复(728b15f69)。相邻定向套件全绿、typecheck 干净,全量以 CI 为准。合并归你(AGENTS.md §4)。Sieuno commentedon Oct 19, 2026
补充一个 0.1.201 / DSH / DeepSeek Anthropic Messages 兼容端点的真实案例,供确认 #3739 是否覆盖。已看到 #3738 关闭、修复进入 0.1.202;本案例发生在 0.1.201,尚未在 0.1.202 复测,不是在声称新版本回归。本评论由用户授权的 OpenClaw Agent 根据脱敏日志整理发布。
与原报告的差异
0.2.1-rc.2,桌面壳构建号未核实。billion-context@0.1.201,面板构建ff42dcc1,shadow 存储。deepseek-flash,端点https://api.deepseek.com/anthropic/v1/messages;最终成功走deepseek-officialAPI Key 路由。256000;随后已降为32768,但输入本身仍超过上游窗口,因此并非仅高输出预留造成。compress.modelContextLimit=700000并应用后,日志确认普通预检 target 为700000;但后续同会话的 overflow 请求内重试 target 变成1048576。上游错误数字如下,均为上游返回值,不是本地估算:
同一会话的关键序列
以下为 2026-10-18 的 UTC 日志时间,对应北京时间 10 月 19 日凌晨;
SESSION_C是稳定匿名别名。17:33:44Z:约1557491本地估算,预检 target700000。17:46:44Z:32 段压缩后汇总1557491 → 904983,仍有 130 个可压范围;预算耗尽,fail-fast 502。此次超窗主请求没有转发。17:46:48Z:后续同会话请求命中 estimate-only evidence probe,转发一次。该请求由客户端自动恢复还是用户动作触发,未严格确认。17:46:52Z请求内重试日志 target 为1048576,不是 operator 配置的700000;仅再折叠 1 段,仍收到 400:1117786 + 32768 = 1150554。17:49:25Z:在同一会话底部正常追加继续消息,不分支/不编辑历史,target 又为700000;沿用已有压缩成果继续折叠。18:01:12Z:成功取得上游 usageinput=773909 cached=5888,后续思考/工具调用继续,用户确认本轮跑通。最终同会话已恢复,压缩确实执行且成果可续用。不是“不会压缩”“全部状态丢失”或“永久无法恢复”。 700K 是本地保守预算,不是上游硬上限;成功输入 773909 不代表违反真实模型上限。各日志计量口径不同,不将 baseline、local-est、循环 payload 与真实 usage 混算。
希望维护者确认的两个边界
700000→ probe → retry1048576→ 下一次普通请求又700000,是否为预期行为?只读核对时,当前 master 的
src/util.ts中shouldReserveOutputHeadroom仍为protocol !== "anthropic";src/server/handle.ts请求内 retry 将realWindow传入 pipeline,预检调用处仍有overflowWindow !== undefined ? { ...reqConfig, modelContextLimit: overflowWindow } : reqConfig。这是静态线索,未做新的端到端复现,不能据此宣称 0.1.202 仍必现。恢复 workaround 为:当前实际 provider 的输出预算
32768、上述模型的 operator limit700000、应用路由配置,并在同一会话、不编辑历史或新建分支的前提下继续。32 轮耗尽后已有成果保留,后续继续压缩成功。另有 fork adoption 404 / snapshot 409 的伴随记录,但 #2469 / #2821 已有相关讨论,且 404 后还出现备用 resume-inheritance;本评论不将其定性为全部状态丢失,也不将 shadow DIGEST DRIFT 定性为根因。
目前没有可公开的确定性最小转录;完整项目历史、请求 body、凭据均未附。若上述 Messages / operator-limit 差异应独立跟踪,请告知,我们再拆分,先在此补充以避免重复立项。
fix: apply armed completion reserve min-wise against the stated wall (#…
fix: apply armed completion reserve min-wise against the stated wall (#…
fix(ranxianglei#3900): overflow 400s teach the calibration ring — k̂ …
test: clamp the overflow arm against the proven window (ranxianglei#3900
test: clamp the overflow arm against the proven window (ranxianglei#3900
test: clamp the overflow arm against the proven window (ranxianglei#3900
test: clamp the overflow arm against the proven window (ranxianglei#3900
test: clamp the overflow arm against the proven window (ranxianglei#3900
test: clamp the overflow arm against the proven window (ranxianglei#3900