Skip to content

Exporting ONE session decodes EVERY persisted session (loadAll) — multi-long-session machines OOM the documented handoff workaround | 导出单个会话会解码存储里的全部会话(loadAll)——多长寿会话机器上官方交接手段 OOM 崩溃 #5062

Description

@ranxianglei

来源: #5026 (https://github.com/ranxianglei/billion-context/issues/5026)「dsh-native 长会话 turn 裸 TypeError」分析时发现

查重: 无(open+closed 检索 export/OOM/loadAll/heap 关键词,无同机制条目)。

症状

在持有多个长寿会话的机器上执行 bili export <session-id> --output handoff.md,node 直接崩溃:exit 134 (SIGABRT),V8 堆耗尽于默认 ~2 GB 上限(现场来自 #5026:单会话 1184+ 请求、累计输入 ~1.385 亿 token;机器 8 GB 内存)。崩溃恰好打在「坏会话 → 导出交接 → 新会话」这条官方绕行路径上——最需要导出的会话,所在机器最导不出。

机制(master @ 9b6ec23 与 v0.1.208 均已核对)

exportSession(src/export.ts:354-357 @master / :330 @v0.1.208)第一步是:

const all = [...(await store.loadAll()).values()];

即无论是否给了 selector,都把本机全部持久化会话水化进内存(逐个解码状态文件:blocks、保留原文、lastMessages 快照),然后才做选择。listSessions 早已有一条不水化的 SQL 快路径(#2960 R2.1,直接查 unified sessions 表),但 exportSession 的选择环节没有复用它。而选择本身(matchSession)只需要 id/label——summary 行里都有。

影响

建议修法(PR 随后)

  1. 用 store.unifiedSessionSummaries()(一条 SQL,与列表同源同权威)解析出选中的 id,再用既有 store.loadSync(id, meta) 原语只水化这一个会话(web 面板/fork 收养已在用的公开方法);
  2. 无 unified 索引(legacy 模式)或该行水化失败时,保留历史全量扫描路径(stderr 打降级提示);
  3. 行为对齐声明:空库提示文案、no-match 错误、歧义前缀错误文本均不变;unified 模式下选择与列表共用同一索引权威(列表本就隐藏的文件幽灵行从此同样不可选——一致化,PR 内披露);
  4. 回归测试:unified 存储放 N 个会话,selector 导出结果正确且 loadAll 不得被调用(原型 spy 钉住);legacy 腿钉住回退方向(Test Blind Spot Retrospective: Reinforcing Negative Assertion Discipline, Tightening Fake Upstream Strictness, Thin Inst | 测试盲区复盘:补反向断言纪律、假上游严格化、薄安装冒烟、形态矩阵与生命周期覆盖 #3617 门禁纪律:正反双向)。

建议标签:bug / dsh / scope/storage / p2(有绕行的功能性缺陷:加大堆可能可过;非核心车道可用性停摆)

Activity

  1. ranxianglei commented on Oct 28, 2026

    @ranxianglei
    OwnerAuthor

    🤖 Powered by ework · qwen3.8-27b

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

    查重: 不重复。 open+closed 按 export/OOM/loadAll/heap/handoff 多组关键词检索,无同机制条目。最接近的 #4750(bound ordinary session loads)是「单次加载体积上限/惰性字段」机制,与本题「选择环节水化全树」不同失败域;#4247/#4248 是交接文档内容缺失,症状不同。

    标签: bug / dsh / scope/storage / p2(有绕行的功能性缺陷——加大堆可能可过,非核心车道可用性停摆;按 issue 建议打标)。

    审核三步结论:

    1. 复现/查证:master @ 5594c13 上 src/export.ts:357 确认 exportSession 第一步即 const all = [...(await store.loadAll()).values()],与是否给了 selector 无关;而 listSessions(同文件 :47)早已走 unifiedSessionSummaries() 一条 SQL 的快路径(Identity pipeline: key sessions by (agent, conversation id) in the unified store — decouple fold ledgers from protocol/host origin (#2938 / #2847 / #2671 defect-1) #2960 R2.1),选择环节却没复用。issue 描述的机制成立。
    2. 层次判断:根因是「单会话选择」复用了「全树水化」作为数据源,不是解码本身或内存参数问题。选择只需要 id/label,summary 行里都有——修法打在根因上。
    3. 方案评估:同意 issue 的四点建议(summary 行解析选中 id → loadSync(id, meta) 只水化一个;legacy/水化失败保留全量扫描+stderr 提示;错误文案不变;回归测试正反双向钉住)。实现中另发现两个同机制面的点,一并处理并在 PR 披露:
      • unifiedSessionSummaries() 在 mysql 网络后端上返回 [] 而非其契约承诺的 null(sync facade 的 miss 值),这会让 listSessions 在健康的 mysql+full 安装上显示空列表——顺手加一行守卫,独立标注 commit;
      • 「索引存在但零行」的过渡态(刚切 full 模式、手工删过 index.db)保守起见仍走全量扫描,行为逐字节不变,只在正常满索引机器上省掉水化。

    开工计划:分支 2026-10-29_export-single-session-no-loadall,PR 引用 Closes #5062。回归测试:unified 存储放 N 个会话,按 id/label 导出正确且 loadAll 零调用(原型 spy 钉死);legacy 腿钉住仍走 walk 的回退方向(#3617 门禁纪律正反双向);空库文案、no-match、歧义前缀三处错误文本逐字对齐旧行为。

  2. ranxianglei commented on Oct 29, 2026

    @ranxianglei
    OwnerAuthor

    🤖 Powered by ework · qwen3.8-27b

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

    进度汇报:上一会话因一次长时间无响应的操作卡住 30 分钟被强制重启。已从中断现场恢复——工作树里留有一处中断造成的残缺代码(新函数返回了一个不存在的变量,属编译错误),先修复后继续。现已完成开发并开 PR。

    PR #5084(Closes #5062):#5084
    两个提交:①主修复(src/export.ts + 回归测试);②同机制附带发现(src/persist.ts 一行守卫)。

    八问结论:

    1. 是什么问题:bili export <id> 在导出任意一个会话之前会解码磁盘上全部已持久化会话;持有多个长会话的机器上直接 OOM 崩溃(exit 134),文档化的「坏会话导出 handoff」绕行方案([Bug][dsh-native] turn fails with bare TypeError (reading 'length') host-side before the request reaches the proxy - very long-lived session, intermittent #5026)还没跑起来进程就死了。
    2. 什么原因导致的:exportSession 无条件先做全量目录扫描+全量解码,再拿选择器去匹配——选一个会话的代价被绑死在「解码所有会话」上。
    3. 影响范围与严重程度:仅触发于统一存储(full)模式且持久化会话较多/较大的机器;单会话或小体量机器无感。触发时 bili export 完全不可用(崩溃),但不影响代理主链路、压缩与数据本身。
    4. 怎么修复的:选择改为从统一索引的摘要行解析(一条 SQL,与 listSessions 同一权威源;匹配只需要 id+label),再用现成的 loadSync 只水合命中那一个会话。无行(legacy/shadow/降级)、空行(刚切模式/index.db 被删)、或命中行解不开 → 回退原全量扫描,这些状态行为逐字节保持;三种报错文案(空存储/无匹配/前缀歧义)与旧版逐字一致。
    5. 是否有回归问题:逐项审过既有行为——legacy/shadow 路径字节级不变(有测试钉住);空存储消息、无匹配、歧义前缀文案逐字一致;jsonl/markdown 渲染输入与 web 面板/fork adoption 走同一水合路径,输出不变。一处有意变更已在 PR 披露:full 模式下「文件存在但索引没有其行」的会话(sidecar 降级期写入)原本在列表里也不显示(listSessions 早已以行为权威源),现在同样不能按选择器选中——选择与列表共用一个权威源,是趋同不是漂移。另修(独立提交):mysql 网络后端下摘要接口返回 [] 而非契约承诺的 null,健康 mysql+full 安装上会话列表显示为空——现按契约回退文件扫描。
    6. 是否破坏缓存:不涉及——不动压缩状态、前缀缓存、重试与去重策略。
    7. 是否改了设计方向:不涉及——复用既有摘要行与 loadSync 设施,无新架构、无新数据流。
    8. 是否引入了新的配置:无。仅新增一条 stderr 提示行(选中行解不开、回退全量扫描时)。

    验证:新增回归测试 6/6 绿(双向钉死:full 模式下全量扫描可证明零调用;legacy 模式下可证明仍恰好走一次;含损坏信封不阻塞健康会话的 #5026 场景);既有四个 export 相关套件 16/16 绿;typecheck 干净。全量回归以 CI 为准。

    判定:可进入 review/merge(以 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

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions