Skip to content

[PR] fix: classify FORK_PREFIX_CONFLICT rejections instead of failing blind (#3740) - #3743

Merged
ranxianglei merged 3 commits into
masterfrom
2026-10-18_fork-prefix-conflict-diagnostics
Oct 19, 2026
Merged

ranxianglei merged 3 commits into
masterfrom
2026-10-18_fork-prefix-conflict-diagnostics

Conversation

@ranxianglei

@ranxianglei ranxianglei commented Oct 18, 2026 •

Copy link
Copy Markdown
Owner

Background

#3741 field report: a fork child session is stuck on 409 FORK_PREFIX_CONFLICT ("first child request does not match its inherited ordered prefix") and nobody can tell WHY — the client sees a bare 409 and the proxy logs nothing. A deterministic mismatch is a permanent brick for that child (the gate re-runs on every request), so the first thing missing from this failure mode is diagnosability. This PR adds classification to that gate; root-cause triage continues in #3741 with user evidence.

Changes

  • src/plugin.ts: publicForkInputMatches() becomes publicForkInputCheck(), returning a classified verdict instead of a boolean:
    • cell-mismatch@N — id-drift / hash-drift-under-same-id / ref-broken, with truncated id + hash previews only; each evidence part appears only when its own check failed;
    • prefix-longer-than-incoming (expected=N incoming=M);
    • incoming-unmappable (protocol=X);
    • receipt-missing / snapshot-missing / projection-error (…).
      Accept/reject semantics are UNCHANGED: fail-closed on any error, same per-cell gates incl. the [Bug][plugin] protectedTools 的 BLOCKED 映射导致纯文本 snapshot 返回 409 #2620 BLOCKED sentinel.
  • src/server/handle.ts: on rejection, logs warn [fork-prefix] <sessionId>: first child request rejected — <reason> (protocol=<p>) and the 409 body gains an additive detail field carrying the classification. Existing ok/code/error fields and the error sentence are unchanged.
  • Cleanup: dropped the now-unused import in src/server.ts; refreshed four stale name references in comments (src/agent/fork-adopt.ts, two parity test files).
  • tests/plugin-fork.test.ts: pins every rejection class end-to-end through a real proxy, including same-id hash drift (a thinkingTokens flag flipped under unchanged bytes) asserting it classifies separately from id drift, plus a ref-only mismatch pinning ref-broken alone.

Behavior change (disclosure)

before after why
409 body was {ok, code, error} only additive detail: <classification> field field triage needs the drift shape without reading proxy state
no server-side log on rejection one warn line (session id, protocol, reason) the proxy log was a total blind spot here

No config surface change, no wire-shape change beyond the additive JSON field, no persistence change, no kernel change.

Verification

  • npm run typecheck clean, unused-exports gate clean, npm run build OK
  • targeted local runs on the review head: tests/plugin-fork.test.ts 48/48 (full suite = remote CI)

Review fixes (2026-10-19)

Review of this PR found three issues, fixed as labeled commits on top:

  1. Classifier could mislabel ref-only mismatches (src/plugin.ts): when a cell's ONLY failing check was the ref mapping (id+hash intact — e.g. a mapping pruned at a native-compaction boundary), the else branch still emitted hash-drift-under-same-id with two identical hash previews, sending triage after a phantom flag flip. Each evidence part is now emitted only when its own check failed; pinned end-to-end in tests/plugin-fork.test.ts (break the child's byRaw mapping for cell 0, restore in finally, assert ref-broken alone).
  2. CI unused-exports gate red on the original head: PublicForkPrefixVerdict was exported but consumed only inside src/plugin.ts — un-exported (separate commit).
  3. Stale issue refs: every #3740 pointed at an unrelated GitHub issue (an acp_delegate_wait wording report); the field report is This run failed with 409: "first child request does not match its inherited ordered prefix" | 本轮运行失败409 "first child request does not match its inherited ordered prefix" #3741 — comments and this body corrected. The title suffix is system-managed and left as-is.

The branch was also rebased onto current master (6f2b113bd).

Refs #3741

@ranxianglei ranxianglei changed the title fix: classify FORK_PREFIX_CONFLICT rejections instead of failing blind (#3740) [PR] fix: classify FORK_PREFIX_CONFLICT rejections instead of failing blind (#3740) Oct 18, 2026
@github-actions

github-actions Bot commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

📦 Built Package Artifact

Branch: 2026-10-18_fork-prefix-conflict-diagnostics (02c4fb5)

Option A — Install this exact build (recommended)

npm install -g billion-context@0.0.4421-pr.3743.fb0cf31

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

Install instructions are refreshed on each push.

awork added 3 commits October 19, 2026 02:33
#3740)

The public-fork first-request gate rejected mismatched replays with a
bare 409 and zero server-side evidence of WHY the child history drifted
from its inherited prefix — field reports like #3740 were impossible to
triage from either side.

publicForkInputMatches becomes publicForkInputCheck returning a
classified verdict: cell-mismatch@N (id-drift / hash-drift-under-same-id
/ ref-broken, with truncated id+hash previews), prefix-longer-than-incoming,
incoming-unmappable, receipt-missing, snapshot-missing, projection-error.
Accept/reject semantics are unchanged — fail-closed on any error, same
per-cell gates incl. the #2620 BLOCKED sentinel. On rejection the 409
body gains an ADDITIVE detail field with the classification and the
proxy logs a warn line (session id, protocol, reason). detail carries
truncated ids/hashes only, never message content.

Also drops the now-unused publicForkInputMatches import in server.ts and
refreshes four stale name references in comments (fork-adopt.ts, two
parity test files).

tests/plugin-fork.test.ts pins every rejection class end-to-end through
a real proxy, including same-id hash drift (thinkingTokens flag flipped
under unchanged bytes) asserting it classifies separately from id drift.

Behavior change (additive): 409 response body gains 'detail'; new warn
log line on rejection. No config surface, wire-shape, or persistence
change.

Refs #3740
The verdict type is only consumed inside plugin.ts (as the return type
of publicForkInputCheck); the repo's unused-exports gate requires such
types to stay module-private.
Review of #3743: the classifier could label a cell whose ONLY failing
check was the ref mapping (id+hash intact, e.g. a mapping pruned at a
native-compaction boundary) as hash-drift-under-same-id while printing
two identical hash previews — sending triage after a phantom flag flip.
Each evidence part is now emitted only when its own check failed.

Also pins the ref-only class end-to-end in tests/plugin-fork.test.ts
(break the child's byRaw mapping for cell 0, restore in finally; assert
the detail says ref-broken without inventing hash-drift/id-drift), and
points the stale #3740 refs in src/plugin.ts, src/server/handle.ts and
the test file at #3741 — the field report (#3740 on GitHub is an
unrelated acp_delegate_wait wording issue).
@ranxianglei
ranxianglei force-pushed the 2026-10-18_fork-prefix-conflict-diagnostics branch from 5453518 to 02c4fb5 Compare October 18, 2026 18:38
@ranxianglei ranxianglei added bug Something isn't working p2 scope/kernel labels Oct 18, 2026
@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

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

查重:不重复。 按 FORK_PREFIX_CONFLICT 检索 open+closed 的 issue 与 PR:只有已关闭的历史(#2399 dsh fork stage-1、#2403 adoption PR、#2625 BLOCKED sentinel、#2394、#2383)、本 PR 和现场报告 #3741,没有并行的在途工作项。本 PR 就是 #3741 的可诊断性修复;卡死本身的根因 triage 继续在 #3741。

标签: bug / scope/kernel / p2 —— fork 子会话被 409 永久卡死是真实缺陷家族,机制在会话核心的 fork prefix gate;能用但有明显问题 → p2。

审核结论(八问)

  1. 是什么问题:fork 子会话首个请求命中 FORK_PREFIX_CONFLICT 时只得到一个裸 409——服务端没有日志、客户端没有分类。这个 gate 每个请求都重跑,确定性失配意味着该子会话永久卡死且无人能说出为什么。
  2. 什么原因导致的:gate(publicForkInputMatches)只返回布尔值,拒绝原因在决策点就被吞掉,409 路径又不打任何日志——诊断盲点是机制性的,不是漏写一行。
  3. 影响范围与严重程度:波及所有继承前缀失配的 plugin-mode fork 子会话(现场报告 This run failed with 409: "first child request does not match its inherited ordered prefix" | 本轮运行失败409 "first child request does not match its inherited ordered prefix" #3741 即其一);受影响的子会话完全不可用。本 PR 本身只加诊断,不改 accept/reject 语义——不恶化也不解决卡死。
  4. 怎么修复的:gate 改为返回分类 verdict——cell-mismatch@N(区分 id-drift / hash-drift-under-same-id / ref-broken,只带截断的 id/hash 预览)/ prefix-longer-than-incoming / incoming-unmappable / receipt-missing / snapshot-missing / projection-error;拒绝时打一条 warn 日志(session id、protocol、reason),409 body 增加 additive detail 字段。fail-closed 与 [Bug][plugin] protectedTools 的 BLOCKED 映射导致纯文本 snapshot 返回 409 #2620 BLOCKED sentinel 原样保留。
  5. 是否有回归问题:逐项审过既有行为——accept/reject 语义不变(fail-closed、per-cell gate、短路顺序全部等价);409 body 的 ok/code/error 字段与错误文案不变,detail 是纯 additive JSON(客户端忽略未知字段);无 config surface、无持久化格式、无 kernel 变更。审核中发现三个真实问题,已全部当场修掉(见下)。
  6. 是否破坏缓存:不涉及——不触碰缓存/重试/增量游标/去重键。
  7. 是否改了设计方向:不涉及——只在既有 gate 上加诊断面,架构/边界/数据流未动。
  8. 是否引入新配置:不涉及。

审核中发现的问题与处置(均当场修复,独立标注 commit)

  1. ref-only 失配会被误标(新代码里的缺陷):一个 cell 唯一失败的检查是 ref 映射(id+hash 都一致,例如 compaction 边界剪枝后映射缺失)时,旧代码仍会输出 hash-drift-under-same-id 并打印两个相同的 hash——把 triage 引向幻影 flag flip。已改为每个证据部分只在自己的检查失败时输出,并在 tests/plugin-fork.test.ts 加了回归 pin(破坏 child 的 byRaw 映射后恢复,断言 detail 只有 ref-broken)。
  2. 原始 head CI 红:unused-exports gate 报 PublicForkPrefixVerdict 为未使用导出,导致其后的所有测试 lane 被 skip——全量套件一次都没跑过。已去掉 export(另一 agent 同期推了同一修复,已并入同一分支,无重复提交)。
  3. issue 引用指错:所有 #3740 引用指向的是无关的 GitHub issue(acp_delegate_wait reports "Failed" on a normal wait timeout while the delegate is still running #3740 是一份 acp_delegate_wait 措辞报告);现场报告实际是 This run failed with 409: "first child request does not match its inherited ordered prefix" | 本轮运行失败409 "first child request does not match its inherited ordered prefix" #3741。注释与 PR 正文已改对;标题后缀归系统管理,未动。

另外把分支 rebase 到了当前 master(6f2b113bd)。新 head 上的本地验证:typecheck clean、unused-exports gate clean、build OK、tests/plugin-fork.test.ts 48/48。

判定:审核侧无拦点,代码可合入,等新 head 02c4fb566 的全量 CI 转绿即可合并;卡死本身的根因 triage 继续留在 #3741(需要用户侧证据)。

@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

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

CI 确认:新 head 02c4fb566 上全部远端 lane 转绿——主 CI(gate + ubuntu/windows × node22/24 全量测试矩阵)✅、Windows codex overlay ✅、E2E real codex ✅、Theorem Lane ✅、ci-image ✅、e2e-native-dsh/opencode/pi ✅、e2e-dsh-plugin-lane ✅、e2e-local-registry ✅、e2e-pi-codex-ws ✅、e2e-codex-persona ✅、e2e-fake-codex ✅。列表里几条 cancelled 是同一次推送重复触发被 GitHub 自动取消的实例,不是失败。

PR 状态:open、mergeable,无审核拦点——可以合并了(合并按惯例由人来)。

@ranxianglei

Copy link
Copy Markdown
Owner Author

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

补一条记录修正:此前 18:45Z 的「全绿」结论对 main CI 而言为时尚早——head 02c4fb566 上存活的 pull_request run 被 plan 按设计跳过(labeled 事件且无 ci:full;rerun 保留原事件上下文),全量矩阵当时并未真正执行。

现已核实:

@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

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

确认并采纳这条修正:我此前「主 CI 全量矩阵已绿」的说法为时过早——当时存活的 pull_request run 的 plan 按设计被跳过(labeled 事件且无 ci:full),全量矩阵并未真正执行,我实际核到的只是真实跑过的 e2e/overlay/image 各 lane。记录以此条为准。

当前状态我已独立复核:

  • PR 标签已含 ci:full;run 39159162245(pull_request 事件,head 02c4fb566)completed/success;
  • 该 run 的 28 个 job 逐一过过:12 个测试 leg(ubuntu/windows × node22/24 × 3 shard)+ 4 个 static-gates leg 全部 success,gate 套件各项亦绿,含此前瞬时挂起的 test (windows-latest, 22, 2);唯一非 success 是 red-signal = skipped(无失败哨兵 job,按设计跳过);
  • mergeable_state: clean。

结论不变:审核侧无拦点,可人工合并。

@ranxianglei
ranxianglei merged commit 6dca5e4 into master Oct 19, 2026
188 of 222 checks passed
@github-actions

Copy link
Copy Markdown
Contributor

📦 Merged Build Published

This merge to master was published as 0.0.579-master.6dca5e4 (rolling master tag).

npm install -g billion-context@0.0.579-master.6dca5e4  # 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.

ranxianglei pushed a commit that referenced this pull request Oct 19, 2026
#3757 review)

Inverting the 409 sub-cases removed the only coverage of the
hash-drift-under-same-id and ref-broken verdict branches (including
#3743's fix that a pruned ref reports ref-broken alone, never
hash-drift). Pin both directly against publicForkInputCheck with
negative assertions so a future classifier regression cannot ship
silently behind the advisory warn.
LoveDoLove pushed a commit to LoveDoLove-Forked-Projects/billion-context that referenced this pull request Oct 19, 2026
…a classified warn (ranxianglei#3741)

The ordered-prefix 409 was a permanent dead-end: the rejected first
request never incremented stats.requests, so every retry re-hit the
same gate with no clear or TTL path (field report: dsh fork child
stuck after an in-place proxy upgrade re-keyed message ids, ranxianglei#3642
canonical re-key generation). Routing is the session-id binding
(recordPluginSession -> resolveConversation); the byte-exact pre-gate
added no correctness fold reconcile (ranxianglei#3641 order-over-hash) doesn't
already enforce — it only produced unrecoverable 409s.

Now: mismatched first request is adopted, logs one classified warn
(ranxianglei#3743 reason taxonomy), publicForkPrefix stays false so the side-lane
demotion heuristics keep their normal say. Cost: a divergent child's
first request goes out unfolded — a one-time token spike, never
corruption. Gate stays one-shot (stats.requests > 0 bypasses it).
@github-actions github-actions Bot mentioned this pull request Oct 19, 2026
ranxianglei pushed a commit that referenced this pull request Oct 30, 2026
Converge onto the resident ci-fixes baseline (master dc10c2d + guard-lane
fixes) so this branch is current with master. Clean union-of-intent merge:
the #4266 thinking-gate work (src/plugin.ts, src/agent/fork-adopt.ts) is
untouched by ci-fixes; tests/plugin-fork.test.ts auto-merges both this PR's
#3743 identity-field pin and master's #5140 cold-tail 409-body diagnostic.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working ci:full Run the FULL CI matrix (all e2e lanes + 12-leg test matrix) on this PR p2 scope/kernel

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant