Repository navigation
image_full 无法被正确调用:同一张图每轮重复降采样(38 次)、restore 提示注入 38 次,模型只能猜 ref → 每轮一次失败+重发,缓存命中从 75% 掉到 28% #3266
Description
环境
- billion-context
0.1.197(Windows / npm 全局),bili codexlane,protocol=responses,模型gpt-6.1-sol - 会话:
01a134c1-9f0e-7863-9ad6-e6237bd181f7 - 配置:
compress.stripImages=true、compress.imageCompression.enabled=true(现已由用户关闭以止血)
现象
会话里连续出现红色失败回执(用户截图):
[ACP] image_full FAILED: no downscaled image is recorded for m04963 — it was passed through untouched or never carried an image
[ACP] image_full FAILED: no downscaled image is recorded for m04974 — it was passed through untouched or never carried an image
[ACP] image_full FAILED: ref m04986 does not exist in this session
证据
1)只有一张图被降采样,却被重复降采样 38 次、提示也注入 38 次
16:05:16 [acp-image] m04327: image/png 214575B→~3410B (1105→105 tok, saved 141165B)
16:05:16 [inject] image-full restore note appended as trailing user turn (234 chars)
16:06:11 [acp-image] m04327: image/png 214575B→~3410B …
16:06:11 [inject] image-full restore note appended as trailing user turn (234 chars)
16:07:11 …(同一 ref、同一张图,反复)
统计(当前 bili.log 窗口):[acp-image] m04327 = 38 次;image-full restore note appended = 38 次;成功 image_full = 0 次。
2)模型调用的 ref 全是它自己的近期消息(在"猜")
16:04:20 [acp-loop] round 1: 2 call(s): image_full({"ref": "m05020"}) |
16:04:46 image_full m05026: rejected (no downscaled image is recorded …)
16:05:33 image_full m05033: rejected
16:06:34 image_full m05040: rejected
16:07:26 image_full m05047: rejected
16:08:24 [acp-loop] round 1: 2 call(s): image_full({"ref": "m05051"}) |
ref 每轮 +7 递增,典型"顺着最新 ref 往后试"。DB 交叉核对(sessions/index.db 的 messages 表):
| 被调用的 ref | 实际角色/类型 | 是否图片消息 |
|---|---|---|
m04963 / m04974 / m04986 |
assistant / tool-call | ❌ |
m05026 |
tool / tool-result | ❌ |
m05033 / m05040 / m05047 |
同上(模型自己的近邻消息) | ❌ |
| 正确的目标 | m04327(用户贴图,被降采样的那一张) |
✅ |
3)代价:每轮一次失败就重发整条请求
每次 rejection 都跟一条 [acp-loop] round 1: proxy tool executed; re-requesting so the model sees the result;面板 缓存命中率 当前 28.2%(近 10 次平均 75.2%)——每轮重发都是几十 k token 的实付。
判断
no downscaled image is recorded= 该 ref 存在但不是图片消息;ref does not exist in this session= 纯猜测的 ref。两种都是"模型不知道正确 ref"的表现。- 正确 ref(
m04327)每轮都在被降采样、note 也每轮注入,说明 note 没有起到指引作用。两个可能:- note 在转发上游时被"临时内容剥离"逻辑一起剥掉(日志里同时存在
stripped N ACP status marker line(s) from incoming history (issue #1029)); - note 送到了但模型忽略,改用自己的近邻 ref(ref 递增行为支持这一解释)。
- note 在转发上游时被"临时内容剥离"逻辑一起剥掉(日志里同时存在
- 无论哪种,缺少失败护栏:现在会无限重复"注入 note → 模型猜 ref → 失败 → 重发"。
Ask
- 失败护栏:同一 session 内
image_full连续失败 ≥2 次后,停止注入 restore note(或在 note 里明确"本会话当前没有可还原的图片"),避免无限猜 ref。 - 提示可达性可诊断:确认 restore note 不会被 incoming-history 的临时标记剥离;若被剥离,打一条 debug 日志(现在从日志无法判断 note 是否到了上游)。
- 降采样应 memoize:同一 ref + 同一内容不应每轮重算(本例 38 次 × 214KB→3.4KB),按
(ref, content_hash)缓存即可,省 CPU 也省日志体积。 - 失败回执里给出正确 ref:rejection 文案可以带上"本会话可还原的 ref 列表(或:无)",让模型一次命中,而不是靠猜。
1. 唯一被降采样的图片:m04327(重复 38 次)
[acp-image] m04327: image/png 214575B→~3410B (1105→105 tok, saved 141165B/0 tok)
[inject] image-full restore note appended as trailing user turn (234 chars)
同一对日志在 16:05:16 / 16:06:11 / 16:07:11 … 每轮重复。
当前 bili.log 计数:
| 计数项 | 次数 |
|---|---|
[acp-image] m04327:(降采样) |
38 |
image-full restore note appended(提示注入) |
38 |
成功的 image_full(非 FAILED 的 result) |
0 |
被降采样的 ref 分布(去重后只有一个):
m04327 38
2. 模型的 image_full 调用序列(ref 在猜)
16:04:20 [acp-loop] round 1: 2 call(s): image_full({"ref": "m05020"}) |
16:04:46 [acp-image] image_full m05026: rejected ([image_full FAILED: no downscaled image is recorded for m05026 — it was passed through untouched or never carried an image])
16:04:46 [acp-loop] round 1: 1 call(s): image_full({"ref":"m05026"})
16:04:46 [acp-loop] → image_full result: [image_full FAILED: no downscaled image is recorded for m05026 …]
16:05:33 [acp-image] image_full m05033: rejected (…same…)
16:06:34 [acp-image] image_full m05040: rejected (…same…)
16:07:26 [acp-image] image_full m05047: rejected (…same…)
16:08:24 [acp-loop] round 1: 2 call(s): image_full({"ref": "m05051"}) |
用户侧截图里的三条(同一模式的更早片段):
[ACP] image_full FAILED: no downscaled image is recorded for m04963 — it was passed through untouched or never carried an image
[ACP] image_full FAILED: no downscaled image is recorded for m04974 — it was passed through untouched or never carried an image
[ACP] image_full FAILED: ref m04986 does not exist in this session
失败 ref 汇总(重启后 16:04–16:08):m04999 / m05004 / m05020 / m05026 / m05033 / m05040 / m05047(各 1 次,另有 m05051 再次发起)。每次失败都跟一条 proxy tool executed; re-requesting。
3. DB 交叉核对(~/.local/share/billion-context/sessions/index.db,messages 表)
select ref, role, kind from messages where session_id=? and ref in (...)| ref | role | kind | 结论 |
|---|---|---|---|
| m04963 | assistant | tool-call | 模型自己的工具调用,非图片 |
| m04974 | assistant | tool-call | 同上 |
| m04986 | assistant | tool-call | 同上 |
| m05026 | tool | tool-result | 上一次失败的回执,非图片 |
| m04327 | (被降采样的那一张,用户贴图) | — | 正确目标 |
→ 模型引用的全是它自己近邻的消息 ref;正确 ref(m04327)在提示里给了 38 次,但从未被采用。
4. 代价(面板数据)
| 指标 | 值 |
|---|---|
| 缓存命中率(当前) | 28.2% |
| 缓存命中率(近 10 次平均) | 75.2% |
| 最近一条助手消息 | 输入 198 / 缓存读取 103,296 / 缓存写入 0 / 输出 355 |
| 单轮耗时 | 71m47s(会话累计)、首字延迟 50.8s |
每次 rejection 都触发一次整请求重发,且重发时前缀已变(re-requesting so the model sees the result),命中率因此塌陷。
5. 采集命令(只读)
$f="$env:USERPROFILE\.local\state\billion-context\bili.log"
# 降采样与提示注入计数
(Get-Content $f | Where-Object { $_ -match "\[acp-image\] (m\d+):" }).Count
(Get-Content $f | Where-Object { $_ -match "image-full restore note appended" }).Count
# image_full 失败 ref 分布
Get-Content $f | Where-Object { $_ -match "image_full (m\d+): rejected" } |
ForEach-Object { if ($_ -match "image_full (m\d+): rejected") { $matches[1] } } | Group-Object
# 调用序列
Get-Content $f | Where-Object { $_ -match "image_full" } | Select-Object -Last 20# DB 核对被调用 ref 的真实角色
python3 - <<'EOF'
import sqlite3
con=sqlite3.connect("file:$HOME/.local/share/billion-context/sessions/index.db?immutable=1", uri=True)
for ref in ("m04963","m04974","m04986","m05026","m04327"):
print(ref, list(con.execute(
"select role,kind from messages where session_id=? and ref=?",
("01a134c1-9f0e-7863-9ad6-e6237bd181f7", ref))))
EOF
Activity
ranxianglei commentedon Oct 15, 2026
(ai: vllm-qwen/qwen3.8-27b)
查重: 不重复。 全库检索(含已关闭)只有本 issue 涉及 image_full;相关的 ranxianglei/acp-kernel#353 是该功能的原始设计 issue(另一个仓库、功能立项性质),不是同一问题的重复报告。
版本核对: 你跑的是 v0.1.197。我核对了 v0.1.197 → master 之间所有触碰图像压缩路径的提交——没有一项改动这个功能(期间只有持久化 fsync 自愈 #3157/#3163 等无关修复),所以以下分析直接适用于你的构建。
审核结论(三步 triage)
① 复现/查证 — 问题真实存在,且比表面更深一层。
src/image-compress.ts的imageFullTrailingNote()只把数量传给 kernel 的buildImageFullSystemNote(count),提示原文就是 "N image(s) were reduced … call image_full with that message's ref"——没有任何 ref。模型只能从自己最近见过的 ref 往前猜(+7 递增正是它在按消息序号试错)。你列的 4 点里第 2 点是根因,其余都是它的下游症状。[acp-image] m04327: ...这行日志不带 session id,这是可诊断性缺陷,我会一并修。② 层次判断: 表面现象 = 模型猜 ref 失败循环 + 缓存命中率崩;根本原因 = (a) restore 提示/拒绝回执都不携带可操作信息(ref 列表),(b) 降采样记账对状态重置不鲁棒且不可诊断。你的 4 点建议全部打在正确层次上,我按此实现。
③ 方案评估 — 采纳你的 4 点,全部在宿主侧(src/)修,不动 kernel:
[inject]日志带上 ref 列表/session id——可诊断性;开始实现,完成后开 PR(Closes #3266)并在本帖回复链接。
nsdbjh commentedon Oct 15, 2026
关键新证据:宿主侧根本没有这个工具
codex 的 stderr(由宿主 Nest 侧捕获,
CodexJsonRpcClient,level=40):今日累计 45 次,与 bili 侧每次
image_full({"ref": …})一一对应 →image_full不在宿主(codex lane)的工具表里,工具路由直接判 unsupported。判断:三重叠加
image_fullimage-full restore note appended as trailing user turn× 43error=unsupported call: image_full× 45no downscaled image is recorded for m05xxx …× 22三者叠加 → 模型无论怎么调都拿不到原图 → 换 ref 再试 → 无限循环。
代价
每次 rejection 都跟一条
[acp-loop] round 1: proxy tool executed; re-requesting so the model sees the result,即整条请求重发;面板缓存命中率由近 10 次平均 75.2% 掉到当前 28.2%,每轮重发都是几十 k token 的实付。背景(非因果结论)
死前 22.5 分钟里 13 次失败重发 + 42 次重降采样,每轮都要重新投影该会话的 20.8 MiB / 5,053 条历史。该循环与随后的一次进程异常退出是否有因果关系未证实(无 WER、无 dump、无异常栈,相关证据已被启动方式的 stderr 截断销毁),此处仅作为"循环不只是浪费 token"的背景提供。
新增两条 Ask(优先级高于正文那四条)
image_full——宿主已经在报unsupported call,此时注入只会制造死循环。(正文原 4 条 Ask 仍然有效:失败护栏 / 提示可达性可诊断 / 降采样 memoize / 回执给出正确 ref。)
用户侧止血(已做)
compress.imageCompression.enabled → false:不再降采样、不再注入 restore note,该类失败立即消失(代价:图片按原尺寸进上下文)。ranxianglei commentedon Oct 15, 2026
(ai: vllm-qwen/qwen3.8-27b)
这条新证据把死循环的最后一环钉死了——问题比「模型猜错 ref」更根本:restore 通道在 codex lane 上结构性不成立。
机制分析(已对照 codex 源码与测试套件核实)
error=unsupported call: image_full不是无害噪音。codex 的工具路由器对未知工具的处理是(codex-rs/core/src/tools/registry.rs,且其自身测试 compact.rs / search_tool.rs 明确断言了这个行为):给该调用合成一条function_call_output = "unsupported call: image_full",写进自己的会话历史,并在下一次请求里原样重发。于是同一次
image_full调用在两份历史里留下两条互相矛盾的记录:no downscaled image is recorded for m05xxx …unsupported call: image_full后果链:① 模型永远拿不到干净的成功——即使猜对 ref,codex 的历史里这次调用仍记为 unsupported;② 每次尝试都在上游缓存行与客户端重发历史之间制造一个永久分叉点 → 前缀缓存命中率持续衰减(75.2%→28.2%);③ 每轮一次失败 + 整条 re-request,几十 k token 实付。你面板里的三个现象(note×43、host error×45、rejection×22)就是这一条链的三个观测面。
结论与方案更新
你的两条新 Ask 判定正确,升为最高优先。更新后的修复(全部宿主侧,不动 kernel):
[acp-image]日志补 session id——你那 66 次重复降采样到底是状态丢失还是 session id 变化,下个版本一眼可辨。另注:
acp_retrieve(CCR)在 codex lane 上有同样的结构风险(同为 bili 注入工具、宿主无法路由),但 CCR 关断涉及占位符替换语义,属行为决策,本次不动,我在 issue 里单列等你判断。开始实现,完成后开 PR。
ranxianglei commentedon Oct 15, 2026
(ai: vllm-qwen/qwen3.8-27b)
修复已提交:PR #3270 — #3270 (分支
2026-10-15_image-full-lane-gate,已 rebase 到最新 master,typecheck+build+定向测试本地全绿,全量回归以 CI 为准)。全部改动在宿主侧(src/),未动 kernel。以下按八问作答:1. 是什么问题
codex lane 上 image_full 的「还原通道」结构性不可用:bili 向模型承诺了一个宿主根本路由不到的工具。模型每轮按提示调用 → 宿主报
unsupported call并把合成错误写进自己的重发历史 → bili 服务端又执行一次并回执拒绝 → 整条请求重发。提示里只有计数没有 ref,模型只能从自己最近的消息往前猜(你的 +7 递增模式),猜错必然,于是死循环 + 缓存命中率 75%→28%。「同一张图每轮重复降采样」是叠加的第二机制(见 2)。2. 什么原因导致的
三层:① 能力错配——bili 注入的工具声明只对「能路由该工具的宿主」成立;codex 的工具路由器对未知工具不是静默忽略,而是合成一条
function_call_output="unsupported call: ..."写进会话历史并原样重发(已对照 codex-rs/core/src/tools/registry.rs 源码及其自身测试核实)。这使每次尝试在客户端历史与上游缓存行之间制造一个永久分叉点,且即使猜对 ref,codex 历史里记的仍是 unsupported——该 lane 上还原路径从根上走不通。② 提示缺信息——restore note 只给计数不给 ref 列表(kernel 的buildImageFullSystemNote(count)设计如此),模型无依据只能猜。③ 重载后状态不一致——进程重启后持久化的 shrink 记录还在、内存指纹表没了,守卫判定「有记录但指纹未知」→ 跳过压缩 → 原始字节从此直发上游,功能静默关闭且每轮日志看似仍在活动。3. 影响范围与严重程度
所有 responses+codex lane 且开了
compress.imageCompression.enabled并降采样过截图的会话:每轮实付几十 k token 的重发 + 缓存命中持续衰减 + 模型永远拿不到原图(功能性失效,不只是浪费)。其他 lane(anthropic/openai/google)不受门控影响,仅提示/回执文案增强。你已用enabled=false止血,与本 PR 不冲突(关掉后两者都不发生)。4. 怎么修复的
[acp-image]info 行补 session id——「同 ref 反复降采样」下个版本一眼可辨是状态丢失还是 session id 变化。提示可达性部分结论:note 是出站 ephemeral user message,客户端永不回传,incoming-history 剥离路径碰不到它(你日志里 note×43 即证据),无需改。5. 是否有回归问题
逐项审过:既有 image 测试 14 例未改断言全绿;wire-contract golden 未动(工具 schema 没变);plugin mode 本就排除(stamp 条件含 !pluginMode,同旧行为);未 stamp 的路径默认 armed = 完全保持 #3266 前行为;master 新合入的 lazy-metadata proxy(#3050)对新 metadata 写入透明安全(set 触发物化,且 stamp 每请求覆盖)。PR 正文附了 7 行「旧→新」行为变更表。
6. 是否破坏缓存
不新增破坏:note 字节跨轮稳定(排序 ref、固定上限);护栏抑制反而移除了一个随失败变化的元素;重载会话的 wire 字节从「原尺寸直发」变「重新降采样」是一次性的有意修正(表 f 行)。
7. 是否改了设计方向
轻微转向:#1095 的单一 enabled 开关拆成两个正交门控(降采样通道 / 还原通道),因为「宿主能否路由该工具」是 per-host 属性而非功能开关。遗留一项待你决策:
acp_retrieve(CCR)在 codex lane 上有同样的结构风险(同为 bili 注入、宿主无法路由),但关断涉及占位符替换语义,属行为决策,本 PR 不动——要不要同样按 host 门控,等你拍板。8. 是否引入新配置
无。config surface 不涉及;新增的只是内部 per-request stamp 和 stats 字段
imageFullFailStreak(持久化统计,非配置项)。判定:可以进入 review/合并流程;CI 已在推送的 head 上跑,红了我会定向排查。
fix: #3266 persist imageFullFailStreak across session reloads
nsdbjh commentedon Oct 15, 2026
追加一条观察:成功还原后,同一张图又立刻被重新降采样
(日志时间为 UTC;会话
01a134c1-9f0e-7863-9ad6-e6237bd181f7)image_full成功那次的回执明确承诺"本会话内保持全分辨率":但同一张图在前后几秒内仍在被降采样:
→ 回执承诺的 "full resolution applies for the rest of this session" 没有真正生效。
这是长期现象,不是一次抖动
imageShrunkCountm04327)imageFullRestoresimageBytesSavedimageTokensSaved[acp-usage]img-saved=**0tok**/787KB x37、0tok/13359KB x136…(全部 0 tok)[acp-image]行的 token 对(92/92 条)1105→1105 tok(从未变过)两个结论
① 重复降采样是纯浪费。 当前
maxDimension: 1600对 1440×900 的截图不触发缩放,只做重新编码:字节 214KB→73KB,但 token 恒为 1105,一个没省(imageTokensSaved=0且每轮img-saved=0tok即证据)。反复做的只有编码 CPU 与日志体积。② "已还原"状态没有保住。 客户端每轮重发的是原始字节,bili 每轮重新看到未压缩的图;若"本会话已还原(full-res armed)"是按请求/内存态保存,就会在折叠重投影或进程重启后丢失,于是反复走"重新压缩"。这与本 PR 修的"重载后尺寸识别"(shrink 记录还在、内存指纹表丢了)属于同一类状态丢失。
Ask(可选,不阻塞本 PR)
(ref, content_hash)持久化,并在下次遇到同一原始字节时短路:不再重新降采样、不再重复写[acp-image]日志。[acp-image]行里区分情形(如first shrink/re-shrink after restore)——本 PR 刚加的 session id 已让"状态丢失 vs session id 变化"可分,这一条能把"是不是还原状态没保住"也变成一眼可辨。fix: #3266 review fixes — presence-preserving streak reload; regenera…
fix: #3266 require the active downsample pass in the image_full tool-…
ranxianglei commentedon Oct 15, 2026
(ai: vllm-qwen/qwen3.8-27b)
这条观察很关键——它把「重复降采样」和「restore 承诺不生效」并成了同一个状态问题。逐条回应:
① 重复降采样是纯浪费 —— 确认。
1105→1105 tok证明 maxDimension=1600 对 1440×900 根本没触发缩放,每轮只做了重新编码(214KB→73KB,token 一个没省)。PR 新增的尺寸识别(重载后按记录的 originalBytes 认出原始字节)把这类情况收敛为:同一进程生命周期内只重编码一次,之后字节稳定、不再刷日志。跨进程重启仍会各重编码一次(编码缓存是内存态),属可接受代价。② 「已还原」没保住 —— 机制上有两条路:
[acp-image]行加上[sessionId]前缀,升级后一眼可辨:sessionId 每轮变 = 会话身份在漂;不变而其他状态重置 = 状态没活过一轮。你侧的止血(enabled=false)与 PR 方向一致:codex lane 上本来就该保留降采样、撤掉永远走不通的 restore 承诺。
PR 最新状态:#3270 已更新到 fbc0b1d。CI 红的 6 个 test 分片完成分诊:4 个是存储快照的元数据摘要漂移(真实回归,已修复——无条件打戳是新行为,四个 storage-render 快照按既有先例重新生成);2 个是 Windows 跑机时序抖动(断言零差异、对应 Ubuntu 分片通过、本地复跑全绿,证据见 PR 评论)。另补了两处:失败计数跨重启持久化(此前重启会悄悄丢掉「≥2 次连续失败即停注提示」的护栏)、四条 wire 的工具注入改为「降采样通道激活 且 宿主可路由」双门控。等本轮 CI 全绿即可合并验证。
ranxianglei commentedon Oct 15, 2026
(ai: vllm-qwen/qwen3.8-27b)
本轮收工状态汇总(细节见上方各条回复):
2026-10-15_image-full-lane-gate,headfbc0b1daf)。覆盖本 issue 全部 6 条 Ask:codex lane 不再注入 image_full/restore 提示(改纯文本「不可还原」)、note 与失败回执列出真实可还原 ref、连续 ≥2 次失败自动停注(成功复位、streak 随会话持久化)、重载后按 originalBytes 尺寸识别原始字节重新降采样(不再永久直通)、[acp-image]日志带[sessionId]。合并由你操作;重跑结果如有异常我会跟进。
Merge pull request #3270 from ranxianglei/2026-10-15_image-full-lane-…
fix(review): finish the #3266 removal — orphaned kernel image types/s…
test(#3295): make the #3266 back-compat corpus pin real
Merge pull request #3295 from ranxianglei/2026-10-15_remove-image-com…
Merge branch 'master' into pr3298 review test (union resolution: #3266 …
Merge branch 'master' into 2026-10-15_ptc-runcode-support (#3298)
fix: ranxianglei#3266 gate image_full restore channel per host lane; …
fix: ranxianglei#3272 disarm CCR arming on the codex responses lane (…
feat!: remove image pre-compression and the image_full tool (ranxiang…
Merge branch 'master' into 2026-10-15_reasoning-drop-attribution
fix(#2388): guard CompressionState clone fidelity; complete kernel hy…
fix(#2388): guard CompressionState clone fidelity; complete kernel hy…
docs: fill in PR references and drop dangling grounding-doc pointers (r…
test: golden drive refuses to run on a stale kernel dist (#3325 lesson)
fix: warn INERT for removed compress.absorb / compress.imageCompressi…
fix: warn INERT for removed compress.absorb / compress.imageCompressi…
feat: egress image downscale + read_original_image (#3400)
fix: un-inert compress.absorb after its revival — align the #3344 rem…