Repository navigation
Description
Description *
What happened
I forked a long bili-compressed session from the DSH Web UI ("在新会话中分支" / fork into a new session). The child session came up with no compressed blocks at all, while the parent's entire history — including its compress tool calls — was replayed verbatim into the child's context.
Net effect: every token bili had folded away came back as raw messages. Context jumped from <10% to ~35% of the window on the first turn of the child session, and search_context / acp_status report "no compressed blocks" so there is no way to recover the old folds from inside the child.
Environment
- billion-context
0.1.202(6deb454), DSH host (desktop Web GUI), Windows 11 / pwsh 7 storage.unified=shadow(default); noindex.dbon disk, JSON envelope is the source of truthforkAdoptionwas not set (so it took the defaultfalse);resumeInheritancealso not set (defaulttrue)- Neither
BILI_FORK_ADOPTIONnorBILI_RESUME_INHERITANCEpresent in the environment
Steps to reproduce
- In a long session, run
compressa few times untilacp_statusshows N active blocks (mine had ~300K original tokens folded). - Use the host's "fork into a new session" action on that session (DSH writes
parentSession+isSeeded: trueinto the child's journal header). - In the child, call
acp_status.
Expected
The child inherits the blocks whose source messages are fully present in the replayed context (that is exactly what forkAdoption's doc string promises), or at minimum search_context still resolves the parent's blocks.
Actual
acp_status:COMPRESSED BLOCKS — none, with the fullm00001–m00465range (465 messages, 381.4K) listed as compressible again.search_context("<anything>"):[No compressed blocks exist yet — nothing to search.]acp_cache: 0 folds,compress re-pay0 tok — bili does not even register that it had folded this content before.- The child's own journal literally contains the parent's
tool/callevents forcompressat the same seq numbers and timestamps as the parent (seq 249/293/298/367/519/636/975/982), then diverges. So the history replay is confirmed; only the block index is missing.
Root cause (from reading dist/index.js of 0.1.202)
Two separate adoption paths exist, and only the one that cannot fire mid-session is enabled by default:
- Startup sweep —
reconcileDerivedLineage(listSessions(), {log, adoptionEnabled: opts.resumeInheritance !== false}), called once in the boot sequence right afterinitSessions(), logging[lineage-reconcile] restored fold state for N forked session(s).resumeInheritancedefaults to true, so this path is on — but it only runs at proxy startup, so a fork created while the proxy is already running is never seen by it. - Runtime adoption —
maybeAdoptForkBlocks({session, parentId, protocol, parsed, upstreamOrigin, enabled: opts.forkAdoption === true, log}), gated onanonAffinity?.via === "new" && anonAffinity.lineage?.reason === "forked" && session.stats.requests === 0. This is the path that actually fires for a live fork, and it is disabled becauseforkAdoptiondefaults to **false`:
if(!enabled){
log2("info",`[fork-adoption] fork of ${parentId}: parent has ${plan.adoptedActive} adoptable block(s) covering ~${plan.adoptedTokens} tokens; forkAdoption disabled, starting fresh (set forkAdoption: true or BILI_FORK_ADOPTION=1)`);
return
}So by design the runtime path bails out and "starts fresh". Combined with the replay of the parent's compress events, "fresh" means un-compressed: the events say blocks were created, the state says there are none.
Note this is not the same shape as #629 (anonymous fork with no adoption support) — here the adoptable blocks exist and planForkAdoption computes them; the feature is simply off by default.
Suggested fix
- Default
forkAdoptionto true, or at least piggyback: whenresumeInheritanceis true, let the runtimemaybeAdoptForkBlockspath inherit blocks too (it already hasplanForkAdoption's output in hand). - If the feature stays opt-in, make the degradation visible rather than silent: when the incoming request clearly replays prior
compressevents butstate.blocksis empty, surface a one-line warning inacp_status(e.g. "child session of<parent>— N blocks not inherited; enableforkAdoption") so users are not left guessing why their context doubled. - Consider
search_contextfalling back to the parent session's store (thederivedAncestorSessionswalk already exists inexecuteSearchContextTarget) instead of short-circuiting on an empty localblocksarray.
Workaround
Re-run compress in the child immediately after forking. It works (I re-folded 7 blocks / 431.5K original tokens in one call), but you pay the full re-read of the unfolded history for one turn, which is the exact cost bili exists to avoid.
Happy to test a patch — the reproduction is 2 clicks.
bili version *
0.1.202
Usage mode *
Proxy mode (client manually points its base URL at bili)
Client and version *
dsh v0.2.1-rc.2
Operating system *
windows 11
Upstream and model *
workbuddy/deepseek-v4.1-flash 1M
Compression-related config (optional)
Log excerpt (strongly recommended)
/acp or acp_status output (optional)
Activity
ranxianglei commentedon Oct 20, 2026
(ai: vllm-qwen/qwen3.8-27b)
查重: 同症状家族 #3011 — 并入该 umbrella。 Per the owner's 2026-10-13 policy (same user-visible symptom = ONE issue, even when the underlying mechanisms differ), this is the same "fork inheritance lost → zero-fold child" problem #3011 tracks. Your mechanism (proxy-lane anonymous fork with
forkAdoptionoff by default) differs from #3011's (plugin-lane 409 retryability classification) — so it goes in as a new checklist item (C9) in #3025 rather than a separate issue. First-come-first-served: #3011 (Oct 13) predates this one (Oct 20), so I'll migrate your full finding there and close this issue once the fix PR is up — your repro details are preserved in the migration comment. If you disagree with the merge, say so and I'll reopen.标签: bug, dsh, scope/kernel, p1 — "fork 继承被永久放弃" is a named p1 category per the owner's 2026-10-19 revision (session-recovery/data-loss family); scope/kernel because this is session-core fold state, not storage mechanics.
Your root-cause reading is correct — verified against source at v0.1.202 (your exact build):
forkAdoptiondefaults to false (src/config.ts:1459: true only when the file setsforkAdoption: trueorBILI_FORK_ADOPTION=1).src/server/handle.ts:1425, gatevia === "new" && lineage.reason === "forked" && requests === 0) and bails becauseenabled: opts.forkAdoption === trueis false.reconcileDerivedLineageruns startup-only and only sees sessions carrying a persistentderivedFromSessionIdlink.One addition beyond your analysis — it's worse than "until next restart": when the runtime path bails (adoption disabled), it records no persistent lineage at all. Consequences:
derivedFromSessionId), so a restart does NOT heal it either;search_context/decompressuse (thederivedAncestorSessionswalk insrc/decompress-shared.ts) is keyed off that same link, so it never engages for this child.So under default config the defect is permanent for the child's lifetime, not merely until restart. (After a restart the child's replay matches its own stored chain, so the fork-detection gate doesn't re-fire either.)
One nuance, explicit: in proxy mode DSH sends no recognized session header, so the child becomes an anonymous
pfa-*session — which is why path 1 (anonymous fork) is the one involved, not the identified resume-fork path. Your dist read plus the m00001-starting refs corroborate this.Plan:
acp_statuswarning line naming the parent and the N blocks not inherited, with how to enable adoption. That makes the failure diagnosable from the GUI, lets the existing boot sweep heal the child across a restart, and restores thesearch_context/decompressparent fallback for free (your suggestion 3 — the ancestor walk already exists, it just needs the link).forkAdoptionshould default to true / followresumeInheritance. Options + recommendation go to @ranxianglei in the [Bug] Fork of DSH branch clone: inheritance permanently abandoned by 409 SNAPSHOT_UNAVAILABLE — zero-fold clone triggers context_length_exceeded 400 on first turn | [Bug] DSH 分支克隆的 fork 继承被 409 SNAPSHOT_UNAVAILABLE 永久放弃:克隆零折叠,首轮即 context_length_exceeded … #3011 thread.@Leeszzz — glad you're up for testing: I'll post the installable artifact in the #3011 thread once CI is green. A 2-click repro is exactly what the new regression tests will encode.
ranxianglei commentedon Oct 20, 2026
(ai: vllm-qwen/qwen3.8-27b)
查重:重复 #3011(同症状族:fork/克隆失去继承的压缩状态 → 零折叠子会话;按 owner policy 2026-10-13,同症状 = 一个 issue,内部按机制 checklist 管理)。本 issue 的机制与 #3011 主体不同(那边:插件车道 409 可重试性误分类,#3024/#3039 已修/在途;这边:代理车道匿名 fork 继承 off-by-default + 缺 lineage 链接)→ 实质已迁移为 #3025 checklist 新项 C9,本 issue 按先来后到关闭(#3011 开于 10-13,早于本 issue 的 10-20,先到保留、后到关闭并补信息)。@Leeszzz 后续请在 #3011 线程跟踪;你提过愿意测补丁——PR #3951 的 CI 构建出来后可以直接用你的两次点击复现验证。
标签:bug, dsh, scope/kernel, p1, duplicate(fork 继承被永久放弃属 owner 2026-10-19 修订的 p1 类别①——压缩状态不手动干预无法恢复;范围是会话核心/上下文管理逻辑而非存储机制)。
八问:
10%→35%),search_context 找不到任何块。maybeAdoptForkBlocks被forkAdoption===true门控,而它默认 false(src/config.ts:1459)——你对 dist/index.js 的分析完全正确。metadata.derivedFromSessionId的会话,读时 decompress/search_context 的父链遍历也走同一字段——匿名 fork 从不写这个字段(只有 identified 车道的 Claude 上 resume 导致消息错乱 #1486/[pi plugin] Prime RLM inline 派生子会话在代理模式下零继承:搜不到也解压不了父会话压缩块(billion-context-pi#531 同场景) #1333 路径写)。所以重启 bili 也不会愈合,且重启后子会话重放匹配自己的存储链,fork 检测不会再次触发。「到下次重启为止」的判断偏乐观:实际是永久的。收工判定:本 issue 作为 #3011 的重复关闭(实质现为 #3025 的 C9);缓解 PR #3951 open(CI 待跑);默认值决策已在 #3011 提出。
fix: link anonymous forks to their parent so skipped fold-inheritance…
fix: link anonymous forks to their parent so skipped fold-inheritance…
fix(gate): force chunked corpus reads at t1 + accept fresh-start fork…