Repository navigation
[PR] fix: pause compression nudges when folds stop shrinking the session - #5092
ranxianglei wants to merge 19 commits into
Conversation
…5058) Companion-style long sessions hit a pure positive feedback loop: every nudge round ends in a successful compress whose own action cost (tool-call args, receipts, reasoning — all persisted by the host and re-sent next turn) outweighs what the fold removed, so the flat-50K growth gate re-arms forever and the session walks past its window while every standing guard stays silent (the #2748/#3322 gate judges one commit in isolation, the #2146 breaker counts failures, #2155 D1 needs reductions to STOP landing, #3461 covers host emergency injections only). Add the cross-round trajectory view: recordNetProgressBaseline at each successful fold commit (live-view tokens + post-fold estimate, same kernel counter space per #1911); evaluateNetProgressRound once per nudge-carrying request compares the client's raw resend against pre-fold-level-minus-margin (25_000 = half the flat step). Two consecutive non-progress rounds latch a per-session nudge suppression through nudgeInjectionBlocked at all four lane gates (decide-ladder fallback punch-through preserved, #3004 deadlock guard shared with D1); any demonstrable reduction lifts it early and the SELF_HEAL_SUPPRESS_CAP_MS wall cap bounds silence. Armed state surfaces on /__bili/sessions (netProgress) and as a Web UI badge; arm/recovery/expiry log [net-progress] lines. No config surface — constants by design (#2155 precedent). Tests: tests/issue5058-net-progress-guard.test.ts (strike state machine, boundary, wall cap, fresh-window reset, corrupt-state tolerance, OR semantics against the #2155 latch).
) CLIENTS.md (en+zh): the pre-send local refusal failure mode for long-lived plugin-mode sessions (dsh's overflow guard measures its own unpruned local store, so once it exceeds the window no traffic reaches bili and no automatic recovery can fire), the known refusal wording for host-side detection regexes, and the rotation / nudgeGrowthTokens / file-size-watch mitigations plus a pointer at #5052. TECHNICAL-NOTES.md: net-progress guard constants and semantics (baseline/strike/margin/wall-cap), alongside the #2155 self-heal section.
📦 Built Package ArtifactBranch: Latest build — install this exact versionnpm install -g billion-context@0.0.7163-pr.5092.7e06930This exact version is immutable and reproducible. The rolling Recent builds (commit → version)
Download artifact
tar xzf billion-context-pr5092.tgz
npm install -g packageInstall instructions are refreshed on each push. |
(ai: vllm-qwen/qwen3.8-27b) 查重:不重复。 本 PR 是 #5058(陪伴类长会话 nudge 自我放大直到超窗永久报废)的修复单;open+closed issue/PR 全量检索无同机制的并行工作(跨轮净进展守卫 / nudge 暂停)。相关条目:#5033(同死锁模式不同机制,不并案)、#5052(关联功能请求)。 这是已提交的修复 PR(#5058 线程 floor 27),按规则不重做 —— 现在做独立审核:重新通读全量 diff、审既有行为回归、本地定向测试、核对 pushed head 的 CI 状态。完成后按结构化八问回复结论。 |
(ai: vllm-qwen/qwen3.8-27b) 进度汇报(进程曾被重启,已恢复继续):独立审核进行中。已完成:通读新模块 src/net-progress-guard.ts(140 行)与全部接线 diff(handle.ts / stream.ts / lane-decisions.ts / prepare-*×4 / web 三件)。正在核实一个关键前提 —— 「基线与轮次测量同 token 空间」:stream.ts 的基线记在 |
recordNetProgressBaseline ran on EVERY successful fold commit and wrote session.metadata["netProgressGuard"]; persist serializes session.metadata into the state file, so every long-session storage golden drifted its persisted-state digest deterministically on all three CI platforms (the six red test shards at b88abf4: storage-engine-proofs full-mode restart x4 wires + golden-storage-render x4 wires). The latch now lives process-local in a WeakMap keyed by the session object (#4790 precedent - same mechanism family, same fix shape): entries die with their session object, an evicted-and-reloaded session re-seeds silently, which is harmless because the latch self-expires within SELF_HEAL_SUPPRESS_CAP_MS (10 min) anyway, and a fresh arm needs one more successful fold commit plus at most NET_PROGRESS_STRIKE_LIMIT non-progress rounds. /__bili/sessions netProgress reads the live store (badge visible while the session object is live; disk rows do not badge). Adds a regression pin asserting session.metadata stays untouched across every mutation path.
(ai: vllm-qwen/qwen3.8-27b) CI 红已修复,全车道绿。 最终 head 八问:
行为变更披露(本 PR 两个 commit 之间,旧值→新值+原因):latch 状态原设计随 验证链:本地复现红(digest 漂移值与 CI 一致)→ 修复后定向套件绿 → push → 远端 CI 全绿(判定以 CI 为准,本地未跑全量)。结论:可合并(请人工合并 #5092 );#4599 已留一行结论。 |
(ai: vllm-qwen/qwen3.8-27b) 这条失败通知是过期重复转发,无需再修。 它指向的 head 仍是
详细八问见上一条回复。结论不变:可合并(请人工合并 #5092 )。不重复开 issue、不做重复修复。 |
(ai: vllm-qwen/qwen3.8-27b) [bot] 进程被重启前的进度汇报:本 issue 的工作已全部完成,无阻塞。
当前无待办:等人工合并 #5092 。若合并后有新事件(如 owner 评论或新一轮 CI),再按新指令处理。 |
…in client-resend space Review corrections for the #5092 guard (two facets of one measurement integrity defect): 1. Lane scope — plugin lanes only (session.metadata.pluginAgent bound). Only plugin hosts persist and re-send the compression action itself, so only there does the resent payload shrink when a fold works. Proxy-mode clients keep their full local history and re-send it verbatim every turn; once folded coverage exceeds the 25K margin the raw resend structurally never drops below pre-fold-minus-margin, so an unscoped latch would arm on every mature proxy session and silence the flat-50K nudge cadence (KDD #8) without defending anything. The #5058 swelling domain is plugin-mode only. 2. Same-space baseline — recordNetProgressBaseline now takes the inbound RESEND total (stream.ts rawViewTokens — same array class as the round check counts over prepared.originalMessages) instead of the rendered live view. Mixing spaces would distort the bar by the reasoning-drop delta on heavy-reasoning hosts. postCtx stays the rendered post-fold estimate (diagnostic only). Tests: proxy-mode negative row (guard stays inert, cadence untouched); persistence pin updated. TECHNICAL-NOTES.md: lane-scope + baseline-space bullets.
(ai: vllm-qwen/qwen3.8-27b) 独立审核结论:方向正确,发现并当场修复两处测量缺陷——其中一处是原实现引入的未披露回归(proxy 模式平 50K nudge 节奏被整体掐灭),已把守卫收窄到插件车道。 diff 全部 15 个文件逐行读过(未沿用 issue 侧结论),四个既有防御机制的漏法逐一验证。 ① 方向关 ✅:#5058 根因成立——压缩动作本身(tool-call 参数含摘要全文、回执、回合推理)被插件宿主持久化进历史、每轮重发,fold 省得比动作花得少时纯正反馈不收敛;kernel 每次成功 compress 后重置基线,下一次 nudge 永远再隔一个平 50K 步到来。既有四道防御(#2748/#3322 单轮净收益、#2146 失败断路器、#2155 自愈 D1、#3461 紧急退避)各按设计恰好漏掉这个形状。加跨轮轨迹视图方向对;补救用抑制不用删除,#3004 死锁逃生口保留。 ② 回归审计——发现原实现一处未披露回归(已修):baseline 记的是渲染活视图(stream.ts ②b 第二处测量空间缺陷(顺手修):即便插件车道内,baseline 也应与观测同一空间(客户端重发量),否则重推理宿主会被 reasoning-drop delta 扭曲门槛。stream.ts 改传 ③ 代码关 ✅:状态机逻辑、常量(margin 25K=半步,吸收估算噪声+一轮物化滞后;2 strikes)、wall cap 复用 SELF_HEAL_SUPPRESS_CAP_MS(10min)、与 #2155 的 OR 语义、Web 徽章/i18n 均无问题。作者自推的 3f4b7ef(latch 从 session.metadata 移入进程本地 WeakMap,#4790 先例,修 storage golden drift 红)方向正确,两套 storage 套件本地绿。 ④ 测试有效性:单测有牙(严格 八问:
行为披露(old→new):PR 正文称守卫作用于「all sessions」、baseline 为「live-view token total」——修正后实际交付 = 仅插件车道 + baseline = 客户端重发量。原因:proxy 客户端每轮重发完整历史且 fold 不缩小它,套用会纯伤害(KDD #8 节奏被掐);异空间测量在重推理宿主上扭曲门槛。 当前状态与合并判定:修正 commit 本地就绪(定向测试+typecheck 全绿),但推送暂被本环境到 github.com 的网络故障阻塞(Connection reset by peer,含只读 ls-remote 在内多次重试约 10 分钟失败,判断为瞬时出口故障而非凭据问题)。注意:修正第一版 98dc0ba 已推上去,其中新增的 proxy 负例有一个 JS 默认参数坑(undefined 触发默认值)会在 CI 显示 1 红;修正版 6a57f3db4 网络恢复后立即推上并补说明,届时以 CI 为准。CI 绿后建议合并。超范围项 A/B/C(summaryMaxChars clamp / DSH 上游诉求 / compaction 归属策略)已在 PR 正文列出待你拍板,不阻塞本 PR 合并。 |
(ai: vllm-qwen/qwen3.8-27b) 补充状态:到 github.com 的出口网络故障仍在持续(约 25 分钟,只读探测同样失败),修正版 commit |
…inel for unbound fakes
CI (3 shards, same subtest): 'proxy-mode sessions are out of scope' expected no evidence window but got {baseline: 600000, expectedFloor: 120000, strikes: 0}. Root cause is the test helper, not the guard: a JS default argument fires when the passed value IS undefined, so mkSession(undefined) hits the '= "dsh"' default and produces metadata {pluginAgent: "dsh"} — exactly the binding that arms the guard. The implementation already gates on session.metadata.pluginAgent per spec (plugin lanes only), so the proxy-mode row needs a sentinel that survives the signature: null.
|
(ai: sglang-gpu0/qwen3.8-27b) CI 三个 shard 全红的是同一条子测试( 根因不在守卫实现,在测试的假会话构造器。 最新提交已经把守卫门控在 修法:测试 helper 改用 本地验证: old→new:此前这条本应证明「proxy 模式不 arm」的 CI 行其实一直在 arm 一个 dsh 绑定的假会话;现在它真的构造无绑定会话并证实守卫不碰、nudge flat-50K 节奏不受影响。插件车道行为不变。新提交 4fc74f1,请对新提交看 CI。 |
…uires (#4599) The #5140 recovery test proves forkSnapshot reconstructs a non-retained parent's raw tail from the UNIFIED index — but it never pinned its own storage mode. Under the CI storage baseline (preload-storage-baseline.mjs sets BILI_STORAGE_UNIFIED=legacy for the broad corpus) no unified rows exist, so the reconstruction has no source and forkSnapshot fails closed with 409 SNAPSHOT_UNAVAILABLE. Deterministic, not a flake: reproduces 100% on master and any merged branch with BILI_STORAGE_UNIFIED=legacy, passes without it (default shadow). Pin full like the sibling persist tests and restore the ambient value afterwards.
(ai: vllm-qwen/qwen3.8-27b) No new merge needed for this trigger: master has not advanced past the previous sync — live master is still |
ci-fixes at 67f8e89 still leaves four Windows shards red, and all four deltas already exist on PR #5178. This ports the missing hunks so the convergence branch actually converges. 1. `test (windows-latest, 2x, 3)` — #5103. `UnifiedShadow` builds its db path as `${dir}/index.db`, so the open-failure warning reads `C:\...\sessions/index.db`. `issue5103` asserts the warning contains `path.join(dir, "index.db")` — `...\index.db` on win32 — so a mixed separator never matches. `path` is already imported at :30 and the db is opened through the same join, so POSIX output is byte-identical. 2. `test (windows-latest, 22, 4)` + the 4788 leg — #4739/#4788 teardown. The suites arm a real `SessionStorage` under `BILI_STORAGE_UNIFIED=full`, so the sidecar is still open at teardown. `cancelAll()` clears timers and the build map but never closes the shadow; Windows refuses to unlink a directory holding an open file (`EBUSY`/`EPERM` on `index.db`). Use `closeQuietly()` — cancel, then close — in the short-lived-store teardown. 3. `test (ubuntu-latest, 24, 3)` + `(windows-latest, 22, 2)` — #5155. Both cold-rebuild cases waited for the debounced durable flush with a fixed `setTimeout(20)`. A millisecond budget is a deadline a loaded runner can miss, and the miss is an honest `409 !== 200` from a one-turn-stale table. Await the settle itself: `afterNextFlush("conv-ptrs")` resolves once the armed save lands. 4. `test (ubuntu-latest, 22, 3)` — #4788 verdict deadlock. `afterNextFlush` parks a resolver in `flushWaiters`, and the only thing that drains it is the debounce timer — which is armed `unref?.()`. Once that timer is the last pending handle the loop drains, the promise never settles, and node reports `Promise resolution is still pending but the event loop has already resolved`. Keep the timer referenced exactly while a waiter exists (`ref()` on waiter registration; the host-upkeep default stays unref'd). Both view hunks and the two test-teardown hunks are byte-identical to the ones already on PR #5178, so this converges the branches instead of inventing a third variant. Verified locally: `npx tsc --noEmit` clean; issue4788 0/4 -> 4/4; issue4739 4/4; issue5103 5/5; inc-canonical-pointers 3/3. The `cancelAll` -> `closeQuietly` hunk has no local negative control: reverting it keeps Linux green because `EBUSY`/`EPERM` only fire where an open handle blocks unlink. That control can only run on a Windows lane — stated rather than papered over. The #4788 arm's own reader is confirmed instead: `cancelAll()` leaves `index.db`, `index.db-shm`, `index.db-wal` in `/proc/self/fd` while `closeQuietly()` leaves none (probed directly).
…e settle (#4599) The turn() helper arms scheduleSave whose debounce timer is unref by design (persist.ts: a pending save must not hold the process open at shutdown) and then awaits afterNextFlush. Nothing else in that await is refd, so in a node:test child whose only surviving handles are unref sockets the event loop drains before the 50 ms debounce fires and the settle promise never resolves: all four subtests fail with "Promise resolution is still pending but the event loop has already resolved" / cancelledByParent. Reproduced deterministically on master 44067a5 AND on pre-merge ci-fixes 67f8e89 (standalone run, exact CI preload chain), while the same file has passed some CI legs by ambient-liveness luck - a born-flake per section 7.2 (no environmental luck). The pin at 67f8e89 does not touch this path. Fix: arm a refd 25 ms keep-alive interval around the afterNextFlush await (cleared in finally) so the debounce fires deterministically on every host. Test-only diff; production settle semantics untouched.
…ts ref-tracking fix (#4599) PR #5262 fixes #4788 at the source: scheduleSave now leaves the debounce timer refd while flush waiters exist (and afterNextFlush refs a pending unrefd timer when a waiter registers), so the settle is deterministic for EVERY caller on every host. The 25 ms keep-alive interval added in a3019b0 only patched this one test file and is redundant now; removing it leaves one mechanism instead of two. The comment stays: without it a future reader re-adds the interval or deletes the persist.ts tracking as dead weight.
Closes #5058
What this fixes
Companion-style long sessions (hundreds of turns, days) hit a pure positive feedback loop in plugin mode: every nudge round ends in a successful compress whose own action cost (tool-call args incl. summary text, receipts, turn reasoning — all persisted by the host and re-sent next turn) outweighs what the fold removed. The kernel's baseline reset after each successful commit makes the next nudge due another flat 50K step later, and the action re-trips the growth gate — no convergence. In the reported session (0.1.208, dsh-native, deepseek-flash 750K window) that walked the host's local store to 30 MB / 24,051 events until dsh's pre-send overflow guard refused everything locally: no provider error, so no automatic recovery anywhere could fire, and the session was permanently bricked.
Every standing guard misses this shape by design:
How it works
New
src/net-progress-guard.tsadds the trajectory view:recordNetProgressBaseline,src/stream.ts): live-view token total + rendered post-fold estimate, both from the same kernel counter over the same request (postCtx≈0 的退化折叠会摧毁缓存前缀(shrink 287%/129%,cache ceiling ≥0%) #1911 same-space discipline). A new fold opens a fresh evidence window; strikes reset.evaluateNetProgressRound,src/server/handle.ts, inside the existing!selfHealRoundDone && prepared.nudge !== undefinedblock): the client's raw resend (countMessageTokensoverprepared.originalMessages) below pre-fold-level-minus-margin (NET_PROGRESS_MARGIN_TOKENS= 25_000, half the flat step — absorbs estimator noise and the one-round materialization lag where the post-compress re-request still carries the unfolded history, fix: net post-compress usage credit + hand launcher context windows to the proxy (#252) #252) counts as progress and resets; otherwise a strike.NET_PROGRESS_STRIKE_LIMIT= 2 consecutive non-progress rounds arms.nudgeInjectionBlocked(session)(the 会话卡死自愈机制:卡死模式检测与代理侧主动补救 #2155 predicate OR'd with this latch) at all four lane gates — the decide-ladder fallback punch-through is preserved (same dsh上下文700K不触发压缩 #3004 deadlock guard as D1). Latch state{ baseline, expectedFloor, strikes, armedAt }kept PROCESS-LOCAL in a WeakMap keyed by the session object — never persisted: persist serializes session.metadata into the state file, and this guard fires on every successful fold commit, so a persisted latch would drift the byte-for-byte storage goldens on every long session ([PR] Warn when the outbound bili tool set flips mid-session (#4789) #4790 precedent, same mechanism family). Wall expiry reusesSELF_HEAL_SUPPRESS_CAP_MS(10 min); any real reduction lifts it early; an evicted-and-reloaded session re-seeds silently (a fresh arm needs one more successful fold plus at most 2 non-progress rounds). Evaluate runs while suppressed so recovery can be observed./__bili/sessions(netProgress) + Web UI badge ("swelling"); arm/recovery/expiry log[net-progress]lines with the observed-vs-baseline numbers and rotate-to-fresh-session advice.Docs: CLIENTS.md (en+zh) dsh section documents the overflow-wall failure mode, the known refusal wording for host-side detection regexes, and the mitigations (rotation /
compress.nudgeGrowthTokens/ file-size watch / #5052 pointer); TECHNICAL-NOTES.md records the constants beside the #2155 section.Behavior change (old → new)
Sessions whose folds stop shrinking them now get their compression nudges PAUSED after two consecutive non-progress rounds (old: nudges continued forever, each compress enlarging the session further). The pause lifts on the first round that demonstrably reduces the context below pre-fold-level-minus-margin, or after 10 minutes wall-clock. Healthy sessions are unaffected: any fold buying back more than ~25K tokens of growth counts as progress immediately. This is a suppression (the decide ladder's fallback escape still punches through one legacy advisory), not a removal of the compression surface.
Config surface
None. Two named module constants by design (#2155 precedent: guard thresholds are internals, not operator knobs). Operators who want a lazier cadence for companion sessions already have
compress.nudgeGrowthTokens.Verification
tests/issue5058-net-progress-guard.test.ts(strike state machine, boundary, wall cap, fresh-window reset, corrupt-state tolerance, OR semantics vs the 会话卡死自愈机制:卡死模式检测与代理侧主动补救 #2155 latch) +tests/issue2155-self-heal.test.ts+tests/lane-decisions-matrix.test.ts+tests/web-i18n.test.ts— all green (123 tests); typecheck clean; build clean (dist-external OK). Full-suite verdict from CI on this head.Out of scope (tracked separately)
summaryMaxChars(behavior change vs the documented "pass summaryMaxChars to increase the limit") — decision item A in the issue thread.CI fix after first push (head b88abf4 was red)
The first commit stored the latch in
session.metadata, which persist serializes into the state file — every long-session storage golden drifted its persisted-state digest deterministically on all three CI platforms (six red test shards:storage-engine-proofsfull-mode restart x4 wires +golden-storage-renderx4 wires; master itself green). Follow-up commit3f4b7efe3keeps the state process-local (WeakMap keyed by the session object, #4790 precedent) and adds a regression pin assertingsession.metadatastays untouched across every mutation path. Behavior delta vs the first commit: the latch no longer survives a proxy restart (it self-expires within 10 min anyway), and the/__bili/sessionsnetProgressbadge shows live sessions only.