Skip to content

[PR] fix(#5595): keep fork children sendable when raw-history rebuild emits empty tool-call arguments - #5602

Open
ranxianglei wants to merge 26 commits into
masterfrom
2026-11-03_fork-multimodal-adoption
Open

ranxianglei wants to merge 26 commits into
masterfrom
2026-11-03_fork-multimodal-adoption

Conversation

@ranxianglei

Copy link
Copy Markdown
Owner

Closes #5595

What

A fork child whose parent held multimodal/thinking content cannot be textually compared, so adoption degrades to a full raw-history rebuild. That rebuild serializes unrecovered tool-call cores as empty function.arguments, which OpenAI-compatible providers reject with a hard 400 (InvalidArgument: ...function.arguments must be a valid JSON object string). Compaction/preflight cannot rescue it — it is a validation failure, not a size failure — so the forked session stays unusable forever.

This PR makes the reported bricking go away at the boundary we own, plus fixes a misleading diagnostic on the same path.

Changes

F1 — outbound arg-validity guard (the availability fix). Enforces the provider contract that every OpenAI tool_calls[].function.arguments is a valid JSON object string, at BOTH finalize sites where bili assembles the outbound OpenAI body:

  • src/server/prepare-openai.ts (proxy-mode rebuild)
  • src/loop/adapter-openai.ts (compress-loop re-request)

New hardenOpenaiToolCallArguments in src/util.ts mirrors the existing MALFORMED_PROXY_TOOL_ARGS_REPLAY precedent (src/loop/core.ts, which already fixes the model-produced non-JSON case): valid arguments pass byte-exact; only empty/non-object JSON is swapped for a parseable placeholder, and one warn line records how many were rebuilt. Wire fidelity holds (§7.3): no legitimate argument bytes change.

F4 — name the permanent legacy-storage bail. When durable storage is legacy-only (storage.unified="legacy"), there are no unified tail rows, so fork raw-tail reconstruction bails permanently — but it reported the transient sidecar-missing reason, mislabeling a structural gap as a recoverable open failure. It now reports the actionable cause.

⚠️ Behavior change (F4): the fork-inheritance-unavailable log/reason string changed from replicasUnavailable sidecar-missing to an actionable message naming the unified-store requirement when durable storage is legacy-only. Log/diagnostic text only — no protocol, default, threshold, timing, or output-format change.

Scope & what is deliberately NOT here

In-flight overlap on the broader "fork adoption fails → child unusable" family: #4900 (thinking gate), #3024 (retryable snapshot), #5126 (false-success receipt), #5538 (oversized rebuild). This PR covers the distinct facets §4.2 (empty-args schema 400) + §4.3 (legacy no-reconstruction diagnostic) uncovered by those.

Verification

  • npm run typecheck clean (exit 0).
  • New regression suite tests/issue5595-fork-empty-tool-args.test.ts (8 tests incl. gate-negative byte-exactness + a live buildRequest wiring test) passes.
  • Related suites green: issue3661-malformed-proxy-args-replay, issue3926-empty-tool-calls, loop-adapters — 52 pass / 0 fail (no regression in the touched adapter/wire path). Full-suite verdict = remote CI on the pushed head.

No config surface added/changed. No kernel change. No dependency added.

ework-agent and others added 23 commits October 31, 2026 18:03
Converging master's red gates onto ci-fixes. lifecycle-ledger-guard is already fixed here (LIFECYCLE.md entries for BILI_SESSION_CONTENT_STORE_PIN / BILI_SPAWN_FINGERPRINT from the prior sentinel pass, preserved through the master merge); this commit clears the other two.

docs-sync: #5319 documented the new 'chat' builtin prompt pack in CONFIGURATION.md/.zh-CN.md but never updated the single-source seed, so regeneration dropped it and the gate failed. Add 'chat' to the compress.promptPack seed (type/desc_en/desc_zh) and regenerate website/docs/index.html to match.

unused-exports: CodexWindowSource (src/codex-models.ts) and PackInstallOptions (src/pack-install.ts) are referenced only inside their own modules and imported nowhere else; drop the leading export (INTERNAL_ONLY) as the guard prescribes.

Verified locally: lifecycle-ledger-guard OK, check-unused-exports clean, gen-config-docs regenerate matches committed artifacts (zero drift).
…safe family gate)

pack-install.ts took the last 3 npm stderr lines via split+slice(-3), tripping the #816/#1615 family gate in tests/text-safe.test.ts even though the receiver is an array of lines (code-unit-safe). Use a non-negative computed start index so the literal negative-slice token is gone; output is identical (last up-to-3 lines clamped to 300 chars). Inherited from #5340, which shipped without satisfying the gate.
Inherited red resolved by convergence, not rewrite: ci-fixes was 12 commits
behind master and its two unique fix commits (cbdd67d negative-slice, 701688d
docs-sync/unused-export) are strict subsets of what master already merged
better (e030d3b+4a12c1a4 safePrefix+windows npm shim, 3491e2d dead export,
b76f0c9 lifecycle reconcile). Both failing shards were inherited from master's
own newer commits that ci-fixes lacked:
  - #5103 refusal-remedy test -> pinned by 892a378
  - flushAll dirty content store -> fixed by 1f209ff (src/store.ts adopt)
Conflicts (codex-models.ts, pack-install.ts) resolved toward master = superset.
Resulting ci-fixes tree is byte-identical to origin/master (git diff empty).
…n (suite preload parity)

The whole suite runs under tests/preload-test-env.mjs, which pins the #4313
message-timestamp display gate OFF so byte-comparing baselines keep their
pre-feature wire bytes. The regen script only carried the Parity #1/#2 pins
(image rescale, snapshot shadow) — a bare 'node --import tsx' regen baked
time-stamped ACP tags plus the timestamp-aware ACP-TAGS doc variant into
every per-turn digest (+157B constant per body, +58B per turn), so freshly
regenerated goldens would red all four wires again on the next CI run.

Discovered while converging the master red at c78ecb4 (#4599 sentinel
cycle): the local regen drift was initially misread as an environment
divergence until body-level forensics isolated the stamp attributes and the
flag-conditional doc section in kernel/src/compress-tools.ts.
#5416 (c91ef1a, kernel 0.0.176) added absorbedDebt/absorbCount to the
persisted nudge state (host booking in src/session.ts absorbAbortedTurnGrowth,
reset in kernel applyCompression) — an intentional persisted-state schema
change, declared in that commit ('kernel-change: nudge decision only ... no
config surface added'). After any fold the persisted session now carries the
two new keys, so the state digests drift by design.

Regenerated via the now-parity-pinned scripts/update-storage-render-goldens.ts.
Diff is surgical: per wire ONLY stateDigest, stateDigestAligned,
sectionDigests.state and stateKeyDigests.nudge change; every per-turn
sha/bytes/shaCiteNorm is byte-identical to the committed goldens (wire parity
proof) and blockContents/stats/metadata sections are untouched. Both consuming
suites green locally under the exact CI preload chain (17/17).
…iscussion, not node)

The new blog pipeline (#5414) selected "{ node { number url } }" from the
createDiscussion mutation, but GitHub's CreateDiscussionPayload exposes
"discussion", not "node" — so the first master run of sync-discussions failed
with "Field 'node' doesn't exist on type 'CreateDiscussionPayload'". Align the
query and its result reads, and pin the selection in the unit test (positive +
negative) so the wrong field cannot silently ship again.
The pipeline opens exactly one PR for the human merge step (POST /pulls), which
requires the pull-requests:write permission; without it the write-back would 403
immediately after a discussion was created.
…pshot (#4599)

The "PFA stamp must be on disk after restart" assertion flaked on CI load
(ubuntu-22 shard1; reproduced 1/24 locally under concurrency, idle-green).
Root cause: the test read the raw envelope while the still-running server
could still fire leftover turn debounced saves, which overwrote the terminal
snapshot without the just-set anonymousPrefixAffinity field. A real restart
stops the old process writing, so drain (flushAll) + cancel leftover debounces
(cancelAll) and swap in the reload store BEFORE reading. All four original
byte assertions are preserved verbatim; a genuine persistence failure still
trips them. Validated: idle 4/4 + 112 concurrent runs, 0 failures.
…py sweep

#5555/#5556 intentionally reworded the nudge-appended notes (FIRST_SIGHT_DRAIN_NOTE/_EXTERNAL, SUMMARY_BUDGET_NOTE), the retrieve-tool description, the absorb/decompress receipts, the acp-status empty-ranges line, and the image-rescale note, but regenerated only the wire-contract goldens. The storage-render suite pins the persisted nudge/state digest, so all four wires drifted deterministically (blockContents unchanged; only state/stats/metadata moved) and reded 8 test shards on master d244fff.

Regenerated tests/golden/storage-render/{chat,anthropic,responses,google}.json via scripts/update-storage-render-goldens.ts under its parity pins (BILI_IMAGE_RESCALE=0, BILI_SNAPSHOT_SHADOW=0, BILI_MESSAGE_TIMESTAMPS=0). Local regen stateDigests reproduce CI's actual drifted digests exactly (chat b7ba6bb1, anthropic e0e58faa, responses 134a78c7, google 9e9ddc00) => deterministic, not flake.

Sentinel convergence onto ci-fixes for the master-red (#4599); flows to master via #5358.
…tbound OpenAI wire

A fork child whose parent held multimodal/thinking content cannot be textually
compared, so adoption degrades to a full raw-history rebuild whose unrecovered
tool-call cores serialize as empty function.arguments - a hard provider 400
(InvalidArgument) that compaction/preflight cannot rescue, bricking the child.
Enforce the provider contract at the two OpenAI finalize sites (proxy prepare +
loop adapter), mirroring MALFORMED_PROXY_TOOL_ARGS_REPLAY: valid arguments pass
byte-exact, only empty/non-object JSON is swapped for a parseable placeholder.
When durable storage is legacy-only (json) there are no unified tail rows, so
the fork raw-tail reconstruction bails permanently - but it reported the
transient 'sidecar-missing' reason, mislabeling a structural gap as a
recoverable open failure. Report the actionable cause instead.
@github-actions

github-actions Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

📦 Built Package Artifact

Branch: 2026-11-03_fork-multimodal-adoption (836c127)

Latest build — install this exact version

npm install -g billion-context@0.0.7992-pr.5602.420ace1

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
836c127 billion-context@0.0.7992-pr.5602.420ace1 build
c156bc0 billion-context@0.0.7988-pr.5602.9385dbb build
0964efe billion-context@0.0.7981-pr.5602.96a5106 build

Download artifact

  1. Download the artifact from the Actions run
  2. Extract the tarball and install:
tar xzf billion-context-pr5602.tgz
npm install -g package

Install instructions are refreshed on each push.

@ranxianglei ranxianglei changed the title fix(#5595): keep fork children sendable when raw-history rebuild emits empty tool-call arguments [PR] fix(#5595): keep fork children sendable when raw-history rebuild emits empty tool-call arguments Nov 3, 2026
@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

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

查重:不重复,但与在途 PR #5537(镜像 #5540)同机制家族、存在直接代码冲突 —— 先报这个,因为它影响合入顺序:

  1. 同名函数冲突:[PR] fix: repair non-JSON tool-call arguments at the OpenAI/Responses egress chokepoint (#5534) #5537 在 src/util.ts 新增的导出函数也叫 hardenOpenaiToolCallArguments(插在 hardenOpenaiAssistantContent 之后,与本 PR 同一锚点),签名不同((messages, log, sessionId): T[] vs (messages): {messages, replaced})、谓词不同。两个 PR 都合入 = src/util.ts 重复声明,typecheck 必红。后合入的一方必须解决符号冲突。
  2. 谓词差异(关键):[PR] fix: repair non-JSON tool-call arguments at the OpenAI/Responses egress chokepoint (#5534) #5537 只替换「非空且 JSON.parse 失败」的载荷——空字符串放行(其文档注释的显式边界:latch clip 恒非空)。而本 issue 报告的 400 恰好就是空 arguments(expected value at line 1 column 1)——只合 [PR] fix: repair non-JSON tool-call arguments at the OpenAI/Responses egress chokepoint (#5534) #5537 修不掉 Fork adoption fails on multimodal parents, then degrades into a raw-history rebuild that the provider rejects (400 InvalidArgument) — the forked session stays unusable #5595。本 PR 的 isValidToolCallArguments(必须是 JSON object string)是更严的正确契约,替换集是 [PR] fix: repair non-JSON tool-call arguments at the OpenAI/Responses egress chokepoint (#5534) #5537 的超集。
  3. 覆盖互补:[PR] fix: repair non-JSON tool-call arguments at the OpenAI/Responses egress chokepoint (#5534) #5537 覆盖 prepare-openai 双路径(含 catch-fallback 原样转发 parsed.messages 的路径——本 PR 包在 coreToOpenai 调用点上的守卫不覆盖该 fallback)+ prepare-responses + nudge-decide 侧体;本 PR 覆盖 loop adapter 出口([PR] fix: repair non-JSON tool-call arguments at the OpenAI/Responses egress chokepoint (#5534) #5537 审核中 🔶 决策项 A/B 正在等 owner 拍板的那处 adapter-openai.ts:247)。建议见最终八问回复(收敛为单一共享 guard + 单一汇聚点)。

标签:bug / dsh / scope/wiring / p1(fork 子会话永久不可用 = owner 2026-10-19 修订的「fork 继承被永久放弃」类 p1;车道按报告环境取 dsh)。

CI 状态:head 0964efe06 多分片红 = 继承性 master 红(#4599:master d244ffff2 缺 storage-render golden 再生,修复已在 ci-fixes 4809a831c;与 #5601/#5597 同签名)。将按协议 merge origin/ci-fixes 进分支并推送。

初审已发现一处需修的小问题(F4):liveStorageStamp === "legacy" 在 !replicasAvailable() 分支内恒为真(stamp 的定义里 shadow 缺失/禁用就返回 legacy)——F4 三元式的 else 分支是死代码,结果是所有 replicasUnavailable reconMiss(包括配置了 full/shadow 但 sidecar 瞬态宕机的实例)都会被打上「durable storage is legacy-only」的新文案,把可恢复故障误标成结构性缺口——正好和 F4 要修的问题相反。修法已想清(按构造期解析的配置模式判别),将按小问题直接修纪律以独立标注 commit 推上分支。

完整八问结论 + 本地验证结果随后补一条。

@ranxianglei ranxianglei added bug Something isn't working dsh p1 labels Nov 3, 2026
ci-check added 2 commits November 3, 2026 17:48
…rage stamp

Review follow-up on 0964efe: inside the !replicasAvailable() branch,
liveStorageStamp === "legacy" is always true (the stamp reads legacy
whenever the sidecar is absent/disabled), so the previous ternary's else
branch was dead code and every unified-configured instance with a down
sidecar (3-strike latch, corrupt-index boot-without-index) got stamped
"durable storage is legacy-only" — mislabeling exactly the recoverable/
latched cases F4 exists to tell apart.

Discriminate on the constructor-time resolution instead: new
SessionStore.configuredUnified getter (enabled AND knob tier non-legacy AND
no encryption hard-block; the block flag is now stored as a ctor-time field
instead of a local const). Structurally legacy-only instances keep the
actionable config message; unified-configured instances whose sidecar is
down right now keep the #5188 sidecarDownReason() vocabulary.

Tests: three store-level pins appended to
tests/issue5595-fork-empty-tool-args.test.ts (legacy knob / full with a
3-strike-latched sidecar via the #4942 seam / encryption hard-block),
including the conflation assertion itself (live stamp reads "legacy" while
configuredUnified stays true).
@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

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

Review 结论:可合(等新 head c156bc049 CI 转绿)+ 两个合并期协调事项(见末尾)。首条回复已给查重结论(不重复;与在途 #5537 同家族、有符号冲突与谓词差异),标签已打齐 bug/dsh/scope/wiring/p1。本轮 review 中我在 PR 分支补了一个标注独立 commit bb1b6e01c(小问题直接修,见 §4),非静默改动。

八问

  1. 是什么问题:父会话含图片/思考块时,fork 子会话无法走快照比对继承,退化成全量 raw-history rebuild;rebuild 把未恢复的 tool-call core 序列化成空 function.arguments,OpenAI 兼容网关直接 400(must be a valid JSON object string)——压缩/预检都救不了(是校验失败不是体积失败),fork 出来的会话永久不可用。附带:legacy 存储下该路径的失败诊断把结构性缺口误报成可恢复的 sidecar-missing。
  2. 什么原因导致的:①rebuild 出口不强制 provider 的「arguments 必须是合法 JSON 对象串」契约——内核侧各 wire 序列化器都没做(Responses/Anthropic/Google 同款缺陷,作者按 Kernel Boundary 留给人工门,合理);本 PR 在宿主拥有的两个 OpenAI egress 收口点兜住。②诊断面用「活体存储戳」(liveStorageStamp)去做需要「配置档位」判断的事。PR 原版还有更深一层:在 !replicasAvailable() 分支里该戳恒等于 legacy(replicasAvailable=false ⟺ sidecar 缺失或被熔断 ⟹ 戳读 legacy,纯布尔代数、无 await 窗口),三元 else 分支是死代码——即所有 unified 配置但 sidecar 当轮不可用的实例(NFS 尖峰、三连熔断、坏索引 boot-without-index)也会被盖上「durable storage is legacy-only」,恰好把 F4 要区分的两类标反了。
  3. 影响范围与严重程度:p1。触发条件:fork 一个含多模态/思考内容的父会话(报告方:dsh threads,bili 0.1.208–0.1.212,OpenAI 兼容网关;§5 另证实超大父会话还叠加 [Bug][pi][preflight] Fork/resume child judges its first requests on the uncalibrated raw estimate: preflight never fires on a 1.86M-token payload, and an estimate-grade emergency baseline (361%) is ignored #5538 的 oversized-rebuild 问题,本 PR 不覆盖那半边)。结果=会话完全不能用,无绕行(除手工移植状态)。诊断缺陷仅日志措辞,影响面窄(unified 配置 + sidecar 当轮挂角的组合)。
  4. 怎么修复的:F1——在宿主组装出站 OpenAI body 的两个收口点(src/server/prepare-openai.ts 代理模式重建、src/loop/adapter-openai.ts 压缩循环重发)统一过 hardenOpenaiToolCallArguments:合法 arguments 逐字节放行,仅空/非对象 JSON 换成可解析占位对象(沿用 core.ts MALFORMED_PROXY_TOOL_ARGS_REPLAY 的 {"_bili":…} 形状先例),一条 warn 记替换数。F4(我修的版本)——新增 SessionStore.configuredUnified(构造期解析:enabled ∧ 档位非 legacy ∧ 无加密硬阻断;阻断标志从局部 const 升为 ctor 字段),plugin.ts 用它区分「结构性 legacy-only」(给可操作的配置指引)与「unified 配置但 sidecar 当轮不可用」(保留 [PR] fix(#5140): name the failed raw-tail reconstruction gate instead of failing closed silently #5188 的 sidecarDownReason 词表)。
  5. 是否有回归问题:审过的既有行为逐项:合法 arguments 字节不变(测试钉了 gate-negative);模型产出的非 JSON 参数走既有 core.ts replay,其占位串本身是合法 JSON 对象,新守卫对其逐字节放行,无双处理;RECON_REPLICAS_WAIT_MS=750ms、reconMissGate 记录的 gate 值(决定对外 reason 文案)均未动;persist.ts 只是 const→readonly field 搬家,四个使用点语义不变。本地定向验证:typecheck 干净;issue5595 套件 11/11(原 8 + 我补的 3 个 store 级钉);相邻回归集(issue3661/3926/loop-adapters/issue5140-cold-raw-tail-fork/fork-replica-convergence/persist-tail/acp-status-storage-degraded/dsh-fork-adopt/plugin-render-endpoint)= 52+43 全绿。已知边界(非回归):prepare-openai 的 catch-fallback 路径(processTurn 抛错后原样转发客户端 messages)不在本 PR 守卫内——那条路由 [PR] fix: repair non-JSON tool-call arguments at the OpenAI/Responses egress chokepoint (#5534) #5537 覆盖,两 PR 合起来才全。
  6. 是否破坏缓存:否。账本键/前缀接缝/去重全未触碰;占位只改那些必然导致 400 的字节。
  7. 是否改了设计方向:否。宿主边界兜底,镜像既有先例;全 wire 根治留在 kernel(人工门)。
  8. 是否引入新配置:否。

行为变更披露(旧→新):①出站 OpenAI body 中空/非对象 JSON 的 tool_calls[].function.arguments:旧=原样发出→硬 400 砖死;新={"_bili":…} 占位→请求被接受,工具结果可见占位说明。合法参数零变化。②fork-inheritance-unavailable 日志 detail:master 一律 sidecar-missing;PR 原版把所有人改成 legacy-only 指引;最终态(bb1b6e0)= 结构性 legacy-only 给配置指引,unified 配置但 sidecar 不可用保留 sidecar-missing/sidecar-disabled。纯日志/诊断文本,无协议/阈值/时序变化。

我补的 commit bb1b6e01c(+133/−9,persist.ts/plugin.ts/测试):理由如上 §2②——死代码分支 + 标反方向,属「修法显而易见的几行级缺陷」,按纪律直接在 PR 分支修掉并独立标注;三个 store 级测试分别钉住 legacy 档 / full 档+三连熔断(经 #4942 seam,并断言了戳混淆本身:活戳读 legacy 而 configuredUnified=true)/ 加密硬阻断三种情形。

合并期协调事项(两条,均不阻塞本 PR 单独成立):

CI:新 head c156bc049 = 原两提交 + bb1b6e0 + merge origin/ci-fixes(master d244fff 的 storage-render golden 继承红,已在 #4599 报备一行)。本地按 CI preload 链(--import ./tests/preload-test-env.mjs)验证 golden-storage-render + storage-engine-proofs 17/17 绿;全量裁决以远端 CI 为准。

收工判定:代码与行为审毕,可合;等 c156bc0 CI 绿 + 上述两条协调事项落定(建议 owner 拍板 #5537 合并顺序)。

@ranxianglei

Copy link
Copy Markdown
Owner Author

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

结论:REQUEST_CHANGES —— 一个阻塞项(CI 红即该问题,非继承红);其余通过。

阻塞发现

src/loop/adapter-openai.ts:247 — buildRequest 处的硬化打破了 #1039 字节保真语料。

修法(选项 A,推荐):删掉 src/loop/adapter-openai.ts:247-248 的 hardenOpenaiToolCallArguments 调用(恢复钉死的契约),保留 src/server/prepare-openai.ts:378 的守卫(覆盖所报复现),并调整/移除编码了违规行为的 "#5595 wiring: buildRequest heals…" 测试。若 owner 希望连 loop 路径一起覆盖(选项 B),则需对行为变更(loop re-request 重建旧值 '' → 新值占位符)明确签核 + 带理由更新语料 pin——见下方 🔶 决策评论,不要按现状合并。

非阻塞发现(已验证,本分支无需动作)

  1. F4 判别修复(bb1b6e0)正确。 独立证实了原缺陷:!store.replicasAvailable() 分支内 liveStorageStamp 恒为 "legacy"(persist.ts 的 liveStorageStamp 把死掉的 sidecar 塌缩为 legacy),else 分支是死代码,full/shadow 实例的瞬态 sidecar 故障会被误标成结构性 legacy-only。落地的 configuredUnified getter(enabled && mode≠legacy && !encryption-blocked)判别正确,并补了含混淆断言的 3 个 store 级 pin。本地验证:typecheck 干净、issue5595 套件 11/11、plugin-fork 套件 56/56。(本审核轮并行做过等价修复,发现 bb1b6e0 先落地且多覆盖加密阻断/持久化禁用场景后,弃掉重复提交未推送。)
  2. Responses wire 残留缺口已妥善路由:kernel/src/wire/responses.ts:699/978 的 arguments: message.text ?? "" 存在同源暴露;Anthropic/Google 安全降级(safeParse/jsonValue),受影响的是 OpenAI+Responses 一对。已在 Fork adoption fails on multimodal parents, then degrades into a raw-history rebuild that the provider rejects (400 InvalidArgument) — the forked session stays unusable #5595 的 🔶 决策评论中跟踪(kernel 级统一守卫的选项 A/B),不扩本 PR 范围。
  3. "Closes Fork adoption fails on multimodal parents, then degrades into a raw-history rebuild that the provider rejects (400 InvalidArgument) — the forked session stays unusable #5595" 成立:选项 A 落地后,终局症状(child 会话被硬 400 永久打死)由 F1+F4 解除;F2(deferred,与在途 [PR] fix(plugin-fork): lift the thinking hard gate — thinking sessions can fork (#4266) #4900 在 src/plugin.ts:1264 冲突)/F3/F5 作为质量/架构后续留在同一决策评论跟踪。

回归审查(diff 触碰路径的既有行为,逐项结论)

触碰面 既有行为 本 PR 后 判定
prepare-openai 出站重建 逐字转发 kernel 序列化(含 "" arguments) 仅空/非对象 JSON 换占位符,合法 arguments 字节精确 有意变更,PR 正文已披露 ✓
adapter-openai buildRequest(loop re-request) 字节精确序列化器(语料 pin,#1039/#1960) 改写空/不可解析 arguments 无意回归 — 须回退 ✗
fork raw-tail bail detail sidecar-missing(瞬态措辞) configured legacy → actionable 句,否则 sidecarDownReason 有意的诊断变更,已披露 ✓
前缀缓存 — 合法 arguments 不变 ⇒ cache-neutral ✓
其他 wire / streaming 路径 不变 不变 ✓(responses 残留路由至 #5595)

八问

  1. 是什么问题:多模态父会话 fork adoption 失败后,子会话靠 raw-history 重建,其中工具调用的 arguments 序列化为空串,严格 OpenAI 兼容网关整请求拒收(400 InvalidArgument),fork 出的会话永久不可用。
  2. 什么原因导致的:kernel 的 OpenAI 序列化对 unrecovered tool-call cores 发 arguments: m.text ?? ""(kernel-owned,human-gated,本 PR 正确地未触碰);叠加 legacy-only 存储下 fork 重建必然 bail,子会话被强制走 raw-rebuild 路径,空 arguments 撞上网关校验。
  3. 影响范围与严重程度:p1 可用性——storage.unified=legacy 下的一切 fork(F2/F3 之前还包括一切多模态父会话的 fork)产出的子会话第一轮请求即死;无数据丢失,但不迁移出 legacy 存储该能力不可用。
  4. 怎么修复的:在 bili 拥有的出站边界(prepare-openai 重建)强制网关契约——仅把空/非对象 JSON 的 arguments 换成可解析占位符 + 一条 warn 日志;永久性 legacy 存储 bail 改为自解释(按配置模式而非存活戳判别)。
  5. 是否有回归问题:一处——第二个守卫点(adapter-openai buildRequest)打破 [bug] 插件透传会剥掉工具调用参数里的 render tag(arguments 流挂了 tag-echo 过滤器) #1039 字节保真语料(3 subtest × 3 shard,本地复现 3/674)。其余审计项为有意/已披露或中性(见上表)。
  6. 是否破坏缓存:否——合法 arguments 字节精确通过;占位符只出现在原字节本就发不出的位置;前缀缓存中性。
  7. 是否改了设计方向:否——宿主侧边界守卫,镜像既有 MALFORMED_PROXY_TOOL_ARGS_REPLAY 先例(core.ts:1282);kernel 未动;Responses wire 扩展留给 owner 决策(Fork adoption fails on multimodal parents, then degrades into a raw-history rebuild that the provider rejects (400 InvalidArgument) — the forked session stays unusable #5595)。
  8. 是否引入了新的配置:无。无 env/config/CLI 面新增。

合并判定:现状不可合。 按选项 A 回退 adapter 侧守卫后分支其余部分干净——F1+F4 正确、双向测试齐全、覆盖所报复现。选项 B 需 owner 签核(见 🔶 评论)。

@ranxianglei

Copy link
Copy Markdown
Owner Author

🔶 需要决策

PR #5602(head c156bc0)被卡 diff:src/loop/adapter-openai.ts:247-248(buildRequest,即 loop re-request 路径)的 hardenOpenaiToolCallArguments 调用 vs tests/toolargs-adversarial.test.ts section C 的字节精确 pin(master 提交 4e22b1a,#1039/#1960:"arguments are user intent — every payload a host will EXECUTE or PERSIST must travel verbatim in EVERY direction on EVERY wire",空串/空白/不可解析载荷被明确列为必须原样通过的对抗用例)。两者当前直接矛盾——CI 3 shard 红 = 本地复现 3/674 挂。该取舍是教义级(#1039 逐字保真 vs 严格网关可用性),按审核纪律不由 reviewer 代决。

选项:

请裁定 A/B。(Responses wire 同源暴露 kernel/src/wire/responses.ts:699/978 已在 #5595 的决策评论单独跟踪,不并入本裁定。)

@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

[bot] 🏷 审核完成 → REQUEST_CHANGES(现状不可合),已走 🔶 决策流。

结论:PR #5602(fix #5595 fork 空 tool-call arguments)有一个阻塞项,其余通过。

投递:完整审核 → PR #5602 评论;🔶 需要决策 评论 + needs-decision 标签(本线程);一行报告 → issue #5595。等 owner 对 A/B 拍板 + 作者回退 adapter 侧守卫后复审。

@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

[bot] 🏷 收到——这条是审核轮在 PR #5602 上发的 🔶 决策请求同步回本线程(非新增信息)。当前状态不变:PR #5602 拦下等 owner 裁定 A/B(A = 删 adapter 侧守卫、保留 prepare-openai 守卫,推荐;B = 双点守卫 + 更新语料 pin,需签核)。裁定前本 lane 不动手;作者回退后我会复审。

@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

[bot] 🏷 这条是完整审核结论从 PR #5602 同步回本线程(非新增信息)。状态不变:REQUEST_CHANGES,等 owner 裁定 A/B(A 推荐)。裁定 + 作者回退 adapter 侧守卫后复审。

…in prepare only

Owner review of PR #5602: the hardenOpenaiToolCallArguments call added to the openai LOOP adapter's buildRequest broke #1039's tool-args-verbatim corpus (toolargs-adversarial 'C chat rebuild verbatim': bare empty / whitespace-only / unparseable-truncated args must ride byte-exact). The host re-sends its own history on the loop re-request, so any rewrite desyncs the session (#1039/#1960).

Remove the guard from the adapter (loop path stays verbatim); it remains in the proxy-mode prepare path (prepare-openai), which covers this issue's fork-rebuild repro. Replace the now-invalid adapter-heals wiring test with a boundary pin asserting the loop forwards degenerate args verbatim. Builds on bb1b6e0's configuredUnified F4 refinement (kept) and its three F4 store-level pins.

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

1 participant