Repository navigation
Description
Description
What happens
Anonymous-lane requests (clients going through the HTTP proxy without the plugin package) run their compress calls inside the proxy's internal loop. Since v0.1.209, if a round consists ONLY of failed compress/decompress calls (#4186's "failed-only mutating round"), the loop ends the whole turn itself with a fabricated finish_reason: "length" (src/loop/core.ts). No client on any wire auto-continues that signal:
- OpenAI chat/responses wires:
lengthis native but no client auto-retries it — pi and dsh stop mid-task (field reports: "压缩几次后自动停止"). - Anthropic wire:
lengthis not even a legal stop reason; Claude Code treats unknown values as end-of-turn (Claude Code stops mid-task when bili ends a turn early: the Anthropic adapter sends stop_reason "length" #5179).
So a single bad compress call (ineligible range, too-short summary, stale refs) kills the user's running task.
Root cause chain
Model calls compress → kernel rejects inside the proxy → v≤0.1.208 re-requested upstream so the model saw the failure and continued (measured: turn finishes normally with stop) → but those ephemeral frames reached the provider once and were absent from the next client request = unsanctioned cache seam (fuzz seed 1592 red, 7/17 seeds) → #4186/#4217 replaced the re-request with an immediate break → the break is cache-clean but emits a finish reason every client interprets as "turn over" → task dies. The receipt explaining WHY was written FOR THE MODEL and delivered TO THE CLIENT instead.
Proposed fix (two independent changes)
- Receipt re-request (core fix, every wire). On a failed-only round, do NOT end the turn. Re-request upstream, with the retry body carrying ONE plain assistant text message whose bytes are exactly what the visibility marker already streamed to the client. The model reads its own prior "utterance" ("❌ compression failed, continue without compressing") and proceeds; the client stores the same text in its transcript and carries it in the next request, so the provider prefix never diverges — ephemeral tool frames stay off the wire, fuzz stays green. Safety valves: 小模型(2B级)compress 循环空转:同参数重复失败烧满 10 轮 + 幻觉 ref + 摘要长度约束不遵守 #156's narrow guard restored (a round where EVERY failed signature was already seen in an earlier round still breaks — pure ping-pong), plus the existing
MAX_LOOP_ROUNDScap. - Anthropic wire validity (re-land of reverted [PR] fix(#5179): change stop_reason from "length" to "max_tokens" sent to Claude Code when all compression attempts fail and | fix(#5179): 压缩全败提前收尾时发给 Claude Code 的 stop_reason "length" 改为 "max_tokens",避免任务中途自动停住 #5202). Map the loop's early
lengthfinish onto Anthropic'smax_tokens. Still needed because theMAX_LOOP_ROUNDSpath keeps emittinglength, and"length"is simply not a legal Anthropic stop reason. ([PR] fix(#5179): change stop_reason from "length" to "max_tokens" sent to Claude Code when all compression attempts fail and | fix(#5179): 压缩全败提前收尾时发给 Claude Code 的 stop_reason "length" 改为 "max_tokens",避免任务中途自动停住 #5202 was merged then reverted by [PR] revert #5202: stop_reason mapping fix — pulling back for now; users reported issues on the OpenAI protocol, need full ro | revert #5202: stop_reason 映射修复先撤回——报告用户在 OpenAI 协议上,还需完整定位后再合 #5211 while openai-wire coverage was missing; with item 1 in place the mapping becomes hygiene, not the main load-bearing fix.)
The plugin lane (dsh plugin header / pi with the bili package) never enters this loop and is unaffected on all wires.
Tests
New deterministic gate tests (wire-level, fake upstream): failed-only round must NOT terminate the turn; retry body must contain the assistant receipt and NO tool frames; stream completes with the model's own natural finish. Loop-limit path on the Anthropic wire still ends max_tokens. Cache-seam fuzz (default seed + sweep) green. Existing #4186-era contract tests adapted to the new contract.
Closes nothing; supersedes the behavioral half of #4217 (frame hygiene kept) and #5179's diagnosis. Related: #156, #1548, #4186, #5202, #5211, #3302.
Activity
ranxianglei commentedon Oct 30, 2026
(ai: vllm-qwen/qwen3.8-27b)
查重: 不重复,但有一个重要发现——PR #5231 已实现本 issue。
按失败签名(failed-only round / finish_reason "length" / recovery receipt / cache-seam fuzz seed 1592 / Anthropic max_tokens mapping)检索 open+closed issue/PR:相关条目只有 #4186/#4217(seam 侧,已闭)与 GitHub 侧 #5179/#5202/#5211(Anthropic stop reason,已 revert),均非同一张票;无其他同机制在途条目。但检出 PR #5231(分支
2026-11-01_failed-compress-receipt-retry)与本 issue 标题一致、正文覆盖 issue 的两部分修法(receipt re-request + Anthropic max_tokens 映射),且已 rebase 到含 #5188 的 master。按「一个 issue 至多一个 PR」纪律,我不开第二个 PR,转为对 #5231 做独立审核:自己通读 diff、核对 CI、跑定向测试,结论按八问给出。若审核发现缺口,直接在 PR 分支补 commit。标签: bug, scope/wiring, p1, pi, dsh, claude-code —— 缺陷类;主修在 src/loop 请求管道(wiring);严重度 p1:任何一次例行的 compress 失败(stale ref / 截断参数)都会静默杀死用户正在执行的任务,且没有任何 wire 上的客户端会自动续跑——「能用但体验毁灭」档;命中客户端按 issue 正文为 pi/dsh(OpenAI wire)与 Claude Code(Anthropic wire)。
审核结论稍后回复。
ranxianglei commentedon Oct 30, 2026
(ai: vllm-qwen/qwen3.8-27b)
审核结论:PR #5231 正是本 issue 的实现,方向正确、机制逐条核实无误;审核中发现一个边缘缺陷,已直接修复并补回归测试(commit b5f2e21)。建议 CI 绿后合并。
八问:
是什么问题:v0.1.209 起,匿名车道里一轮内所有 compress/decompress 调用都失败时(范围不合格、摘要太短、refs 过期等),代理循环自己结束整个回合并伪造
finish_reason: "length"。没有任何客户端会把这个信号当作「继续」——pi/dsh 停在任务中途,Claude Code 直接视为回合结束(Anthropic 线上length根本不是合法 stop reason)。一次例行压缩失败就杀死用户正在跑的任务。什么原因导致的:三代因果链。Bundled cache-seam fuzz deterministically fails under default seed 1592 (widened 12 scenarios): failed compression pairs | 自带 cache-seam fuzz 在默认种子 1592(加宽 12 场景)下确定性失败:失败压缩对凭空消失 + 运行时判官同族 I1 告警 #4186 之前失败后会再请求上游,模型能看到失败并继续,但那些临时帧到达 provider 一次后就从客户端下一次请求中消失 = 缓存接缝(fuzz 默认种子红,owner 扫 17 个种子中 7 个红);Bundled cache-seam fuzz deterministically fails under default seed 1592 (widened 12 scenarios): failed compression pairs | 自带 cache-seam fuzz 在默认种子 1592(加宽 12 场景)下确定性失败:失败压缩对凭空消失 + 运行时判官同族 I1 告警 #4186/[PR] fix: failed-only compress rounds end the turn — ephemeral failure frames never reach upstream (#4186) #4217 为保缓存干净把再请求换成立即 break,但 break 发出的 finish reason 被所有客户端解读为「回合结束」。根本原因:解释失败原因的回执是写给模型的,却交付给了客户端,而循环里没有一条「把发生了什么告诉模型、又不污染线上」的路径。
影响范围与严重程度:p1。所有匿名车道客户端(走 HTTP 代理的 pi/dsh/Claude Code)都受影响;触发条件是任何一次被内核拒绝的压缩调用(不罕见:编辑后 refs 过期、范围太小都会触发)。严重度:运行中的任务静默死亡,用户视角就是「压缩几次后自动停止」,没有任何错误提示。
怎么修复的(PR [PR] fix(loop): a failed compress must not kill the task — feed the model a recovery receipt instead of ending the turn #5231 方案,已逐项核实):failed-only 回合不再结束回合,而是向上游再请求一次,重试体里带一条普通 assistant 文本消息,字节与已经流给客户端的可见性标记完全一致——模型读到自己的前一句「❌ 压缩失败,请不压缩继续」然后继续干活。客户端把同样的文本存进自己的 transcript 并在下次请求带回,provider 前缀永不分叉(缓存干净按归纳成立)。安全阀:恢复 小模型(2B级)compress 循环空转:同参数重复失败烧满 10 轮 + 幻觉 ref + 摘要长度约束不遵守 #156 签名断路器(同一个失败的调用原样重发才 break)+ MAX_LOOP_ROUNDS 上限。另将 Anthropic 线上的早期 length 映射到 max_tokens(重落被回滚的 [PR] fix(#5179): change stop_reason from "length" to "max_tokens" sent to Claude Code when all compression attempts fail and | fix(#5179): 压缩全败提前收尾时发给 Claude Code 的 stop_reason "length" 改为 "max_tokens",避免任务中途自动停住 #5202,MAX_LOOP_ROUNDS 路径仍需要它)。回执与流的字节相等性、ephemeral 帧 splice 路径、threads-continue 排除理由、hideConsumed 交互均在源码级核实;两个新门控测试文件有牙(断言重试体内容,不只是计数)。
是否有回归问题(diff 触碰路径的既有行为逐项过):
是否破坏缓存:无新接缝——超出客户端历史的唯一新增字节是客户端确实看到并存储的 assistant 文本(只增尾巴,不改写中段),归纳保持。混合回合泄漏是既有且被接受的。一点诚实说明:默认种子的 cache-seam fuzz 绿,但我 dump 检查确认默认种子场景下根本没发生过 failed-only 回合(❌ 只出现在系统提示词里),所以这条路径的接缝证明实际由两个新的 wire 级门控测试的重试体断言承担(必须含回执 + 零工具帧),均通过。
是否改了设计方向:没有。仍在循环既有的请求/再请求机制内,无架构转向。
是否引入新配置:没有。
审核中发现并已直接修复的缺陷:当某个 failed-only 回合的所有失败标记都在更早回合被去重过(例如两个等大范围先后都撞「内容太小」这类与具体 refs 无关的失败文案),回执变成空串,代码会把一条零字节 assistant 消息推进重试体——严格线上(Anthropic 拒绝空 text block)是非法帧,其他线上是无意义零字节回合。确定性复现(两条 4 字符消息,先压 m00001..m00001 再压 m00003..m00003,第二次重请求体里观察到空 assistant 项)。修法:没有新标记字节时跳过回执推送——请求体退化为历史 + prose,仍然缓存干净,乒乓由断路器/MAX_LOOP_ROUNDS 限界;回归测试 T6 钉住该边缘(负向控制已验证:去掉修复代码该测试精确失败在这条断言上)。顺手补了最后一个未测的守卫分支(threads-continue 回退路径)测试。commit b5f2e21:本地定向测试 51/51 绿,typecheck 干净。
状态:CI 已在 b5f2e21 上重新触发(最近一次读取:14 条 lane queued,0 failed;config-surface-guard 作者声明在 master 基线同样红,与本 diff 无关,若它单独立红我会按 #4599 流程跟进)。判定:CI 绿后可合并。
标签:bug pi dsh claude-code scope/wiring p1 —— 例行压缩失败静默杀死用户任务,全匿名车道可用级缺陷。
cybercall commentedon Oct 31, 2026
Additional reproduction: Pi plugin-installed subagent falling back because of its tool allowlist
I reproduced this on an isolated synthetic fixture and compared it with the native ACP tool path. Posting here rather than opening a duplicate issue.
Scope clarification: the statement in the issue that the plugin lane is unaffected needs a qualification. Even with the billion-context Pi plugin installed, a
pi-subagentschild whose strict tool allowlist omits ACP tools can enter the proxy-style compression channel. This child then exhibits the failed-only early termination described here. The native ACP path did recover in the comparison.Environment actually tested
0.1.209, build39a0ada5, packdefault.1.4.0.0.84.0.auto/auto-mid, thinkingmedium. The actual model performing the reads and compression steps was GLM-5.3 in both probes. Both had an initial unsuccessful attempt on a different routed model before GLM executed the experiment; this was not a fixed-model, no-routing test.Reproduction setup
Prerequisites: Pi with the billion-context and pi-subagents extensions loaded. Do not disable ambient extension loading for the child. Use a new disposable directory, not an application checkout. Keep the two probes in separate fresh child sessions.
Create two files in that directory. The following generates equivalent entirely synthetic input; the particular numbers and anchor are not important:
Create a temporary project-scoped agent definition at
.pi/agents/worker-acp-fallback-probe.md:auto/auto-midwas the local configured model alias, not a new requirement of billion-context. If that alias is unavailable, select an already configured model that supports tool calls and record which model actually executes the probe. No credentials or provider configuration are needed in the bug report.Launch that agent through the normal pi-subagents entry point with the temporary cwd and fresh context. In the tested setup, the execution options were
context: "fresh"andtimeoutMs: 3600000. The failure occurred long before that timeout.Supply this task:
The malformed argument is intentional: this tests error propagation, not whether the model can invent a valid compression request. Three reads and approximately 9K compressible tokens were used to match the comparison, not to imply that this much input is necessary to trigger the bug.
Actual fallback result
All three
readcalls completed. The fallbackacp_statusalso succeeded. The subsequentcompresswas rejected with:The error includes advice to re-issue the call, but the agent received no subsequent model turn to act on it. There was no second status call and no read of continuation.txt. The saved output ended with the compression error rather than a normal experiment report.
Sanitized plugin log excerpts, in execution order (timestamps, absolute paths, session IDs and tool-call IDs removed):
The persisted Pi session independently shows three successful host
readtool calls, followed by the final assistant message withstopReason: "length"and no host tool calls. The ACP calls were consumed inside the proxy loop, so they were not recorded as ordinary hosttoolCall/toolResultpairs. The plugin logs above establish that they did execute.Context usage was reported around 2%; this was neither context exhaustion nor the child runtime timeout. A runner-level “completed” notification did not mean the requested experiment steps completed.
Native-tool comparison
In the same temporary directory, create a second project agent with the same model/thinking settings and synthetic-only restrictions, but include these additional tools in its strict allowlist:
Run it in a fresh child session. Keep the same three reads, status call, and exact deliberately malformed compress argument. For the post-error step in this native probe, instruct it to:
acp_statusagain.ACPNATIVE-4729.continuation.txt.search_context.Observed native result:
isError: true.acp_status.isError: false, creating one block and saving approximately 9K tokens.readreturnedCONTINUATION_PROBE_OK; anchor lookup succeeded.stopReason: "stop", not"length".Sanitized native plugin excerpts:
Expected behavior / relevance to the proposed fix
A recoverable parameter rejection should not itself fabricate a length termination and end the user's task. The model should get an accurate recovery receipt and a bounded opportunity to correct the call or continue without compression, while preserving the cache/history constraints described in this issue.
Please consider covering Pi + the installed native plugin + a subagent strict allowlist that omits ACP tools in the regression scope, not only clients connecting to a standalone HTTP proxy. This is the additional entry path demonstrated here.
Evidence limits and privacy
fix(loop): a failed compress feeds the model a recovery receipt and r…
fix(loop): a deduped failed-only round must not push an empty receipt…
ranxianglei commentedon Oct 31, 2026
(ai: vllm-qwen/qwen3.8-27b)
Thanks for the detailed repro and the native-ACP comparison — I verified your mechanism in code, and the coverage gap you pointed out is now pinned.
Your mechanism confirmed (code-level). A child whose strict allowlist omits every ACP tool hits the #2268 channel fallback (
src/server/handle.ts→piSubagentChannelFallback): session identity stays plugin-bound, but compression runs through the proxy-style channel — i.e. the exact samerunCompressLooppath this issue is about. So your early termination is the same defect through a different entry. The issue's "plugin lane unaffected" holds for pure plugin lanes (children whose tool list includes compress/decompress); the #2268 fallback channel is proxy-style and was affected.Important context first: PR #5231 — the recovery-receipt fix — has since been merged into master. Your build (0.1.209 / 39a0ada) predates it. On the fixed code a failed-only compress round re-requests with the receipt instead of ending the turn with a fabricated
length, so upgrading resolves the early termination itself; it ships with the next release.The missing pin you asked for. #5231's gate tests drive the loop directly and never exercise the #2268 demotion decision; the real-pi e2e lane covers only the fallback channel's success path. Follow-up PR #5310 (test-only supplement to #5231) adds a server-level test driving the real pipeline with your exact failure class —
compresscalled with a string-valuedcontent→ content-not-array rejection — asserting: the turn survives (exactly one re-request), the retry body carries the streamed ❌ [ACP] marker bytes as an assistant receipt with no ephemeral tool frames, and the client ends on the model's own natural finish. Negative control: reverse-applying #5231's loop changes onto the same tree makes the new test fail with exactly your symptom (turn breaks after one upstream request); restored, it's green. File suite 16/16 locally, typecheck clean, CI running on the pushed head.八问(增量更新):
结论:主修复已随 #5231 合入 master,#5310 补上最后一块覆盖缺口;建议升级后验证你的复现场景。
fix(#5604): cap the failed-compress recovery receipt so the re-reques…
ranxianglei commentedon Nov 12, 2026
该issue已按根因归类纳入 [wire-fidelity] 根因汇总 #6338,由汇总单统一跟踪处理;本单不再单独跟进,故关闭。如认为归类有误或问题仍未解决,请直接回复,会重新打开。
ranxianglei commentedon Nov 12, 2026
状态更正(自动化脚本误操作): 本单此前被「纳入根因聚合」流程误关闭, 现重新打开。机器人工作已停, 本单由根因聚合线程统一跟踪, 根因修复落地后请手动关闭或在本单回复「close」。
[automated: re-open after bot-stop-only policy]
ranxianglei commentedon Nov 12, 2026
更正说明:本单此前被归类聚合流程误关闭。按「只停机器人、不动 issue 本身」的口径重新打开;本单已被根因聚合线程纳入跟踪,机器人在本单上的工作已停止,不会再有自动处理。归类或纳入有误请直接回复指出。
ranxianglei commentedon Nov 12, 2026
重新打开:此前被归类聚合脚本误关。按 owner 口径,纳入根因聚合的 issue 只停机器人、不关闭,由根因聚合线程统一跟踪,根因修复落地后统一关闭。
(reopened: absorbed issues must not be closed, only bot work stopped — owner policy 2026-11-12)
ranxianglei commentedon Nov 12, 2026
(更正)此前按"清理"口径误关闭;按 owner 口径纠正:被根因聚合纳入的 issue 只停机器人、不关闭。本 issue 重新打开,机器人工作已停。