Skip to content

fix: 释放会话派生缓存与诊断请求底层字符串 - #2923

Merged
ranxianglei merged 1 commit into
ranxianglei:masterfrom
overFloweee:2026-10-12_bounded-history-memory
Oct 12, 2026
Merged

ranxianglei merged 1 commit into
ranxianglei:masterfrom
overFloweee:2026-10-12_bounded-history-memory

Conversation

@overFloweee

Copy link
Copy Markdown
Contributor

当插件工具视图保存在进程级 Map、缓存诊断请求保存在会话外 WeakMap 时,会话淘汰和驻留字节计量不能完整覆盖它们。此外,512 KiB 的诊断截段可能保留整个大请求的 V8 底层字符串。

这次把工具视图、待结算和已结算诊断请求移入 Session.transient,让它们随会话释放,并让会话对象图的字节计量可以看到这些缓存。诊断截段及有上限的完整副本使用独立 UTF-16 拷贝,保留孤立 surrogate、原来的消息数、摘要、认证指纹和结算行为。持久化仍使用现有显式投影;原文、图片、fork revision 和上游协议格式不变。

Fixes #2920.

与已有内存修复的关系

补充 #2803,复用其已获 owner 批准的按需加载和驻留预算,不重复实现启动索引、预算配置或持久化架构。当前 master 上本 PR 已能解除诊断字符串的底层保留,并使会话删除立即释放工具原文;统一的驻留字节预算依赖 #2803。两者在隔离 worktree 中三方合并成功,并验证插件视图与诊断副本都触发预算淘汰、执行中的会话仍固定。

没有新增配置、依赖、kernel 修改、版本变更或持久化迁移。没有修改或重启本机正在运行的 bili。

验证

  • typecheck、build、git diff --check 通过;基于当前 upstream/master(9972ff2d)。
  • 5 项新增测试覆盖 UTF-16 内容、结算引用转移、持久化原文恢复、删除后禁止重挂工具快照、截段/完整副本的底层字符串释放。
  • 同源合成内存 A/B:8 × 12 MiB 请求的诊断保留量从 100,701,552 bytes 降到约 4.0 MiB;删除 8 个会话后,插件原文保留量从 33,554,216 bytes 降到近零,并实际释放约 32 MiB。统计为强制 GC 后 heapUsed + external,不等同进程 RSS/footprint。
  • 117 项缓存诊断、持久化、Codex Responses 回归通过;Bound startup session loading and preserve historical recovery #2803 组合的 64 项加载/预算/fork/内存回归通过。
  • 真实 Codex CLI + 本地模拟上游,以及图片计费端到端测试:13 项通过,使用隔离 home 和合成内容,无真实模型调用。
  • 最终提交 3823e827 的 npm test:6499 项,6468 通过、29 跳过、2 失败。失败仅为 tests/main-path-transport-retry.test.ts 的 主模型请求路径缺少 fail-fast 传输重试:一次 DNS 抖动即打断整轮(acp-loop/preflight 已有 #1263 重试,主路径不对称) #1688 DNS 预算断言、tests/tunnel-guard.test.ts 的 403 = 请求被服务端基于权限拒绝 导致任务打断 #1686 unresolvable 断言;两项均在未改 master 基线上复现,本机返回 fetch failed / other side closed 而非 ENOTFOUND/EAI_AGAIN。没有新增失败;没有把全量套件报成全绿。

边界

没有做生产会话 heapdump、长期 8GB/OOM 压测或付费真实模型 E2E;上述合成数据证明这两条保留路径得到修复,不能单独证明整个代理的驻留内存问题已经全部解决。已做两轮自查,重点检查调用方、fork、恢复投影及字符串保真。

@ranxianglei

Copy link
Copy Markdown
Owner

🤖 Powered by ework · qwen3.8-27b

[bot] 🏷 收到,开始 review。初步状态:PR 基于当前 upstream/master(9972ff2d),单提交 3823e827,diff 收敛在 8 个文件(+186/−35):src/plugin.ts / src/cache-ledger.ts / src/session.ts + 新测试 tests/session-transient-memory.test.ts,无 kernel、无配置、无版本变更——与 #2920 拟修复范围一致。正在逐文件精读 + 定向验证,结论按八问体例回帖。

@ranxianglei

Copy link
Copy Markdown
Owner

🤖 Powered by ework · qwen3.8-27b

[bot] 🏷 Review 完成。结论先行:可合并(建议人工合并——跨 3 个源模块,不满足 §7.4 自动合并的「单模块」条;其余门禁项全过)。按八问作答:

1. 是什么问题:两条会话派生缓存绕过了会话生命周期与驻留计量——插件工具视图存在模块级 Map(淘汰后仍强引用到下一次 remember),诊断请求副本存在模块级 WeakMap(随对象回收但不可见);且 512 KiB 诊断截段是 V8 SlicedString 视图,钉住整个大请求的底层字符串。

2. 什么原因导致的:存储位置与所有权错位——派生数据挂在「进程级容器」而非「被派生的 Session」上,导致 (a) 释放时机晚于会话淘汰(plugin 视图要等下一次任意 remember 的清扫),(b) 对象图字节计量遍历不到(#2803 的 sessionWeight() 递归走 Session 自有属性),(c) slice() 在 V8 里是共享 backing store 的视图而非拷贝,截断不释放。根本原因是一处机制:派生缓存的所有权不属于会话。

3. 影响范围与严重程度:长会话/大会话代理进程的驻留内存——OOM 前置因素(#2920 报告了 ~4 GiB heap OOM)。触发条件:插件模式会话(工具视图)+ 任何转发请求(诊断副本)+ 大 body(>512 KiB 时截段钉住全量)。不是功能故障,是内存保留缺陷。

4. 怎么修复的:所有权归位——三类派生数据移入 Session.transient(显式 in-memory-only 字段),随会话对象释放、自动进入对象图计量;诊断截段与有界 full 副本改为独立 UTF-16LE 往返拷贝切断 SlicedString 视图(code-unit 级保真,孤立 surrogate 不变形,SHA-256 仍基于完整原 body)。不引入第二套缓存/配置/磁盘格式,不动 kernel。

5. 是否有回归问题:逐项审过既有行为——

  • seam 诊断管线(note→detectSeam LCP→settle 转移 sent→settled):全部读写点已迁移,digest/msgs/keyFp 继承语义逐行核对不变 ✅
  • [bug] #388 的侧请求隔离只认「输出预算 ≤ 200」,1 条消息的审查请求冲掉 remembered 快照,长会话里 plugin 路径的 compress 全败(Requested range(s) cannot be anchored) #1307 零重叠守护、fork 路径(child 挂自己的 transient)、status/tool 读路径回退链(mem→pluginSnapshot)✅
  • 持久化:buildRecord 是显式投影(src/persist.ts:1096 起逐字段列举),transient 不在其中;新测试钉住 restored.transient === undefined ✅
  • wire/消息形状、config/env、kernel、version、lockfile:均未触碰 ✅
  • 有意行为变更(披露):① rememberPluginMessages 在无存活 session 时从「照写模块 Map」改为直接 no-op——生产唯一调用点(src/server/handle.ts:2225,session lock 内)恒有活会话,且旧行为让同 id 复活会话看到死会话的陈旧视图,新行为更贴合 Subagent first token stalls 1-2min: withSessionLock covers full forward + slow flash upstream first byte #970 单写者纪律;② str/full 现在总是独立拷贝(小 body 也走 utf16le 往返)——每次转发多 ≤1 MiB 有界分配,digest 本已 O(n) 扫全量,量级不变。
  • 本地定向验证:typecheck 干净;25 个受影响测试文件 234 项全过;在未改 master 基线(9972ff2d)worktree 复跑新测试 5 项全失败,实测基线保留量:诊断截段 100,700,656 bytes(≈96 MiB)、插件原文淘汰后仍留 33,572,816 bytes(released 仅 17 KB)——与 会话外缓存绕过 resident budget,诊断 substring 保留完整请求 #2920 报告及 PR 描述数字一致;PR head 上分别为 4,232,288 / 14,024 bytes。回归测试真实复现原 bug ✅
  • CI 全绿(e2e-codex-persona / e2e-fake-codex / e2e-dsh-plugin-lane / e2e-native-dsh / ci-image / Theorem Lane 等 10 条 lane 均 success),PR mergeable=clean。

6. 是否破坏缓存:不影响前缀缓存/重试/去重键。seam slot 的 digest 语义不变(LCP 归因、be 标记逻辑未动);conversations LRU Map 未动。唯一变化是诊断副本的字节来源(别名→独立拷贝),内容逐 code unit 相等。

7. 是否改了设计方向:没有。是 #2803 已获批准方向的补全(cache ownership 归位),明确不重复其启动索引/warm 集/budget 本体。注意:「统一驻留字节预算能看到这些缓存」这一半收益以 #2803 合入为前提(master 上尚无 sessionWeight());本 PR 单独合入即生效的是「淘汰即释放 + 底层字符串释放」。PR 描述对此表述诚实,无夸大。

8. 是否引入了新的配置:无新 env/config/CLI/默认值。scripts/run-ci-tests.mjs 把新 spawn 测试登记进 LIVE_FILES(并发纪律正确做法)。

测试有效性:5 项新测试断言有牙——两项 heap probe 在基线上真实复现(我独立复跑确认失败模式就是内存断言,非导入错误),阈值留 ~4× 余量(16 MiB 上限 vs 实测 4.2 MiB;24 MiB 释放下限 vs 实测 33.5 MiB),forced-GC 测量在重载 CI 上有轻微抖动风险但余量足够;UTF-16 保真测试用尾部孤立 \ud800 钉住了 surrogate 边界。持久化测试同时钉住「不落盘」和「淘汰后禁止重挂」。

diff 整洁度:干净。8 文件 +186/−35,每行都服务于该机制;architecture.md 新增一条模块说明符合文档纪律;无空行/重排噪音。

查重结论:非重复。open PR 中引用 #2920 的仅此一件;与 #2803/#2776 的关系是声明过的互补(ownership vs budget),范围不重叠。

一处需作者处理的小修(review 发现,分支在外部 fork 我无权直推):src/plugin.ts:331 注释 // remembered[sessionId] is intentionally left alone here: 引用了本 PR 删除的模块级 remembered,建议改为 // session.transient.pluginMessages is intentionally left alone here:(注释意图本身仍然成立,只是标识符悬空)。若合并时已顺手改掉请忽略。

判定:可合并。合并后 #2920 可关(PR 带 Fixes #2920)。

@ranxianglei
ranxianglei merged commit f8f7d40 into ranxianglei:master Oct 12, 2026
33 checks passed
@github-actions

Copy link
Copy Markdown
Contributor

📦 Merged Build Published

This merge to master was published as 0.0.325-master.f8f7d40 (rolling master tag).

npm install -g billion-context@0.0.325-master.f8f7d40  # this exact build
npm install -g billion-context@master  # always the newest merged build

npm view billion-context@master prints the current version at any time.

ranxianglei pushed a commit that referenced this pull request Oct 12, 2026
The retention assertions now read the UTF-8 encoding back out of the
Buffer slot (byte-equal for valid UTF-8 bodies), and a new test pins
that a lone surrogate is retained as U+FFFD — it has no UTF-8 form.
Heap-probe thresholds unchanged.
ranxianglei pushed a commit that referenced this pull request Oct 13, 2026
The retention assertions now read the UTF-8 encoding back out of the
Buffer slot (byte-equal for valid UTF-8 bodies), and a new test pins
that a lone surrogate is retained as U+FFFD — it has no UTF-8 form.
Heap-probe thresholds unchanged.
ranxianglei pushed a commit that referenced this pull request Oct 13, 2026
The retention assertions now read the UTF-8 encoding back out of the
Buffer slot (byte-equal for valid UTF-8 bodies), and a new test pins
that a lone surrogate is retained as U+FFFD — it has no UTF-8 form.
Heap-probe thresholds unchanged.
ranxianglei pushed a commit that referenced this pull request Oct 14, 2026
The retention assertions now read the UTF-8 encoding back out of the
Buffer slot (byte-equal for valid UTF-8 bodies), and a new test pins
that a lone surrogate is retained as U+FFFD — it has no UTF-8 form.
Heap-probe thresholds unchanged.
@github-actions github-actions Bot mentioned this pull request Oct 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

会话外缓存绕过 resident budget,诊断 substring 保留完整请求

2 participants