Skip to content

[PR] fix(fork-adopt): defer adoption when the capability probe cannot reach the proxy (#4266) - #4345

Open
ranxianglei wants to merge 7 commits into
masterfrom
2026-10-23_fork-capability-probe-transient
Open

ranxianglei wants to merge 7 commits into
masterfrom
2026-10-23_fork-capability-probe-transient

Conversation

@ranxianglei

Copy link
Copy Markdown
Owner

Addresses item ⑨ of the zero-inheritance-cloning family tracker #4266 — reported as a production incident on that issue.

What was wrong

createForkAdopter's capability gate (src/agent/fork-adopt.ts) treated any failed GET /__bili/plugin/manifest probe as terminal: on the first failure it called done.add(child) with no retry budget. A probe fails for two very different reasons:

  1. the proxy genuinely lacks fork support (old build) — terminal is correct;
  2. the proxy simply isn't reachable yet (host starting up / restarting / port changing) — transient, should retry.

The gate conflated the two. In case 2 the child was permanently retired from adoption for the process lifetime (the capability cache self-heals, but done.add had already pinned the child), then dead-ended downstream on the first-turn context_length_exceeded. The snapshot/fork degraded path immediately below already classified transient failures and retried within maxAttempts; the capability gate above it had no such classification.

Fix

New probeForkCapability() returns {capable, definitive}:

  • definitive=true — manifest read OK but fork not advertised (old proxy): terminal on first probe, unchanged (keeps such proxies off the request path / avoids repeated 404 noise);
  • definitive=false — manifest unreadable right now (!res.ok / timeout / network error): transient, deferred and retried up to maxAttempts, adopting once the proxy becomes reachable.

Only definitive results stay cached per base; a failed read is never cached, so the next request re-hits the wire and self-heals. manifestForkCapable remains a thin boolean wrapper so existing callers/tests are untouched. All three native lanes (dsh / opencode / pi) share createForkAdopter, so one fix covers all of them.

Behavior change (old → new)

A single transient capability-probe failure used to retire a child's adoption for the process lifetime; it now retries within the existing maxAttempts budget (default 3). Definitively-incapable proxies are unchanged (still terminal on the first probe). No config surface, wire-shape, persistence, message-identity, or kernel change.

Tests

  • Rewrote the exploding-network case: it previously asserted "closes the window" on the first failed probe (pinning the bug); it now asserts deferred-until-cap, then closed.
  • Added a regression repro: proxy down on request 1 (5xx manifest), up on request 2 → the child adopts (fork POST fires exactly once) instead of being permanently retired.
  • 45/45 green across tests/{pi-omp,dsh,opencode}-fork-adopt.test.ts; typecheck clean.

Part of #4266 (item ⑨). Not closing the tracker — sibling items ①–⑧ are tracked there and land via their own PRs (#3024 / #3039 / #3951 / #4223 / #4263 plus pending owner decisions).

@ranxianglei ranxianglei changed the title fix(fork-adopt): defer adoption when the capability probe cannot reach the proxy (#4266) [PR] fix(fork-adopt): defer adoption when the capability probe cannot reach the proxy (#4266) Oct 23, 2026
@github-actions

github-actions Bot commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

📦 Built Package Artifact

Branch: 2026-10-23_fork-capability-probe-transient (ec954bf)

Latest build — install this exact version

npm install -g [email protected]

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
ec954bf [email protected] build
5d7f4ca [email protected] build
4aedc2b [email protected] build

Download artifact

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

Install instructions are refreshed on each push.

@ranxianglei
ranxianglei force-pushed the 2026-10-23_fork-capability-probe-transient branch from c61d0f1 to d7cf89b Compare October 23, 2026 00:29
@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

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

查重: 不重复——本 PR 是 #4266 家族 item ⑨ 的修复(我自己开的);①–⑧ 由各自 PR 承载(#3024/#3039/#3951/#4223/#4263),无其他 open issue/PR 覆盖「capability probe 瞬态失败分类」这一机制。

标签: bug / dsh+pi+opencode / scope/wiring / p1(fork 继承被进程级永久放弃,属「会话数据丢失类」,按 2026-10-19 口径一律 p1)——已打上。

审核结论(八问)

  1. 是什么问题:fork 采纳的 capability gate 把「暂时够不到 proxy」当成「proxy 不支持 fork」——第一次 GET /__bili/plugin/manifest 失败就 done.add(child) 永久退役该子会话的采纳资格(直到宿主进程重启),后续请求在首轮 context_length_exceeded 上死掉。
  2. 什么原因导致的:probe 失败有两种语义被混为一谈——manifest 读到了但没有 fork(老 proxy,确定态)vs manifest 此刻读不到(宿主启动中/重启中/端口漂移,瞬态)。gate 对两者一刀切终局;而它下方的 snapshot/fork 降级路径本来就有这个分类和 maxAttempts 重试预算,gate 却独缺。
  3. 影响范围与严重程度:dsh/opencode/pi 三条 native 车道共享 createForkAdopter,一处修复全覆盖。触发条件=子会话首个请求恰好落在 proxy 不可达窗口;后果=p1:子会话在该进程生命周期内永久失去父上下文继承,下游表现为窗口超限报错。
  4. 怎么修复的:probe 结果拆成 {capable, definitive}——definitive=true(读到但无 fork)首次即终局,行为不变;definitive=false(读不到)判瞬态,延迟并在既有 maxAttempts(默认 3)预算内重试;只有 definitive 结果进 per-base 缓存,读失败永不缓存,下一个请求自然重探、自愈。manifestForkCapable 保留为 boolean 薄包装,直接调用方与测试零改动。
  5. 是否有回归问题:逐项清点过既有行为——definitive-incapable 路径不变(首探终局+缓存);缓存语义不变(仅成功读取入缓存,与旧行为一致);attempt 预算与下方 tryForkAdoption 瞬态重试共用同一 attempts map 与 maybeAdopt 上限,无新预算;single-flight、done 集、google-wire skip、side-shaped body skip 全部未触碰;CHILD_CONFLICT 处理不变。无语义漂移、无 fail-fast 变吞错、无阈值/时序/输出格式变化。测试侧:原「爆炸网络」用例本来就把 bug 钉成了断言,已重写为 deferred-until-cap;新增回归复现(proxy 第 1 次 5xx、第 2 次可达 → fork POST 恰好发一次)。
  6. 是否破坏缓存:不涉及——prefix cache/cache ref/重试游标/去重键路径未触碰;capability cache 仅 per-base 记忆化,语义保持。
  7. 是否改了设计方向:不涉及——只在 createForkAdopter 内部给重试纪律补了一个分类维度,与下方既有瞬态重试同机制族。
  8. 是否引入新配置:不涉及。

行为变更披露(old→new):单次瞬态 capability-probe 失败,旧=子会话采纳资格进程级永久退役 → 新=在 maxAttempts=3 预算内重试;确定不支持 fork 的 proxy 完全不变(仍首探终局)。PR 正文已载明。

本轮动作(已验证执行)

判定:代码与行为审核通过,无拦下项;新 head d7cf89b84 的全量 CI(test 矩阵 + mysql-contract 复跑)转绿后即可合并——合并操作按规矩留给你。

@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

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

更正上一条的 flake 判断:mysql-contract 不是 runner flake,是确定性缺陷——查证后收回。本 head d7cf89b84 的 CI 红全部继承自 master(aa198fe61)自身的红,与本 PR diff 无关。已核对到根因与在途修复:

本 PR 自身状态:guards/static-gates/e2e 各车道全绿;typecheck、定向 45/45、unused-exports gate 本地+CI 双绿。代码面没有待办。

后续安排:master 转绿(A+B 任一组合合入)后,我立即把本分支 rebase 到新 master 并确认全量绿,再交合并。当前无需你操作本 PR。

awork added 3 commits October 23, 2026 18:07
…h the proxy (#4266)

Item 9 of the zero-inheritance-cloning family (#4266): createForkAdopter's capability gate treated ANY failed manifest probe as terminal (done.add on first failure, no retry budget), even when the failure was only that the proxy was not reachable yet (host starting up / restarting / port changing). A child forked in that window was permanently zero-inherited, then dead-ended on the first-turn context_length_exceeded downstream.

probeForkCapability now distinguishes definitive=true (manifest read OK but fork not advertised - old proxy: terminal on first probe, unchanged, avoids 404 noise) from definitive=false (manifest unreadable right now - !res.ok / timeout / network error: transient, deferred and retried up to maxAttempts, adopting once the proxy is reachable). Only definitive results stay cached; a failed read is never cached, so the next request re-hits the wire and self-heals. manifestForkCapable stays a thin boolean wrapper so existing callers/tests are unaffected. All native lanes (dsh/opencode/pi) share createForkAdopter, so the fix covers all of them.

Behavior change (old -> new): a single transient capability-probe failure used to retire a child's adoption for the process lifetime; it now retries within the existing maxAttempts budget (default 3). Definitively-incapable proxies are unchanged (still terminal on first probe).

Tests: rewrote the exploding-network case (now asserts deferred-until-cap instead of closes-on-first) and added a regression repro (proxy down on request 1, up on request 2 -> child adopts). 45/45 green across the three fork-adopt suites.

Addresses item 9 of #4266.
ForkCapabilityProbe is module-internal; only probeForkCapability and
manifestForkCapable are consumed across modules. INTERNAL_ONLY fix per
scripts/check-unused-exports.mjs.
…used-exports gate)

In-passing fix, unrelated to this PR's fork-adopt change: #4378 landed ENGPROOF_CHECK_NAMES / BANNER_CHECK_NAMES / CorruptIndexCheck / CorruptIndexVerdict exported but referenced only inside the same file, which the unused-exports gate (scripts/check-unused-exports.mjs, ci.yml unused-exports lane) rejects on every head including this one. Dropped the four leading exports; verified no importer outside the file (parents use ENGPROOF_CHECKS/BANNER_CHECKS/spawnCorruptIndexChild only). Gate clean + both consumer suites 17/17.
@ranxianglei
ranxianglei force-pushed the 2026-10-23_fork-capability-probe-transient branch from d7cf89b to 65f9db3 Compare October 23, 2026 10:14
@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

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

master 已转绿,本分支已按承诺 rebase 并推送(新 head 65f9db373,基于 72926f6fb):

  1. rebase 干净:两个原 commit 无冲突落地,diff 相对 master 仍只含 src/agent/fork-adopt.ts + tests/pi-omp-fork-adopt.test.ts。上游进展核对:子缺陷 A(LIVE_FILES)随 [PR] web: Rotate the "Compressed Structure" tree view of large sessions to vertical full size, no longer shrunk to sub-pixel | web: 大会话「压缩结构」树图旋转为竖向全尺寸,不再缩成亚像素 (#4173) #4174 合入修复(005e75d99+2d6debc18),子缺陷 B(mysql-contract 缺 kernel 构建步)由 master mysql-contract lane red: #4177's store→runtime-status chain pulled acp-kernel runtime imports into the storage co | master mysql-contract 车道红:#4177 的 store→runtime-status 链把 acp-kernel 运行时 import 带进了 storage 契约测试,job 没构建 kernel #4336 修复合入(fd9043780);[PR] fix(ci): build acp-kernel in the mysql-contract lane (sub-defect B of #4326) #4330/[PR] ci: register tests/storage-durability.test.ts in LIVE_FILES (#4333) #4334 作为重复关闭——与我在场的判断一致。
  2. 本地定向验证:fork-adopt 三套件 45/45 绿;unused-exports gate 绿。
  3. 顺手小修(独立标注 commit 65f9db373,与本 PR 主题无关):rebase 后发现 master 自身有一处 gate 红——[PR] fix(ci): run corrupt index.db degrade checks in a child process so windows cleanup can't hit EBUSY (#4338) #4378 合入的 tests/corrupt-index-child.ts 有 4 个名字(ENGPROOF_CHECK_NAMES/BANNER_CHECK_NAMES/CorruptIndexCheck/CorruptIndexVerdict)export 了但仅文件内引用,unused-exports lane 在任何 head 上都会红(含本 PR)。已去掉 4 个 export:先验证过文件外零引用(父测试只用 ENGPROOF_CHECKS/BANNER_CHECKS/spawnCorruptIndexChild),gate clean + 两个消费方测试文件 17/17 绿。若另有在途修复撞车,以先合入者为准、本 commit 可弃。
  4. 待办:全量 typecheck + 回归交给 CI(本地不跑全量)。CI 全绿后我确认并在此回帖,即可交合并。

ci-check added 4 commits October 26, 2026 10:19
master window commits 293ae65/de5e03659 bumped kernel/package.json to
0.0.165 without regenerating the lockfiles; lock-sync re-resolution then
rewrites the stale recorded versions (root 0.0.163, kernel 0.0.164) and
fails. Pure recorded-version sync via npm install --package-lock-only;
no dependency or semantic change.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant