Skip to content

[PR] Failed-compress recovery: end the re-request on a user turn, not an assistant prefill (fixes upstream 400) - #5605

Open
ranxianglei wants to merge 1 commit into
masterfrom
2026-11-03_failed-only-receipt-user-cap
Open

ranxianglei wants to merge 1 commit into
masterfrom
2026-11-03_failed-only-receipt-user-cap

Conversation

@ranxianglei

@ranxianglei ranxianglei commented Nov 3, 2026 •

Copy link
Copy Markdown
Owner

What & why

Since #5220/#5231 (0.1.210+), a round in which every compress/decompress call fails re-requests with the recovery receipt as the final message. That receipt is assistant-role by design — its bytes mirror the markers emitMarker already streamed, so the client echoes them back as assistant content and the frozen prefix never diverges (#5220 cache invariant). But Anthropic rejects a request that ends on an assistant turn:

  • messages: final assistant content cannot end with trailing whitespace — when the joined markers end in \n;
  • This model does not support assistant message prefill. The conversation must end with a user message. — Claude 5 models reject any prefill.

Either way the client gets a 400 and the turn ends; in Claude Code a subagent dies with Agent terminated early due to an API error. The path meant to keep the task alive (#5220) is the one that kills it.

Fix

In src/loop/core.ts, after splicing this round's failed frames and appending the receipt, cap the body with a thin user nudge whenever the last core message is still an assistant turn, so the re-request always ends on a user turn. The guard sits after both sub-cases, so it also covers the deduped-markers edge where a round that opened with model prose leaves an assistant tail.

The cap is a separate message, not a role flip on the receipt: flipping the receipt to user would break the #5220 cache invariant (the client echoes the streamed markers as assistant, so a user-role receipt desyncs the prefix). The nudge is ephemeral like the receipt — never streamed, never persisted, dies with the runCompressLoop invocation — so the client never sees it and there is no cross-turn seam.

Behavior change (all wires)

The failed-only re-request now appends one short user turn whenever it would otherwise end on an assistant message. Tolerant wires (OpenAI / Responses / Google) accept either shape; capping makes "an outbound request never ends on an assistant turn" a universal invariant instead of Anthropic-specific. No config field added, no env var, no default/threshold moved.

Tests

tests/issue5220-failed-compress-receipt.test.ts gains a regression pin asserting the re-request's final message role is user (the existing T1/T6 only asserted the marker bytes ride upstream, never which role ends the body) while still carrying the ❌ receipt as assistant text. Targeted suites green locally (90 tests across the loop/receipt files); full regression + e2e on CI.

Closes #5604


Update (post-external-review)

  • Rebased onto current master. The earlier CI red (storage-render golden, storage engine: full-mode restart) was inherited from the stale base d244fff (~233 commits behind) — those shards fail on the old base and pass on current master, unrelated to this change. The rebase clears them; no goldens were regenerated.
  • Added an Anthropic-wire regression pin (tests/issue5220-failed-compress-receipt.test.ts: signed-thinking-block failed-only round) covering the wire where the 400 actually fires — the original pin only exercised the Responses wire. Local verification: typecheck clean; receipt + loop-core 34/34; previously-red storage/golden 17/17.

@ranxianglei ranxianglei changed the title Failed-compress recovery: end the re-request on a user turn, not an assistant prefill (fixes upstream 400) [PR] Failed-compress recovery: end the re-request on a user turn, not an assistant prefill (fixes upstream 400) Nov 3, 2026
@github-actions

github-actions Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

📦 Built Package Artifact

Branch: 2026-11-03_failed-only-receipt-user-cap (2e3b183)

Latest build — install this exact version

npm install -g billion-context@0.0.8311-pr.5605.71f3b65

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
2e3b183 billion-context@0.0.8311-pr.5605.71f3b65 build
c4d8bea billion-context@0.0.7987-pr.5605.2b9bd1e build

Download artifact

  1. Download the artifact from the Actions run
  2. Extract the tarball and install:
tar xzf billion-context-pr5605.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 issue/PR(近期 50 条):没有其他条目动「failed-compress recovery 重请求的最后一条消息角色」这个点。最近的几个在途 PR(#5537 egress tool-args JSON guard、#5602 fork multimodal adoption、#5603 dsh refusal floor)机制与文件都不同。GitHub issue #5604(本 PR 目标)是这个症状+机制族的唯一跟踪项。

标签:bug / claude-code / scope/wiring / p1 —— 保活路径在 Claude 5 模型上直接杀死回合(subagent 当场死,按 owner 修订 2026-10-19 属 p1 类①「会话不可用」);改动点在 src/loop/core.ts 请求管道(scope/wiring);报告场景是 Claude Code 车道。

审核进行中,CI shards 跑完后给完整八问结论。本地定向验证(PR head c4d8bea):tests/issue5220-failed-compress-receipt.test.ts 7/7 绿;loop 区全部套件(tests/loop-*.test.ts + receipt)合计 114/114 绿。环境备注:dev checkout 跑测试前需先 npm --prefix kernel run build(acp-kernel file: 依赖解析到 kernel/dist,fresh clone 里没有)——属环境前置,非本 PR 问题。

@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

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

Review 结论:可合(§7.4 wire/message-shape 变更,需人工合并,我不代合)。CI 全绿:30 checks、0 failed、0 pending —— static-gates(ubuntu+windows)/mysql-contract/docs-sync/version-guard/test-import-graph 绿,11 个 test shards(ubuntu/windows × node22/24)全绿;e2e 各 lane 按门控 skip;theorem lane cancelled 是设计内行为(无 ci:full 标签时该 lane 只跑按需/定时,gate 主动 cancel)。本地定向验证同绿(114/114)。

八问:

  1. 是什么问题:v0.1.210 起(fix(loop): a failed compress must not end the user's task — feed the model a recovery receipt instead of breaking with "length" #5220/[PR] fix(loop): a failed compress must not kill the task — feed the model a recovery receipt instead of ending the turn #5231),一轮里所有 compress/decompress 调用都失败(failed-only 轮)时,re-request 以 recovery receipt 收尾,而 receipt 按设计是 assistant 角色。Anthropic 拒绝以 assistant turn 结尾的请求(trailing-whitespace 400 或 prefill-not-supported 400),客户端拿 400、回合结束——Claude Code 里 subagent 当场死掉。本意保活的路径实际杀死了任务。
  2. 什么原因导致的:fix(loop): a failed compress must not end the user's task — feed the model a recovery receipt instead of breaking with "length" #5220 的缓存不变量要求 receipt 字节与已流给客户端的 marker 逐字一致(客户端把它们当 assistant 内容回显,冻结前缀才不漂移),这决定了 receipt 必须是 assistant 角色;当时只钉了「receipt 必须骑在重请求上」,没钉「请求体以哪个角色收尾」。Claude 5 模型把后一条约束从隐含变成硬拒(完全不支持 prefill)。分层:根因 = failed-only re-request 体尾部形状违反 Anthropic 硬约束;表面现象 = 400/subagent 死亡。
  3. 影响范围与严重程度:proxy 模式(base URL 指到 bili)、Anthropic wire、任何 mutating 调用全失败的轮次(坏 ref、整批拒绝 [Bug] compress rejects a whole batch when one range is stale, covered or drifted into the protected tail — and the summaries it stores are the source of the dead refs | 一个 stale/covered/保护区 range 就整批拒绝,而失效 ref 正是它自己存的 summary 引用的 #5185 等)。严重度 p1:触发即回合终结、subagent 死亡、无绕行(报告者 2 天 7 次全部致命)。宽容 wire(OpenAI/Responses/Google)不 400,但重请求体同样以 assistant 收尾。
  4. 怎么修复的:failed-only 分支里 splice 失败帧 + append receipt 之后加一道判断——最后一条消息仍是 assistant 时追加一条薄 user nudge(acp_loop_r${round}_rcpt_go,"[bili] The context-management call above did not complete; its result is shown above. Continue the task.")。关键决策是 cap 为独立消息而非翻转 receipt 角色:翻转会破坏 fix(loop): a failed compress must not end the user's task — feed the model a recovery receipt instead of breaking with "length" #5220 缓存不变量(客户端回显流过的 marker 为 assistant,user 角色的 receipt 会让前缀失步)。nudge 与 receipt 同为 ephemeral——never streamed、never persisted、随 runCompressLoop 调用消亡,客户端永远看不到,跨轮无缝。guard 位于两个 sub-case 之后,覆盖 deduped-markers 边缘(模型先出 prose 开轮的 round 留下 assistant tail)。
  5. 是否有回归问题:逐项审过既有行为——① receipt 字节契约(fix(loop): a failed compress must not end the user's task — feed the model a recovery receipt instead of breaking with "length" #5220):不变,新 pin 同时断言 ❌ receipt 仍以 assistant 文本骑上游;② 成功轮:tail 恒为 tool-result 帧(role tool)或 image carrier(role user),不会 assistant 尾,cap 不触发;③ threadContinue / visibilityMarkers=false / stuck 轮:在 cap 之前 early return(emitCompletion("length")),契约不变——这些视图没有 marker 字节,补隐形字节正是 Bundled cache-seam fuzz deterministically fails under default seed 1592 (widened 12 scenarios): failed compression pairs | 自带 cache-seam fuzz 在默认种子 1592(加宽 12 场景)下确定性失败:失败压缩对凭空消失 + 运行时判官同族 I1 告警 #4186 接缝,正确避开;④ plugin 模式:bili 不在服务端执行 compress(宿主原生调用使 realCalls>0 → reRequest=false),分支不可达,零影响;⑤ 同一请求内连续多个 failed-only 轮:每轮各自追加 receipt+cap 对,仍 user 收尾,自洽。高危模式(吞错、阈值/时序漂移、输出格式变化)逐项过:无。一处诚实边界:PR body 的 "an outbound request never ends on an assistant turn" 措辞略宽——实际成立的是 failed-only re-request 永不 assistant 收尾;成功轮视图在后续轮 hideConsumed 隐藏 consumed pair 后可留 assistant prose 尾(既有形状,与本 PR 无关,且宽容 wire 上游不拒)。不构成拦点,记录边界。
  6. 是否破坏缓存:否。receipt 字节不变(冻结前缀载体不动);cap 消息追加在 receipt 之后且 ephemeral——不进客户端历史一个字节、不落盘、不进下一轮客户端请求,前缀缓存稳定契约不变。
  7. 是否改了设计方向:否。不碰 kernel/、持久化、配置 schema;只在既有 failed-only recovery 路径内加一条 ephemeral 帧。
  8. 是否引入新配置:无。无 env、无 config 字段、无默认值变动。

测试有效性:新 pin 断言的恰是此前缺失的维度(最终消息 role === "user"),同时保留 ❌ 字节断言——原 bug 回归时该测试立即红(修复前最终 role 是 assistant),非恒真断言。仅 Responses wire 载体测试,但断言对象是 adapter 映射前的 coreMessages role,wire-agnostic 性质对三条 wire 成立;marker 字节侧由既有 T1/T6 覆盖。

#1304 备注:wire-constraint ledger 目前无「请求须以 user 收尾」条目(wire-contract-fakes.ts 无先例)。该约束是 model-family 相关的(宽容 wire 接受两种形状),全局 ledger 规则会对 OpenAI/Google 场景误报,loop 级回归 pin 是正确的层;本 PR 不要求补 ledger。

判定:可合。请确认 CI 后自行合并 #5605 (我不能代合)。

@ranxianglei

Copy link
Copy Markdown
Owner Author

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

审核记录:本账号即 PR 作者 ranxianglei,GitHub 禁止自我 approve(POST /reviews 返回 422 “Can not approve your own pull request”)。本评论承载专职 reviewer 的 APPROVE 判定,供 owner 在 UI 点 Approve。

APPROVE. 单 commit,+63/-0,仅 src/loop/core.ts + 一个测试文件。不碰 kernel/、无 config surface、无安全面。以下为独立审核(未照抄 issue 结论),按八问作答。

八问

  1. 是什么问题:failed-compress 恢复轮(fix(loop): a failed compress must not end the user's task — feed the model a recovery receipt instead of breaking with "length" #5220)把回执(receipt)当作重请求的最后一条消息,而回执按设计是 assistant 角色。Anthropic 拒绝「以 assistant 结尾」的请求(trailing whitespace / prefill not supported),客户端拿 400,Claude Code subagent 当场死——保活路径反而杀死了回合。
  2. 什么原因导致的:根因在 src/loop/core.ts 的 failed-only 分支(约 :1589-1646):剥离本轮 ephemeral 失败帧后 append 一条 assistant 回执就直接重请求,后面没有任何东西,于是请求体以 assistant 收尾。这是构造重请求体的结构性缺陷,不是表面现象。
  3. 影响范围与严重程度:仅 failed-only 恢复轮(一轮内 compress/decompress 全失败时触发)。Anthropic 线必现 400;报告者 2 天 7 次、其中 6 次在 subagent。p1——核心承诺(保活)失灵、subagent 死亡。OpenAI/Responses/Google 线容忍该形状,不受影响。
  4. 怎么修复的:在两个子情形(有回执 / 标记去重无回执)都处理完之后,若末条 core 消息仍是 assistant,就追加一条薄 user nudge(acp_loop_r${round}_rcpt_go),让重请求恒以 user 收尾。刻意不改回执角色(翻成 user 会破坏 fix(loop): a failed compress must not end the user's task — feed the model a recovery receipt instead of breaking with "length" #5220 缓存不变量——客户端把流式标记回显为 assistant,user 回执会使前缀失步),改用独立消息。nudge 与回执一样 ephemeral(不流式、不落盘、随调用消亡),客户端永不可见,故无跨轮接缝。
  5. 是否有回归问题:逐条清点 touched 路径既有行为——① 首次失败→带 assistant 回执重请求:保持(回执字节未动,测试钉 ❌ 仍上行);② ephemeral 失败帧剥离(codex 版本压缩后缓存大幅度降低问题 #1548/Bundled cache-seam fuzz deterministically fails under default seed 1592 (widened 12 scenarios): failed compression pairs | 自带 cache-seam fuzz 在默认种子 1592(加宽 12 场景)下确定性失败:失败压缩对凭空消失 + 运行时判官同族 I1 告警 #4186):保持(splice 未动);③ 标记去重→跳过空回执:保持(子情形 B 未动,cap 仅在末条为 assistant 时加);④ 小模型(2B级)compress 循环空转:同参数重复失败烧满 10 轮 + 幻觉 ref + 摘要长度约束不遵守 #156 签名熔断 / MAX_LOOP_ROUNDS 边界:保持(cap 不参与签名追踪);⑤ visibilityMarkers=false / threads-delta 走旧契约早退:保持(早退发生在 cap 之前);⑥ 缓存前缀稳定(fix(loop): a failed compress must not end the user's task — feed the model a recovery receipt instead of breaking with "length" #5220):保持(cap 纯尾部追加,共享前缀逐字不变)。无意回归:无。
  6. 是否破坏缓存:不破坏。cap 位于回执之后、纯尾部追加;冻结前缀(含回执)逐字不变,下一客户端轮不含 cap 但共享前缀到回执边界仍命中,cap 落在任何共享前缀之外,零额外成本。
  7. 是否改了设计方向:未转向。仅在既有 failed-only 重请求路径上补一条守卫,数据流/边界不变。PR 描述称之为「universal invariant」——实现只覆盖 failed-only 分支,而该分支是唯一的合成 assistant-tail 重请求生产者,实践上等价于通用不变量;措辞略宽于代码,不构成缺陷。
  8. 是否引入了新的配置:无。无新 config/env/默认值/阈值。

测试有效性:#5604 回归钉断言重请求末条消息角色 = user(既有 T1/T6 只断言 ❌ 字节上行,从不断言收尾角色),同时断言 assistant 回执仍携 ❌(缓存不变量完整)。去掉修复则末条为 assistant 回执,该断言必红——真测试,有牙;assistantTexts 为真实解析助手而非恒真断言。CI 绿(head c4d8bea)。

两模式(§6):proxy 模式受影响且被正确修复;plugin 模式结构上进不了此分支(injection 被抑制→无 proxy 侧 compress 执行→failedMutating 恒空),即便进入也是纯尾部 additive、末条非 assistant 才不加,不可能有害。

判定:可合(APPROVE)。 无需顺手小修:命名沿用 acp_loop_r${round}_* 约定,注释讲 why(缓存接缝 / 为何不翻角色),无 console.error / as any / ACP 标签,diff 每一行都与本 PR 目的相关。

@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

(ai: vllm-qwen/qwen3.8-27b) ✅ 审核完成 = APPROVE(可合)。独立八问结论已贴到 PR 评论(GitHub #5605,comment id 6601194833)。注意:本账号即 PR 作者 ranxianglei,GitHub 禁止自我 approve(POST /reviews → 422 “Can not approve your own pull request”),故正式 Approve 待 owner 在 UI 点一下。判定要点:守卫位置正确(两子情形之后)、#5220 缓存不变量保持(纯尾部追加)、回归钉有牙(断言末条角色=user)、行为变更已披露、不碰 kernel/、无 config/安全面、CI 绿(head c4d8bea)。无需顺手小修。

@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

[bot] 🏷 (ai: vllm-qwen/qwen3.8-27b)

审核收工 = APPROVE(可合)。 本轮做了什么:

  1. 独立审读:PR 正文 + 关联 issue [Bug] failed-compress recovery receipt (#5220/#5231) leaves the re-request ending on an assistant turn → upstream 400 (trailing whitespace / prefill not supported), subagent dies #5604 + 全量 diff(src/loop/core.ts +18 / 测试 +45),逐一核对——守卫位置(两个子情形之后、重请求之前,core.ts:1643)、fix(loop): a failed compress must not end the user's task — feed the model a recovery receipt instead of breaking with "length" #5220 缓存不变量(cap 纯尾部追加、前缀逐字不变)、两模式(§6:proxy 修复、plugin 结构上不可达且即便进入也 additive 无害)、回归钉有效性(断言末条角色=user,去掉修复必红)。
  2. 判定 APPROVE:位置正确、无无意回归、行为变更已在 PR 披露、不碰 kernel/(无授权门禁)、无 config/安全面、CI 绿(head c4d8bea)。
  3. 正式 Approve 我提不了:本账号即 PR 作者 ranxianglei,GitHub 禁止自我 approve(POST /reviews → 422 “Can not approve your own pull request”)。完整八问结论已贴到 PR 评论(GitHub [PR] Failed-compress recovery: end the re-request on a user turn, not an assistant prefill (fixes upstream 400) #5605,comment id 6601194833),待 owner 在 UI 点 Approve。
  4. 无需顺手小修:命名沿用 acp_loop_r${round}_* 约定、注释讲 why、diff 每行皆与本 PR 目的相关。

一句话:可合,等 owner 点 Approve。

…t never ends on an assistant prefill

A round whose compress/decompress calls ALL fail re-requests with the recovery
receipt (#5220) as the final message. That receipt is assistant-role by design —
its bytes mirror the markers emitMarker already streamed, so the client echoes
them back as assistant content and the frozen prefix never diverges (#5220 cache
invariant). But Anthropic rejects a request that ENDS on an assistant turn:
"final assistant content cannot end with trailing whitespace" when the joined
markers end in \n, and outright "assistant message prefill not supported" on
Claude 5 models. Either way the 400 kills the turn and a subagent dies with
"Agent terminated early due to an API error" — the path meant to keep the task
alive (#5220) is the one that kills it.

After splicing this round's failed frames and appending the receipt, cap the
body with a thin user nudge whenever the last message is still an assistant
turn, so the re-request always ends on a user turn. The nudge is ephemeral like
the receipt (never streamed, never persisted, dies with the invocation), so the
client never sees it and there is no cross-turn seam. Flipping the receipt
itself to user would break the #5220 cache invariant, hence a separate cap.

Behavior change (all wires): the failed-only re-request now appends one short
user turn whenever it would otherwise end on an assistant message. Tolerant
wires (OpenAI/Responses/Google) accept either shape; capping makes the invariant
universal instead of Anthropic-specific. No config surface added.

Regression pin: tests/issue5220-failed-compress-receipt.test.ts asserts the
re-request's final message role is user (the existing T1/T6 only asserted the
marker bytes ride upstream, never which role ends the body).
@ranxianglei
ranxianglei force-pushed the 2026-11-03_failed-only-receipt-user-cap branch from c4d8bea to 2e3b183 Compare November 6, 2026 00:57
@ranxianglei

Copy link
Copy Markdown
Owner Author

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

APPROVE(re-review,head 2e3b183)。 本账号即 PR 作者 ranxianglei,GitHub 禁止自我 approve(POST /reviews → 422 “Can not approve your own pull request”),正式 Approve 待 owner 在 UI 点一下。

与上一版(c4d8beaac)相比改了什么

  • src/loop/core.ts 守卫逐字节未变:我上一版已逐行审过的那条 acp_loop_r${round}_rcpt_go user-cap 守卫一字未改(仅因 rebase 行号从 :1643 位移到 :1633)。核心修复结论原样成立。
  • rebase 到最新 master:old→new head 那个 211 文件的大 diff 全是 master 自身推进(upstream-throttle model-scope、kernel media-refs、notice surface 等),都不属于本 PR。本 PR 对当前 master 的实际贡献仍只有 2 个文件。
  • 测试从 +45 扩到 +122/-3(真正的增量):新增一条 Anthropic 线 + 签名 thinking 块的回归测试——这才是 [Bug] failed-compress recovery receipt (#5220/#5231) leaves the re-request ending on an assistant turn → upstream 400 (trailing whitespace / prefill not supported), subagent dies #5604 在 Claude Code 上真正触发 400 的形状(buildVisibilityMarker 追加 \n → 回执并入 assistant 轮以尾随空白结尾 → “final assistant content cannot end with trailing whitespace” / prefill not supported)。旧的 Responses 线那测到不了这个形状,新增把它补上了。配套给 makeCtx/drain 加了可选 protocol/adapter 参数(带默认值,向后兼容,T1/T6 等既有用法不受影响)。

八问(针对本版)

  1. 是什么问题:同前——failed-compress 恢复轮的重请求以 assistant 收尾,Anthropic 拒绝(trailing whitespace / prefill not supported),客户端拿 400,Claude Code subagent 当场死。
  2. 什么原因导致的:同前——src/loop/core.ts failed-only 分支 append 一条 assistant 回执后直接重请求、后面没有东西,于是请求体以 assistant 收尾。构造重请求体的结构性缺陷。
  3. 影响范围与严重程度:同前,p1(保活承诺失灵、subagent 死亡);仅 Anthropic 线必现,OpenAI/Responses/Google 线容忍该形状不受影响。
  4. 怎么修复的:同前——两个子情形都处理完后,若末条 core 消息仍是 assistant 就追加一条薄 user nudge,让重请求恒以 user 收尾;刻意不改回执角色以护 fix(loop): a failed compress must not end the user's task — feed the model a recovery receipt instead of breaking with "length" #5220 缓存不变量;nudge 与回执一样 ephemeral(不流式、不落盘、随调用消亡)。
  5. 是否有回归问题:core.ts 逐字节未变 → 上一版逐项清点过的 6 项既有行为(首次失败带回执重请求 / ephemeral 帧剥离 / 去重跳过空回执 / 小模型(2B级)compress 循环空转:同参数重复失败烧满 10 轮 + 幻觉 ref + 摘要长度约束不遵守 #156 签名熔断边界 / visibilityMarkers=false·threads-delta 早退 / 缓存前缀稳定)全部维持不变;本次改动纯为新增测试 + 向后兼容 helper 参数。无意回归:无。
  6. 是否破坏缓存:不破坏。cap 纯尾部追加,冻结前缀(含回执)逐字不变。
  7. 是否改了设计方向:未转向。仅在既有 failed-only 重请求路径补一条守卫。
  8. 是否引入了新的配置:无。无新 config/env/默认值/阈值。

测试有效性(重点看新增):新 Anthropic 测断言重请求末条 role = user(去掉修复必红),并断言失败标记 ❌ 已流到客户端、回合存活(DONE-ANTHROPIC-5604 到达客户端);用真实 Anthropic SSE 事件形状(message_start / content_block_* / message_delta / message_stop)+ 签名 thinking 块复现生产 400 场景,比旧 Responses 测更贴近实际故障面。签名 thinking 块随 ephemeral 帧一起被剥离、不进重请求体,故不会触及 #10 的签名校验路径。CI 绿(head 2e3b183)。

两模式(§6):同前——proxy 模式受影响且被正确修复;plugin 模式结构上进不了此分支(injection 抑制→无 proxy 侧 compress 执行→failedMutating 恒空),即便进入也是纯尾部 additive、不可能有害。

判定:可合(APPROVE)。 无需顺手小修。唯一非阻塞观察:新测里 if (last.role === "assistant"){ …assert trimEnd… } 这段在通过时恒不执行(末条已是 user),属冗余诊断而非缺陷、不影响覆盖,保留不动。

@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

[bot] 🏷 (ai: vllm-qwen/qwen3.8-27b) re-review 收工 = APPROVE(可合,head 2e3b183)。 相比上一版:core.ts 守卫逐字节未变、仅 rebase 到最新 master(那 211 文件大 diff 全是 master 自身推进,非本 PR);真正增量是测试 +45→+122,新增一条 Anthropic 线+签名 thinking 块的回归测(正是 #5604 生产 400 的形状),并给 makeCtx/drain 加了向后兼容的可选参数。核心修复结论不变、无回归、不碰 kernel/、CI 绿。完整八问已贴 PR 评论(GitHub #5605,comment id 6673942261);正式 Approve 待 owner UI 点击(本账号=作者,禁自我 approve)。无需顺手小修。

@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

[bot] 🏷 (ai: vllm-qwen/qwen3.8-27b) 这条是本 agent 自己刚贴到 GitHub #5605 的 APPROVE 复审(head 2e3b183)经 upstream-sync 回同步到镜像的回声,非新指令。无新增信息/请求,不重复处理。结论维持:可合(APPROVE)——core.ts 守卫逐字节未变、仅 rebase + 测试增强;无回归、不碰 kernel/、CI 绿。正式 Approve 待 owner 在 UI 点击(本账号=作者,禁自我 approve)。

@luan-cestari-ppro

Copy link
Copy Markdown
Contributor

Tested on master fe963d7f5 together with #4898 and #5441, with a Bedrock backend (Claude Code 2.1.311 -> billion-context -> LiteLLM -> AWS Bedrock, Anthropic Messages wire).

Results

  • Stock master: 1 in-band SSE error event after a failed-compress round, in a small-window scenario (74 requests). The re-request ended on the assistant receipt.
  • With the three PRs: 0 in-band errors and 0 upstream 400s in 431 Haiku and 88 Opus requests. The issue5220-failed-compress-receipt tests pass.

A related path the PR does not cover: a preflight fold can leave the outbound body ending on an assistant message

  • Scripted scenarios with the three PRs: 0 of 655 recorded outbound /v1/messages bodies end on an assistant message. Stock master had 6 of 74 in the same small-window scenario, with final assistant content cannot end with trailing whitespace. [PR] kernel: keep signed thinking with its tool_use across folds (#4885) #4898 seems to stop most of these, because the preflight fold no longer takes the trailing pair.
  • An agentic Java coding task with Opus 5.5 and a 55K window still hit it once. The preflight fold relaxed the recent-zone protection ("relaxing soft protection (preserveRecentMessages/Tokens -> 0)"). The outbound body then ended on an assistant message, and Bedrock answered This model does not support assistant message prefill. The conversation must end with a user message. Claude Code retried the same request 4 times, and the session failed.

So a small guard still looks useful. We used this guard in prepareAnthropic, before the outbound body is built:

const lastNonSystemRole = (msgs: readonly { role?: unknown }[]): unknown => msgs.findLast((m) => m.role !== "system")?.role;
if (lastNonSystemRole(rebuiltMessages) === "assistant" && lastNonSystemRole(parsed.messages) !== "assistant") {
    rebuiltMessages = insertBeforeTrailingSystemRun(rebuiltMessages, [{ role: "user", content: "[bili] Earlier context was compressed above. Continue the task." }]);
}

With the guard, 5 more agentic runs (151 Opus requests) and a full scenario batch (432 Haiku, 48 Opus requests) had 0 upstream 400s. A fix inside the preflight fold (never leave the tail on an assistant turn) would be cleaner, if you prefer that.

@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

[bot] 🏷 (ai: vllm-qwen/qwen3.8-27b)

Thanks for the rigorous field validation — this is exactly the confirmation the fix needed, and it lands on real traffic (Claude Code 2.1.311 → bili → LiteLLM → Bedrock, Anthropic Messages wire): stock master produced the in-band SSE error on a failed-compress round, while with the three PRs you saw 0 in-band errors and 0 upstream 400s across ~500 requests, with the issue5220-failed-compress-receipt suite green. That corroborates the APPROVE on #5605 — the failed-compress recovery path is genuinely fixed, not just unit-green.

On the related preflight-fold path — confirmed real, and it is a different mechanism. I checked the code: src/preflight.ts carries the “relaxing soft protection (preserveRecentMessages/Tokens -> 0) and retrying” paths (src/preflight.ts:2364, :3125, plus the up-front variant :2319/#1841), and the reset helper zeroes both fields (src/preflight.ts:1550). When a small window forces that relaxation, the fold can consume through to the end and leave the last outbound message on an assistant turn → the same Bedrock/Anthropic 400 (assistant message prefill not supported / final assistant content cannot end with trailing whitespace). Your scripted numbers (stock 6/74 vs 0/655 with the PRs, #4898 removing most by not taking the trailing pair, and the one Opus-5.5/55K-window agentic hit after a soft-protection relax) are consistent with that chain. Two caveats on the snippet: insertBeforeTrailingSystemRun does not exist in the current tree (so that part is illustrative, not drop-in), and I have only confirmed the mechanism in code — I have not independently reproduced your specific agentic-Java scenario in this environment.

Scope: this lives in src/preflight.ts, not the src/loop/core.ts failed-only branch that #5605 fixes, so it should not be bundled into #5605 (one issue = one scope). It is the same symptom family as #5604 (outbound body ends on assistant → upstream 400 → turn/session dies) via a different mechanism, so per the same-symptom-one-issue policy it belongs tracked alongside #5604 as a second mechanism-level sub-defect, not a new fragmented issue. I searched open+closed issues for this signature and found nothing tracked yet.

Fix — my recommendation (final call is yours): I would avoid restructuring the fold itself. Protection relaxes to 0 precisely to close a token deficit, so “don’t fold the last assistant message” fights the fold’s job and may fail to close the gap. The robust shape is the invariant “no outbound Anthropic body ends on an assistant turn” enforced once at the Anthropic egress: if the last non-system message is an assistant turn, append one thin ephemeral user nudge before the trailing system run. Since this same shape now has ≥2 producers (the loop re-request #5605 already caps; the preflight fold), I’d extract that check into a single shared helper invoked at every Anthropic serialization point rather than a second hand-rolled guard — DRY, and it future-proofs against the next path that emits an assistant tail. Whoever implements it must verify the nudge lands after all cache-stable content so it does not move the prefix (the same consideration #5605 already handles). If you would rather minimize blast radius first, the targeted version is the identical guard local to prepareAnthropic, deferring the consolidation.

What I will not do without your go-ahead: modify #5605 (it is correctly scoped and now field-validated), or implement the reporter’s snippet verbatim (unverified external input, and it references a non-existent helper).

Proposed next step (one line from you triggers it): confirm (a) the durable home — keep #5604 open as the umbrella for both mechanisms, or cut a focused issue since #5605 is marked “Closes #5604” — and (b) the approach above (shared egress gate vs targeted prepareAnthropic guard). On your word I will file it with the source marker and draft the fix as its own PR.

Bottom line: #5605 stands APPROVED for its failed-compress scope; the preflight-fold assistant-tail is a real, separate, p1-class gap in the same symptom family, ready to be tracked and fixed separately once you pick the home + approach.

@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

[bot] 🏷 (ai: vllm-qwen/qwen3.8-27b)

Recap of handling for this report (full detail in the message above):

Nothing modified, nothing filed yet — deliberately held at the owner-decision boundary (unverified source + design choice). Proceeding on your word.

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

Labels

Projects

None yet

2 participants