Skip to content

Archive-then-fold: default folding for archivable media (Closes #2607, Fixes #2609) - #2640

Merged
ranxianglei merged 5 commits into
masterfrom
2026-10-10_archive-then-fold
Oct 10, 2026
Merged

ranxianglei merged 5 commits into
masterfrom
2026-10-10_archive-then-fold

Conversation

@ranxianglei

Copy link
Copy Markdown
Owner

kernel-change: media messages were exempt from folding forever (#1188), which made image-heavy sessions' input floor grow monotonically and fragmented compressible ranges (billion-context#2607). Owner approved the archive-then-fold design in the #2607 thread (2026-10-10): archivable inline media folds like text with its pixels archived by ref BEFORE the fold; only payloads whose bytes bili cannot store stay pinned. The host side cannot express this — the exemption lives in the kernel's ref/range/apply pipeline.

Closes #2607 · Fixes #2609

What changes

Kernel (acp-kernel 0.0.111 → 0.0.112):

  • New exported hasUnrecoverableMediaPayload alongside hasMediaPayload: pins only media whose bytes bili cannot store locally — remote-URL images, DeepSeek {type:"file"} refs, google fileData/videoMetadata (the last was previously undetected entirely — Google wire 的 fileData(URL 引用)图像不被识别为媒体负载:可被折叠、折叠后静默丢失且无警告(#1188 不变量缺口) #2609). Archivable = inline base64 / data-URL, including data-URLs inside anthropic url sources (mirrors what the host's archiver actually stores).
  • The three 这个插件压缩上下文,会导致deepseek识图出问题。 #1188 exemption sites switch to the new predicate: assignRefs protection, buildCompressibleRanges barrier, applyCompression exclusion + warning. Archivable media now folds like text: it gets a numeric mNNNNN ref, no longer fragments compressible ranges, and enters block coverage.
  • Ref migration: legacy sessions that persisted media under BLOCKED mint a fresh numeric ref at the next prepare — fresh number only, never a reuse (id contract intact).
  • Fold warning rewritten: names unrecoverable media specifically and points at decompress({ imageRef }) for the archivable path instead of suggesting stripImages.

Host:

  • The recovery index (buildIncomingImageIndex) runs on every session request instead of only when stripImages is enabled, plus a merge-refresh after prepare — when the turn's refs are assigned — so images in a fold range spill to <state>/retrieve/img/<session>/ under their ref before any fold can happen. This is what makes default folding lossless for archivable media.
  • CONFIGURATION en+zh document default folding and the one-image residual; e2e lanes updated to the folded-forward shape.

Behavior changes (old → new)

Aspect Old (≤ v0.1.190) New Why
Archivable inline media (base64/data-URL) in a fold range Never folded; stayed on the wire forever (monotonic floor), fragmented compressible ranges, repeated "Excluded N… enable stripImages" warnings Folds like text: placeholder + ref in the summary input, bytes archived before the fold, restorable via decompress({ imageRef }) Owner decision (#2607): the old behavior degrades image-heavy sessions monotonically with no recovery path
Unrecoverable media (remote URL / file ref) Never folded + warning suggesting stripImages Still never folded (exemption unchanged); warning no longer suggests stripImages (it cannot help these payloads) stripImages cannot recover bytes bili never had
Google fileData / videoMetadata URLs Undetected as media → silently folded away (data loss) Detected → pinned (never folded) #2609
Legacy sessions with BLOCKED media refs Media permanently unreferenceable Fresh numeric ref minted at next prepare Restorability requirement from the #2607 approval ("图片要分配 id")
<state>/retrieve/img/<session>/ disk usage Written only with stripImages: true Written for every session carrying archivable media (existing 7-day TTL applies) Archive-before-fold requires it
stripImages / stripImagesKeepRecent Only way to release old image bytes Semantics unchanged; now largely redundant (default folding releases bytes at compression events); remains as the lever for stripping UN-folded history between events No config-surface change

Residual (documented, accepted): the kernel pins the session-opening user message through folds (first-user pin, prune.ts rebuildMessages / DESIGN.md §8.1 — strict providers reject conversations with no user message). If that opening message carries an inline image, that single image stays on the wire for the session's life; every later image folds away. The floor becomes constant (one image), not growing. Changing pin semantics is a kernel product decision beyond this scope — flagged here for owner review rather than silently shipped.

Config surface

No new fields, no renames, no re-semantics of existing keys. compress.stripImages and compress.stripImagesKeepRecent keep exactly their current semantics; docs updated to state they are now an extra lever on top of default folding.

Tests

  • Kernel suite 1287/1287, incl. the reworked media-payload.test.ts (19 tests): full hasUnrecoverableMediaPayload matrix across all four wires (incl. google fileData/videoMetadata/functionResponse nesting for Google wire 的 fileData(URL 引用)图像不被识别为媒体负载:可被折叠、折叠后静默丢失且无警告(#1188 不变量缺口) #2609), assignRefs archivable→numeric + BLOCKED→fresh-ref migration, range span/unspan split, applyCompression fold/exclude split with the new warning text.
  • Bili targeted set (19 image/strip/decompress files) 135/135, incl. new tests/fold-images-archive.test.ts (always-on archive: spill file appears under the session dir with stripImages OFF, stable across turns) and the rewritten fix-781 e2e lanes (placeholder + ref reach the summary input; the forward carries no folded image bytes except the pinned opener).
  • npm run typecheck + npm run build green; kernel built and consumed by the bili suite via file: link.

Full regression left to CI.

ework-agent added 2 commits October 10, 2026 17:27
…ads (#2607)

Split hasMediaPayload into the media-presence predicate and a new
hasUnrecoverableMediaPayload that pins only payloads whose bytes bili cannot
store (remote URLs, DeepSeek file refs, google fileData/videoMetadata).
Archivable inline media now folds like any other message: assignRefs mints it
a numeric ref (legacy BLOCKED entries migrate to fresh numbers, never reusing
old ones), buildCompressibleRanges no longer treats it as a barrier, and
applyCompression folds it instead of excluding it. Unrecoverable payloads keep
the #1188 never-fold exemption; the fold warning now names them and points at
decompress({ imageRef }) for the archivable path. Also closes the google
fileData detection gap (#2609).

package-lock.json: acp-kernel 0.0.110 -> 0.0.112 (this bump; 0.0.110 was
already stale vs master's 0.0.111) and pi-subagents 0.1.3 -> 0.1.4 (pre-existing
drift synced by npm install --package-lock-only).
…#2607)

The recovery index previously ran only with stripImages enabled, but archivable
media now folds by default (#2607) and its pixels must exist BEFORE the fold:
run buildIncomingImageIndex on every session request (gate drops the flag) and
merge-refresh it after prepare, when the turn's refs are assigned, so images in
the folded range spill to <state>/retrieve/img/<session>/ under their mNNNNN
ref and decompress({ imageRef }) restores them. New e2e test pins the always-on
archive; fix-781 e2e lanes now assert the folded-forward shape (placeholders
with refs reach the summary input, image bytes leave the forward except the
kernel's pinned session-opening user message - first-user pin, prune.ts /
DESIGN.md section 8.1). CONFIGURATION en+zh document default folding and the
one-image residual.
@github-actions

github-actions Bot commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

📦 Built Package Artifact

Branch: 2026-10-10_archive-then-fold (b6af4b7)

Option A — Install this exact build (recommended)

npm install -g billion-context@0.0.2892-pr.2640.bf52d39

This exact version is immutable and reproducible. The rolling pr tag tracks the newest test build across all PRs.

Option B — Download artifact

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

Install instructions are refreshed on each push.

@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

[bot] 🏷 收到,开始 review #2640(archive-then-fold:可归档媒体默认折叠)。这是 kernel 变更 + host 侧改动,按 Kernel Boundary 走人工审查层级。我先拉分支核对代码、确认 CI 状态,再按结构化八问给结论。稍等。

@ranxianglei

Copy link
Copy Markdown
Owner Author

🤖 Powered by ework · qwen3.8-27b

[bot] 🏷

结论先行:方向正确、实现干净、测试有牙。唯一硬红灯是 kernel-guard(机械性版本冲突,非代码问题);另分支落后 master 48 提交需先 rebase。因含 kernel/** 变更,按 Kernel Boundary 属人工合并层,我不代合。

我做了什么:checkout PR 分支,通读全部 14 个改动文件(kernel 源 6 含 package.json、kernel 测试 1、host 2、文档 2、bili 测试 2、lockfile 1),逐 wire 核对「可恢复 ⇔ 已落盘」镜像不变量与 id 不复用契约,核对了 CI 状态与断言有效性。本地未预跑全量——node_modules/kernel-dist 未构建,装依赖等于重复 CI 正在跑的 ubuntu+windows 双矩阵,按本地测试纪律交给远程 CI。


结构化八问

  1. 是什么问题:建议:compress.stripImages 默认开启——否则带图消息不可压缩会形成只升不降的输入地板撑爆上下文 #2607——可归档的内联媒体(base64/data-URL)此前被永久豁免折叠,图片密集会话的输入地板单调上涨,压缩永远收不回这些字节;Google wire 的 fileData(URL 引用)图像不被识别为媒体负载:可被折叠、折叠后静默丢失且无警告(#1188 不变量缺口) #2609——Google fileData/videoMetadata 根本没被识别为媒体,被静默折叠 = 真实数据丢失。

  2. 什么原因导致的:两者同根于「媒体谓词」粒度错误。旧 hasMediaPayload 是「任何媒体→永不折叠」,把「bili 存得下的图」和「存不下的引用」一刀切都保护了(过度保护→地板涨);同时它只认内联 base64、不认远程/文件引用,后者漏成「无媒体」(欠检测→丢数据)。修法是把谓词拆两半:hasUnrecoverableMediaPayload(只有存不下字节的才 pin)vs 可归档(折叠+先归档)。

  3. 影响范围与严重程度:四条 wire(anthropic/openai/responses/google)所有带媒体会话。修复前 建议:compress.stripImages 默认开启——否则带图消息不可压缩会形成只升不降的输入地板撑爆上下文 #2607 是成本劣化(输入单调增长)非数据丢失;Google wire 的 fileData(URL 引用)图像不被识别为媒体负载:可被折叠、折叠后静默丢失且无警告(#1188 不变量缺口) #2609 是真·静默数据丢失。修复后收敛为单图残差(会话首条 user 消息若带内联图则整段会话留在 wire 上,是常量而非增长)。严重度:高→已解决。

  4. 怎么修复的:三处机制协同——① kernel 拆媒体谓词(hasUnrecoverableMediaPayload 只对远程 URL/file 引用返回 pin,内联 base64/data-URL 一律可折叠,含 anthropic url 型 data-URL);② refs.ts 把旧会话里被误标 BLOCKED 的可归档媒体迁移为全新数字 ref(id 安全见第 5 问);③ host 恢复索引从「仅 stripImages 时跑」改为「每请求都跑」+ prepare 后 merge-refresh,保证像素在任何折叠之前落盘 <state>/retrieve/img/<session>/。归档先行 ⇒ 可恢复性有保证。

  5. 是否回归问题:逐项核过既有行为,未见无意回归——

  6. 是否破坏缓存:否。归档只写旁路文件,折叠之间不动 wire;anthropic fold-anchored strip cutoff 未改;两次索引用 skip-if-exists 幂等(turn2 不产生重复文件,测试钉死)。折叠本身改 wire 是压缩本意,非回归。

  7. 是否改设计方向:部分转向——媒体处理从「永久豁免」改为「归档后折叠」,与整体 lean-context 哲学同向,边界清晰、有文档,非架构回退。

  8. 是否引入新配置:无。无新 env/config/flag(config-surface 零变更,作者声明属实)。stripImages 措辞微调(现与默认折叠重叠)但字段本身不变。


阻塞项(merge 前必处理)

  1. 🔴 kernel-guard CI 红(机械冲突):PR 将 kernel 0.0.111→0.0.112,但 merge-base(e1cd9e11c,v0.1.190)之后 master 已经 d16c34157([bug] outgoing body is never scrubbed for unpaired surrogate escapes (JSON.stringify hides the half as escape text) → strict upstream 400, session permanently stuck (#816/#828/#1615 family, 4th site; 0.1.189) #2565) 独立到 0.0.112。于是相对当前 master 的 diff 里 kernel 版本无净 bump 而 kernel/src/** 改了 → guard 触发「kernel/ source changed without bumping the version in kernel/package.json」。
    修法:rebase 到当前 master + 把 kernel/package.json bump 到 0.0.113。
  2. ⚠️ 分支落后 master 48 提交:按 §7.2 rebase-to-current-master,rebase 后重跑 typecheck + 全量测试 + build 再谈可合(rebase 顺带消掉上面版本冲突)。
  3. 人工合并:含 kernel/**,人工评审层。CI test job(ubuntu/windows)会跑 npm --prefix kernel test(ci.yml:41)+ bili 全量,绿了即两层测试都过(author 报 kernel 1287/1287、bili targeted 135/135)。

次要发现(不阻塞)

  • hasMediaPayload 现只剩定义+导出,src/kernel 内部无人调用(疑似孤儿公共导出)。CI unused-exports 过,说明要么有意保留作公共 parity 面、要么被某测试引用。低优先,可留可清。
  • kernel/src/protected.ts:429 的 if (item.type === "input_image") parts.push(item) 是防御性死枝——responses codec 无顶层 input_image item(图总在 message.content[],image_url 为字符串),永不触发。无害,不值得单独 commit。

判定:方向与实现均对,测试(新增 fold-images-archive、反转后的 fix-781 e2e、kernel media-payload)断言有牙、能兜住这次行为反转。解掉版本冲突并 rebase 后,即可进入人工合并评审。

…efault fold, google inlineData fold

- responses lane: default-config fold of an image message, then
  resolveDecompress({ imageRef }) against the live session restores
  byte-identical pixels from the always-on archive pass
- google lane: inlineData folds out of rebuilt contents once the range
  clears the soft-protected zone (last 5 msgs + 5000 tokens + last user);
  demand fires at >=20 refs with endId = len-8 so the turn-3 image rides
  deep inside the range; post-fold bodies carry no PNG bytes, pixels
  spill by ref, decompress restores them
- client echoes the model text it actually receives (fabricated echoes
  churn the ref space)
@ranxianglei
ranxianglei merged commit a85e5cd into master Oct 10, 2026
29 checks passed
@github-actions

Copy link
Copy Markdown
Contributor

📦 Merged Build Published

This merge to master was published as 0.0.226-master.a85e5cd (rolling master tag).

npm install -g billion-context@0.0.226-master.a85e5cd  # 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.

YunzhouLi-hub pushed a commit to YunzhouLi-hub/billion-context that referenced this pull request Oct 11, 2026
…ia folding replaces it

Default archivable-media folding (ranxianglei#2640) made the opt-in wire-side image
strip redundant, and the strip itself broke the prompt cache every turn on
three of four wires (sliding cutoff moves the byte boundary per turn; only
anthropic had the fold-anchored boundary). Media handling now has exactly
one path: fold + archive + decompress({ imageRef }) recovery.

- kernel: strip-images.ts (stripHistoricalImages) retired; wire barrel and
  wire-google tests updated; acp-kernel 0.0.113 -> 0.0.114
  (kernel-change: removing a public wire export that exists only for the
  removed host policy; no compression behavior changes)
- host: strip wiring, fold-anchored cutoff, and DEFAULT_STRIP_IMAGES_KEEP_RECENT
  removed from handle.ts/server.ts/compress-settings.ts; image-restore.ts keeps
  the always-on index + spill + decompress machinery (that is the default-fold
  recovery path, not the strip's)
- config: compress.stripImages / compress.stripImagesKeepRecent dropped from
  the type, whitelist, and parser; old configs get a one-time INERT warning
  (warnInertStripImages) instead of silent no-op
- docs: seeds + generated tables regenerated; EN/ZH prose sections replaced
  with removal notes; decompress error texts no longer reference the flag
- tests: strip-images / strip-images-compact / strip-log-observability /
  strip-cutoff-anchor suites deleted; compress-settings cascade/parse cases
  removed; new strip-images-removal.test.ts pins the INERT notice + silence

Closes ranxianglei#2608 (archival-failure-still-strips existed only on the strip path;
with the strip gone there is nothing to fail)
@ranxianglei
ranxianglei deleted the 2026-10-10_archive-then-fold branch October 15, 2026 05:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant