Преглед изворни кода

docs: propose logging each request's image projection

creatixchu пре 1 месец
родитељ
комит
1516c6018d

+ 6 - 0
.agents/notes/proposed/architecture/2026-09-02-log-image-request-projection.i18n.yaml

@@ -0,0 +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

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

@@ -0,0 +1,62 @@
+# 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.

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

@@ -0,0 +1,62 @@
+# 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 到循环的回报通道是生成结果上的新表面,不声明范围就会诱使其他临时请求事实进入日志。本提案只覆盖图片投影事实。