Repository navigation
feat(#2399): adopt dsh fork-child state via the plugin fork protocol - #2403
Conversation
📦 Built Package ArtifactBranch: Option A — Install this exact build (recommended)npm install -g billion-context@0.0.2620-pr.2403.b06e52fThis exact version is immutable and reproducible. The rolling Option B — Download artifact
tar xzf billion-context-pr2403.tgz
npm install -g packageInstall instructions are refreshed on each push. |
…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.
[bot] 🏷 #2403 审查完成(结构化八问)。方向关、回归关、代码关、测试关全部过完;顺手修了两个问题(见「审查中直接修复」)。新 head 一、验证了什么(证据)Hash parity(防中毒核心) — 逐字节核对,不是只看测试:
投影 parity — 中毒失效模式确认 — server.ts:2234-2248:child 有 receipt 且首请求不匹配前缀 → 409 FORK_PREFIX_CONFLICT,receipt 不清除,后续请求同样被拒直至匹配 → 子会话永久锁死。所以 PR 的保守匹配设计(N=0 不发 fork)是正确且必要的,全链路 poison test(真实 proxy 上 child replay 必须 200)是这套机制里最有牙的测试。✓ id 对齐 — 二、回归审查(触碰路径的既有行为清单)
有意的新行为(披露):adoption 成功后、首个重放主 turn 到达前,发往 child sid 且不带完整前缀的请求(如 dsh sidecar)会收到 409 FORK_PREFIX_CONFLICT 而非 200——cosmetic 噪音,主 turn 一到即自愈(stats.requests>0 解除防线)。若 dsh 对 seeded 会话确实会在首 turn 前发 sidecar,这是可预期的短暂现象,不算回归。 三、审查中直接修的小问题(已推 PR 分支)
本地定向验证(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 拍板)
五、结构化八问
判定:可合并(待 |
|
Stages 2+3 on top of the review fixes (rebased, union-merged 7a12a1e/dc2a9599/a7ecfda6): Stage 2 — pi/omp (#2399):
Stage 3 — opencode V2 (#2399): adoption at the 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); 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). |
17ab93f to
1379827
Compare
…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.
[bot] 🏷 ## PR #2403 第二轮审查结论(stage 2+3:pi/omp + opencode V2 + 服务端 B-plan) 审查对象:rebase 后新栈(9f782ab→13798271e)+ 我本轮补的修复 结构化八问1. 是什么问题 2. 什么原因导致的 3. 影响范围与严重程度 4. 怎么修复的
5. 是否有回归问题
6. 是否破坏缓存 7. 是否改了设计方向 8. 是否引入新配置 本地验证(定向)新 head 遗留跟踪项(#2394,不阻塞)
判定:可合并。 合并由人执行:#2403 |
[bot] 🏷 收到。不过我这边刚核验(GitHub API ×2 + npm),当前可验证的状态与「已合并发布 v0.1.209」对不上,如实报告:
所以要么合并/发布动作还没实际落下去(评论先于操作发出?),要么走了我看不到的通道。请确认一下;如果合并还没点,现在点即可——审查侧已全部就绪,无需再改任何东西。若 v0.1.209 确实已发出,请告诉我它来自哪个 ref,我再核一遍内容是否等于 |
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
…ry on later requests
…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.
6778c2a to
788a03e
Compare
📦 Merged Build PublishedThis merge to master was published as npm install -g billion-context@0.0.149-master.3b57fcc # this exact build
npm install -g billion-context@master # always the newest merged build
|
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
incomingCoreMessagesopenai 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 ofsrc/plugin.ts— the server'spublicForkInputMatches409-rejects a replay whose prefix drifts from the receipt, so parity is pinned by tests against the now-exportedforkMessageIdentityHash. 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_CONFLICTretries once with a fresh snapshot.beforeSendhook awaited after the tools gate and beforeheadersForin both the routed/bili/and rewrite branches; hook errors are logged and never break the request.AgentLikegainssession.header; per-sid single-flight adoption with a 3-attempt cap; gates:register.toolsReady(wire mode skips),isSeeded === true(dsh subagents carryparentSessionWITHOUTisSeeded— they run fresh task contexts and must not inherit), manifestcapabilities.forkadvertisement (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.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),matchForkPrefixboundaries, the full adoption/degradation matrix against a scripted proxy (incl. revision-conflict retry and never-post-on-zero-match),manifestForkCapablecaching, 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:beforeSendruns beforeheadersForin 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/issue2101failures reproduce identically on clean master (live bili sessions + sandbox firewall — environmental).tsc --noEmitclean. Bundle check:dist/agent/dsh-native.jsgrows 2,242,475 → 2,261,524 bytes with zero server-side references (nostartServer/SessionStoredrag; 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
parentConversationIdOfupgrade, ompparentSession), stage 3 (opencodeparentIDwith 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.
createForkAdoptercoordinator insrc/agent/fork-adopt.ts(once-window, transient-retry cap, single-flight, side-shaped skip — absorbs the review-fix semantics so every host gets them);anthropicBodyToCoremirrors the server projection;src/agent/pi.tsadopts inbefore_provider_request(pi resolves a path-shapedparentSessionby reading the parent session file once per sid).src/plugin.ts): pi/omp identity-register at session_start claims the child id before the fork POST → the CHILD_CONFLICT check now yields for a same-parent claim; a successful fork deletes the stale claim.http.requestseam (session id + body); parents from the existingsession.createdchannel. The prefix match is the 全景:派生会话(fork / resume / 子代理 / 辅助人格)的会话身份治理 — 现状盘点与缺口 #2394 replay gate — persona ids ([Bug] opencode system-context reconcile:每次 AGENTS.md 编辑使 Responses 会话 key 里的 instructions 指纹漂移,压缩状态被重置(master 现存) #1102) never match, post no fork. V1 keeps the read-only derived register (no body seam atchat.headers).