Skip to content

[PR] fix: enforce anthropic-wire-only contract in the incremental-turns path (#3433) - #3440

Closed
ranxianglei wants to merge 1 commit into
masterfrom
2026-10-17_dsh-pi-incremental-anthropic-guard
Closed

ranxianglei wants to merge 1 commit into
masterfrom
2026-10-17_dsh-pi-incremental-anthropic-guard

Conversation

@ranxianglei

Copy link
Copy Markdown
Owner

Closes #3433

What this does

The incremental-turns lane (#3248) is end-to-end Anthropic-wire-only: it ingests with anthropicToCore, re-emits with coreToAnthropic, and forwards the assembled body to the ORIGINAL request path. But two host entry points claimed "anthropic wire only" without enforcing it:

  • src/agent/dsh-native.ts's maybeIncrementalNative checked only Array.isArray(messages) + typeof model === "string" — an OpenAI chat-completions body passes both;
  • src/agent/pi.ts's maybeIncrementalTurn (shared by pi AND pi-native by design) had the same gap, and pi genuinely serves openai/responses/google provider APIs.

With the rollout flag on, an OpenAI conversation would be converted onto the lane where: assistant.tool_calls are silently dropped, role:"tool" messages are mis-mapped into user text blocks (tool_use_id association lost), and content:null bodies throw mid-apply. omp/opencode already guarded with wireMessagesLookAnthropic; this brings dsh/pi to parity and enforces the contract at the trust boundary too.

Changes

  1. Client guards (the issue's minimal fix): wire the existing wireMessagesLookAnthropic into dsh-native (src/agent/dsh-native.ts) and pi (src/agent/pi.ts). Non-anthropic shapes now stay on full resend — exactly what PLUGIN.md documents.
  2. Lane boundary enforcement: src/incremental-lane.ts rejects non-anthropic message shapes with 400 (laneMessagesLookAnthropic, a keep-in-sync mirror of the client helper — same pattern as REQUEST_PARAM_KEYS / INCREMENTAL_TURN_MARKER_HEADER). No host can corrupt the lane's canonical state anymore; the empty-messages sticky-system-update case stays legal (deliberate divergence from the client predicate, documented in the docstring).
  3. Regression tests:
    • tests/dsh-native.test.ts — OpenAI shape (tool_calls + role:"tool") rides full resend untouched, no marker; anthropic control still converts.
    • tests/issue3433-pi-incremental-guard.test.ts (new) — real proxy with the lane LIVE (unified storage + rollout forced on) so the probe passes and the GUARD is what decides: OpenAI shape not replaced; anthropic control converts (seed, sinceSeq 0).
    • tests/incremental-lane.test.ts — lane 400s role:"tool" and tool_calls bodies with a clear error, applies nothing, still accepts a valid seed afterwards.

Behavior changes (old → new)

Path Old New Impact today
dsh/pi incremental entry, non-anthropic body converted onto the anthropic-only lane → tool calls dropped / role:"tool" corrupted / content:null throws stays on full resend (documented contract) none — rollout pluginIncrementalTurns is at percent 0, no traffic reaches this code
POST /__bili/plugin/turn (+ marker-diverted turns) with non-anthropic message shapes TypeError or silent corruption inside applyTurn 400 with explicit error none — correct hosts never send these shapes; a pre-fix host build gets a clean 400 instead of corruption
dsh empty messages array passed the array check into planIncrementalTurn rejected client-side (parity with omp/opencode) dsh system-only updates ride full resend like every other lane's do

No config surface changes. No kernel/ changes. No version bump.

Verification

  • npm run typecheck clean (kernel + pi-subagents + bili).
  • Targeted local runs: tests/dsh-native.test.ts 55/55, tests/incremental-lane.test.ts 18/18, tests/issue3433-pi-incremental-guard.test.ts 1/1, adjacent tests/pi-omp-fork-adopt.test.ts 22/22. Full-suite verdict = CI on this head.

Out of scope (tracked as a decision request in #3433): making the lane itself format-aware (detectWireFormat dispatch to openaiToCore/coreToOpenai + per-conversation format recording) — the "real fix" that would let OpenAI lanes actually USE incremental turns; a feature/wire change requiring owner sign-off.

…turns path (#3433)

dsh-native's maybeIncrementalNative claimed 'anthropic wire only' but only checked messages-array + model-string, so OpenAI chat-completions bodies (assistant.tool_calls / role:"tool") were converted onto a lane that ingests with anthropicToCore: tool calls silently dropped, role:"tool" mis-mapped into user text, content:null bodies throwing mid-apply. pi/pi-native shared the same gap in the factory hook (pi serves openai/responses/google provider APIs).

- wire the existing wireMessagesLookAnthropic into the dsh-native and pi entry points (omp/opencode parity; restores the PLUGIN.md-documented contract)
- enforce the same predicate at the lane boundary: 400 for non-anthropic message shapes instead of trusting every host to pre-filter (mirror of the client helper, keep-in-sync marked)
- regression tests: dsh OpenAI shape rides full resend (+ anthropic control), pi OpenAI shape not replaced under a live lane (+ anthropic control), lane 400s role:"tool"/tool_calls and still accepts a valid seed afterwards
@github-actions

Copy link
Copy Markdown
Contributor

📦 Built Package Artifact

Branch: 2026-10-17_dsh-pi-incremental-anthropic-guard (6f9956c)

Option A — Install this exact build (recommended)

npm install -g [email protected]

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

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

查重:不重复。 open+closed 的 issue/PR 全搜了 wireMessagesLookAnthropic 与 incremental-lane 相关项:只与本 PR 的同源 issue #3433 及已合入的 #3292(phase-2 rollout,不同范围)相关;在途的 #3414/#3340/#3257/#3349/#3355 症状与机制均不同(writeNow 竞态测试、ack 归属、prefix fast path、marker-divert 管理面、文档遗留),无重复。

审核方式:diff 全文通读(6 文件 +229/-3,单 commit 6f9956cdc,分支与 master 同步无落后);对照原 issue #3433 逐层验证修法是否打到根因;本地定向跑测试(dsh-native 55/55、incremental-lane 18/18、issue3433-pi-incremental-guard 1/1、相邻 pi-omp-fork-adopt 22/22——首跑一次失败是本地 pi-subagents 未构建的环境问题,构建后通过,非代码缺陷);CI 在本 head 全绿(static-gates + 6 个 test shard + docs/config/lock 门禁,e2e 门控 lane 按设计 skip);与其他在途 PR 无文件重叠。

八问:

  1. 是什么问题:增量车道端到端是 Anthropic-wire-only(anthropicToCore 进、coreToAnthropic 出),但 dsh 和 pi 两个宿主入口只查了「messages 是数组 + model 是字符串」——OpenAI chat-completions body 原样满足这两条,开启 pluginIncrementalTurns 后 OpenAI 会话会被转进这条车道,assistant.tool_calls 被静默丢弃、role:"tool" 被错映射成 user 文本块。
  2. 什么原因导致的:两层。直接原因是接线遗漏——守卫函数 wireMessagesLookAnthropic 已存在且 omp/opencode 已接入,dsh-native/pi 漏接(bundle grep 计数为 0);结构性原因是车道服务端信任客户端预筛,信任边界自身没有校验。
  3. 影响范围与严重程度:今天零用户影响(rollout pluginIncrementalTurns percent 0,流量到不了这段代码)。但它是潜在静默数据损坏——一旦 rollout 就会命中最常见的 wire shape(报告者机器上 openai 262 个会话 vs anthropic 2 个),且损坏发生在 applyTurn 内部(丢 tool call / 错映射),不易从表面察觉。严重度随启用而升级。
  4. 怎么修复的:双层契约强制。① 客户端:把现有 wireMessagesLookAnthropic 接进 dsh-native 和 pi 两个入口,非 anthropic shape 留在 full resend(正是 PLUGIN.md 文档化的契约,pi-native 经 createBiliPlugin() 共享同一守卫一次覆盖);② 服务端:车道加 laneMessagesLookAnthropic(keep-in-sync 镜像,允许空数组以保 sticky-system-update 合法,分歧已在 docstring 注明),非 anthropic shape 在任何状态变更前直接 400。「真正的修复」(车道 format-aware,detectWireFormat 分发)明确划出范围,作为决策请求留在 [bug] dsh-native not wired to wireMessagesLookAnthropic: Enabling pluginIncrementalTurns on the OpenAI lane silently dro | [bug] dsh-native 未接入 wireMessagesLookAnthropic:OpenAI 车道启用 pluginIncrementalTurns 会静默丢弃 tool_calls / role:"tool" #3433 等 owner 拍板——无决策依赖部分先修、需决策部分挂起,拆分正确。
  5. 是否有回归问题:逐项清点触碰路径的既有行为——anthropic shape 转换行为不变(两个测试的 control 断言证明转换仍发生);full-resend 回退语义保持(return undefined = 不替换、不打 marker,与该 handler 其他早退一致);车道的空 messages + system sticky 更新仍合法;400 发生在 turn lock 和任何状态变更前(测试断言拒绝后有效 seed 正常落 seq 1);客户端对 400 有现成处理(noteIncrementalRejection 熔断:reset → 下一轮 reseed,连续 strike 才 dead)→ 旧版宿主 build 拿到干净 400 + 自动回退 full resend,不坏会话。方向是 fail-soft → fail-fast,与高危模式(吞错化)相反。PR 披露的两处行为变更均核对无误:① dsh 空 messages 数组原来会传进 planIncrementalTurn,现在留 full resend(退化输入,full resend 恒安全);② 车道收非 anthropic shape 从 TypeError/静默损坏变显式 400。
  6. 是否破坏缓存:不涉及。拒绝先于 seq/状态变更,缓存键、重试策略、增量游标、去重键均无变化。
  7. 是否改了设计方向:否。车道架构、数据流、模式判定均未动;本 PR 只是把 PLUGIN.md 文档化的契约从「文档承诺」变成「代码强制」。镜像函数模式与既有 REQUEST_PARAM_KEYS 镜像一致。
  8. 是否引入了新的配置:无。无 env/config/CLI 新增,无 version bump,无 kernel/ 改动。

测试有效性:三组测试双向有牙——既断言回归(OpenAI shape 不被替换 / 车道 400 且不落任何状态),也断言 control(anthropic shape 仍转换 / 有效 seed 仍正常落地),去掉守卫或过度拦截都会红;pi 测试用真实 proxy 且强制车道 LIVE(unified storage + rollout=1),确保 probe 通过、由守卫本身裁决,隔离正确。

结论:可合。 代码、diff 整洁度(每行都服务于目的,无空白/锁文件噪音)、测试质量全部过关;CI 在本 head 全绿。无阻塞项。

@ranxianglei

Copy link
Copy Markdown
Owner Author

(ai: zhipuai-lb/glm-5.3)

已并入 #4590。关单收尾;误判请 reopen。

@ranxianglei ranxianglei changed the title fix: enforce anthropic-wire-only contract in the incremental-turns path (#3433) [PR] fix: enforce anthropic-wire-only contract in the incremental-turns path (#3433) Oct 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant