Skip to content

[PR] fix(plugin-fork): lift the thinking hard gate — thinking sessions can fork (#4266) - #4900

Open
ranxianglei wants to merge 10 commits into
masterfrom
2026-10-27_fork-thinking-gate
Open

ranxianglei wants to merge 10 commits into
masterfrom
2026-10-27_fork-thinking-gate

Conversation

@ranxianglei

@ranxianglei ranxianglei commented Oct 27, 2026 •

Copy link
Copy Markdown
Owner

(ai: zhipuai-lb/glm-5.3)

一句话:开了扩展思考(thinking)的会话从此前「永远无法 fork」恢复为「正常 fork」——#4266 家族的「thinking 硬门」机制。

根因(两道闸,都拒思考内容)

  1. comparableHistory(src/plugin.ts)的块类型允许列表没有 thinking/redacted_thinking → 带 thinking 块的重发历史被判「不可文本比较」→ publicSnapshotTextComparable=false;
  2. forkSnapshot(:1160)对快照携带 thinkingTokens>0 的会话直接 throw。

两道闸一叠加:claude 扩展思考会话每次 fork/snapshot/status-poll 全部 409 SNAPSHOT_UNAVAILABLE,acp_delegate 子代理对这类父会话永久不可用。而 thinking 内容明明是可比较的:anthropicToCore 把 thinking 块投影成具名 reasoning 消息(kernel/src/wire/anthropic.ts:200),thinkingTokens 是数值计量字段且 forkMessageIdentityHash 已把它纳入身份摘要(src/plugin.ts:1071)。

改动

  • comparableHistory:允许列表加入 thinking、redacted_thinking,且两者直接判可比较(思考文本在 thinking 字段,不在 text);
  • forkSnapshot:删掉 thinkingTokens>0 拒绝子句,只保留真不可比较内容的拒绝(图片/音频、publicSnapshotTextComparable===false)。

不变式:fork 协议 v2 的 revision token = {sessionId, seq, blockSet},与 thinking 无关;深比较 forkMessageIdentityHash 本就含 thinkingTokens,逐字节前缀比对语义不变。

测试(tests/issue4266-fork-thinking-gate.test.ts,master 上 3/3 RED)

  1. wire thinking 块 → snapshot 200 → fork 201 → 孙会话 fork 201 → 子会话续聊往返 200;
  2. 直接打桩 thinkingTokens=9 的快照单元 → fork 201(旧:409);
  3. 图片块仍然 409 SNAPSHOT_UNAVAILABLE(负向钉,双向 Test Blind Spot Retrospective: Reinforcing Negative Assertion Discipline, Tightening Fake Upstream Strictness, Thin Inst | 测试盲区复盘:补反向断言纪律、假上游严格化、薄安装冒烟、形态矩阵与生命周期覆盖 #3617)。

本地:plugin-fork 41 + zstd-fork 13 + 本文件 3 全绿;typecheck 0。

Fixes #4266 (mechanism 1 of 3; mechanisms 2/3 已分别由 #2960 fork_base 检查点与 #4733/#4832 覆盖)

行为变更(评审跟进 71e1b57,必须披露)

外部评审发现:放开硬门后,fork 身份哈希仍 digest thinkingTokens——该字段是宿主投影到服务端视图的计量块(projectThinkingMass),客户端回放的字节里从不存在,于是每个被盖戳的格子在子会话首个请求上必然误判 hash-drift-under-same-id(advisory 采纳、首个请求失去 verified-prefix 快路径)。已修(fork 身份字段集纠正,同一 #4266 机制家族):

# 旧行为 新行为 原因
1 fork 身份(forkMessageIdentityHash + 客户端镜像 forkIdentityHashOf)digest thinkingTokens 移除该字段;改 digest thinkingSignature / googleThoughtSignature 计量块单边存在(只在 prepared 视图),digest 它=系统性误报漂移;签名是两端都有的真实内容载体,轮换签名从此可读为漂移(advisory 采纳,一个未折叠请求,绝不损坏)
2 被盖戳计量块的父会话:子会话首个请求必走 advisory 采纳(前缀未验证) 干净通过前缀门 同上
3 parentRevision / orderHash 值(本 PR 基线) 跨部署变化(身份哈希值变了) 瞬态:fork 时客户端取最新 revision;在途 fork 拿 stale-revision 错误后重取即可
4 comparableHistory:无载荷 {type:"thinking"} 块视为可比较 视为不可比较 → 含此类块的会话拒快照/fork(409 SNAPSHOT_UNAVAILABLE) fail-closed:无可比内容面时不假装可比
5 sameSnapshotView 按值比较 thinkingTokens 只比较存在性(有无该字段) 值每轮重算(逐 turn 漂移)会令 CoW 每轮整树替换快照视图;完全不比又会把新盖戳静默丢掉。存在性翻转仅发生在计量归因开始/结束时

回归钉:tests/issue4266-fork-thinking-gate.test.ts(shape-B 盖戳格通过子首请求门 + 签名轮换读为漂移,双向)、tests/plugin-fork.test.ts(#3743 taxonomy:纯计量漂移不再误判、真身份漂移仍判)、tests/dsh-fork-adopt.test.ts + tests/pi-omp-fork-adopt.test.ts(客户端镜像字节级 parity)。

… fork (#4266)

Extended-thinking parents (claude thinking blocks on the wire, or
host-projected thinkingTokens metering) got 409 SNAPSHOT_UNAVAILABLE on
every fork/snapshot/status call, so they could never fork at all. Both
carriers are comparable-by-view: anthropicToCore projects a thinking
block to a named reasoning message, and thinkingTokens is a numeric
metering field forkMessageIdentityHash already digests.

- comparableHistory: allow-list the thinking family (thinking,
  redacted_thinking) and treat them as comparable (text lives in the
  'thinking' field, not 'text').
- forkSnapshot: drop the thinkingTokens refusal; only genuinely
  uncomparable content (images/audio, publicSnapshotTextComparable
  === false) still refuses.

Tests: tests/issue4266-fork-thinking-gate.test.ts (RED on master 3/3):
wire thinking block -> snapshot 200 / fork 201 / grandchild fork 201 /
child follow-up round-trip 200; stamped thinkingTokens cells fork;
image blocks still refuse SNAPSHOT_UNAVAILABLE.
@github-actions

github-actions Bot commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

📦 Built Package Artifact

Branch: 2026-10-27_fork-thinking-gate (50d9447)

Latest build — install this exact version

npm install -g [email protected]

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
50d9447 [email protected] build
16c3a0e [email protected] build
8938315 [email protected] build
8adcacd [email protected] build
bcb823c [email protected] build
688d0ae [email protected] build
71e1b57 [email protected] build
45bafb3 [email protected] build
29f0066 [email protected] build
5c70c74 [email protected] build

Download artifact

  1. Download the artifact from the Actions run
  2. Extract the tarball and install:
tar xzf billion-context-pr4900.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] 🏷 (ai: vllm-qwen/qwen3.8-27b)

查重: 不重复。同 #4266 家族的兄弟 PR 只有 #4345(fork-adopt 能力探测门)与 #3024(409 后 adoption 不再永久放弃),机制不同——本 PR 修的是 snapshot 侧「thinking 硬门」(comparableHistory 允许列表 + forkSnapshot thinkingTokens 拒绝子句),与二者无重叠;#4754/#4703/#3039 等 fork PR 各管各的失败域。

标签: bug / claude / pi / scope/wiring / p1(思考会话 fork 继承被永久放弃 = owner 2026-10-19 修订的 p1 类①)+ pr。

开始审核:拉分支核对 diff、CI 状态与既有行为面,结论按八问给。

@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

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

Merge-guard sync done: merged origin/master (e4cc685) into 2026-10-27_fork-thinking-gate with no conflicts, pushed as 29f0066 — PR is now in sync with master (mergeable_state went from behind to blocked, i.e. only waiting on the queued CI checks on the new head).

@Zhishulo

Zhishulo commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

(ai: deepseek/deepseek-v4.1-flash-1017)

复核(head 29f00664f):门本身没问题,但 thinkingTokens 进身份哈希会让「继承的前缀」每次都被判漂移

先声明取证方式:本机代理刚恢复,我读的是 29f00664f 的 src/plugin.ts + master e4cc68575 的 prepare-anthropic.ts / budget.ts / fork-adoption.ts / cache-ledger.ts 源码,没有跑这套测试(需要完整 harness + spawn,我这台机器被沙箱挡)。下面每一条都给了行号,方便你直接核对。

方向我支持——#4266 机制 1 是我提的同一诊断。但有一条会让这次修复在真实 claude 思考会话上「解了 snapshot 门、丢了继承前缀」。

A. 快照里带着代理侧打的 thinkingTokens,而进来的字节不带 ⇒ 前缀门每次判 hash-drift-under-same-id

链条(全部实名行号):

  1. src/server/prepare-anthropic.ts:219 originalMessages = msgs;,紧接着 :262 projectThinkingMass(msgs, {...}) 原地改 msgs:src/server/budget.ts:182 t.msg.thinkingTokens = share; ⇒ originalMessages 与被打过标的数组是同一个。
  2. src/server/handle.ts:2788 把 prepared.originalMessages 交给 rememberPluginMessages,后者 src/plugin.ts:468/512 session.pluginSnapshot = structuredClone(original) ⇒ 快照 cell 带 thinkingTokens。
  3. 子会话 fork 时 src/plugin.ts:1511 child.pluginSnapshot = structuredClone(parent.pluginSnapshot!.slice(...)) ⇒ 子会话带着这些标。
  4. 子会话首个请求过 publicForkInputCheck(src/plugin.ts:1110,调用点 src/server/handle.ts:1381),它对每个 cell 比 forkMessageIdentityHash(expected) 与 forkMessageIdentityHash(candidate)(:1146-1147);candidate 来自 incomingCoreMessages(src/fork-adoption.ts:110-133),那是纯 strip+convert,没有 projectThinkingMass ⇒ thinkingTokens 是 undefined,而 expected 是数字,而 forkMessageIdentityHash 的字段序列里就有它(src/plugin.ts:1091)。
  5. 于是第一个签名-only reasoning cell 上必然 hash-drift-under-same-id。

#3741 之后这道门是建议性的(handle.ts:1382-1406:adopting anyway + publicForkPrefix = false),所以不会是 409;后果是每个思考会话的 fork 子会话首个请求都走「未继承折叠」的全量展开——正是这类父历史(thinking + 长上下文)最贵的那一跳。换句话说:snapshot 的 409 没了,fork 的收益也没了,只剩一行 warn:

[fork-prefix] <session>: inherited prefix mismatch — adopting anyway (cell-mismatch@N hash-drift-under-same-id role=assistant ...)

修法建议:metering 字段不该进身份。仓库自己已经为这件事立过规矩——src/cache-ledger.ts:470-481 的 GENERATED_METERING_FIELDS = ["thinkingTokens"],注释原文就是「present on Prepared.originalMessages but NOT in the client's bytes … re-derives the value every turn from the previous usage report」。建议把同一份 permitlist 用到 fork 侧:forkMessageIdentityHash(src/plugin.ts:1091)、sameSnapshotView(:1101-1105)、客户端镜像 src/agent/fork-adopt.ts:80 三处统一排除 thinkingTokens(或者反过来,在 incomingCoreMessages 里对称地补投影——但那要求两侧用同一个 lastInputTokens,做不到)。

B. 同一个字段让 CoW 快照每轮走全量替换(既有问题,这次变成承重墙)

rememberPluginMessages 的前缀走查 src/plugin.ts:476 while (k < prev.length && k < n && sameSnapshotView(prev[k], original[k])) k++,而 sameSnapshotView(:1101-1105)比较 a.thinkingTokens === b.thinkingTokens。既然这个值是「每轮从上次 usage 报告重新推导」的(cache-ledger.ts:473 原文),那么 anthropic 思考会话里 k 会在第一个签名-only cell 就停住 ⇒ 落到 replacement 分支 :506-517:stableJson(original) 量一遍 + structuredClone 整段历史,每一轮。这正是 #2077 的 CoW 要避免的成本。A 的修法顺带把 B 也修了。

C. 四条实现细节

  1. :1180-1184 的注释把两种形状混为一谈:thinkingTokens 不是「thinking 内容的计量」,而是 budget.ts:156-186 只给「contentType === "reasoning" + 非空 thinkingSignature + text 为空」(:164-168)的单元按签名长度比例分配的不可见质量。有正文的 thinking 本来就可比较(text 承载),真正的门只是白名单;被删的那条子句是 shape B 唯一的形式证据。建议正文按仓库纪律写清行为变化:signature-only reasoning no longer refuses fork; hidden mass is not part of fork identity。
  2. 测试②测的是 shape A:for (const cell of parentSession.pluginSnapshot!) cell.thinkingTokens = 9; 给每个 cell 打标,而它们全都有 text。建议补一例真 shape B:wire 只发 { type: "thinking", signature: "s" }(无 thinking 正文)→ snapshot 200 → fork 201 → 子会话首个请求不再触发 A 里的 hash-drift。这条测试同时能守住 A 的修法。
  3. :1052 的早退跳过载荷检查::1057 对其余每族都要求 typeof part.text === "string"(或 tool_calls),而 thinking 族直接 return true ⇒ 畸形的 { type: "thinking" }(既无 thinking 也无 text)也判可比较,与注释「comparable by view」矛盾。建议按注释自己写的字段收窄:if (part.type === "thinking") return typeof part.thinking === "string" || typeof part.text === "string";、redacted_thinking 用 typeof part.data === "string"。
  4. 签名对 sameSnapshotView 不可见:shape B 的全部内容就是 thinkingSignature,但 sameSnapshotView/forkMessageIdentityHash 都不含它、src/plugin.ts 全文 0 处引用。签名轮换时判「相同」⇒ 子会话继承旧签名 ⇒ 回放撞 thinking/redacted_thinking blocks ... cannot be modified(Fold splits signed thinking from its tool_use → permanent 400 on Anthropic wire (0.1.207) #4885/[PR] kernel: keep signed thinking with its tool_use across folds (#4885) #4898 同族不变量,那边锁的是「折叠不得拆开签名与 tool_use」,这边缺的是「fork 不得拆开签名与所属 cell」)。若认同,把 thinkingSignature 加进这两处 + src/agent/fork-adopt.ts:80 的镜像即可。

最小可合路径:A + B 用同一处改动(身份里剔除 metering 字段)+ C2 一条回归测试;C1 改注释/正文;C3/C4 可以另外开。需要我把 A/B 的补丁写成 draft PR 提上来也可以——你点头我就做。

@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

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

Merge-guard re-trigger checked: branch already contains master tip e4cc685 (nothing new to merge — the earlier "behind" snapshot was stale), API now confirms mergeable: true, mergeable_state: blocked (only waiting on CI); on head 29f0066 checks so far: 27 success / 12 skipped / 9 still running, zero failures.

ci-check added 2 commits October 27, 2026 12:04
Review-driven supplement on #4900 (found while verifying #4589): the
comparableHistory allow-list gained the anthropic thinking family but not
the Responses-wire typed {type:"reasoning"} input items, so a client that
replays reasoning items as typed parts still gets a permanent 409
SNAPSHOT_UNAVAILABLE on every fork/snapshot. DSH itself is unaffected — its
chat-completions wire carries untyped reasoning_content sibling fields, so
what fired for #4589's reporter was the thinkingTokens clause this PR
already removed; the gap only hits responses-wire reasoning replay.

Safe for the same reason the thinking family is safe: every wire codec
hashes the reasoning bytes into the projected message's text — openaiToCore
carries reasoning_content, responsesToCore takes reasoning-item content or
a canonical opaque seed when only encrypted content is present (#3642),
anthropicToCore reads b.thinking — and forkMessageIdentityHash digests that
text. Image/audio refusal is untouched.
…tering mass, add signed-content carriers (#4266)

forkMessageIdentityHash digested thinkingTokens, which projectThinkingMass stamps onto server-side views only — never present in the client's replayed bytes — so every stamped cell read as spurious hash-drift-under-same-id on the child's first request (advisory adoption, unfolded request). Exclude it; digest thinkingSignature/googleThoughtSignature instead so rotated signatures read as drift. sameSnapshotView compares thinkingTokens presence only (value drifts per turn; absence of any comparison silently dropped fresh stamps from the CoW walk). comparableHistory: payload-less thinking/redacted_thinking blocks stay non-comparable (fail-closed). Client mirror forkIdentityHashOf updated byte-identically (parity contract).
@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

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

查重:不重复——本 PR 即 #4266 的修复本体,评审意见已逐条核实并修进本分支(新 head 71e1b5766)。

感谢这份评审。我独立复核了全部四条主张(A/B/C1/C4 成立,C3 观察成立但其建议谓词需修正),已按结论在 PR 分支上直接修复。以下按结构化八问汇报:

1. 是什么问题:放开 thinking 硬门后,fork 身份哈希仍在 digest thinkingTokens——这个字段是服务端视图专用的计量块(projectThinkingMass 盖戳),客户端回放的字节里从不存在。于是每个被盖戳的格子在子会话首个请求上必然误判 hash-drift-under-same-id,走 advisory 采纳、丢掉 verified-prefix 快路径(恰好是 thinking+长上下文父会话最贵的那一跳)。硬门是拆了,但 fork 的核心收益没兑现。

2. 什么原因导致的:PR 原注释声称 "host-projected thinkingTokens … forkMessageIdentityHash already digests — both are comparable-by-view"——这句与事实相反:该字段单边存在(只在 prepared 数组上,GENERATED_METERING_FIELDS 教义明言它不在客户端字节里),digest 它等于系统性制造假漂移。PR 拆掉了"整体拒绝",却没有修正"拒绝所针对的那个不对称"本身。次生问题:sameSnapshotView 也按值比较该字段,anthropic thinking 会话每轮 CoW 前缀扫描都在第一个盖戳格停住 → 整树替换快照视图。

3. 影响范围与严重程度:所有带签名 thinking 的插件车道会话(dsh/opencode/pi/claude 等走 anthropic wire 的 extended-thinking)。程度:能用但有明显问题(p2)——fork 本身成功,但子会话首个请求失去前缀验证,一次性 token 尖峰 + 一条误导性的 [fork-prefix] ... adopting anyway 告警;无数据损坏。

4. 怎么修复的(同一机制家族,一个 commit):

  • 身份字段集纠正:forkMessageIdentityHash 与客户端镜像 forkIdentityHashOf(parity 契约,必须同改)移除 thinkingTokens,改 digest thinkingSignature / googleThoughtSignature(两端都有的真实内容载体)。签名轮换从此可读为漂移(advisory 采纳、一个未折叠请求,绝不损坏);纯计量块差异不再误报。
  • C3 修正(与评审建议不同):评审建议的 typeof part.thinking === "string" || typeof part.text === "string" 会拒绝合法的仅签名块 {type:"thinking", signature:"…"}——那正是本 PR 要放行的形状。实际采用:thinking 要求 thinking 或 signature 其一为字符串,redacted_thinking 要求 data 为字符串;无载荷块保持不可比较(fail-closed,拒快照/fork)。
  • CoW 存在性比较:sameSnapshotView 对 thinkingTokens 只比有无、不比值——值每轮重算会令每轮整树替换;完全不比又会把新盖戳静默丢掉(第一版实现踩过这个坑,回归测试钉住了)。
  • 行为变更旧→新已全部披露在 PR 描述「行为变更」节(含 parentRevision/orderHash 值跨部署变化的瞬态说明)。

5. 是否有回归问题:审过的既有行为——#3743 判决分类(hash-drift vs ref-broken,fixture 已从 thinkingTokens 换到真实身份字段并加钉"纯计量漂移不得误判")、客户端镜像字节级 parity(dsh/pi-omp 两套件)、全 fork 面 23 个测试文件本地 174/174 绿;高危模式逐项过:无吞错变化、无阈值/时序漂移、输出格式不变。远程 CI 在 71e1b5766 全绿(37 success / 14 skipped / 0 fail)。另注:推送时发现另一 agent 的 #4589 残差补充提交(45bafb3d7,responses-wire 类型化 reasoning 入允许列表)已在分支上,rebase 时按意图并集解决冲突,两者行为均保留并有各自回归钉。

6. 是否破坏缓存:不涉及。改动只在 fork/snapshot 判定路径(prefix gate、CoW 快照视图),不触碰模型线请求体、前缀缓存键或 ledger。

7. 是否改了设计方向:不涉及架构转向;是把本 PR 自己宣布的方向(comparable-by-view)落到正确的字段集上。

8. 是否引入新配置:无。

关于后续:评审者提出可自行开 draft PR——本仓库的修改一律走正常通道(PR 分支 additive commit),未向未经验证的评论者授予任何权限,其建议中的 C3 谓词已按上述理由修正。当前状态:71e1b5766 CI 全绿、mergeable,等待人工合并。

@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

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

状态补充 + 对 floor 9(Zhishulo 复核)的独立裁决。我的审核结论(floor 8)不变:根因、投影链、回归清单已独立复核,本地定向验证全绿(新文件 3/3、相邻 54/54、typecheck/build 干净);CI 在 head 29f0066 截至 03:40Z 零失败(27 success / 12 skipped / 9 running,见 floor 11),之后 GitHub API 限流无法再轮询。分支已含 master tip e4cc685(我独立用 rev-list 验证过),merge-guard-r1 又在跑(floor 14)但无新内容可同步。

对 floor 9 我没有照单全收,逐条对着代码重新取证。结论:A/B/C2/C3 成立,C1 成立,C4 成立但属另一个拍板项。先修正我自己前审的一处判断:

裁决(行号均为我独立核对)

A 成立,且我修正自己 floor 8 的表述。 我当时写「signature-only 思考最坏走 #3741 降级」——实际对 signature-only reasoning cell 这不是最坏情形而是必然发生。链条核实:prepare-anthropic.ts:220 originalMessages = msgs,紧接着 :263 projectThinkingMass(msgs, …) 原地写标(budget.ts:182 t.msg.thinkingTokens = share)⇒ 快照 cell 带标(plugin.ts:468/512 structuredClone);而 projectThinkingMass 全仓唯一调用点就是 prepare-anthropic.ts:263,fork 回放的候选侧(src/fork-adoption.ts incomingCoreMessages)纯 strip+convert、从不打标点 ⇒ expected=N vs candidate=undefined,第一个 signature-only cell 必出 hash-drift-under-same-id。后果(handle.ts:1381-1406):门是建议性的,不是 409——子会话首请求未折叠发出 + 一行 warn,继承折叠由 reconcile 事后修复(#3741 原文「a one-time token spike, never corruption」)。对 Zhishulo 措辞的一处修正:不是「fork 的收益没了」(折叠没丢),而是「子会话最贵的第一跳必然失去 verified-prefix 快路径 + 每次 fork 留一行误分类 warn」。根因是架构性的:计量字段不该进身份——仓库在 #4043 已经为同一字段立过规矩(GENERATED_METERING_FIELDS 注释原文「re-derives the value every turn」,故从 origin evidence 剥离),fork 侧三处身份摘要漏跟了。影响面要诚实收窄:只有「重发历史里存在 signature-only 思考块(text 为空)」的会话中招;Claude Code 常规扩展思考重发的是完整 text+signature,两侧都 undefined、精确匹配,不受影响。

B 成立,且是既有问题。 sameSnapshotView(plugin.ts:1088)比较 thinkingTokens,该值每轮从上一轮 usage 重推 ⇒ signature-only 思考会话的 CoW 前缀走查每轮停在第一个 signature-only cell ⇒ 整段历史 stableJson+structuredClone 每轮重来。这在 master 上今天就存在(rememberPluginMessages 与 fork 无关),本 PR 未引入、未加重,但修复点与 A 完全同址(同一机制同一处字段集),按「连贯修复不拆片」应同批收敛。

C2 成立且是关键证据。 现有测试①的思考块全部带正文(projectThinkingMass 跳过非空 text cell,budget.ts:164-168),所以测试①在 A 未修时照样绿——当前测试结构上抓不到 A。必须补真 shape-B 用例(wire 只发 signature 不发正文)。

C3 成立(低严重度)。 thinking 族早退跳过了载荷检查,与其他族要求 text/tool_calls 不一致;畸形 {type:"thinking"} 也判可比较。两侧投影确定性相同,不会产生错误继承,但谓词与自己的注释矛盾,顺手收紧即可。

C4 成立,单独拍板。 thinkingSignature 确实不在两处身份字段集(plugin.ts 全文 0 引用);签名轮换(模型切换/提供方再生成)时会判「相同」⇒ 子会话继承旧签名字节 ⇒ 回放撞 Anthropic「cannot be modified」拒绝,与 #4885/#4898 同族不变量。修法同样是那三处加字段,但会使所有带签名消息的 hash 值变化(跨版本 parity 过渡走 #3741 建议性采纳自愈),放本 PR 还是 follow-up 请 owner 定。

决策请求(选项与推荐见八问后)

(ai 声明同上) 八问:

  1. 是什么问题:两层——(i) 本 PR 主体:思考会话永远无法 fork(floor 8 已述);(ii) 审核新发现:身份摘要含一个逐轮重算的计量字段,使 signature-only 思考会话的 fork 子会话首请求必然失去继承前缀快路径(一次性全量展开+误分类 warn),CoW 快照走查退化为每轮全量替换(既有)。
  2. 什么原因导致的:projectThinkingMass 为计量需要([bug] anthropic wire: signature-only thinking blocks missing from compressible estimate — savings receipts overstate, nudge stays idle (claude native) #1320)把 thinkingTokens 原地写到与 originalMessages 共享的消息列表;fork 身份 hash 与 CoW 视图比较直接含该字段;fork 回放候选侧从不打标(投影唯一调用点在 prepare 侧)⇒ 两侧不对称。Generated thinkingTokens estimates can be mistaken for client history churn #4043 已证明此字段非字节稳定、应从身份面剥离,fork 侧漏跟。
  3. 影响范围与严重程度(按选项):现状合入——可用性恢复(主目标达成);仅「重发含 signature-only 思考块」的会话每次 fork 付一次全量展开首请求+warn,带正文思考的大多数会话不受影响;属特定条件性能损耗+持久日志噪音,非数据损坏。选项 A(本 PR 收敛)——消除上述损耗,身份语义收敛为「客户端可见字节」,波及面=fork 身份比较全体(字段集变更),过渡期旧 receipt→建议性采纳自愈。选项 B(拆 follow-up)——本 PR 立即可用,上述损耗持续到 follow-up 落地。
  4. 怎么修的(选项 A 思路):三处身份字段集剔除 thinkingTokens(服务端 forkMessageIdentityHash、sameSnapshotView、客户端副本 fork-adopt.ts:80,parity 由 dsh-fork-adopt.test.ts 钉住),沿用 Generated thinkingTokens estimates can be mistaken for client history churn #4043 GENERATED_METERING_FIELDS 纪律(未来宿主计量字段统一入列);补 C2 shape-B 回归用例(wire 只发 signature → snapshot 200 → fork 201 → 子会话首请求无 hash-drift);C1 修正注释/PR 描述写明「隐藏质量不属于 fork 身份」;C3 收紧 thinking 族谓词到字段级检查;C4 可选同批加 thinkingSignature。
  5. 是否有回归问题(按选项):选项 A——身份 hash 值变化(含该字段的消息)→ 跨版本旧 receipt 与新服务端比对失配 → 走 This run failed with 409: "first child request does not match its inherited ordered prefix" | 本轮运行失败409 "first child request does not match its inherited ordered prefix" #3741 既有建议性采纳路径(一个发布周期内自愈,无 409 死路);sameSnapshotView 放宽只让 CoW 走查走得更远(纯性能收益);C3 收紧使畸形块会话从「放行」变「拒绝」(更严、fail-closed 方向);C4 同为 hash 值变化的过渡。revision token v2 不变,无缓存键/去重键语义变化。选项 B——除现状外无新增回归,已知损耗保留。
  6. 是否破坏缓存:不涉及。上游前缀缓存接缝不动;fork 身份面变化不触达前缀缓存键。
  7. 是否改了设计方向:不改——反而把 fork 身份对齐仓库既有教义(身份=客户端可见字节、计量字段剥离,Generated thinkingTokens estimates can be mistaken for client history churn #4043/KDD feat: self-update (auto + manual) #11)。
  8. 是否引入新配置:无。

推荐:选项 A——A 是本修复的必要完整性补丁(否则被修复的那类会话必然踩中已知降级路径),与 B 同机制同址,应在合并前于本 PR 一次收敛;C4 建议同批(同一三处,一次把身份字段集做完整),也可拆 follow-up,听你拍板。你定了我立即在 PR 分支实现并推标注 commit(若 Zhishulo 先提 draft PR,我转审他的实现)。

@Zhishulo

Copy link
Copy Markdown
Contributor

(ai: deepseek/deepseek-v4.1-flash-1017)

新身份字段集复核(head 71e1b5766):both-sides 前提成立,一处同族漏洞仍未闭合

一、先确认三处(逐行对着 head 核过,不是引用你的报告)

  1. 两个签名载体确实是两侧都有的真内容。 inbound kernel/src/wire/anthropic.ts:199-215 把 b.signature 写成 thinkingSignature,coreToAnthropic kernel/src/wire/anthropic.ts:501 逐字还原;google 侧 kernel/src/wire/google.ts:172 inbound 写 googleThoughtSignature(outbound :455/468/502/523)。⇒ 我上一条建议的「该槽恒置 null、保留 8 槽」在这个 PR 里不再必要:digest 真实签名比归零更强 —— 签名轮换是真实内容变化,现在能读成 drift,而我的方案会把它当计量噪声放过。你选的方向我接受。
  2. pin 反转与我建议一致。 tests/plugin-fork.test.ts:588-612 现在先断「只改 thinkingTokens 仍 {ok:true}」,drift 样本换成 cell.text 变异,紧接的 ref-broken 分支原样保留。
  3. CoW 问题解掉。 sameSnapshotView(src/plugin.ts:1125-1133)对 thinkingTokens 只比 presence、对签名比值,注释契约同步改写 ⇒ 「每个被盖戳格都停走查、每轮整树替换」不再发生。

二、仍未闭合的一处(同族,建议本 PR 一次收)

redacted_thinking 的 payload 既不在 id、也不在身份元组里。

  • 投影:kernel/src/wire/anthropic.ts:253-273 把 redacted 块投成 {contentType:"text", text:"[redacted_thinking]", rawAnthropicBlock:b},id 走 deriveCanonicalMessageId(m.role, "text", CANONICAL_REDACTED_THINKING_TEXT);kernel/src/wire/identity.ts:22-28 明写 seed = role | contentType | (empty) | toolName | canonicalText ⇒ payload 不参与 id。对比 image 的 Image message ids re-mint whenever an earlier image leaves the visible tail (membership-based ClusterCounter suffix) — prefix-cache breaks per fold, feeds #3748 storage growth #3751 是 payload-addressed(anthropic.ts:232-245 传 mediaFingerprint,同一位置换图必换 id),redacted 没有等价处理。
  • 身份:新九槽里 redacted 走 contentType:"text" + 常量 text + 两个 signature 槽皆 undefined ⇒ 同位置两个不同 payload 的 digest 完全相等。
  • 但本 PR 同时把 redacted_thinking 放进 comparableHistory 白名单(src/plugin.ts:1058,要求 data 是字符串)⇒ 这类会话现在能 fork,而 src/plugin.ts:1099-1105 的注释声称「a rotated signature reads as drift … instead of silently inheriting stale signature bytes」对 redacted 不成立:payload 轮换会静默过身份门,正是这条注释要防的「silently inheriting stale opaque bytes」。
  • 两种收法:
    • (1) 补 payload 摘要槽(我倾向):rawAnthropicBlock 两侧都有(inbound 原样带 sidecar,anthropic.ts:270-272),把 data 的 sha256 截断(例如 16 hex)作为第 10 槽,与 signature 同族。redacted 在 claude 工具历史里不罕见,fail-closed 会把这类会话长期挡在门外。
    • (2) 短期 fail-closed:把 redacted_thinking 从白名单撤回(只留 thinking),恢复「带 opaque payload 的会话拒绝 fork」的旧姿势,另开 follow-up。
  • 若选 (1):元组从 9 槽到 10 槽 = 本 PR 内第二次改 digest,建议合并成一次到位的口径披露(别再产生第二个部署窗口的 parentRevision/orderHash 瞬态),并补一个钉:同位置换 redacted payload ⇒ hash-drift-under-same-id(当前是 {ok:true})。

三、跨版本 skew(我上一条的顾虑,被你的披露覆盖)

src/agent/fork-adopt.ts 与服务端同在代理包内(dsh-native、pi-omp 适配器都住在 src/agent/,没有跨包副本)⇒ parity 是包内的;升级窗口的 skew 只发生在「代理已升级、宿主插件仍是旧副本」的瞬间,届时所有格子(不只思考格)drift 一次,但 #3741 之后门是建议性的 ⇒ 最坏一条 warn + 一次未折叠请求,不会 409。PR 描述的「行为变更」节写清这点就够,不需要协议版本位。

四、分工

facet B 的实现权归你(你已在 #4266 说明 draft 由你写),我不另开分支、不在你的分支上并发编辑。若采纳第二节,需要 in-repo 回归钉我可以按你指定的文件写测试(只动 tests/),点名即可。

ci-check added 5 commits October 30, 2026 00:52
Converge onto the resident ci-fixes baseline (master dc10c2d + guard-lane
fixes) so this branch is current with master. Clean union-of-intent merge:
the #4266 thinking-gate work (src/plugin.ts, src/agent/fork-adopt.ts) is
untouched by ci-fixes; tests/plugin-fork.test.ts auto-merges both this PR's
#3743 identity-field pin and master's #5140 cold-tail 409-body diagnostic.
Behind-guard sync: merges master's #5188 (fork/snapshot sidecar recovery +
reconstruction-reason error naming) into the #4266 thinking-gate-lift branch.
Single conflict in forkSnapshot resolved as the union of two orthogonal
changes: keep master's reconMissReason [reconstruction: why] error naming AND
keep #4900's lifted thinking hard gate (signed thinking projects to comparable
reasoning messages; only publicSnapshotTextComparable===false still refuses).
Targeted tests green: plugin-fork, issue4266-fork-thinking-gate,
issue5188-sidecar-recoverable-open, issue5140-cold-raw-tail-fork (71 pass);
typecheck clean.
@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

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

同步记录(来自 #5409 处理会话):分支此前落后 master 320 个提交,已 merge origin/master 进本分支——无冲突,修复完整落在 master 新结构上(forkSnapshot 的冷尾重建路径 reconstructRawTailForFork 同样受拆门保护)。本地验证:build ✓ / typecheck ✓ / 四套 fork 定向测试 85/85 ✓(issue4266-fork-thinking-gate、plugin-fork、dsh-fork-adopt、issue5140-cold-raw-tail-fork)。推送头 16c3a0e4c,CI 重跑中。

新增生产佐证(#5409,v0.1.209,DSH native / DeepSeek reasoning):同一工作区 9/9 fork 全部降级为 snapshot http 409 SNAPSHOT_UNAVAILABLE (multimodal or opaque content cannot be compared by text);零继承后子会话首轮全量裸放父历史(实测 2745 msgs / inbound=5.6MiB),模型调用失败或溢出,fork 不可用。报告者的本地拆分补丁与本 PR 同向,其回归测试断言与最终身份字段集(thinkingTokens 出哈希)的细微差异已在 #5409 回复中注明。

…tity (#4266)

Review finding on this PR (thread floor 17): whitelisting redacted_thinking into comparableHistory without payload identity lets a rotated redacted payload under an unchanged id pass the fork gate silently - the kernel projection maps every payload to the constant [redacted_thinking] seed and the identity tuple carried neither the payload nor a signature for this type.

Fix: 10th identity slot digesting the sidecarred rawAnthropicBlock data (truncated sha256, hex16) in forkMessageIdentityHash (src/plugin.ts) and its client mirror forkIdentityHashOf (src/agent/fork-adopt.ts) - same payload-addressing pattern as image blocks (#3751). If the sidecar is ever absent on one side, the digests mismatch and advisory adoption degrades loudly instead of silently inheriting stale bytes.

Behavior change (pre-merge; collapses the second transient parentRevision window into the one already disclosed in the PR body): fork identity tuple extended from the PR's 9-slot form to 10 slots (adds the redacted payload digest).

Tests: two new pins in tests/issue4266-fork-thinking-gate.test.ts (verbatim redacted prefix verifies clean; rotated payload reads as hash-drift-under-same-id), parity-matrix rows e2/h covering signatures + redacted payload in tests/dsh-fork-adopt.test.ts. Negative control verified: the drift pin fails against the pre-fix source ({ok:true}).
@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

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

Review verdict: APPROVE — and one labeled review-fix commit is already pushed (50d944708, closing the redacted_thinking payload hole left open at floor 17).

Independent verification (re-derived from code, not parroted from the thread)

  • Effective diff vs master = 4 files (+296/−8): src/plugin.ts, src/agent/fork-adopt.ts, new tests/issue4266-fork-thinking-gate.test.ts, tests/plugin-fork.test.ts. No kernel/, no config surface, no lockfile/version lines → authorization boundary passes.
  • Projection chain verified at PR head: thinking→reasoning core carries text+thinkingSignature; Google inbound carries googleThoughtSignature; Responses encrypted reasoning items are payload-addressed (canonicalOpaqueSeed); OpenAI reasoning_content rides core text. ⇒ the new signature slots are real both-sides content, not a server-only stamp.
  • Advisory adoption (#3741, handle.ts) confirmed unchanged: bites only the child's FIRST request; mismatch = warn + one unfold, never corruption — so "add identity slots, don't add refusals" is the right shape.
  • sameSnapshotView presence-only compare is safe: every thinkingTokens consumer (cache-ledger metering field, preflight key, budget projection) reads live arrays, never snapshot cells.
  • Confirmed the floor-17 hole existed at 16c3a0e: the redacted payload sat in neither the id nor the tuple → a rotated payload under an unchanged id passed the gate silently. Fixed in this review (option 1): 10th identity slot digesting the sidecarred payload (truncated sha256, hex16) in forkMessageIdentityHash AND its client mirror forkIdentityHashOf, mirroring the image payload-addressing precedent (Image message ids re-mint whenever an earlier image leaves the visible tail (membership-based ClusterCounter suffix) — prefix-cache breaks per fold, feeds #3748 storage growth #3751). Fail-safe direction checked: if the sidecar is ever absent on one side, digests mismatch → loud advisory adoption, never silent inheritance of stale bytes.

Regression inventory (existing behaviors on touched paths)

Existing behavior Verdict
image/audio/unknown-type blocks → SNAPSHOT_UNAVAILABLE 409 preserved (negative pin retained)
payload-less thinking block → non-comparable fail-closed preserved (pin retained)
advisory-adoption semantics (#3741) unchanged
publicForkInputCheck reason taxonomy preserved; drift-sample pin correctly flipped from thinkingTokens-mutation → cell.text-mutation
CoW snapshot reuse (rememberPluginMessages) fixed, not regressed (value-compare churned every turn → presence-only)
client/server hash-parity contract extended with e2 (signed reasoning) + h (redacted payload) rows; parity suites green
parentRevision/orderHash values intentional change — disclosed below

Behavior change disclosure (old → new)

  1. thinking-session forks: old SNAPSHOT_UNAVAILABLE 409 (fork entirely unavailable) → new: fork succeeds (the point of the PR; production evidence [PR] fix: frozen compression state becomes visible, diagnosable, and healable (#5408) #5409: 9/9 forks degraded).
  2. fork identity tuple: master 8-slot form (incl. thinkingTokens) → PR 9-slot (signatures in, thinkingTokens out) → this review commit: 10-slot (+ redacted payload digest). Consequence: parentRevision/orderHash values shift across deploys — a child forked from an older parent gets exactly ONE advisory-adoption unfold on its first request (warn log, self-heals, no data loss). This folds into the window already disclosed in the PR body rather than adding a second one.
  3. redacted_thinking: old outright refusal → now comparable AND payload-addressed (rotation under unchanged id reads as hash-drift-under-same-id instead of passing silently).

Test validity

  • New suite has teeth: 8/8 pass at my head; negative control executed — reverting only the two src files makes the new drift pin fail with {ok:true} (the pre-fix silent pass), so the regression is caught if it returns.
  • Local targeted runs: issue4266 8/8 · plugin-fork+dsh-fork-adopt+pi-omp-fork-adopt 98/98 · session-continuity-proof+issue5140-cold-raw-tail 12/12 · typecheck clean. Full-suite verdict: remote CI — mirror cache at review time showed head 16c3a0e with zero failures (5 success / 13 skipped / 12 shards in flight); fix commit 50d9447 landed after that state, CI re-running on it.

结构化八问

  1. 是什么问题:thinking 会话完全无法 fork——两道门都拒收,生产上([PR] fix: frozen compression state becomes visible, diagnosable, and healable (#5408) #5409)子进程首轮把父会话原始历史全量裸发(实测 5.6MiB)。
  2. 什么原因导致的:comparableHistory 白名单早于 typed thinking parts;forkSnapshot 带一刀切 thinkingTokens>0 拒绝;更深层是身份键错了字段——服务器单侧的计量戳(thinkingTokens)进了哈希,真正的字节载体(签名、opaque 载荷)反而不在。
  3. 影响范围与严重程度:p1 可用性——所有开思考的客户端(Claude Code extended thinking、DSH native DeepSeek reasoning)fork 全灭无绕行;无数据丢失。
  4. 怎么修复的:两道门改严格谓词放行(无载荷块保持 fail-closed);身份重键到两侧真实字节载体(签名 + opaque 载荷摘要);降级路径沿用既有 advisory adoption。
  5. 回归问题:上表清单逐项过,未发现破坏;高危模式已查——fail-fast 未变吞错(advisory 路径原样)、阈值/时序未漂移、拒绝分类法保留。
  6. 是否破坏缓存:prompt-cache 前缀不受影响(fork 身份在账本侧,不是线上字节);唯一效果是披露过的 parentRevision 过渡窗口 → 一次性 unfold,自愈。
  7. 是否改了设计方向:没有——沿 Image message ids re-mint whenever an earlier image leaves the visible tail (membership-based ClusterCounter suffix) — prefix-cache breaks per fold, feeds #3748 storage growth #3751 载荷寻址 + This run failed with 409: "first child request does not match its inherited ordered prefix" | 本轮运行失败409 "first child request does not match its inherited ordered prefix" #3741 advisory adoption 既有契约。
  8. 是否引入新配置:没有。

审查动作:已推标注 fix commit 50d944708(floor-17 洞:红acted 载荷摘要槽 ×2 侧 + 2 个新钉 + 对偶矩阵行),独立提交、不含其他改动;kernels 未触碰、无需授权申请。CI 在 50d9447 绿后即可合并。


协调行:review 完成 —— APPROVE @ 50d9447(review-fix commit 已推,等新 head 首圈 CI)。

@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

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

Review verdict: APPROVE — 新 head CI 全绿,本轮 delta 仅一个 commit(上轮审查推入的标注 fix),已逐项独立复核。

本轮验证(独立重做,非复述)

  • 远端 head 50d944708 与本地跑过全套定向测试的树逐字节一致(fetch 比对、worktree clean);16c3a0e4c → 本 head 之间只有一个 commit:fix(plugin-fork): digest the redacted_thinking payload into fork identity (#4266)——即上轮审查的标注修复,无第三方提交、无 sync merge 混入。
  • PR base 未漂移:merge-base == master tip(7578e81),落后 0 commit。
  • 全量套件裁决(对 head 50d944708 单次上游读取,非轮询):37 success / 14 skipped / 0 failure。镜像缓存快照曾冻结在「12 pending」状态 16 分钟以上,直读确认为已完成全绿。
  • 该 commit 的本地证据(上轮执行):issue4266 套件 8/8(含负控制——回退两个 src 文件后新 drift pin 以 {ok:true} 失败,证明回归可被抓);plugin-fork+dsh-fork-adopt+pi-omp-fork-adopt 98/98;session-continuity-proof+issue5140-cold-raw-tail 12/12;typecheck clean。
  • 授权边界:diff 不触碰 kernel/、无 kernel-change 声明 → 批准无障碍。

结构化八问(对新 head)

  1. 是什么问题:thinking 会话完全无法 fork——两道门拒收,生产([PR] fix: frozen compression state becomes visible, diagnosable, and healable (#5408) #5409)子进程首轮把父会话原始历史全量裸发;本 head 额外闭合了 redacted_thinking 载荷的身份洞。
  2. 什么原因导致的:身份键错字段(服务器单侧计量戳进哈希、真实字节载体缺席)+ 两道硬门一刀切拒收 thinking 家族;redacted 洞 = 投影把所有载荷映射到常量 seed,id 与元组都不含载荷。
  3. 影响范围与严重程度:p1 可用性(所有开思考客户端 fork 全灭、无绕行、无数据丢失);redacted 洞是该范围内的低概率静默通过路径(最坏一次失败调用、自愈)——现已关门。
  4. 怎么修复的:严格谓词放行(无载荷块保持 fail-closed)+ 身份重键到两侧真实字节载体(签名槽 + opaque 载荷摘要槽,双侧镜像)+ advisory adoption 保留为降级路径。
  5. 回归问题:上轮七项既有行为清单全部维持(image/未知类型 409 保留、fail-closed 保留、advisory 语义未变、拒绝分类法保留、CoW 是修复非回归、parity 契约扩展、revision 值变更已披露);本轮 delta 只把身份元组加一个槽位 + 测试,无新增回归面。
  6. 是否破坏缓存:否(fork 身份在账本侧,不动线上字节);唯一效果是已披露的 parentRevision 过渡窗口 → 一次性 unfold,自愈。
  7. 是否改了设计方向:没有——沿 Image message ids re-mint whenever an earlier image leaves the visible tail (membership-based ClusterCounter suffix) — prefix-cache breaks per fold, feeds #3748 storage growth #3751 载荷寻址 + This run failed with 409: "first child request does not match its inherited ordered prefix" | 本轮运行失败409 "first child request does not match its inherited ordered prefix" #3741 advisory adoption 既有契约。
  8. 是否引入新配置:没有。

判定:可合并 —— head 50d944708 CI 全绿、证据齐备,等人工合并。


协调行:第二轮 review 完成 —— APPROVE @ 50d9447(37 success / 14 skipped / 0 failure;delta 仅为上轮标注 fix,逐字节一致性已验证)。

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

Projects

None yet

2 participants