Repository navigation
[PR] [storage] Degradation Semantics Bundle: Seven accepted fixes consolidated into one branch — a single merge suffices (inc | [storage] 降级语义全家桶:七个已验收修复合成一个分支,合并一次即可(含撤回损坏库自动改名) - #5213
Merged
Conversation
added 30 commits
October 25, 2026 18:04
#4664: since #4390 the unified store's getMessages filters retired_at IS NULL by default (tombstone-based rewind history), so the JSONL export silently dropped every rewound message. The store's own design comment names export a historian caller that must opt into includeRetired — it never did. - widen the StorageBackend contract: getMessages(sessionId, opts?) with includeRetired and retiredAt on every row (both backends already implemented this natively; SqliteBackend now forwards it) - renderSessionJsonlFromTables reads with { includeRetired: true } and emits retiredAt on tombstoned rows only — live rows omit the field, so output for sessions without retirements is byte-identical to before Regression test in tests/export-jsonl.test.ts drives the real ingestSession sweep (full ingest -> truncated re-ingest retires row one).
The archive dump SELECT and restore INSERT predated #4390's v8 row-state columns: every restored row landed with identity='' (the full-ingest JOIN key can never match again -> next full save re-mints every message beside the restored copy = duplicate messages) and retired_at=NULL (a rolled-back row resurrects as live). Both backends now carry both fields verbatim; pre-fix dumps lacking them fall back to recomputing identity from the CAS body (the v8 migration backfill formula) and restore retired rows as live (state unrecoverable there).
…help-merge sweep)
…help-merge sweep)
…help-merge sweep)
…help-merge sweep)
…testing (#4626) The surfaced contest was persisted into the storage runtime state at detection time and replayed verbatim on every /__bili/status read until a manual ack — but detection fires while the restarting peer's tombstone marker is still seconds fresh (its graceful-shutdown dispose() rewrites the marker), so a restart handover left an alarming card behind for hours after the peer pid was gone. The stored verdict now re-evaluates against CURRENT evidence at the status read, with the same never-freeze discipline as the sibling second-process warning: the server registers the very scan that produced the verdict (findContestedWriters over its own writers dir), and when that scan no longer reports the accused peer (pid gone + marker past its freshness grace, or the contest window lapsed) the entry clears itself with an info log line. Without a registered check the legacy replay behavior stands (standalone record/ack use). The fresh-tombstone case keeps persisting: a dead pid alone is not proof of absence on every platform, so the notice stays honest while marker freshness says the peer may be alive.
…#4626) slice(0, 8) of a "session-…" id renders literally "session-" — the ack log line and the web banner both showed an unusable prefix. Log the full id (the [split-brain] error line already does) and give the card a 24-char prefix so distinct sessions are tellable apart.
…ecover the unified-store breaker (#4949) Two defects in one failure domain, both observed in production on v0.1.206 after host storage exhaustion: 1. Error masking: when an I/O-class failure (disk full / SQLITE_FULL) aborts BEGIN or COMMIT, SQLite auto-aborts the transaction, so the follow-up ROLLBACK in the node:sqlite emulation throws 'cannot rollback - no transaction is active' and replaces the real error before it reaches the sidecar breaker's lastError / status wire. The rollback attempt is now guarded so the original error always wins. The better-sqlite3 seam gets an annotated message at our boundary because that library loses the original inside its own wrapper. 2. Permanent circuit breaker: stats.disabled was set on 3 consecutive failures but never cleared — the unified store stayed disabled for the whole process lifetime even after the disk was freed (only a restart recovered it). Both sync and async sidecar paths now run a cooldown-gated writability probe (REENABLE_PROBE_COOLDOWN_MS = 5min, one probe per window on the next save) through the same transaction path production writes use; success re-enables the store and the triggering record ingests immediately. Missed sessions converge via the existing envelope staleness guard at load plus the next full ingest.
- A: injected engine-level COMMIT abort over a real DatabaseSync proves the original error surfaces (pre-fix this failed with exactly the reported 'cannot rollback - no transaction is active' mask); aborted rows do not persist. - A2: negative control — nested transactions on a constraint failure still roll back cleanly and propagate the original (gate both directions, #3617). - B: full breaker loop on a real store — 3 strikes disable with the true cause visible, cooldown gates probe frequency (exactly one probe per window), healing + one probe re-enables, triggering record lands, unified reads work again, exactly one RE-ENABLED log line. - C: pins REENABLE_PROBE_COOLDOWN_MS at the documented 5-minute cadence.
…ckends (#4942) MySQL-backed unified storage had no synchronous read facade: viewFor() returns null and listSessionIds() returns [] on network backends, so every hot-path read (cold load after restart, LRU eviction reload, boot enumeration) fell back to local JSON envelopes. Any deployment that stops writing envelopes lost every session at every process restart. Owner decision on #4942: option A (async pre-warm + async reload seam), keeping the byte-bounded resident pool intact. - SessionStore.loadAsync: envelope/index-first (zero behavior change whenever the sync path serves), DB fallback only when the sync read misses AND the sidecar is an async backend; awaits ready() then viewForAsync(). - getSessionAsync: single-flight variant of getSession for async call sites (concurrent first-touches share one load); main request entry in server/handle.ts switched to it. - boot/bootBounded/loadAll unions now enumerate DB ids via sessionSummariesAsync() (newest-first under the existing maxSessions/maxBytes budget) when a network full backend is expected; sqlite/file paths unchanged. - resolveForkConversation / publishForkSession / maybeAdoptForkBlocks / reconcileDerivedLineage / handlePluginStatus made async (all callers already in async contexts). - UnifiedShadow.fromBackend factory + exported recordToLegacyView for hermetic testing against stub backends. Deliberate sync boundaries left as-is (documented in PR body): hasProcessedState, resume-inheritance parent probe, decompress chain walker. Tested: new hermetic tests/storage-mysql-seam.test.ts (typed stub implementing the full StorageBackend contract, no DSN required). Related pre-existing ingest-verify finding filed separately: #4986.
…ane repair) The new seam suite shipped red at the PR head (verified against the pre-rebase commit): 8 tsc errors from a top-level 'blocks' access that does not exist on Session (blocks live under state), and two over-strict reference-identity assertions between separate cold loads. - .blocks -> .state.blocks (4 sites): mechanical API alignment; makes the typecheck lane pass. - assert.equal(viaAsync, viaSync) / assert.equal(b, a) -> deepEqual (2 sites): every cold loadSync builds a fresh Session object (no cross-call caching), so reference identity was never part of the contract - even loadSync-vs-loadSync differs. The real contracts stay pinned: envelope precedence, zero extra DB reads while an envelope exists (the viewCalls guard is unchanged), and no-sidecar parity of loadAsync over loadSync.
resolveForkConversation probed with loadAsync but materialized with the sync getSession. On a network storage backend a DB-only conversation id (no local envelope/index copy) then minted an EMPTY resident ghost, and that ghost poisoned every later getSessionAsync of the same id for the process lifetime — plugin status/fork/snapshot surfaces operated on an empty session and fork children inherited zero blocks (#4942 seam). Materialization now rides the same async seam as the probe. Regression: the seam suite drives handlePluginStatus for a DB-only corpus session and asserts the resident pool holds the DB state (fails on pre-fix code).
…4987 CI) #4942 made handlePluginStatus() and reconcileDerivedLineage() async (and maybeAdoptForkBlocks()/publishForkSession() in the same seam). Six existing suites still called them synchronously and asserted on the response/log before the microtask completed, red across 7 of 9 CI shards: - tests/issue404-restore-timestamps.test.ts: JSON.parse of the empty mock body - tests/advisory-panel.test.ts, tests/update-notes.test.ts, tests/issue2029-status-baseline-provenance.test.ts: 0 !== 200 on the mock res - tests/lineage-reconcile-boot.test.ts (AC10): 'not on disk' log asserted before it was written - tests/fork-ccr-adoption.test.ts: same class, passed only because the parent stayed resident (sync completion by luck) — aligned too
…ync-reload-seam # Conflicts: # src/plugin.ts
…ry, not lane alone (#5046) The storage-side second-writer guard keys on the sessions directory while the [instances] guard keyed on lane overlap — after isolating a second install via BILI_SESSIONS_DIR, the warn kept claiming 'both processes will write the same sessions directory', a factually wrong false positive that drowns the real signal. Registry markers now carry the resolved sessions dir; when both sides report different dirs the notice drops to info level (unknown dir on either side stays conservative = warn). Both the startup warning and the #2401 slow-tick rescan use the same criterion.
…#5046) Two installs on one machine (e.g. two DSH profiles) default to the same XDG sessions dir + config; with storage.unified on they double-write one index.db (SQLITE_BUSY → failed turns). Document the BILI_SESSIONS_DIR + BILI_CONFIG_FILE isolation recipe in both languages.
…tion; middle-truncate long degrade reasons (#5108)
…d when it exceeds the window (#4816)
- plugin-fork: pin the #5140 cold-restore snapshot test to unified-full — under the global legacy test baseline its sidecar never opens, so the index-reconstruction gate can never pass (409 replicasUnavailable sidecar-missing) - plugin-fork: pin the new broken-dir refuse-to-start assertion to full mode; legacy never touches sqlite at all so the ctor cannot throw there - multi-instance-live: spawn children with BILI_STORAGE_UNIFIED=full despite the global legacy baseline pin, else instance A holds no index.db.lock and B has nothing to refuse over (boot-refusal needs a live foreign writer) - export-jsonl: mysql-without-DSN now refuses at open time — assert on the actual refusal message instead of the pre-refusal #3388 tag - env-only.yaml: sync BILI_STORAGE_QUICK_CHECK seed with the final alarm-only policy wording already shipped in CONFIGURATION.md/.zh-CN (docs-sync gate)
…n parity) The convergence fix 40bcb2f re-synced desc_en of BILI_STORAGE_QUICK_CHECK but deleted the desc_zh line — env-only.yaml went 65 zh / 66 en while every other seed file stays balanced and the generator requires all five fields (type/default/group/desc_en/desc_zh) on each entry. Result: the docs-sync gate fails at the generate step before it ever reaches its diff check. Restores the exact final-form Chinese wording already committed in CONFIGURATION.zh-CN.md (alarm-only: corruption proves itself by THIS boot's own scan, files stay in place, service keeps running on envelopes with a loud alarm; FAIL never cached; transient open failures still refuse). Verified: tools/gen-config-docs.mjs generate -> zero diff on all four generated files; check-coverage rc=0.
- storage gc --vacuum full: the no-degradation constructor refusal makes the old in-runGc guard unreachable — catch StorageSecondWriterError at the CLI boundary and refuse with exit 1 plus the actionable one-line reason (the #4612 pin). - storage-full-flip: #3818 (iii) repins to refuse-to-start when the full-mode sidecar cannot open; #3818 (vi) proves the removed breaker's same-tick JSON valve stays gone — under jsonWriter=off no envelope appears and the save lands in the index (index-only persistence is the contract).
red-family fixes Conflicts resolved in favor of owner policy + master's improvements: - src/storage/shadow.ts: generic sidecar-failure log keeps the round-3 'REFUSING TO START' wording (owner policy: no silent legacy fallback); sessionTipSeq takes master's MAX(seq) aggregate (#5239/#897756a7) but drops the breaker-era assertLive() line (method removed with the 3-strike breaker, 2026-10-30 owner policy). - tests/plugin-fork.test.ts: both #5140 pins keep master's version (same BILI_STORAGE_UNIFIED=full pinning, clearer comments). - src/persist.ts auto-merge: #4788 debounce-timer ref/unref host fix is compatible with the D2 spill path (afterNextFlush waiters re-ref). Verified: tsc clean; storage-tipseq-narrow-read + issue4739 + issue4788 + inc-canonical-pointers 12/12; plugin-fork + issue5140 + issue5188 + multi-instance-live 62/62; integrity policy core (issue4816/durability/ review-fixes/integrity-cache/issue5103/issue4949) 29/29.
This was referenced Oct 31, 2026
Closed
Contributor
📦 Merged Build PublishedThis merge to master was published as npm install -g billion-context@0.0.932-master.7c9e65b # this exact build
npm install -g billion-context@master # always the newest merged build
|
This was referenced Oct 31, 2026
Owner
Author
|
(ai: vllm-qwen/qwen3.8-27b) Merge-guard closing note: this PR is already merged (state=closed, merged=true, merge commit |
This was referenced Oct 31, 2026
ranxianglei
added a commit
that referenced
this pull request
Oct 31, 2026
…r-no-block [PR] fix(storage): 活着的第二写者不再阻止启动——共享一个本地盘 index.db 是受支持拓扑(撤回 #5213 的拒绝策略)
ranxianglei
added a commit
to Zhishulo/billion-context
that referenced
this pull request
Oct 31, 2026
…decision 2026-10-31) Sharing one local-disk index.db IS the supported topology (ranxianglei#5082/ranxianglei#5091): SQLite WAL is multi-process safe locally, so a peer holding <db>.lock is informational-only now (info log + web/status wire) whatever its lane — inline lanes never register, so 'unknown lane' was routinely mistaken for risky and bricked resumes (the 2026-10-31 `bili pi -r` boot failure, ranxianglei#5213's refuse-to-start withdrawn). Withdrawn call sites: UnifiedStore.open secondWriterPolicy:'degrade' opt-in (option removed; crossLaneSafe math deleted), shadow.ts open-catch refusal + recordSidecarOpenFailure line, runtime-status degradeReason verbatim branch, storage-maintenance gc catch (the full-VACUUM starvation guard in runGc via store.unifiedActiveWriter() is untouched). StorageSecondWriterError stays exported (@deprecated) so external import/catch sites keep compiling. Genuine hazard unchanged: two instances walking the SAME session still alarm via the split-brain contest (ranxianglei#3923). Tests rewritten to the never-block contract incl. regression pins for both formerly-refused shapes (same-lane, unknown-lane fleet). Docs: CONFIGURATION EN/ZH seed + README multi-instance section updated.
This was referenced Oct 31, 2026
Closed
Closed
Merged
This was referenced Nov 1, 2026
Closed
ranxianglei
pushed a commit
to fathonix/billion-context
that referenced
this pull request
Nov 12, 2026
is resolved by ranxianglei#5213's async reload seam, which is on master; the gate stays sqlite-only as a separate decision (ranxianglei#4936 sync)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
[storage] 降级语义全家桶:七个已验收的修复合入一个分支,合并一次即可
这七个修复各自都是一个独立的 PR(见文末),但七条分支都停在旧基线上,而 master
在这之后又动了存储层三次(#5193 GC、#5198 内存治理、D2 常驻上限),导致每一条
单独提都会和另外六条 + master 撞在同一批文件上(
store.ts被四条同时改、shadow.ts被四条同时改),谁先合谁干净,后面六个全部要重新解冲突。这个 PR 把七条分支按依赖顺序全部并到最新 master,冲突一次解完、类型检查过、
全部相关测试跑绿。合并这一个 PR,下面七件事一起落地:
#5123 的原始方案沿用了 #4175 的老做法:确认 index.db 损坏后,进程自动把它
重命名成
index.db.corrupt-<时间戳>再开一个空库。这个自动改名被撤回了,理由:改完名并不能证明数据全部可恢复——checkpoint 行只存在 index.db 内部
(JSON envelope 不保证是它的完整副本,
BILI_STORAGE_JSON_WRITER=off模式下index.db 更是唯一持久副本),把一套无法验证完整的文件自动改名走,风险是
销毁唯一副本却仍然没有换来“已证明的恢复”。
新行为:quick_check 可证伪地失败时,所有文件原地一字不动;服务大声降级到
JSON envelope 路径(不停服、不断流),FAIL 结论写入旁路缓存(下次启动直接
降级、不再重扫),日志与 /__bili/status 给出人工处置指引(先备份再把文件集
挪走);照做后下次启动自动重建空库、经 envelope 补回会话——测试里 e2e 钉住。
分类器不变:只有真正的损坏形状才走这条路径,BUSY/LOCKED/IOERR/CANTOPEN
等瞬时错误照样原样上抛、绝不触发降级换库。
修了什么
冲突怎么解的(要点)
[PR] fix: stale split-brain notice clears itself once the peer stops contesting (#4626) #4630(脑裂自动清除)与 master 上 [PR] release v0.1.209 #4982 已合的"优雅停机标记":两者给 marker
加了不同名字的时间戳字段,这里让
dispose()两个字段都写、读侧任一命中即视为优雅退出——新旧构建互相可读,四组相关测试原样通过。
persist.ts一处双方都在同一位置插函数的冲突:保留本侧restoreBlockContents抽取 + 对侧新增导出
recordToLegacyView(MySQL 接缝测试要用),去掉重复的buildSession声明。web/sessions-data.ts取 d-4987 的异步装载形态,映射到 [PR] fix: web overview rides the persist store singleton instead of opening a second SessionStore #5002 之后的进程单例getStore().loadAsync()(d-4987 基线里的局部getDiskStore()已被统一掉)。sessionViewRows/readSnapshot/ retiredAt成员(stub 行为不变,仅接口对齐)。验证
npm run typecheck0 错(含 kernel/pi-subagents 预构建)。119 tests → 117 pass / 0 fail / 2 skip(skip 需真实 MySQL 实例)。
[bug] 会话状态写入缺 fsync:内核崩溃后留下 size 正常但 VDL=0 的全零状态文件,loadSync 永久抛错 → 502 acp-proxy failure 且不自愈(含 7 处 tmp+rename 写路径盘点) #3157×2,均为预存在问题,非本次引入)。
review-fixes 34 过、engine-proofs+web-banner 17 过、unified/ext/watermark/
multi-instance/persist 三件套 49 过+1 skip、second-writer/rollback-mask/
instance-governance 28 过;typecheck 0 错。
备注
这里按上面"双字段互认"方式合流,两个 issue 都由本 PR 关闭。
追加:第二轮裁决(2026-10-30,提交 f05a405)——连降级也撤回,可证损坏 = 拒绝启动
owner 在"不再自动改名"之后进一步裁决:回退 legacy-only 也不行——回退后 JSON
envelope 与死掉的索引库各走各的、永久分叉、无法调和;宁停勿降。据此:
shadow.tsmaybeOpen:StorageIntegrityError(quick_check 证明的库损坏,白名单分类器判定)不再吞掉降级,原样上抛 → 构造即失败 → 进程拒绝启动,
ERROR 日志带完整手动处置步骤(备份 index.db{,-wal,-shm} → 挪走 → 重启,
新库自动从 JSON envelope 重建)。启动承接已核实:server.ts:559 在 listen()
之前首调 getStore(),抛错直接上溢退出。
只读 / 第二写者 / MySQL 失败。
查出损坏时只报警+缓存,强制点在下次启动——这是有意的设计边界)。
storage-review-fixes review 3、engine-proofs 子进程场景(checks 收窄为
noAutoRename/refusesToStart/midFlowGuard/fdGuard——拒绝后不得泄漏句柄,
否则 operator 挪文件会被 EBUSY 挡)。原 [Bug][storage] corrupt index.db bricks SessionStore construction in shadow/full mode (violates the sidecar never-breaks-legacy policy) #2771 "必须存活" 被此裁决显式取代,
仅针对可证损坏;瞬态失败仍存活。
issue4816/durability/review-fixes/issue4666 24 tests 21 pass 3 skip 0 fail;
engine-proofs+integrity-cache 18/18;此前一轮:web-banner+issue-cache+
instance-governance+second-writer+rollback-mask 39/39、persist 三件套 14/14。
Fixes #4816, #4614, #4949, #4942, #4664, #4665, #4626, #5046
Supersedes(合并本 PR 后以下原 PR 请关闭,代码已全部进入):
#5123, #4955, #4987, #4670, #4672, #4630, #5075
追加:第三轮裁决(2026-10-30,提交 5159ce9)——任何情况下都不降级
Owner 裁决:“为啥打开还降级,需要在任何情况下不降级”。所有统一存储打开失败现在都拒启动(listen 前抛错退出),不再有静默 legacy-only:
startServer在 listen() 前await store.ready(),失败即process.exit(1);构造期不炸、就绪期 reject,persist.attachMysql挂mysqlReady.catch(()=>{})防 unhandled rejection。BILI_STORAGE_UNIFIED=legacy/unset、BILI_ENCRYPTION_KEY 显式 opt-out、BILI_PERSIST=0。要 JSON-only 服务就显式声明。追加:第四轮裁决(2026-10-30,提交 b9eb27a)——运行中永远不放弃索引写入;后台检查 FAIL 只报警永不拒启动
第三轮留下的两个运行期残留点,owner 本轮全部裁决:都撤掉阻断/禁用机制。
① 撤掉 ingest 熔断(3-strike self-disable):连续索引写入失败后,旧行为是第 3 次起跳过所有后续索引写(reads 翻回 legacy-only、fork 操作 fail-closed),5 分钟后再探活。新行为:
MAX_CONSECUTIVE_FAILURES/REENABLE_PROBE_COOLDOWN_MS/noteProbeFailure/clearBreaker等熔断机制整体删除——每次 save 无条件重试真实索引写(首次失败带文件名+错误详情的 warn 保留,可见性不降),读路径永远 unified,fork 操作不再因"已禁用"拒绝。约 13 处stats.disabled门控行删除(字段本身恒 false 保留,panel/wire API 形状不变)。persist 侧随之删掉 #3135① 的 breaker-中强制 JSON 补写块与writerOffActive()的 disabled 条件(jsonWriter=off 即接受 index-only 持久化,语义不变)。② 完整性检查 FAIL 改纯报警:运行期发现(大库后台 worker 检查 FAIL、扫描抛损坏)只打 ERROR 日志 + runtime-status 展示,绝不写 .chk FAIL 缓存、绝不杀进程、绝不让下次启动因此被拒。每个启动对当前文件集做自己的新鲜 quick_check,用自己的证据决定。旧版本遗留的
.chkFAIL 视同缓存失效→重扫(stale verdict 永不挡 boot)。PASS 缓存保留(跳过重复全扫)。本 boot 自己扫出可证损坏 → 仍拒绝启动(第一/二轮裁决,保持不变)。追加:第五轮裁决(2026-10-30,提交 1766e2d)——可证损坏也照常启动:只报警、文件一个字节都不动、绝不自动恢复
owner 原话(m02675):"拒绝这样做.不要搬家 不要复制 直接启动 只给警告和修复方案 不要从json恢复 有灾难性后果"。此前矩阵最后一格"boot 自扫证明损坏 → 拒绝启动"被翻掉,最终矩阵:
writerOffActive()(src/persist.ts)要求 sidecar 存活才跳过信封写入 → sidecar 没起来时即使 jsonWriter=off,每一轮也照写 JSON 信封,不存在无持久副本的窗口(JSDoc 同步更新)。return null,其余失败仍 throw 拒启动)、runtime-status.ts wire 文案(left UNTOUCHED + starting WITHOUT the index + 手工修复步骤)、store.ts/sqlite-errors/persist 注释、CONFIGURATION.md(+zh-CN) QUICK_CHECK 行按两类分别重写。UnifiedStore.open 内部仍抛 typed 错误作为层间信号(export/CLI 直面坏库时照样响亮失败)。追加:第六轮(merge origin/master,提交 b8ba5d7)——与 #5188「sidecar 可恢复打开」收敛
master 已合入 #5188(fork/snapshot 门禁):sidecar 在启动时因瞬态故障没起来的话,读路径/保存路径会按冷却窗(默认 30s)驱动重试打开——这套需求驱动的自动恢复保留,但收敛进本 PR 的最终政策:
tryRecoverSidecar只保留重试能力本身。hadIntegrityQuickCheckFailure()(src/storage/shadow.ts)。tryRecoverSidecar加了 try/catch:进程内保存/flush 钩子保持 never-throw 契约,失败仅告警并按冷却窗再试(src/persist.ts)。追加:第七轮(提交 28454a1)——收敛后 CI 红的三处收口
40bcb2f 与 28454a1240bcb2f1 与 28454a1 两个收尾提交修掉了上一推(b8ba5d7f)留给 CI 的确定性红灯:
BILI_STORAGE_UNIFIED=legacy(preload-test-env.mjs),legacy 下 sidecar 永不打开 → index-reconstruction gate 永远拿不到 sidecar → snapshot endpoint 稳定 409 replicasUnavailable。facet 现在显式 pin full。BILI_STORAGE_UNIFIED=full。tools/gen-config-docs.mjs generate四文件零 diff,check-coverage rc=0。验证:plugin-fork + multi-instance-live + export-jsonl 本地 63/63 绿(node --test --test-concurrency=1);此前全量回归(六批 ~200 测试)与 typecheck 见上轮数据。
追加:第八轮(merge origin/master,提交 78a0d4a / 7ffda72)——收敛 #5239 + #4599
master 推进了两个与存储相关的修复,已合并进本分支:
MAX(seq)聚合(sessionTipSeq)。本分支侧的裁决:保留 master 的聚合实现,去掉其中的 breaker 时代assertLive()调用(该方法随三轮裁决的熔断器一并删除)。src/persist.ts的 [Performance] v0.1.208 long sessions exceed 2 GiB memory and spend 14–22 seconds in local processing #4788 debounce-timer ref/unref 宿主修复——有等待者的 flush 判定期间保持 timer 引用;与本 PR 的 D2 spill 路径(afterNextFlush 等待者)语义兼容,自动合入。shadow.ts通用打开失败日志保留第三轮 "REFUSING TO START" 政策文案(弃 master 旧 "staying legacy-only");plugin-fork.test.ts两处 [Bug][fork] Cold restore cannot use an intact unified raw tail for a non-retained parent (#4266 item 4) #5140 pin 取 master 版(pin 内容相同、注释更准)。627c2580(storage gc full VACUUM holds the write lock across the whole O(db) rewrite with no progress — online proxy shadow disabled mid-run | storage gc 全量 VACUUM 全程持写锁且无进度:在线代理影子层被降级 #4612 CLI 边界拒绝 + storage-full-flip 重钉)已并入并验证。验证:tsc 干净;tipseq+4739+4788+inc-canonical-pointers 12/12;plugin-fork+5140+5188+multi-instance-live 62/62;完整性政策核心六件 29/29;storage-full-flip 10/10。