Skip to content

feat(#2399): adopt dsh fork-child state via the plugin fork protocol - #2403

Merged
ranxianglei merged 8 commits into
masterfrom
2026-10-08_dsh-fork-adopt
Oct 8, 2026
Merged

ranxianglei merged 8 commits into
masterfrom
2026-10-08_dsh-fork-adopt

Conversation

@ranxianglei

@ranxianglei ranxianglei commented Oct 8, 2026 •

Copy link
Copy Markdown
Owner

Closes #2399. Stage 1 of the L0 plan in #2394 (four hosts with fork lineage signals; dsh first because #2383 is a live incident).

What

dsh fork children (header.isSeeded + parentSession) replay their parent's event prefix under a fresh session id. That replay landed as a brand-new conversation, so preflight refolded the entire inherited history from scratch — the non-converging burn documented in #2383. The dsh native extension now adopts the parent's compression state through the existing plugin fork protocol (PLUGIN.md §8, #2077) BEFORE the child's first stamped model request, so the very request that carries the replay already rides the inherited folds.

How

  • src/agent/fork-adopt.ts (new): the client half of the fork protocol. It projects the outgoing OpenAI body exactly like the server's incomingCoreMessages openai branch (clone → strip ACP panel echoes → strip status markers → openaiToCore), matches the longest parent prefix by identity hash, and posts the fork receipt. The hash recipe (forkStableJson/forkIdentityHashOf/forkOrderHashOf) is a byte-identical replica of src/plugin.ts — the server's publicForkInputMatches 409-rejects a replay whose prefix drifts from the receipt, so parity is pinned by tests against the now-exported forkMessageIdentityHash. Degradation matrix: snapshot 404/malformed/error, zero-length prefix match (never posts — poisoning guard), fork 4xx/5xx, network errors — all return to today's behavior; PARENT_REVISION_CONFLICT retries once with a fresh snapshot.
  • src/agent/native-intercept.ts: new async beforeSend hook awaited after the tools gate and before headersFor in both the routed /bili/ and rewrite branches; hook errors are logged and never break the request.
  • src/agent/dsh-native.ts: AgentLike gains session.header; per-sid single-flight adoption with a 3-attempt cap; gates: register.toolsReady (wire mode skips), isSeeded === true (dsh subagents carry parentSession WITHOUT isSeeded — they run fresh task contexts and must not inherit), manifest capabilities.fork advertisement (old proxies skip cleanly = today's behavior). dsh's main adapter is OpenAI wire (/chat/completions), which is what the projection covers; other wire shapes degrade safely to N=0.
  • src/plugin.ts: export forkMessageIdentityHash (parity anchor only, no behavior change).

Tests

  • tests/dsh-fork-adopt.test.ts: hash parity vs the server function (8-message identity matrix incl. toolIsError/thinkingTokens/summaryOfBlockId), orderHash recipe, projection units (system hoisting, marker strip, malformed bodies), matchForkPrefix boundaries, the full adoption/degradation matrix against a scripted proxy (incl. revision-conflict retry and never-post-on-zero-match), manifestForkCapable caching, and a full-chain test against a real proxy: openai-wire parent → adoption (branchPoint: 3, system hoisted) → child snapshot equals the parent prefix → the child's replayed request is ACCEPTED (200, not FORK_PREFIX_CONFLICT — the poison test) → a divergent second fork for the same child id is refused (409 CHILD_CONFLICT).
  • tests/native-intercept.test.ts: beforeSend runs before headersFor in both branches; a throwing hook never breaks the request.

Local validation on this machine: new suites 56/56 green, plugin-fork 43/43, plugin family 99/99, dsh-acp/persona-fingerprint/refresh/compaction-guard/issue2028 green; dsh-native/dsh-settings-ui/issue2082/issue2101 failures reproduce identically on clean master (live bili sessions + sandbox firewall — environmental). tsc --noEmit clean. Bundle check: dist/agent/dsh-native.js grows 2,242,475 → 2,261,524 bytes with zero server-side references (no startServer/SessionStore drag; the acp-panel import chain is type-erased).

Config surface

None — no config fields, env vars, CLI flags, persistence, or wire changes. Adoption is fully additive and self-gating (manifest capability + isSeeded).

Not in scope (follow-ups)

Stage 2 (pi parentConversationIdOf upgrade, omp parentSession), stage 3 (opencode parentID with prefix-verification gate) per #2394 L0 ordering; dsh anthropic-wire projection if dsh ever ships a non-openai main adapter.


Scope update (stages 2+3, head 1379827, rebased on master 96be7a6): owner-approved per thread — all four hosts now adopt.

@github-actions

github-actions Bot commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

📦 Built Package Artifact

Branch: 2026-10-08_dsh-fork-adopt (788a03e)

Option A — Install this exact build (recommended)

npm install -g billion-context@0.0.2620-pr.2403.b06e52f

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-pr2403.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

[bot] 🏷 收到,开始按 PR Review 规范审查 #2403(dsh fork-child state adoption)。步骤:① 原 issue(#2399/#2394/#2383)方向关 → ② 拉分支通读完整 diff + 回归审查 → ③ 代码/测试有效性 → 结论用结构化八问回复。稍后更新。

ranxianglei pushed a commit that referenced this pull request Oct 8, 2026
…ing budget (spec gate ④)

Review fix for #2403: issue #2399 gate ④ requires side-shaped requests
(no tools + small budget, dsh title-gen / auto-review sidecars) to be
skipped WITHOUT consuming the per-sid attempt budget. Without it, a
sidecar firing before the child's first replayed main turn would
N=0-degrade the sid (terminal 'no prefix match') and permanently block
adoption for that session — silently recreating the #2383 burn. The
predicate mirrors the server-side side heuristic budget bound (#388:
maxTokens <= 200); a main replay always carries registered tools
(toolsReady gate), so the AND condition cannot catch it. Pinned by a
unit test.
@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

[bot] 🏷 #2403 审查完成(结构化八问)。方向关、回归关、代码关、测试关全部过完;顺手修了两个问题(见「审查中直接修复」)。新 head 7a12a1ebd 的 CI 在跑,此前唯一红的 unused-exports 已转绿。

一、验证了什么(证据)

Hash parity(防中毒核心) — 逐字节核对,不是只看测试:

  • forkStableJson vs plugin.ts:918 stableJson:递归配方完全一致(数组 join、key filter+sort、leaf JSON.stringify ?? "null");
  • forkIdentityHashOf vs plugin.ts:939 forkMessageIdentityHash:字段数组 [role, contentType, text??null, toolName??null, toolCallId??null, thinkingTokens??null, summaryOfBlockId??null, toolIsError] 逐项一致;
  • forkOrderHashOf vs plugin.ts:931 forkOrderHash:同为 plain JSON.stringify + sha256。key-order 风险已核实:snapshot 响应在 plugin.ts:1005 以字面量构造 {rawId, ref, identityHash},orderHash 就是对同一数组算的(plugin.ts:1009),wire 序列化保序,客户端 parse→slice→stringify 可复现。partial fork 时服务端校验(plugin.ts:1088)用 stableJson(prefix) !== stableJson(request.orderedMessages) 对 key-order 免疫,orderHash 比对的是客户端自己发的 payload,自洽。

投影 parity — openaiBodyToCore 与 incomingCoreMessages(fork-adoption.ts:131-135)openai 分支同函数同顺序:clone → stripAcpPanelMessages → stripAcpStatusMarkers → openaiToCore。system hoist 两侧对称(system drift 不破坏匹配,是特性不是缺陷)。

中毒失效模式确认 — server.ts:2234-2248:child 有 receipt 且首请求不匹配前缀 → 409 FORK_PREFIX_CONFLICT,receipt 不清除,后续请求同样被拒直至匹配 → 子会话永久锁死。所以 PR 的保守匹配设计(N=0 不发 fork)是正确且必要的,全链路 poison test(真实 proxy 上 child replay 必须 200)是这套机制里最有牙的测试。✓

id 对齐 — forkChildOf 与 headersFor→sessionIdOf 走同一个 ALS 路径(currentInitiator().session.id),stamp 的 conversation id 与 adoption 的 childConversationId 必然一致(async gap 内 session.id 不变,ALS 上下文跨 await 保持)。

二、回归审查(触碰路径的既有行为清单)

既有行为 结论
native-intercept routed 分支:unattributed passthrough(#1117)、#1268 tools gate hold 保持(hook 在 gate 之后,passthrough 在 gate 之前早退)
native-intercept rewrite 分支:signed direct(#1884)、self-origin short-circuit、bootstrap-failure direct send、proxy-death retry/recover(#1130/#1410) 保持(均在 hook 之前早退或不受影响;hook 后只加 header,body 零改动——两分支都核实过)
非 dsh agent(pi/omp/claude/hermes/zcode…) 保持(state.beforeSend === undefined,单属性检查零开销)
dsh-native 稳态请求路径 保持(非 fork 会话:一次 ALS 读 + Set.has,O(1);无额外 fetch)
plugin.ts fork 协议行为 保持(仅 export 化 forkMessageIdentityHash)
wire 协议 / config 面 / 持久化格式 / kernel 无变化(PR 声明属实,我逐项核对过 diff)

有意的新行为(披露):adoption 成功后、首个重放主 turn 到达前,发往 child sid 且不带完整前缀的请求(如 dsh sidecar)会收到 409 FORK_PREFIX_CONFLICT 而非 200——cosmetic 噪音,主 turn 一到即自愈(stats.requests>0 解除防线)。若 dsh 对 seeded 会话确实会在首 turn 前发 sidecar,这是可预期的短暂现象,不算回归。

三、审查中直接修的小问题(已推 PR 分支)

  1. unused-exports CI 红:ForkAdoptionResult 导出但仅文件内使用。审查期间作者并行推了 a7ecfda68(同款修复)+ dc2a95992(attempt-cap 语义修正:transient 失败按 cap 重试而非终局降级——原注释声称的行为与代码不符,这个修法合理,我核对了 transient 正则覆盖的 reason 集合与 CHILD_CONFLICT latch 推理,成立)。我最初的重复 commit 已撤,分支上不留冗余。
  2. spec gate ④ 缺失(我推了 7a12a1ebd):dsh 扩展接入 fork 协议(stage 1/#2394 L0):fork 子会话首请求前 snapshot→match→fork 收养 #2399 方案第 2 条明确要求「side 形状请求(无 tools + 小预算)跳过不消耗尝试预算」,实现里漏了。后果:dsh sidecar(title-gen/auto-review classifyRisk,[bug] dsh:side request 先到会抢走 persona 锚点,主会话被 fork 到 |sub:<fp>,原生 compress 永久失败 #2156 记载其在会话启动时即发射)若先于首个重放主 turn 到达,forkChildOf 通过、body 可解析 → N=0 → 终局 degrade(no prefix match 不在 transient 集合)→ forkDone 永久封死该 sid → 该会话静默退回 DSH 分叉会话后触发大量 preflight overflow compress #2383 烧钱路径。修复:sideShapedBody()(no-tools AND budget≤200,与 server 侧 标题生成等侧请求污染主会话状态:prepareAnthropic 缺 isTitleGen 守卫、快照无条件覆盖、survivedCount 每请求空转 #388 启发式同界),跳过发生在 attempt 计数之前;主重放必带已注册 tools(toolsReady 门保证注册完成),AND 条件不会误伤。补了 9 断言单测钉边界(200/201、max_tokens/max_completion_tokens、tools 空/非空、非对象)。

本地定向验证(rebased 到作者新 head 后):dsh-fork-adopt 15/15、dsh-native 43/43、native-intercept 42/42、plugin-fork 43/43、typecheck clean、unused-exports gate clean。全量回归交给 CI。

四、遗留确认项(非阻塞,请 owner 拍板)

  1. dsh sidecar 实际形状:gate ④ 的 AND 条件(no tools + budget≤200)是否覆盖 dsh 真实 sidecar?若 dsh sidecar 带 tools 或不发 max_tokens 字段,gate 不触发、degrade 回现状(安全但功能对该会话失效)。需要 dsh 侧流量证据确认。
  2. body 提取范围:bodyJsonOf 处理 string/Uint8Array/ArrayBuffer;若 dsh 以 Request 对象发模型调用(init.body 为空),adoption 静默跳过(安全降级,不烧预算)。值得向 dsh 侧确认调用形态。
  3. 分支落后 master 一个 fix: scale-gate fake-completion detection and strip self-closing render tags (#2348) #2353 merge(fake-completion/tag-echo-filter,与本 PR 零文件交集,GitHub mergeable=True)——无需 rebase。

五、结构化八问

  1. 是什么问题:dsh fork 子会话在新 session id 下整段重放父历史,bili 按 id 键控认不出血缘,当裸新会话从零重建压缩状态 → preflight 溢出折叠几十轮不收敛(DSH 分叉会话后触发大量 preflight overflow compress #2383:~5.09M token ≈ 21.2× 窗口)。
  2. 什么原因导致的:根因是插件 fork 协议([plugin] fork snapshot maintenance is O(full history) every request + snapshot persisted into every session file #2077)snapshot/fork 端点完备但零宿主消费——扩展侧最后一跳缺失;dsh 自己的血缘声明(header.parentSession+isSeeded)一直没人读。表面现象是 preflight 不收敛,病因是状态继承断链。
  3. 影响范围与严重程度:仅 plugin native 模式下 dsh fork 长会话的子会话;触发=fork + 首次请求携带大段父历史重放。严重度=该会话完全不可用(30+ 次 overflow compress 无法开始对话);其余会话/宿主/模式零影响。
  4. 怎么修复的:扩展侧 beforeSend 钩子在子会话首个 stamped 请求前走客户端收养:同配方投影 outgoing body → identity hash 对齐最长父前缀 N → POST fork(N≥1 才发,N=len 复用 orderHash);五个门(toolsReady/isSeeded===true/capabilities.fork/side-shape 跳预算/3-attempt cap+single-flight);任何失败降级回现状,绝不让子请求失败。
  5. 是否有回归问题:见第二节的逐项清单——触碰路径既有行为全部保持;唯一有意新行为(adoption 成功后至主 turn 前 child sid 的非前缀请求收 409)已披露,自愈。高危模式核查:无 fail-fast 变吞错、无语义漂移、无阈值/时序/输出格式变化。
  6. 是否破坏缓存:否。缓存/重试/增量游标/去重键均未触碰;child 继承整个 issued-ref namespace(含死号,id-never-reused 不变量保持);client 侧 capability cache 只缓存成功的 manifest 读、按 base 键控,transient 失败不入缓存。
  7. 是否改了设计方向:否。按 PLUGIN.md §8 已有协议消费,是 全景:派生会话(fork / resume / 子代理 / 辅助人格)的会话身份治理 — 现状盘点与缺口 #2394 L0 分层路线的 stage 1(dsh),未引入新架构面。
  8. 是否引入了新的配置:否。无 env、无 config 字段、无 CLI flag;三个超时(15s/15s/5s)为模块内部常量,不构成配置面。

判定:可合并(待 7a12a1ebd 的 CI 全绿;当前 unused-exports 已绿,其余在跑)。合并由人执行:#2403 。第四节两个确认项建议合入后在 #2394 跟踪,不阻塞本 PR。

@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

[bot] 🏷 CI 跟进:最终 head 7a12a1ebd 的 26/26 检查全部通过(含 windows-latest),无失败。审查结论维持不变——可合并,合并由人执行:#2403 。第四节两个 dsh 侧确认项(sidecar 形状、Request 对象调用形态)建议合入后在 #2394 跟踪。

ranxianglei pushed a commit that referenced this pull request Oct 8, 2026
@ranxianglei

Copy link
Copy Markdown
Owner Author

Stages 2+3 on top of the review fixes (rebased, union-merged 7a12a1e/dc2a9599/a7ecfda6):

Stage 2 — pi/omp (#2399):

  • createForkAdopter coordinator in src/agent/fork-adopt.ts absorbs the per-child bookkeeping (once-window, transient-retry cap, single-flight, side-shaped skip); dsh-native rewires onto it; anthropicBodyToCore mirrors the server's anthropic projection for parity.
  • src/agent/pi.ts adopts in before_provider_request (pi + omp share the plugin): parent from parentConversationIdOf (pi resolves path→id by reading the parent session file once per sid), base from proxyBaseForCtx ?? awaitNativeProxyOrigin.
  • Server B-plan (src/plugin.ts): the identity registers that pi/omp fire at session_start claim the child id in registeredIds BEFORE the fork POST → the CHILD_CONFLICT check now yields when the held claim declares the SAME parent (forkChildHeldBySameParent), and a successful fork deletes the stale claim.

Stage 3 — opencode V2 (#2399): adoption at the http.request seam (the only V2 hook seeing both session id and body); parents from the existing session.created channel (#1362). The prefix match IS the #2394 gate — persona ids (#1102) mint fresh contexts, never match, and post no fork. V1 (chat.headers has no body seam) stays on the read-only derived register, documented in the code comment.

Review-fix merge: the owner-side transient-retry + side-shaped-skip semantics now live in the shared coordinator (so pi/omp/opencode get them too); sideShapedBody moved to fork-adopt.ts with dsh-native re-exporting for the pinned test. Two new coordinator-level tests pin transient-retry-until-cap and side-shaped-no-budget.

Evidence (rebased head 17ab93f, master 96be7a6): typecheck clean; dsh-fork-adopt 15/15 (incl. the owner's new gate-④ test), pi-omp-fork-adopt 13/13, opencode-fork-adopt 2/2 (new — full-chain V2 adoption + persona no-fork), plugin-fork 43/43, plugin-agent 64/64, fork-adoption 2/2, issue1958 5/5, responses-websocket 40/40. opencode-v2 29/31 + opencode-native 33/34: the 3 failures are pre-existing local-env flakes (verified identical on stashed clean tree).

@ranxianglei
ranxianglei force-pushed the 2026-10-08_dsh-fork-adopt branch from 17ab93f to 1379827 Compare October 8, 2026 10:08
ranxianglei pushed a commit that referenced this pull request Oct 8, 2026
…ing budget (spec gate ④)

Review fix for #2403: issue #2399 gate ④ requires side-shaped requests
(no tools + small budget, dsh title-gen / auto-review sidecars) to be
skipped WITHOUT consuming the per-sid attempt budget. Without it, a
sidecar firing before the child's first replayed main turn would
N=0-degrade the sid (terminal 'no prefix match') and permanently block
adoption for that session — silently recreating the #2383 burn. The
predicate mirrors the server-side side heuristic budget bound (#388:
maxTokens <= 200); a main replay always carries registered tools
(toolsReady gate), so the AND condition cannot catch it. Pinned by a
unit test.
ranxianglei pushed a commit that referenced this pull request Oct 8, 2026
ranxianglei pushed a commit that referenced this pull request Oct 8, 2026
…dget (#2403 review)

A request whose body the host did not hand us (opencode V2's ws-handshake
synthetic Request) previously flowed into tryForkAdoption as 'body
unmappable' — a transient outcome that consumed one of the three attempt
slots. Repeated handshakes/reconnects could therefore close the adoption
window terminally before the child's first real model request ever arrived.
The coordinator now skips undefined bodies before any attempt bookkeeping
(same discipline as the side-shaped gate), and the doc comments state the
actual attempt semantics instead of claiming responses/google skip free.
@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

[bot] 🏷 ## PR #2403 第二轮审查结论(stage 2+3:pi/omp + opencode V2 + 服务端 B-plan)

审查对象:rebase 后新栈(9f782ab→13798271e)+ 我本轮补的修复 6778c2a40(已推到 PR 分支)。注:审查期间你 force-push 重排了栈,我核对过 7 个 PR 文件在新旧 head 间逐字节一致,第一轮+本轮的代码分析对新 head 完全适用。

结构化八问

1. 是什么问题
Stage 1 只让 dsh 的 fork 子会话继承父会话压缩状态;pi(/fork、--fork)、omp(fork())、opencode V2(subagent/fork)的派生会话同样重放父历史,不接入就会走同一条 #2383 燃烧路径(preflight 把整段继承历史当全新会话反复折叠)。且这三个 host 各自在首个模型请求前就 identity-register 子会话 id,导致扩展侧 fork POST 撞上 registeredIds → 409 CHILD_CONFLICT,即使客户端实现了也用不了——这是比"没实现"更深的阻塞。

2. 什么原因导致的
两层:①每个 host 都需要同一套 per-child 簿记(once-window/transient-retry/single-flight/side-skip),没有共享协调器就必然三处复制漂移;②服务端 CHILD_CONFLICT 的设计前提是"id 已被认领 = 竞争者",但 pi/omp/opencode 的 register 是同一 host 对同一 lineage 的合法声明(register 携带与 fork 请求相同的 parent),被当成冲突是判定层缺少"同源声明"这一维度。B-plan 在决策点(plugin.ts handlePluginFork)补上这个维度,而不是在客户端绕路。

3. 影响范围与严重程度
改前:pi/omp/opencode-V2 的 fork 子会话全部退化为裸新会话 → #2383 级燃烧(长会话 fork 后不可用)。改后:openai/anthropic HTTP wire 生效;responses/google/ws-wire 安全退化(功能缺位但不中毒、不报错)。严重程度从高降到低,退化面与 stage 1 披露口径一致。

4. 怎么修复的

  • createForkAdopter 共享协调器(fork-adopt.ts)吸收全部簿记;dsh 原样重挂上去(行为保持,dsh 套件 15/15 验证)。
  • anthropicBodyToCore 镜像服务端 anthropic 投影;与 incomingCoreMessages("anthropic", …) 做逐消息 identity-hash 等价测试钉住 parity(与 stage 1 openai parity 同纪律)。
  • pi/omp:在 before_provider_request 的 await 槽内、registerTools 之后执行——进程内顺序保证 register→adoption→send,请求永远追不上 fork 尝试;per-sid parent 缓存避免每次模型请求同步读 64KB 父会话文件。
  • opencode V2:http.request seam(唯一同时看到 session id 和 body 的 V2 hook);parent 来自 session.created(feat: extend derived-session inheritance to remaining lanes (opencode V1, omp) — follow-up to #1333 #1362 通道);prefix match 本身就是 全景:派生会话(fork / resume / 子代理 / 辅助人格)的会话身份治理 — 现状盘点与缺口 #2394 要求的门禁——persona id([Bug] opencode system-context reconcile:每次 AGENTS.md 编辑使 Responses 会话 key 里的 instructions 指纹漂移,压缩状态被重置(master 现存) #1102)新开上下文 N=0,不发 fork,有专门测试钉住。
  • 服务端 B-plan:forkChildHeldBySameParent —— 仅当 held claim 声明相同 parent 时 CHILD_CONFLICT 让步,fork 成功后删除陈旧 claim;不同 parent / 无 parent 仍 409;pendingRegisters(headless spawn)保持严格不动。
  • 我的补修 6778c2a40:无 body 的请求(opencode V2 ws-handshake 合成 Request,opencode-v2.ts:376)原来会落进 tryForkAdoption 变成 transient "body unmappable"、消耗 3 次尝试预算之一——WS 断线重连几次就能在第一个真实模型请求到来前把窗口永久关闭,stage 3 在最需要的 Responses-WS 车道反而自我锁死。协调器现在对 body === undefined 直接跳过、不记账(side-gate 同纪律),并修正了两处与实际尝试语义不符的文档注释。测试加 5×body-less 循环 + 窗口仍开断言。

5. 是否有回归问题
逐项清点过:

  • CHILD_CONFLICT 不变量(最高危):穷举了 same/different/bare/并发/陈旧五种 claim 场景——child id 不可换亲的不变量保持(不同 parent 仍 409;并发双 fork 经 withSessionLock 串行化,第二个撞 conversations map 409;陈旧 register 从"永久 409 直到 LRU 驱逐"变为"可恢复",纯改善)。新 B-plan 测试三个方向都钉住了。
  • register 晚到边界(register 首试失败→adoption 成功→重试 register 落在 fork 之后):derived-link 记录被 publicForkReceipt 门控(server.ts:2420),link-only 且幂等,无害。
  • registeredIds.delete 安全性:删除只影响 consumePluginRegisterFor 的 derived-parent 路径,plugin-mode 绑定由 conversations map(recordPluginSession)承担,无双重绑定。
  • 非 fork 会话零成本:toolsReady/parent 声明/manifest capability 三重前置门;普通 pi 会话一次廉价的 getHeader 读后即缓存。
  • dsh 行为保持:一处有意微漂移披露——run() 内意外抛错现在标记 done(stage-1 留窗口),防错误循环,更安全。
  • wire/配置/持久化/kernel:未触碰。

6. 是否破坏缓存
capability cache(per-base、仅成功缓存)未动;prefix-affinity/session 缓存不受影响;我的修复减少网络负载(body-less 不再触发 snapshot GET)。无失效影响。

7. 是否改了设计方向
无转向——按 #2394 L0 计划完成多 host 铺开,复用既有协议。B-plan 是服务端加法(精确 parent 匹配门控)。已知阶段限制(非回归):responses/google/ws-wire 车道不投影客户端侧 → 安全退化不 adoption;opencode V1 保持只读 derived register(代码注释已记录)。

8. 是否引入新配置
无。

本地验证(定向)

新 head 6778c2a40:pi-omp-fork-adopt 13/13、opencode-fork-adopt 2/2、dsh-fork-adopt 15/15、plugin-fork 43/43 共 73/73;tsc --noEmit 干净;unused-exports gate 干净。全量交给 CI。

遗留跟踪项(#2394,不阻塞)

  1. opencode V2 的 Responses-over-WebSocket 车道(openai/azure provider)模型流量不走 http.request seam → 该车道无 adoption(http.request 只看到握手);若后续要覆盖需在 WS 帧层找 seam,属新阶段工作。
  2. pi parent 解析失败会永久缓存 undefined(防永久坏配置下每请求同步读文件的权衡);header 自会话创建即稳定,实际风险极低。
  3. 第一轮的两个 dsh 确认项(sidecar 形状 vs gate-④ AND 条件、Request 对象调用形态)仍待实测流量回答。

判定:可合并。 合并由人执行:#2403

@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

[bot] 🏷 收到。不过我这边刚核验(GitHub API ×2 + npm),当前可验证的状态与「已合并发布 v0.1.209」对不上,如实报告:

  • PR feat(#2399): adopt dsh fork-child state via the plugin fork protocol #2403:state=open,merged=false,merged_at=null,head 仍是 6778c2a40(含我的 body-less 修复)
  • origin/master 仍在 96be7a68b(#2363 merge commit),6778c2a40 不在 master 上
  • npm registry:billion-context@latest = 0.1.188,0.1.209 查无此版本
  • CI(唯一能确认的):head 6778c2a40 上 22/22 已完成检查全绿,4 个还在跑,无失败

所以要么合并/发布动作还没实际落下去(评论先于操作发出?),要么走了我看不到的通道。请确认一下;如果合并还没点,现在点即可——审查侧已全部就绪,无需再改任何东西。若 v0.1.209 确实已发出,请告诉我它来自哪个 ref,我再核一遍内容是否等于 6778c2a40。

e-work and others added 8 commits October 8, 2026 22:17
dsh fork children (header.isSeeded + parentSession) replay their parent's
event prefix under a fresh session id, which landed as a brand-new
conversation and forced preflight to refold the entire inherited history
(#2383). The dsh native extension now adopts the parent's compression
state through the plugin fork protocol BEFORE the child's first stamped
model request, so the replay rides the inherited folds instead.

- src/agent/fork-adopt.ts: client half of the fork protocol — projects the
  outgoing OpenAI body exactly like the server's incomingCoreMessages
  (clone -> strip panel echoes/status markers -> openaiToCore), matches the
  longest parent prefix by identity hash (byte-identical recipe to
  plugin.ts, parity-pinned by tests), and posts the fork receipt. Every
  failure degrades to today's behavior; a zero-length prefix match never
  posts (poisoning guard). PARENT_REVISION_CONFLICT retries once.
- src/agent/native-intercept.ts: new async beforeSend hook, awaited after
  the tools gate and before headersFor in both the routed and rewrite
  branches; hook errors never break the request.
- src/agent/dsh-native.ts: AgentLike gains session.header; per-sid
  single-flight adoption with a 3-attempt cap, gated on isSeeded === true
  (subagents carry parentSession without isSeeded and stay fresh),
  manifest capabilities.fork advertisement, and wire mode (toolsReady).
- src/plugin.ts: export forkMessageIdentityHash as the parity anchor.
- tests: hash parity, projection units, adoption matrix (mock fetch),
  intercept hook ordering, and a full-chain test against a real proxy
  (openai-wire parent, adoption, accepted replay, divergent-fork refusal).

Refs #2399
…ing budget (spec gate ④)

Review fix for #2403: issue #2399 gate ④ requires side-shaped requests
(no tools + small budget, dsh title-gen / auto-review sidecars) to be
skipped WITHOUT consuming the per-sid attempt budget. Without it, a
sidecar firing before the child's first replayed main turn would
N=0-degrade the sid (terminal 'no prefix match') and permanently block
adoption for that session — silently recreating the #2383 burn. The
predicate mirrors the server-side side heuristic budget bound (#388:
maxTokens <= 200); a main replay always carries registered tools
(toolsReady gate), so the AND condition cannot catch it. Pinned by a
unit test.
…dget (#2403 review)

A request whose body the host did not hand us (opencode V2's ws-handshake
synthetic Request) previously flowed into tryForkAdoption as 'body
unmappable' — a transient outcome that consumed one of the three attempt
slots. Repeated handshakes/reconnects could therefore close the adoption
window terminally before the child's first real model request ever arrived.
The coordinator now skips undefined bodies before any attempt bookkeeping
(same discipline as the side-shaped gate), and the doc comments state the
actual attempt semantics instead of claiming responses/google skip free.
@ranxianglei
ranxianglei force-pushed the 2026-10-08_dsh-fork-adopt branch from 6778c2a to 788a03e Compare October 8, 2026 14:19
@ranxianglei
ranxianglei merged commit 3b57fcc into master Oct 8, 2026
26 checks passed
@github-actions

github-actions Bot commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

📦 Merged Build Published

This merge to master was published as 0.0.149-master.3b57fcc (rolling master tag).

npm install -g billion-context@0.0.149-master.3b57fcc  # 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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

dsh 扩展接入 fork 协议(stage 1/#2394 L0):fork 子会话首请求前 snapshot→match→fork 收养

1 participant