Repository navigation
[Bug] 换账号后长会话反复写入大额缓存:rewrite 丢弃稳定断点(v1.3.188 离线复现) #524
Description
提交前自查
- 已升级到最新 Release,问题仍存在(或无法升级,已在下方说明原因)
- 已搜索现有 Issues,没有重复
- 已阅读 ISSUE_GUIDE.md,并对贴出的所有内容做了脱敏
问题类型
其他
1. 使用的版本
生产样本:基于 v1.3.178 的定制版;原版离线复现:v1.3.188,0629424e63cdcf26301b155f609defc7ba63d878。新版真实上游缓存复验尚未执行。
部署方式
手动 Docker Compose
问题描述
同一入站 Session 标识的长上下文请求切到另一账号/槽位后,首条冷写入可以理解,但后续同账号串行成功请求仍反复写入约 82–84 万 token,只读取 14,621 token(约 1.73%–1.74%)。相邻账号有的很快恢复约 83 万读取,有的四条观察内未恢复。
实际是“反复大量写入、读取率低”,并非 cache_creation_input_tokens=0。我们不要求跨账号直接共享上游缓存;希望新账号写入后,后续轮次能复用未变化的长前缀。
核对原版 v1.3.188:只有识别为官方流量且 official_cc_passthrough=true 时才保留调用方断点;默认关闭时仍清除原断点,固定标记消息尾部与倒数第二条 user。这个开关还同时控制是否保留客户端 system,需要平台 persona 时,无法用它单独选择保留客户端缓存断点。想请作者确认是否为预期及推荐处理方式。
**证据边界:**生产用量来自 v1.3.178 定制版,不声称已在原版 v1.3.188 生产复现;下方在固定新版原版源码上的离线脚本证明 Node 断点改写,缓存查找部分仅是机制模拟。历史请求无 debug body,不能据此认定本次事故唯一根因或归责用户。
2. 可复现路线
在原版 v1.3.188 源码目录,按 package-lock.json 安装依赖。将脚本保存为任意目录的 repro.mjs,执行 node /path/to/repro.mjs "$PWD"。不启动服务、不联网、不使用账号、不读写生产数据库。
合成 915 条单文本 block 消息,在稳定位置 910 放调用方断点;下一轮只改变历史位置 912。消息数保持相同是为隔离断点行为的最小反例,不是实际客户端完整对话重放。
import assert from 'node:assert/strict'
import { createHash } from 'node:crypto'
import path from 'node:path'
import { pathToFileURL } from 'node:url'
const root = path.resolve(process.argv[2] || '.')
const load = (file) => import(pathToFileURL(path.join(root, 'src/lib', file)).href)
const { prepareCliHopBody } = await load('protocol/outbound-attempt.mjs')
const { officialCcPassthroughFromRouting } = await load('identity/crs-persona.mjs')
assert.equal(officialCcPassthroughFromRouting({}), false)
const first = {
model: 'claude-opus-5-5', max_tokens: 32,
messages: Array.from({ length: 915 }, (_, i) => ({
role: i % 2 ? 'assistant' : 'user',
content: [{ type: 'text', text: `stable-${i}` }],
})),
}
first.messages[910].content[0].cache_control = { type: 'ephemeral', ttl: '1h' }
const second = structuredClone(first)
second.messages[912].content[0].text = 'changed-historical-block'
const markers = (body) => body.messages.flatMap((m, i) =>
m.content.some((b) => b.cache_control) ? [i] : [])
const prefix = (body, i) => createHash('sha256').update(JSON.stringify(
body.messages.slice(0, i + 1), (key, value) => key === 'cache_control' ? undefined : value,
)).digest('hex')
for (const preserveSystem of [false, true]) {
const options = { preserveSystem, preserveThinkingBlocks: true }
const a = prepareCliHopBody(structuredClone(first), options)
const b = prepareCliHopBody(structuredClone(second), options)
const written = new Set(markers(a).map((i) => prefix(a, i)))
// 示意模型:仅缓存实际写入断点,最多向前检查 20 个单文本 block。
// 不是上游服务实现,不是实际缓存命中率测试。
let match = null
for (const end of markers(b)) {
for (let i = end; i >= Math.max(0, end - 19); i--) {
if (written.has(prefix(b, i))) match = Math.max(match ?? -1, i)
}
}
assert.deepEqual(markers(a), preserveSystem ? [910] : [912, 914])
assert.equal(match, preserveSystem ? 910 : null)
console.log(JSON.stringify({ fixture_only: true, upstream_calls: 0,
mode: preserveSystem ? 'official_passthrough' : 'rewrite',
message_markers: markers(a), new_account_next_turn_matching_message: match }))
}在独立导出的原版 v1.3.188 上实际执行,Node v22.22.2 / macOS arm64;复用本机按锁文件已安装的依赖。退出码 0,输出:
{"fixture_only":true,"upstream_calls":0,"mode":"rewrite","message_markers":[912,914],"new_account_next_turn_matching_message":null}
{"fixture_only":true,"upstream_calls":0,"mode":"official_passthrough","message_markers":[910],"new_account_next_turn_matching_message":910}期望结果 / 实际结果
期望:在四个总断点限制内,对合法调用方断点有明确、可配置的保留策略;需要平台 persona 时,也能保护历史稳定长前缀锚点。换账号可以先冷写入,后续不应仅因稳定锚点被网关删掉而反复重写长历史。
实际:默认 rewrite 删除 910,首轮仅在 912/914 写入;下一轮 912 改变后,两条历史条目均不匹配。向前查找未写过的位置不能凭空生成稳定前缀条目。保留 910 时,同一模拟可匹配到 910。
这证明一种能够产生该形态的机制,尚未证明是生产低命中的唯一原因。线上四条均 HTTP 200、单次 attempt,无执行重叠,详见请求日志。
官方 Prompt caching / automatic prefix checking 说明条目在断点创建,向前检查匹配已创建条目,且每断点有 block 查找窗口。延长 TTL 或保留相同 Session UUID 本身不能修复前缀失配。
复现频率
必现
3. 运行环境(VPS / 宿主机)
- 生产:Linux x86_64,Docker Compose 控制面 + Docker 槽位,源码构建定制版。
- inference.dataplane=wrap / CLI-hop,多账号池,本文对照四个账号/槽位。
- 宿主机具体内核、Docker 版本、云地区、资源规格及是否多 VPS 本次未收集,不据此推断根因。
- 四个槽核验时 kernel 与 cli-node 的 SHA-256 分别相同;persona 均为 official,默认 TTL 为 1h,不能用槽间二进制版本不同解释这个对照。
- 离线验证环境:macOS arm64,Node v22.22.2;固定 v1.3.188 tag 的独立原版源码。
代理 / 出口
出口是否共享、代理类别/地区、DoH 和透明代理本次未核实;不披露代理账号、地址或公网 IP。各槽有固定时区差异,但固定环境差异只能说明换槽首次可能冷启动,不能单独解释随后同槽反复低命中。
账号与客户端
- 多账号池,订阅档位未核实,不披露真实账号/凭据。
- 经 new-api 中转到 /v1/messages。UA 自报 claude-cli/2.1.301 (external, cli),不能仅凭 UA 验证真实客户端。
- 主模型 claude-opus-5-5,effort high,包含工具调用。夹带其他模型的辅助请求未混入下面 Opus 用量表。
- 排查时配置:official_cc_passthrough 未设置(有效 false);detect_proxied_official_cc=true;cache_ttl=1h;cache_breakpoints={enabled:true,preserve_client:true,system_tail:false,tools_tail:false,messages:"cli-hop"}。这不是逐请求历史配置快照。
4.1 VM 创建 / 槽位日志
4.2 请求日志
来源:只读核对生产 SQLite usage_logs、request_attempts.usage_json、控制面 Docker 日志。A/B/C/D 是匿名账号/槽位;C1-C4 是请求代号,不是真实 ID。
同一新账号 C 四条 Opus 请求,按实际执行顺序:
请求 | input | cache creation | cache read | output | read/总输入
C1 | 2 | 838500 | 0 | 265 | 0%
C2 | 2 | 823969 | 14621 | 18 | 1.7435%
C3 | 2 | 831254 | 14621 | 710 | 1.7285%
C4 | 2 | 825726 | 14621 | 867 | 1.7399%
分母=input+cache_creation+cache_read,input=2 不代表只发了2 token。
均 HTTP 200,attempt_count=1,执行区间不重叠。写入全部在 1h 分项,写入总量与 TTL 分项求和一致,attempt 与汇总 usage 一致。
这是 kernel 返回/控制面记录值,不是独立抓取的 Anthropic 原始 SSE;不能完全排除更下层统计问题。
同一入站 Session 标识下相邻 Opus 的 cache read 顺序:
A:41次,0 → 822811 → 824752 → … → 837283
B: 4次,0 → 14620 → 14620 → 837873
C: 4次,0 → 14621 → 14621 → 14621
D: 4次,0 → 14620 → 14620 → 838028
A 第二条已大量读取;B/D 后来恢复,不是新账号永久不能缓存。C 只观察四条,不能推断继续使用也不会恢复。相同入站标识不足以证明每条都是严格追加的同一分支。
匿名控制面日志:
[cache-prefix] request <C2> turn 3 broke at tools
[cache-prefix] request <C3> turn 4 broke at messages[912]
[cache-prefix] request <C4> turn 5 broke at messages[912]
tools 分歧前夹带不同模型;当时 tracker 按 account/session 而非 model 隔离,不能据此认定同模型工具改变。A 的高命中轮次也出现过 messages[912] 分歧,不能只凭该警告断定缓存必然全部失效。
5. 源码定位(可选)
以下均为固定原版 v1.3.188 commit 的源码:
- handle-protocol.mjs:officialPassthrough / prepareCliHopBody:只有识别为 official 且开关 true,才传 preserveSystem:true;同一条件也决定跳过平台 persona 改写。
- crs-persona.mjs:开关默认 false:只接受 compatibility.official_cc_passthrough===true。
- outbound-attempt.mjs:清除后硬编码 rewrite:!preserveSystem 时 removeCacheControlFields,再 applyMessageBreakpoints(...,'rewrite');未直接使用 preserve_client / messages=cli-hop 选择保留策略。
- cache-ttl.mjs:倒数第二个 user 与消息尾部断点:rewrite 丢掉已有标记再选两个位置,没有专门保护历史首次变化之前的稳定锚点。
- cli-hop-body.test.mjs:透传混合 TTL 与断点保留 已覆盖透传;本报告关注开关关闭时另一条路径,不是认为新版完全没有缓存修复。
**已确认:**Node 默认 rewrite 会丢掉客户端稳定断点;缓存策略与 system/persona 透传绑定。
**待证解释:**旧账号可能积累变化点前的早期缓存条目,新账号首次接入完整长历史时只在变化点后的尾部写入;历史中间再变,两条新条目都无法复用。这能解释“旧号正常、新号反复写入”,但没有最终 wire,不能作为历史事故唯一归因。
建议与咨询作者
以下仅为修复讨论方向,尚未真实上游验真:
- 把“保留调用方缓存断点”和“采用平台 persona/system”拆开,或让 CLI-hop 明确遵循 preserve_client。继续校验四断点上限、位置和 TTL 顺序,不无条件叠加。
- 必须 rewrite 的长历史冷迁移,在断点预算内保留变化点前的稳定长前缀锚点,再安排必要尾部锚点。结构摘要可用于选择位置,不能当成已迁移上游缓存。
- 增加“新账号首轮写入 → 同账号下一轮历史局部改变”回归;比较入站、Node、最终 CLI wire 的结构哈希与断点,真实验证再对照 usage。
想请教:
- 默认 rewrite 丢弃客户端断点是协议要求,还是可调整策略?preserve_client=true 对 CLI-hop 不生效是否应在 UI/文档明确?
- 需要平台 persona,又希望保留客户端稳定断点,目前推荐什么配置?是否只能开 official_cc_passthrough 并接受保留客户端 system?
- kernel/CLI 是否在 Node 后另有断点规则,会修正这个反例?推荐怎样在不暴露提示词/凭据的情况下确认最终布局?
- 对冷账号后续反复重写,作者更推荐保留客户端断点、稳定锚点,还是已有其他修复?如需补证,请告知最小采集项。
已尝试的操作 / 补充
- 生产观测来自 v1.3.178 定制版;相关 outbound-attempt/cache-ttl/identity-rewrite/cache-prefix 已与当时运行容器逐文件核对。仍不能排除定制或其他层影响,所以另在原版 v1.3.188 离线复现机制。
- 暂未为本报告升级生产或发送真实模型请求;新版本实际用量/最终 CLI 出站尚未验真。历史请求无 debug body,session dump 未启用,无法事后重建原始正文。低命中也不是用户恶意行为的证据。
- 已核对最新正式版 v1.3.188;发帖前 main 为 2a5f8bd,与该 tag 比较,上面四个请求处理/断点/开关源文件未变。复现对象仍为固定 tag。
- 已搜索开放/关闭 Issues 的 cache、缓存,并阅读 在claude cli/desktop 缓存命中很低 #6/缓存命中异常 #9/[Bug] 消息级 effort 被删除或提升到顶层,切换后无法保留缓存语义(v1.3.150) #383/[Bug] cli-hop 缓存前缀存出站、查入站,稳定热会话被误判 prefix_break(1.3.154 / 1.3.164) #416/[Bug] 请求收尾误改 tool_result 中的计费头示例,旧历史每轮变化导致缓存反复重写(1.3.154 / 1.3.164) #417/[Feature] 支持按独立会话分流,避免同设备多会话集中耗尽 Pro 账号并反复重建缓存 #464;如重复请指正。[Feature] 支持按独立会话分流,避免同设备多会话集中耗尽 Pro 账号并反复重建缓存 #464 关注减少迁移,本报告关注迁移发生后新账号内持续复用。fix: reuse search-history repairs and align Claude 2.1.296 #517 是搜索历史 400→修复路径,本样本全部 200/单 attempt,不直接视为同一问题。
- 已阅读 ISSUE_GUIDE.md。全文仅匿名计数、公开源码与合成脚本,不含真实请求/会话 ID、账号、生产地址、代理信息、凭据、提示词或数据库。
- 复现频率:上述原版 Node 离线断点改写必现;真实低命中是一个长会话窗口中的多次观测,没有统计全量用户发生率,也未完成新版真实上游复验。
Activity