Skip to content

[PR] storage: default unified mode flips to full + six #3135 cleanups (#3818) - #3819

Merged
ranxianglei merged 11 commits into
masterfrom
2026-10-19_storage-full-flip
Oct 19, 2026
Merged

ranxianglei merged 11 commits into
masterfrom
2026-10-19_storage-full-flip

Conversation

@ranxianglei

Copy link
Copy Markdown
Owner

Fixes #3818
Fixes #3135

What

One PR, two layers:

  1. The default flip: resolveStorageMode absent-everywhere default shadow → full. The shadow dry-run has been the default since Inbound markers: one declarative strip engine for all four wires (#2789, #2798, #2811 plan B) #2815 and digest reconciliation held everywhere it mattered; with full, the SQLite index becomes the load path (unified-first, organic import, legacy fallback). JSON envelopes keep being written; legacy remains the one-line escape hatch (unset-trumps-everything tier unchanged).
  2. The six Storage Subsystem Wrap-up Checklist (#3130 Review): Dropping the last tick upon self-disable; BILI_PERSIST=0 falsely triggering a "Storage Degraded" banner; storageMode stamp mismatched with the actual enabled mode; and 3 more (6 items total) | 存储子系统收尾清单… #3135 cleanups that full-as-default turns from polish into correctness requirements:
# Hole Fix
1 A writerOff archive whose sidecar 3-strike-disables mid-build ends the tick with the record on NEITHER copy (index got no rows, envelope never armed) The writerOff runner re-checks stats.disabled after the build and lands one synchronous JSON envelope write (store.writeNow + stampOwnWrite) in the same tick
2 BILI_PERSIST=0 / enabled:false fabricated a "storage degraded - no SQLite engine" banner for a sidecar that was never supposed to open recordRequestedMode takes persistDisabled; the banner reports intentional opt-out (same class as the encryption opt-out)
3 The archive storageMode stamp described the REQUESTED mode: with full as default, every node<22.5 box (engine download fails) would stamp full onto records that only ever exist on the JSON envelope New liveStorageStamp getter on the store instance (disabled / no sidecar / sidecar self-disabled → legacy); guardedBuild stamps it; the module-level effectiveFileStorageMode() consults the live singleton (peek, never construct) and keeps pure knob resolution only pre-store
4 Digest drift was a log line + counter nobody reads verifyDrift + driftedSessions (live count) on the /__bili/status wire; loadSync serves drifted sessions from the JSON envelope (fresher copy) until a clean sample re-admits them; organic import is skipped for drifted records so the watermark is not re-stamped as if verified
5 A self-bootstrap cache that verifies but cannot load (major Node upgrade, truncated write) was warn-forever-stuck on legacy The unloadable copy is removed and ONE pinned sha512 download retried, then the existing cooldown protects against loops
6 bootBounded warmed only what the file scan saw — pure-index sessions (jsonWriter=off) were invisible after restart The warm set unions shadow.listSessionIds() (still budget-bounded, stop at maxSessions)

Stakes-first

Seven red tests were written on clean master first (tests/storage-full-flip.test.ts 6 + tests/storage-engine-bootstrap.test.ts 1), each failing on the intended defect; all seven green after the fixes. One stake assertion was deliberately revised during implementation (module-level stamp when the singleton is disabled-by-config): the implemented semantic — disabled persistence reports legacy because nothing is unified — is the honest reading of #3135-3, documented in the test.

Compatibility

  • Zero kernel, zero wire changes; JSON envelopes keep being written in full mode, so rollback = unset the knob (or set legacy) + restart.
  • jsonWriter=off users get ⑥ (enumerability after restart) — previously that posture silently required a JSON file to exist for the boot scan.
  • storage-unified.test.ts file-tier expectations updated ([undefined, "shadow"] → "full"); every other storage/status/golden family passes unchanged (storage-replicas 4/4, golden-storage-render 5/5, acp-status families, memory-diagnostics 3/3).

Docs

CONFIGURATION.md (+zh) and the website/config-reference/process.yaml seed regenerated via tools/gen-config-docs.mjs (default column + EN/ZH descriptions state the #3818 flip and the escape hatch).

Not in this PR

GC / index.db growth (#3463) — disclosed, separate; multi-lane posture doc (#3633); incremental-lane cold-resync gap (#3798); replicas 409-inheritance family (#3011/#3296/#3025).

🤖 Generated with pi

- resolveStorageMode: absent-everywhere default shadow -> full (#3818;
  the shadow dry-run reconciled cleanly since #2815 - JSON envelopes
  keep being written, legacy remains the one-line escape hatch)
- writerOff saves: a mid-build 3-strike sidecar disable now lands one
  synchronous JSON envelope write in the same tick (#3135 1)
- persistence disabled (BILI_PERSIST=0 / enabled:false) is recorded as
  an intentional opt-out - no fabricated storage-degraded banner (#3135 2)
- the storageMode stamp follows the live instance: a dead/absent sidecar
  stamps legacy so records describe the copy that actually holds data
  (#3135 3)
- verifyDrift + driftedSessions on the /__bili/status wire; drifted
  sessions serve from the JSON envelope until a clean sample (#3135 4)
- self-bootstrap: an unloadable cached engine copy is removed and one
  pinned sha512 download retried instead of warn-forever-stuck (#3135 5)
- bounded boot unions index-only sessions so jsonWriter=off sessions
  stay enumerable after restart (#3135 6)
@ranxianglei ranxianglei changed the title storage: default unified mode flips to full + six #3135 cleanups (#3818) [PR] storage: default unified mode flips to full + six #3135 cleanups (#3818) Oct 19, 2026
@github-actions

github-actions Bot commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

📦 Built Package Artifact

Branch: 2026-10-19_storage-full-flip (74e1217)

Option A — Install this exact build (recommended)

npm install -g billion-context@0.0.4551-pr.3819.fa5fc06

This exact version is immutable and reproducible. The rolling pr tag tracks the newest test build across all PRs.

Option B — Download artifact

  1. Download the artifact from the Actions run
  2. Extract the tarball and install:
tar xzf billion-context-pr3819.tgz
npm install -g package

Install instructions are refreshed on each push.

@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

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

查重: 不重复。 本 PR(#3819,分支 2026-10-19_storage-full-flip,单 commit ec56adc90)就是 issue #3818(翻 full)+ #3135(六项收尾)的唯一在途实现,无并列的同类 PR。注意两处热文件重叠(不是重复,是合入顺序协调):#3790 同改 src/storage/engine-bootstrap.ts + src/storage/runtime-status.ts,#3765 同改 src/persist.ts——两者与本 PR 任一先合,后合者需 rebase 解语义冲突。

标签: bug + scope/storage + p3(六项均为存储子系统缺陷,#3135 原级 p3;翻默认是有回滚通道的产品变更,不升档)。

开始审核:已拉取分支逐文件读 diff(knobs/persist/shadow/runtime-status/engine-bootstrap + 三个测试文件 + 文档),接下来做既有行为回归清点、本地定向测试、核对推送头 CI。结论按八问体例在本线程给出。

@ranxianglei ranxianglei added bug Something isn't working p3 scope/storage labels Oct 19, 2026
awork added 3 commits October 19, 2026 19:52
…ded-byte budget (#3818 review)

The #3135-6 union in bootBounded re-loaded every indexed id, including
envelope-backed ones the file scan had already offered under the
decoded-byte budget (#2803). With the full default (#3818) that bypassed
maxBytes for every legacy file on disk (bounded-storage warm-budget test
red: 2 sessions warmed where <=1 was allowed). Skip ids whose state file
still exists; the union now only warms pure-index (jsonWriter=off era)
ids, which is its stated purpose.
…3818 review)

The flip changed resolveStorageMode's absent-everywhere result from
shadow to full, but two suites still asserted the old shadow default:
web-storage-banner (requested mode + blob text + healthy-sidecar block)
and storage-mode-label (absent-eboth label row). Update expectations to
full; explicit-shadow rows untouched.
…review)

The #3135-5 retry path cleared the cooldown whenever the download itself
succeeded, even if the freshly downloaded engine still would not load
(AV quarantine, truncated extract, platform drift). In a persistently
broken environment that re-downloads ~11 MB on every boot and reported
status "downloaded" for an engine that never registered. Now that path
mirrors the catch block: writeCooldown(reason) + status "failed" + a
warn naming the reason. Behavior change (new path introduced by this PR):
per-boot retry -> 6h cooldown bounded retries; panel/status wire shows
bootstrap="failed" instead of "downloaded" in that case.
@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

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

Review 结论(PR #3819,head 现为 11e5bda66)

总判定:方向正确、实现扎实,但 flip 暴露了一个被 shadow 默认值掩盖的既有 full 模式数据丢失缺陷(已立项 #3831)。在 #3831 修复或 owner 拍板范围前,本 PR CI 会在两个测试上保持红 → 暂不可合并。 审核过程中发现的三个 PR 侧问题已直接以标注 commit 修入分支(见第 4 问)。

结构化八问

  1. 是什么问题:按 owner 要求(Storage wrap-up: 6 fixes + switch default mode to full (single PR) | 存储收尾六修 + 缺省模式翻 full(一个 PR 统一处理) #3818)把 unified storage 默认从 shadow 翻到 full,顺带清掉 shadow 时代遗留的六个小缺陷(Storage Subsystem Wrap-up Checklist (#3130 Review): Dropping the last tick upon self-disable; BILI_PERSIST=0 falsely triggering a "Storage Degraded" banner; storageMode stamp mismatched with the actual enabled mode; and 3 more (6 items total) | 存储子系统收尾清单… #3135 ①–⑥)。
  2. 什么原因导致的(审核核心发现):flip 本身没问题,问题在它掀开了一个既有缺陷——kernel 的 unified schema 没有 blockContents 的独立载体(kernel/src 零命中),src/storage/ingest.ts:465-467 只从 state.blocks 的槽位重建 view 的 blockContents。于是 state.blocks=[] 但带归档原文(blockContents 非空)的会话,在 full 模式「loadUnified miss → loadEnvelope hit → importRecord → viewFor」链路上原文丢失(探针实测:importRecord 输入带着 blockContents,落库后的 view 返回 {})。无崩溃、正常重启即丢;shadow/legacy 直读路径(buildSession(envelope.payload))保留原文,所以 master(默认 shadow)一直是绿的。已用 probe 钉死机制,非照抄 issue 结论。
  3. 影响范围与严重程度:flip 生效后所有用户默认走 full 读路径;触发条件是特定会话形状(blocks 为空 + 有归档原文,如归档后无活动块的会话)。后果:重载后 decompress 无法再恢复原文,writerOn 用户盘上 envelope 文件还在(数据可从文件找回但不再被 serve),writerOff 用户无文件 = 永久丢失。严重度:p2(能用、无报错,但静默丢数据;触发面窄但后果不可逆)。
  4. 怎么修复的(审核动作):独立审 diff + probe 验证机制后,向 PR 分支推了 3 个标注 commit:
  5. 是否有回归问题:逐项清点 diff 触碰路径的既有行为——①writerOff 3-strike JSON 落盘:双向都成立(纯 index 会话可 warm、构建中 writer 翻回时 JSON 落地),顺序安全由 writeNow per-id 串行 + deferred build 保证(区间审查(#3331)残留 11 项中小问题集中修:守卫旋钮、存储竞态、静默丢参、CI 触发残角等 #3406 不变量,tests/issue3406-writenow-concurrent.test.ts 钉住);②persistDisabled→intentionalOptOut:degraded 公式未动,仅「用户主动关持久化」不再误报 degraded banner(有意变更,已披露);③liveStorageStamp/effectiveFileStorageMode:persist.ts 外唯一调用点是 src/session.ts:1015 的 per-record stamp,波及面小且精确;④verifyDrift/driftedSessions:drift 会话 serve envelope、跳过 organic import,与 legacy fallback 分支逐行对照(clamp/logColdLoad parity 确认);⑤engine-bootstrap:cooldown 循环保护成立(cooldown 检查先于下载、下载失败 arm 6h),注册失败路径缺口已由 11e5bda66 补上;⑥bootBounded:由 07463490f 修复。高危模式扫描:无 fail-fast→吞错、无阈值/时序漂移;status wire 新增 verifyDrift/driftedSessions 为纯增字段。结论:PR 引入的唯一回归(F1)已在分支内修掉,其余既有行为保持。
  6. 是否破坏缓存:不涉及。存储层只改读路径选择与 boot warm 集合,不触碰模型流量字节,前缀缓存/重试/增量游标均不受影响。
  7. 是否改了设计方向:是——默认存储模式 shadow → full(PR 本身的目标,Storage wrap-up: 6 fixes + switch default mode to full (single PR) | 存储收尾六修 + 缺省模式翻 full(一个 PR 统一处理) #3818 owner 拍板;回退方式:显式设 BILI_STORAGE_UNIFIED=shadow 或 storage.unified=shadow)。无其他架构转向。
  8. 是否引入新配置:不涉及。无新 env/config;flip 是既有 knob 默认值变更(见第 7 问)。

行为变更清单(强制披露)

变更 旧 新 原因
knob 全缺省时的默认 unified 模式 shadow full #3818 owner 决策
「下载 OK 但引擎加载不了」路径的 bootstrap 状态(本 PR 新增路径) "downloaded" + clearCooldown(每 boot 重下 ~11MB) "failed" + 6h cooldown 11e5bda66:避免谎报成功 + 无界重下

其他说明

收工判定

暂不可合并 —— 等 #3831 拍板:A(kernel 侧给 blockContents 独立载体,根治,Kernel Boundary 人审)/ B(host 侧 loadSync 统一路径发现 view 丢了 envelope 里还有的 contents 时改 serve envelope,小改恢复 flip 后默认可用性,writerOff 不覆盖)/ C(hold flip,默认留 shadow)。我的推荐:B 先行 + A 跟踪根治。owner 拍板后对应修法落地(或按决策处理两例测试),其余部分本 PR 已具备合并条件。

…ow-ups)

- shadow record/importRecord skip records whose blockContents carry ids
  no state.blocks entry claims: the ingest cannot represent them, so a
  viewFor rebuild silently dropped the orphans on first load. Real kernel
  sessions never trip this (state.blocks keeps consumed blocks); hand-
  built archives and web seeds do. JSON envelope stays the serving copy
  (orphanSkips counter on the stats surface).
- bootBounded union now respects the decoded-byte budget as well as
  maxSessions: index-only (jsonWriter=off) candidates are measured by
  their serialized record — the same proxy readRawFileWithBytes gives the
  file path (#2803: budget is a RAM proxy).
- loadSync organic import serves the freshly parsed envelope view
  (lossless incl. orphans) instead of a viewFor rebuild.
- test expectations for the default flip: identity-registry restores the
  legacy baseline instead of deleting the env var (absent = full now),
  web-sessions stamp legs build per-leg stores (#3135-3: the stamp is
  the live instance's, not the knob at write time) and read the
  encrypted leg back with its key, storage-mode-label absent leg says
  full, stale 'defaults to shadow' comments updated.
@ranxianglei

Copy link
Copy Markdown
Owner Author

ci失败

awork added 2 commits October 19, 2026 21:19
The per-leg stores open a sidecar each (shadow/full legs) and were never
closed — on Windows the live index.db handles can outlive rmrf(dir) and
flake the suite teardown. Close each leg after its write.
…3818 review)

reapDestroyedSubstrate keeps blockContents while state.blocks loses the
entry — real reaped sessions trip the orphan-contents guard, not only
hand-built archives. Pin both legs: the save skips the mirror
(orphanSkips), and same-process plus cross-restart reads serve the lossless
envelope view with imports still 0.
@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

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

CI 失败已查清:两处红,都不是本 PR 的代码回归。 当前 head 3b4332f7(含 9a16af66e orphan-contents guard + merge master):12 个 workflow run,10 绿,2 红——逐条如下。

① test (windows-latest, 22, 1) — 已知 Windows 瞬态锁 flake,非回归

  • 该 shard 跑了 3469 个测试,只挂 1 个:web-sessions.test.ts 的 session detail surfaces the per-file storage-mode stamp (#2671)(3456 pass / 1 fail / 12 skip)。
  • 失败点不在断言,在测试收尾:EBUSY: resource busy or locked, unlink '…\bili-web-sess-s9ZZFj\index.db' —— Windows 上 index.db 句柄还被占着时删不掉文件。
  • 判定为 flake 的证据链:
    1. 错误签名与 tests/tmp-rm.ts 注释里记录的 既有 flake 家族完全一致(test(windows): driveWire golden-harness 清理期 index.db EBUSY 复发(#2799 族,#2808 后残留) #2908/Windows CI: storage-engine-proofs 套件清理 temp 目录遇 EBUSY/EPERM,windows test leg 随机红 #2799:win-latest 在 c=4 并发负载下 EBUSY unlink index.db,归因于 Windows AV/索引器的瞬态外部锁);tmp-rm 的重试阶梯 [25…3200ms] 总预算 ~10.1s,本次失败 duration_ms=10104 —— 恰好把整个重试预算耗光,是外部锁持续时间超预算的典型形态;
    2. 全日志检索:该测试前后没有 sidecar sqlite close failed 警告(全日志仅 1 条,在 8 分钟前另一个测试里,且是良性的 double-close "database is not open")—— 即本 PR 改的 per-leg store 三条腿的 closeQuietly() 都同步关干净了,不是本 PR 引入的句柄泄漏;
    3. 同一 head 其余全部 shard 绿:ubuntu 22/24 × 3 shard、windows 24、static-gates、kernel-guard、version-guard、config-surface-guard 等。
  • 已提交 rerun-failed-jobs(run 39254622547),结果出来我跟进;若复绿即坐实 flake。

② ci-gate-plugins.yml — 幻影 run,与本 PR 无关的全仓既有现象

  • 这个 workflow 是纯手动触发(on: workflow_dispatch,无任何 push/PR 触发器),但每次 push 都会产生一个零 job、瞬间 failure 的幻影 run(创建与完成同一秒)。
  • 实测:最近一小时内所有分支包括 master 的每次 push 都产生同样的 failure(master 上 13:17:56、13:17:16、13:15:24、12:16:30 四条同型)——系统性平台行为,不是本 PR 引入,也不影响本 PR 的必需检查判定。

顺带确认:9a16af66e 的 orphan-contents guard(+ loadSync 无损 import)已把我此前报的 #3831 机制修掉——之前红的 bounded-storage / bounded-storage-scale 两个测试在本 head 已转绿。#3831 待 CI 全绿后我更新状态(建议合并后关闭)。

结论:等 windows shard 重跑复绿后,本 PR CI 即为实质干净;合并前唯一剩余事项仍是 #3831 的状态收口(guard 已在分支内,属"已修复待验证关闭")。

…view follow-up)

reapDestroyedSubstrate (#2695) keeps blockContents while state.blocks
loses the entry — real reaped sessions trip the orphan-contents guard
(owner-pinned in 21b70f4: never gutted into the index). Under
BILI_STORAGE_JSON_WRITER=off that skip left the session on NO copy at
all (index skipped the ingest, writerOff wrote no file). The valve lands
one synchronous JSON envelope for exactly that shape; every later read
stays envelope-served (imports remain 0).
@ranxianglei

Copy link
Copy Markdown
Owner Author

#3831 缺陷已在当前分支修掉(时间线说明)

review 头 11e5bda66 之后,9a16af66 已把 #3831 的子缺陷 a 直接修在本 PR 内,机制与 #3835 的选项 B 等价但更前置:

#3835 与本修法重叠(serve 时比对 vs ingest 前拦截,同一止血目标)。建议:本 PR 合并后关闭 #3835 为 superseded,#3831 收敛到「选项 A(kernel 侧 blockContents 独立载体)+ 子缺陷 b(label 重复行)」两个后续项。

@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

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

同意时间线与收敛方案,已逐条核实:

收敛执行

6d36b45d(writerOff 安全阀)快速复核,无问题

CI 终态(头 6d36b45d):主 CI run 39263305958 completed / success——9 个测试分片全绿(含上一头 EBUSY flake 的 windows-22-1,本次未复现),static-gates/各 guard/e2e 车道全绿,42 个 check-run 零失败。之前那两处红均已定性:windows job = 已记录的 teardown EBUSY flake 家族(#2908,重试预算恰好耗尽于观测时长),ci-gate-plugins = 系统性幻影(仅 workflow_dispatch 的手动 workflow 在每次 push 上零 job 秒败,master 同样中招,与本 PR 无关)。

结论:分支处于可合并状态(CI 全绿、mergeable=true;当前 blocked 仅为等待人工合并)。按你的方案,合并后我来关 #3835 + 收敛 #3831。

@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

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

补充一条对我上一条「blocked 仅为等待人工合并」的精确化——核实合并状态时发现 blocked 的真实构成:

master 分支保护仍钉着旧版 12 个 test 分片,而 core scope(普通 PR push)只跑 6 个(ubuntu 22×3、ubuntu 24×1、windows 22×2)。所以本 PR 的 mergeable_state=blocked 不是内容问题,而是 6 个必需 context 不在 core scope 内、永远不上报——实证:今天 13:17 合并的 #3828 就是同样形状(6 leg 全绿 + bypass)。ci.yml 的 plan 注释自称「branch protection requires exactly these 6」,与线上设置矛盾,说明 #3295 落地时保护规则没同步收窄。

排查中顺带钉死两处 CI 管道问题,已分别立项(均带来源标记与查重声明):

本 PR 本身的内容结论不变:CI 全绿(6/6 core leg + static + guards + e2e)、#3831 子缺陷 a 已在分支内修掉并有测试钉住,可合并;合并后我按约定关 #3835、收敛 #3831。

@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

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

刚拉了当前头 6d36b45d7 的完整状态(无新 push),逐条核对如下:

实质 CI 全部绿:该 commit 上共 77 条 check-run——49 success + 28 skipped,零 failure。这个头上跑过两批完整批次(13:48 与 14:25),两批都是全绿:主 CI(6 个 core test leg 含 windows-22-1、static-gates、kernel-guard 等全部 guards)+ 全部 e2e 泳道(real codex / fake-codex / pi / opencode / dsh / local-registry / persona / ws)+ Windows codex overlay + PR Build Artifact。

页面上唯一的红是 ci-gate-plugins 幻影 run(run 39263296529,13:48:24 创建、秒败、零 job)——就是已立项的 #3852:dispatch-only workflow 在每次 push(含 master)上的平台侧幻影,不在必需检查里、不阻塞合并。你看到的「ci 还是失败」应该就是它。

如果你看到的是别的具体某项红(名字或链接发我一下),我马上查——按 API 数据现在不存在第二条红的 check-run。

@ranxianglei
ranxianglei merged commit fc15b1e into master Oct 19, 2026
19 of 21 checks passed
@github-actions

Copy link
Copy Markdown
Contributor

📦 Merged Build Published

This merge to master was published as 0.0.617-master.fc15b1e (rolling master tag).

npm install -g billion-context@0.0.617-master.fc15b1e  # 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.

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