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

refactor(llm): recover IMAGE_OFFLOAD_REQUIRED in llm-retry instead of a new plugin package

Remove the llm-image-offload package and every registration it needed. The
recovery branch lives in dsh-llm-retry, which already sits on the
agent/request-error waterfall: an IMAGE_OFFLOAD_REQUIRED failure advances the
image/offload watermark by the count the adapter names and retries the step
outside the provider retry budget. Drop pre-dispatch planning with it: routes
no longer declare a request-image budget on their model info, and the byte
checks that stay in the DeepSeek and pi-ai adapters are provisional until the
provider reports uncacheable images itself.
creatixchu пре 2 недеља
родитељ
комит
fcb976d3dc
66 измењених фајлова са 165 додато и 754 уклоњено
  1. 2 2
      .agents/notes/implemented/architecture/2026-09-02-image-offload-watermark.i18n.yaml
  2. 5 5
      .agents/notes/implemented/architecture/2026-09-02-image-offload-watermark.md
  3. 5 5
      .agents/notes/implemented/architecture/2026-09-02-image-offload-watermark.zh.md
  4. 0 3
      apps/cli/composition.md
  5. 2 2
      docs/config-catalog.i18n.yaml
  6. 1 14
      docs/config-catalog.md
  7. 1 14
      docs/config-catalog.zh.md
  8. 2 2
      docs/event-producer-consumer.i18n.yaml
  9. 2 2
      docs/event-producer-consumer.md
  10. 2 2
      docs/event-producer-consumer.zh.md
  11. 2 2
      docs/module-graph.i18n.yaml
  12. 0 5
      docs/module-graph.md
  13. 0 5
      docs/module-graph.zh.md
  14. 2 2
      docs/subsystems/llm-streaming.i18n.yaml
  15. 2 4
      docs/subsystems/llm-streaming.md
  16. 2 4
      docs/subsystems/llm-streaming.zh.md
  17. 2 2
      docs/subsystems/session.i18n.yaml
  18. 1 1
      docs/subsystems/session.md
  19. 1 1
      docs/subsystems/session.zh.md
  20. 0 3
      packages/bundle/base/cordis.patch.yml
  21. 0 1
      packages/bundle/base/package.json
  22. 0 3
      packages/bundle/sdk-minimal/cordis.patch.yml
  23. 0 1
      packages/bundle/sdk-minimal/package.json
  24. 0 1
      packages/bundle/sdk-minimal/tests/sdk-minimal.spec.ts
  25. 2 2
      packages/core/session/README.i18n.yaml
  26. 1 1
      packages/core/session/README.md
  27. 1 1
      packages/core/session/README.zh.md
  28. 1 5
      packages/extensions/tool-cordis/src/api-catalog.ts
  29. 2 2
      packages/llm/README.i18n.yaml
  30. 1 2
      packages/llm/README.md
  31. 1 2
      packages/llm/README.zh.md
  32. 2 2
      packages/llm/llm-deepseek/README.i18n.yaml
  33. 2 2
      packages/llm/llm-deepseek/README.md
  34. 2 2
      packages/llm/llm-deepseek/README.zh.md
  35. 2 16
      packages/llm/llm-deepseek/src/adapter.ts
  36. 0 24
      packages/llm/llm-deepseek/tests/adapter.spec.ts
  37. 1 4
      packages/llm/llm-deepseek/tests/dynamic-config.spec.ts
  38. 0 6
      packages/llm/llm-image-offload/README.i18n.yaml
  39. 0 106
      packages/llm/llm-image-offload/README.md
  40. 0 106
      packages/llm/llm-image-offload/README.zh.md
  41. 0 46
      packages/llm/llm-image-offload/package.json
  42. 0 112
      packages/llm/llm-image-offload/src/index.ts
  43. 0 30
      packages/llm/llm-image-offload/tsconfig.json
  44. 0 13
      packages/llm/llm-image-offload/tsdown.config.ts
  45. 2 2
      packages/llm/llm-pi-ai/README.i18n.yaml
  46. 2 2
      packages/llm/llm-pi-ai/README.md
  47. 2 2
      packages/llm/llm-pi-ai/README.zh.md
  48. 0 9
      packages/llm/llm-pi-ai/src/adapter.ts
  49. 2 2
      packages/llm/llm-retry/README.i18n.yaml
  50. 5 4
      packages/llm/llm-retry/README.md
  51. 5 4
      packages/llm/llm-retry/README.zh.md
  52. 44 0
      packages/llm/llm-retry/src/image-offload.ts
  53. 7 0
      packages/llm/llm-retry/src/index.ts
  54. 24 86
      packages/llm/llm-retry/tests/image-offload.spec.ts
  55. 2 2
      packages/llm/llm/README.i18n.yaml
  56. 0 0
      packages/llm/llm/README.md
  57. 1 1
      packages/llm/llm/README.zh.md
  58. 7 10
      packages/llm/llm/src/content.ts
  59. 2 2
      packages/llm/llm/src/error.ts
  60. 0 2
      packages/llm/llm/src/index.ts
  61. 6 13
      packages/llm/llm/src/types.ts
  62. 2 3
      packages/llm/llm/tests/content.spec.ts
  63. 0 37
      pnpm-lock.yaml
  64. 0 1
      python/sdk-runtime/package.json
  65. 0 1
      tsconfig.base.json
  66. 0 1
      tsconfig.host.json

+ 2 - 2
.agents/notes/implemented/architecture/2026-09-02-image-offload-watermark.i18n.yaml

@@ -2,5 +2,5 @@
 # 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/implemented/architecture/2026-09-02-image-offload-watermark.md
-2026-09-02-image-offload-watermark.md: 2b682cd3f3de19c47c0c38f986dd430f86a1ca51
-2026-09-02-image-offload-watermark.zh.md: 6a07f8f37b33fa08898adba46c7a8ca9d8bf432b
+2026-09-02-image-offload-watermark.md: f178ea96ae982d89b575795a93e7321fed7afad6
+2026-09-02-image-offload-watermark.zh.md: cdd92011ce2ee3b80c5ea3641988cec1d2e1a80c

+ 5 - 5
.agents/notes/implemented/architecture/2026-09-02-image-offload-watermark.md

@@ -20,9 +20,9 @@ The offload point is a durable session fact: the core `image/offload` event reco
 
 **Only advances.** 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. An occurrence below the watermark that compaction later shadows leaves the watermark valid, because the comparison is positional.
 
-**Decision owned by one plugin, budgets declared by routes.** An image-capable route declares an `LlmImageRequestBudget` (`representation`, `maxBytes`, `maxImages`, both quanta, and the request-version byte target) as `imageRequest` on its `LlmResolvedModelInfo`. The `dsh-llm-image-offload` plugin listens on `agent/pre-step`: once downstream listeners have entered the step, it resolves the budget of the route the latest `request/header` names through `ctx.llm.resolveModelInfo()`, collects retained occurrences in model request order, and plans the removal prefix with the pure `offloadedImagePrefixCount()` (represented bytes are the normalized count clamped to the version target and base64-expanded for inline routes, removed in whole quanta). A surface replacement can put a newer event before older events; the plugin converts the request prefix to the greatest durable position in that prefix, which may omit additional occurrences but always satisfies the requested removal. The event lands before `step/start`, so the loop's ordinary derivation sends the offloaded set. Before the first header, and for occurrences the entering step itself appends, no plan runs; the adapter failure below covers them. The DeepSeek adapter declares its file-mode budget and the pi-ai adapter its base64 bound; the agent loop is unchanged.
+**Decision owned by `dsh-llm-retry`, budgets enforced by routes.** An image-capable route enforces an `LlmImageRequestBudget` (`representation`, `maxBytes`, `maxImages`, both quanta) over its retained occurrences' exact request-version bytes and reports the overflow as a failure; nothing plans an offload before dispatch. `dsh-llm-retry`, the recovery executor already on the `agent/request-error` waterfall, handles `IMAGE_OFFLOAD_REQUIRED` before any provider policy: it walks the surface in model request order, converts the reported count to the greatest durable position in that request-order prefix (a surface replacement can put a newer event before older ones, so the advance may offload additional occurrences but always covers the prefix), appends `image/offload`, and returns the `retry` action without spending the provider retry budget or logging `llm/retry`. The loop re-runs the step over the derived surface; the agent loop is unchanged.
 
-**Adapters project, never decide.** Serialization renders every `offloaded` block as `offloadedImageText` with the currently resolved access path and prepares only retained occurrences. When the retained occurrences' exact request-version bytes still exceed the route budget, in file mode, under the inline fallback's tighter budget, or under the pi-ai bound, the adapter fails the attempt with `IMAGE_OFFLOAD_REQUIRED` and `LlmFailure.offloadImages` naming how many more oldest occurrences must be offloaded, computed with the shared `requiredImageOffload()`. The plugin's `agent/request-error` listener advances the watermark by that count and returns the `retry` action, so the loop re-runs the step over the derived surface; when nothing remains to offload the listener delegates and the failure reaches ordinary recovery.
+**Adapters project, never decide.** Serialization renders every `offloaded` block as `offloadedImageText` with the currently resolved access path and prepares only retained occurrences. When the retained occurrences' exact request-version bytes still exceed the route budget, in file mode, under the inline fallback's tighter budget, or under the pi-ai bound, the adapter fails the attempt with `IMAGE_OFFLOAD_REQUIRED` and `LlmFailure.offloadImages` naming how many more oldest occurrences must be offloaded, computed with the shared `requiredImageOffload()`. When nothing remains to offload, `dsh-llm-retry` delegates and the failure reaches ordinary recovery.
 
 **Token accounting.** `priceImages` receives surface `ImageBlock`s and prices an `offloaded` one as its placeholder text; the DeepSeek and replay pricing no longer reproduce any offload arithmetic. The meter folds `image/offload` into its replay state, prices the current surface under the current watermark and each usage anchor under the watermark its request was derived with. Provider usage remains the anchor for completed requests.
 
@@ -40,16 +40,16 @@ The offload point is a durable session fact: the core `image/offload` event reco
 
 **Let each adapter append the event.** The adapter owns the budgets but not the session surface; a surface fact appended below the loop bypasses the session's derived history and lets two adapters define the surface differently. Adapters report the count they need instead.
 
-**Plan and advance inside the agent loop.** The loop knows the exact prepared route before deriving each request, so planning there never spends a failed attempt. It also puts a route-specific policy into the one component every profile shares, changes the documented step order, and bypasses the `agent/request-error` waterfall that context-overflow compaction and retry already use for the same recover-and-retry pattern. A plugin on the existing `agent/pre-step` and `agent/request-error` extension points costs one failed attempt when a step's own messages or an unknown route push the retained set past the budget, and keeps the loop unchanged.
+**Plan the offload before dispatch, in the loop or in a plugin.** The loop knows the exact prepared route before deriving each request, so planning there never spends a failed attempt; but it puts a route-specific policy into the one component every profile shares, changes the documented step order, and bypasses the `agent/request-error` waterfall that context-overflow compaction and retry already use for the same repair-and-retry pattern. A pre-step plugin keeps the loop unchanged but cannot see the step's own messages or the first request's route, so the failure path stays necessary, and the plan needs every route to declare its budget on its model info. Handling only the failure costs one attempt per quantum crossing (64 MiB in DeepSeek file mode, 20 MiB on pi-ai) and matches the planned direction: routes will stop checking sizes locally, send every image, and the provider will report what it cannot cache, which is exactly a failure naming an offload point.
 
 **Keep a transient extra offload for the inline fallback and exact-byte overflow.** Would have sent an unlogged projection in exactly the cases the invariant exists for; the failure-and-advance path costs one serialization attempt and keeps every dispatched request derivable from the log.
 
 ## Consequences
 
-Offloaded images never return automatically when budgets grow, a larger route is selected, or compaction lowers the total; recovery is the read-only path in the placeholder, which the model uses deliberately. A Files outage or a temporary switch to a small-budget route advances the watermark permanently; both are accepted for the same reason.
+Offloaded images never return automatically when budgets grow, a larger route is selected, or compaction lowers the total; recovery is the read-only path in the placeholder, which the model uses deliberately. A Files outage or a temporary switch to a small-budget route advances the watermark permanently; both are accepted for the same reason. The route-local byte checks are provisional: once the provider reports uncacheable images itself, `requiredImageOffload()` and the route budgets go away while the event, the derivation, and the recovery branch stay as they are.
 
 Every dispatched request's image set is determined by the log alone, across file mode, inline fallback, resume, fork, retry, and compaction, and the provider cache prefix no longer oscillates. 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.
 
 ## Testing
 
-`packages/llm/llm/tests/content.spec.ts` pins arbitrary-depth image traversal, represented bytes, projection, and the prefix count, including the 129-to-64 MiB quantum example. `packages/core/session/tests/image-offload.spec.ts` pins append and seed validation, exact current-surface image paths, rejection of persisted derived markers, strict advance, arbitrary-depth marking, frozen copies, cache rebuild, and scratch-replay equality. `packages/llm/llm-image-offload/tests/image-offload.spec.ts` pins the pre-step advance, the non-retreating watermark, request-order counts after a surface replacement, the `IMAGE_OFFLOAD_REQUIRED` advance-and-retry path, and the exhausted case that delegates downstream. Compaction tests pin watermark application to direct summarization input. Adapter specs pin placeholder projection, prepared-only-retained reads, and the exact-byte failure with its count; `route-pricing.spec.ts` pins watermark pricing. The `inline-image-prompt` TypeScript SDK snapshot replays an authored `IMAGE_OFFLOAD_REQUIRED` attempt through the shipped profile and pins the advance and the retried request. The Python SDK notification test pins lossless forwarding of the new event and its nested watermark.
+`packages/llm/llm/tests/content.spec.ts` pins arbitrary-depth image traversal, represented bytes, projection, and the prefix count, including the 129-to-64 MiB quantum example. `packages/core/session/tests/image-offload.spec.ts` pins append and seed validation, exact current-surface image paths, rejection of persisted derived markers, strict advance, arbitrary-depth marking, frozen copies, cache rebuild, and scratch-replay equality. `packages/llm/llm-retry/tests/image-offload.spec.ts` pins the `IMAGE_OFFLOAD_REQUIRED` advance-and-retry path without a retry event, request-order counts after a surface replacement, and the exhausted case that delegates downstream. Compaction tests pin watermark application to direct summarization input. Adapter specs pin placeholder projection, prepared-only-retained reads, and the exact-byte failure with its count; `route-pricing.spec.ts` pins watermark pricing. The `inline-image-prompt` TypeScript SDK snapshot replays an authored `IMAGE_OFFLOAD_REQUIRED` attempt through the shipped profile and pins the advance and the retried request. The Python SDK notification test pins lossless forwarding of the new event and its nested watermark.

+ 5 - 5
.agents/notes/implemented/architecture/2026-09-02-image-offload-watermark.zh.md

@@ -20,9 +20,9 @@ offload 位置是持久的会话事实:核心事件 `image/offload` 记录一
 
 **只前进。** 预算变大、路由切换或 compaction 降低总量时水位永不回退,所以模型可见前缀和 provider 缓存前缀只向前移动。水位之下的出现位置后来被 compaction 遮蔽也不影响水位有效性,因为比较是按位置进行的。
 
-**决定权在一个插件,预算由路由声明。** 支持图片的路由在其 `LlmResolvedModelInfo` 上以 `imageRequest` 声明一个 `LlmImageRequestBudget`(`representation`、`maxBytes`、`maxImages`、两个量子与请求版本字节目标)。`dsh-llm-image-offload` 插件监听 `agent/pre-step`:下游监听器决定进入 step 后,它通过 `ctx.llm.resolveModelInfo()` 解析最新 `request/header` 所指路由的预算,按模型请求顺序收集保留的出现位置,用纯函数 `offloadedImagePrefixCount()` 规划删除前缀(表示字节是归一化字节数按版本目标截断后的值,内联路由再按 base64 展开,按整量子删除)。表层替换可能把较新的事件放到较老事件之前;插件把请求前缀换算成其中最大的持久位置,因此可能额外省略一些出现位置,但一定满足要求的删除量。事件落在 `step/start` 之前,循环的常规派生因此发送已省略的集合。第一个 header 之前,以及进入的 step 自己追加的出现位置,不做规划,由下面的 adapter 失败路径覆盖。DeepSeek adapter 声明其 file 模式预算,pi-ai adapter 声明其 base64 上限;agent loop 不变。
+**决定权在 `dsh-llm-retry`,预算由路由执行。** 支持图片的路由按保留的出现位置的精确请求版本字节执行一个 `LlmImageRequestBudget`(`representation`、`maxBytes`、`maxImages` 与两个量子),超出时以失败上报;发送前没有任何规划。`dsh-llm-retry` 已经是 `agent/request-error` waterfall 上的恢复执行器,它在任何提供方策略之前处理 `IMAGE_OFFLOAD_REQUIRED`:按模型请求顺序遍历表层,把上报的数量换算成该请求序前缀中最大的持久位置(表层替换可能把较新的事件放到较老事件之前,推进因此可能额外省略一些出现位置,但一定覆盖该前缀),追加 `image/offload`,返回 `retry` 动作,不占提供方重试预算,也不记录 `llm/retry`。循环在派生表层上重跑该 step;agent loop 不变。
 
-**adapter 只投影,不决定。** 序列化把每个 `offloaded` 块渲染为带当前已解析访问路径的 `offloadedImageText`,只准备保留的出现位置。当保留的出现位置按精确请求版本字节仍超过路由预算,无论是 file 模式、内联回退更紧的预算还是 pi-ai 上限,adapter 都以 `IMAGE_OFFLOAD_REQUIRED` 让本次尝试失败,并在 `LlmFailure.offloadImages` 中用共享的 `requiredImageOffload()` 算出还需省略多少最老的出现位置。插件的 `agent/request-error` 监听器按该数量推进水位并返回 `retry` 动作,循环据此在派生表层上重跑该 step;没有可省略的出现位置时,监听器向下游委托,失败进入普通恢复路径。
+**adapter 只投影,不决定。** 序列化把每个 `offloaded` 块渲染为带当前已解析访问路径的 `offloadedImageText`,只准备保留的出现位置。当保留的出现位置按精确请求版本字节仍超过路由预算,无论是 file 模式、内联回退更紧的预算还是 pi-ai 上限,adapter 都以 `IMAGE_OFFLOAD_REQUIRED` 让本次尝试失败,并在 `LlmFailure.offloadImages` 中用共享的 `requiredImageOffload()` 算出还需省略多少最老的出现位置。没有可省略的出现位置时,`dsh-llm-retry` 向下游委托,失败进入普通恢复路径。
 
 **token 记账。** `priceImages` 接收表层的 `ImageBlock`,把 `offloaded` 的按占位文本定价;DeepSeek 和 replay 的定价不再复现任何 offload 算术。meter 把 `image/offload` 折进其重放状态,按当前水位为当前表层定价,按每个 usage 锚点的请求派生时的水位为该锚点定价。已完成请求仍以 provider usage 为锚点。
 
@@ -40,16 +40,16 @@ offload 位置是持久的会话事实:核心事件 `image/offload` 记录一
 
 **让各个 adapter 自己追加事件。** adapter 拥有预算,但不拥有会话表层;在循环之下追加表层事实绕过了会话的派生历史,也会让两个 adapter 对表层做出不同定义。adapter 改为上报它需要的数量。
 
-**在 agent loop 内部规划并推进。** 循环在派生每个请求之前就知道精确的已准备路由,在那里规划永远不会多花一次失败的尝试但这会把一条路由专属的策略放进所有 profile 共用的那个组件,改变已记录的 step 顺序,还绕过了上下文溢出 compaction 和重试已经在用的同一套 `agent/request-error` waterfall。放在现有 `agent/pre-step` 与 `agent/request-error` 扩展点上的插件,只在 step 自己的消息或未知路由把保留集合推过预算时多花一次失败的尝试,循环保持不变
+**在发送前规划省略,无论放在循环里还是插件里。** 循环在派生每个请求之前就知道精确的已准备路由,在那里规划永远不会多花一次失败的尝试但这会把一条路由专属的策略放进所有 profile 共用的那个组件,改变已记录的 step 顺序,还绕过了上下文溢出 compaction 和重试已经在用的同一套 `agent/request-error` waterfall。pre-step 插件不改循环,但看不到 step 自己的消息和第一个请求的路由,失败路径仍然必需,而且规划要求每条路由在模型信息上声明预算。只处理失败的代价是每越过一个量子多一次尝试(DeepSeek file 模式 64 MiB,pi-ai 20 MiB),并且和既定方向一致:路由将不再本地检查大小,全部发送,由 provider 报告无法缓存的部分,那正是一个指明省略点的失败
 
 **为内联回退和精确字节溢出保留临时的额外省略。** 恰好会在不变量所针对的场景发送未记录的投影;失败再推进的路径只多花一次序列化尝试,且让每个已发出请求都可由日志派生。
 
 ## 后果
 
-预算变大、选中更大的路由或 compaction 降低总量时,被省略的图片不会自动回归;恢复手段是占位文本中的只读路径,模型需要时主动使用。一次 Files 故障或临时切到小预算路由会永久推进水位,两者出于同一理由被接受。
+预算变大、选中更大的路由或 compaction 降低总量时,被省略的图片不会自动回归;恢复手段是占位文本中的只读路径,模型需要时主动使用。一次 Files 故障或临时切到小预算路由会永久推进水位,两者出于同一理由被接受。路由本地的字节检查是过渡实现:provider 自己报告无法缓存的图片之后,`requiredImageOffload()` 和路由预算会被删掉,事件、派生和恢复分支保持不变。
 
 每个已发出请求的图片集合仅由日志决定,覆盖 file 模式、内联回退、resume、fork、retry 和 compaction,provider 缓存前缀不再抖动。占位和句柄文本中嵌入的执行世界访问路径仍在序列化时解析,这个缺口对保留的图片同样存在,属于另一个关于记录执行世界映射的决定。
 
 ## 测试
 
-`packages/llm/llm/tests/content.spec.ts` 钉住任意深度的图片遍历、表示字节、投影和前缀计数,包括 129 到 64 MiB 的量子示例。`packages/core/session/tests/image-offload.spec.ts` 钉住追加与 seed 校验、当前表层图片路径、拒绝持久化派生标记、严格推进、任意深度标记、冻结副本、缓存重建和从头重放的一致性。`packages/llm/llm-image-offload/tests/image-offload.spec.ts` 钉住 step 前推进、不回退的水位、表层替换后的请求顺序计数、`IMAGE_OFFLOAD_REQUIRED` 的推进并重试路径以及向下游委托的耗尽情况。compaction 测试钉住直接摘要输入中的水位应用。adapter 测试钉住占位投影、只读取保留图片以及带数量的精确字节失败;`route-pricing.spec.ts` 钉住水位定价。`inline-image-prompt` TypeScript SDK 快照通过发布的 profile 重放一次手工编写的 `IMAGE_OFFLOAD_REQUIRED` 尝试,钉住推进和重试后的请求。Python SDK 通知测试钉住新事件及其嵌套水位的无损转发。
+`packages/llm/llm/tests/content.spec.ts` 钉住任意深度的图片遍历、表示字节、投影和前缀计数,包括 129 到 64 MiB 的量子示例。`packages/core/session/tests/image-offload.spec.ts` 钉住追加与 seed 校验、当前表层图片路径、拒绝持久化派生标记、严格推进、任意深度标记、冻结副本、缓存重建和从头重放的一致性。`packages/llm/llm-retry/tests/image-offload.spec.ts` 钉住 `IMAGE_OFFLOAD_REQUIRED` 推进并重试且不追加重试事件的路径、表层替换后的请求顺序计数,以及向下游委托的耗尽情况。compaction 测试钉住直接摘要输入中的水位应用。adapter 测试钉住占位投影、只读取保留图片以及带数量的精确字节失败;`route-pricing.spec.ts` 钉住水位定价。`inline-image-prompt` TypeScript SDK 快照通过发布的 profile 重放一次手工编写的 `IMAGE_OFFLOAD_REQUIRED` 尝试,钉住推进和重试后的请求。Python SDK 通知测试钉住新事件及其嵌套水位的无损转发。

+ 0 - 3
apps/cli/composition.md

@@ -42,8 +42,6 @@ flowchart LR
   cfg --> plugin_dsh_base_jobs
   plugin_dsh_base_llm_retry["llm-retry<br/>@deepseek-ai/dsh-llm-retry"]
   cfg --> plugin_dsh_base_llm_retry
-  plugin_dsh_base_llm_image_offload["llm-image-offload<br/>@deepseek-ai/dsh-llm-image-offload"]
-  cfg --> plugin_dsh_base_llm_image_offload
   plugin_dsh_base_settings["settings<br/>@deepseek-ai/dsh-settings-file"]
   cfg --> plugin_dsh_base_settings
   plugin_dsh_base_credentials["credentials<br/>@deepseek-ai/dsh-credentials-local"]
@@ -199,7 +197,6 @@ flowchart LR
 | `agent-default-model` | `@deepseek-ai/dsh-agent-default-model` |
 | `jobs` | `@deepseek-ai/dsh-jobs-local` |
 | `llm-retry` | `@deepseek-ai/dsh-llm-retry` |
-| `llm-image-offload` | `@deepseek-ai/dsh-llm-image-offload` |
 | `settings` | `@deepseek-ai/dsh-settings-file` |
 | `credentials` | `@deepseek-ai/dsh-credentials-local` |
 | `llm-pi-ai` | `@deepseek-ai/dsh-llm-pi-ai` |

+ 2 - 2
docs/config-catalog.i18n.yaml

@@ -2,5 +2,5 @@
 # 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 docs/config-catalog.md
-config-catalog.md: d31ccd412ec108b54de5162590ed0951e8d36739
-config-catalog.zh.md: 649a510b7feedf0708c109d9ed838745306528e5
+config-catalog.md: fd30c7391f96221544f1d8b608923e51dd84b623
+config-catalog.zh.md: 6ec95d0e76d687e82f9cfb2fa29ab13e20bcd7db

+ 1 - 14
docs/config-catalog.md

@@ -1031,19 +1031,6 @@ Depends on: [`ModelModality`](../packages/llm/llm/src/index.ts) · [`RetryPolicy
 
 Source: [`packages/llm/llm-deepseek/src/index.ts:125`](../packages/llm/llm-deepseek/src/index.ts)
 
-<a id="deepseek-aidsh-llm-image-offload"></a>
-
-## `@deepseek-ai/dsh-llm-image-offload`
-
-Requires: `llm` · `agents`
-
-```ts config-catalog
-/** The plugin has no configuration; image-capable routes declare their budgets. */
-export type Config = Readonly<Record<string, never>>
-```
-
-Source: [`packages/llm/llm-image-offload/src/index.ts:25`](../packages/llm/llm-image-offload/src/index.ts)
-
 <a id="deepseek-aidsh-llm-pi-ai"></a>
 
 ## `@deepseek-ai/dsh-llm-pi-ai`
@@ -1407,7 +1394,7 @@ Requires: `agents` · `sessionProjections`
 export type Config = Readonly<Record<string, never>>
 ```
 
-Source: [`packages/llm/llm-retry/src/index.ts:25`](../packages/llm/llm-retry/src/index.ts)
+Source: [`packages/llm/llm-retry/src/index.ts:27`](../packages/llm/llm-retry/src/index.ts)
 
 <a id="deepseek-aidsh-lsp-stdio"></a>
 

+ 1 - 14
docs/config-catalog.zh.md

@@ -1033,19 +1033,6 @@ export interface DeepSeekCatalogModel {
 
 来源:[`packages/llm/llm-deepseek/src/index.ts:125`](../packages/llm/llm-deepseek/src/index.ts)
 
-<a id="deepseek-aidsh-llm-image-offload"></a>
-
-## `@deepseek-ai/dsh-llm-image-offload`
-
-需要:`llm` · `agents`
-
-```ts config-catalog
-/** The plugin has no configuration; image-capable routes declare their budgets. */
-export type Config = Readonly<Record<string, never>>
-```
-
-来源:[`packages/llm/llm-image-offload/src/index.ts:25`](../packages/llm/llm-image-offload/src/index.ts)
-
 <a id="deepseek-aidsh-llm-pi-ai"></a>
 
 ## `@deepseek-ai/dsh-llm-pi-ai`
@@ -1409,7 +1396,7 @@ export interface ReplayModelConfig {
 export type Config = Readonly<Record<string, never>>
 ```
 
-来源:[`packages/llm/llm-retry/src/index.ts:25`](../packages/llm/llm-retry/src/index.ts)
+来源:[`packages/llm/llm-retry/src/index.ts:27`](../packages/llm/llm-retry/src/index.ts)
 
 <a id="deepseek-aidsh-lsp-stdio"></a>
 

+ 2 - 2
docs/event-producer-consumer.i18n.yaml

@@ -2,5 +2,5 @@
 # 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 docs/event-producer-consumer.md
-event-producer-consumer.md: 4911452782d5bb26d214e86c71cf24f27bac0fd4
-event-producer-consumer.zh.md: 8f08cc98d05d2a96b81e49b6fd67b308c0bfc5dc
+event-producer-consumer.md: 1f8bb7772f3941e69f09425e4ed54e1f424a2e99
+event-producer-consumer.zh.md: 31e01a2f6082e59bf21541660c6b28d77222f04d

+ 2 - 2
docs/event-producer-consumer.md

@@ -16,9 +16,9 @@ This matrix shows which packages dispatch each harness-owned event and which pac
 | `agent/inbox/claimed` | `emit` | [`packages/core/agent/src/runtime-types.ts:242`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-jobs`](../packages/jobs/tool-jobs) |
 | `agent/inbox/discarded` | `emit` | [`packages/core/agent/src/runtime-types.ts:250`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent) |
 | `agent/inbox/inserted` | `emit` | [`packages/core/agent/src/runtime-types.ts:231`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver) |
-| `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:276`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent-instructions`](../packages/context/agent-instructions), [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`llm-image-offload`](../packages/llm/llm-image-offload), [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-reference`](../packages/context/session-reference), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-cordis`](../packages/extensions/tool-cordis), [`tool-skill`](../packages/skill/tool-skill), [`tool-subagent`](../packages/subagent/tool-subagent) |
+| `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:276`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent-instructions`](../packages/context/agent-instructions), [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-reference`](../packages/context/session-reference), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-cordis`](../packages/extensions/tool-cordis), [`tool-skill`](../packages/skill/tool-skill), [`tool-subagent`](../packages/subagent/tool-subagent) |
 | `agent/request` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:289`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`webhook`](../packages/webhook/webhook) |
-| `agent/request-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:305`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compaction-basic`](../packages/compaction/compaction-basic), [`llm-image-offload`](../packages/llm/llm-image-offload), [`llm-retry`](../packages/llm/llm-retry) |
+| `agent/request-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:305`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compaction-basic`](../packages/compaction/compaction-basic), [`llm-retry`](../packages/llm/llm-retry) |
 | `agent/session-start` | `emit` | [`packages/core/agent/src/runtime-types.ts:262`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emitAgentEvent`) | `agent-team`, [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex) |
 | `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:223`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`agent`](../packages/core/agent), `agent-team`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`schedule`](../packages/schedule/schedule), `server`, `session-controller` |
 | `agent/turn-stopping` | `serial` | [`packages/core/agent/src/runtime-types.ts:333`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex) |

+ 2 - 2
docs/event-producer-consumer.zh.md

@@ -18,9 +18,9 @@
 | `agent/inbox/claimed` | `emit` | [`packages/core/agent/src/runtime-types.ts:242`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`acp`](../packages/acp/acp), [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent), [`tool-jobs`](../packages/jobs/tool-jobs) |
 | `agent/inbox/discarded` | `emit` | [`packages/core/agent/src/runtime-types.ts:250`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver), [`subagent`](../packages/subagent/subagent) |
 | `agent/inbox/inserted` | `emit` | [`packages/core/agent/src/runtime-types.ts:231`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver) |
-| `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:276`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent-instructions`](../packages/context/agent-instructions), [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`llm-image-offload`](../packages/llm/llm-image-offload), [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-reference`](../packages/context/session-reference), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-cordis`](../packages/extensions/tool-cordis), [`tool-skill`](../packages/skill/tool-skill), [`tool-subagent`](../packages/subagent/tool-subagent) |
+| `agent/pre-step` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:276`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent-instructions`](../packages/context/agent-instructions), [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex), [`plan-mode`](../packages/plan/plan-mode), [`repeat-tool-reminder`](../packages/guard/repeat-tool-reminder), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-reference`](../packages/context/session-reference), [`subagent-in-process-driver`](../packages/subagent/subagent-in-process-driver), [`time-context`](../packages/context/time-context), [`tmux-context`](../packages/context/tmux-context), [`tool-cordis`](../packages/extensions/tool-cordis), [`tool-skill`](../packages/skill/tool-skill), [`tool-subagent`](../packages/subagent/tool-subagent) |
 | `agent/request` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:289`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`agent`](../packages/core/agent), [`webhook`](../packages/webhook/webhook) |
-| `agent/request-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:305`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compaction-basic`](../packages/compaction/compaction-basic), [`llm-image-offload`](../packages/llm/llm-image-offload), [`llm-retry`](../packages/llm/llm-retry) |
+| `agent/request-error` | `waterfall` | [`packages/core/agent/src/runtime-types.ts:305`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`waterfall`) | [`compaction-basic`](../packages/compaction/compaction-basic), [`llm-retry`](../packages/llm/llm-retry) |
 | `agent/session-start` | `emit` | [`packages/core/agent/src/runtime-types.ts:262`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emitAgentEvent`) | `agent-team`, [`goal`](../packages/goal/goal), [`goal-round-driver`](../packages/goal/goal-round-driver), [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex) |
 | `agent/status` | `emit` | [`packages/core/agent/src/runtime-types.ts:223`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`emit`) | [`agent`](../packages/core/agent), `agent-team`, [`compaction-basic`](../packages/compaction/compaction-basic), [`goal-round-driver`](../packages/goal/goal-round-driver), [`schedule`](../packages/schedule/schedule), `server`, `session-controller` |
 | `agent/turn-stopping` | `serial` | [`packages/core/agent/src/runtime-types.ts:333`](../packages/core/agent/src/runtime-types.ts) | [`agent-loop`](../packages/core/agent-loop) (`serial`) | [`hooks-claude-code`](../packages/hooks/hooks-claude-code), [`hooks-codex`](../packages/hooks/hooks-codex) |

+ 2 - 2
docs/module-graph.i18n.yaml

@@ -2,5 +2,5 @@
 # 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 docs/module-graph.md
-module-graph.md: 550b29c672089d024162c0b0a1ad3efd8007ef0a
-module-graph.zh.md: c4f8ff30c93b726657b7e5d1193e0ec9ded4993e
+module-graph.md: 9da7242a5c6fca227175892eaaf65ccf45c61316
+module-graph.zh.md: 58f0e7eda73e8094072390041e683f1cf7e5c140

+ 0 - 5
docs/module-graph.md

@@ -27,7 +27,6 @@ flowchart TD
     pkg_deepseek_llm_api_extensions["deepseek-llm-api-extensions"]
     pkg_llm["llm"]
     pkg_llm_deepseek["llm-deepseek"]
-    pkg_llm_image_offload["llm-image-offload"]
     pkg_llm_pi_ai["llm-pi-ai"]
     pkg_llm_retry["llm-retry"]
     pkg_plugin_package_inventory_deepseek["plugin-package-inventory-deepseek"]
@@ -505,9 +504,6 @@ flowchart TD
   pkg_llm_deepseek --> pkg_llm
   pkg_llm_deepseek --> pkg_settings
   pkg_llm_deepseek --> pkg_timeout
-  pkg_llm_image_offload --> pkg_agent
-  pkg_llm_image_offload --> pkg_llm
-  pkg_llm_image_offload --> pkg_session
   pkg_llm_pi_ai --> pkg_attachment
   pkg_llm_pi_ai --> pkg_authorization
   pkg_llm_pi_ai --> pkg_credentials
@@ -1304,7 +1300,6 @@ flowchart TD
 | [`shell`](../packages/shell/shell) | `shell` | [`sandbox`](../packages/sandbox/sandbox), [`settings`](../packages/settings/settings), [`subprocess`](../packages/subprocess/subprocess) |
 | [`workspace`](../packages/workspace/workspace) | `workspace` | [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`storage`](../packages/storage/storage), [`storage-domain`](../packages/storage/storage-domain), [`typert-protocol`](../packages/typert/protocol) |
 | [`llm-deepseek`](../packages/llm/llm-deepseek) | `llm` | [`anonymous-user-id`](../packages/identity/anonymous-user-id), [`atomic-write`](../packages/util/atomic-write), [`attachment`](../packages/attachment/attachment), [`credentials`](../packages/credentials/credentials), [`deepseek-llm-api-extensions`](../packages/llm/deepseek-llm-api-extensions), [`fs`](../packages/fs/fs), [`home-paths`](../packages/util/home-paths), [`launch-environment`](../packages/util/launch-environment), [`llm`](../packages/llm/llm), [`settings`](../packages/settings/settings), [`timeout`](../packages/util/timeout) |
-| [`llm-image-offload`](../packages/llm/llm-image-offload) | `llm` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
 | [`llm-pi-ai`](../packages/llm/llm-pi-ai) | `llm` | [`attachment`](../packages/attachment/attachment), [`authorization`](../packages/credentials/authorization), [`credentials`](../packages/credentials/credentials), [`fs`](../packages/fs/fs), [`launch-environment`](../packages/util/launch-environment), [`llm`](../packages/llm/llm), [`settings`](../packages/settings/settings), [`timeout`](../packages/util/timeout) |
 | [`llm-retry`](../packages/llm/llm-retry) | `llm` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`timeout`](../packages/util/timeout) |
 | [`agent-default-model`](../packages/core/agent-default-model) | `core` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`settings`](../packages/settings/settings) |

+ 0 - 5
docs/module-graph.zh.md

@@ -29,7 +29,6 @@ flowchart TD
     pkg_deepseek_llm_api_extensions["deepseek-llm-api-extensions"]
     pkg_llm["llm"]
     pkg_llm_deepseek["llm-deepseek"]
-    pkg_llm_image_offload["llm-image-offload"]
     pkg_llm_pi_ai["llm-pi-ai"]
     pkg_llm_retry["llm-retry"]
     pkg_plugin_package_inventory_deepseek["plugin-package-inventory-deepseek"]
@@ -507,9 +506,6 @@ flowchart TD
   pkg_llm_deepseek --> pkg_llm
   pkg_llm_deepseek --> pkg_settings
   pkg_llm_deepseek --> pkg_timeout
-  pkg_llm_image_offload --> pkg_agent
-  pkg_llm_image_offload --> pkg_llm
-  pkg_llm_image_offload --> pkg_session
   pkg_llm_pi_ai --> pkg_attachment
   pkg_llm_pi_ai --> pkg_authorization
   pkg_llm_pi_ai --> pkg_credentials
@@ -1306,7 +1302,6 @@ flowchart TD
 | [`shell`](../packages/shell/shell) | `shell` | [`sandbox`](../packages/sandbox/sandbox), [`settings`](../packages/settings/settings), [`subprocess`](../packages/subprocess/subprocess) |
 | [`workspace`](../packages/workspace/workspace) | `workspace` | [`invariants`](../packages/runtime-diagnostics/invariants), [`session`](../packages/core/session), [`session-persistence`](../packages/session/session-persistence), [`storage`](../packages/storage/storage), [`storage-domain`](../packages/storage/storage-domain), [`typert-protocol`](../packages/typert/protocol) |
 | [`llm-deepseek`](../packages/llm/llm-deepseek) | `llm` | [`anonymous-user-id`](../packages/identity/anonymous-user-id), [`atomic-write`](../packages/util/atomic-write), [`attachment`](../packages/attachment/attachment), [`credentials`](../packages/credentials/credentials), [`deepseek-llm-api-extensions`](../packages/llm/deepseek-llm-api-extensions), [`fs`](../packages/fs/fs), [`home-paths`](../packages/util/home-paths), [`launch-environment`](../packages/util/launch-environment), [`llm`](../packages/llm/llm), [`settings`](../packages/settings/settings), [`timeout`](../packages/util/timeout) |
-| [`llm-image-offload`](../packages/llm/llm-image-offload) | `llm` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`session`](../packages/core/session) |
 | [`llm-pi-ai`](../packages/llm/llm-pi-ai) | `llm` | [`attachment`](../packages/attachment/attachment), [`authorization`](../packages/credentials/authorization), [`credentials`](../packages/credentials/credentials), [`fs`](../packages/fs/fs), [`launch-environment`](../packages/util/launch-environment), [`llm`](../packages/llm/llm), [`settings`](../packages/settings/settings), [`timeout`](../packages/util/timeout) |
 | [`llm-retry`](../packages/llm/llm-retry) | `llm` | [`agent`](../packages/core/agent), [`brand`](../packages/util/brand), [`invariants`](../packages/runtime-diagnostics/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-projection`](../packages/session/session-projection), [`timeout`](../packages/util/timeout) |
 | [`agent-default-model`](../packages/core/agent-default-model) | `core` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`settings`](../packages/settings/settings) |

+ 2 - 2
docs/subsystems/llm-streaming.i18n.yaml

@@ -2,5 +2,5 @@
 # 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 docs/subsystems/llm-streaming.md
-llm-streaming.md: b0414499e9cfc2fb98bcceed1142aafdb0c59dbf
-llm-streaming.zh.md: 6c509e18a8095c8b420e94c4a0410523dd958486
+llm-streaming.md: f2c8c09779e3e8e808dc62f9861230b7ac69cb87
+llm-streaming.zh.md: ddf7ae1dd5a514b58178521c526b0de1d6c165e6

+ 2 - 4
docs/subsystems/llm-streaming.md

@@ -247,8 +247,8 @@ interface LlmFailure {
   /**
    * With code `IMAGE_OFFLOAD_REQUIRED`: how many more of the oldest retained
    * image occurrences the route needs offloaded before the same request fits
-   * its exact byte accounting. The `dsh-llm-image-offload` plugin advances the
-   * durable watermark by this count and retries the step.
+   * its exact byte accounting. `dsh-llm-retry` advances the session's durable
+   * `image/offload` watermark by this count and retries the step.
    */
   readonly offloadImages?: number
 }
@@ -570,8 +570,6 @@ interface LlmResolvedModelInfo extends LlmModelInfo {
   defaultMaxTokens?: number
   /** Adapter-owned selectable reasoning levels when exposed. */
   reasoning?: LlmModelReasoningInfo
-  /** Request-image budget the route enforces; absent for routes that never offload. */
-  imageRequest?: LlmImageRequestBudget
 }
 ```
 

+ 2 - 4
docs/subsystems/llm-streaming.zh.md

@@ -249,8 +249,8 @@ interface LlmFailure {
   /**
    * With code `IMAGE_OFFLOAD_REQUIRED`: how many more of the oldest retained
    * image occurrences the route needs offloaded before the same request fits
-   * its exact byte accounting. The `dsh-llm-image-offload` plugin advances the
-   * durable watermark by this count and retries the step.
+   * its exact byte accounting. `dsh-llm-retry` advances the session's durable
+   * `image/offload` watermark by this count and retries the step.
    */
   readonly offloadImages?: number
 }
@@ -576,8 +576,6 @@ interface LlmResolvedModelInfo extends LlmModelInfo {
   defaultMaxTokens?: number
   /** Adapter-owned selectable reasoning levels when exposed. */
   reasoning?: LlmModelReasoningInfo
-  /** Request-image budget the route enforces; absent for routes that never offload. */
-  imageRequest?: LlmImageRequestBudget
 }
 ```
 

+ 2 - 2
docs/subsystems/session.i18n.yaml

@@ -2,5 +2,5 @@
 # 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 docs/subsystems/session.md
-session.md: acebb773edfdef905d9d1488d8d19cf2773b39bf
-session.zh.md: b8de950392e8643618de16b41a4f850ee47e2501
+session.md: 68beb5f1fb77f723c59adfc583380773e41be1ae
+session.zh.md: 7134819ee4572ba65e89b3f35be837c7a139fe99

+ 1 - 1
docs/subsystems/session.md

@@ -200,7 +200,7 @@ interface RequestContext {
 
 ### The image offload event: `image/offload`
 
-The `dsh-llm-image-offload` plugin appends `image/offload` inside an open turn: before a step enters, when the surface's retained image occurrences exceed the request-image budget of the route the latest `request/header` names, and on the `agent/request-error` waterfall when an adapter fails an attempt with `IMAGE_OFFLOAD_REQUIRED`, before the step retries. Its `watermark` names the last offloaded occurrence; `deriveMessages()` and the Session instance's `deriveEventMessage()` mark every occurrence positioned at or before it `offloaded: true`, and each route renders those marks as placeholder text. `Session.append` and seeding require the watermark to identify an image on the current surface, reject malformed and non-advancing positions, and reject request-only `offloaded` markers in durable messages. `session.imageOffloadWatermark()` folds the latest frozen position. Like `request/header`, it is not a `SurfaceEventType`; unlike it, it changes the derived surface, so it stays required-on-read ([decision](../../.agents/notes/implemented/architecture/2026-09-02-image-offload-watermark.md)).
+`dsh-llm-retry` appends `image/offload` on the `agent/request-error` waterfall when an adapter fails an attempt with `IMAGE_OFFLOAD_REQUIRED`, before the step retries. Its `watermark` names the last offloaded occurrence; `deriveMessages()` and the Session instance's `deriveEventMessage()` mark every occurrence positioned at or before it `offloaded: true`, and each route renders those marks as placeholder text. `Session.append` and seeding require the watermark to identify an image on the current surface, reject malformed and non-advancing positions, and reject request-only `offloaded` markers in durable messages. `session.imageOffloadWatermark()` folds the latest frozen position. Like `request/header`, it is not a `SurfaceEventType`; unlike it, it changes the derived surface, so it stays required-on-read ([decision](../../.agents/notes/implemented/architecture/2026-09-02-image-offload-watermark.md)).
 
 ```ts type-equiv
 /**

+ 1 - 1
docs/subsystems/session.zh.md

@@ -200,7 +200,7 @@ interface RequestContext {
 
 ### 图片 offload 事件:`image/offload`
 
-`dsh-llm-image-offload` 插件在打开的轮次内追加 `image/offload`:step 进入前,表层上保留的图片出现位置超过最新 `request/header` 所指路由的请求图片预算时追加一次;adapter 以 `IMAGE_OFFLOAD_REQUIRED` 让一次尝试失败时,在 `agent/request-error` waterfall 上、step 重试前再追加一次。其 `watermark` 指向最后一个被省略的出现位置;`deriveMessages()` 与 Session 实例的 `deriveEventMessage()` 把位于它及之前的每个出现位置标为 `offloaded: true`,每条路由把这些标记渲染为占位文本。`Session.append` 与 seed 要求水位指向当前表层的图片,拒绝畸形和没有严格前进的位置,并拒绝持久消息中的请求专用 `offloaded` 标记。`session.imageOffloadWatermark()` 折叠最新的冻结位置。它和 `request/header` 一样不是 `SurfaceEventType`;与之不同的是它改变派生表层,因此读取时必须识别([决定](../../.agents/notes/implemented/architecture/2026-09-02-image-offload-watermark.zh.md))。
+adapter 以 `IMAGE_OFFLOAD_REQUIRED` 让一次尝试失败时,`dsh-llm-retry` 在 `agent/request-error` waterfall 上、step 重试前追加 `image/offload`。其 `watermark` 指向最后一个被省略的出现位置;`deriveMessages()` 与 Session 实例的 `deriveEventMessage()` 把位于它及之前的每个出现位置标为 `offloaded: true`,每条路由把这些标记渲染为占位文本。`Session.append` 与 seed 要求水位指向当前表层的图片,拒绝畸形和没有严格前进的位置,并拒绝持久消息中的请求专用 `offloaded` 标记。`session.imageOffloadWatermark()` 折叠最新的冻结位置。它和 `request/header` 一样不是 `SurfaceEventType`;与之不同的是它改变派生表层,因此读取时必须识别([决定](../../.agents/notes/implemented/architecture/2026-09-02-image-offload-watermark.zh.md))。
 
 ```ts type-equiv
 /**

+ 0 - 3
packages/bundle/base/cordis.patch.yml

@@ -84,9 +84,6 @@
     - id: llm-retry
       name: '@deepseek-ai/dsh-llm-retry'
 
-    - id: llm-image-offload
-      name: '@deepseek-ai/dsh-llm-image-offload'
-
     # User-settings document (`$DSH_HOME/settings.yaml`, hot-reloaded): a
     # `llm-deepseek:` or `llm-pi-ai:` section there overrides the adapter entries
     # below without a restart, and is what the web Models page writes.

+ 0 - 1
packages/bundle/base/package.json

@@ -60,7 +60,6 @@
     "@deepseek-ai/dsh-llm": "workspace:^",
     "@deepseek-ai/dsh-llm-deepseek": "workspace:^",
     "@deepseek-ai/dsh-llm-pi-ai": "workspace:^",
-    "@deepseek-ai/dsh-llm-image-offload": "workspace:^",
     "@deepseek-ai/dsh-llm-retry": "workspace:^",
     "@deepseek-ai/dsh-permission-presets": "workspace:^",
     "@deepseek-ai/dsh-plan-mode": "workspace:^",

+ 0 - 3
packages/bundle/sdk-minimal/cordis.patch.yml

@@ -104,9 +104,6 @@
     - id: llm-retry
       name: '@deepseek-ai/dsh-llm-retry'
 
-    - id: llm-image-offload
-      name: '@deepseek-ai/dsh-llm-image-offload'
-
     - id: jobs
       name: '@deepseek-ai/dsh-jobs-local'
 

+ 0 - 1
packages/bundle/sdk-minimal/package.json

@@ -43,7 +43,6 @@
     "@deepseek-ai/dsh-jobs-local": "workspace:^",
     "@deepseek-ai/dsh-llm": "workspace:^",
     "@deepseek-ai/dsh-llm-deepseek": "workspace:^",
-    "@deepseek-ai/dsh-llm-image-offload": "workspace:^",
     "@deepseek-ai/dsh-llm-retry": "workspace:^",
     "@deepseek-ai/dsh-plugin-package-inventory-deepseek": "workspace:^",
     "@deepseek-ai/dsh-sandbox-local": "workspace:^",

+ 0 - 1
packages/bundle/sdk-minimal/tests/sdk-minimal.spec.ts

@@ -48,7 +48,6 @@ describe('dsh-sdk-minimal bundle', () => {
       ['tools', '@deepseek-ai/dsh-tools'],
       ['agent', '@deepseek-ai/dsh-agent'],
       ['llm-retry', '@deepseek-ai/dsh-llm-retry'],
-      ['llm-image-offload', '@deepseek-ai/dsh-llm-image-offload'],
       ['jobs', '@deepseek-ai/dsh-jobs-local'],
       ['invariants', '@deepseek-ai/dsh-invariants'],
       ['session-invariant', '@deepseek-ai/dsh-session/invariant'],

+ 2 - 2
packages/core/session/README.i18n.yaml

@@ -2,5 +2,5 @@
 # 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 packages/core/session/README.md
-README.md: 75aa851751e48e58829834f08118f5cd5685fcf2
-README.zh.md: 31e6a08024872bd5c1afde7200f1607e96da52ea
+README.md: 1c58fafe55b28916b94b917caaefbbad18a2dc86
+README.zh.md: ba2f84c13c82cd97cf3fee53e84a845b46569c07

+ 1 - 1
packages/core/session/README.md

@@ -83,7 +83,7 @@ The package is built on event sourcing: a `Session` is an append-only log of typ
 
 `request/header` stores a full canonical snapshot of the non-history request envelope with reason `initial`, `resume`, `change`, or `series`. An explicit message-series start or a surface replacement writes a `series` snapshot when the envelope is unchanged; a simultaneous change uses `startsSeries: true`. Same-series steps, retries, and ordinary later turns inherit the latest snapshot. `adapterDefaults` distinguishes values resolved by the adapter from explicit settings, and `foldRequestHeader()` selects the latest snapshot. This self-contained record supports partial-window rendering and exact reconstruction at the cost of growth per message series; the [reconstructable-requests Agent Note](../../../.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.md) owns the detail.
 
-`image/offload` records the durable image offload watermark the `dsh-llm-image-offload` plugin advances when a route's request-image budget is exceeded: an `ImageOccurrencePosition` naming the last offloaded occurrence by event seq and its complete nested block path. The derivation marks every occurrence at or before it `offloaded: true`, so each route sends its placeholder text instead of the image. The position only advances; `Session.append` and seeding require it to identify a current surface image and reject request-only `offloaded` markers in durable messages. Because the event changes the derived surface it is required-on-read: a build without the type refuses the log rather than replaying images the model never saw ([decision](../../../.agents/notes/implemented/architecture/2026-09-02-image-offload-watermark.md)).
+`image/offload` records the durable image offload watermark `dsh-llm-retry` advances when an adapter reports that a route's request-image budget is exceeded: an `ImageOccurrencePosition` naming the last offloaded occurrence by event seq and its complete nested block path. The derivation marks every occurrence at or before it `offloaded: true`, so each route sends its placeholder text instead of the image. The position only advances; `Session.append` and seeding require it to identify a current surface image and reject request-only `offloaded` markers in durable messages. Because the event changes the derived surface it is required-on-read: a build without the type refuses the log rather than replaying images the model never saw ([decision](../../../.agents/notes/implemented/architecture/2026-09-02-image-offload-watermark.md)).
 
 ### Source map
 

+ 1 - 1
packages/core/session/README.zh.md

@@ -83,7 +83,7 @@ session.deriveMessages()         // the derived model history
 
 `request/header` 存储非历史请求 envelope 的完整规范快照,原因为 `initial`、`resume`、`change` 或 `series`。显式消息序列起点或表层替换会在 envelope 不变时写入 `series` 快照;同时发生变化时使用 `startsSeries: true`。同一序列内的步骤、重试与普通后续轮次继承最新快照。`adapterDefaults` 区分由适配器解析的值与显式设置,`foldRequestHeader()` 选择最新快照。这种自包含记录以每个消息序列增加存储为代价,支持局部窗口渲染与精确重建;细节由[可重建请求 Agent Note](../../../.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.zh.md)负责。
 
-`image/offload` 记录 `dsh-llm-image-offload` 插件在路由请求图片预算被超过时推进的持久图片 offload 水位:一个以事件序号加完整嵌套块路径指向最后一个被省略出现位置的 `ImageOccurrencePosition`。派生把位于水位及之前的每个出现位置标为 `offloaded: true`,于是每条路由发送其占位文本而不是图片。位置只会推进;`Session.append` 与 seed 要求它指向当前表层图片,并拒绝持久消息中的请求专用 `offloaded` 标记。该事件改变派生表层,因此读取时必须识别:不认识该类型的构建拒绝这份日志,而不是带着模型从未见过的图片重放([决定](../../../.agents/notes/implemented/architecture/2026-09-02-image-offload-watermark.zh.md))。
+`image/offload` 记录 `dsh-llm-retry` 在 adapter 报告路由请求图片预算被超过时推进的持久图片 offload 水位:一个以事件序号加完整嵌套块路径指向最后一个被省略出现位置的 `ImageOccurrencePosition`。派生把位于水位及之前的每个出现位置标为 `offloaded: true`,于是每条路由发送其占位文本而不是图片。位置只会推进;`Session.append` 与 seed 要求它指向当前表层图片,并拒绝持久消息中的请求专用 `offloaded` 标记。该事件改变派生表层,因此读取时必须识别:不认识该类型的构建拒绝这份日志,而不是带着模型从未见过的图片重放([决定](../../../.agents/notes/implemented/architecture/2026-09-02-image-offload-watermark.zh.md))。
 
 ### 源码地图
 

+ 1 - 5
packages/extensions/tool-cordis/src/api-catalog.ts

@@ -4429,10 +4429,6 @@ export const TYPE_API: readonly TypeApiEntry[] = [
     name: 'LlmFailure',
     declaration: 'export interface LlmFailure {\n    readonly message: string;\n    readonly code: string;\n    readonly status?: number;\n    readonly providerRetryAfterMs?: number;\n    readonly requestId?: ProviderRequestId;\n    readonly offloadImages?: number;\n}',
   },
-  {
-    name: 'LlmImageRequestBudget',
-    declaration: 'export interface LlmImageRequestBudget {\n    representation: \'raw\' | \'base64\';\n    maxBytes?: number;\n    maxImages?: number;\n    byteQuantum?: number;\n    countQuantum?: number;\n    versionMaxBytes?: number;\n}',
-  },
   {
     name: 'LlmImageRequestPrice',
     declaration: 'export interface LlmImageRequestPrice {\n    visualTokens: number;\n    text: string;\n}',
@@ -4467,7 +4463,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [
   },
   {
     name: 'LlmResolvedModelInfo',
-    declaration: 'export interface LlmResolvedModelInfo extends LlmModelInfo {\n    context?: LlmModelContext;\n    defaultMaxTokens?: number;\n    reasoning?: LlmModelReasoningInfo;\n    imageRequest?: LlmImageRequestBudget;\n}',
+    declaration: 'export interface LlmResolvedModelInfo extends LlmModelInfo {\n    context?: LlmModelContext;\n    defaultMaxTokens?: number;\n    reasoning?: LlmModelReasoningInfo;\n}',
   },
   {
     name: 'LlmRuntime',

+ 2 - 2
packages/llm/README.i18n.yaml

@@ -2,5 +2,5 @@
 # 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 packages/llm/README.md
-README.md: 31f6ecc6b84e23169a539627befef030f023d0ef
-README.zh.md: 6d1d69441ac425d03f13a83fb655733a69b3a228
+README.md: 9f7e8f444345bb60677a57679ad559f1e06be83f
+README.zh.md: 5030a71a256af0d59dcf8d8e4875c0b9790e7a55

+ 1 - 2
packages/llm/README.md

@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
 
 ## Summary
 
-The llm group provides the harness's model-call capability: one provider-neutral service through which any composition streams requests to a model provider, plus adapters, provider-specific request metadata, retry execution, and measurement. The core `llm` package defines the message, content-block, and stream-chunk vocabulary every plugin and the session log use; provider adapters translate a provider's wire format into that vocabulary; DeepSeek request-extension plugins contribute lifecycle-owned metadata outside model input; `llm-retry` re-runs failed requests at durable agent-step boundaries; `llm-image-offload` advances the durable `image/offload` watermark that keeps request images within each route's budget; and `token-meter` measures request and context pressure from the durable log. This page maps the group; each package README owns its per-package contract.
+The llm group provides the harness's model-call capability: one provider-neutral service through which any composition streams requests to a model provider, plus adapters, provider-specific request metadata, retry execution, and measurement. The core `llm` package defines the message, content-block, and stream-chunk vocabulary every plugin and the session log use; provider adapters translate a provider's wire format into that vocabulary; DeepSeek request-extension plugins contribute lifecycle-owned metadata outside model input; `llm-retry` re-runs failed requests at durable agent-step boundaries; and `token-meter` measures request and context pressure from the durable log. This page maps the group; each package README owns its per-package contract.
 
 ## Table of Contents
 
@@ -30,7 +30,6 @@ The llm group provides the harness's model-call capability: one provider-neutral
 | [`deepseek-llm-api-extensions/`](deepseek-llm-api-extensions/README.md) | Registers lifecycle-owned top-level fields on official DeepSeek requests | `ctx.deepseekLlmApiExtensions` |
 | [`plugin-package-inventory-deepseek/`](plugin-package-inventory-deepseek/README.md) | Contributes the active Loader package inventory to official DeepSeek requests | contributes `dsh_plugin_packages` |
 | [`llm-retry/`](llm-retry/README.md) | Retries failed model requests under each provider's policy at durable agent-step boundaries | listens to `agent/request-error` |
-| [`llm-image-offload/`](llm-image-offload/README.md) | Advances the session's durable `image/offload` watermark from route budgets and adapter failures | listens to `agent/pre-step`, `agent/request-error` |
 | [`token-meter/`](token-meter/README.md) | Measures request and context pressure from the durable session log with a fixed heuristic | `ctx.tokenMeter` |
 
 -----

+ 1 - 2
packages/llm/README.zh.md

@@ -9,7 +9,7 @@ kind: "package-group"
 
 ## 概述
 
-llm 组提供 harness 的模型调用能力:一个提供方无关的服务,任何组合都可以通过它向模型提供方发起流式请求,外加适配器、提供方专用请求元数据、重试执行与计量。核心 `llm` 包定义所有插件与会话日志使用的消息、内容块与流式分片词汇;提供方适配器把某个提供方的协议格式翻译为该词汇;DeepSeek 请求扩展插件在模型输入之外贡献具有生命周期归属的元数据;`llm-retry` 在持久 agent 步骤边界上重跑失败的请求;`llm-image-offload` 推进持久 `image/offload` 水位,使请求图片不超过各路由预算;`token-meter` 从持久日志测量请求与上下文压力。本页是组的映射;每个包 README 负责各自的包级约定。
+llm 组提供 harness 的模型调用能力:一个提供方无关的服务,任何组合都可以通过它向模型提供方发起流式请求,外加适配器、提供方专用请求元数据、重试执行与计量。核心 `llm` 包定义所有插件与会话日志使用的消息、内容块与流式分片词汇;提供方适配器把某个提供方的协议格式翻译为该词汇;DeepSeek 请求扩展插件在模型输入之外贡献具有生命周期归属的元数据;`llm-retry` 在持久 agent 步骤边界上重跑失败的请求;`token-meter` 从持久日志测量请求与上下文压力。本页是组的映射;每个包 README 负责各自的包级约定。
 
 ## 目录
 
@@ -30,7 +30,6 @@ llm 组提供 harness 的模型调用能力:一个提供方无关的服务,
 | [`deepseek-llm-api-extensions/`](deepseek-llm-api-extensions/README.zh.md) | 在官方 DeepSeek 请求上注册具有生命周期归属的顶层字段 | `ctx.deepseekLlmApiExtensions` |
 | [`plugin-package-inventory-deepseek/`](plugin-package-inventory-deepseek/README.zh.md) | 为官方 DeepSeek 请求贡献活跃 Loader 包清单 | 贡献 `dsh_plugin_packages` |
 | [`llm-retry/`](llm-retry/README.zh.md) | 在持久 agent 步骤边界上按各提供方策略重试失败的模型请求 | 监听 `agent/request-error` |
-| [`llm-image-offload/`](llm-image-offload/README.zh.md) | 依据路由预算与适配器失败推进会话的持久 `image/offload` 水位 | 监听 `agent/pre-step`、`agent/request-error` |
 | [`token-meter/`](token-meter/README.zh.md) | 用固定启发式规则从持久会话日志测量请求与上下文压力 | `ctx.tokenMeter` |
 
 -----

+ 2 - 2
packages/llm/llm-deepseek/README.i18n.yaml

@@ -2,5 +2,5 @@
 # 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 packages/llm/llm-deepseek/README.md
-README.md: 25b79b13a0a1597e510d71c68032bc02a01f9451
-README.zh.md: 2f2dcd8444745e93724e56ce94c6337ef0b9d444
+README.md: ad9b89a35fb6bd53579d573aa8d88ee12dc80a77
+README.zh.md: dce5b3f7332173f4ef9a82d1dd6845b0597d009d

+ 2 - 2
packages/llm/llm-deepseek/README.md

@@ -58,7 +58,7 @@ A request selects the route with `provider: deepseek-official`; the model id pas
 | `defaultContextWindow` | `1,000,000` | Capacity fallback for models without an exact value |
 | `models` | V4 Flash + V4 Pro + V4 Flash Vision Exp | Advisory catalog shown by discovery consumers |
 | `streamIdleTimeoutMs` | `300,000` | Maximum provider idle time per outstanding stream read |
-| `maxRequestFilesBytes` | `128 MiB` | File-mode request-image byte budget declared to the `dsh-llm-image-offload` plugin's `image/offload` watermark planning |
+| `maxRequestFilesBytes` | `128 MiB` | File-mode request-image byte budget; a request whose retained images exceed it fails with `IMAGE_OFFLOAD_REQUIRED` |
 | `maxInlineRequestImageBytes` | `20 MiB` | Independent base64 fallback high watermark |
 | `maxImagesPerRequest` | `600` | High watermark for retained request-image count |
 | `imageOffloadByteQuantum` | `64 MiB` | Files-mode oldest-prefix removal quantum |
@@ -156,7 +156,7 @@ The selected DeepSeek model receives the harness system prompt, message history,
 
 #### Token effect
 
-Provider tokenization governs exact text and image-token input. The adapter declares per-route `imageRequestPricing`: it prices each occurrence the session's `image/offload` watermark marks offloaded as its placeholder text and each retained image at its projected dimensions with the published v4 vision accounting (14px patch grid, 3:1 downsampling, 384-token cap, worst-case alignment pad). This lets the token meter price image pressure before a request; reported usage remains authoritative. Reasoning passback carries every reasoned turn's chain of thought into later requests, while offloaded images stop costing visual tokens. Each image-capable catalog route also declares its file-mode budget (`maxRequestFilesBytes`, `maxImagesPerRequest`, both quanta, and the request-version byte target) as `imageRequest` on its resolved model info, so the `dsh-llm-image-offload` plugin advances the watermark from logged facts; a request whose retained occurrences still exceed the file-mode or inline-fallback budget at their exact request-version bytes fails with `IMAGE_OFFLOAD_REQUIRED` naming the additional oldest occurrences to offload. Cache-read usage is reported when available. `totalTokens` is the exact `prompt_tokens + completion_tokens` aggregate and is omitted if a supplied `total_tokens` disagrees.
+Provider tokenization governs exact text and image-token input. The adapter declares per-route `imageRequestPricing`: it prices each occurrence the session's `image/offload` watermark marks offloaded as its placeholder text and each retained image at its projected dimensions with the published v4 vision accounting (14px patch grid, 3:1 downsampling, 384-token cap, worst-case alignment pad). This lets the token meter price image pressure before a request; reported usage remains authoritative. Reasoning passback carries every reasoned turn's chain of thought into later requests, while offloaded images stop costing visual tokens. A request whose retained occurrences exceed the file-mode or inline-fallback budget (`maxRequestFilesBytes`, `maxImagesPerRequest`, both quanta) at their exact request-version bytes fails with `IMAGE_OFFLOAD_REQUIRED` naming the additional oldest occurrences to offload, and `dsh-llm-retry` advances the watermark and retries. Cache-read usage is reported when available. `totalTokens` is the exact `prompt_tokens + completion_tokens` aggregate and is omitted if a supplied `total_tokens` disagrees.
 
 #### KV Cache effect
 

+ 2 - 2
packages/llm/llm-deepseek/README.zh.md

@@ -58,7 +58,7 @@ kind: "package-reference"
 | `defaultContextWindow` | `1,000,000` | 无精确值模型的容量回退 |
 | `models` | V4 Flash + V4 Pro + V4 Flash Vision Exp | 供发现消费方查看的建议性目录 |
 | `streamIdleTimeoutMs` | `300,000` | 单次流读取未完成的最大提供方空闲时间 |
-| `maxRequestFilesBytes` | `128 MiB` | 向 `dsh-llm-image-offload` 插件的 `image/offload` 水位规划声明的 file 模式请求图片字节预算 |
+| `maxRequestFilesBytes` | `128 MiB` | file 模式请求图片字节预算,保留图片超过时请求以 `IMAGE_OFFLOAD_REQUIRED` 失败 |
 | `maxInlineRequestImageBytes` | `20 MiB` | 独立的 base64 回退高水位 |
 | `maxImagesPerRequest` | `600` | 保留请求图片数量的高水位 |
 | `imageOffloadByteQuantum` | `64 MiB` | Files 模式最旧前缀移除量子 |
@@ -156,7 +156,7 @@ Files 模式通过 `maxRequestFilesBytes` 与 `maxImagesPerRequest` 限制保留
 
 #### Token 影响
 
-提供方分词决定精确的文本与图片 token 输入。适配器声明按路由的 `imageRequestPricing`:把会话 `image/offload` 水位标记为已省略的每个出现位置按其占位文本计价,并按投影后的尺寸使用官方公布的 v4 视觉计量(14px patch 网格、3:1 降采样、单图 384 token 上限、最坏对齐 pad)为每张保留图片计价。这使 token 计量服务可以在请求发出前为图片压力定价;上报的 usage 仍是权威值。推理回传会把每个推理轮次的思维链带进后续请求,而已省略的图片不再消耗视觉 token。每条支持图片的目录路由还会把 file 模式预算(`maxRequestFilesBytes`、`maxImagesPerRequest`、两个量子与请求版本字节目标)作为 `imageRequest` 声明在其解析后的模型信息上,供 `dsh-llm-image-offload` 插件仅凭已记录事实推进水位;保留的出现位置按精确请求版本字节仍超过 file 模式或内联回退预算的请求,以 `IMAGE_OFFLOAD_REQUIRED` 失败并说明还需省略多少最老的出现位置。可用时报告缓存读取用量。`totalTokens` 是精确的 `prompt_tokens + completion_tokens` 汇总值;提供方给出的 `total_tokens` 不一致时省略该值。
+提供方分词决定精确的文本与图片 token 输入。适配器声明按路由的 `imageRequestPricing`:把会话 `image/offload` 水位标记为已省略的每个出现位置按其占位文本计价,并按投影后的尺寸使用官方公布的 v4 视觉计量(14px patch 网格、3:1 降采样、单图 384 token 上限、最坏对齐 pad)为每张保留图片计价。这使 token 计量服务可以在请求发出前为图片压力定价;上报的 usage 仍是权威值。推理回传会把每个推理轮次的思维链带进后续请求,而已省略的图片不再消耗视觉 token。保留的出现位置按精确请求版本字节超过 file 模式或内联回退预算(`maxRequestFilesBytes`、`maxImagesPerRequest` 与两个量子)的请求,以 `IMAGE_OFFLOAD_REQUIRED` 失败并说明还需省略多少最老的出现位置,由 `dsh-llm-retry` 推进水位并重试。可用时报告缓存读取用量。`totalTokens` 是精确的 `prompt_tokens + completion_tokens` 汇总值;提供方给出的 `total_tokens` 不一致时省略该值。
 
 #### KV Cache 影响
 

+ 2 - 16
packages/llm/llm-deepseek/src/adapter.ts

@@ -19,7 +19,8 @@ import type {
   LlmResolvedModelInfo,
   ModelModality,
   ResolvedRetryPolicy,
-  StreamChunk, LlmImageRequestBudget } from '@deepseek-ai/dsh-llm'
+  StreamChunk,
+} from '@deepseek-ai/dsh-llm'
 import type {
   AttachmentId,
   AttachmentStore,
@@ -212,18 +213,6 @@ function collectImageRefs(
   }
 }
 
-/** The file-mode request-image budget one image-capable catalog route declares to the agent loop. */
-function imageRequestBudget(connection: DeepSeekConnectionOptions, model: DeepSeekCatalogModel): LlmImageRequestBudget {
-  return {
-    representation: 'raw',
-    maxBytes: connection.maxRequestFilesBytes,
-    maxImages: connection.maxImagesPerRequest,
-    byteQuantum: connection.imageOffloadByteQuantum,
-    countQuantum: connection.imageOffloadCountQuantum,
-    versionMaxBytes: resolveRequestImagePolicy(model).maxBytes,
-  }
-}
-
 async function prepareRequestImages(
   options: GenerateOptions,
   attachments: AttachmentStore,
@@ -421,9 +410,6 @@ export class DeepSeekAdapter extends LlmAdapter {
         : modelInfo(provider, configured),
       context: { contextWindow },
       defaultMaxTokens: configured?.maxTokens ?? connection.maxTokens,
-      ...configured?.inputModalities?.includes('image') === true
-        ? { imageRequest: imageRequestBudget(connection, configured) }
-        : {},
       ...connection.defaults.thinking === 'disabled'
         ? {
           reasoning: {

+ 0 - 24
packages/llm/llm-deepseek/tests/adapter.spec.ts

@@ -629,30 +629,6 @@ describe('DeepSeekAdapter against a mock server', () => {
     })
   })
 
-  it('declares the file-mode request-image budget only for image-capable catalog routes', async () => {
-    const adapter = adapterOf({
-      maxRequestFilesBytes: 4096,
-      maxImagesPerRequest: 40,
-      imageOffloadByteQuantum: 1024,
-      imageOffloadCountQuantum: 20,
-      models: [
-        { id: 'vision', inputModalities: ['text', 'image'], imageMaxBytes: 2048 },
-        { id: 'text-only' },
-      ],
-    })
-    await expect(adapter.resolveModel('deepseek-official', 'vision')).resolves.toMatchObject({
-      imageRequest: {
-        representation: 'raw',
-        maxBytes: 4096,
-        maxImages: 40,
-        byteQuantum: 1024,
-        countQuantum: 20,
-        versionMaxBytes: 2048,
-      },
-    })
-    const textOnly = await adapter.resolveModel('deepseek-official', 'text-only')
-    expect(textOnly.imageRequest).toBeUndefined()
-  })
   it('projects nested tool-result images with route-owned request budgets', async () => {
     const server = await mockServer([
       { kind: 'sse', events: textEvents },

+ 1 - 4
packages/llm/llm-deepseek/tests/dynamic-config.spec.ts

@@ -219,10 +219,7 @@ describe('request-level dynamic configuration', () => {
 
     await assemble(ctx, { model: 'deepseek-v4-flash-vision-exp', messages })
     await ctx.settings.update(NS, { maxRequestFilesBytes: 4, imageOffloadByteQuantum: 2 })
-    // The tightened budget reaches the agent loop through the resolved route metadata...
-    await expect(ctx.llm.resolveModelInfo('deepseek-official', 'deepseek-v4-flash-vision-exp'))
-      .resolves.toMatchObject({ imageRequest: { representation: 'raw', maxBytes: 4, byteQuantum: 2 } })
-    // ...and a request whose retained exact bytes still exceed it names the occurrences to offload.
+    // A request whose retained exact bytes exceed the tightened budget names the occurrences to offload.
     const rejected = await assemble(ctx, { model: 'deepseek-v4-flash-vision-exp', messages })
     expect(rejected.finish).toMatchObject({
       kind: 'error',

+ 0 - 6
packages/llm/llm-image-offload/README.i18n.yaml

@@ -1,6 +0,0 @@
-# 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 packages/llm/llm-image-offload/README.md
-README.md: 6f00392b9d5e3462845980a23332f9bd64bbf673
-README.zh.md: 24ecdd57a9096072721cb73affb7c8f587619179

+ 0 - 106
packages/llm/llm-image-offload/README.md

@@ -1,106 +0,0 @@
----
-description: "The image-offload plugin for users and maintainers keeping request images within each route's budget through the session's durable image/offload watermark."
-kind: "package-reference"
----
-
-# @deepseek-ai/dsh-llm-image-offload
-
-English | [中文](README.zh.md)
-
-## Summary
-
-`@deepseek-ai/dsh-llm-image-offload` keeps request images within each route's budget by advancing the session's durable `image/offload` watermark. Before a step enters, it plans the watermark from the request-image budget the routed model declares and the image occurrences the surface still sends; when an adapter fails an attempt with `IMAGE_OFFLOAD_REQUIRED`, it advances the watermark by the count the failure names and retries the step. The watermark only advances, so the images the model sees and the provider cache prefix move only forward, and every dispatched request's image set is determined by the session log alone.
-
-## Table of Contents
-
-- [Use this package](#use-this-package)
-- [Understand the implementation](#understand-the-implementation)
-- [Further Exploration](#further-exploration)
-- [Model Experience](#model-experience)
-- [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
-- [Dev Note](#dev-note)
-
------
-
-<a id="use-this-package"></a>
-## Use this package
-
-Mount this plugin in every composition that runs the agent loop with an image-capable route. Without it, an image-capable adapter whose budget is exceeded fails the step with `IMAGE_OFFLOAD_REQUIRED` and no recovery advances the watermark. The plugin has no configuration: the DeepSeek adapter declares its file-mode budget and the pi-ai adapter its base64 bound as `imageRequest` on their resolved model info.
-
-### Minimal configuration
-
-```yaml
-- name: '@deepseek-ai/dsh-llm-image-offload'
-```
-
-### What you can observe
-
-Each advance is one durable `image/offload` event carrying the turn, the step, and the watermark: the seq of the event carrying the last offloaded occurrence and the block path inside its content. A pre-step advance lands after `turn/start` and before `step/start`; a failure-driven advance lands after the `assistant/attempt` it answers and before the retried request. `session.imageOffloadWatermark()` reads the watermark in force, and every derived image block at or before it carries `offloaded: true`, which each route renders as placeholder text naming the image and its read-only path.
-
-### Failures and recovery
-
-The pre-step plan reads the route the latest `request/header` names and covers the surface as it stands before the step's own messages are appended; it skips before the first header, for a route without an adapter, and for a route that declares no budget. Occurrences the plan cannot see, and occurrences whose exact request bytes exceed the budget only at serialization, reach the model through one failed attempt: the adapter fails with `IMAGE_OFFLOAD_REQUIRED` and `offloadImages`, the plugin advances by that count on the `agent/request-error` waterfall and returns the `retry` action. When nothing remains to offload the plugin delegates, so the failure reaches downstream recovery and otherwise ends the turn.
-
------
-
-<a id="understand-the-implementation"></a>
-## Understand the implementation
-
-<details>
-<summary>Implementation internals — click to expand</summary>
-
-The plugin is one function plugin with two listeners. On `agent/pre-step` it awaits the downstream decision, and for an `enter` decision resolves the routed budget through `ctx.llm.resolveModelInfo()`, walks the surface nodes through `Session.deriveEventMessage()` collecting every retained occurrence in request order with its durable position, and plans the removal prefix with the pure `offloadedImagePrefixCount()` over the occurrences' represented bytes. On `agent/request-error` it handles only `IMAGE_OFFLOAD_REQUIRED` failures carrying `offloadImages`. Both paths append one `image/offload` event naming the greatest durable position in the request-order prefix, which a surface replacement can place before older events; the advance may offload additional occurrences but always covers the prefix. Adapters never append the event: the session surface has one owner of its offloaded set.
-
-No invariant companion is published: `Session.append` and seeding already reject a malformed, non-advancing, or off-surface watermark, so no independent observation can diverge from the event the plugin appends.
-
-</details>
-
------
-
-<a id="further-exploration"></a>
-## Further Exploration
-
-- [Durable image offload watermark](../../../.agents/notes/implemented/architecture/2026-09-02-image-offload-watermark.md) — the decision this plugin implements and the alternatives it replaced.
-- [dsh-session](../../core/session/README.md) — the `image/offload` event, its validation, and the marked derivation.
-- [dsh-llm](../llm/README.md) — `LlmImageRequestBudget`, `IMAGE_OFFLOAD_REQUIRED`, and the shared image walk.
-- [llm-deepseek adapter](../llm-deepseek/README.md) — the file-mode budget and inline fallback that report offload counts.
-- [llm-pi-ai adapter](../llm-pi-ai/README.md) — the base64 bound that reports offload counts.
-
------
-
-<a id="model-experience"></a>
-## Model Experience
-
-### Request-image offload
-
-#### What the model sees
-
-Every image occurrence at or before the watermark reaches the model as the route's placeholder text (`offloadedImageText`) naming the attachment and its read-only path instead of the image; occurrences after it stay images. The set never shrinks on its own, so the model can rely on an offloaded image staying offloaded and read it back through the path when it needs the content again.
-
-#### Token effect
-
-An offloaded occurrence costs its placeholder text instead of visual tokens. The token meter prices the current surface under the current watermark and each usage anchor under the watermark its request was derived with.
-
-#### KV Cache effect
-
-An advance replaces earlier images with placeholder text, so provider cache reuse ends at the first replaced message for that request. Because the watermark never retreats, the prefix stays stable afterwards.
-
-## Known Limitations and Deferred Work
-
-<a id="known-limitations-and-deferred-work"></a>
-
-- **Offloaded images never return automatically** — a larger budget, a larger route, or compaction lowering the total leaves the watermark in place; recovery is the read-only path in the placeholder.
-- **The pre-step plan is one step behind the entering messages** — images a user message or the step's own tool results add reach the model through one failed attempt when they push the retained set past the budget.
-- **A temporary small budget advances permanently** — a Files outage that forces the inline fallback, or a temporary switch to a small-budget route, offloads images that a later route would have sent.
-
-<a id="dev-note"></a>
-### Dev Note
-
-<details>
-<summary>Working context for maintainers — click to expand</summary>
-
-This Dev Note is non-authoritative working context: notes for maintainers and open questions. Shipped behavior and accepted rationale live in the sections above, the package code, and the linked Agent Notes.
-
-- The pre-step plan walks every surface node on each step of a route with a budget; nothing caches the retained set. Revisit only with profiling evidence from long image-heavy sessions.
-
-</details>

+ 0 - 106
packages/llm/llm-image-offload/README.zh.md

@@ -1,106 +0,0 @@
----
-description: "面向用户与维护者的图片省略插件说明:通过会话的持久 image/offload 水位,把请求图片控制在各路由预算之内。"
-kind: "package-reference"
----
-
-# @deepseek-ai/dsh-llm-image-offload
-
-[English](README.md) | 中文
-
-## 概述
-
-`@deepseek-ai/dsh-llm-image-offload` 通过推进会话的持久 `image/offload` 水位,把请求图片控制在各路由的预算之内。step 进入前,它根据路由模型声明的请求图片预算和表层仍在发送的图片出现位置规划水位;adapter 以 `IMAGE_OFFLOAD_REQUIRED` 让一次尝试失败时,它按失败中给出的数量推进水位并重试该 step。水位只会前进,因此模型看到的图片和 provider 缓存前缀只向前移动,每个已发出请求的图片集合都仅由会话日志决定。
-
-## 目录
-
-- [使用本包](#use-this-package)
-- [理解实现](#understand-the-implementation)
-- [进一步探索](#further-exploration)
-- [模型体验](#model-experience)
-- [已知限制与延期工作](#known-limitations-and-deferred-work)
-- [开发备注](#dev-note)
-
------
-
-<a id="use-this-package"></a>
-## 使用本包
-
-凡是运行 agent loop 且带有支持图片路由的组合,都应挂载本插件。没有它,支持图片的 adapter 在预算超出时会以 `IMAGE_OFFLOAD_REQUIRED` 让 step 失败,且没有任何恢复路径会推进水位。本插件没有配置:DeepSeek adapter 在其解析后的模型信息上以 `imageRequest` 声明 file 模式预算,pi-ai adapter 声明其 base64 上限。
-
-### 最小配置
-
-```yaml
-- name: '@deepseek-ai/dsh-llm-image-offload'
-```
-
-### 你可以观察到什么
-
-每次推进都是一个持久的 `image/offload` 事件,携带轮次、step 和水位:承载最后一个被省略出现位置的事件序号,以及它在内容中的块路径。step 前的推进落在 `turn/start` 之后、`step/start` 之前;由失败驱动的推进落在它所回应的 `assistant/attempt` 之后、重试请求之前。`session.imageOffloadWatermark()` 读取当前生效的水位,位于水位及之前的每个派生图片块都带有 `offloaded: true`,每条路由把它渲染为注明图片及其只读路径的占位文本。
-
-### 失败与恢复
-
-step 前的规划读取最新 `request/header` 所指的路由,覆盖的是 step 自己的消息追加之前的表层;第一个 header 之前、路由没有 adapter、路由未声明预算时都会跳过。规划看不到的出现位置,以及只有在序列化时才发现精确请求字节超出预算的出现位置,会经过一次失败的尝试到达模型:adapter 以 `IMAGE_OFFLOAD_REQUIRED` 和 `offloadImages` 失败,插件在 `agent/request-error` waterfall 上按该数量推进并返回 `retry` 动作。没有可省略的出现位置时插件向下游委托,失败因此进入下游恢复,否则结束该轮次。
-
------
-
-<a id="understand-the-implementation"></a>
-## 理解实现
-
-<details>
-<summary>实现内部——点击展开</summary>
-
-本插件是带两个监听器的函数插件。在 `agent/pre-step` 上它先等待下游决定,对 `enter` 决定通过 `ctx.llm.resolveModelInfo()` 解析路由预算,经 `Session.deriveEventMessage()` 遍历表层节点,按请求顺序收集每个保留的出现位置及其持久位置,再用纯函数 `offloadedImagePrefixCount()` 对出现位置的表示字节规划删除前缀。在 `agent/request-error` 上它只处理带 `offloadImages` 的 `IMAGE_OFFLOAD_REQUIRED` 失败。两条路径都追加一个 `image/offload` 事件,指向请求序前缀中最大的持久位置;表层替换可能把该位置放在较老事件之前,推进因此可能额外省略一些出现位置,但一定覆盖该前缀。adapter 从不追加该事件:会话表层的已省略集合只有一个所有者。
-
-本包不发布 invariant 伴生插件:`Session.append` 与 seed 已经拒绝畸形、未前进或不在表层上的水位,没有任何独立观察能与插件追加的事件产生分歧。
-
-</details>
-
------
-
-<a id="further-exploration"></a>
-## 进一步探索
-
-- [持久图片 offload 水位](../../../.agents/notes/implemented/architecture/2026-09-02-image-offload-watermark.zh.md)——本插件实现的决定及其替代的方案。
-- [dsh-session](../../core/session/README.zh.md)——`image/offload` 事件、它的校验与带标记的派生。
-- [dsh-llm](../llm/README.zh.md)——`LlmImageRequestBudget`、`IMAGE_OFFLOAD_REQUIRED` 与共享的图片遍历。
-- [llm-deepseek 适配器](../llm-deepseek/README.zh.md)——上报省略数量的 file 模式预算与内联回退。
-- [llm-pi-ai 适配器](../llm-pi-ai/README.zh.md)——上报省略数量的 base64 上限。
-
------
-
-<a id="model-experience"></a>
-## 模型体验
-
-### 请求图片省略
-
-#### 模型看到什么
-
-位于水位及之前的每个图片出现位置,以路由的占位文本(`offloadedImageText`)到达模型,文本注明附件及其只读路径而不是图片本身;水位之后的出现位置仍是图片。该集合不会自行缩小,模型可以确信被省略的图片会一直被省略,再需要内容时通过路径读回。
-
-#### Token 影响
-
-被省略的出现位置只花费占位文本,不再花费视觉 token。token meter 按当前水位为当前表层定价,按每个 usage 锚点的请求派生时的水位为该锚点定价。
-
-#### KV Cache 影响
-
-一次推进把较早的图片替换为占位文本,该请求的 provider 缓存复用因此止于第一条被替换的消息。水位永不回退,之后的前缀保持稳定。
-
-## 已知限制与延期工作
-
-<a id="known-limitations-and-deferred-work"></a>
-
-- **被省略的图片不会自动回归**——更大的预算、更大的路由或 compaction 降低总量都不会移动水位;恢复手段是占位文本中的只读路径。
-- **step 前的规划比进入的消息晚一步**——用户消息或 step 自己的工具结果新增的图片把保留集合推过预算时,会经过一次失败的尝试才到达模型。
-- **临时的小预算会永久推进**——迫使内联回退的 Files 故障,或临时切到小预算路由,会省略后来的路由本可以发送的图片。
-
-<a id="dev-note"></a>
-### 开发备注
-
-<details>
-<summary>维护者工作背景——点击展开</summary>
-
-本开发备注是非权威的工作背景:给维护者的备注和未决问题。已交付的行为与被接受的理由见上文各节、包代码和链接的 Agent Note。
-
-- 对声明了预算的路由,step 前的规划每一步都遍历全部表层节点,没有缓存保留集合。只有在图片密集的长会话中拿到 profiling 证据后再考虑优化。
-
-</details>

+ 0 - 46
packages/llm/llm-image-offload/package.json

@@ -1,46 +0,0 @@
-{
-  "name": "@deepseek-ai/dsh-llm-image-offload",
-  "description": "Durable request-image offload for the DeepSeek Harness agent loop",
-  "version": "0.1.3-alpha.1",
-  "publishConfig": {
-    "access": "public"
-  },
-  "repository": {
-    "type": "git",
-    "url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
-    "directory": "packages/llm/llm-image-offload"
-  },
-  "type": "module",
-  "main": "lib/index.js",
-  "types": "lib/types/index.d.ts",
-  "exports": {
-    ".": {
-      "types": "./lib/types/index.d.ts",
-      "default": "./lib/index.js"
-    },
-    "./package.json": "./package.json"
-  },
-  "files": [
-    "lib/index.js",
-    "lib/types/**/*.d.ts"
-  ],
-  "license": "MIT",
-  "peerDependencies": {
-    "@deepseek-ai/dsh-agent": "workspace:^",
-    "@deepseek-ai/dsh-llm": "workspace:^",
-    "@deepseek-ai/dsh-session": "workspace:^",
-    "@deepseek-ai/cordis": "workspace:^"
-  },
-  "dependencies": {
-    "@deepseek-ai/schemastery": "workspace:^"
-  },
-  "devDependencies": {
-    "@deepseek-ai/dsh-agent": "workspace:^",
-    "@deepseek-ai/dsh-agent-loop": "workspace:^",
-    "@deepseek-ai/dsh-agent-loop-testkit": "workspace:^",
-    "@deepseek-ai/dsh-llm": "workspace:^",
-    "@deepseek-ai/dsh-session": "workspace:^",
-    "@deepseek-ai/dsh-session-projection": "workspace:^",
-    "@deepseek-ai/cordis": "workspace:^"
-  }
-}

+ 0 - 112
packages/llm/llm-image-offload/src/index.ts

@@ -1,112 +0,0 @@
-/**
- * Durable request-image offload on the agent loop's step extension points.
- * Before a step enters, the plugin plans the session's `image/offload`
- * watermark from the request-image budget of the route the latest
- * `request/header` names; when an adapter fails an attempt with
- * `IMAGE_OFFLOAD_REQUIRED`, it advances the watermark by the named count and
- * retries the step, so every dispatched request carries the offloaded set the
- * log records.
- *
- * @module @deepseek-ai/dsh-llm-image-offload
- */
-
-import type { Context } from '@deepseek-ai/cordis'
-import z from '@deepseek-ai/schemastery'
-import type { PreStepDecision, RequestErrorAction } from '@deepseek-ai/dsh-agent'
-import { IMAGE_OFFLOAD_REQUIRED_CODE, LlmError, offloadedImagePrefixCount, representedImageBytes, visitImageBlocks } from '@deepseek-ai/dsh-llm'
-import type { LlmImageRequestBudget } from '@deepseek-ai/dsh-llm'
-import { compareImagePositions } from '@deepseek-ai/dsh-session'
-import type { ImageOccurrencePosition, Session } from '@deepseek-ai/dsh-session'
-
-export const name = 'llm-image-offload'
-export const inject = ['llm', 'agents']
-
-/** The plugin has no configuration; image-capable routes declare their budgets. */
-export type Config = Readonly<Record<string, never>>
-
-/** Runtime schema for {@link Config}. */
-export const Config = z.object({}) as unknown as z<Config>
-
-/** One image occurrence the surface still sends. */
-interface RetainedOccurrence {
-  /** Durable position an advance names when this occurrence becomes the last offloaded one. */
-  readonly position: ImageOccurrencePosition
-  /** Normalized attachment byte count. */
-  readonly bytes: number
-}
-
-/** Every retained image occurrence in model request order. */
-function retainedOccurrences(session: Session): RetainedOccurrence[] {
-  const watermark = session.imageOffloadWatermark()
-  const retained: RetainedOccurrence[] = []
-  for (const seq of session.surface.nodes) {
-    // oxlint-disable-next-line typescript/no-non-null-assertion -- surface nodes index the durable log
-    const message = session.deriveEventMessage(session.eventAt(seq)!)
-    if (message === null) continue
-    visitImageBlocks(message.content, (block, path) => {
-      const position = { seq, path: [...path] }
-      if (watermark !== undefined && compareImagePositions(position, watermark) <= 0) return
-      retained.push({ position, bytes: block.attachment.bytes })
-    })
-  }
-  return retained
-}
-
-/**
- * Advance the watermark past the `count` oldest retained occurrences. A
- * surface replacement can place a newer event before older ones in request
- * order, so the advance names the greatest durable position in that prefix,
- * which may offload additional occurrences but always covers the prefix.
- * @returns whether any occurrence remained to offload.
- */
-function advance(session: Session, turn: number, step: number, count: number): boolean {
-  let watermark: ImageOccurrencePosition | undefined
-  for (const occurrence of retainedOccurrences(session).slice(0, count)) {
-    if (watermark === undefined || compareImagePositions(occurrence.position, watermark) > 0) {
-      watermark = occurrence.position
-    }
-  }
-  if (watermark === undefined) return false
-  session.append('image/offload', { turn, step, watermark })
-  return true
-}
-
-/** Request-image budget of the route the latest request header names; undefined before the first header or for a route without one. */
-async function routedBudget(ctx: Context, session: Session, signal: AbortSignal): Promise<LlmImageRequestBudget | undefined> {
-  const config = session.requestHeader()?.config
-  if (config === undefined) return undefined
-  try {
-    return (await ctx.llm.resolveModelInfo(config.provider, config.model, signal)).imageRequest
-  } catch (error: unknown) {
-    // Middleware may serve a route without an adapter; the loop dispatches it the same way.
-    if (error instanceof LlmError && error.code === 'NO_ADAPTER') return undefined
-    throw error
-  }
-}
-
-/**
- * Mount the offload listeners.
- * @param ctx - the plugin context.
- * @param _config - no configuration.
- */
-export function apply(ctx: Context, _config: Config = {}): void {
-  ctx.on('agent/pre-step', async ({ agent, turn, step, signal }, next): Promise<PreStepDecision> => {
-    const decision = await next()
-    if (decision.kind !== 'enter') return decision
-    const budget = await routedBudget(ctx, agent.session, signal)
-    if (budget === undefined || signal.aborted) return decision
-    const retained = retainedOccurrences(agent.session)
-    const count = offloadedImagePrefixCount(
-      retained.map(occurrence => representedImageBytes(occurrence.bytes, budget)),
-      budget,
-    )
-    if (count > 0) advance(agent.session, turn, step, count)
-    return decision
-  })
-
-  ctx.on('agent/request-error', ({ agent, turn, step, failure }, next): Promise<RequestErrorAction> => {
-    if (failure.code !== IMAGE_OFFLOAD_REQUIRED_CODE || failure.offloadImages === undefined) return next()
-    if (!advance(agent.session, turn, step, failure.offloadImages)) return next()
-    return Promise.resolve<RequestErrorAction>({ kind: 'retry' })
-  })
-}

+ 0 - 30
packages/llm/llm-image-offload/tsconfig.json

@@ -1,30 +0,0 @@
-{
-  "extends": "../../../tsconfig.base.json",
-  "compilerOptions": {
-    "rootDir": "src",
-    "outDir": "lib/types"
-  },
-  "include": [
-    "src"
-  ],
-  "references": [
-    {
-      "path": "../../../vendor/cosmokit"
-    },
-    {
-      "path": "../../../vendor/cordis"
-    },
-    {
-      "path": "../../../vendor/schemastery"
-    },
-    {
-      "path": "../../llm/llm"
-    },
-    {
-      "path": "../../core/session"
-    },
-    {
-      "path": "../../core/agent"
-    }
-  ]
-}

+ 0 - 13
packages/llm/llm-image-offload/tsdown.config.ts

@@ -1,13 +0,0 @@
-import { defineConfig } from 'tsdown'
-
-/** Bundle the package root from its emitted declarations build. */
-export default defineConfig({
-  entry: ['lib/types/index.js'],
-  outDir: 'lib',
-  format: ['esm'],
-  platform: 'node',
-  target: 'es2024',
-  fixedExtension: false,
-  dts: false,
-  clean: false,
-})

+ 2 - 2
packages/llm/llm-pi-ai/README.i18n.yaml

@@ -2,5 +2,5 @@
 # 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 packages/llm/llm-pi-ai/README.md
-README.md: 08a5d9a220af2ec3a0e929926c74cc6e6c9b7499
-README.zh.md: c6eb57bf02538b07edb571a1b6c69029f18cbf24
+README.md: 56d68aa62fbb8497d4f6e34ded79d1ad27f41985
+README.zh.md: ff6734e57a4e2e725b4d45fee5b24aa01221cb21

+ 2 - 2
packages/llm/llm-pi-ai/README.md

@@ -83,7 +83,7 @@ Each profile may set a `retryPolicy`; omission uses normal mode with five retrie
 | `defaultMaxTokens` | `32,768` | Output-cap fallback for undescribed models |
 | `requestImagePixelBudget` | `4,194,304` | Total-pixel budget for each deterministic request image |
 | `requestImageMaxBytes` | `1 MiB` | Encoded-byte target for each request image before base64 expansion |
-| `maxRequestImageBytes` | `20 MiB` | Aggregate base64 image-payload bound declared to the `dsh-llm-image-offload` plugin's `image/offload` watermark planning |
+| `maxRequestImageBytes` | `20 MiB` | Aggregate base64 image-payload bound; a request whose retained images exceed it fails with `IMAGE_OFFLOAD_REQUIRED` |
 | `retryPolicy` | normal, 5 retries | Provider-owned retry policy executed by `dsh-llm-retry` |
 
 The generated [configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-llm-pi-ai) is the exhaustive source for every accepted field and its JSDoc.
@@ -176,7 +176,7 @@ Read these pages when the package-level contract is not enough. They move from t
 
 #### What the model sees
 
-The selected catalog model receives `GenerateOptions.system`, history, tools, and sampling fields supported by pi-ai's common streaming API. Each retained image is preceded by text naming its complete attachment id and actual request dimensions. When the current execution filesystem maps the attachment provider's host object, the text also carries a read-only normalized-object path and warns that normalization or request projection may have resized or re-encoded the upload. Each occurrence the session's `image/offload` watermark marks offloaded keeps its own identity and currently resolved access in replacement text, and its normalized attachment is not read or transformed. When the retained occurrences' exact base64 payload still exceeds the route's `maxRequestImageBytes`, the call fails with `IMAGE_OFFLOAD_REQUIRED` so the `dsh-llm-image-offload` plugin advances the watermark and retries the step. Provider-native replay metadata is restored only when the adapter validates it for the historical content.
+The selected catalog model receives `GenerateOptions.system`, history, tools, and sampling fields supported by pi-ai's common streaming API. Each retained image is preceded by text naming its complete attachment id and actual request dimensions. When the current execution filesystem maps the attachment provider's host object, the text also carries a read-only normalized-object path and warns that normalization or request projection may have resized or re-encoded the upload. Each occurrence the session's `image/offload` watermark marks offloaded keeps its own identity and currently resolved access in replacement text, and its normalized attachment is not read or transformed. When the retained occurrences' exact base64 payload still exceeds the route's `maxRequestImageBytes`, the call fails with `IMAGE_OFFLOAD_REQUIRED` so `dsh-llm-retry` advances the watermark and retries the step. Provider-native replay metadata is restored only when the adapter validates it for the historical content.
 
 #### Token effect
 

+ 2 - 2
packages/llm/llm-pi-ai/README.zh.md

@@ -83,7 +83,7 @@ kind: "package-reference"
 | `defaultMaxTokens` | `32,768` | 未描述模型的输出上限回退 |
 | `requestImagePixelBudget` | `4,194,304` | 每张确定性请求图片的总像素预算 |
 | `requestImageMaxBytes` | `1 MiB` | 每张请求图片在 base64 扩展前的编码字节目标 |
-| `maxRequestImageBytes` | `20 MiB` | 向 `dsh-llm-image-offload` 插件的 `image/offload` 水位规划声明的 base64 图片载荷总上限 |
+| `maxRequestImageBytes` | `20 MiB` | base64 图片载荷总上限,保留图片超过时请求以 `IMAGE_OFFLOAD_REQUIRED` 失败 |
 | `retryPolicy` | normal,5 次重试 | 由 `dsh-llm-retry` 执行的提供方自有重试策略 |
 
 生成的[配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-llm-pi-ai)是每个受支持字段及其 JSDoc 的穷尽式真源。
@@ -176,7 +176,7 @@ pi-ai 不提供的路由需要 `api`、`baseURL` 与非空 `models` 列表;无
 
 #### 模型看到什么
 
-所选目录模型会收到 `GenerateOptions.system`、历史、工具与 pi-ai 通用流式 API 支持的采样字段。每张保留图片前都会有文本,注明其完整附件 id 与实际请求尺寸。当前执行文件系统可以映射附件提供方的宿主对象时,该文本还会携带只读规范化对象路径,并警告规范化或请求投影可能缩放或重新编码上传内容。会话 `image/offload` 水位标记为已卸载的每个出现位置都会在替换文本中保留自己的身份与当前已解析访问方式,其规范化附件不会读取或变换。当保留的出现位置按精确 base64 载荷仍超过路由的 `maxRequestImageBytes` 时,调用以 `IMAGE_OFFLOAD_REQUIRED` 失败,由 `dsh-llm-image-offload` 插件推进水位并重试 step。提供方原生回放元数据只在适配器针对历史内容校验通过后恢复。
+所选目录模型会收到 `GenerateOptions.system`、历史、工具与 pi-ai 通用流式 API 支持的采样字段。每张保留图片前都会有文本,注明其完整附件 id 与实际请求尺寸。当前执行文件系统可以映射附件提供方的宿主对象时,该文本还会携带只读规范化对象路径,并警告规范化或请求投影可能缩放或重新编码上传内容。会话 `image/offload` 水位标记为已卸载的每个出现位置都会在替换文本中保留自己的身份与当前已解析访问方式,其规范化附件不会读取或变换。当保留的出现位置按精确 base64 载荷仍超过路由的 `maxRequestImageBytes` 时,调用以 `IMAGE_OFFLOAD_REQUIRED` 失败,由 `dsh-llm-retry` 推进水位并重试 step。提供方原生回放元数据只在适配器针对历史内容校验通过后恢复。
 
 #### Token 影响
 

+ 0 - 9
packages/llm/llm-pi-ai/src/adapter.ts

@@ -306,15 +306,6 @@ export class PiAiAdapter extends LlmAdapter {
       inputModalities: [...resolvedModel.input],
       context: { contextWindow: resolvedModel.contextWindow },
       ...configuredMaxTokens === undefined ? {} : { defaultMaxTokens: configuredMaxTokens },
-      ...resolvedModel.input.includes('image')
-        ? {
-          imageRequest: {
-            representation: 'base64' as const,
-            maxBytes: profile.maxRequestImageBytes,
-            versionMaxBytes: profile.requestImageMaxBytes,
-          },
-        }
-        : {},
       ...reasoningInfo(resolvedModel, defaultLevel),
     }
   }

+ 2 - 2
packages/llm/llm-retry/README.i18n.yaml

@@ -2,5 +2,5 @@
 # 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 packages/llm/llm-retry/README.md
-README.md: 0ea815130212a165582540e43aad7d59fad32d2c
-README.zh.md: 43a0187c68d272e2764d34db35fa26e51a9d3a83
+README.md: e5c97f602b4db1450ae1e2566e34b40fc6c385fd
+README.zh.md: 2c3c310c9cc86e439f57de8c2f3068cc26176312

+ 5 - 4
packages/llm/llm-retry/README.md

@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
 
 ## Summary
 
-`@deepseek-ai/dsh-llm-retry` is the retry executor for failed model requests: it applies each provider's resolved retry policy at the agent loop's open-step `agent/request-error` extension point, so every retry re-runs the same step inside the same open turn over the same durable history. It does not wrap the streaming call itself — every adapter call remains one provider attempt, and direct `ctx.llm.stream()` consumers stay single-attempt. Retry scheduling is durable: the plugin appends `llm/retry` events to the session log before waiting, and cancellation during backoff leaves the log consistent. Normal mode retries a bounded set of failure codes up to `maxRetries` with exponential backoff; always mode asks downstream recovery first, then retries every failure without an attempt limit.
+`@deepseek-ai/dsh-llm-retry` is the retry executor for failed model requests: it applies each provider's resolved retry policy at the agent loop's open-step `agent/request-error` extension point, so every retry re-runs the same step inside the same open turn over the same durable history. It does not wrap the streaming call itself — every adapter call remains one provider attempt, and direct `ctx.llm.stream()` consumers stay single-attempt. Retry scheduling is durable: the plugin appends `llm/retry` events to the session log before waiting, and cancellation during backoff leaves the log consistent. Normal mode retries a bounded set of failure codes up to `maxRetries` with exponential backoff; always mode asks downstream recovery first, then retries every failure without an attempt limit. It also owns one durable repair: an adapter's `IMAGE_OFFLOAD_REQUIRED` failure advances the session's `image/offload` watermark by the count the failure names and retries the step, outside any provider retry budget.
 
 ## Table of Contents
 
@@ -55,7 +55,7 @@ Each scheduled retry is durable before its wait: the plugin appends a non-surfac
 
 ### Failures and recovery
 
-A failure before any final adapter is selected has no provider policy and delegates downstream unchanged. In normal mode, a failure code outside the eligible set, or an exhausted budget, delegates; in always mode, an over-cap provider delay uses the configured local backoff so the policy cannot terminate on that instruction. Nothing here is model-visible: no retry event, delay, provider error, or failed partial output reaches the model or derived messages.
+A failure before any final adapter is selected has no provider policy and delegates downstream unchanged. In normal mode, a failure code outside the eligible set, or an exhausted budget, delegates; in always mode, an over-cap provider delay uses the configured local backoff so the policy cannot terminate on that instruction. Nothing here is model-visible: no retry event, delay, provider error, or failed partial output reaches the model or derived messages. An `IMAGE_OFFLOAD_REQUIRED` failure carrying `offloadImages` is handled before any policy: the plugin appends one `image/offload` event naming the greatest durable position among that many oldest retained image occurrences in request order and returns `retry`; when nothing remains to offload it delegates. That retry spends no retry budget and logs no `llm/retry` event, and the retried request sends placeholder text for the offloaded images ([decision](../../../.agents/notes/implemented/architecture/2026-09-02-image-offload-watermark.md)).
 
 -----
 
@@ -77,12 +77,13 @@ The executor is built on one rule: **durable before wait, open-step boundaries.*
 |---|---|
 | [`src/index.ts`](src/index.ts) | The function plugin: waterfall listener, policy lookup, backoff, durable event appends |
 | [`src/history.ts`](src/history.ts) | Durable retry-history lookup from the session log |
+| [`src/image-offload.ts`](src/image-offload.ts) | The `image/offload` advance for `IMAGE_OFFLOAD_REQUIRED` failures |
 | [`src/types.ts`](src/types.ts) | Browser-safe `llm/retry` and `llm/retry-started` event payload types |
 | [`src/brand.ts`](src/brand.ts) | The `RetryId` brand shared by the event payloads |
 
 ### Recovery flow
 
-A failed step arrives on the waterfall with its provider and resolved policy. Always mode settles downstream recovery first and honors a downstream `retry` decision; normal mode first checks that the failure code is eligible and the budget is not exhausted. The plugin computes the delay — provider `Retry-After` when valid and within bounds, otherwise local bounded exponential backoff with symmetric jitter — appends the `llm/retry` event, waits on a cancellable timer, appends `llm/retry-started`, and returns `{ kind: 'retry' }`. The loop then re-runs the failed step inside the same open turn over the same durable history.
+A failed step arrives on the waterfall with its provider and resolved policy. An `IMAGE_OFFLOAD_REQUIRED` failure naming `offloadImages` is repaired at once: the watermark advances and the step retries with no delay. Always mode settles downstream recovery first and honors a downstream `retry` decision; normal mode first checks that the failure code is eligible and the budget is not exhausted. The plugin computes the delay — provider `Retry-After` when valid and within bounds, otherwise local bounded exponential backoff with symmetric jitter — appends the `llm/retry` event, waits on a cancellable timer, appends `llm/retry-started`, and returns `{ kind: 'retry' }`. The loop then re-runs the failed step inside the same open turn over the same durable history.
 
 ### Waterfall composition
 
@@ -112,7 +113,7 @@ Read these pages when the package-level contract is not enough. They move from t
 
 #### What the model sees
 
-No retry event, delay, provider error, or failed partial output is model-visible. The retried step reconstructs the same explicit provider/model request from durable surface history unless a downstream recovery policy deliberately changes that surface; failed chunks never enter derived messages.
+No retry event, delay, provider error, or failed partial output is model-visible. The retried step reconstructs the same explicit provider/model request from durable surface history unless a downstream recovery policy deliberately changes that surface; failed chunks never enter derived messages. An `IMAGE_OFFLOAD_REQUIRED` repair is the one exception: the retried request replaces the offloaded images with placeholder text naming each image and its read-only path.
 
 #### Token effect
 

+ 5 - 4
packages/llm/llm-retry/README.zh.md

@@ -9,7 +9,7 @@ kind: "package-reference"
 
 ## 概述
 
-`@deepseek-ai/dsh-llm-retry` 是失败模型请求的重试执行器:它在 agent loop 的打开步骤 `agent/request-error` 扩展点上应用各提供方解析后的重试策略,因此每次重试都会在同一个打开的轮次内重跑同一个步骤(基于同一份持久历史)。它不包装流式调用本身——每次适配器调用仍是一次提供方尝试,直接 `ctx.llm.stream()` 消费方仍是单次尝试。重试调度是持久的:插件在等待之前就把 `llm/retry` 事件追加进会话日志,退避期间取消会让日志保持一致。normal mode 以指数退避重试一组有界的失败 code,最多 `maxRetries` 次;always mode 先询问下游恢复,然后无尝试上限地重试每个失败。
+`@deepseek-ai/dsh-llm-retry` 是失败模型请求的重试执行器:它在 agent loop 的打开步骤 `agent/request-error` 扩展点上应用各提供方解析后的重试策略,因此每次重试都会在同一个打开的轮次内重跑同一个步骤(基于同一份持久历史)。它不包装流式调用本身——每次适配器调用仍是一次提供方尝试,直接 `ctx.llm.stream()` 消费方仍是单次尝试。重试调度是持久的:插件在等待之前就把 `llm/retry` 事件追加进会话日志,退避期间取消会让日志保持一致。normal mode 以指数退避重试一组有界的失败 code,最多 `maxRetries` 次;always mode 先询问下游恢复,然后无尝试上限地重试每个失败。它还负责一种持久修复:adapter 以 `IMAGE_OFFLOAD_REQUIRED` 失败时,按失败给出的数量推进会话的 `image/offload` 水位并重试该步骤,不占任何提供方重试预算。
 
 ## 目录
 
@@ -55,7 +55,7 @@ kind: "package-reference"
 
 ### 失败与恢复
 
-在任何最终适配器被选中之前发生的失败没有提供方策略,原样委派下游。normal mode 中,不在合格集合内的失败 code 或已耗尽的预算会委派;always mode 中,超上限的提供方延迟使用配置的本地退避,因此策略不会因该指令终止。这里没有任何模型可见内容:重试事件、延迟、提供方错误或失败的部分输出都不会到达模型或派生消息。
+在任何最终适配器被选中之前发生的失败没有提供方策略,原样委派下游。normal mode 中,不在合格集合内的失败 code 或已耗尽的预算会委派;always mode 中,超上限的提供方延迟使用配置的本地退避,因此策略不会因该指令终止。这里没有任何模型可见内容:重试事件、延迟、提供方错误或失败的部分输出都不会到达模型或派生消息。带 `offloadImages` 的 `IMAGE_OFFLOAD_REQUIRED` 失败在任何策略之前处理:插件追加一个 `image/offload` 事件,指向按请求顺序最老的那些保留图片中最大的持久位置,然后返回 `retry`;没有可省略的图片时委派下游。这次重试不占重试预算,也不追加 `llm/retry` 事件,重试的请求为被省略的图片发送占位文本([决定](../../../.agents/notes/implemented/architecture/2026-09-02-image-offload-watermark.zh.md))。
 
 -----
 
@@ -77,12 +77,13 @@ kind: "package-reference"
 |---|---|
 | [`src/index.ts`](src/index.ts) | 函数插件:waterfall 监听器、策略查找、退避、持久事件追加 |
 | [`src/history.ts`](src/history.ts) | 从会话日志查找持久重试历史 |
+| [`src/image-offload.ts`](src/image-offload.ts) | 针对 `IMAGE_OFFLOAD_REQUIRED` 失败的 `image/offload` 推进 |
 | [`src/types.ts`](src/types.ts) | 浏览器安全的 `llm/retry` 与 `llm/retry-started` 事件载荷类型 |
 | [`src/brand.ts`](src/brand.ts) | 事件载荷共享的 `RetryId` 品牌 |
 
 ### 恢复流程
 
-失败步骤连同其提供方与解析后的策略一起到达 waterfall。always mode 先结算下游恢复,并遵循下游的 `retry` 决定;normal mode 先检查失败 code 是否合格、预算是否未耗尽。插件计算延迟——有效且在边界内的提供方 `Retry-After`,否则带对称抖动的本地有界指数退避——追加 `llm/retry` 事件,在可取消定时器上等待,追加 `llm/retry-started`,然后返回 `{ kind: 'retry' }`。loop 随后在同一个打开的轮次内重跑失败步骤(仍基于同一份持久历史)。
+失败步骤连同其提供方与解析后的策略一起到达 waterfall。带 `offloadImages` 的 `IMAGE_OFFLOAD_REQUIRED` 失败会立即修复:推进水位,步骤无延迟重试。always mode 先结算下游恢复,并遵循下游的 `retry` 决定;normal mode 先检查失败 code 是否合格、预算是否未耗尽。插件计算延迟——有效且在边界内的提供方 `Retry-After`,否则带对称抖动的本地有界指数退避——追加 `llm/retry` 事件,在可取消定时器上等待,追加 `llm/retry-started`,然后返回 `{ kind: 'retry' }`。loop 随后在同一个打开的轮次内重跑失败步骤(仍基于同一份持久历史)。
 
 ### Waterfall 组合
 
@@ -112,7 +113,7 @@ kind: "package-reference"
 
 #### 模型看到什么
 
-重试事件、延迟、提供方错误或失败的部分输出都不会对模型可见。除非下游恢复策略刻意改变表面,重试步骤从持久表面历史重建同一显式提供方/模型请求;失败分片绝不进入派生消息。
+重试事件、延迟、提供方错误或失败的部分输出都不会对模型可见。除非下游恢复策略刻意改变表面,重试步骤从持久表面历史重建同一显式提供方/模型请求;失败分片绝不进入派生消息。`IMAGE_OFFLOAD_REQUIRED` 修复是唯一的例外:重试的请求把被省略的图片替换为注明图片及其只读路径的占位文本。
 
 #### Token 影响
 

+ 44 - 0
packages/llm/llm-retry/src/image-offload.ts

@@ -0,0 +1,44 @@
+/**
+ * Durable image offload recovery: advance the session's `image/offload`
+ * watermark past the oldest retained image occurrences an adapter's
+ * `IMAGE_OFFLOAD_REQUIRED` failure names, so the retried step derives the
+ * offloaded set the log records.
+ *
+ * @module @deepseek-ai/dsh-llm-retry/image-offload
+ */
+
+import { visitImageBlocks } from '@deepseek-ai/dsh-llm'
+import { compareImagePositions } from '@deepseek-ai/dsh-session'
+import type { ImageOccurrencePosition, Session } from '@deepseek-ai/dsh-session'
+
+/**
+ * Append one `image/offload` advance covering the `count` oldest image
+ * occurrences the surface still sends, in model request order. A surface
+ * replacement can place a newer event before older ones, so the advance
+ * names the greatest durable position in that prefix; it may offload
+ * additional occurrences but always covers the prefix.
+ * @param session - the session whose derived surface the retried request reads.
+ * @param turn - the open turn.
+ * @param step - the step being retried.
+ * @param count - how many more oldest retained occurrences the adapter needs offloaded.
+ * @returns whether any occurrence remained to offload.
+ */
+export function advanceImageOffload(session: Session, turn: number, step: number, count: number): boolean {
+  let remaining = count
+  let watermark: ImageOccurrencePosition | undefined
+  for (const seq of session.surface.nodes) {
+    if (remaining === 0) break
+    // oxlint-disable-next-line typescript/no-non-null-assertion -- surface nodes index the durable log
+    const message = session.deriveEventMessage(session.eventAt(seq)!)
+    if (message === null) continue
+    visitImageBlocks(message.content, (block, path) => {
+      if (remaining === 0 || block.offloaded === true) return
+      remaining -= 1
+      const position = { seq, path: [...path] }
+      if (watermark === undefined || compareImagePositions(position, watermark) > 0) watermark = position
+    })
+  }
+  if (watermark === undefined) return false
+  session.append('image/offload', { turn, step, watermark })
+  return true
+}

+ 7 - 0
packages/llm/llm-retry/src/index.ts

@@ -10,9 +10,11 @@ import type { Context, Events } from '@deepseek-ai/cordis'
 import z from '@deepseek-ai/schemastery'
 import { z as zod } from 'zod'
 import type { Agent, RequestErrorAction } from '@deepseek-ai/dsh-agent'
+import { IMAGE_OFFLOAD_REQUIRED_CODE } from '@deepseek-ai/dsh-llm'
 import type { LlmFailure, ResolvedRetryPolicy } from '@deepseek-ai/dsh-llm'
 import type {} from '@deepseek-ai/dsh-session-projection'
 import { RetryId } from './brand.ts'
+import { advanceImageOffload } from './image-offload.ts'
 import type { LlmRetryEventData } from './types.ts'
 
 export type { LlmRetryEventData, LlmRetryStartedEventData } from './types.ts'
@@ -195,6 +197,11 @@ export function apply(ctx: Context, config: Config = {}, internals: RetryInterna
     { agent, turn, step, provider, failure, retryPolicy: policy, signal }: Parameters<Events['agent/request-error']>[0],
     next: () => Promise<RequestErrorAction>,
   ): Promise<RequestErrorAction> {
+    // A durable surface repair, not a provider retry: it spends no retry budget and logs no retry event.
+    if (failure.code === IMAGE_OFFLOAD_REQUIRED_CODE && failure.offloadImages !== undefined
+      && advanceImageOffload(agent.session, turn, step, failure.offloadImages)) {
+      return { kind: 'retry' }
+    }
     if (policy === undefined) return next()
     if (policy.mode === 'always') {
       if (signal.aborted || lifetime.signal.aborted) return

+ 24 - 86
packages/llm/llm-image-offload/tests/image-offload.spec.ts → packages/llm/llm-retry/tests/image-offload.spec.ts

@@ -1,8 +1,7 @@
 /**
- * Durable image offload through the plugin: the routed budget advances the
- * `image/offload` watermark before a step, the watermark never retreats, and
- * an adapter's `IMAGE_OFFLOAD_REQUIRED` failure advances it by the named count
- * and retries the step.
+ * Image offload recovery: an adapter's `IMAGE_OFFLOAD_REQUIRED` failure
+ * advances the durable watermark by the named count and retries the step
+ * without spending the provider retry budget.
  */
 
 import { describe, expect, it } from 'vitest'
@@ -11,30 +10,21 @@ import AgentLoop from '@deepseek-ai/dsh-agent-loop'
 import { mountAgentLoopTestDependencies } from '@deepseek-ai/dsh-agent-loop-testkit'
 import SessionProjectionRegistry from '@deepseek-ai/dsh-session-projection'
 import { createUserMessage, IMAGE_OFFLOAD_REQUIRED_CODE, LlmAdapter, LlmError, ToolCallId } from '@deepseek-ai/dsh-llm'
-import type { ContentBlock, GenerateOptions, LlmImageRequestBudget, LlmResolvedModelInfo, StreamChunk } from '@deepseek-ai/dsh-llm'
+import type { ContentBlock, GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm'
 import { SessionId } from '@deepseek-ai/dsh-session'
 import type { Session } from '@deepseek-ai/dsh-session'
-import * as offload from '../src/index.ts'
+import * as retry from '../src/index.ts'
 
 type ScriptEntry = StreamChunk[] | (() => never)
 
-/** Replies one scripted stream per request and declares an optional request-image budget. */
+/** Replies one scripted stream per request and declares no retry policy. */
 class ScriptedAdapter extends LlmAdapter {
   readonly requests: GenerateOptions[] = []
 
-  constructor(private readonly script: ScriptEntry[], private readonly imageRequest?: LlmImageRequestBudget) {
+  constructor(private readonly script: ScriptEntry[]) {
     super()
   }
 
-  override resolveModel(provider: string, model: string): Promise<LlmResolvedModelInfo> {
-    return Promise.resolve({
-      provider,
-      id: model,
-      name: model,
-      ...this.imageRequest === undefined ? {} : { inputModalities: ['text', 'image'], imageRequest: this.imageRequest },
-    })
-  }
-
   async * stream(options: GenerateOptions): AsyncIterable<StreamChunk> {
     this.requests.push(options)
     const entry = this.script.shift()
@@ -62,23 +52,19 @@ async function harness(adapter: ScriptedAdapter): Promise<Context> {
   const ctx = new Context()
   await mountAgentLoopTestDependencies(ctx)
   await ctx.plugin(SessionProjectionRegistry)
-  await ctx.plugin(Object.assign((inner: Context) => { offload.apply(inner, {}) }, { inject: offload.inject }))
+  await ctx.plugin(Object.assign((inner: Context) => { retry.apply(inner, {}) }, { inject: retry.inject }))
   await ctx.plugin(AgentLoop, { agents: [] })
   ctx.llm.registerAdapter(['mock'], adapter)
   return ctx
 }
 
-function image(name: string, bytes: number): Extract<ContentBlock, { type: 'image' }> {
+function image(name: string): Extract<ContentBlock, { type: 'image' }> {
   return {
     type: 'image',
-    attachment: { attachmentId: `sha256:${'a'.repeat(64)}` as never, name, mediaType: 'image/png', bytes, width: 1, height: 1 },
+    attachment: { attachmentId: `sha256:${'a'.repeat(64)}` as never, name, mediaType: 'image/png', bytes: 1, width: 1, height: 1 },
   }
 }
 
-function text(value: string): ReturnType<typeof createUserMessage> {
-  return createUserMessage({ content: [{ type: 'text', text: value }], source: { kind: 'user' } })
-}
-
 function offloadedNames(options: GenerateOptions): string[] {
   const names: string[] = []
   const visit = (blocks: readonly ContentBlock[]): void => {
@@ -102,59 +88,9 @@ function offloadEvents(session: Session): { turn: number; step: number; seq: num
     }))
 }
 
-const budget: LlmImageRequestBudget = { representation: 'raw', maxBytes: 10, byteQuantum: 4 }
-
-describe('llm-image-offload', () => {
-  it('advances the watermark before a step from the routed budget and never retreats', async () => {
-    const adapter = new ScriptedAdapter([textResponse('one'), textResponse('two'), textResponse('three')], budget)
-    const ctx = await harness(adapter)
-    const agent = await ctx.agentLoop.create(SessionId('offload-advance'), { provider: 'mock', model: 'mock' })
-
-    // The first turn logs the request header that names the route.
-    agent.followup(text('hello'))
-    await agent.whenIdle()
-    expect(offloadEvents(agent.session)).toHaveLength(0)
-
-    // 12 raw bytes exceed the 10-byte bound by 2, rounded up to one 4-byte quantum: 'a'
-    // alone removes 4 bytes but the quantum rule crosses into 'b'.
-    agent.session.append('user/message', createUserMessage({
-      content: [image('a', 4), { type: 'tool-result', toolCallId: ToolCallId('shot'), content: [image('b', 4)] }, image('c', 4)],
-      source: { kind: 'user' },
-    }), { surfaceOp: 'append' })
-    agent.followup(text('again'))
-    await agent.whenIdle()
-
-    expect(offloadedNames(adapter.requests[1]!)).toEqual(['a', 'b'])
-    const events = offloadEvents(agent.session)
-    expect(events).toHaveLength(1)
-    expect(events[0]).toMatchObject({ turn: 2, step: 1, path: [1, 0] })
-    const order = agent.session.snapshotEvents().map(event => event.type)
-    expect(order.lastIndexOf('turn/start')).toBeLessThan(order.indexOf('image/offload'))
-    expect(order.indexOf('image/offload')).toBeLessThan(order.lastIndexOf('step/start'))
-
-    // The next turn adds no bytes beyond the bound, so the watermark holds and the
-    // second request's offloaded set is still what the third request sends.
-    agent.followup(text('once more'))
-    await agent.whenIdle()
-    expect(offloadedNames(adapter.requests[2]!)).toEqual(['a', 'b'])
-    expect(offloadEvents(agent.session)).toHaveLength(1)
-  })
-
-  it('leaves a route without a declared budget alone', async () => {
-    const adapter = new ScriptedAdapter([textResponse('ok'), textResponse('ok')])
-    const ctx = await harness(adapter)
-    const agent = await ctx.agentLoop.create(SessionId('offload-none'), { provider: 'mock', model: 'mock' })
-    agent.followup(text('hello'))
-    await agent.whenIdle()
-    agent.session.append('user/message', createUserMessage({ content: [image('a', 400)], source: { kind: 'user' } }), { surfaceOp: 'append' })
-    agent.followup(text('again'))
-    await agent.whenIdle()
-    expect(offloadedNames(adapter.requests[1]!)).toEqual([])
-    expect(offloadEvents(agent.session)).toHaveLength(0)
-  })
-
-  it('advances by the count an IMAGE_OFFLOAD_REQUIRED failure names and retries the step', async () => {
-    const adapter = new ScriptedAdapter([offloadRequired(2), textResponse('sent')], { representation: 'raw', maxBytes: 100 })
+describe('image offload recovery', () => {
+  it('advances the watermark by the named count and retries without a retry event', async () => {
+    const adapter = new ScriptedAdapter([offloadRequired(2), textResponse('sent')])
     const ctx = await harness(adapter)
     const agent = await ctx.agentLoop.create(SessionId('offload-required'), { provider: 'mock', model: 'mock' })
     const delegated: string[] = []
@@ -164,39 +100,41 @@ describe('llm-image-offload', () => {
     })
 
     agent.followup(createUserMessage({
-      content: [image('a', 1), image('b', 1), image('c', 1)], source: { kind: 'user' },
+      content: [image('a'), { type: 'tool-result', toolCallId: ToolCallId('shot'), content: [image('b')] }, image('c')],
+      source: { kind: 'user' },
     }))
     await agent.whenIdle()
 
     expect(adapter.requests).toHaveLength(2)
     expect(offloadedNames(adapter.requests[0]!)).toEqual([])
     expect(offloadedNames(adapter.requests[1]!)).toEqual(['a', 'b'])
-    expect(offloadEvents(agent.session)).toEqual([{ turn: 1, step: 1, seq: expect.any(Number) as never, path: [1] }])
+    expect(offloadEvents(agent.session)).toMatchObject([{ turn: 1, step: 1, path: [1, 0] }])
     expect(delegated).toEqual([])
     const types = agent.session.snapshotEvents().map(event => event.type)
+    expect(types.filter(type => type === 'llm/retry')).toHaveLength(0)
     expect(types.filter(type => type === 'assistant/attempt')).toHaveLength(1)
-    expect(types.filter(type => type === 'assistant/message')).toHaveLength(1)
     expect(types.indexOf('assistant/attempt')).toBeLessThan(types.indexOf('image/offload'))
+    expect(types.indexOf('image/offload')).toBeLessThan(types.indexOf('assistant/message'))
   })
 
-  it('interprets an adapter count in request order after a surface replacement', async () => {
+  it('counts the adapter prefix in request order after a surface replacement', async () => {
     const adapter = new ScriptedAdapter([offloadRequired(2), textResponse('sent')])
     const ctx = await harness(adapter)
     const agent = await ctx.agentLoop.create(SessionId('offload-reordered'), { provider: 'mock', model: 'mock' })
     const first = agent.session.append('user/message', createUserMessage({
-      content: [image('first', 1)], source: { kind: 'user' },
+      content: [image('first')], source: { kind: 'user' },
     }), { surfaceOp: 'append' })
     agent.session.append('user/message', createUserMessage({
-      content: [image('second', 1)], source: { kind: 'user' },
+      content: [image('second')], source: { kind: 'user' },
     }), { surfaceOp: 'append' })
     agent.session.append('user/message', createUserMessage({
-      content: [image('replacement', 1)], source: { kind: 'user' },
+      content: [image('replacement')], source: { kind: 'user' },
     }), {
       surfaceOp: { op: 'replace', start: first.seq, end: first.seq },
       sourceEventSeqs: [first.seq],
     })
 
-    agent.followup(text('send'))
+    agent.followup(createUserMessage({ content: [{ type: 'text', text: 'send' }], source: { kind: 'user' } }))
     await agent.whenIdle()
 
     expect(adapter.requests).toHaveLength(2)
@@ -213,7 +151,7 @@ describe('llm-image-offload', () => {
       delegated.push(failure.code)
       return next()
     })
-    agent.followup(text('no images'))
+    agent.followup(createUserMessage({ content: [{ type: 'text', text: 'no images' }], source: { kind: 'user' } }))
     await agent.whenIdle()
     expect(delegated).toEqual([IMAGE_OFFLOAD_REQUIRED_CODE])
     expect(offloadEvents(agent.session)).toHaveLength(0)

+ 2 - 2
packages/llm/llm/README.i18n.yaml

@@ -2,5 +2,5 @@
 # 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 packages/llm/llm/README.md
-README.md: dc5fb35d5bcb9a5a129ff4b9db9e595a5342f19c
-README.zh.md: 20fa7102eea142720153feb01deb0185205fda28
+README.md: 0fec0b06c9e9d7725e9efe6b98e27ccae1cb1b65
+README.zh.md: 4b1d9738769f3568c66d2bad5a124f0087eaced3

Разлика између датотеке није приказан због своје велике величине
+ 0 - 0
packages/llm/llm/README.md


+ 1 - 1
packages/llm/llm/README.zh.md

@@ -101,7 +101,7 @@ for await (const chunk of ctx.llm.stream({
 
 ### 主流程
 
-请求会对照其精确模型的能力——上下文窗口、输出默认值、推理强度与输入模态——校验,填入任何适配器配置的默认值,然后整个请求被深度冻结。`prepareCall()` 把这些事实、分离的上下文与重试策略绑定到执行最终分发的精确适配器代次,因此 HMR 或动态设置无法把一个代次的图片能力与另一代次的端点混用。支持图片的适配器把持久引用投影为路由专用请求版本;`resolveImageAttachmentAccess()` 会单独把附件提供方的可选宿主对象映射进当前工具执行世界,而不改变请求图片或其 `variantId`。纯文本路由接收确定性的逐图片占位符,包括嵌套工具结果图片,而不会改写仅追加会话历史。持久 `FileBlock` 引用永远不会到达任何适配器:请求组装把每个引用(包括嵌套工具结果中的出现)替换为确定性 handle 文本,指出文件与其只读保存路径,路径经由挂载的附件与文件系统提供方解析。`ctx.llm.fileRequestText(ref)` 向请求计量公开相同的同步投影。支持图片的路由在其解析后的模型信息上声明 `LlmImageRequestBudget`;`dsh-llm-image-offload` 插件用纯函数 `offloadedImagePrefixCount()` 据此规划会话的持久 `image/offload` 水位,表层派生把位于水位及之前的出现位置标为 `offloaded: true`,`projectOffloadedImages()` 把这些标记渲染为路由所属的占位文本。adapter 按精确字节计量仍超预算时,以 `IMAGE_OFFLOAD_REQUIRED` 失败并说明还需省略的出现位置(`requiredImageOffload()`),绝不发送未记录的投影。对视觉 token 收费的适配器声明按路由的 `imageRequestPricing`,`ctx.llm.imageRequestPricing(provider, model)` 为 token meter 同步解析它。分发经过 `llm/stream` waterfall,随后分片以 token 级增量返回,每个适配器结果都以唯一一个终止 `finish` 分片到达消费方。
+请求会对照其精确模型的能力——上下文窗口、输出默认值、推理强度与输入模态——校验,填入任何适配器配置的默认值,然后整个请求被深度冻结。`prepareCall()` 把这些事实、分离的上下文与重试策略绑定到执行最终分发的精确适配器代次,因此 HMR 或动态设置无法把一个代次的图片能力与另一代次的端点混用。支持图片的适配器把持久引用投影为路由专用请求版本;`resolveImageAttachmentAccess()` 会单独把附件提供方的可选宿主对象映射进当前工具执行世界,而不改变请求图片或其 `variantId`。纯文本路由接收确定性的逐图片占位符,包括嵌套工具结果图片,而不会改写仅追加会话历史。持久 `FileBlock` 引用永远不会到达任何适配器:请求组装把每个引用(包括嵌套工具结果中的出现)替换为确定性 handle 文本,指出文件与其只读保存路径,路径经由挂载的附件与文件系统提供方解析。`ctx.llm.fileRequestText(ref)` 向请求计量公开相同的同步投影。表层派生把位于会话持久 `image/offload` 水位及之前的出现位置标为 `offloaded: true`,`projectOffloadedImages()` 把这些标记渲染为路由所属的占位文本。支持图片的路由在保留的出现位置按精确字节超过其 `LlmImageRequestBudget` 时,以 `IMAGE_OFFLOAD_REQUIRED` 失败并说明还需省略多少最老的出现位置(`requiredImageOffload()`),绝不发送未记录的投影;`dsh-llm-retry` 推进水位并重试。对视觉 token 收费的适配器声明按路由的 `imageRequestPricing`,`ctx.llm.imageRequestPricing(provider, model)` 为 token meter 同步解析它。分发经过 `llm/stream` waterfall,随后分片以 token 级增量返回,每个适配器结果都以唯一一个终止 `finish` 分片到达消费方。
 
 ### 不变式
 

+ 7 - 10
packages/llm/llm/src/content.ts

@@ -240,18 +240,16 @@ export function visitImageBlocks(
 
 /**
  * Represented byte length of one image occurrence under a route budget: the
- * normalized byte count clamped to the route's request-version target, then
- * base64-expanded for an inline representation.
- * @param bytes - normalized attachment byte count.
- * @param budget - route representation and request-version target.
+ * request-version byte count, base64-expanded for an inline representation.
+ * @param bytes - request-version byte count.
+ * @param budget - route representation.
  * @returns the byte length the route's request accounting charges.
  */
 export function representedImageBytes(
   bytes: number,
-  budget: Pick<LlmImageRequestBudget, 'representation' | 'versionMaxBytes'>,
+  budget: Pick<LlmImageRequestBudget, 'representation'>,
 ): number {
-  const clamped = budget.versionMaxBytes === undefined ? bytes : Math.min(bytes, budget.versionMaxBytes)
-  return budget.representation === 'base64' ? base64Length(clamped) : clamped
+  return budget.representation === 'base64' ? base64Length(bytes) : bytes
 }
 
 /** Replace every offloaded occurrence, including nested tool results, with its placeholder. */
@@ -300,9 +298,8 @@ export function projectOffloadedImages(
 /**
  * Number of oldest retained image occurrences one route budget removes, in
  * whole count and byte quanta, once the budget is exceeded. The result depends
- * only on the represented lengths, so the image-offload plugin plans the
- * durable watermark from logged facts and an adapter names the same count
- * when its exact accounting still overflows.
+ * only on the represented lengths, so every route names the count the same
+ * way.
  * @param lengths - represented byte length of every retained occurrence, oldest first.
  * @param budget - count/byte budgets and removal quanta; unbounded when absent.
  * @returns how many leading occurrences to offload.

+ 2 - 2
packages/llm/llm/src/error.ts

@@ -166,7 +166,7 @@ export function isHarnessError(value: unknown): value is HarnessError {
  * Canonical code for a request an image-capable route cannot send until the
  * session's durable `image/offload` watermark advances. The failure's
  * `offloadImages` names how many more of the oldest retained occurrences must
- * be offloaded; the `dsh-llm-image-offload` plugin appends the advance and
- * retries the step, so the model never receives an unlogged projection.
+ * be offloaded; `dsh-llm-retry` appends the advance and retries the step, so
+ * the model never receives an unlogged projection.
  */
 export const IMAGE_OFFLOAD_REQUIRED_CODE = 'IMAGE_OFFLOAD_REQUIRED'

+ 0 - 2
packages/llm/llm/src/index.ts

@@ -782,7 +782,6 @@ export class LlmRuntime extends TypertRemoteService {
         'INVALID_MODEL_MAX_TOKENS',
       )
     }
-    const imageRequest = resolved.imageRequest === undefined ? undefined : { ...resolved.imageRequest }
     const info: LlmResolvedModelInfo = {
       provider,
       id: model,
@@ -791,7 +790,6 @@ export class LlmRuntime extends TypertRemoteService {
       ...inputModalities === undefined ? {} : { inputModalities },
       ...context === undefined ? {} : { context: { contextWindow: context.contextWindow } },
       ...defaultMaxTokens === undefined ? {} : { defaultMaxTokens },
-      ...imageRequest === undefined ? {} : { imageRequest },
     }
     const reasoning = resolved.reasoning
     if (reasoning === undefined) return info

+ 6 - 13
packages/llm/llm/src/types.ts

@@ -51,8 +51,8 @@ export interface LlmFailure {
   /**
    * With code `IMAGE_OFFLOAD_REQUIRED`: how many more of the oldest retained
    * image occurrences the route needs offloaded before the same request fits
-   * its exact byte accounting. The `dsh-llm-image-offload` plugin advances the
-   * durable watermark by this count and retries the step.
+   * its exact byte accounting. `dsh-llm-retry` advances the session's durable
+   * `image/offload` watermark by this count and retries the step.
    */
   readonly offloadImages?: number
 }
@@ -330,13 +330,10 @@ export interface LlmModelContext {
 }
 
 /**
- * Request-image budget one exact image-capable route declares, so the
- * `dsh-llm-image-offload` plugin can advance the session's durable
- * `image/offload` watermark before a step from logged facts alone. Byte
- * accounting clamps each occurrence's
- * normalized byte count to {@link versionMaxBytes} and, for the `base64`
- * representation, expands it to its encoded length. A route whose exact
- * request accounting still exceeds the budget fails the request with
+ * Request-image budget one exact image-capable route enforces over the
+ * retained occurrences' exact request-version bytes; for the `base64`
+ * representation each byte count expands to its encoded length. A route whose
+ * retained occurrences exceed the budget fails the request with
  * `IMAGE_OFFLOAD_REQUIRED` naming the additional occurrences to offload.
  */
 export interface LlmImageRequestBudget {
@@ -350,8 +347,6 @@ export interface LlmImageRequestBudget {
   byteQuantum?: number
   /** Occurrences removed as one deterministic advance step; absent removes the minimum. */
   countQuantum?: number
-  /** Encoded-byte target of the route's derived request version; absent accounts normalized bytes. */
-  versionMaxBytes?: number
 }
 
 /** Display metadata for one adapter-owned reasoning effort. */
@@ -383,8 +378,6 @@ export interface LlmResolvedModelInfo extends LlmModelInfo {
   defaultMaxTokens?: number
   /** Adapter-owned selectable reasoning levels when exposed. */
   reasoning?: LlmModelReasoningInfo
-  /** Request-image budget the route enforces; absent for routes that never offload. */
-  imageRequest?: LlmImageRequestBudget
 }
 
 /**

+ 2 - 3
packages/llm/llm/tests/content.spec.ts

@@ -62,11 +62,10 @@ describe('visitImageBlocks', () => {
 })
 
 describe('representedImageBytes', () => {
-  it('clamps to the request-version target and expands inline representations', () => {
+  it('expands inline representations to their encoded length', () => {
     expect(representedImageBytes(100, { representation: 'raw' })).toBe(100)
-    expect(representedImageBytes(100, { representation: 'raw', versionMaxBytes: 60 })).toBe(60)
     expect(representedImageBytes(3, { representation: 'base64' })).toBe(4)
-    expect(representedImageBytes(4, { representation: 'base64', versionMaxBytes: 60 })).toBe(8)
+    expect(representedImageBytes(4, { representation: 'base64' })).toBe(8)
   })
 })
 

+ 0 - 37
pnpm-lock.yaml

@@ -1209,9 +1209,6 @@ importers:
       '@deepseek-ai/dsh-llm-deepseek':
         specifier: workspace:^
         version: link:../../llm/llm-deepseek
-      '@deepseek-ai/dsh-llm-image-offload':
-        specifier: workspace:^
-        version: link:../../llm/llm-image-offload
       '@deepseek-ai/dsh-llm-pi-ai':
         specifier: workspace:^
         version: link:../../llm/llm-pi-ai
@@ -1482,9 +1479,6 @@ importers:
       '@deepseek-ai/dsh-llm-deepseek':
         specifier: workspace:^
         version: link:../../llm/llm-deepseek
-      '@deepseek-ai/dsh-llm-image-offload':
-        specifier: workspace:^
-        version: link:../../llm/llm-image-offload
       '@deepseek-ai/dsh-llm-retry':
         specifier: workspace:^
         version: link:../../llm/llm-retry
@@ -6366,34 +6360,6 @@ importers:
         specifier: workspace:^
         version: link:../../util/timeout
 
-  packages/llm/llm-image-offload:
-    dependencies:
-      '@deepseek-ai/schemastery':
-        specifier: link:../../../vendor/schemastery
-        version: link:../../../vendor/schemastery
-    devDependencies:
-      '@deepseek-ai/cordis':
-        specifier: workspace:^
-        version: link:../../../vendor/cordis
-      '@deepseek-ai/dsh-agent':
-        specifier: workspace:^
-        version: link:../../core/agent
-      '@deepseek-ai/dsh-agent-loop':
-        specifier: workspace:^
-        version: link:../../core/agent-loop
-      '@deepseek-ai/dsh-agent-loop-testkit':
-        specifier: workspace:^
-        version: link:../../test-support/agent-loop-testkit
-      '@deepseek-ai/dsh-llm':
-        specifier: workspace:^
-        version: link:../llm
-      '@deepseek-ai/dsh-session':
-        specifier: workspace:^
-        version: link:../../core/session
-      '@deepseek-ai/dsh-session-projection':
-        specifier: workspace:^
-        version: link:../../session/session-projection
-
   packages/llm/llm-pi-ai:
     dependencies:
       '@deepseek-ai/dsh-brand':
@@ -10226,9 +10192,6 @@ importers:
       '@deepseek-ai/dsh-llm-deepseek':
         specifier: workspace:^
         version: link:../../packages/llm/llm-deepseek
-      '@deepseek-ai/dsh-llm-image-offload':
-        specifier: workspace:^
-        version: link:../../packages/llm/llm-image-offload
       '@deepseek-ai/dsh-llm-pi-ai':
         specifier: workspace:^
         version: link:../../packages/llm/llm-pi-ai

+ 0 - 1
python/sdk-runtime/package.json

@@ -53,7 +53,6 @@
     "@deepseek-ai/dsh-llm": "workspace:^",
     "@deepseek-ai/dsh-llm-deepseek": "workspace:^",
     "@deepseek-ai/dsh-llm-pi-ai": "workspace:^",
-    "@deepseek-ai/dsh-llm-image-offload": "workspace:^",
     "@deepseek-ai/dsh-llm-retry": "workspace:^",
     "@deepseek-ai/dsh-mcp-client": "workspace:^",
     "@deepseek-ai/dsh-output-retention": "workspace:^",

+ 0 - 1
tsconfig.base.json

@@ -297,7 +297,6 @@
       "@deepseek-ai/dsh-llm": ["./packages/llm/llm/src"],
       "@deepseek-ai/dsh-llm/invariant": ["./packages/llm/llm/src/invariant.ts"],
       "@deepseek-ai/dsh-llm-deepseek": ["./packages/llm/llm-deepseek/src"],
-      "@deepseek-ai/dsh-llm-image-offload": ["./packages/llm/llm-image-offload/src"],
       "@deepseek-ai/dsh-llm-mock-server": ["./packages/test-support/llm-mock-server/src"],
       "@deepseek-ai/dsh-llm-pi-ai": ["./packages/llm/llm-pi-ai/src"],
       "@deepseek-ai/dsh-llm-replay": ["./packages/test-support/llm-replay/src"],

+ 0 - 1
tsconfig.host.json

@@ -225,7 +225,6 @@
     { "path": "./packages/context/agent-instructions" },
     { "path": "./packages/core/agent-loop" },
     { "path": "./packages/llm/llm-retry" },
-    { "path": "./packages/llm/llm-image-offload" },
     { "path": "./packages/subprocess/subprocess" },
     { "path": "./packages/subprocess/subprocess-local" },
     { "path": "./packages/subprocess/win32-process" },

Неке датотеке нису приказане због велике количине промена