Skip to content

[Bug] Evict watermark sessions incrementally at cache capacity #6044

Closed

Description

@Afloat16

Version

billion-context 0.1.213, master 4be21b08ed3da8b69d1800a10a9c0b9c5a707f71.

Problem

Admitting session 1,025 clears every existing watermark handshake.

Proposed correction

Evict only the least recently used session and refresh recency when a known session is captured or used.

Regression coverage

A full 1,024-session table retains all but the oldest entry, refreshes recency on emit/capture/learn, and does not allocate slots for unknown reads.

Validation

Passed against exact fork commit ffb5a3dcf0ecbf07a6d28e3db54a64bd05193fa8 in an isolated GitHub Actions checkout: validation job. The targeted regression and related suites, npm run typecheck, and npm run build all passed on Node 22. This is targeted verification, not the full upstream CI matrix.

node --import ./tests/preload-storage-baseline.mjs --import ./tests/preload-test-env.mjs --import tsx --test tests/watermark-cache-eviction.test.ts tests/pf-channel.test.ts tests/msgid-rule-parity.test.ts
npm run typecheck
npm run build

Related work checked

Inspected pending protocol PRs #5652 and #5855; both leave the full-clear capacity behavior unchanged.

Activity

  1. ranxianglei commented on Nov 9, 2026

    @ranxianglei
    Owner

    🤖 Powered by ework · qwen3.8-27b

    (ai: vllm-qwen/qwen3.8-27b)

    Duplicate screening: not a duplicate. One open PR targets this exact thread — your own #6054 (same symptom, same mechanism, Closes #6044). Per the one-fix-PR-per-thread rule I adopted and reviewed it instead of opening a parallel PR. A repo-wide sweep for other work on the pf-channel capacity behavior found none: the nearest neighbors are distinct problems (#5813 = count header never emitted due to pre-merge headers; #5855 = fastpath-v6 re-land, which you already checked leaves clear-all untouched).

    Verification (bug confirmed at master 4be21b08, which is also the current master tip — the PR sits 0 commits behind):

    • Root cause is exactly where you pointed: src/agent/pf-channel.ts takeSlot() used tracked.clear() as its capacity policy — the 1,025th distinct conversation in one host process wiped all 1,024 live handshakes, forcing every active session back onto the legacy full-parse path until each got restamped. That's a capacity-strategy defect in the client-side state machine, not a caller or wire problem — your diagnosis hits the mechanism directly.
    • The LRU fix is complete at the single chokepoint: all consumers (the native-intercept emit/learn sites plus the dsh/pi/omp/opencode capture adapters) funnel through the same three entry points, so no other code path carries the old clear-all assumption. Wire format is byte-identical; proxy mode is untouched (the module is plugin-lane client-side by design).

    Review of #6054 + what I did on it:

    • Ran the targeted validation locally on the PR head: new eviction test + existing pf-channel suite + msgid-rule-parity = 18/18 pass, npm run build OK, npm run typecheck clean.
    • Test quality check: the regression test has teeth — if clear-all regressed, the retain loop fails on every surviving session; if unknown reads started allocating slots, the 2,000–3,999 probe loop would evict all real sessions and the assertions would blow up. Deterministic, no ports or env luck.
    • Added one comment-only commit (168e8ca1cd) to the PR branch: the MAX_TRACKED constant's comment still described the old "same hygiene as the old per-lane maps" rationale after the policy changed; it now names the evict-one-LRU behavior. Zero behavioral change.
    • Labeled PR [PR] fix: evict watermark sessions incrementally at cache capacity #6054 from its own diff: bug / p2 / scope/wiring (degradation only under >1,024 concurrent watermark sessions with graceful fallback → p2, not p1; host↔bili handshake wiring, not kernel/storage). Mirror issue labeled to match.

    Live status (checked minutes ago): CI on the updated head 168e8ca1cd has 53 runs registered, zero failures so far (zero-token e2e lanes skip immediately; the test lanes are still running).


    结构化八问:

    1. 是什么问题:客户端水位握手表(每宿主进程 1,024 会话上限)在接纳第 1,025 个会话时清空整张表——所有活跃会话的水位握手全部丢失,后续轮次全部退回全量解析旧路径,直到逐个重新盖戳。
    2. 什么原因导致的:容量管理策略本身——takeSlot() 用 tracked.clear()(清全部)而不是逐出单条。根因在状态机的容量策略层,不在任何调用方或线上协议;报告者定位准确。
    3. 影响范围与严重程度:仅当单个宿主进程并发 >1,024 个不同会话时触发(典型如 dsh 多会话 profile);触发后所有活跃会话降级为 legacy 全量解析(每个会话多一轮往返重新盖戳)。可用性不受损、降级路径完好 → p2,非 p1(不是不可用、不是缓存泄露类)。
    4. 怎么修复的:以 Map 插入序作为 LRU 账本:已知会话在 emit/capture/learn 三个触点重插到尾部刷新近度;表满且新会话到来时只逐出最旧一条;未知 sid 的读永远不分配槽位。单一咽喉点(native-intercept + 四车道采集适配器共用同一组入口),无遗漏路径。
    5. 是否有回归问题:逐项审过既有行为——未知读返回 {} 且不分配(不变,新测试 2000–3999 探针环钉死);已盖戳会话发出的头字节级不变(既有套件 12/12 绿);T=0 戳的存储语义不变;线格式不变;proxy 模式不涉及。有意变更两处均已披露:容量行为 clear-all→逐出一条;emit/capture/learn 新增近度刷新。本地定向验证 18/18 + typecheck + build 全绿。
    6. 是否破坏缓存:不。被修的这个表本身就是缓存,服务端缓存/重试/去重键均未触碰;被逐出的会话下一轮响应即重新盖戳(设计上的一轮往返成本,正是修复目的)。
    7. 是否改了设计方向:否——同模块、同三入口、同线契约,只换内部逐出策略。
    8. 是否引入了新的配置:无。

    判定:#6054 可合(待 CI 转绿;当前运行中、截至检查时无失败项)。合并为人工操作。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions