Skip to content

[PR] fix: link anonymous forks to their parent so skipped fold-inheritance heals across a restart - #3951

Closed
ranxianglei wants to merge 3 commits into
masterfrom
2026-10-20_anon-fork-lineage-link
Closed

ranxianglei wants to merge 3 commits into
masterfrom
2026-10-20_anon-fork-lineage-link

Conversation

@ranxianglei

Copy link
Copy Markdown
Owner

What & why

Proxy-mode anonymous forks ("new session from here" when the client sends no session id) are detected at the first request by prefix-affinity (LCP >= 3, via:"new", reason:"forked") but were only fed into maybeAdoptForkBlocks — which bails silently when forkAdoption is off (the default). No lineage metadata was recorded at all, so neither existing recovery path could ever see the fork:

  • the startup sweep (reconcileDerivedLineage) iterates sessions carrying metadata.derivedFromSessionId only;
  • the read-time decompress/search_context parent walk follows the same field.

Result: a child of a folded parent replayed the entire parent history (including the parent's own compress tool calls), context re-inflated (the whole folded span showed up compressible again), and search_context reported "no compressed blocks". It stayed broken across restarts too — after a restart the child's replay matches its OWN stored chain (via:"match"), so fork detection never re-fires. Repro + full evidence: #3925 (closed as duplicate of #3011; tracked there as checklist item C9 in #3025).

Changes

  1. Record the lineage link on every detected anonymous fork whose parent loads (memory or disk), before the outcome branches, made durable immediately via writeNow (same rationale as persistLineageLinkNow, [Bug] 血缘元数据落盘窗口:derivedFromSessionId 可能未持久化(非优雅退出/多实例覆写),kind 子会话永久孤儿 #2986). A bili restart then lets the EXISTING startup sweep inherit the parent's fully-present blocks, and read-time search/decompress fall back to the parent chain for free — no new search mechanism.
  2. Disabled bail: persist a forkAdoptionSkipped {parentId, blocks, tok} marker; log upgraded info -> warn.
  3. acp_status (no-scope views): one conditional FORK ADOPTION SKIPPED line naming the parent, the block/tok counts and how to enable adoption; suppressed once the child has any active block; malformed markers ignored.
  4. Success path sets derivedBlocksAdopted = true (Forked child permanently loses block inheritance when its registered parent had no blocks at first contact (weak-link gate closes unconditionally) | 注册父会话在子会话首触时还没有折叠块,子会话将永久失去块继承(弱链接门无条件关闭) #3351 discipline — the gate closes only on real block adoption).

Adoption semantics are unchanged: raw-id set intersection, no positional fuzzy matching, copy-on-fork deep copy. A mis-guessed parent (two chains sharing a 3-item prefix) can at most be searched read-only, never adopted from.

Behavior changes (old -> new)

surface old new why
anon-fork child lineage no persistent link unless adoption succeeded persistent derivedFromSessionId whenever the parent loads, even when adoption is off the core fix: enables restart healing + read-time fallback
bail log level info warn the degradation was invisible (#3925)
acp_status "COMPRESSED BLOCKS - none" + one warning line while the child has zero active blocks user-visible gap + remediation hint
success-path marker unset derivedBlocksAdopted = true consistent with #3351; sweep skip already held via blocks>0

Deliberately NOT included

Flipping the forkAdoption default (the root cause of the report) is a product decision — options A/B/C are on the table in the #3011 thread for owner sign-off. This PR makes the off-by-default state diagnosable and self-healing instead of permanent and invisible.

Testing

  • Targeted local (local-test discipline): tests/fork-adoption.test.ts (new D2 assertions in both scenarios), tests/acp-status.test.ts (new negative / positive / malformed / suppression rows), plus adjacent tests/lineage-reconcile-boot.test.ts (boot-sweep AC9 intact) and tests/issue1333-derived-inheritance.test.ts — 23/23 pass; typecheck clean. Full suite = CI.
  • Gate discipline: the new status line is asserted in BOTH directions (appears exactly when skipped AND zero active blocks; absent otherwise).

Refs #3925 (duplicate of #3011), #3011, #3025 (C9). No config-surface change; no kernel change; touches session-lineage logic -> human review lane (AGENTS.md 7.4).

@ranxianglei ranxianglei changed the title fix: link anonymous forks to their parent so skipped fold-inheritance heals across a restart [PR] fix: link anonymous forks to their parent so skipped fold-inheritance heals across a restart Oct 20, 2026
@github-actions

github-actions Bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

📦 Built Package Artifact

Branch: 2026-10-20_anon-fork-lineage-link (fe92959)

Option A — Install this exact build (recommended)

npm install -g [email protected]

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-pr3951.tgz
npm install -g package

Install instructions are refreshed on each push.

This was referenced Oct 20, 2026
awork added 2 commits October 23, 2026 11:55
… heals across a restart (#3925 -> #3011 C9)

Proxy-mode anonymous forks ('new session from here' with no session id) were
detected at the first request (prefix-affinity LCP >= 3, via='new',
reason='forked') but only fed into maybeAdoptForkBlocks — which bails silently
when forkAdoption is off (the default). No lineage metadata was recorded, so
neither the startup sweep (reconcileDerivedLineage iterates
derivedFromSessionId entries only) nor the read-time decompress/search_context
parent walk could ever recover the lost block state. The child replayed the
whole parent history (including the parent's compress tool calls), context
re-inflated, and search_context reported 'no compressed blocks'.

- record derivedFrom/derivedFromSessionId on every detected anonymous fork
  whose parent loads (memory or disk), before the outcome branches, and make
  it durable immediately (writeNow) like persistLineageLinkNow (#2986);
  a bili restart then lets the existing sweep inherit the parent's fully-
  present blocks, and read-time search/decompress fall back to the parent
  chain for free
- on the disabled bail, persist a forkAdoptionSkipped marker {parentId,
  blocks, tok} and upgrade the log to warn so the degradation is visible
- acp_status (no-scope views) prints one FORK ADOPTION SKIPPED line naming
  the parent, the block/tok counts and how to enable adoption; suppressed
  once the child has any active block; malformed markers are ignored
- success path sets derivedBlocksAdopted = true (#3351 discipline)

Behavior changes (old -> new):
- anon fork children: no persistent lineage -> persistent parent link even
  when adoption is off (enables restart healing + read-time fallback)
- bail log: info -> warn
- acp_status: gains the conditional warning line (additive display)
- success path: derivedBlocksAdopted now set (sweep skip already held via
  blocks > 0; marker keeps the gate consistent)

The forkAdoption default itself is deliberately untouched here — flipping it
is a product decision tracked in #3011.

Refs #3925, #3011, #3025
… rename linked->justLinked

Review pass on this PR found two small flaws:
- The status line told users to 'set forkAdoption: true and restart', but the
  startup sweep is gated by resumeInheritance (on by default), NOT by
  forkAdoption — a plain restart already heals the session under defaults.
  Reworded: restart heals now; forkAdoption:true makes future forks adopt at
  first request instead of waiting for a restart.
- linked -> justLinked in maybeAdoptForkBlocks: the old name read as
  'already linked' while it means 'link just created'.
@ranxianglei
ranxianglei force-pushed the 2026-10-20_anon-fork-lineage-link branch from dbb1fb9 to fe92959 Compare October 23, 2026 04:03
@ranxianglei ranxianglei added bug Something isn't working p1 scope/kernel labels Oct 23, 2026
@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

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

查重: 不重复。本 PR 是伞 issue #4266 第 ⑥ 项(proxy-lane 匿名 fork 零折叠)的缓解切片——#3025 清单里的 C9,复现 #3925(已按重复关闭并入 #3011)。与同家族其余在途 PR 机制正交、无重叠:#3024(409 快照)、#3039(CHILD_CONFLICT latch)、#4223(DSH native 入口诊断)、#4263(DashScope 溢出)、#4345(能力探测瞬态)。

标签: bug p1 scope/kernel(保留 pr)——缺陷类;按 owner 2026-10-19 修订「fork 继承被永久放弃」属 p1 类;领域为会话核心/折叠继承(src 侧压缩决策面)。匿名 fork 车道与客户端无关(#3925 未给出具体客户端证据),不打客户端标签。

审核结论: 可合并(人工审核 lane——§7.4 会话血缘逻辑,不走 auto-merge)。已 rebase 到当前 master 并补了一个 review 小修 commit(fe92959),详见下文。

结构化八问

  1. 是什么问题:proxy-mode 客户端不带 session id「从这里开新会话」时,bili 能认出这是 fork,但 forkAdoption 关闭时(默认值)不仅跳过折叠继承,连父子关系都没记下来——子会话永远逐字重放父会话已折叠的历史,上下文重新膨胀、search_context 报「没有压缩块」,而且重启后也修不好。
  2. 什么原因导致的:血缘链接(derivedFromSessionId)过去只在 adoption 成功路径里写;而两条既有恢复通道(启动清扫 reconcileDeriveLineage、读时父链回溯)都只认这个字段——adoption 一 bail,两条通道永远看不到这个 fork。再叠加:重启后子会话的重放匹配自己的存储链(via:"match"),fork 检测不再触发,所以永久坏死。
  3. 影响范围与严重程度:所有「父会话已折叠 + 不带 session id 新开会话」的 proxy-mode 用户,默认配置(forkAdoption=false)即命中。不是完全不能用,但体验毁灭级:上下文重新膨胀并从头再折一遍(成本+延迟),读时检索/解压拿不到父块。符合 p1「fork 继承被永久放弃」。
  4. 怎么修复的:在检测点(确认父会话可加载后)立即写 derivedFrom/derivedFromSessionId 链接并用 writeNow 即时落盘(不等 debounce,同 [Bug] 血缘元数据落盘窗口:derivedFromSessionId 可能未持久化(非优雅退出/多实例覆写),kind 子会话永久孤儿 #2986 理由),让两条既有恢复通道免费生效——重启后启动清扫继承父会话 fully-present 块,读时 search/decompress 回退父链。bail 分支另存 forkAdoptionSkipped{parentId,blocks,tokens} 标记、日志 info→warn;acp_status(no-scope 视图)在子会话 0 active block 期间加一行 FORK ADOPTION SKIPPED 警告(有任一 active block 即抑制,畸形标记忽略)。adoption 语义不变:raw-id 集合交集、无位置模糊匹配、copy-on-fork 深拷贝;父猜错(两条链共享 3 项前缀)最多被只读检索到,绝不会被采纳。
  5. 是否有回归问题:逐项审过既有行为——
  6. 是否破坏缓存:不涉及。前缀缓存/baseline/snapshot 逻辑零触碰;改动只有元数据链接、一条日志级别和一行条件警告。
  7. 是否改了设计方向:没有。明确不引入新恢复机制,复用既有 sweep + 读时回溯(PR 自述「no new search mechanism」,与实现一致)。
  8. 是否引入新配置:没有。复用现有 forkAdoption/BILI_FORK_ADOPTION,无新字段。

行为变更披露(old → new)

面 旧 新 原因
anon-fork 子会话血缘 仅 adoption 成功才持久化链接 只要父会话可加载就持久化(即使 adoption 关) 本修复核心:启用重启自愈 + 读时回退
语义后果:显式设 forkAdoption:false 的用户 运行时不继承,重启后也不会有任何动作 运行时仍不采纳,但重启后启动清扫会继承(受默认开的 resumeInheritance 门控) 正是 #4266 ⑥ owner 认可的缓解方向;副作用:D1 选项 C(保持默认 false)在本 PR 合并后明显变弱,建议作为 D1 拍板输入
bail 日志级别 info warn 降级不可见(#3925)
acp_status 「COMPRESSED BLOCKS — none」 子会话 0 active block 期间 +1 行 FORK ADOPTION SKIPPED 用户可见缺口 + 补救提示
成功路径标记 未设置 derivedBlocksAdopted=true 与 #3351 一致

审核操作记录

  • Rebase:原分支落后 master 237 commits(base 6f2b113 → 994a467),rebase 无冲突;触碰文件在 master 侧的并发变更(fork-adoption.ts ×4 含 [Bug] MCP failed compression record rotation prematurely rewrites the history prefix, triggering expensive cache recompu | [Bug] MCP 失败压缩记录轮换提前改写历史前缀,触发大额缓存重算 #3769 orphan-ledger、acp-status.ts 的 keepalive 块)逐一核对,与本 PR 无交互。
  • 本地定向验证(local-test 纪律):typecheck clean;fork-adoption / acp-status / lineage-reconcile-boot / issue1333-derived-inheritance 四套件 23/23 pass。全量 = CI:新 head fe92959 上 22 个 check 已排队,结果出来我再补一条。
  • review 顺手修了两处小问题(独立标注 commit fe92959):① 原 acp_status 警告行让用户「设 forkAdoption:true 并重启」,但清扫门控实际是 resumeInheritance(默认开)——默认配置下单纯重启就能自愈,措辞已改为「重启即可修复;开 forkAdoption 让以后的 fork 首请求即采纳」;② linked → justLinked(原命名读起来像「已链接」,实际含义是「刚创建」。两处均经上述定向测试验证。
  • Gate 纪律:新警告行双向断言齐(出现当且仅当 skipped 且 0 active block;否则缺席)✓
  • 残余覆盖说明(不拦合并):adoptedActive===0 分支(父会话无任何 fully-present 块、仅建链)无专门测试场景——构造需要新 fixture(子从压缩区间内部 fork),非几行工作量;该分支未测增量仅为日志行选择,link+persist 机制本身已被另两个场景钉住。留作后续判断。
  • 无 kernel 变更、无配置面变更、metadata 为自由 Record 字段(无持久化版本 bump)。

收工判定:可合并——机制正确、回归清单干净、测试双向钉住;待 CI 绿确认后由人合入(我不代合)。

@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

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

CI 进度补报:新 head fe92959 上 9/10 条 lane 已绿(e2e-native-dsh / e2e-local-registry / e2e-native-opencode / e2e-fake-codex / e2e-native-pi / e2e-codex-persona / e2e-pi-codex-ws / e2e-dsh-plugin-lane / Windows codex overlay 全部 success)。主 CI(test shards)在 runner 队列里排了约一小时还没开跑——是排队不是失败。出结果我再补最终一条,无需任何人操作。

CI unused-exports gate failed on this branch: ForkAdoptionSkipped was
exported but never imported anywhere (tests reference it via
ReturnType<typeof forkAdoptionSkippedOf>). Drop the leading export per
the gate's INTERNAL_ONLY remedy. Verified locally: gate clean,
typecheck clean, targeted suites 10/10.
@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

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

CI 补报 ②:主 CI 里 unused-exports gate 红了——PR 原提交导出的 ForkAdoptionSkipped 类型全仓无人 import(测试走的是 ReturnType<typeof forkAdoptionSkippedOf>),被 gate 判为 INTERNAL_ONLY。已在分支上补一个独立标注 commit(17595ee):按 gate 的 INTERNAL_ONLY 处置法去掉 export(类型仍被 forkAdoptionSkippedOf 的返回签名使用,同文件引用不受影响)。本地已验证:gate clean + typecheck clean + 定向套件 10/10。新 head 上 CI 重跑中,出结果再补最终一条。

@ranxianglei

Copy link
Copy Markdown
Owner Author

(ai: zhipuai-lb/glm-5.3)

已并入 #4590。关单收尾;误判请 reopen。

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 scope/kernel

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant