Bläddra i källkod

docs: propose a durable image offload watermark

creatixchu 1 månad sedan
förälder
incheckning
c1b1cd2984

+ 3 - 3
.agents/notes/proposed/architecture/2026-09-02-log-image-request-projection.i18n.yaml → .agents/notes/proposed/architecture/2026-09-02-image-offload-watermark.i18n.yaml

@@ -1,6 +1,6 @@
 # Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
-#   pnpm run verify-translation-pairing --write .agents/notes/proposed/architecture/2026-09-02-log-image-request-projection.md
-2026-09-02-log-image-request-projection.md: d1acc5007875f1f0178498d9e6d934e1d5c1843d
-2026-09-02-log-image-request-projection.zh.md: 041f63570453325970cd29407f09d17068512f3d
+#   pnpm run verify-translation-pairing --write .agents/notes/proposed/architecture/2026-09-02-image-offload-watermark.md
+2026-09-02-image-offload-watermark.md: 61864bb8fd81af0ac0e3668b0c0ebc872ac1c031
+2026-09-02-image-offload-watermark.zh.md: 7fb9503ad8de8e02f358ada598bbb54f2a493445

+ 60 - 0
.agents/notes/proposed/architecture/2026-09-02-image-offload-watermark.md

@@ -0,0 +1,60 @@
+# Agent Note: Durable image offload watermark
+
+Status: proposed
+
+English | [中文](2026-09-02-image-offload-watermark.zh.md)
+
+## Problem
+
+Request-size image offload is recomputed from scratch on every request. Each route collects every image occurrence on the derived surface, oldest first, and once the accumulated bytes exceed its budget it rounds the excess up to a whole removal quantum and replaces that many oldest occurrences with placeholder text, per the [unified image request pipeline](../../implemented/feature/2026-08-20-unified-image-request-pipeline.md). Under the DeepSeek defaults of a 128 MiB budget and a 64 MiB quantum, 129 one-mebibyte images offload the oldest 65, and that prefix holds until history passes 192 MiB. Nothing remembers where the previous request stopped; the prefix is stable only because the arithmetic over an append-only history repeats itself.
+
+That stability fails wherever the arithmetic inputs move. The [Files inline fallback](../../implemented/bug-fix/2026-08-21-deepseek-files-inline-fallback.md) rebuilds a request under a 20 MiB inline budget with a 10 MiB quantum, offloading far more images for that one request, and the next request in file mode brings them back. The pi-ai route uses a quantum of one byte, so its prefix moves on almost every request. Compaction lowers the total and returns previously offloaded images. A route switch moves every step boundary. Each move changes the model-visible prefix and invalidates the provider cache prefix.
+
+The same recomputation breaks the repository invariant that model-visible input is reconstructable from the session log. Which representation was dispatched, the exact derived request-version byte lengths, and the route budgets and quanta are runtime or configuration facts that never enter the log; `request/header` records call config, system prompt, and tools only. Provider usage anchors token totals and cannot recover the image set, and the [route-priced estimate](../../implemented/feature/2026-08-24-route-priced-image-request-pressure.md) documents that it does not reproduce the fallback budget. No consumer can pair a logged assistant response with the image set its request carried.
+
+## Proposal
+
+Make the offload point a durable session fact: an image offload watermark recorded as a surface-changing session event that only advances.
+
+**Event.** A new required-on-read `SessionEventMap` member (working name `image/offload`) carrying `{ turn, step }` and the watermark position. It is a surface event in the same sense as the compaction events: derived history marks every image occurrence positioned before the watermark as offloaded, and every route projects a marked occurrence as its `offloadedImageText` placeholder. Occurrences at or after the watermark are retained. Adapters keep their existing placeholder rendering; the only change is that the offloaded set comes from the surface rather than from per-request arithmetic.
+
+**Only advances.** A route whose budget is exceeded advances the watermark; a route with headroom leaves it alone. The watermark never retreats when a budget grows, a route changes, or compaction lowers the total, so the model-visible prefix and the provider cache prefix move only forward. Advancement keeps the existing count and byte quanta, so one advance still jumps to the next removal boundary rather than to the minimum that fits.
+
+**Position by occurrence.** The watermark names the first retained image occurrence by the sequence number of the event that carries it and the block path inside that event's content, including nested tool-result content. It does not use attachment ids, which are content digests that repeat when one image is attached twice, and it does not count occurrences, which compaction pruning would shift.
+
+**Appended before dispatch.** Like `request/header`, the event is appended inside its step before the request is sent, so the projection of every dispatched request is fully determined by the log at dispatch time. When the file-to-inline fallback needs a further advance, the adapter reports the requirement before the inline dispatch and the loop appends a second advance in the same step; the fallback still never sends an unlogged projection.
+
+**Never ignorable.** The event changes the derived surface, so a build that does not know the type must refuse the log rather than replay it with images the model never saw. `SESSION_FORMAT_VERSION` stays unchanged because the log structure does not change.
+
+**Decision owned by the loop.** `LlmRuntime` computes the watermark from budgets the route declares — representation, byte and count budgets, quanta — and appends the event; adapters declare budgets and report fallback requirements but do not decide the offloaded set. This makes the route-capability metadata seam that #2644 deferred a prerequisite of the implementation.
+
+**Consequences for other consumers.** The token meter prices the exact offloaded set instead of reproducing first-stage arithmetic. Resume, fork, and replay reproduce the surface from the log. Compaction that prunes messages below the watermark leaves it valid because the position names a retained occurrence. Text-only routes keep their separate whole-history substitution and do not touch the watermark.
+
+**Out of scope.** The execution-world access path embedded in placeholder and handle text is still resolved at serialization time; that gap exists for retained images too and belongs to a separate decision about recording the execution-world mapping. Recording per-request projection outcomes as a log-only event is unnecessary once the surface owns the offloaded set.
+
+## Alternatives considered
+
+**Keep recomputing the offload point per request (status quo).** Stable only while the arithmetic inputs hold still; the inline fallback, the pi-ai quantum, compaction, and route switches all move the prefix, and no consumer can reconstruct a historical request's image set. This is the state the issue asks to resolve.
+
+**Record each request's projection outcome as a log-only event.** Restores reconstructability but not stability: the recorded outcome is not a decision input, so every oscillation above still happens and the log merely documents it. It also creates two sources of truth for the offloaded set, the surface arithmetic and the record, which can disagree.
+
+**Log the full projected request body.** Everything except the offload decision is already derivable; repeating the history per request grows the log quadratically to record one position.
+
+**Identify the watermark by attachment id or by occurrence count.** Attachment ids repeat for duplicate attachments, so the position is ambiguous; counts shift when compaction prunes earlier occurrences. A sequence number plus block path is unambiguous and survives pruning.
+
+**Let each adapter append the event.** The adapter owns the budgets but not the session surface; a surface event appended below the loop bypasses the loop's ownership of derived history and would let two adapters define the surface differently.
+
+## Acceptance criteria
+
+- The watermark advances under file-mode budget pressure, inline fallback, and a route switch to a smaller budget, and never retreats after compaction, a budget increase, or a switch back.
+- From the log alone, the offloaded set of every dispatched request is determined, including the fallback's second advance, across resume, fork, retry, and compaction.
+- Replay, resume, and fork derive an identical surface, and a build without the event type refuses the log.
+- Keyless recorded-session snapshots cover file-mode offload, inline-fallback advance, and compaction below the watermark; unit tests pin the position encoding, monotonicity, and pre-dispatch append timing; both SDK expected outputs update for the new surface event.
+- The token meter's image pricing consumes the surface's offloaded set and the first-stage arithmetic reproduction is removed.
+
+## Risks
+
+- Offloaded images never return automatically when budgets grow; recovery is the read-only path in the placeholder, which the model must act on deliberately.
+- The loop-side decision requires routes to declare budgets through a capability metadata seam that does not exist yet; the implementation cannot land without it.
+- The fallback's pre-dispatch second advance adds an adapter-to-loop channel; without a stated scope it invites other transient request facts into surface events.
+- The execution-world path in placeholder text remains unlogged; this proposal narrows the reconstruction gap to that one input rather than closing it.

+ 60 - 0
.agents/notes/proposed/architecture/2026-09-02-image-offload-watermark.zh.md

@@ -0,0 +1,60 @@
+# Agent Note: 持久的图片 offload 水位
+
+Status: proposed
+
+[English](2026-09-02-image-offload-watermark.md) | 中文
+
+## 问题
+
+请求级图片 offload 在每次请求时都从头重算。每条路由按最老优先的顺序收集派生表层上的全部图片出现位置,累计字节一旦超过预算,就把超出部分向上取整到整个删除量子,把这么多最老的出现位置替换为占位文本,见[统一图片请求管线](../../implemented/feature/2026-08-20-unified-image-request-pipeline.zh.md)。在 DeepSeek 默认的 128 MiB 预算和 64 MiB 量子下,129 张 1 MiB 的图片会省略最老的 65 张,这个前缀维持到历史超过 192 MiB。没有任何东西记住上一次请求停在哪里,前缀稳定只是因为对 append-only 历史做同样的算术会得到同样的结果。
+
+算术的输入一动,这种稳定就失效。[Files 内联回退](../../implemented/bug-fix/2026-08-21-deepseek-files-inline-fallback.zh.md)会用 20 MiB 内联预算和 10 MiB 量子重建请求,那一次省略多得多的图片,下一次 file 模式的请求又把它们带回来。pi-ai 路由的量子是一个字节,前缀几乎每次请求都会移动。compaction 降低总量,让先前省略的图片回归。切换路由会移动每个台阶边界。每一次移动都改变模型可见前缀,并让 provider 的缓存前缀失效。
+
+同样的重算也破坏了仓库不变量:模型可见输入必须能从 session log 重建。实际发出的表示方式、派生请求版本的精确字节长度、路由预算和量子都是运行时或配置事实,从不进入日志,`request/header` 只记录调用配置、系统提示词和工具。provider usage 只锚定 token 总量,恢复不了图片集合,[按路由定价的估计](../../implemented/feature/2026-08-24-route-priced-image-request-pressure.zh.md)也写明它不复现回退预算。没有任何消费方能把一条已记录的助手响应和它的请求携带的图片集合配对。
+
+## 提案
+
+把 offload 位置变成持久的会话事实:以一条只会前进的、改变表层的会话事件记录图片 offload 水位。
+
+**事件。** 新增一个读取时必须识别的 `SessionEventMap` 成员(工作名 `image/offload`),携带 `{ turn, step }` 和水位位置。它与 compaction 事件同属表层事件:派生历史把位于水位之前的每个图片出现位置标记为已省略,每条路由把被标记的出现位置投影为它的 `offloadedImageText` 占位文本。位于水位及之后的出现位置保留。adapter 保持现有的占位渲染,唯一的变化是省略集合来自表层而不是每次请求的算术。
+
+**只前进。** 预算被超过的路由推进水位,有余量的路由不动它。预算变大、路由切换或 compaction 降低总量时水位永不回退,所以模型可见前缀和 provider 缓存前缀只向前移动。推进保留现有的张数和字节量子,一次推进仍然跳到下一个删除边界,而不是刚好够用的最小值。
+
+**按出现位置定位。** 水位以承载事件的序号加该事件内容中的块路径(含嵌套的工具结果内容)指向第一个保留的图片出现位置。它不用附件 id,因为附件 id 是内容摘要,同一张图附加两次就会重复;也不按出现次数计数,因为 compaction 剪枝会让计数漂移。
+
+**发送前追加。** 与 `request/header` 一样,事件在所属 step 内、请求发出之前追加,因此每个已发出请求的投影在发出时刻已完全由日志决定。file 到内联的回退需要进一步推进时,adapter 在内联发送之前上报需求,循环在同一 step 内追加第二次推进,回退仍然从不发出未记录的投影。
+
+**永不可忽略。** 该事件改变派生表层,不认识该类型的构建必须拒绝这份日志,而不是带着模型从未见过的图片重放它。日志结构没有变化,`SESSION_FORMAT_VERSION` 保持不变。
+
+**决定权在循环。** `LlmRuntime` 根据路由声明的预算(表示方式、字节和张数预算、量子)计算水位并追加事件;adapter 声明预算、上报回退需求,但不决定省略集合。这让 #2644 推迟的路由能力元数据 seam 成为实现的前置条件。
+
+**对其他消费方的影响。** token meter 直接为精确的省略集合定价,不再复现第一轮算术。resume、fork 和重放从日志复现表层。剪掉水位之下消息的 compaction 不影响水位有效性,因为位置指向的是一个保留的出现位置。纯文本路由保留各自的全历史替换,不碰水位。
+
+**范围之外。** 占位和句柄文本中嵌入的执行世界访问路径仍在序列化时解析,这个缺口对保留的图片同样存在,属于另一个关于记录执行世界映射的决定。表层拥有省略集合之后,再用 log-only 事件记录每次请求的投影结果已无必要。
+
+## 考虑过的替代方案
+
+**继续每次请求重算 offload 位置(现状)。** 只在算术输入不动时稳定,内联回退、pi-ai 量子、compaction 和路由切换都会移动前缀,且没有消费方能重建历史请求的图片集合。这正是 issue 要解决的状态。
+
+**用 log-only 事件记录每次请求的投影结果。** 恢复了可重建性但没有稳定性:记录的结果不是决策输入,上述每一种抖动照旧发生,日志只是把它记下来。它还为省略集合制造了两个事实来源,表层算术和记录,两者可能不一致。
+
+**记录完整的投影后请求体。** 除 offload 决定外一切都已可派生,为记录一个位置而每次请求重复整段历史会让日志平方级增长。
+
+**用附件 id 或出现次数标识水位。** 附件 id 在重复附加时重复,位置因此含糊;compaction 剪掉更早的出现位置后计数会漂移。事件序号加块路径没有歧义,也不受剪枝影响。
+
+**让各个 adapter 自己追加事件。** adapter 拥有预算,但不拥有会话表层;在循环之下追加表层事件绕过了循环对派生历史的所有权,也会让两个 adapter 对表层做出不同定义。
+
+## 验收标准
+
+- 水位在 file 模式预算压力、内联回退和切换到更小预算的路由时推进,在 compaction、预算增大或切换回来之后永不回退。
+- 仅凭日志即可确定每个已发出请求的省略集合,包括回退的第二次推进,覆盖 resume、fork、retry 和 compaction。
+- 重放、resume 和 fork 派生出完全相同的表层,不认识该事件类型的构建拒绝这份日志。
+- keyless 录制会话快照覆盖 file 模式省略、内联回退推进和水位之下的 compaction;单元测试钉住位置编码、单调性和发送前的追加时机;两个 SDK 的期望输出为新的表层事件同步更新。
+- token meter 的图片定价消费表层的省略集合,第一轮算术的复现被移除。
+
+## 风险
+
+- 预算变大时被省略的图片不会自动回归,恢复手段是占位文本中的只读路径,模型必须主动使用它。
+- 循环侧决定要求路由通过一个尚不存在的能力元数据 seam 声明预算,没有它实现无法落地。
+- 回退的发送前第二次推进新增了一条 adapter 到循环的通道,不声明范围就会诱使其他临时请求事实进入表层事件。
+- 占位文本中的执行世界路径仍未记录,本提案把重建缺口收窄到这一个输入,而不是完全关闭。

+ 0 - 62
.agents/notes/proposed/architecture/2026-09-02-log-image-request-projection.md

@@ -1,62 +0,0 @@
-# Agent Note: Record each request's image projection in the session log
-
-Status: proposed
-
-English | [中文](2026-09-02-log-image-request-projection.zh.md)
-
-## Problem
-
-The repository invariant says model-visible ⟺ logged: anything that reaches a model request must be reconstructable from the session log. Request-size image offload currently breaks the reconstruction half of that invariant for historical requests.
-
-Offload itself is deliberately transient: the durable surface keeps every original image reference, and each request replaces an oldest-first prefix of image occurrences with placeholder text at serialization time, per the [unified image request pipeline](../../implemented/feature/2026-08-20-unified-image-request-pipeline.md). The projection of a *future* request is a function of durable history plus live route configuration, and that half is healthy.
-
-Reconstructing what a *past* request actually contained is not. Four decision inputs never enter the log:
-
-1. **The representation actually dispatched.** A Files resolution failure rebuilds the request inline under the tighter `maxInlineRequestImageBytes` budget and its own removal quantum, offloading more images than file mode would, per the [Files inline fallback](../../implemented/bug-fix/2026-08-21-deepseek-files-inline-fallback.md). Which path ran is a per-request network outcome with no trace in the log.
-2. **Exact derived request-version bytes.** The second projection stage uses the encoded byte length of each derived request version, which depends on the encoder pipeline, not on any logged fact; the log holds only the normalized attachment's byte count.
-3. **Execution-world access paths.** `offloadedImageText` and `requestImageHandleText` embed the read-only path resolved for the current tool execution world into model-visible text. Resume or fork in another environment reconstructs different text than the request carried.
-4. **Route budgets and quanta.** `maxRequestFilesBytes`, `maxImagesPerRequest`, both removal quanta, the inline watermark, and the per-model pixel/byte policy live only in composed connection configuration. The `request/header` snapshot records call config, system prompt, and tools; a configuration change silently changes every reprojection of old history.
-
-Provider usage anchors token totals only and cannot recover the image set. The token meter's [route-priced estimate](../../implemented/feature/2026-08-24-route-priced-image-request-pressure.md) reproduces just the first projection stage and documents that the fallback budget is not reproduced and that access paths resolve at pricing time. So today no consumer — debugging, accounting reconciliation, or replay tooling — can pair a logged assistant response with the exact model-visible input that produced it.
-
-## Proposal
-
-Keep offload a transient projection and record its outcome: append one log-only event describing the image projection of each dispatched request whose outcome the log records.
-
-**Event.** A new `SessionEventMap` member (working name `request/images`) carrying `{ turn, step }` plus the projection facts, appended within its step beside `request/header`. Like `request/header` it is log-only: derived message history and the durable surface are unchanged, and future requests still reproject from durable history and live configuration.
-
-**Payload.** The representation dispatched (`file`, `inline`, or `text-only`) and, per image occurrence in request order: a retained occurrence's request-version width, height, and encoded byte count together with its model-visible handle text, or an offloaded occurrence's model-visible placeholder text. Substitution text is recorded verbatim so reconstruction does not depend on the stability of the text-building functions or on any execution environment. The vocabulary stays in `dsh-llm` terms so the pi-ai and DeepSeek adapters share one schema.
-
-**Reporting channel.** The adapter that serialized the request owns these facts and reports them with the generation outcome, the same way `usage` travels to `assistant/message` today; the loop appends the event. The exact `LlmAdapter` result field is implementation detail for the follow-up PR.
-
-**Attempt rule.** One event per dispatched request whose outcome enters the log. A pre-dispatch re-serialization (the file-to-inline fallback) records only the projection actually dispatched; the once-permitted stale-file retry records the retried dispatch, whose model-visible image set is unchanged because the fallback reuses the already derived request versions. Whether dispatches that end in a terminal provider failure also record is an open review question; the default is yes, so a failed request's content is as reconstructable as a successful one's.
-
-**Format impact.** The event carries `ignorable: true` under the [session log version mechanism](../../implemented/architecture/2026-08-10-session-log-version-mechanism.md): a build that does not know the type still derives the surface correctly, and `SESSION_FORMAT_VERSION` stays unchanged because no structural format changes.
-
-**Token accounting.** Unchanged. Provider usage remains the anchor for completed requests and the route-priced projection remains a synchronous estimate; the recorded event additionally enables exact post-hoc reconciliation of any historical request.
-
-**Out of scope.** Copying budgets into `request/header` is unnecessary once outcomes are recorded and would change header equality semantics. Permanent image eviction — durably replacing old image occurrences on the message surface — remains a separate future decision requiring its own surface-replacement event; this event never changes the surface.
-
-## Alternatives considered
-
-**Pure derivation, with budgets logged into `request/header`.** Fixes only input 4. The dispatched representation and the exact derived byte lengths are runtime results no amount of logged configuration makes derivable, and access paths would still leak the environment into model-visible text. Making pure derivation true would require deleting the inline fallback and the exact second projection stage, trading away request deliverability for reconstructability.
-
-**Log the full projected request body per request.** Complete but redundant: everything except the projection outcome is already derivable, and repeating the whole history per request grows the log quadratically. The outcome facts are the only new information.
-
-**Permanently evict offloaded images from the durable surface.** Different semantics, not a cheaper encoding of the same one: it changes every future request, forfeits the property that images return when budgets rise or routes change, and conflates request-level omission with surface mutation — exactly what the issue requires keeping separate.
-
-**Accept approximate reconstruction.** Leaves a model-visible divergence with zero trace: two replays of one log can disagree with what the model actually saw, and the fallback path makes the disagreement unbounded. This narrows the invariant instead of honoring it.
-
-## Acceptance criteria
-
-- From the log alone, every recorded model outcome in an image-bearing session pairs with the exact ordered image set — identity, request dimensions, encoded bytes — and the verbatim substitution texts its request carried, across file-mode requests, inline fallback, configuration changes, resume, fork, retry, and compaction.
-- Replaying a log containing the event derives an unchanged message surface, and a build without the event type still accepts the log.
-- Unit tests pin the event's append timing, attempt rule, and payload; keyless recorded-session snapshots cover offload in file mode and through the inline fallback; recovery tests cover resume and fork; both SDK expected outputs update if their projections surface the event.
-- Review of this note files the follow-up implementation work items.
-
-## Risks
-
-- The payload vocabulary must stay provider-neutral; a schema that encodes DeepSeek transport concepts will strain the next adapter.
-- Verbatim substitution text grows the log for every image-bearing request; the strings are short and per-occurrence, but image-dense sessions pay it on every step.
-- Recording only the dispatched attempt hides earlier rejected dispatches; if those ever matter, the attempt rule needs revisiting rather than silent extension.
-- The adapter-to-loop reporting channel is new surface on generation results; without a stated scope it invites other transient request facts into the log. This proposal covers image projection facts only.

+ 0 - 62
.agents/notes/proposed/architecture/2026-09-02-log-image-request-projection.zh.md

@@ -1,62 +0,0 @@
-# Agent Note: 在 session log 中记录每次请求的图片投影
-
-Status: proposed
-
-[English](2026-09-02-log-image-request-projection.md) | 中文
-
-## 问题
-
-仓库不变量要求模型可见 ⟺ 已记录:凡是进入模型请求的内容都必须能从 session log 重建。请求级图片 offload 目前在历史请求这一半上破坏了该不变量。
-
-offload 本身被刻意设计为临时投影:持久表层保留全部原始图片引用,每次请求在序列化时把最老的一段图片出现位置替换为占位文本,见[统一图片请求管线](../../implemented/feature/2026-08-20-unified-image-request-pipeline.zh.md)。未来请求的投影是持久历史加当前路由配置的函数,这一半是健康的。
-
-重建某次过去请求实际包含的内容则做不到。有四个决定输入从不进入日志:
-
-1. **实际发出的表示方式。** Files 解析失败会用更紧的 `maxInlineRequestImageBytes` 预算和独立的删除量子以内联方式重建请求,比 file 模式 offload 掉更多图片,见 [Files 内联回退](../../implemented/bug-fix/2026-08-21-deepseek-files-inline-fallback.zh.md)。走了哪条路是当次网络结果,日志里没有任何痕迹。
-2. **派生请求版本的精确字节数。** 第二轮投影使用每个派生请求版本的编码字节长度,它取决于编码管线而非任何已记录事实,日志里只有归一化附件的字节数。
-3. **执行世界访问路径。** `offloadedImageText` 和 `requestImageHandleText` 会把为当前工具执行世界解析出的只读路径写进模型可见文本。在另一个环境 resume 或 fork 时,重建出的文本和请求当时携带的不同。
-4. **路由预算与量子。** `maxRequestFilesBytes`、`maxImagesPerRequest`、两个删除量子、内联水位以及按模型的像素和字节策略只存在于组合出的连接配置中。`request/header` 快照记录调用配置、系统提示词和工具,配置一变,对旧历史的每次重投影都会悄悄改变。
-
-Provider usage 只锚定 token 总量,恢复不了图片集合。token meter 的[按路由定价估计](../../implemented/feature/2026-08-24-route-priced-image-request-pressure.zh.md)只复现第一轮投影,并自己写明不复现回退预算、访问路径在定价时才解析。所以今天没有任何消费方(调试、计费对账或重放工具)能把一条已记录的助手响应与产生它的精确模型可见输入配对。
-
-## 提案
-
-保持 offload 为临时投影,同时记录它的结果:为每个结果进入日志的已发出请求追加一条 log-only 事件,描述该请求的图片投影。
-
-**事件。** 新增一个 `SessionEventMap` 成员(工作名 `request/images`),携带 `{ turn, step }` 和投影事实,在所属 step 内与 `request/header` 一样追加。它和 `request/header` 同为 log-only:派生消息历史与持久表层不变,未来请求仍从持久历史和当前配置重投影。
-
-**载荷。** 发出的表示方式(`file`、`inline` 或 `text-only`),以及按请求顺序的每个图片出现位置:保留的记请求版本宽、高、编码字节数和它的模型可见句柄文本,被 offload 的记它的模型可见占位文本。替换文本按原样记录,重建因此不依赖文本构造函数的稳定性,也不依赖任何执行环境。词汇保持在 `dsh-llm` 层面,让 pi-ai 和 DeepSeek 两个 adapter 共用一个 schema。
-
-**回报通道。** 序列化请求的 adapter 拥有这些事实,随生成结果一起回报,方式与今天 `usage` 传到 `assistant/message` 相同,由循环追加事件。具体落在 `LlmAdapter` 结果的哪个字段是后续实现 PR 的细节。
-
-**attempt 规则。** 结果进入日志的每次已发出请求记一条事件。发出前的重新序列化(file 到内联的回退)只记录实际发出的那份投影;被允许一次的过期 fileId 重试记录重试后的那次发出,因回退复用已派生的请求版本,其模型可见图片集合不变。以终局 provider 失败收场的发出是否也记录留作评审的开放问题,默认记录,让失败请求的内容和成功请求同样可重建。
-
-**格式影响。** 事件按[session log 版本机制](../../implemented/architecture/2026-08-10-session-log-version-mechanism.zh.md)携带 `ignorable: true`:不认识该类型的构建仍能正确派生表层,且没有结构性格式变化,`SESSION_FORMAT_VERSION` 保持不变。
-
-**token 记账。** 不变。provider usage 仍是已完成请求的锚点,按路由定价的投影仍是同步估计;记录的事件额外让任何历史请求可以事后精确对账。
-
-**范围之外。** 把预算复制进 `request/header` 在结果被记录后已无必要,且会改变 header 相等性语义。永久淘汰图片(在消息表层上持久替换旧图片出现位置)仍是需要独立表层替换事件的未来决定,本事件永不改变表层。
-
-## 考虑过的替代方案
-
-**纯派生,并把预算记入 `request/header`。** 只修复输入 4。发出的表示方式和精确派生字节数是运行时结果,记录再多配置也推不出来,访问路径也仍会把环境泄漏进模型可见文本。要让纯派生成立就得删掉内联回退和精确的第二轮投影,用请求的可发出性换可重建性。
-
-**每次请求记录完整投影后的请求体。** 完整但冗余:除投影结果外一切都已可派生,每次请求重复整段历史让日志平方级增长。结果事实才是唯一的新信息。
-
-**从持久表层永久淘汰被 offload 的图片。** 这是不同的语义,不是同一语义的更省编码:它改变每个未来请求,放弃预算上调或路由切换后图片回归的性质,并把请求级省略与表层修改混为一谈,恰是 issue 要求分开的两件事。
-
-**接受近似重建。** 留下一处零痕迹的模型可见分歧:对同一份日志的两次重放可以和模型实际所见不一致,回退路径让这种不一致没有上界。这是收窄不变量,不是履行它。
-
-## 验收标准
-
-- 仅凭日志,含图会话中每条已记录的模型结果都能与其请求携带的精确有序图片集合(身份、请求尺寸、编码字节数)及逐字替换文本配对,覆盖 file 模式请求、内联回退、配置变化、resume、fork、retry 和 compaction。
-- 重放含该事件的日志派生出不变的消息表层,不认识该事件类型的构建仍接受这份日志。
-- 单元测试钉住事件的追加时机、attempt 规则和载荷;keyless 录制会话快照覆盖 file 模式与内联回退下的 offload;恢复测试覆盖 resume 和 fork;若 SDK 投影呈现该事件,两个 SDK 的期望输出同步更新。
-- 本 note 的评审建立后续实现工作项。
-
-## 风险
-
-- 载荷词汇必须保持 provider 中立,编入 DeepSeek 传输概念的 schema 会让下一个 adapter 难以复用。
-- 逐字替换文本让每个含图请求的日志变大;字符串短且按出现位置计,但图片密集的会话每个 step 都要付这笔开销。
-- 只记录实际发出的那次 attempt 会隐藏更早被拒绝的发出;若这些将来变得重要,应重新审视 attempt 规则而不是悄悄扩展。
-- adapter 到循环的回报通道是生成结果上的新表面,不声明范围就会诱使其他临时请求事实进入日志。本提案只覆盖图片投影事实。