Skip to content

[PR] fix: insert Anthropic ephemeral tails before trailing client systems (#5432) - #5441

Merged
ranxianglei merged 4 commits into
masterfrom
2026-11-01_anthropic-trailing-system-tails
Nov 11, 2026
Merged

ranxianglei merged 4 commits into
masterfrom
2026-11-01_anthropic-trailing-system-tails

Conversation

@ranxianglei

@ranxianglei ranxianglei commented Nov 1, 2026 •

Copy link
Copy Markdown
Owner

What this fixes

Host-side half of #5432 — every place bili appends or mutates a trailing user message into an Anthropic-wire body after position healing has run. Two 400s were observed (claude-code lane, proxy mode, upstream Anthropic with mid-conversation-tool-changes-2026-07-01), e.g. ~0.5 s after every nudge INJECT:

messages.N: role 'system' must precede an 'assistant' message or end the array

Root cause: Anthropic's mid-conversation system rule is two-sided — an entry must FOLLOW a user message (or an assistant ending in a server tool result) AND PRECEDE an assistant message or end the array (directive-only systems exempt). When the client history ends in a legal trailing system (Claude Code's "The user hasn't heard from you…" reminder; field reports from the claude-code and omp lanes both show host requests ending in such an entry), any user appended behind it makes the array illegal and the whole request 400s. Four host-side producers did exactly that:

  1. Steady path src/server/prepare-anthropic.ts — seven ephemeral-appends (sysNotes / nudge fallback / legacy nudge / decide prompt / decide directive / statusLine carrier / retrieval notes) after coreToAnthropic().
  2. Round-2 compress-loop adapter src/loop/adapter-anthropic.ts — its notes append.
  3. Post-hoc body mutation src/wire-body.ts appendTrailingUserText (continuation / fake-completion retries) — pushed the continuation text at array end.
  4. Optional chain-checkpoint carrier src/chain-checkpoint.ts insertCheckpointCarrier — its frozen contract (design: Chain Checkpoint — first-processor-wins idempotent interop (Step 2/3) #1395 decision (a) / [design] chain checkpoint step 3: carrier stamp shape on role-alternation-strict wires (anthropic/google) #1421) admits the carrier ONLY inside "the trailing run of user-role entries after the last non-user entry"; a system-final body has no legal slot for it, so stamping now skips such bodies (returns null → relay forwards unstamped).

Fix

Injected users land before any trailing run of system messages instead of after it, via the kernel's exported insertBeforeTrailingSystemRun (kernel/src/wire/anthropic.ts, #5438/#5432 — single source of truth for the rule): sites 1–3 route through it; site 4 refuses to mutate (skip-stamp). A small host-side trailingSystemRunStart(messages) in src/util.ts supplies the run index for the two splice/null-check sites that have no kernel counterpart. The client's systems stay verbatim and still end the array (legal); without a trailing run the result is byte-identical to a plain append. Sites 1–2 additionally keep master's #4570 heal-backstop as the final pass over the array actually sent (union of intent from the rebase conflict resolution); the decide side call also keeps its #5688 output_config dedupe ahead of the insertion.

Kernel half status (case 1)

The follow-side heal requested in #5438 was approved and already landed on master: ca532d4f3 (two-sided healIllegalSystemPositions) + f06619ffc (legitimates the real *_tool_result server-block family on the follow side), with a dedicated regression suite (kernel/tests/issue5432-follow-side-heal.test.ts). Case 1 (must follow a 'user' message after a fold orphaned the sidecar) is covered there; nothing in this PR depends on further kernel work. Note: the originally planned "post-merge follow-up PR swapping the host helper for the kernel export" is absorbed into this PR — since the kernel export is on master, the interim host copy was deleted here instead (one less PR).

Behavior change (disclosed per policy)

  • Ephemeral tail injections (steady path + adapter): old = appended at the very end of outbound messages; new = inserted before a trailing system run when one exists. Bodies whose rebuilt history does NOT end in a system run emit byte-identical output (negative tests pin this). Cache marks ride on the last stable message inside coreToAnthropic, so breakpoints are untouched.
  • wire-body: anthropic protocol only — same before-run insertion; the openai/responses/google branches are byte-for-byte unchanged (a pin test asserts the openai path still plain-appends, since those wires have no such rule).
  • chain-checkpoint: old = a system-final body got the carrier user appended AFTER the trailing system run (the exact illegal shape that 400s); new = stamping returns null for those bodies and the relay forwards them unstamped (its existing graceful path). Only reachable with egress stamping enabled (opt-in, default off).

Tests

New tests/issue5432-trailing-system-tails.test.ts (13 tests, both directions per #3617 discipline):

  • helper unit: insert before a multi-entry trailing run / plain append when no run / empty additions same-ref / all-system input / sequential-batch order (now exercised against the kernel export directly);
  • round-2 adapter buildRequest: note lands before the trailing client system (verbatim, still last); negative — no trailing system → note dead-last as before. Fixture uses the legal-follow shape required since the two-sided heal landed on master;
  • post-hoc paths: wire-body anthropic inserts before the run (verbatim last) / negative no-run plain-append + user-final merge / openai system-final still plain-appends (no-drift pin) / chain-checkpoint system-final → null skip / assistant-final still gets the appended carrier;
  • real steady path via hermetic server harness: client history ending in a system + armed over-limit nudge → captured upstream body ends in the client's system with the nudge user immediately before it; negative — no trailing system → nudge dead-last.

Local targeted runs green on the rebased head: this file (13/13) plus issue3910 lane-nudge wiring, chain step2/step3, issue3159, issue1683, fake-completion and degenerate-retry families — 89/89 combined; typecheck clean. Full-suite verdict = CI on the pushed head. CI note: cache-proof.test.ts test 7 was red on the previous base independent of this PR (verified in a clean master worktree with its own deps); reported in #4599 and did not reproduce on the prior head's CI run. This head is rebased linearly onto current master (541cf260c4).

No config surface changes. No version bump.

Refs #5432 (host-side half; the kernel half landed via #5438).

@github-actions

github-actions Bot commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

📦 Built Package Artifact

Branch: 2026-11-01_anthropic-trailing-system-tails (7513559)

Latest build — install this exact version

npm install -g billion-context@0.0.8894-pr.5441.978ce32

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
7513559 billion-context@0.0.8894-pr.5441.978ce32 build
aaaa7a9 billion-context@0.0.8660-pr.5441.411a18e build
d0237cd billion-context@0.0.7912-pr.5441.f2ee04b build
9d9a982 billion-context@0.0.7707-pr.5441.a40b5f3 build

Download artifact

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

Install instructions are refreshed on each push.

@ranxianglei ranxianglei changed the title fix: insert Anthropic ephemeral tails before trailing client systems (#5432) [PR] fix: insert Anthropic ephemeral tails before trailing client systems (#5432) Nov 1, 2026
@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

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

查重: 不重复。 与 #5432 / #4570 / #4457 同症状家族(Anthropic system 位置 400),但与家族内其他条目机制不同:本 PR 只修 case 2 —— 宿主侧 ephemeral 尾部注入落在客户端 trailing system run 之后;#4715(open PR)与 #5438(kernel 授权申请,p1)处理的是 case 1 —— kernel healIllegalSystemPositions 的 follow 侧。#5438 已明确规划:批准后由 kernel 版 helper 取代本 PR 的宿主副本(规则单源)。互补关系,非并列重复。

标签: bug claude-code scope/wiring p2(缺陷修复;受影响客户端为 claude-code;宿主侧 wire 排序而非 kernel;严重程度随父 issue #5432 —— nudge 轮硬 400,但客户端自清 mid-conversation systems 后有恢复路径,故 p2 不升 p1)

分支已拉取,代码审核进行中——结构化八问结论随后补上。

@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

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

PR Review 结论 — case-2 宿主侧止血

① 原 issue 方向(最高优先):#5432 case 2 —— 宿主在 kernel coreToAnthropic 位置修复「之后」追加 ephemeral 尾部注入,客户端历史以合法 trailing system run 结尾时,user 落在其后 → Anthropic 400(role 'system' must precede an 'assistant' message or end the array)。根因定位正确,我逐点核对了代码路径(steady-path 7 处 + round-2 adapter 1 处,无遗漏)。修法打在产生侧(插入位置),不是遮症状:客户端字节零改写,client systems 仍 verbatim 收尾。Case 1(follow 侧)正确留给 #5438 的 kernel 审批流程,且取代计划已写明(批准后 kernel 版接管、宿主删本地副本)——符合止血例外政策(≤2 PR、根治吸收止血)。

② 回归审查(既有行为清点):

  • prepare-anthropic.ts 7 处 + adapter 1 处:无 trailing system run 时出站字节与旧行为一致(负向测试三层钉住:helper / adapter / 真 server);有 run 时旧行为是必然 400(坏状态),不损失任何原本可用的行为。
  • 其余全部 coreToAnthropic 调用点逐一排查同类缺陷:prepareCountTokens(src/server.ts:3022,无尾部注入)、loop/core.ts:732 buildThreadBody/buildLoopRequestBody(extra 在 encode 前拼入、由 kernel heal 覆盖)、plugin-render.ts:139(展示层非上游流量)、incremental-lane.ts(无 ephemeral 尾)——均安全。OpenAI 车道(prepare-openai.ts 同款 7 处)有意不动:该约束是 Anthropic wire 专属。
  • 无下游消费者假设「末元素必为 ephemeral 注入」(rebuiltMessages 上无 .at(-1)/length-1 读取);warnAnthropicThinkingPairs(src/server.ts:2534)是 id-set 判定,位置无关。
  • 高危模式逐项过:披露范围外无语义漂移、无 fail-fast→吞错、无阈值/时序/输出格式变化、无缓存/重试策略变化。
  • diff 整洁:4 文件全部与目的相关,无 lockfile/生成物/版本 bump/kernel 变更;分支落后 master 仅 [PR] (ai: anthropic/claude-opus-4-6) fix(release): nuclear box = deps download + build + publish, zero checks; add local zero-install publish script #5436(release 路径合并)零文件重叠,mergeable=true(BEHIND 非阻塞)。

③ 代码:helper 最小且正确——空 additions 返回原数组(identity 不复制);从尾部回扫连续 system 在其前插入;全 system 输入 additions 居首(合法形状);顺序批保持相对序。本地 typecheck 干净。helper 不校验 additions 必为 user,但调用点全传 user 且函数名已声明意图,不算缺陷。

④ 测试有效性:8 个测试双向(#3617 纪律)——正向钉新位置、负向钉 legacy dead-last 行为,断言有牙(回退修法则 steady-path 正向必挂:末元素变成 nudge user 而非 client system)。steady-path 走真 server(startServer hermetic harness + 捕获的 upstream body),不是对受测逻辑打 mock;两轮 arm+inject 设计沿用 #3910 纪律。本地定向跑:新文件 8/8 + issue3910/issue1470/loop-adapters/issue2499 合计 52/52 绿。


结构化八问:

  1. 是什么问题:claude-code 代理模式会话,历史以 system 消息结尾时,每次 nudge 注入后请求硬 400,直到客户端自清 mid-conversation systems。
  2. 什么原因导致的:Anthropic mid-conversation system 规则是双侧的(FOLLOW user 且 PRECEDE assistant 或收尾),而宿主 ephemeral 尾部注入在位置修复之后追加——把 user 放进了合法 trailing system run 的后面。
  3. 影响范围与严重程度:claude-code 车道(proxy mode)+ Anthropic 上游 + 历史尾 system + nudge armed 才触发;触发后后续请求持续 400 直至客户端自清(现场观察到约 1 分钟自恢复)→ p2(随父 issue Anthropic 400: mid-conversation system must FOLLOW a user message — heal only checks the follower, and trailing nudge/notes land after a trailing system #5432)。
  4. 怎么修复的:单一 helper 把 8 处注入从「数组最末」改为「trailing system run 之前」;client systems 保持 verbatim 收尾,顺带修复该 run 的 follow 侧(前驱为 assistant 时)。kernel 半(case 1)等 kernel authorization request: heal must enforce the FOLLOW side of Anthropic's mid-conversation system position rule (#5432) #5438 批准。
  5. 是否有回归问题:无。审过的既有行为:无 trailing run 时逐字节不变(负向测试钉住);有 run 时旧行为即 400;无其他消费者依赖注入位置;CI guards 全绿(kernel-guard / version-guard / config-surface-guard / pi-subagents-guard / lifecycle-ledger-guard / unused-exports)。
  6. 是否破坏缓存:否。cache marks 骑在 coreToAnthropic 内最后一条稳定消息上,断点不动;ephemeral 尾本来就每轮变化(此前也追加在最末),位置移动不引入新的失效。
  7. 是否改了设计方向:否。宿主侧止血,规则所有权仍在 kernel——kernel authorization request: heal must enforce the FOLLOW side of Anthropic's mid-conversation system position rule (#5432) #5438 批准后规则单源化、宿主副本删除。
  8. 是否引入新配置:无。

CI:运行中(本条发出时 0 failed / 13 pending / guards 全绿;full-suite 判定以此 head 的 test lanes 为准)。若任一 test lane 红,按 #4599 跟进。

判定:可合(wire/message-shape 变更按 §7.4 属 human-only,请 owner 在 CI 转绿后自行合并)。

@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

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

CI 已全绿(head 9d9a982,30 项 check:12 success / 18 skipped〔zero-token 真实客户端车道,设计内跳过〕/ 0 failed)。判定不变:可合,等 owner 合并。

@ranxianglei

Copy link
Copy Markdown
Owner Author

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

说明:GitHub 禁止对本人账号提交的 PR 发 APPROVE 事件(本 PR 以 ranxianglei 账号提交,422),故以评论形式给出等价评审结论;正式批准需 owner 手动操作。

独立复审 @ head d0237cdba — APPROVE

方向:#5432 case 2 的根因定位成立。Anthropic mid-conversation system 规则是双侧的(FOLLOW 一个 user,且 PRECEDE assistant 或收尾);宿主在 coreToAnthropic() 位置修复「之后」追加 ephemeral 尾 user,客户端历史以合法 trailing system run 结尾时 user 落在其后 → 硬 400。修法打在产生侧(插入位置),客户端字节零改写、client systems 仍 verbatim 收尾。

完备性核对(逐点自查):anthropic wire 上所有 post-assembly 变更点均已覆盖——prepare-anthropic.ts L490/524/534/541/557/577/587(7 处)+ adapter-anthropic.ts L316 + wire-body.ts L71(splice 形态)+ chain-checkpoint.ts L518(guard 形态)。其余 spread-append 站点全在 openai/responses 车道(该约束是 Anthropic mid-conversation-tool-changes 专属;openai legacy 行为有负向测试钉住)。无遗漏。

kernel 边界:PR 自身 diff 6 个文件,无一在 kernel/ 下(ci-fixes 合并为空合并,零净文件变化),kernel-guard 通过。kernel 侧双侧修复(#5438/#5522)已合入 master 且是本分支祖先——fixture pin 提交(d0237cdba)的前提成立。kernel 已导出自己的 insertBeforeTrailingSystemRun,宿主副本是有文档记载的过渡期重复(两侧注释一致:双端合并后删宿主副本、规则单源化)。

chain-checkpoint guard(post-hoc fix):对照 #1395 冻结的 carrier 契约核实——carrier 只在 trailing USER run 内被识别;system-final body 插到 run 之前永远不会被再识别,append 到 run 之后正是 400 形状本身,故返回 null 是唯一与契约一致的选择。relay.ts 对 null 处理正常(unstamped 转发,best-effort);egress stamping 是 opt-in(chainEgressStamp 默认 OFF),影响面有限。

测试:最终 head 共 13 个顶层测试,双向断言有牙——任何一处退回 plain append,正向用例必挂(末元素必须是 client system verbatim、次末是 nudge user);steady path 走真 server harness 捕获 upstream body,不是对受测逻辑打 mock。

本地验证(定向,全量归 CI):build 干净(dist-import-annotations / dist-external OK)· typecheck 干净 · 定向跑 42/42(新文件 + chain-checkpoint-step2/3 + util-surrogate-clamp)。

CI:exact head d0237cdba,30 checks,0 failed / 0 pending(镜像缓存读取)。

回归审查(既有行为清点 + 结论):

  • 无 trailing system run → 出站字节与旧行为完全一致(helper / adapter / 真 server 三层负向钉住)——保持;
  • 有 trailing run → 旧行为必然 400(坏状态),不损失任何原本可用的行为——有意变更,body 已披露旧值→新值+原因;
  • 缓存:cache marks 骑在 coreToAnthropic 内最后一条稳定消息上、断点不动;ephemeral 尾本来就每轮变位置(此前也追加在最末),未引入新失效——不破坏;
  • 无下游消费者假设「末元素 = ephemeral 注入」(rebuiltMessages 上无 .at(-1)/[length-1] 读取,grep 核实);warnAnthropicThinkingPairs(src/server.ts:2565)是 id-set 成员判定、位置无关——保持;
  • 高危模式逐项过:披露范围外无语义漂移、无 fail-fast→吞错、无阈值/时序/输出格式漂移、无配置面变更、无版本 bump。

信息项(非阻塞,代码未动):

结构化八问:

  1. 是什么问题:claude-code 代理模式会话,历史以 system 消息结尾时,每次 nudge/notes 注入后请求硬 400,直到客户端自清 mid-conversation systems。
  2. 什么原因导致的:system 位置规则是双侧的,而宿主 ephemeral 尾注入发生在位置修复之后,把 user 放进了合法 trailing system run 的后面。
  3. 影响范围与严重程度:claude-code 车道 + Anthropic 上游 + 历史尾 system + nudge armed 同时满足才触发;触发后后续请求持续 400 直至自清(现场观察到约分钟级)→ p2(随父 issue Anthropic 400: mid-conversation system must FOLLOW a user message — heal only checks the follower, and trailing nudge/notes land after a trailing system #5432)。
  4. 怎么修复的:单一 helper 把全部 8 处注入从「数组最末」改为「trailing system run 之前」;client systems verbatim 收尾,前驱为 assistant 时顺带修复该 run 的 follow 侧;kernel 半(case 1)已经 kernel authorization request: heal must enforce the FOLLOW side of Anthropic's mid-conversation system position rule (#5432) #5438 批准单独合入。
  5. 是否有回归问题:无(详见上方回归审查清单逐项结论)。
  6. 是否破坏缓存:否。
  7. 是否改了设计方向:否。宿主侧止血,规则所有权仍在 kernel,单源化计划不变。
  8. 是否引入新配置:无。

判定:可合。 Wire/message-shape 变更按 §7.4 属 human-only 合并——请 owner 自行合并。

@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

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

复审完成:对 head d0237cdba 做了独立复审——判定可合(APPROVE 等价);GitHub 禁止自批本人账号的 PR(422),正式 APPROVE 事件无法发出,完整八问结论见上方评论。CI 在 exact head 全绿(30 checks / 0 failed),本地 build + typecheck + 定向测试 42/42;无回归、不破坏缓存、无配置面/版本/kernel 变更。无顺手修项;两个信息项已备注在评审里(PR body “8 tests” 为过期计数,最终 head 实际 13;#5522 既已合入,「宿主副本换 kernel import」的收敛小 PR 现在可以开)。待 owner 手动批准合并(§7.4 wire-shape 变更 human-only)。

@0xTidalBore

Copy link
Copy Markdown

(ai: anthropic/claude)

Independent e2e A/B verification of #5441 (head d0237cd) against a repro of #5432. Posting evidence only; no change requested.

Field symptom on 0.1.212. Claude Code 2.1.309 remote session (CLAUDE_CODE_ENTRYPOINT=claude-desktop) through a bili start daemon (MITM api.anthropic.com, plugin mode via the MCP face register), claude-sonnet-5-5: upstream 400 messages.11: role 'system' must precede an 'assistant' message or end the array … about 0.3 s after nudge INJECT T1. That request carried a UserPromptSubmit hook block: Claude Code renders hook additionalContext as a mid-conversation system entry right after the user turn.

Repro.

  • A mock /v1/messages on 127.0.0.1 that enforces the precede side of the rule and streams a fixed reply.
  • A driver that registers plugin identity (POST /__bili/plugin/register, agent:"mcp") and sends a Claude-Code-shaped body: a global-scope system block, mcp__bili__* tools on the wire, and x-claude-code-session-id. It grows a Read / tool_result conversation for 40 turns at about 3K tokens per turn.
  • Modes: hooks = a hook system entry after every user turn; start = after the first turn only; nohooks = none.
  • Each arm gets a fresh port, sessions dir and log. The mock upstream's window is pinned to 200000 in config.
  • Environment: Ubuntu 22.04.5, Node 22.22.3. 0.1.212 = the npm tarball (daa082ea); [PR] fix: insert Anthropic ephemeral tails before trailing client systems (#5432) #5441 = d0237cd, built locally with npm run build.
build mode requests 400 (illegal system position) nudge INJECT 200s with [plugin] [acp-usage]
0.1.212 hooks 40 15 5 25
0.1.212 start 40 0 3 40
0.1.212 nohooks 40 0 3 40
#5441 hooks 40 0 3 40
#5441 start 40 0 3 40
#5441 nohooks 40 0 3 40

On 0.1.212, every rejected body ends … user(text) · system(hook) · user(string nudge): the nudge turn is appended behind a legal trailing system. With #5441, the same driver produces no illegal position, and nudges still fire.

Tests. On the PR checkout, node --import tsx --test tests/issue5432-trailing-system-tails.test.ts → 13/13 pass. The PR's CI is green; mergeStateStatus is currently BEHIND master.

Caveats.

Conclusion: #5441 fixes the trailing-system + nudge-append shape from #5432 in this environment.

mock_anthropic.py
#!/usr/bin/env python3
# Mock Anthropic /v1/messages: validates role order like the real API, logs each request's shape (roles only), streams a tiny reply.
import json, sys, time
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
LOG = sys.argv[2] if len(sys.argv) > 2 else "mock.log"
def violations(msgs):
    out = []
    for i, m in enumerate(msgs):
        if m.get("role") != "system": continue
        if isinstance(m.get("content"), list) and len(m["content"]) == 0 and m.get("output_config") is not None: continue
        nxt = msgs[i+1] if i+1 < len(msgs) else None
        if nxt is not None and nxt.get("role") != "assistant":
            out.append(f"messages.{i}: role 'system' must precede an 'assistant' message or end the array")
    return out
def shape(msgs):
    s = []
    for m in msgs:
        c = m.get("content"); kinds = []
        if isinstance(c, list): kinds = [b.get("type","?") for b in c]
        else: kinds = ["str"]
        s.append(m.get("role","?")[0].upper() + ":" + ",".join(sorted(set(kinds))))
    return s
class H(BaseHTTPRequestHandler):
    def log_message(self, *a): pass
    def do_POST(self):
        n = int(self.headers.get("content-length", 0)); body = json.loads(self.rfile.read(n) or b"{}")
        msgs = body.get("messages", []); v = violations(msgs)
        tools = [t.get("name") for t in body.get("tools", []) if isinstance(t, dict)]
        rec = {"t": time.strftime("%H:%M:%S"), "n": len(msgs), "bytes": n, "violations": v, "shape": shape(msgs),
               "bili_tools": [t for t in tools if t and ("compress" in t or "acp" in t or "context" in t)],
               "sys_global": any(isinstance(b, dict) and (b.get("cache_control") or {}).get("scope") == "global" for b in (body.get("system") or []) if isinstance(body.get("system"), list))}
        open(LOG, "a").write(json.dumps(rec) + "\n")
        if v:
            err = json.dumps({"type":"error","error":{"type":"invalid_request_error","message": v[0]}}).encode()
            self.send_response(400); self.send_header("content-type","application/json"); self.send_header("content-length",str(len(err))); self.end_headers(); self.wfile.write(err); return
        inp = max(1, n // 4)
        ev = [("message_start", {"type":"message_start","message":{"id":"msg_mock","type":"message","role":"assistant","model":body.get("model"),"content":[],"stop_reason":None,"usage":{"input_tokens":inp,"output_tokens":1,"cache_read_input_tokens":0,"cache_creation_input_tokens":0}}}),
              ("content_block_start", {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}),
              ("content_block_delta", {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Noted. Continuing."}}),
              ("content_block_stop", {"type":"content_block_stop","index":0}),
              ("message_delta", {"type":"message_delta","delta":{"stop_reason":"end_turn"},"usage":{"output_tokens":5}}),
              ("message_stop", {"type":"message_stop"})]
        self.send_response(200); self.send_header("content-type","text/event-stream"); self.end_headers()
        for name, data in ev: self.wfile.write(f"event: {name}\ndata: {json.dumps(data)}\n\n".encode())
ThreadingHTTPServer(("127.0.0.1", int(sys.argv[1])), H).serve_forever()
drive.py (usage: drive.py <bili origin> <upstream origin> <session id> <turns> hooks|start|nohooks)
#!/usr/bin/env python3
# Replays a growing Claude-Code-shaped conversation through a test billion-context instance in plugin mode.
import json, sys, urllib.request, random, string
BILI, UP, SID, TURNS, MODE = sys.argv[1], sys.argv[2], sys.argv[3], int(sys.argv[4]), sys.argv[5]
def post(url, body, headers):
    r = urllib.request.Request(url, data=json.dumps(body).encode(), headers={"content-type":"application/json", **headers}, method="POST")
    try:
        with urllib.request.urlopen(r, timeout=120) as resp: return resp.status, resp.read().decode(errors="ignore")
    except urllib.error.HTTPError as e: return e.code, e.read().decode(errors="ignore")
print("register:", post(f"{BILI}/__bili/plugin/register", {"conversationId": SID, "agent": "mcp", "identity": True}, {})[0])
random.seed(7)
filler = lambda k: " ".join("".join(random.choice(string.ascii_lowercase) for _ in range(7)) for _ in range(k))
system = [{"type":"text","text":"You are Claude Code. " + filler(400),"cache_control":{"type":"ephemeral","scope":"global"}},
          {"type":"text","text":"Environment and CLAUDE.md. " + filler(300),"cache_control":{"type":"ephemeral"}}]
tools = [{"name": n, "description": "tool", "input_schema": {"type":"object","properties":{}}} for n in
         ["Read","Bash","Edit","mcp__bili__compress","mcp__bili__decompress","mcp__bili__search_context","mcp__bili__acp_status"]]
msgs = []
def hook(text): return {"role":"system","content":[{"type":"text","text":text}]}
for t in range(TURNS):
    msgs.append({"role":"user","content":[{"type":"text","text":f"Step {t}: read the next file and summarise it."}]})
    if (MODE == "hooks") or (MODE == "start" and t == 0): msgs.append(hook(("SessionStart hook additional context: <context_window_protection> route big outputs through ctx tools </context_window_protection>" if t == 0 else "UserPromptSubmit hook additional context: <routing> prefer ctx_execute </routing>")))
    body = {"model":"claude-opus-5-5","max_tokens":32000,"stream":True,"system":system,"tools":tools,"messages":msgs}
    code, text = post(f"{BILI}/bili/{UP}/v1/messages", body, {"x-claude-code-session-id": SID, "anthropic-version":"2023-06-01", "x-api-key":"mock-not-a-secret"})
    if code != 200: print(f"turn {t}: HTTP {code} {text[:200]}")
    tid = f"toolu_{t:04d}"
    msgs.append({"role":"assistant","content":[{"type":"text","text":"Reading."},{"type":"tool_use","id":tid,"name":"Read","input":{"path":f"/tmp/f{t}.md"}}]})
    msgs.append({"role":"user","content":[{"type":"tool_result","tool_use_id":tid,"content": filler(1400)}]})
    msgs.append({"role":"assistant","content":[{"type":"text","text":f"File {t} summarised: " + filler(60)}]})
print("done", TURNS, "turns")

@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

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

感谢这份独立 e2e A/B 证据——与本次复审的结论互相印证:

评审结论维持:可合,待 owner 手动批准合并(GitHub 禁止 agent 自批本人账号的 PR)。

ranxianglei pushed a commit to Mako-L/billion-context that referenced this pull request Nov 4, 2026
…i#5438 two-sided heal; keep final-array backstop; kernel 0.0.181)

Conflict resolution: master shipped the sibling fix for this family (ranxianglei#5438 via PR ranxianglei#5522: two-sided heal inside coreToAnthropic, demote-only) plus the insertBeforeTrailingSystemRun helper (host wiring pending in ranxianglei#5441). Take master's approved heal semantics wholesale; keep ranxianglei#4715's residual contribution: the healAnthropicSystemPositions standalone export + the three host final-array call sites + WC-023 pins (ledger entry kept from master's side, one entry per id). Branch move-past-next-user design superseded by the owner-approved ranxianglei#5438 approach. Kernel bumped 0.0.180 -> 0.0.181 (kernel-change-vs-base rule).
@luan-cestari-ppro

Copy link
Copy Markdown
Contributor

Tested on master fe963d7f5 with a Bedrock backend (Claude Code 2.1.311 -> billion-context -> LiteLLM -> AWS Bedrock, Anthropic Messages wire, Claude Haiku 5.5 and Opus 5.5).

Error without the PR

messages.12: role 'system' must precede an 'assistant' message or end the array

Claude Code ends some requests with its silent-turn reminder as a mid-conversation system message ("The user hasn't heard from you in a while ..."). The array-end position is legal. bili then appends its nudge (or another ephemeral note) as a user message after it, and the reminder is no longer at the end.

Rates

Sites on master that append a tail without insertBeforeTrailingSystemRun (all covered by the PR)

  1. src/server/prepare-anthropic.ts: sysNotes, nudge, decide side body, decide directive, status line and retrieval notes ([...rebuiltMessages, X]).
  2. src/loop/adapter-anthropic.ts:320: loop-round notes.
  3. src/wire-body.ts:66: the Anthropic branch of appendTrailingUserText.
  4. src/chain-checkpoint.ts: the checkpoint carrier.

Timing note

In the first case we traced (on 0.1.212), bili logged nudge INJECT, then the upstream 400. The fold for that session came 27 s later. So the 400 came before the fold, and the fold summary is not the cause. Every captured summary goes out in a user message.

Conflicts on fe963d7f5 (both trivial, from #5688 landing after the PR branch):

  1. src/loop/adapter-anthropic.ts imports: keep master's dedupeAdjacentOutputConfig import and add insertBeforeTrailingSystemRun to the ../util.js import.
  2. src/server/prepare-anthropic.ts, decide side body: apply both changes:
messages: insertBeforeTrailingSystemRun(dedupeAdjacentOutputConfig(rebuiltMessages), [{ role: "user", content: buildDecisionPrompt(ranges) }]),

After that, npx tsc --noEmit is clean. The PR test issue5432-trailing-system-tails passes, and the root suite shows no new failures.

@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

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

Thank you for the Bedrock/LiteLLM evidence — it independently corroborates the review verdict on three axes at once: the exact 400 shape (messages.N: role 'system' must precede an 'assistant' message or end the array), the replay delta (9/13 → 0/34), and the full-session rate (this error was 23 of 80 Haiku 400s + 1 Opus 400 on master; 0 across 431+88 requests with #4898+#5441+#5605). Your timing note (the 400 lands ~immediately after nudge INJECT, the fold 27 s later) matches the mechanism analysis: the fold summary is not involved, every captured summary goes out as a user message.

Conflicts: confirmed and resolved. I merged current master (82e2d53c6e, which carries #5688 via #5690) into the PR branch and got exactly the two hunks you identified — no more, no less:

  1. src/loop/adapter-anthropic.ts imports — union: kept master's dedupeAdjacentOutputConfig import line AND added insertBeforeTrailingSystemRun to the ../util.js import.
  2. src/server/prepare-anthropic.ts decide side body — union of both intents: messages: insertBeforeTrailingSystemRun(dedupeAdjacentOutputConfig(rebuiltMessages), [{ role: "user", content: buildDecisionPrompt(ranges) }]), with [Bug] fold reconstruction can emit two adjacent output_config effort directives → deterministic 400 on Anthropic lane (pi thinkingLevel max + OpenRouter) #5688's comment preserved.

I verified the ordering against src/dedupe-adjacent-output-config.ts rather than applying the snippet by trust: the dedupe only collapses adjacent empty-content system directives (later effort wins), while #5441 inserts a non-empty user message before the trailing system run — insertion neither creates nor breaks any system-system adjacency, so the two operations commute; dedupe-then-insert also mirrors what the auto-merged loop round-2 rebuild already does (dedupeAdjacentOutputConfig(coreToAnthropic(...)) then insertBeforeTrailingSystemRun). Your proposed resolution matched this independently derived result.

New head: aaaa7a9887 (merge commit, pushed fast-forward to 2026-11-01_anthropic-trailing-system-tails). Local verification on the merged tree: tsc --noEmit clean, npm run build clean (dist-import-annotations OK, dist-external OK), targeted suites 86/86 pass — issue5432-trailing-system-tails (13/13), both #5688 pins (issue5688-adjacent-output-config, issue5688-egress-wiring), chain-checkpoint-step2/3, loop-adapters. CI will re-run on the new head; full-suite verdict comes from there per house discipline.

Review verdict stands: mergeable, pending owner approval (GitHub blocks self-approval on this account).

@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

[ework] CI fix verified green ✓

@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

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

Closing summary for this review round — post-conflict re-verification complete:

What was done. The two trivial conflicts reported against master fe963d7f5 (both from #5688 landing after the branch) were resolved by merging current master (82e2d53c6e) into the PR branch — exactly the two hunks, no more, no less: import union in src/loop/adapter-anthropic.ts, and the decide-side body in src/server/prepare-anthropic.ts becoming insertBeforeTrailingSystemRun(dedupeAdjacentOutputConfig(rebuiltMessages), [...]). Operation order was verified against src/dedupe-adjacent-output-config.ts, not applied by trust: dedupe only collapses adjacent empty-content system directives while the helper inserts a non-empty user before the trailing system run, so the two operations commute. New head aaaa7a9887, pushed. Local verification on the merged tree: tsc --noEmit clean, build clean, targeted suites 86/86.

Status (re-verified upstream today):

  • CI on exact head aaaa7a9887: 30 checks — 15 success / 15 skipped / 0 failed;
  • PR open, mergeable=true, state behind — the 6 master commits since the merge base touch none of this PR's files (src/util.ts, src/server/prepare-anthropic.ts, src/loop/adapter-anthropic.ts, src/wire-body.ts, src/chain-checkpoint.ts, the test file), so the behind state is inert churn with no textual or semantic interaction;
  • Both independent field verifications (hook-driven e2e A/B: 15/40 → 0 illegal positions; Bedrock/LiteLLM replay: 9/13 → 0/34, plus full-session rates) corroborate the fix shape.

结构化八问(答案与 head d0237cdba 独立复审一致——merge commit 仅含上述两处冲突解法,无语义变更):

  1. 是什么问题:claude-code 代理模式会话,历史以 system 消息结尾时,每次 nudge/notes 注入后请求硬 400,直到客户端自清 mid-conversation systems。
  2. 什么原因导致的:Anthropic mid-conversation system 规则是双侧的(FOLLOW 一个 user 且 PRECEDE assistant 或收尾),而宿主 ephemeral 尾注入发生在位置修复之后,把 user 放进了合法 trailing system run 的后面。
  3. 影响范围与严重程度:claude-code 车道 + Anthropic 上游 + 历史尾 system + nudge armed 同时满足才触发;触发后持续 400 直至自清(现场观察到分钟级)→ p2(随父 issue Anthropic 400: mid-conversation system must FOLLOW a user message — heal only checks the follower, and trailing nudge/notes land after a trailing system #5432)。现场数据:master 上该错误占 Haiku 400s 的 23/80 + Opus 1/1;修复后 431+88 请求零发生。
  4. 怎么修复的:单一 helper 把全部 8 处注入从「数组最末」改为「trailing system run 之前」;client systems verbatim 收尾,前驱为 assistant 时顺带修复该 run 的 follow 侧;kernel 半(case 1)已经 kernel authorization request: heal must enforce the FOLLOW side of Anthropic's mid-conversation system position rule (#5432) #5438/[PR] fix(kernel): heal the follow side of Anthropic's mid-conversation system rule (#5438) #5522 合入。
  5. 是否有回归问题:无。无 trailing run → 出站字节逐字节不变(helper/adapter/真 server 三层负向测试钉住);有 run → 旧行为必然 400;无下游消费者依赖注入位置;高危模式逐项过——无语义漂移、无 fail-fast→吞错、无阈值/时序/输出格式漂移、无配置面变更、无版本 bump。
  6. 是否破坏缓存:否。cache marks 骑在 coreToAnthropic 内最后一条稳定消息上,断点不动;ephemeral 尾本来就每轮变化,位置移动不引入新失效。
  7. 是否改了设计方向:否。宿主侧止血,规则所有权仍在 kernel;单源化计划([PR] fix(kernel): heal the follow side of Anthropic's mid-conversation system rule (#5438) #5522 既已合入,「宿主副本换 kernel import」收敛小 PR 可随时开)已写在两侧注释。
  8. 是否引入新配置:无。

判定:可合。 Wire/message-shape 变更按 §7.4 属 human-only 合并——请 owner 手动合并(https://github.com/ranxianglei/billion-context/pull/5441)。CI 在 exact head 全绿,无需再等。

ci-check added 4 commits November 10, 2026 12:51
…5432)

Anthropic's mid-conversation system rule is two-sided: an entry must FOLLOW
a user message AND PRECEDE an assistant message or end the array. The host
appended its ephemeral tail users (sysNotes/nudge/decide/statusLine/retrieval
notes) AFTER coreToAnthropic's position heal, so a client history ending in
a legal trailing system (Claude Code's 'user hasn't heard from you' reminder)
got a user after it and 400'd ('role system must precede an assistant message
or end the array'). The round-2 compress-loop adapter had the same shape.

New src/util.ts helper insertBeforeTrailingSystemRun inserts the injections
BEFORE the trailing system run: the client's systems stay verbatim and still
end the array, and the new user also repairs the run's follow side when its
predecessor was an assistant. All seven steady-path sites in prepare-anthropic
plus the adapter notes site now go through it. Sessions without a trailing
system run emit byte-identical bodies (pinned by negative tests).

This is the host-side half of #5432 (case 2). Case 1 — a fold orphaning a
sidecar system right after an assistant — needs the kernel heal to enforce
the follow side too; that change is gated on owner approval per the Kernel
Boundary (see the kernel authorization request issue). No kernel files are
touched here.
…ms (#5432)

- wire-body appendTrailingUserText (fake-completion hint / degenerate-turn retry nudge): insert the user turn BEFORE a trailing system run on the anthropic wire instead of pushing after it; openai bytes unchanged (no such rule there).
- chain-checkpoint insertCheckpointCarrier: an anthropic body ending in a trailing system run admits no carrier slot under the frozen #1395/#1421 contract (recognition only sees the trailing USER run); return null instead of appending after the run (the exact 400 shape) — relay forwards unstamped (best-effort).
- util: factor trailingSystemRunStart out of insertBeforeTrailingSystemRun so every post-assembly mutator shares one definition.
- tests: +6 two-direction rows covering both paths plus the openai no-drift pin.

Refs #5432
…e host interim copy (#5432)

The kernel now exports the same helper (single source of truth for the
trailing-system-run ordering rule, #5438/#5432); route the steady path,
the round-2 adapter and the regression tests through acp-kernel/wire
instead of the interim src/util.ts copy. trailingSystemRunStart stays
host-side (wire-body splice + chain-checkpoint null check have no kernel
counterpart). Behavior unchanged.
@ranxianglei
ranxianglei force-pushed the 2026-11-01_anthropic-trailing-system-tails branch from aaaa7a9 to 7513559 Compare November 10, 2026 05:13
@ranxianglei
ranxianglei merged commit 31684f3 into master Nov 11, 2026
54 checks passed
@github-actions

Copy link
Copy Markdown
Contributor

📦 Merged Build Published

This merge to master was published as 0.0.1175-master.31684f3 (rolling master tag).

npm install -g billion-context@0.0.1175-master.31684f3  # this exact build
npm install -g billion-context@master  # always the newest merged build

npm view billion-context@master prints the current version at any time.

ranxianglei pushed a commit that referenced this pull request Nov 12, 2026
master moved past the previous sync (dc041dd) by the web handoff-render
(#6260) + anthropic trailing-system (#5432/#5441) + snapshot-write-lifecycle
(#6186) landings. None of those commits modify src/plugin.ts or
src/agent/fork-adopt.ts (master's version of both == merge-base), so this
merge is clean and preserves the #5123/#4266 union resolution untouched.
ranxianglei pushed a commit that referenced this pull request Nov 12, 2026
Master advanced past the dc041dd merged earlier (web handoff rendering,
anthropic trailing-system tails #5432/#5441, snapshot write lifecycle #6186,
handoff-render #6260). Clean auto-merge — none of those commits touch the
side-lane surface (handle.ts / side-request.ts / side-lane tests), so no
conflicts and no logic change.
ranxianglei pushed a commit to john08281/billion-context that referenced this pull request Nov 12, 2026
… round 9)

Master advanced 33104b1 -> c26c31a (ranxianglei#6260, ranxianglei#5441, ranxianglei#6186, ranxianglei#5774,
ranxianglei#6166 et al.). Clean auto-merge, no conflicts: the new window's
blockOps work (ranxianglei#5765/ranxianglei#6166) and this PR's decide-lane keys land in
different regions of every shared file.

Also completes the round-8 docs regeneration that was left unstaged:
the committed head carried the decide-lane keys in the compress.yaml
seed but not in the generated output. Re-ran
node tools/gen-config-docs.mjs generate on the merged seed (341
entries; i18n ok: 256 keys, EN/ZH equal) — generated output now
matches the seed in docs/configuration[.zh-CN].md, website/main.js
and website/docs/index.html (blockOps + decide-lane keys, both
languages).

Verified: tsc --noEmit clean; unused-exports gate clean; targeted
suites green (nudge-decide + issue3004 selfheal + config-surface-guard
+ nudge-model-decided e2e). Full-suite verdict = remote CI.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants