Skip to content

[PR] feat: outbound-prefix LCP triage line for prompt-cache-break diagnosis (#5385) - #5386

Open
ranxianglei wants to merge 6 commits into
masterfrom
2026-11-01_outbound-prefix-lcp-triage
Open

ranxianglei wants to merge 6 commits into
masterfrom
2026-11-01_outbound-prefix-lcp-triage

Conversation

@ranxianglei

Copy link
Copy Markdown
Owner

What

Adds an opt-in observation instrument that directly answers the diagnostic ask in #5385 ("is there a diagnostic for which message index first differs between consecutive outbound requests?").

New module src/server/outbound-prefix.ts, wired into prepareAnthropic right after the final rebuiltMessages assembly (src/server/prepare-anthropic.ts). Each turn it:

  1. fingerprints every rebuilt outbound message (role | content shape | block types | thinking presence | cache_control presence | length | djb2 hash of the bytes),
  2. keeps the previous turn's fingerprint list in session.metadata,
  3. computes the longest-common-prefix of the two lists and logs it, plus the exact message index where the two histories first diverge, together with the positions carrying cache_control.

Example lines:

[<session>] outbound-prefix: lcp=57/58 (prev 57) cc@[0] (#5385)          <- healthy tail append
[<session>] outbound-prefix: lcp=12/58 (prev 57) cc@[0] BREAK@12 prev[..]="user|blk|..." -> cur[..]="assistant|blk|..." (#5385)   <- shrinkage: first divergence at index 12

Log-only. It never touches wire bytes. When the knob is off (the default) the function returns immediately — zero overhead, zero behavior change.

Why this shape

  • Reuses the existing ACP_DUMP_BODY / diagnostics.dumpBody knob (off by default). No new config field, no new env var — so no config-surface change. Enable it on the affected session to collect the data.
  • Fingerprint-per-message + LCP, not a raw body diff: cheap (one hash per message), and the divergence index is exactly what localizes the cache break — the Anthropic prompt-cache prefix match stops at the first changed byte, i.e. the first changed message.
  • The reported position is the array index (what the prefix match cares about); each fingerprint carries a content hash so a same-length re-encoding is still caught.

Verification

  • 6 targeted unit tests in tests/issue5385-outbound-prefix-diff.test.ts: off-by-default no-op (no log, no session-state write), first-turn baseline, healthy tail-append (debug, no BREAK), head change (info, BREAK@0), mid change (info, BREAK@1 + cc@[..]), same-length re-encoding caught by content hash. All pass.
  • npm run typecheck clean (kernel/ + pi-subagents/ pre-hook builds OK). Full-suite verdict = remote CI on this head.
  • No kernel/ change; no wire/message-shape change; no config schema change; no default behavior change.

Scope note

This is instrumentation only — it does not fix #5385. It produces the evidence needed to distinguish the two remaining candidate mechanisms (mid-history fold churn vs. extra per-turn byte instability) once the reporter enables it on the affected session. #5385 stays open pending that data.

@github-actions

github-actions Bot commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor

📦 Built Package Artifact

Branch: 2026-11-01_outbound-prefix-lcp-triage (2bffbe3)

Latest build — install this exact version

npm install -g billion-context@0.0.9074-pr.5386.eeb3b7c

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
2bffbe3 billion-context@0.0.9074-pr.5386.eeb3b7c build
7bd548e billion-context@0.0.9054-pr.5386.e75168d build
0bcdabf billion-context@0.0.9041-pr.5386.cc88e38 build
7ca0011 billion-context@0.0.7705-pr.5386.69cc5b0 build
e20f157 billion-context@0.0.7609-pr.5386.0a935c7 build

Download artifact

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

Install instructions are refreshed on each push.

@ranxianglei ranxianglei changed the title feat: outbound-prefix LCP triage line for prompt-cache-break diagnosis (#5385) [PR] feat: outbound-prefix LCP triage line for prompt-cache-break diagnosis (#5385) Oct 31, 2026
@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

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

查重:不重复。 本单对应 GitHub PR #5386(分支 2026-11-01_outbound-prefix-lcp-triage,单 commit)。它是 #5385 里明确请求的那条诊断("is there a diagnostic for which message index first differs between consecutive outbound requests?")的唯一实现——#5385 floor 2 已记录「已开 PR #5386」;open+closed 全量检索(outbound prefix / LCP / cache break / 逐条指纹诊断)无并行或重复条目。按 [PR] 识别规则处理:不重做,拉分支独立审核 + 核对 CI + 报告结论。

标签: pr scope/wiring p3(请求管道观测面;非缺陷类不打 bug;车道无关不打客户端标;p3=opt-in 诊断工具,默认关时对任何用户零影响)。

独立审核结论(不是复读 PR 描述,以下全部在 PR head e20f157 上亲自验证过)

方向(①): #5385 的两个剩余候选机制(中段折叠 churn vs 额外每轮字节不稳定)确实只有「连续两次出站从第几条开始分叉」这枚证据能区分,fingerprint-per-message + LCP 正是最小且够用的形状——Anthropic 前缀缓存在第一个变化字节处断掉,分叉下标就是缓存断点定位。方向成立。

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

  • 调用点(src/server/prepare-anthropic.ts:587,final rebuiltMessages 组装后):纯读、纯日志。核了 rebuiltMessages 全部赋值路径(171/482/484/518/535/551/570 行)——无论 kernel transform 成功、nudge 注入、还是 transform 失败走 forwarding-unchanged(catch 里它保持 parsed.messages 原投影),观测到的都是实际出站的字节,语义在所有路径上正确。
  • off 路径:bodyDumpEnabled()(src/knobs.ts:632-636,动态读 ACP_DUMP_BODY > diagnostics.dumpBody > false)为假时函数第一行 return——无日志、无 session 状态写入(测试钉死了这两点)。零行为变更属实。
  • session.metadata:新键 outboundPrefixFingerprint,与现有键无碰撞。一个已知取舍,审过、判可接受不修:开关开着时该数组会随 persist.ts:2949 的 { ...session.metadata } 落进会话文件(长会话几百 KB 量级),关开关后残留为惰性数据。理由:与紧邻调用点上方每轮都写、同样持久化的 systemPromptTokens 惯例一致;且该开关本身语义就是「body 落盘」(无 GC:dumps/、raw/、launcher 临时日志、bili.log.old 永久累积 #276),诊断指纹不比它更重。若要在 persist 保存路径里 scrub 这个键,代价是动热文件换微小收益,不值。
  • 空 catch:与本文件 observability 惯例一致(prepare-anthropic.ts:630 MSGID_OBSERVED 块「an observability failure must never disturb the turn」),注释说明了意图,不算吞错违规。
  • 高危模式逐项:无语义漂移、无 fail-fast 变吞错(请求路径不受影响)、无阈值/时序/默认值/输出格式变化、wire 字节零触碰(测试有 mutation 断言)。
  • djb2 32-bit 哈希:理论上同格碰撞可掩盖一处真实分歧(假 LCP 延长),概率 ~2⁻³²/格且有 len+类型前缀收窄——诊断工具量级可接受,不构成问题。

测试有效性(④): 6 个测试双向钉死(#3617 纪律):off⇒无日志+无状态写;on⇒首轮基线静默 / 尾部追加 debug 无 BREAK / 头部变 info BREAK@0 / 中部变 info BREAK@1+cc@[..] / 等长重编码靠内容 hash 抓出。断言有牙(具体 lcp 值、BREAK@k、cc 位置正则匹配),bug 回归时抓得住。env 卫生:模块级 ACP_DUMP_BODY=1 被 node:test 每文件独立进程隔离,off 测试 save/restore 正确。

diff 整洁度(⑤): 3 文件 +172 行,每一行都与目的相关;无空白噪音、无锁文件连带、commit 规范(feat: + 引用 #5385)。

本地定向验证: typecheck(kernel/pi-subagents pre-hook build 后)clean;目标测试文件 6/6 通过。全量回归按 §7.2 交给 CI。

CI 状态(镜像缓存 + 单次 check-runs 拉取): static-gates(gate×2、lane-guard)、mysql-contract 已绿,e2e lanes 照常 skip(env-gated);12 个 test shards(ubuntu/windows × node22/24 × 3)写作时在跑、暂无红。终审判定 = 本 head 的 CI test lanes 转绿。

八问

  1. 是什么问题: 不是缺陷修复,是诊断工具——回答 Long Claude Code session re-caches ~140-175K tokens per turn (cache read stuck at the fixed prefix) #5385 的取证请求:连续两次出站请求从第几条消息开始不一样。
  2. 什么原因导致的: 不适用(非缺陷);背景是 Long Claude Code session re-caches ~140-175K tokens per turn (cache read stuck at the fixed prefix) #5385 超长会话 cache_read 卡死在固定前缀,两个候选机制无法在拿到分叉下标前区分。
  3. 影响范围与严重程度: 默认关→对任何用户零影响;开着(opt-in 排障)时每轮多一条 debug/info 日志行,不改任何出站字节。p3。
  4. 怎么修复的: 不适用(交付的是证据采集器);机制=逐条指纹(role|内容形状|块类型|thinking/cc 存在|长度|内容 hash)与上一轮算最长公共前缀,打出首个分叉下标 + cache_control 位置;复用现成 dump 开关。
  5. 是否有回归问题: 无。上面②节逐项列了审过的既有行为与结论;唯一取舍(metadata 数组随会话文件持久化)已说明并判可接受。
  6. 是否破坏缓存: 不影响。只读只记日志,不碰 cache_control/消息内容/账本。
  7. 是否改了设计方向: 没有。纯观测面新增,不动架构/边界/数据流。
  8. 是否引入了新的配置: 没有新配置项/env var——复用已文档化的 ACP_DUMP_BODY(env)/ diagnostics.dumpBody(config file),两者默认关。

收工判定: 代码与行为审核通过,CI test shards 转绿即可合并(feat: 新增观测面 + 触 prepare-anthropic.ts 热文件 → 按 §7.4 人工合并,不走 auto-merge)。本 PR 不修 #5385,#5385 保持 open 等报告者在受影响会话开 ACP_DUMP_BODY=1 后贴回 outbound-prefix: 行;数据一到即可钉死机制再给修复方案。

…eaks (#5385)

Add observeOutboundPrefix() (src/server/outbound-prefix.ts), an opt-in
observation instrument wired into prepareAnthropic right after the final
rebuiltMessages assembly. Gated behind the existing ACP_DUMP_BODY /
diagnostics.dumpBody knob (off by default, zero overhead when off): each turn it
fingerprints the rebuilt outbound messages, stores the previous turn's list in
session.metadata, and logs the longest-common-prefix length plus the exact
message index where two consecutive outbound histories first diverge, together
with cache_control positions. Log-only; never touches wire bytes.

Directly answers #5385's ask ('which message index first differs between
consecutive outbound requests'). No behavior change when the knob is off.
@ranxianglei
ranxianglei force-pushed the 2026-11-01_outbound-prefix-lcp-triage branch from e20f157 to 7ca0011 Compare November 1, 2026 10:34
…lve prepare-anthropic.ts conflict (#5385)

Master refactored the outbound rebuild tail (#5604 ensureUserTail -> #5688
dedupeAdjacentOutputConfig -> #4570 healAnthropicSystemPositions), so the
PR's observeOutboundPrefix hook now runs on the final sent array
(healedMessages) instead of the pre-heal rebuiltMessages, keeping both intents.
@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

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

查重: 不适用 —— 本任务是该 PR(#5386)的 CI 接冲突车道(合并冲突自动修复),不是新缺陷/新功能立项,没有可重复的并列工作。

What I did this run

  • Located the PR: #5386 = branch 2026-11-01_outbound-prefix-lcp-triage, a single commit 7ca0011328 based on old base e226544f55; master had advanced to 58a8796224 → merge state dirty.
  • Fetched fresh from both the local mirror and GitHub (both agreed on head=7ca0011328, master=58a8796224) before touching anything.
  • Merged origin/master into the branch (merge, not rebase — matches this repo's convention for conflict worktrees and avoids force-push risk while other agents may share the branch).
  • Resolved the single conflict in src/server/prepare-anthropic.ts. Everything else was clean git auto-merge of already-green master commits.
  • Validated locally: npm ci + full typecheck clean (built kernel/ + pi-subagents/, tsc --noEmit over src+tests passes); the PR's own test tests/issue5385-outbound-prefix-diff.test.ts passes 6/6.
  • Committed merge 0bcdabff87 (parents 7ca0011328 + 58a8796224), re-fetched before push (head unchanged → no concurrent edits), then fast-forward pushed to GitHub: 7ca0011328..0bcdabff87. Re-fetch confirms the remote head moved.
  • Proved the conflict is cleared: upstream/master is now an ancestor of HEAD, so there is no remaining conflict with the base branch.
  • Did not gh-poll check states (shared-PAT budget policy). The mirror check-cache still shows the pre-push sha (7ca0011328, old runs cancelled) and refreshes within ~60s; CI will run on the new head.

The conflict, in one line

Master refactored the outbound-rebuild tail in prepare-anthropic.ts (#5604 ensureUserTail → #5688 dedupeAdjacentOutputConfig → #4570 healAnthropicSystemPositions), which collides with where the PR inserted its observeOutboundPrefix(...) hook. Resolution keeps master's full transform chain verbatim and re-homes the hook onto the final sent array healedMessages (master renamed the pre-heal rebuiltMessages into healedMessages as the last transform), placed right after warnAnthropicThinkingPairs exactly where the PR put it. Both intents preserved.


结构化八问

  1. 是什么问题:PR [PR] feat: outbound-prefix LCP triage line for prompt-cache-break diagnosis (#5385) #5386(outbound-prefix LCP 缓存断点诊断线)与其基线分支 master 出现合并冲突(merge state dirty),需要接冲突让 PR 恢复可合并。
  2. 什么原因导致的:PR 基于旧基线 e226544f55,而 master 已前进到 58a8796224;master 恰好重构了 prepare-anthropic.ts 出站重建尾部([Bug] failed-compress recovery receipt (#5220/#5231) leaves the re-request ending on an assistant turn → upstream 400 (trailing whitespace / prefill not supported), subagent dies #5604/[Bug] fold reconstruction can emit two adjacent output_config effort directives → deterministic 400 on Anthropic lane (pi thinkingLevel max + OpenRouter) #5688/[Bug][anthropic] 0.1.206: system 400 still fires on nudge turns: the heal keeps a trailing system, then prepare-anthropic appends user notes after it #4570),与 PR 在同一代码区域插入 observeOutboundPrefix 调用,形成文本冲突。
  3. 影响范围与严重程度:仅该 PR 分支暂时无法合并;不影响任何线上用户——该功能纯观测、默认关闭。属「PR 阻塞」类,非线上缺陷。
  4. 怎么修复的:把 origin/master 合入 PR 分支(用 merge 而非 rebase,规避并发下的 force-push 风险);唯一冲突文件保留 master 完整变换链,将 observeOutboundPrefix 调用改挂到最终出站数组 healedMessages(master 把原 rebuiltMessages 重构成最后一步的 healedMessages),位置仍在 warnAnthropicThinkingPairs 之后——同时保住双方意图。
  5. 是否有回归问题:无。逐条核对了 master 侧触碰路径的既有行为([Bug] failed-compress recovery receipt (#5220/#5231) leaves the re-request ending on an assistant turn → upstream 400 (trailing whitespace / prefill not supported), subagent dies #5604 尾部补 user nudge、[Bug] fold reconstruction can emit two adjacent output_config effort directives → deterministic 400 on Anthropic lane (pi thinkingLevel max + OpenRouter) #5688 相邻 output_config 折叠、[Bug][anthropic] 0.1.206: system 400 still fires on nudge turns: the heal keeps a trailing system, then prepare-anthropic appends user notes after it #4570 system 位置修复、prompt_cache_key 剥离)全部原样保留;PR 侧净新增仅一行观测调用。本地全量 typecheck 干净 + 该特性自带测试 6/6 通过。
  6. 是否破坏缓存:不破坏。observeOutboundPrefix 只读重建后的消息列表做指纹比对并写日志,从不改写 wire 字节;且默认关闭(diagnostics.dumpBody / ACP_DUMP_BODY),零运行时开销。改成观测最终数组反而让缓存断点定位更准确。
  7. 是否改了设计方向:否。未改架构/边界/数据流,只是把观测钩子对齐到 master 重构后的最终出站数组。
  8. 是否引入了新的配置:否。未新增任何 env/config/默认值(dumpBody 门控是 PR 原有,非本次引入)。

判定:冲突已解决并推送成功(fast-forward 至 0bcdabff87),本地已证明 master 是 HEAD 祖先、无残留冲突;待 CI 在新 head 上跑绿即可合并。

… master fold-point removal (#6132/#6150), auto-merged clean (#5385)

Master removed the compressAtFoldPoint feature; that removal does not overlap
the outbound-prefix hook resolution, so this merge is conflict-free. The
observeOutboundPrefix hook still runs on the final sent array (healedMessages).
@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

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

查重: 不适用 —— 本任务是该 PR(#5386)的 CI 接冲突车道(合并冲突自动修复),不是新缺陷/新功能立项,没有可重复的并列工作。

What I did this run

This was a re-trigger: after my previous fix I pushed 0bcdabff87, but master advanced again (58a8796224 → 33104b1f95), so the branch fell behind/diverged and the PR went dirty again.

  • Verified live state (not from memory): remote head was 0bcdabff87, master was now 33104b1f95, and the two were diverged → conflict genuinely present.
  • Identified the new master delta: removal of the compressAtFoldPoint feature (Remove the compressAtFoldPoint feature — it reorders resent history and caused an authorship-misattribution incident in a live session #6132/[PR] remove: the experimental compressAtFoldPoint feature — fold-point relocation (v1 #5471 + unreleased v2 #5949), #6132 #6150) — touches prepare-anthropic.ts (-5/+1) plus 40 other files (kernel/, website/, config-reference/).
  • Merged latest origin/master into the branch → git auto-merged clean, no conflict. The removed block lives in the loopConfig region (~line 305), which does not overlap my earlier resolution (the outbound-rebuild tail + the observeOutboundPrefix hook), so both intents survive without manual edits.
  • Verified correctness: the removed symbols (makeFoldPointReasoningDroppable, compressAtFoldPointClearThinking) are fully gone (no leftover import/reference); my hook is intact (import line 48, call line 633 on healedMessages); the branch's net delta over current master is still exactly the 3 PR files (+172).
  • Re-validated locally: full typecheck clean (exit 0) + the PR's own test tests/issue5385-outbound-prefix-diff.test.ts passes 6/6.
  • Committed merge 7bd548e690 (parents [0bcdabff87, 33104b1f95]), re-fetched before push (head unchanged → clean fast-forward), then pushed 0bcdabff87..7bd548e690. Re-fetch confirms the remote head moved.
  • Proved the conflict is cleared: upstream/master (33104b1f95) is now an ancestor of HEAD → no remaining conflict with the base branch.
  • Did not gh-poll. The mirror check-cache still reflects the first push 0bcdabff87, where CI ran green (16 success / 14 skipped / 0 fail) but was flagged behind master — that "behind" is precisely why this lane re-fired. It refreshes to 7bd548e690 within ~60s for a fresh run.

结构化八问

  1. 是什么问题:上一轮我已把冲突修好并推送(0bcdabff87),但随后 master 又前进(58a8796224→33104b1f95),分支再次落后/分叉,PR 重新变回 dirty,本车道因此被再次触发。
  2. 什么原因导致的:master 合入了删除 compressAtFoldPoint 特性的 Remove the compressAtFoldPoint feature — it reorders resent history and caused an authorship-misattribution incident in a live session #6132/[PR] remove: the experimental compressAtFoldPoint feature — fold-point relocation (v1 #5471 + unreleased v2 #5949), #6132 #6150(改 prepare-anthropic.ts -5/+1 及 kernel/website 等共 41 文件);我的分支基于旧 master,故再度与基线分叉。属「master 持续前进导致重复接冲突」,非新缺陷。
  3. 影响范围与严重程度:仅该 PR 分支需重新对齐基线才可合并;不影响任何线上用户(该功能纯观测、默认关闭)。属「PR 阻塞」类,非线上缺陷。
  4. 怎么修复的:把最新 origin/master(33104b1f95)合入分支;这次 git 自动合并干净、无冲突——因为 master 删的是 loopConfig 区的 compressAtFoldPoint 块(~第 305 行),与我上轮解决的出站重建尾部([Bug] failed-compress recovery receipt (#5220/#5231) leaves the re-request ending on an assistant turn → upstream 400 (trailing whitespace / prefill not supported), subagent dies #5604/[Bug] fold reconstruction can emit two adjacent output_config effort directives → deterministic 400 on Anthropic lane (pi thinkingLevel max + OpenRouter) #5688/[Bug][anthropic] 0.1.206: system 400 still fires on nudge turns: the heal keeps a trailing system, then prepare-anthropic appends user notes after it #4570)及 observeOutboundPrefix 钩子(第 48/633 行)不重叠。双方意图都保留:master 的删除生效,我的钩子仍挂在最终出站数组 healedMessages 上。
  5. 是否有回归问题:无。逐条核对:makeFoldPointReasoningDroppable / compressAtFoldPointClearThinking 在 prepare-anthropic.ts 中已彻底移除(无残留 import/引用);我的钩子原样保留;分支相对当前 master 的净差异仍只有那 3 个 PR 文件(+172)。本地全量 typecheck 干净 + 特性自带测试 6/6 通过佐证集成无误。
  6. 是否破坏缓存:不涉及。本次是纯 master 同步 + 保持既有解析结果,未改任何出站字节或缓存逻辑;observeOutboundPrefix 依旧只读指纹、默认关闭、零开销。
  7. 是否改了设计方向:否。未改架构/边界/数据流,只是把分支再次对齐到最新 master。
  8. 是否引入了新的配置:否。未新增任何 env/config/默认值(本轮 master 反而删除了一个配置项 compressAtFoldPoint,并非我引入)。

判定:已把最新 master 合入并推送成功(fast-forward 至 7bd548e690),本地已证明 master(33104b1f95)现为 HEAD 祖先、无残留冲突;镜像缓存刷新到 7bd548e690 后会再跑一轮 CI。若 master 继续快速前进,可能还需再同步一次——属正常节奏,非异常。

@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

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

This run: stale re-fire — verified live there is NO conflict and NO red CI, so nothing to fix. Branch left untouched (no push, no edits).

Live evidence (checked this run, not from memory):

  • Local and remote PR head both 7bd548e690; upstream/master = 33104b1f95 — unchanged since my previous fix.
  • git merge-base --is-ancestor upstream/master HEAD → true; the merge-base equals master itself and HEAD..upstream/master is empty → zero master commits missing, no conflict. The trigger's "merge state: dirty" does not match the actual refs.
  • Mirror check-cache (fresh, fetched 04:05:32Z) now reflects head 7bd548e690: 16 success / 14 skipped / 0 failures — CI is green. mergeable_state: blocked; with a fully-green check set that denotes a branch-protection / required-review gate, not a failing check.

结构化八问(本轮)

  1. 是什么问题:不是新问题——车道被再次触发,但实测既无冲突也无红 CI。
  2. 什么原因导致的:上一轮把分支同步到 master(33104b1f95)并推送(7bd548e690)后,触发器读到的是尚未重算完成的旧「dirty」状态;master 此后未再前进,实际已干净。属陈旧重触发,非振荡。
  3. 影响范围与严重程度:无——本轮零改动;PR 现状为「CI 绿 + 待人工合并」。
  4. 怎么修复的:无需修复。本轮只做只读核验(git 祖先关系 + 镜像缓存),未 push、未改任何文件。
  5. 是否有回归问题:不涉及(零改动)。
  6. 是否破坏缓存:不涉及(零改动)。
  7. 是否改了设计方向:否。
  8. 是否引入了新的配置:否。

判定 / 剩余事项:冲突与红 CI 均不存在(前者本轮核验确认,后者镜像缓存 16 绿 0 红)。mergeable_state: blocked 在检查全绿的前提下是分支保护/需人工评审的门槛——按 §7.4(feat + 热文件 prepare-anthropic.ts)本就该人工合并、agent 不得代为合并。因此我这边无需再动作,请人工评审后合并即可。若之后 master 再前进造成真实分叉,该车道会正确再触发,届时才需要接冲突。实际冲突解决的完整过程与八问见上方 floor 13(comment id 71669);本轮无新代码变更,不重复粘贴。

ework-agent added 3 commits November 11, 2026 13:26
…map (#5385)

The architecture-docs CI gate requires every tracked src/ file in the map;
the new module was missing, so the lane would fail at this head.
…hem silently (#5385)

Aligns with the noteMsgIdObservation precedent (warn + context); a failing
observation while body dumps are enabled for triage should be visible.
… suite (#5385)

Covers the cur-shorter-than-prev path: lcp/prev counts, BREAK index and the
<end> sentinel for the missing tail cell.
@ranxianglei

Copy link
Copy Markdown
Owner Author

(ai: vllm-qwen/qwen3.8-27b)
(posted as a comment: GitHub forbids formal review events on one's own pull request)

Verdict: APPROVE — log-only diagnostic instrument, correctly scoped; no wire/config/kernel changes. Three small mechanical findings fixed directly on this branch (receipts below); nothing else blocks.

What it does. observeOutboundPrefix fingerprints each outbound Anthropic message (role|str|len|h for string content, role|blk|types|think|cc|len|h=djb2(content) otherwise) once per turn in prepareAnthropic, immediately before the rebuilt body is serialized — i.e. on the exact array upstream receives. It compares against the previous turn's fingerprint list kept in session.metadata and logs one LCP line: debug on a healthy tail append, info with BREAK@<idx> plus the cache_control positions when the stable prefix shrinks — the prompt-cache-break signature #5385 reports (cache_read pinned at the system+tools prefix while cache_creation grows with session length). Gated by the existing ACP_DUMP_BODY / diagnostics.dumpBody knob: off by default, no new config surface.

Correctness checks.

  • Call site sits after healAnthropicSystemPositions / warnAnthropicThinkingPairs and right before const rebuilt = { ...parsed, messages: healedMessages, ... }; nothing mutates healedMessages between the call and JSON.stringify(rebuilt) → the fingerprinted array is byte-faithful to what goes out.
  • Observation-only: read-only over messages, one metadata write per turn while enabled, whole body in try/catch — cannot disturb the request path.
  • Off-by-default verified against bodyDumpEnabled() (src/knobs.ts, dynamic env>file check): knob off ⇒ zero logging and zero session-state writes (pinned by test 1).
  • Covers the reporter's path: Claude Code speaks the Anthropic wire, which always runs prepareAnthropic; the pf-watermark fastpath only diverts OpenAI-shaped requests.
  • Both compression modes reasoned: plugin mode re-sends host history through the same prepare path, so the per-turn comparison applies identically; nothing mode-specific is assumed.

Tests. All six original cases have teeth — exact log level + regex on the emitted line, and both gate directions per #3617 (fires on shrink, quiet on append). The only untested branch of the detail string was the shrink case (current list shorter than previous → BREAK@<tail> with the <end> sentinel); added as a seventh case.

Regression audit (existing behavior on the touched paths).

  • prepareAnthropic outbound bytes: unchanged — the call has no side effects on the message array.
  • Persistence: the new outboundPrefixFingerprint metadata key is inert for drop decisions (isDroppedMetadata tracks lazy-proxy state, not keys) and is written only while the dump knob is on; a stale baseline across restarts is acceptable because a byte change between consecutive sends is precisely the signal being hunted.
  • Log volume: one extra line/turn at debug when the knob is on (which already implies full body dumps); default-off deployments see nothing.
  • No threshold/default/timing/output-format drift; kernel/ untouched; no config schema change.

Findings fixed on this branch (per owner policy 2026-11-01):

  • 已顺手修:new module missing from the reference/architecture.md map — the architecture-docs CI lane requires every tracked src/ file there and would have failed at the reviewed head (reproduced locally with tools/check-architecture-docs.mjs) + commit cef0993
  • 已顺手修:silent catch {} in observeOutboundPrefix — now logs warn with session id and error, aligning with the noteMsgIdObservation precedent + commit f8015e7
  • 已顺手修:missing regression test for the history-shrinkage path (cur shorter than prev, <end> sentinel) + commit 2bffbe3

The author merged current master into the branch (7bd548e) just before these fixes landed; they are stacked on top, and the architecture-docs guard passes again at the pushed head. Full-suite verdict: remote CI on 2bffbe3.

八问(中文,供人工复核):

  1. 是什么问题:Long Claude Code session re-caches ~140-175K tokens per turn (cache read stuck at the fixed prefix) #5385 报长会话每轮重缓存 140–175K token、cache_read 钉死在固定前缀,缺一个「连续两次出站请求从第几条消息开始不同」的诊断手段;本 PR 提供该手段。
  2. 什么原因导致的:根因未定(疑似中途 fold/重建改写前缀字节);本 PR 不修根因,只加观测——逐消息指纹 + 与上一轮求最长公共前缀,前缀缩短即打印断点位置。
  3. 影响范围与严重程度:纯观测代码,默认关闭(复用既有 ACP_DUMP_BODY/diagnostics.dumpBody 开关),默认部署零影响;开启时每轮多一行日志;不改任何请求字节。
  4. 怎么修复的:在 prepareAnthropic 序列化前对即将出站的精确数组做逐消息指纹(字符串=长度+hash,块消息=类型列表+thinking/cc 标志+内容 djb2),存 session.metadata,下一轮求 LCP;缩短→info 级 BREAK@i prev[..] -> cur[..],健康追加→debug。
  5. 是否有回归问题:审过——出站字节不变(调用无副作用、其后无人改数组);持久化判决不受影响(新键不参与 isDroppedMetadata,且仅开关开启时写入);日志量仅开关开启时增加;默认值/阈值/时序/输出格式无漂移;kernel/ 未触碰。
  6. 是否破坏缓存:不碰任何缓存逻辑(本工具本身就是查 prompt-cache 断点的);指纹存内存 metadata,重启后首轮重新基线化,跨重启的字节变化恰是目标信号,可接受。
  7. 是否改了设计方向:没有,纯 instrument,不改变压缩/重建/渲染的任何决策路径。
  8. 是否引入了新的配置:没有——刻意复用既有 dumpBody 开关,PR 描述已声明,符合配置面纪律。

APPROVE — merge-ready, pending CI green on 2bffbe3.

@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

[bot] 🏷 Review lane complete: GitHub PR #5386 reviewed end-to-end — verdict APPROVE (posted on the PR thread as a comment; a formal review event is impossible from the author's own account). Three small mechanical findings were fixed on-branch per policy and pushed: architecture-map line (cef0993), silent catch → warn log (f8015e7), missing shrink-case regression test (2bffbe3); new head 2bffbe3. Locally verified at head: architecture-docs guard green, edited files syntax-clean, no kernel/config/wire changes, no open findings left unfixed or parked. Full-suite verdict: remote CI on 2bffbe3. Nothing blocks — ready for human merge once CI is green.

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

Development

Successfully merging this pull request may close these issues.

Long Claude Code session re-caches ~140-175K tokens per turn (cache read stuck at the fixed prefix)

1 participant