Skip to content

Fold splits signed thinking from its tool_use → permanent 400 on Anthropic wire (0.1.207) #4885

Open

Description

@javagaba

Setup: pi + billion-context 0.1.207, provider github-copilot (Anthropic messages wire), claude-opus with extended thinking (interleaved, signed).

What happened:

  1. The model called compress with a range that sat inside the current tool loop (right after the protected last-user message).
  2. The fold removed an assistant tool_use and the following tool_use/tool_result pairs, but kept that assistant message's signed thinking block (logged as "exempt ... reasoning=24").
  3. The orphaned thinking block was merged into the next surviving assistant message, producing [thinking A, thinking B, tool_use].
  4. Every request since fails with:
    400 messages.15.content.1: thinking or redacted_thinking blocks in the latest assistant message cannot be modified.
  5. Retries and a host compaction don't recover it: the folded state persists, and only the index changes (15 → 5).

Expected: a fold never separates a signed thinking block from its tool_use sibling. Either fold the thinking along with its tool_use, or refuse ranges that would split them (the docs' #1658 note says signed thinking must round-trip with its tool_use).

Workaround: the host's own transcript is intact; replaying the session without the proxy works.

Activity

  1. ranxianglei commented on Oct 26, 2026

    @ranxianglei
    Owner

    🤖 Powered by ework · qwen3.8-27b

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

    查重: 不重复——同家族、不同机制。最近邻逐一排除:#1658(已关,#651 丢弃入站 thinking 的开关问题)、#1960(已关,in-turn 重请求改写 latest assistant)、#4679(open,bili 自身第 ≥2 轮重请求截断)、#4292/#4457(fold 孤儿化 mid-conversation SYSTEM,靠位置兜底修复)——都不是「fold 把 signed thinking 与其 tool_use 拆开」。
    标签: bug, pi, scope/kernel, p1(触发后会话完全不可用,按 owner 修订属 p1 第一类)。

    已复现(当前 master;你的 0.1.207 行为相同)

    版本核对:git log v0.1.207..origin/master 对 kernel/src/{unit-projection,turn-integrity,reasoning-pairs,tool-pairs,boundaries}.ts 输出为空——折叠判定代码在 0.1.207 与 master 逐文件相同,master 上的复现即你构建的行为。

    最小复现:一条 assistant 消息形如 [thinking(签名)][text][tool_use],模型 compress 范围从 tool_use 起头、跨到后续 pair → 折叠成功、零报错零警告,签名 thinking 被留在外面;会话再走一轮后重建出站 wire,出现单条 assistant 消息 [thinking A | text | thinking C | tool_use]——与你报的 [thinking A, thinking B, tool_use] 同形。provider 对 latest assistant message 做字节级签名校验 → 400;折叠态持久在账本里,重试/宿主 compaction 只会让错误索引滑动(你的 15→5)。完整可跑脚本与逐层证据见授权 issue #4886。

    根因(kernel 三层)

    1. 生产者 kernel/src/reasoning-pairs.ts:79-98:backward 边界调整从范围起点 i-1 起只回扫「连续 reasoning」消息。起点是 tool-call、而它前面隔着同一条消息的 prose(plain text 块,或映射成 text 的 redacted_thinking)时,回扫第一步撞上 text 就停,thinking 拉不进范围。forward 方向是 burst-aware 的([bug] compress 保护区过滤在完整性调整之后挖洞,导致 assistant tool-call 消息缺 reasoning_content(DeepSeek 思考模式 400) #684),backward 不是——完整性门的分组(computeTurnGroups)也是 burst-aware 的,只有 backward adjustment 没跟上这个单元定义。
    2. 放行者 kernel/src/turn-integrity.ts:122-136([bug] compress 保护区过滤在完整性调整之后挖洞,导致 assistant tool-call 消息缺 reasoning_content(DeepSeek 思考模式 400) #684 INV1):只拦「folded 侧有 reasoning、kept 侧有 call」的方向;你这个方向(reasoning+text kept、call+result folded)按 Compression breaks Responses assistant history ordering, causing repeated invalid_assistant_history 400s #564 注释显式放行——对 DeepSeek 类无签名线合法,对 Anthropic 签名线不成立。
    3. 兜底失效 kernel/src/prune.ts:637-661 stripOrphanedReasoning:只删「后面没有 assistant 内容跟随」的孤儿 reasoning;你这个孤儿后面还跟着自己的 prose,检测不到。且删除签名块本身就是改写历史=另一个 400,此层对 signed 块天然无效。

    修复方向(等 kernel 授权,批准前不动代码)

    全部落在 kernel/**,按 Kernel Boundary 需你先行批准:授权申请已开 → #4886(p1,含可复现脚本、三层根因、行为变更披露、A/B/C 选项)。推荐选项 A(主修+加固一次闭环)。

    临时绕行

    除你说的去代理重放外:选 compress 范围时别从「前面带 prose 的 tool_use」起头——从该 turn 的 thinking 块起(或更早),让整 turn 一起进范围。

    八问

    1. 是什么问题:一次合法的 compress 把签名 thinking 和它的 tool_use 拆开后,Anthropic 线永久 400,会话在该 proxy 下不可用。
    2. 什么原因导致的:折叠边界调整对「thinking 与 tool_use 之间夹着 prose」的形状失配(回扫不跨 text);完整性门按无签名线的假设放行了这个方向;重建合并又把孤儿 thinking 拼进下一条 assistant 消息。
    3. 影响范围与严重程度:pi(及任何走 Anthropic 签名 thinking 的车道)+ 模型恰好选中「prose 后 tool_use 起头」的范围时触发;触发后会话完全不能用(每次请求 400),只能去代理重放或弃会话——p1。
    4. 怎么修复的:burst-aware 边界调整 + 签名感知的反向撤回(见上);等 kernel authorization request: folds can split signed thinking from its tool_use when prose sits between them (fixes #4885 permanent 400) #4886 批准后实施。
    5. 是否有回归问题:未动代码,暂无;方案本身的行为变更已在 kernel authorization request: folds can split signed thinking from its tool_use when prose sits between them (fixes #4885 permanent 400) #4886 披露段报备(更多范围被自动加宽、signed 线 bN/carve 新增整 turn 撤回,unsigned 线不变)。
    6. 是否破坏缓存:不涉及(不改缓存键与前缀稳定逻辑;加宽折叠只改变哪些消息进摘要)。
    7. 是否改了设计方向:不涉及(沿用 KDD refactor: reapOrphanBlocks should live in acp-kernel, not the proxy #12 单一投影与 [bug] compress 保护区过滤在完整性调整之后挖洞,导致 assistant tool-call 消息缺 reasoning_content(DeepSeek 思考模式 400) #684 原子 turn 教义,只是把两处实现统一到同一单元定义)。
    8. 是否引入了新的配置:不涉及。

    收工判定:机制已钉死并复现,修复方案已备好并附完整披露,卡在 kernel 授权——等你在 #4886 拍板(A/B/C)。

  2. ranxianglei commented on Oct 27, 2026

    @ranxianglei
    Owner

    开工

  3. worldofgeese commented on Nov 1, 2026

    @worldofgeese

    Same failure here on 0.1.210: pi 1.4.0 with pi-native, Claude Opus 5.5 with thinking, through an Anthropic-compatible gateway:

    400 {"error":{"type":"ValidationException","message":"messages.11.content.2: `thinking` or `redacted_thinking` blocks in the latest assistant message cannot be modified. These blocks must remain as they were in the original response."}}
    

    It started after the third compress call of the session. The earlier folds were m00001–m00012 and m00013–m00025. All 10 requests after it failed, and the last 10 of 16 prompts never ran.

    Later I ran one fold session each on 0.1.211 and on the PR build 0.0.7344-pr.4898.7544462 (1 and 3 compress calls). Both finished without the error. That's one run each, so it says little about #4898, because the trigger depends on where a fold range starts.

  4. ranxianglei commented on Nov 1, 2026

    @ranxianglei
    Owner

    🤖 Powered by ework · qwen3.8-27b

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

    Thanks for the data point — your 0.1.210 failure is expected, not a miss of the fix:

    • 0.1.210 and 0.1.211 do not contain the fix. PR [PR] kernel: keep signed thinking with its tool_use across folds (#4885) #4898 is still open/unmerged, so no released tag has it yet. Your 0.1.210 session failing after the third compress call corroborates the bug exactly as reported.
    • Your passing runs on 0.1.211 and the PR build are consistent with the fix working but, as you say, one run each proves little — the trigger depends on where a fold range starts relative to a signed thinking block. That's why the PR carries a regression suite that pins the reported shape deterministically instead of relying on live-session luck.

    Since your report arrived, I also re-validated today because master moved far past the PR's base:

    1. Reproduced the orphan shape on current master code without the fix → still broken (the bug was never fixed elsewhere).
    2. Re-synced the PR branch onto the latest master (it had fallen ~247 commits behind; one conflict with a recent kernel performance refactor touching the same integrity gate, resolved), kernel version now 0.0.177.
    3. Re-ran the targeted suites on the synced head: new Fold splits signed thinking from its tool_use → permanent 400 on Anthropic wire (0.1.207) #4885 regression suite + reasoning-pairs + turn-integrity + recommend-integrity + tool-pair + recent-pair + wire-reasoning-echo + shared-screening (69+3 pass), bili-level thinking/replay/reconcile/orphan suites (26 pass), typecheck clean. Remote CI will do the full-suite verdict on the pushed head.

    So: nothing more needed from you right now. Once PR #4898 merges and ships in the next release, your scenario should stop 400-ing. Until then the workaround stands: replay the session from the host transcript (the proxy's folded state persists across restarts, so a fresh replay is what clears it).

    If the error ever recurs on a build that contains #4898, please share the session log — that would mean a second split path exists beyond the one pinned by the regression suite, and I'd want to see it.

  5. added 2 commits that reference this issue on Nov 7, 2026
  6. Yang-Code01 commented on Nov 11, 2026

    @Yang-Code01

    Another data point for this issue on 0.1.213 (npm latest as of 2026-11-11), host pi 1.5.0 (pi-native plugin), provider github-copilot (Anthropic messages wire, api.business.githubcopilot.com), model claude-opus-5.5 with interleaved signed thinking. Same mechanism as #5891, reached through a slightly different path.

    Observed (4 sessions, 2026-11-09 … 11-11): every thinking … cannot be modified 400 came right after a compress call whose range reached into the recent protected zone; every retry failed with the same index (messages.3.content.7, other sessions .5.content.5, .5.content.17, .1.content.42) until the model was switched (pi drops cross-model thinking blocks, so the rebuilt message then goes through). Folds that stayed outside the recent zone never triggered it.

    Log signature that distinguishes the bad fold: the compress result says Excluded N protected message(s) … from compression range without Range truncated at that boundary. In pi-native.js the range is truncated at the first protected id only when firstProtectedIdx > 0; when the range starts on a protected id (firstProtectedIdx === 0) it falls through to filteredIds.filter(id => !protectedSet.has(id)), so protected items are skipped one by one and everything between them is folded. Because bash/read tool pairs are in NEVER_PRESERVE_RECENT_TOOLS, each recent turn [thinking(signed), tool_use(bash)] + tool_result loses its tool_use/tool_result but keeps the thinking → the surviving thinking blocks of several turns are adjacent assistant cores → coreToAnthropic merges them into one assistant message with 7+ thinking blocks → 400.

    Session archive confirms the sieve shape (block b3, m00316–m00356): visible/FOLDED alternate exactly as reasoning-visible / bash-call-folded / bash-result-folded.

    2026-11-11T22:16:49 [acp-proxy: [Compressed m00001–m00111, m00112–m00315, m00316–m00356 → 3 block(s), ~55017 tokens saved.
      … Excluded 14 protected message(s) m00284, … m00315 … Range truncated at that boundary — 18 later message(s) … stay visible too (#3953).;
      Excluded 16 protected message(s) m00316, m00317, m00318, m00320, m00322, m00325, m00328, m00331, m00334, m00337, m00341, m00346, m00349, m00350, m00352, m00354 from compression range (recent/last-user zone) … Last-user anchor: m00357 …]
    2026-11-11T22:16:50 processTurn: 63 msgs … I1 violation … exempt carrier=0 reasoning=19 …
    2026-11-11T22:16:52 ← upstream 400 … messages.3.content.7: `thinking` or `redacted_thinking` blocks in the latest assistant message cannot be modified.
    

    Config-only workaround that removes the asymmetry (reasoning protected / its tool pair not): make the recent zone contiguous so the range is always truncated at the first protected item instead of sieved —

    { "compress": { "neverPreserveRecentTools": [] } }

    (CONFIGURATION.md documents [] as valid = maximum protection.) Cost: fresh bash/read results age out of the recent zone by position instead of being foldable immediately. By code reading the protected set is then a pure suffix of the visible cores (+ last-user anchor), so a reasoning core can no longer be protected while its tool_use sibling is not. I have not yet caught a live sieve-shaped fold on Opus 5.5 with this setting to confirm empirically.

    Reference: Anthropic's preserved-thinking docs state the kept thinking blocks "must be an unbroken run of the original sequence" and list "Summarizes or drops old turns on the client and keeps recent turns with their thinking" as a failure mode — so for claude-*-5.x the fold has to drop a turn's signed thinking together with its tool_use (or refuse the range), never keep the thinking alone.

  7. added a commit that references this issue on Nov 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions