Skip to content

fix(loop+wire): preserve redacted_thinking blocks on the Anthropic wire (#1960) - #1980

Merged
ranxianglei merged 3 commits into
masterfrom
2026-10-03_1960-redacted-thinking
Oct 3, 2026
Merged

ranxianglei merged 3 commits into
masterfrom
2026-10-03_1960-redacted-thinking

Conversation

@ranxianglei

Copy link
Copy Markdown
Owner

fix(loop+wire): preserve redacted_thinking blocks on the Anthropic wire (#1960)

Fixes #1960

What was wrong

With Claude extended thinking, an in-turn compress left the session permanently
broken: the compress re-request 400'd with

thinking or redacted_thinking blocks in the latest assistant message cannot be modified. These blocks must remain as they were in the original response.

and every later request in the session returned the same 400.

Root cause (verified by controlled repro driving the real runCompressLoop):
a redacted_thinking block arrives whole in content_block_start — no
delta ever carries its opaque payload — and nothing in either pipeline recorded
it:

  1. Re-request path (this repo): the loop rebuilt the latest assistant
    message from recorded parts ([thinking, tool_use]) without the redacted
    block that sat between them → Anthropic rejected the modified message.
  2. Steady path (acp-kernel): anthropicToCore had no case for
    redacted_thinking, so every client request echoing the round back silently
    dropped the block → the same modified shape went out forever. This is why
    the session could never recover.

The reporter's step-1 hypothesis (hideConsumedCompressCalls dropping the
current call) was checked and disproven: the executed compress call is kept
(its id is stamped onto the new active block, plus the newest-orphaned tail
scan), and the incident log has no hideConsumed hid line.

Changes

Loop (src/loop/core.ts, src/loop/adapter-anthropic.ts)

  • The adapter captures the opaque redacted payload at content_block_start
    and emits a dedicated redacted_thinking event at block stop (instead of
    sealing an empty reasoning segment).
  • One round's assistant parts (reasoning / text / redacted) are accumulated in
    stream order and emitted position-exact on the re-request; redacted
    parts ride the rawAnthropicBlock sidecar, which coreToAnthropic re-emits
    verbatim (same mechanism images already use).
  • Recovery narrowing: a 400 matching cannot be modified skips the degraded
    strip-retry — that retry can only produce a second guaranteed 400 plus a
    swapped error — and surfaces the original upstream error immediately.

Steady path (acp-kernel pin 0.0.99 → 0.0.101)

Tests

Behavior changes (flagged per policy)

  1. Steady requests: previously SILENTLY DROPPED redacted_thinking blocks
    on every request (any session containing one was doomed to repeated 400s);
    now they are preserved byte-exact while their messages stay unmodified or
    are pruned with range. Old → new: dropped → preserved verbatim.
  2. Compress re-request: the rebuilt latest assistant message now carries
    redacted blocks in stream position (previously lost — the defect). Plain
    signed-thinking shapes are byte-identical to before (regression-tested).
  3. Degraded strip-retry: no longer fires for 400s matching
    cannot be modified (it guaranteed a second failure); the client sees the
    original upstream error text. Other 4xx classes retry exactly as before.

Config surface

None — no config field added, renamed, or re-semantics'd. No env vars, no CLI
flags.

ework-agent added 2 commits October 3, 2026 16:22
… re-request (#1960)

Anthropic rejects a modified latest assistant message with 400 "thinking or
redacted_thinking blocks ... cannot be modified". The in-turn compress
re-request rebuilt that message from loop records, but a redacted_thinking
block arrives whole in content_block_start (no delta carries its payload), so
nothing was recorded and the rebuild silently dropped it — every request in
the session then 400'd.

- adapter-anthropic: capture the opaque redacted_thinking payload at
  content_block_start and emit a dedicated redacted_thinking event at block
  stop instead of sealing an empty reasoning segment
- core: accumulate one round's assistant parts (reasoning / text / redacted)
  in STREAM order and emit them position-exact on re-request; redacted parts
  ride the rawAnthropicBlock sidecar, which coreToAnthropic re-emits verbatim
- recovery: a 400 matching "cannot be modified" skips the degraded strip
  retry (it can only produce a second guaranteed 400) and surfaces the
  original upstream error

Tests: tests/issue1960-loop-redacted.test.ts (byte-exact redacted replay,
plain signed-thinking regression guard, no-retry recovery). Preflight:
typecheck clean, targeted loop/thinking suites 185/185 green, build OK.
…eady path (#1960)

The steady request path (client echoes the round back) silently dropped
redacted_thinking blocks because anthropicToCore had no case for them, so
every later request rebuilt the latest assistant message without the block
and Anthropic answered 400 "cannot be modified" forever — the half of #1960
that kept the session stuck after the compress turn.

- acp-kernel pin 0.0.99 → 0.0.101 (exact), which carries kernel PR #500
  (redacted_thinking sidecar round-trip). Also rides along from v0.0.100+:
  #483 ref capacity widened to 9,999,999, #484 case-insensitive protected
  tool matching, #478 nudge cadence re-anchor, #493 language-preservation
  behind an opt-in flag (default off), and #499 prune/recommend shared
  per-turn index (hundreds of ms saved on ordinary requests in large
  sessions — release-note material).
- tests/issue1960-steady-redacted.test.ts: client-echoed
  [thinking(sig), redacted(data), tool_use] must survive prepare→outbound
  byte-identical in order. Verified failing on 0.0.99, passing on 0.0.101.

Preflight: typecheck clean; targeted loop/thinking/reasoning suites 186/186
green incl. both new #1960 files; build OK.
@ranxianglei
ranxianglei force-pushed the 2026-10-03_1960-redacted-thinking branch from 8db3169 to 29f60d3 Compare October 3, 2026 08:24
@github-actions

github-actions Bot commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

📦 Built Package Artifact

Branch: 2026-10-03_1960-redacted-thinking (00cf054)

Option A — Install this exact build (recommended)

npm install -g billion-context@0.0.2001-pr.1980.e5d91d9

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

[bot] 🏷 收到,开始对 PR #1980 做独立 review(不重做,按 [PR] 流程核对代码/CI/回归)。已拉取分支 2026-10-03_1960-redacted-thinking(head 29f60d3c,两个线性 commit),正在本地装 acp-kernel 0.0.101 跑定向测试 + 核对 CI 状态,结论稍后按结构化七问给出。

@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

[bot] 🏷 Review 完成 — 结论:可合并(人工 merge;pin bump 属跨仓依赖,按 §7.4 必须留人)

独立复核(未重做):拉分支 2026-10-03_1960-redacted-thinking(head 29f60d3c,两个线性 commit),本地装 acp-kernel 0.0.101 跑定向验证 + CI 核对。

验证证据

  • 定向批次(24 个 loop/thinking/reasoning 套件,含两个新 [acp-loop] In-turn compress re-request edits the latest assistant message → Anthropic 400 "thinking blocks … cannot be modified" (session stays broken) #1960 文件):235/235 绿;typecheck 干净。
  • 测试有牙(我复验的):把 PR 的新 loop 测试文件放到 master 旧 loop 源码上跑(kernel 仍装 0.0.101 以隔离变量)——恰好钉住 loop 侧修复的两个 case(redacted 重放、no-retry recovery)失败,回归守卫 case 通过。非恒真断言。
  • kernel codec 直接读了 0.0.101 dist 源码核实:入站新 redacted_thinking case 与 image case 同形(text:"[redacted_thinking]" + rawAnthropicBlock);出站对带 sidecar 的 text 消息只推原块、跳过占位文本——占位符不会漏上线变成多余 text block。
  • GitHub CI 15/15 全绿(ubuntu/windows × node22/24、hermetic registry e2e 双 OS、real pi/codex/opencode/dsh lane、image billing e2e、build-artifact、version-guard)。PR mergeable,仅 behind v0.1.181 release merge(版本行,非阻塞)。

结构化七问

  1. 是什么问题:Claude extended thinking 会话在一次 in-turn compress 后永久卡死——重发请求重建的最新 assistant 消息丢了其中的 redacted_thinking 块,Anthropic 400 "thinking blocks cannot be modified",之后每个请求重发同一畸形形状。
  2. 什么原因导致的:redacted 块整体到达于 content_block_start,没有任何 delta 携带其不透明 payload,两条管线都无人记录——① loop 重建只回放已记录 parts(thinking+tool_use);② kernel anthropicToCore 无 redacted case,客户端(pi)忠实回显的历史也被每个稳态请求丢掉。一个机制、两个独立落点。用户 step-1 猜测(hideConsumedCompressCalls)在 [acp-loop] In-turn compress re-request edits the latest assistant message → Anthropic 400 "thinking blocks … cannot be modified" (session stays broken) #1960 triage 中已用证据排除。
  3. 怎么修复的:流时捕获(adapter 在 block start 抓 payload)+ 一轮 parts 按流序累积、重放逐位还原(core,sidecar 携带、coreToAnthropic 原样吐出,复用 image 既有机制);稳态由 kernel fix: forward-once-then-learn for image-dominated payloads (#496) #500 补 codec case(pin 0.0.101);恢复收窄——命中 "cannot be modified" 的 400 跳过注定失败的 strip-retry,直接透传原始上游错误。
  4. 是否有回归问题:逐项过完触碰路径的既有行为,无破坏——纯 signed-thinking 尾部形状字节一致(新测试钉住);unsigned/空 thinking 段仍按 偶尔模型在acp工具处停止执行 #221 丢弃;其他 4xx 类降级重试逻辑一字未动;客户端可见转发帧不变(redacted 走 thinking 同款 meta 路径);tool-call arguments 逐字([bug] 插件透传会剥掉工具调用参数里的 render tag(arguments 流挂了 tag-echo 过滤器) #1039 不变);Google/OpenAI lane 不受影响(新事件 kind 只有 Anthropic adapter 产出)。有意变更两处均已披露:稳态请求 redacted 块 dropped→preserved verbatim;"cannot be modified" 类不再 strip-retry(原错误直出,替代二次必败 400+错误被换)。
  5. 是否破坏缓存:不涉及——前缀缓存、重试策略、增量游标、去重键均未触碰。
  6. 是否改了设计方向:无转向——复用既有 rawAnthropicBlock sidecar 覆盖第三种块类型;loop 重建从固定布局改为流序保真(更忠实上游,常见形状输出不变)。
  7. 是否引入新配置:无(config surface 零变更,PR 内已含该节)。

两点备注(不阻塞)

收工判定:代码、测试、CI、回归审查全部通过,无拦点。请人工合并 #1980(我不会代合);合并即闭环 #1960。

… filters (#1960 filter variant)

Anthropic thinking blocks and Gemini thoughtSignature parts are verified
byte-for-byte when the latest assistant message is replayed, and the bytes the
proxy forwards become the client's persisted history. The #1882 prose filters
on the thinking/thought channels desynchronized text from signature on BOTH
sides: the loop's re-request rebuild carried cleaned text, and the client
echoed the cleaned bytes forever — the session 400s on every request after the
first filtered thinking delta. Remove the thinking/thought filter instances;
the TEXT channel keeps the #206/#1881 strips. Cosmetic tag/marker echoes inside
a thinking pane are accepted (#1960 verdict).

- adapter-anthropic: thinking_delta forwards + accumulates verbatim (raw chunk
  passthrough, no flush tail); redacted capture from #1980 unchanged
- adapter-google: thought parts forward verbatim with their thoughtSignature;
  no editedParts rewrite for thought
- tests: issue1960-thinking-verbatim (loop-level byte equality on re-request
  AND client-visible stream, both wires); issue1881 pins flipped for the two
  SIGNED channels (openai reasoning_content / responses summaries keep the strip)
- AGENTS.md: Key Design Decision #10 — thinking-family payloads are
  signature-verified, never rewrite them; future changes need loop-level
  byte-equality tests on both signed wires
@ranxianglei
ranxianglei merged commit ed3f1f7 into master Oct 3, 2026
15 checks passed
@ranxianglei
ranxianglei deleted the 2026-10-03_1960-redacted-thinking branch October 15, 2026 05:32
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