Skip to content

[PR] Add bili sessions orphan-sweep: remove provably-foreign collapsed blocks from upgraded sessions (D2, #5308) - #5334

Open
ranxianglei wants to merge 4 commits into
masterfrom
2026-10-31_orphan-block-sweep
Open

ranxianglei wants to merge 4 commits into
masterfrom
2026-10-31_orphan-block-sweep

Conversation

@ranxianglei

Copy link
Copy Markdown
Owner

What this fixes (D2 of #5095, tracked as #5308)

A collapsed block committed under a shared conversation id during the ≤v0.1.208 era reappears forever in the parent session after upgrading to ≥v0.1.209:

  • The #2695 zombie-reap exemption shelters it. A block whose covered substrate was never verified present on this session's wire keeps e==0, so it never accrues a death streak and is never reaped (src/fold-reconcile.ts). That exemption exists exactly so healthy proxy-mode folds (whose covered ids are permanently replaced by the anchor and never reappear) are not reaped. A foreign block from the shared-id era has the same signature — "covered ids never on this session's wire" — so the same exemption gives it permanent shelter.
  • Meanwhile the render side (kernel/src/sync.ts stillPresent) keeps the block active as long as any covered-id hash collides with the current request or its anchor is present — which happens readily when the parent and the dead child read the same files. So the foreign summary keeps being re-rendered into the parent transcript, polluting context indefinitely.
  • Once rendered into the client's persisted history the bytes become the client's own; the resent copy carries an h_ hash (not acp_summary_bN), so no proxy-side stale-copy drop can remove it afterward. There is no automatic cleanup path for provably-foreign legacy blocks.

Only upgraded sessions that lived through ≤v0.1.208 are affected (the lane split in #4774 / #5184 / #5163 means new foreign blocks can no longer be produced).

The fix — host-side sweep, option A (owner-pre-approved in #5095 / #5308)

New command bili sessions orphan-sweep [id] [--fix] [--json]:

  • Provably-foreign criterion, not a heuristic: a block is flagged when its set of covered message ids has zero overlap with its own session's unified-store transcript — reading active AND retired rows (a folded message's row is retired/tombstoned, never deleted, so it still proves the substrate belonged to this session). Zero overlap ⇒ the block references ids that never existed in this session's durable record ⇒ provably external/orphan.
  • Legit folds are structurally safe: any genuine fold of this session leaves its covered rows in the store (active or retired), so its overlap is non-empty and it is never swept. This is the negative gate that protects normal compression.
  • Dry-run by default — reports findings only, changes nothing. --fix archives.
  • Reversible archive via a metadata exclusion list: --fix records each swept block in session.metadata.orphanExcludedBlocks (blockId → {at, reason}) and persists it to the session's index.db row (advancing updated_at so the write wins the full-mode load staleness guard). buildSession drops listed blocks on load. No kstate rewrite, no schema change — deleting the entry restores the block. Idempotent (already-excluded blocks are filtered out before detection).
  • Requires full unified storage (BILI_STORAGE_UNIFIED=full); in shadow/legacy mode it degrades to read-only reporting and refuses to perform ("cannot prove", never guess).

This clears the b33-class stock. Option B (block carries creation-time conversation-key lineage — precise, no heuristic) is a kernel-boundary design item tracked separately: it needs a kernel-change declaration + owner approval and is deliberately out of scope here.

Config surface

New CLI subcommand (no new env var, no new config-file field):

Surface Added Why the existing surface can't express it Compat
CLI bili sessions orphan-sweep [id] [--fix] [--json] No existing maintenance command covers block-level provenance cleanup; this is a per-session operation, so it sits under bili sessions <sub> next to rm / archive / restore, reusing the existing --fix perform-token convention from bili doctor --fix. Additive. Dry-run by default (report-only). No-op unless invoked with --fix. Gated on full unified storage. Reversible.

Placement note: the issue proposed this as "bili doctor --fix 孤儿块清扫". I placed it under bili sessions because (a) it operates on individual sessions' blocks, matching sessions rm/archive/restore; (b) doctor --fix currently means dsh-ledger repair specifically, and overloading it would blur semantics. Same capability, same --fix token, better home — trivial to move under doctor if you prefer.

Behavior change

None at runtime. The sweep runs only when explicitly invoked. The single load-path change — buildSession now filters out blocks listed in metadata.orphanExcludedBlocks — is a no-op fast path when that key is absent (the default for every existing session), so loading behavior for all current sessions is unchanged until an operator runs orphan-sweep --fix. Old→new: state: mergeState(parsed.state) → state: filterExcludedOrphanBlocks(mergeState(parsed.state), metadata?.orphanExcludedBlocks); identical output whenever the exclusion list is empty.

Verification

Closes #5308

Related: #5095 (source, D1 closed) · #5184 / #5163 (D1 two-layer fix) · #2695 (e-gate origin) · #3640 (anchor parking) · #4774 (lane split)

ework-agent and others added 2 commits October 31, 2026 16:16
…WN_FINGERPRINT in LIFECYCLE.md (#4599)

The lifecycle-ledger-guard job fails on every tree containing master since c8d7a80 (#4788 phase 2) and fc53fd4 (#4125): two BILI_* env vars read from src/ were never registered in LIFECYCLE.md, so every PR based on current master goes red within seconds (first observed on #5037).

Backfill only, no code change: BILI_SESSION_CONTENT_STORE_PIN -> section 1 transition ledger (escape hatch for #4788 phase 2 idle-fold, default fold); BILI_SPAWN_FINGERPRINT -> section 2.2 internal spawn channel (#4125 spawner-side entry-script hash, launcher to child).
…(D2, #5308)

A collapsed block committed under a shared conversation id during the <=v0.1.208 era reappears forever in the parent session after an upgrade: the #2695 e-gate exemption shelters it (never-present substrate never accrues a death streak) while the render side keeps it active on any covered-id collision. No wire-side path reaps it.

Add a host-side maintenance sweep (bili sessions orphan-sweep [id] [--fix]) that flags blocks whose covered ids have ZERO overlap with their own session's unified-store transcript (active + retired rows) - provably foreign. Dry-run by default; --fix archives via a metadata-backed exclusion list that buildSession honors on load (reversible, no kstate rewrite). Legit folds always retain their covered rows, so they are never swept.
@github-actions

Copy link
Copy Markdown
Contributor

📦 Built Package Artifact

Branch: 2026-10-31_orphan-block-sweep (29fde22)

Latest build — install this exact version

npm install -g billion-context@0.0.7472-pr.5334.2c0aa80

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

Recent builds (commit → version)

commit version build
29fde22 billion-context@0.0.7472-pr.5334.2c0aa80 build

Download artifact

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

Install instructions are refreshed on each push.

@ranxianglei ranxianglei changed the title Add bili sessions orphan-sweep: remove provably-foreign collapsed blocks from upgraded sessions (D2, #5308) [PR] Add bili sessions orphan-sweep: remove provably-foreign collapsed blocks from upgraded sessions (D2, #5308) Oct 31, 2026
@ranxianglei ranxianglei added the p1 label Oct 31, 2026
@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

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

查重: 不重复 —— 本 PR 是 #5308-D2(#5095「孤儿块清理」第 2 项)的唯一实现;按失败签名与关键词检索 open+closed issue/PR,无并行工作。

标签: bug + scope/storage + p2(从原 p1 下调:这是 ≤v0.1.208 时代的有限存量污染,不是缓存泄露类只增不减的失控增长;受影响会话仍可用、有绕行,符合 p2 口径)+ 保留 pr。

中间状态(代码审核已完成,CI 红分账已完成):

  1. 代码侧:7 个文件的实现已逐项对照既有行为核验(归档会话不会误伤、retired 行不删故零重叠判定成立、staleness guard 保证 --fix 不被旧 JSON 回灌等),未发现缺陷;新测试带双向断言(Test Blind Spot Retrospective: Reinforcing Negative Assertion Discipline, Tightening Fake Upstream Strictness, Thin Inst | 测试盲区复盘:补反向断言纪律、假上游严格化、薄安装冒烟、形态矩阵与生命周期覆盖 #3617 纪律)。
  2. CI 红(head 29fde22)共 3 条静态门,已在纯净 master f6e69ae 与 PR head 两棵树上本地复跑确认分账:

修复合入后等 CI 重跑,再给最终八问结论。

@ranxianglei ranxianglei added bug Something isn't working p2 scope/storage labels Oct 31, 2026
ci-check added 2 commits October 31, 2026 18:09
…inding, OrphanSweepPlan)

The two helper interfaces are only used inside src/orphan-sweep.ts; the
module-boundary surface is detectOrphanBlocks/applyOrphanSweep/OrphanSweepResult.
Self-introduced violation of the #2643-era gate on this PR branch (the other
finding, CodexWindowSource, is inherited master red awaiting ci-fixes).
@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

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

最终结论(八问)

  1. 是什么问题:≤v0.1.208 共享会话 id 时代,折叠块可能被提交到「不属于它」的会话名下;升级后这些块在父会话里永久复活渲染——每次请求都带着幻影内容,污染上下文、白烧 token。此前没有任何清理路径(Claude Code sub-agents of the same type share a single compaction lane: unrelated sessions bleed content into each other | Claude Code 同类型子代理共用一条压缩车道:无关会话互相串内容、折叠暂停;旧版共享父 id 的折叠块升级后仍残留在父会话 #5095 的 D2)。本 PR 新增 bili sessions orphan-sweep(默认 dry-run,--fix 归档)并在加载时过滤被排除的块。
  2. 什么原因导致的:因果链两层——(a) 产源:共享 id 时代把 A 会话的折叠状态写进了 B 会话的记录;(b) 为何杀不掉:两个机制互相庇护——kernel e-gate 对「substrate 从未出现过」免计死亡连击 + render 侧 stillPresent 只要任一 covered id 撞上父会话读过的消息就判 active;而唯一能收尸的 orphan-gc 只看当前 wire,这类碰撞恰好落在 wire 上。本 PR 的判别器改用存储存在性(与 wire 存在性独立):covered ids 与会话 unified-store transcript 行零重叠 ⇒ 可证外来;合法折叠必然保留其 covered 行(retire 不 delete),故永不被误伤。
  3. 影响范围与严重程度:仅 ≤v0.1.208 升级来的存量会话(新版本不再生成新孤儿);受影响会话仍可用,只是上下文带幻影内容 + token 成本;有限存量、非只增不减 → p2(已从 p1 下调,理由见上条回复)。--fix 仅在 full 统一存储模式生效,shadow/legacy 只报 performable=false 不动数据。
  4. 怎么修复的:检测 = 逐会话比对 block.effectiveMessageIds 与 store transcript msg_ids(active+retired,两侧经 baseIdOf 归一);transcript 为空的会话直接跳过(判据空真,防误报)。修复 = 写 metadata.orphanExcludedBlocks 排除清单(可逆、不改写 kstate),buildSession 加载时按清单过滤 state.blocks。测试是真行为断言且双向齐全(Test Blind Spot Retrospective: Reinforcing Negative Assertion Discipline, Tightening Fake Upstream Strictness, Thin Inst | 测试盲区复盘:补反向断言纪律、假上游严格化、薄安装冒烟、形态矩阵与生命周期覆盖 #3617):外来块被抓/重叠的合法块不被抓、dry-run 无副作用、--fix 只归档外来块、重载后外来块消失且合法块保留、幂等、清掉清单即还原——全部跑在真实 full-mode store 上,不是恒真断言。
  5. 是否有回归问题:逐项审过的既有行为——① 归档会话:loadAll() 枚举含 tombstone 的全量行,但归档会话无 transcript 行 → 命中跳过分支,不会误伤;② 「retire 永不 delete」前提:src/storage/store.ts 全部 4 处 DELETE FROM messages 逐一核验——ingest-repair 只删 retired 字节重复且 live twin 同 msg_id 存活、另两处是整会话删除/整会话归档,均不产生「行没了但会话还在」→ 零重叠判定成立;③ staleness guard:full 模式 buildForLoadAll 仅当 JSON savedAt > dbAt 才回灌旧 JSON,sweep 写入 updated_at=now 使 DB 行胜出,排除清单不会被旧文件回灌;④ 排除清单缺失/畸形/空 = no-op fast path,对无此键的正常会话零影响;⑤ 未触碰 kernel/、wire 格式、配置面、消息 ref/id 逻辑。已知局限(与既有 sessions rm/archive、storage gc 同类,非本 PR 引入):持有 --fix 之前内存态的宿主进程若在 --fix 之后落盘会覆盖排除清单,直到该会话下次 fresh load——PR 正文已声明「next load/restart 生效」,属离进程维护命令的共同模型。
  6. 是否破坏缓存:不影响前缀缓存/重试/去重键——过滤发生在加载期(state.blocks 收缩),出站渲染照常走服务端正史;排除清单存 metadata_json,不参与 wire 身份与 checkpoint。
  7. 是否改了设计方向:没有。Option B(块携带创建时 lineage)明确留作 kernel 侧独立项(Legacy foreign fold blocks linger forever in parent session after upgrade | 旧版共享父 id 的外来折叠块升级后在父会话永生残留 #5308),本 PR 只做 host 侧维护命令 + 加载期过滤,与现有 storage-maintenance 命令族一致。
  8. 是否引入新配置:无 env/config 字段。新 CLI 子命令 bili sessions orphan-sweep [id] [--fix] [--json];复用既有 --fix flag 名(与 doctor 的 --fix 互斥,因两条命令本身互斥)。

CI 现状与处置(head 29fde22 → 新 head 2d8f680 已推送):3 条静态门红 = 双树本地复跑分账——unused-exports 里 2 条本 PR 自带(OrphanBlockFinding/OrphanSweepPlan 无模块边界引用)已按门禁处方在分支修掉(commit 978ab77);lifecycle-ledger-guard 继承红已 merge ci-fixes@0371089db 转绿(本地 OK);docs-sync(#5319 ±"chat" 种子漂移)与 CodexWindowSource 两条继承红待哨兵收敛进 ci-fixes,已在 #4599 留言。test 矩阵此前因门红被 skip,尚无全量套件判定——等收敛后重跑即为终审证据(本地未跑全量,遵本地测试纪律)。

判定:代码本身审核通过、可合;当前被 2 条继承红阻塞,哨兵把 docs-sync 种子修复与 CodexWindowSource 收敛进 ci-fixes 后,分支再 merge 一次即可全绿合入。

This branch has not been deployed

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

Labels

bug Something isn't working p1 p2 scope/storage

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Legacy foreign fold blocks linger forever in parent session after upgrade | 旧版共享父 id 的外来折叠块升级后在父会话永生残留

1 participant