Repository navigation
Description
TL;DR
A 129-turn DSH session (2,057,076 tokens by the upstream's own count) was rewound. DSH implements "rewind" as fork + full seed: a new session is created with parentSession + isSeeded: true, every parent event is replayed from seq 0, and a session/end-seed {"inherited": true} marker closes the inherited prefix. When the rewind boundary sits at the tail of the parent, this copies the entire history — not a single token is reclaimed.
On the forked session, every subsequent message hangs in "deep thinking" for 10–11 minutes and then fails. bili sees blocks = 0, parent_session_id = null, messages = 8239, so fork adoption takes the "anonymous fork, no adoption support in v1" branch and starts from scratch.
Three separable defects fall out of the logs:
non-monotonic refs after client history rewritedegrades fold granularity to ~5k tokens/round. Every preflight round logsnormalized reversed range m09063–m08294 → m08294–m09063, and the viable-range count drops monotonically 161 → 145. 31 rounds only reclaim 143,035 tokens.- The 31-round ceiling is arithmetically insufficient but still runs to completion. Deficit was 283,543 tokens (1,076,119 vs 792,576); 31 rounds × ~5k ≈ 155k max. It burns 649,275 ms (10.8 min) and then fails anyway.
- After
fail-fast, the payload is forwarded regardless. The error text saysThe over-window payload was NOT forwarded, yet the very next request reached DeepSeek and came back with a 400 carrying the real upstream counts. That 400 is the only reason I could calibrate the estimator (×1.1584).
A fourth, smaller defect: compress.modelContextLimit: 450000 was ignored — [headroom] still printed effective window 1048576 -> 792576.
Environment
| Item | Value |
|---|---|
| OS | Windows 11 (no WSL) |
| Host | DeepSeek Harness (DSH) Desktop Next 2.0.21-next |
| Plugin | billion-context 0.1.212 (npm latest; dist unmodified at the time of the first capture) |
| Usage mode | Plugin native (runs inside the host process, bili-native + bili-compaction-gate) |
| Protocol | Anthropic Messages (/v1/messages) via deepseek-account |
| Model | deepseek-flash, reasoningEffort: max, maxTokens: 256000 |
| Declared window | 1,048,576 → bili target 792,576 (headroom caps at 0.25 × window = 262,144, so the full 256,000 is reserved) |
| Session | 129 turns, 14,955 events, rawInputTokens: 1,538,900, upstream-measured 2,057,076 |
What DSH's rewind actually does
seq 14830 turn/end {turn:127, reason:{kind:"completed"}}
seq 14831 agent/inbox/spliced <- user message, never sent
seq 14832 session/end-seed {"inherited":true}
seq 14833 agent/inbox/spliced {"removedCount":1,"outcome":"canceled"}
seq 14834 user/message "先回我一句话,什么话都行,我做连接测试"
seq 14835 turn/start {turn:128}
Session header of the fork:
{"type":"session","version":4,"id":"session-63d92c8a-…",
"parentSession":"session-ddbeba60-…","isSeeded":true,"delegationDepth":0}Note seq 0 carries the timestamp of four days earlier — the whole history was re-seeded. The chain is five sessions deep, each created 3–13 s after the previous one, each isSeeded: true, each with blocks = 0:
| session | ingested | messages | blocks |
|---|---|---|---|
54ac0879 |
5775 | 5776 | 0 |
c2659989 |
5776 | 5777 | 0 |
515c2bd4 |
5778 | 5779 | 0 |
63d92c8a |
8238 | 8239 | 0 |
So every rewind multiplies the number of sessions that must re-fold the same history from zero, and none of them can inherit the parent's 99 blocks.
Observed behaviour
First preflight (deficit 283,543):
round 15/31: payload ~1002238 tok vs window 792576 (deficit 209662), 161 viable range(s)
round 16/31: payload ~997695 tok (deficit 205119), 160 viable range(s)
round 17/31: payload ~991372 tok (deficit 198796), 159 viable range(s)
round 18/31: payload ~986707 tok (deficit 194131), 158 viable range(s)
round 19/31: payload ~982318 tok (deficit 189742), 157 viable range(s)
round 20/31: payload ~977996 tok (deficit 185420), 156 viable range(s)
round 21/31: payload ~973766 tok (deficit 181190), 155 viable range(s)
round 22/31: payload ~969564 tok (deficit 176988), 154 viable range(s)
round 23/31: payload ~965667 tok (deficit 173091), 153 viable range(s)
round 24/31: payload ~961870 tok (deficit 169294), 152 viable range(s)
round 25/31: payload ~957856 tok (deficit 165280), 151 viable range(s)
round 26/31: payload ~954004 tok (deficit 161428), 150 viable range(s)
round 27/31: payload ~950030 tok (deficit 157454), 149 viable range(s)
round 28/31: payload ~945887 tok (deficit 153311), 148 viable range(s)
round 29/31: payload ~941873 tok (deficit 149297), 147 viable range(s)
round 30/31: payload ~938461 tok (deficit 145885), 146 viable range(s)
round 31/31: payload ~935583 tok (deficit 143007), 145 viable range(s)
15:47:03 preflight compressed 31 range(s), ~143035.37 tokens saved
(1076119.3417761568 → 931914.0431698343) in 649275ms
15:47:04 [error] preflight fail-fast 502 (retryable=true):
context ~931914.04 tokens (down from ~1076119.34 before preflight)
exceeds the model window 792576 (model=deepseek-flash)
Second attempt on the same session (deficit smaller because some folds stuck):
16:06:01 preflight compressed 25 range(s), ~80636.31 tokens saved
(932071.67 → 851163.59) in 691259ms
16:06:01 preflight fail-fast 502: context ~851163.59 exceeds window 792576
By then the viable-range pool had collapsed 161 → 25. The folds are spent; the session can never converge.
Defect 1 — reversed ranges throttle every round to ~5k
Every round emits the same warning, and the count decreases by exactly one each time:
[warn] [preflight] normalized reversed range m09063–m08294 → m08294–m09063
(non-monotonic refs after client history rewrite, #1001)
This is consistent with #2168 (closed — that one was about acp_status displaying reversed ranges; this is the folding consequence). After a seed replay, refs are re-minted in replay order while the existing block spans still point at the parent's numbering, so range boundaries cross. The normalizer fixes each range individually (start > end → swap) but never merges adjacent fixed ranges, so preflight folds them one message at a time — 1.5k–5k tokens each — instead of one 60k block.
Evidence that granularity, not opportunity, is the limit: contextView showed 100+ compressible ranges, each 1.5k–5k tokens, and the sum of what 31 rounds reclaimed (143k) is almost exactly 31 × 4.6k.
#1001 is cited in the message, so the normalization is deliberate — the bug is that normalization is terminal instead of being followed by a coalesce pass.
Defect 2 — a hard round cap that cannot close the deficit
MAX_PREFLIGHT_ROUNDS bounds the loop, and 31 rounds × ~5k ≈ 155k < the 283k deficit. Nothing in the loop notices that progress rate is hopeless: the per-round reclaim is ~4.6k while the deficit stays above 140k for the entire run.
Related open issues that describe the same terminal state from other entry points: #2438, #5022, #4951. This report adds the fold-granularity mechanism (reversed ranges after seed) rather than chain-miss or unfold explosion.
Defect 3 — NOT forwarded is not what happens
The fail-fast message ends with:
The over-window payload was NOT forwarded.
But the request was forwarded. Seconds later the upstream answered, and that response is the only reason the real counts are known:
[acp-proxy: upstream HTTP 400:
{"error":{"message":"This model's maximum context length is 1048576 tokens.
However, you requested 1335502 tokens (1079502 in the messages, 256000 in the completion).
(request_id: 33871f58-…)","type":"invalid_request_error"}}]
931,914 (bili) → 1,079,502 (upstream) = ×1.1584. An earlier attempt at sentTotal 812,875 produced 1,322,953 in the messages = ×1.63. The panel meanwhile said Sent to LLM (after compression, est.): 685k (65% of limit).
Either the message is wrong and the payload is always sent (so bili can learn the real count from the 400 — it doesn't, see #5370), or fail-fast only suppresses the first send and the retry policy sends the identical body again. Either way the user sees a 10-minute spinner followed by a generic error, and bili never records that it was 15.8–63% off.
Defect 4 — compress.modelContextLimit is silently overridden
With
compress:
modelContextLimit: 450000
emergencyThresholdPercent: "70%"
maxContextLimit: "60%"every preflight round still compared against window 792576, and [headroom] printed effective window 1048576 -> 792576 (reserved 256000 for max output 256000, cap 0.25). If modelContextLimit is meant to be the preflight hard wall, headroom should be derived from it (450000 − min(256000, 112500) = 337,500), not from the raw model window. The documented key had no effect at all — and it is the only user-facing lever that could have made this session converge without touching internals.
Root-cause hypothesis
- DSH rewind =
buildForkSeed(events, boundary)copying an inclusive prefix. When the boundary is the last event, the copy is total. - bili's
maybeAdoptForkBlockscannot adopt: the fork arrives withparent_session_id = null(anonymous) and noderivedFromSessionIdmetadata, so it logsanonymous fork … no adoption support in v1, starting fresh (#629)and starts withblocks = 0. - The seeded history is re-minted with fresh refs, but inherited block spans keep parent numbering →
start > endranges → per-round normalization → 4.6k-token folds. MAX_PREFLIGHT_ROUNDS× 4.6k cannot beat the deficit; the loop is not rate-aware.- On failure the payload is (still) sent, gets a 400, and the retry policy re-enters the same path — which is what the user experiences as "hangs forever, then errors".
forkAdoption: true does not help: the adoption path needs incomingCoreMessages to be non-empty, and for an anonymous fork it is falsy.
What would fix it, from a user's seat
- Coalesce after normalize. When
start > endis repaired, merge it with adjacent repaired ranges before folding. One 60k fold per round would have closed 283k in five rounds. - Be rate-aware. If
reclaimed / roundscannot closedeficitwithin the remaining rounds, stop early with a typed error and a concrete suggestion ("history is 2.6× the window; start a new session or lower maxTokens"). 649 s of folding followed by a generic failure is worse than an immediate refusal. - Do not claim "NOT forwarded" unless that is true. And when the upstream does answer with real counts, persist them (
contextTokensSource: "usage"already exists) so the next round budgets against reality instead ofestimate. - Honour
compress.modelContextLimitas the hard wall, including inside headroom. - Give seeded forks a lineage. DSH does write
parentSessionin the header andsession/end-seedin the log; if bili keyed adoption off those instead ofparent_session_id, a rewind would inherit the parent's blocks instead of re-folding 2M tokens.
Logs
/__bili/plugin/status?conversationId=session-63d92c8a-…
contextView.sentTotal 812875
contextView.limit 1048576
contextTokens 3352465 source: estimate
growth -617023
categories sysprompt 24078 / tool 314982 / text 319798 / code 39685 / summaries 114332
panel Sent to LLM (after compression, est.): 813k (78% of limit)
Session-only (compressed originals, est.): 711k — pruned from every request
blocks 99 total, b12/b62 inactive
compressibleRanges 100+ entries, 1.5K–5K tokens each
restoreOutputBudget overriding an explicit DSH config
[sess=session-df4b75cb-…] output budget restored 131072 -> 256000
(#3438: dsh-lane mid-band collapse above the <=2…)
This is arguably correct per #3438, but it means a user cannot lower maxTokens to buy window headroom — the single cheapest escape from an over-window session is silently reverted.
Fold throughput without preflight (the nudge path)
fold seq 63 15:33:20 reclaimed 4022
fold seq 64 15:33:46 reclaimed 4728
fold seq 65 15:34:00 reclaimed 4390
fold seq 66 15:34:21 reclaimed 3686
sentTotal 812,875 → 794,495 (4 folds, 18,380 tokens, ~75 s)
At ~4k per 15–25 s, reclaiming 300k would take 23 minutes of uninterrupted folding — which no client idle timeout survives.
中文摘要
一条 129 轮、205 万 token 的 DSH 会话做了「回退」。DSH 的回退是 fork + 全量 seed:新建会话、parentSession 指向父会话、isSeeded: true、从 seq 0 重放全部事件、session/end-seed {"inherited":true} 收尾。回退点落在父会话末尾时等于完整复制,一条 token 都没减。
之后每条消息都卡在「深度思考中」10–11 分钟再报错。bili 侧 blocks = 0、parent_session_id = null,走的是 anonymous fork 分支,从零重建。
四个可分离的缺陷:
- ref 乱序把每轮 fold 压到约 5k token。 每轮都打
normalized reversed range m09063–m08294 → m08294–m09063 (non-monotonic refs after client history rewrite, #1001),可行范围从 161 单调递减到 145。归一化只交换首尾,不与相邻区间合并,所以只能一条消息一条消息地折。 - 31 轮上限在数学上不够,但仍然跑满。 缺口 283,543,31 轮 × 4.6k ≈ 155k。烧掉 649,275 ms(10.8 分钟)后照样失败。第二次重试可行范围从 161 掉到 25,彻底榨干。
- fail-fast 说「未转发」,实际转发了。 紧接着上游返回 400,带着真实计数——这也是唯一能校准估算系数的机会:
931,914 → 1,079,502(×1.1584),更早一次812,875 → 1,322,953(×1.63)。面板却一直显示685k (65% of limit)。 compress.modelContextLimit: 450000被忽略。 每轮仍按window 792576比较,[headroom]打印effective window 1048576 -> 792576。这是用户唯一能不碰内部实现就让会话收敛的杠杆,却完全无效。
建议:归一化后做区间合并;按回收速率提前判死并给明确错误;「未转发」要么真不转发、要么把上游真实计数学回来(#5370);modelContextLimit 应真正作为硬墙;seed 出来的 fork 应该能靠 header 里的 parentSession + session/end-seed 继承父会话的块,而不是把 200 万 token 从零折一遍。
Activity
ranxianglei commentedon Nov 7, 2026
(ai: vllm-qwen/qwen3.8-27b)
查重结论:不重复。 已检索本仓 open+closed 的 issue/PR,没有针对「seed 重放后 ref 非单调 → preflight 每轮只折 ~5k」这一机制的在途 PR。你引用的几个是同家族不同入口:#2438/#5022/#4951 描述同一个终局态(超窗会话收敛不了、挂 10 分钟后失败)但机制是链 miss / unfold 爆炸;#2168 是 acp_status 显示乱序区间(已关),本单是折叠后果;#5370 是 D3「学不到真实计数」那一半;#629 是 fork 收养那一半。本单新增的机制独立成一条,我交叉引用上面几个、不关闭任何条目。
分层判断(已对照 v0.1.212 代码逐条核实,与你版本一致):
D1 你说对了,但根因比「不合并」多一层。 证据:
src/preflight.ts:2544-2547与2798-2801只做 start/end 交换,归一化后从不合并相邻区间。更底层的一条:LLM 路径(没配 externalSummary 时)每轮只折 1 个区间(src/preflight.ts:2675maxFoldsPerRound = emergencyFold ? Infinity : 1;你的 payload ~1.36× 窗口,远低于 emergency 阈值,所以走 LLM 路径)。两机制叠加才是「~5k/轮」。你的修法①(归一化后合并)方向对,但必须走 KDD#12 统一投影判定(protected-zone / turn-integrity carve-out),不能裸合并跨保护边界。D2 确认。 上限 32 轮(
MAX_PREFLIGHT_ROUNDS*2,src/preflight.ts:1848)。其实已有 futility bail(#1841,src/preflight.ts:2698-2725),但它是按剩余质量总量判、不是按「每轮吞吐 × 剩余轮数」判——碎片化让每轮只折 ~4.6k,而剩余总质量看着还很大,所以永远不触发提前退出。你的修法②(速率感知提前判死)打到了真缺口,也是最能止血的一条(去掉 10 分钟空转)。D3 措辞确实不实。 消息写死了「The over-window payload was NOT forwarded.」(
src/server.ts:4102),你有上游 400 硬证据。两种可能:①这条本地拒绝根本没拦住转发(bug);②宿主自己重试又发了一遍。需先追 plugin 模式下的实际转发路径再定修法——若是①就把拒绝做真(疏);若是②就改措辞 + 从 400 学回真实计数(#5370/#4093)。这半步涉行为变更,我追清再动。D4 确认,但是配置语义问题。 headroom 基准用的是模型原生窗口 1048576,不是你的
compress.modelContextLimit=450000(reserveOutputHeadroom拿到的 window=1048576,src/util.ts:593)。compress.modelContextLimit到底是「硬墙」还是「默认值(per-model 窗口可覆盖)」?现在是后者。改语义属配置面变更,需 owner 拍板,我先不动(选项单独发一条 🔶)。范围:四缺陷同属一个故障域(preflight 超窗收敛 + 失败时诚实报告),按「连贯修复不拆片」放一个 issue + 一个 PR 管理(内部子缺陷清单)。
计划:先修不需决策的 D2(速率感知提前退出)+ D1(归一化后安全合并)+ D3(诚实措辞/学计数);D4 走决策。开一个 PR 引用 GitHub #5885 带回归测试。
建议标签(本环境镜像 API 未开放打标端点,记此):
bug/dsh/scope/kernel/p1。理由:rewind 出的会话一旦可行区间榨干就永久不可用,且你试过的两个逃生杠杆——降 maxTokens 被 #3438 静默还原、设 modelContextLimit 被忽略——全失效,命中 p1「会话永久不可用」。fix: 超窗请求不再裸发上游 — 三道转发闸按「真实出站体」计量 + 32MiB 成本带准入 (#5000 #4543 #5885 #5794
ranxianglei commentedon Nov 8, 2026
诊断结论(2026-11-08)
这是啥问题:DSH rewind(fork+seed)之后,会话的 ref 编号空间变成非单调的;preflight 自动压缩提出的区间大量"落在已被块覆盖的区域里"(区间内没有任何可直接折叠的可见消息),于是每轮只折到 ~5k,283k 的缺口 31 轮折不完;期间还看到 "NOT forwarded" 提示——实际请求是照常转发的,只是那一轮没夹带自动折叠,提示词有误导性。
根因(机制链):rewind 重种历史时,ref 空间按新序号重新铸造,和已有块的 startRef/endRef 覆盖区间错位 → preflight 按 ref 区间选 span 时,span 的可见消息集为空(全是块覆盖)→ 内核合并阶梯对这种"纯覆盖 span"要么无可折(空转 no-op)要么报成对完整性错 → preflight 放弃该 span,只剩零头可折。状态是静态的,所以每轮折的量也稳定卡在 ~5k。
如何修复:
判定:DSH rewind 的 ref 重铸是根因;billion-context 侧无数据损坏、无阻塞性回归;#5929 合并后缓解。
URL: #5885 · 关联 #5919 / #5884 / #5789 · 缓解 PR #5929
Merge pull request #5932 from ranxianglei/2026-11-08_measured-outboun…
ranxianglei commentedon Nov 8, 2026
#5932(2026-11-08 合入)守卫了本 issue 的 defect 3:"NOT forwarded" 谎报的来源路径(#1839 仲裁臂、压缩后再检闸)现在先比真实出站体计量(#554 同源计量器)——实测超窗的体本地 413,不再为"upstream arbitration"发出。设计内的 #2313 取证探针保持原契约、独立留痕。
defect 4(
compress.modelContextLimit: 450000被窗口解析链忽略,headroom 仍按 1,048,576 算)已立项单独 issue:#5937。未修复:defect 1(non-monotonic refs 把折叠粒度降到 ~5k tok/round)、defect 2(31 轮上限算术上填不平 283,543 的 deficit,却仍跑满 649 s 才失败)。保持 open。
Merge upstream/master (fe963d7): adopt the measured outbound-body egr…
Merge pull request #5973 from ranxianglei/2026-11-09_gate-count-ingest
ranxianglei commentedon Nov 9, 2026
关闭说明(四项全部落账):
本 issue 的现场数据(2,057,076 tok、129-turn rewind、31×5k/649s)已分别写进 [Bug][compress] Non-monotonic references after rewind(fork+seed): folding granularity degrades to ~5k tokens/round, 31 r | [Bug][compress] rewind(fork+seed) 后引用非单调:折叠粒度退化到 ~5k token/轮,31 轮关不掉 283k 缺口 (#5885-①) #5977 / [Bug][config] Session-level compress.modelContextLimit is ignored by the window resolution chain: the compression condit | [Bug][config] 会话级 compress.modelContextLimit 被窗口解析链忽略:压缩判式按默认 1M 窗口算,450K 限制永不生效 (#5885-defect-4) #5937 的来源标记。
fix: 会话历史按「真实维度」拦死 — 条数闸(BILI_MAX_MESSAGES) + 32MiB 摄入成本带(BILI_INGEST…