@deepseek-ai/dsh-attachment 0.1.1-rc.1 → 0.1.2-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.i18n.yaml CHANGED
@@ -2,5 +2,5 @@
2
2
  # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
3
  # after editing either side, bring the other along and re-record with:
4
4
  # pnpm run verify-translation-pairing --write packages/attachment/attachment/README.md
5
- README.md: 19232bd4bb86ed33e56fcdca93999967822422ab
6
- README.zh.md: e5e7aab7c1af30b2b101bdcd218044cd1095ae0d
5
+ README.md: 01a5d6ee143ff53175dd7327cd6d419af61938a3
6
+ README.zh.md: 1a1d297297a6815ce5338391af0182e471f99000
package/README.md CHANGED
@@ -1,16 +1,104 @@
1
+ ---
2
+ description: "Durable image attachments for users and maintainers attaching, reusing, or debugging images in prompts and commands."
3
+ kind: "package-reference"
4
+ ---
5
+
1
6
  # @deepseek-ai/dsh-attachment
2
7
 
3
8
  English | [中文](README.zh.md)
4
9
 
5
- The durable attachment seam. `ctx.attachments` validates and durably commits immutable image bytes, then returns a serializable `ImageAttachmentRef`; consumers never persist browser paths, object URLs, provider URLs, or base64 in session events.
10
+ ## Summary
11
+
12
+ You can attach images to prompts and commands, and the harness keeps provider-independent normalized versions durably: each source image is admitted and normalized before your message is processed, reappears in conversation history, and is projected to the selected model route in later turns of the same session. The shipped `dsh` composition enables this with no setup. Attached images survive restarts, while browser paths, provider URLs, local storage paths, and base64 never enter durable session events. Only raster formats (PNG, JPEG, WebP, GIF) are accepted, and unsent composer drafts stay in the browser until you submit. Stored images are never deleted automatically, and non-image files, audio, and video are not supported yet.
13
+
14
+ ## Table of Contents
15
+
16
+ - [Use this package](#use-this-package)
17
+ - [Understand the implementation](#understand-the-implementation)
18
+ - [Further Exploration](#further-exploration)
19
+ - [Model Experience](#model-experience)
20
+ - [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
21
+ - [Dev Note](#dev-note)
22
+
23
+ -----
24
+
25
+ <a id="use-this-package"></a>
26
+ ## Use this package
27
+
28
+ Image attachments work end to end: attach an image to a prompt or a command, and it is saved, shown in history, and sent to the model without any further action from you. In the default `dsh` composition everything is already wired; when you compose your own setup, one plugin enables the capability.
29
+
30
+ ### Attach images to a prompt
31
+
32
+ Attach one or more images to a user prompt in the client UI. Each source is checked, normalized to a provider-independent 8-bit sRGB/sRGBA raster, and saved before your message is processed; if any image is refused, the whole message fails and nothing is published. Supported source formats are PNG, JPEG, WebP, and GIF; a deployment controls source limits separately from normalized-storage and route-specific request limits. The one plugin below enables durable image attachments (the shipped base composition already mounts it):
33
+
34
+ ```yaml
35
+ - name: '@deepseek-ai/dsh-attachment-local'
36
+ ```
37
+
38
+ ### Pass images to commands
39
+
40
+ Commands that accept image input receive attached images the same way. If a command does not accept images, the harness refuses with an error message instead of silently dropping them.
41
+
42
+ ### Reuse images across the session
43
+
44
+ Saved normalized images stay in conversation history and are projected into deterministic, route-sized request versions in later turns; after a restart, a resumed session shows and reuses the same images. When the current execution filesystem maps the stored host object, the request descriptor also carries a read-only process path that the model can inspect. When history or a request version is read back, the stored bytes are checked against what was recorded, so a missing, corrupted, or swapped image surfaces as an error rather than wrong bytes.
45
+
46
+ ### What can go wrong
47
+
48
+ An image can be refused when you attach it — unsupported format, over the size, pixel, or dimension limits, or bytes that do not match their declared type — and the message then fails as a whole. Later, a history read can fail if the stored image was deleted or corrupted on disk. Failures carry stable codes so the client and protocol adapters can explain them in their own words.
49
+
50
+ -----
51
+
52
+ <a id="understand-the-implementation"></a>
53
+ ## Understand the implementation
54
+
55
+ <details>
56
+ <summary>Implementation internals — click to expand</summary>
57
+
58
+ This section explains the design decisions behind the seam and the service operations that realize the user-visible behavior; observable behavior is fully covered in [Use this package](#use-this-package).
59
+
60
+ ### Design decisions
6
61
 
7
- Unsent composer images remain browser-owned temporary drafts. `validateImage` runs the same admission policy without persisting. `saveImages` owns batch count and aggregate-byte limits, validates every member before writing any member, then commits in order and returns references only after the complete batch succeeds. A later storage failure returns no partial references, although an earlier immutable content-addressed object may remain unreachable until reference-aware garbage collection exists. `AttachmentError.code` uses the closed `AttachmentErrorCode` string union. Its `ImageAdmissionErrorCode` subset marks caller-correctable image-input failures; `isImageAdmissionError` recognizes that subset at runtime so each protocol adapter can map its own error vocabulary. `saveImage` commits one accepted image before any model-visible session event is published, and `readImage` verifies the content-addressed object against its logged metadata. Callers may cancel `readImage`; implementations observe cancellation around backend and verification work and preserve it instead of translating it into a storage failure.
62
+ - **Normalize and persist before event.** Every source is prepared and verified before the batch publishes in order, so the session log never references a partial or failed normalization.
63
+ - **Immutable and retention-neutral.** Objects are immutable once published; resumed and forked sessions may share them, so reference-aware garbage collection is deferred rather than tied to any one session's deletion.
64
+ - **Verify on read.** Reads check bytes and metadata against the logged reference before returning them, and request projections fully decode cached bytes, so a missing, corrupted, or swapped object fails closed.
65
+ - **Role-neutral image blocks.** The `ImageBlock` content block in `dsh-llm` carries an `ImageAttachmentRef`; provider adapters resolve it into deterministic request versions with explicit pixel and byte budgets, while execution filesystems may map the immutable host object to a model-readable process path.
66
+ - **Error routing by code.** `AttachmentError` re-implements the `HarnessError` shape instead of extending it because the base lives in `dsh-llm`, which depends on this package; consumers route on `code`, never on the prototype chain.
8
67
 
9
- `admitEncodedImages(attachments, images)` is the shared wire entry used by every RPC endpoint that accepts browser uploads (the session prompt endpoint and the command executor): it enforces canonical base64 on every member, then delegates batch admission — limits, validation, ordered commit — to `saveImages`. The base64 upload form is `EncodedImageAttachment`, exported from `@deepseek-ai/dsh-attachment/types` so wire contracts can reference it.
68
+ ### Service operations
10
69
 
70
+ The service family runs one admission-and-storage flow: every entry point enforces source batch limits and canonical base64, prepares provider-independent normalized attachments before publishing any member, and commits them durably in input order without partial results. `readImageRequest` derives deterministic route-sized variants whose identity includes the attachment id, transform version, pixel and byte budgets, and encoder settings. The pure `requestImageDimensions` export computes each projection's aspect-preserving dimensions from a total-pixel budget, so providers and request pricing share one geometry. `imageHostPath` exposes an implementation-owned host location only to trusted same-process consumers that need execution-world mapping. Callers compose ordered batches while the implementation owns compression concurrency, caching, and singleflight. Reads and projections preserve caller cancellation. Failures carry stable machine-readable codes, and the caller-correctable admission subset is recognizable at runtime so each protocol adapter maps its own vocabulary; the exact per-operation contracts live in [`src/index.ts`](src/index.ts) and [`src/error.ts`](src/error.ts).
71
+
72
+ ### Source map
73
+
74
+ | File | Role |
75
+ |---|---|
76
+ | [`src/index.ts`](src/index.ts) | Plugin entry: abstract `AttachmentStore` service and re-exports |
77
+ | [`src/types.ts`](src/types.ts) | Durable vocabulary: references, limits, upload and store payloads |
78
+ | [`src/admission.ts`](src/admission.ts) | `admitEncodedImages`: canonical-base64 enforcement, then `saveImages` delegation |
79
+ | [`src/error.ts`](src/error.ts) | `AttachmentError` class and the `isImageAdmissionError` runtime subset |
80
+ | [`src/brand.ts`](src/brand.ts) | `AttachmentId` branded opaque identifier |
81
+ | [`src/invariant.ts`](src/invariant.ts) | Invariant companion (no runtime invariant; implementations enforce immutable-store checks) |
82
+
83
+ </details>
84
+
85
+ -----
86
+
87
+ <a id="further-exploration"></a>
88
+ ## Further Exploration
89
+
90
+ For the full service contract and payload types, read the subsystem reference; for the storage that backs this capability, read the local backend.
91
+
92
+ - [Attachment subsystem reference](../../../docs/subsystems/attachment.md) — service contract, payload types, and the `ctx.attachments` cordis surface.
93
+ - [Local filesystem backend](../attachment-local/README.md) — where your attached images are stored on this machine.
94
+ - [Capability seams](../../../docs/capability-seams.md) — how this capability family is split into roles.
95
+
96
+ -----
97
+
98
+ <a id="model-experience"></a>
11
99
  ## Model Experience
12
100
 
13
- Indirectly, through the role-neutral core `ImageBlock` and provider adapters that resolve its durable reference.
101
+ Indirectly, through the provider adapter, which resolves each durable reference into an exact request version and sends its stable attachment id and actual dimensions beside the image. When the execution filesystem maps the stored object, the descriptor also includes a read-only process path and a matching extension for a writable copy.
14
102
 
15
103
  #### KV Cache effect
16
104
 
@@ -18,6 +106,29 @@ Adding an image changes the provider request and therefore invalidates the affec
18
106
 
19
107
  ## Known Limitations and Deferred Work
20
108
 
21
- - Version one accepts PNG, JPEG, WebP, and GIF only.
22
- - Retention and garbage collection are deferred because resumed and forked sessions may share immutable objects.
23
- - Generic files, audio, video, and persistent unsent drafts require separate lifecycle and provider contracts.
109
+ <a id="known-limitations-and-deferred-work"></a>
110
+
111
+
112
+ These limits describe what image attachments can and cannot do; they are current package constraints, not a task backlog.
113
+
114
+ - **Raster images only** — PNG, JPEG, WebP, and GIF are accepted; generic files, audio, and video are not supported yet.
115
+ - **Images are never deleted** — stored images are retained indefinitely; nothing removes them automatically.
116
+ - **Unsent drafts are not saved** — a composer draft stays in the browser until you submit the message.
117
+
118
+ <a id="dev-note"></a>
119
+ ### Dev Note
120
+
121
+ <details>
122
+ <summary>Working context for maintainers — click to expand</summary>
123
+
124
+ This Dev Note is working context for maintainers: undecided directions and open questions. It is explicitly non-authoritative — shipped behavior and limits live in the sections above and the package code.
125
+
126
+ #### Future: reference-aware garbage collection
127
+
128
+ Resumed and forked sessions may share immutable objects, so any retention policy needs a reference model that accounts for session lineage before objects can be collected. No decision is recorded yet; the local backend currently retains everything.
129
+
130
+ #### Future: non-image attachments and assistant-side output
131
+
132
+ Generic files, audio, and video would need separate lifecycle and provider contracts, and the role-neutral `ImageBlock` leaves assistant-side image output as forward compatibility — current production adapters declare text-only output, so only user content carries images. Both directions are undecided.
133
+
134
+ </details>
package/README.zh.md CHANGED
@@ -1,23 +1,134 @@
1
+ ---
2
+ description: "持久图片附件,供用户与维护者在提示词与命令中附加、复用或排查图片。"
3
+ kind: "package-reference"
4
+ ---
5
+
1
6
  # @deepseek-ai/dsh-attachment
2
7
 
3
8
  [English](README.md) | 中文
4
9
 
5
- 持久附件服务边界。`ctx.attachments` 校验并持久提交不可变图片字节,随后返回可序列化的 `ImageAttachmentRef`;消费方绝不会在会话事件中持久保存浏览器路径、对象 URL、提供方 URL 或 base64。
10
+ ## 概述
11
+
12
+ 你可以把图片附加到提示词和命令中,harness 会持久保存提供方无关的规范化版本:每张源图都会在你的消息被处理前准入并规范化,重新出现在对话历史中,并在同一会话的后续轮次投影到所选模型路由。随附的 `dsh` 组合无需任何配置即可支持这一点。已附加的图片在重启后依然存在,而浏览器路径、提供方 URL、本地存储路径与 base64 绝不会进入持久会话事件。只接受光栅格式(PNG、JPEG、WebP、GIF),未发送的输入区草稿在提交前仍留在浏览器中。已存储的图片永远不会被自动删除,通用文件、音频和视频暂不支持。
13
+
14
+ ## 目录
15
+
16
+ - [使用本包](#use-this-package)
17
+ - [理解实现](#understand-the-implementation)
18
+ - [进一步探索](#further-exploration)
19
+ - [模型体验](#model-experience)
20
+ - [已知限制与延期工作](#known-limitations-and-deferred-work)
21
+ - [开发备注](#dev-note)
22
+
23
+ -----
24
+
25
+ <a id="use-this-package"></a>
26
+ ## 使用本包
27
+
28
+ 图片附件端到端可用:把图片附加到提示词或命令,它会自动保存、显示在历史中并发送给模型,无需你再做任何操作。在默认 `dsh` 组合中一切都已接好;自行组合时,一个插件即可启用该能力。
29
+
30
+ ### 在提示词中附加图片
31
+
32
+ 在客户端 UI 中向用户提示词附加一张或多张图片。每个源图都会在你的消息被处理前接受检查、规范化为提供方无关的 8-bit sRGB/sRGBA 光栅并保存;如果任何一张图片被拒绝,整条消息都会失败且不会发布任何内容。支持的源格式为 PNG、JPEG、WebP 与 GIF;部署方分别控制源图限制、规范化存储限制与路由专用请求限制。下面这一个插件即可启用持久图片附件(随附的 base 组合已经挂载它):
33
+
34
+ ```yaml
35
+ - name: '@deepseek-ai/dsh-attachment-local'
36
+ ```
37
+
38
+ ### 把图片传给命令
39
+
40
+ 接受图片输入的命令以相同方式接收附加图片。如果某个命令不接受图片,harness 会以错误消息拒绝,而不是静默丢弃。
41
+
42
+ ### 在整个会话中复用图片
43
+
44
+ 已保存的规范化图片会保留在对话历史中,并在后续轮次投影为确定性的路由尺寸请求版本;重启后,恢复的会话会显示并复用相同的图片。当前执行文件系统可以映射已存宿主对象时,请求描述符还会携带模型可检查的只读进程路径。回读历史或请求版本时,已存储的字节会与记录的内容比对,因此缺失、损坏或被替换的图片会以错误形式呈现,而不是错误的字节。
45
+
46
+ ### 可能出什么问题
47
+
48
+ 附加图片时可能被拒绝——格式不受支持、超出大小、像素或尺寸限制,或者字节与声明类型不符——此时整条消息失败。之后,如果磁盘上的图片被删除或损坏,历史读取也可能失败。失败带有稳定错误码,客户端与协议适配器可以用自己的措辞解释它们。
49
+
50
+ -----
51
+
52
+ <a id="understand-the-implementation"></a>
53
+ ## 理解实现
54
+
55
+ <details>
56
+ <summary>实现细节——点击展开</summary>
57
+
58
+ 本节解释 seam 背后的设计决策,以及实现用户可见行为的服务操作;可观察行为已在[使用本包](#use-this-package)中完整说明。
59
+
60
+ ### 设计决策
6
61
 
7
- 未发送的输入区图片仍是由浏览器持有的临时草稿。`validateImage` 运行相同的准入策略,但不执行持久化。`saveImages` 负责批次图片数量和总字节限制,先校验全部成员,再按顺序提交,并且只在完整批次成功后返回引用。后续存储失败不会返回部分引用,但较早写入的不可变内容寻址对象可能保持不可达,直至具备按引用感知的垃圾回收。`AttachmentError.code` 使用封闭的 `AttachmentErrorCode` 字符串联合类型。其 `ImageAdmissionErrorCode` 子集标记可由调用方修正的图片输入失败;`isImageAdmissionError` 在运行时识别该子集,使每个协议适配器可以映射自己的错误词汇。`saveImage` 会在发布任何模型可见的会话事件前提交一张已接受的图片,`readImage` 则根据已记录的元数据校验内容寻址对象。调用方可以取消 `readImage`;实现会在后端读取与校验工作的边界观察取消,并保留取消语义,而不会将其转换为存储失败。
62
+ - **事件前完成规范化与持久化。** 每个源图都会在批次按序发布前完成准备与校验,因此会话日志绝不会引用部分完成或规范化失败的对象。
63
+ - **不可变且保留策略中立。** 对象一经发布即不可变;恢复和 fork 后的会话可能共享它们,因此引用感知的垃圾回收被推迟,而不是与任何单个会话的删除绑定。
64
+ - **读取时校验。** 读取在返回前把字节和元数据与记录的引用比对,请求投影还会完整解码缓存字节,因此缺失、损坏或被替换的对象都会失败关闭。
65
+ - **角色无关的图片块。** `dsh-llm` 中的 `ImageBlock` 内容块携带 `ImageAttachmentRef`;提供方适配器以显式像素与字节预算把引用解析为确定性请求版本,执行文件系统则可以把不可变宿主对象映射为模型可读的进程路径。
66
+ - **按错误码路由。** `AttachmentError` 重新实现 `HarnessError` 的结构而不是继承它,因为基类位于 `dsh-llm`,而后者依赖本包;消费方按 `code` 路由,绝不依赖原型链。
8
67
 
9
- `admitEncodedImages(attachments, images)` 是每个接受浏览器上传的 RPC 端点(会话 prompt 端点与命令执行器)共用的 wire 入口:它对每个成员强制执行规范 base64,随后把批量准入——限额、校验、有序提交——委托给 `saveImages`。base64 上传形式为 `EncodedImageAttachment`,从 `@deepseek-ai/dsh-attachment/types` 导出,供 wire 契约引用。
68
+ ### 服务操作
10
69
 
70
+ 服务族运行同一条准入与存储流程:每个入口都强制执行源批次限制与规范 base64,在发布任何成员前准备提供方无关的规范化附件,再按输入顺序持久提交而不产生部分结果。`readImageRequest` 派生确定性的路由尺寸变体,其身份包含附件 id、变换版本、像素与字节预算及编码参数。纯函数导出 `requestImageDimensions` 会按总像素预算计算每个投影保持宽高比的尺寸,使提供方与请求定价共享同一套几何计算。`imageHostPath` 只向需要执行世界映射的受信任同进程消费方暴露实现拥有的宿主位置。调用方组合有序批次,而实现拥有压缩并发、缓存与 singleflight。读取和投影保留调用方的取消语义。失败带有稳定且机器可读的错误码,运行时即可识别可由调用方修正的准入子集,让每个协议适配器映射自己的词汇;各操作的确切约定见 [`src/index.ts`](src/index.ts) 与 [`src/error.ts`](src/error.ts)。
71
+
72
+ ### 源码地图
73
+
74
+ | 文件 | 职责 |
75
+ |---|---|
76
+ | [`src/index.ts`](src/index.ts) | 插件入口:抽象 `AttachmentStore` 服务与再导出 |
77
+ | [`src/types.ts`](src/types.ts) | 持久词汇:引用、限额、上传与存储载荷 |
78
+ | [`src/admission.ts`](src/admission.ts) | `admitEncodedImages`:规范 base64 强制,随后委托 `saveImages` |
79
+ | [`src/error.ts`](src/error.ts) | `AttachmentError` 类与 `isImageAdmissionError` 运行时子集 |
80
+ | [`src/brand.ts`](src/brand.ts) | `AttachmentId` 带类型标记的不透明标识符 |
81
+ | [`src/invariant.ts`](src/invariant.ts) | 不变式伴生插件(无运行时不变式;实现负责强制不可变存储检查) |
82
+
83
+ </details>
84
+
85
+ -----
86
+
87
+ <a id="further-exploration"></a>
88
+ ## 进一步探索
89
+
90
+ 完整的服务约定与载荷类型请看子系统参考;支撑这一能力的存储请看本地后端。
91
+
92
+ - [附件子系统参考](../../../docs/subsystems/attachment.zh.md)——服务约定、载荷类型与 `ctx.attachments` 的 cordis 接口面。
93
+ - [本地文件系统后端](../attachment-local/README.zh.md)——你的附加图片在本机上的存储位置。
94
+ - [能力 seam](../../../docs/capability-seams.zh.md)——本能力家族如何拆分为多个角色。
95
+
96
+ -----
97
+
98
+ <a id="model-experience"></a>
11
99
  ## 模型体验
12
100
 
13
- 该包通过角色无关的核心 `ImageBlock`,以及解析其持久引用的提供方适配器,间接影响模型。
101
+ 该包通过提供方适配器间接影响模型;适配器会把每个持久引用解析为确切请求版本,并在图片旁发送稳定附件 id 与实际尺寸。执行文件系统可以映射已存对象时,描述符还会包含只读进程路径,以及可写副本使用的匹配扩展名。
14
102
 
15
- #### KV 缓存影响
103
+ #### KV Cache 影响
16
104
 
17
105
  添加图片会改变提供方请求,因此会使受影响的请求后缀失效。
18
106
 
19
- ## 已知限制与待完成工作
107
+ ## 已知限制与延期工作
108
+
109
+ <a id="known-limitations-and-deferred-work"></a>
110
+
111
+
112
+ 这些限制描述了图片附件能做什么、不能做什么;它们是当前包约束,而非任务积压。
113
+
114
+ - **仅支持光栅图片**——接受 PNG、JPEG、WebP 与 GIF;通用文件、音频和视频暂不支持。
115
+ - **图片永远不会被删除**——已存储的图片无限期保留;没有任何机制自动移除它们。
116
+ - **未发送的草稿不会保存**——输入区草稿在提交消息前一直留在浏览器中。
117
+
118
+ <a id="dev-note"></a>
119
+ ### 开发备注
120
+
121
+ <details>
122
+ <summary>维护者的工作上下文——点击展开</summary>
123
+
124
+ 本开发备注是维护者的工作上下文:尚未决定的探索方向与开放问题。它明确不具权威性——已交付的行为与限制以上文和包代码为准。
125
+
126
+ #### 未来:引用感知的垃圾回收
127
+
128
+ 恢复和 fork 后的会话可能共享不可变对象,因此任何保留策略都需要一个能考虑会话血缘的引用模型,之后才能回收对象。目前尚未记录任何决定;本地后端当前保留一切。
129
+
130
+ #### 未来:非图片附件与助手侧输出
131
+
132
+ 通用文件、音频与视频需要单独的生命周期与提供方契约;角色无关的 `ImageBlock` 也把助手侧图片输出留作前瞻兼容——当前生产适配器声明只输出文本,因此只有用户内容携带图片。两个方向都尚未决定。
20
133
 
21
- - 第一版仅接受 PNG、JPEG、WebP 和 GIF。
22
- - 保留策略与垃圾回收尚未实现,因为恢复和 fork 后的会话可能共享不可变对象。
23
- - 通用文件、音频、视频和持久的未发送草稿需要单独的生命周期与提供方契约。
134
+ </details>
package/lib/index.js CHANGED
@@ -55,6 +55,14 @@ function isImageAdmissionError(error) {
55
55
  function AttachmentId(value) {
56
56
  return value;
57
57
  }
58
+ /**
59
+ * Brand a validated request-image transformation identifier.
60
+ * @param value - attachment-provider-produced opaque identifier.
61
+ * @returns the branded identifier.
62
+ */
63
+ function ImageVariantId(value) {
64
+ return value;
65
+ }
58
66
  //#endregion
59
67
  //#region lib/types/admission.js
60
68
  /** Wire-form admission of base64-encoded image uploads. @module @deepseek-ai/dsh-attachment/admission */
@@ -86,6 +94,48 @@ async function admitEncodedImages(attachments, images) {
86
94
  return attachments.saveImages(images.map(saveInput));
87
95
  }
88
96
  //#endregion
97
+ //#region lib/types/request-projection.js
98
+ /**
99
+ * Pure request-projection geometry shared by attachment providers and
100
+ * provider-side request pricing. @module @deepseek-ai/dsh-attachment/request-projection
101
+ */
102
+ /**
103
+ * Compute aspect-preserving integer dimensions within a hard total-pixel budget.
104
+ * @param width - positive source width.
105
+ * @param height - positive source height.
106
+ * @param maxPixels - positive width-times-height cap.
107
+ * @returns inward-rounded dimensions; small images are not enlarged.
108
+ */
109
+ function requestImageDimensions(width, height, maxPixels) {
110
+ const scale = Math.min(1, Math.sqrt(maxPixels / (width * height)));
111
+ if (scale === 1) return {
112
+ width,
113
+ height
114
+ };
115
+ if (width >= height) {
116
+ let projectedWidth = Math.max(1, Math.floor(width * scale));
117
+ let projectedHeight = Math.max(1, Math.round(projectedWidth * height / width));
118
+ while (projectedWidth * projectedHeight > maxPixels && projectedWidth > 1) {
119
+ projectedWidth -= 1;
120
+ projectedHeight = Math.max(1, Math.round(projectedWidth * height / width));
121
+ }
122
+ return {
123
+ width: projectedWidth,
124
+ height: projectedHeight
125
+ };
126
+ }
127
+ let projectedHeight = Math.max(1, Math.floor(height * scale));
128
+ let projectedWidth = Math.max(1, Math.round(projectedHeight * width / height));
129
+ while (projectedWidth * projectedHeight > maxPixels && projectedHeight > 1) {
130
+ projectedHeight -= 1;
131
+ projectedWidth = Math.max(1, Math.round(projectedHeight * width / height));
132
+ }
133
+ return {
134
+ width: projectedWidth,
135
+ height: projectedHeight
136
+ };
137
+ }
138
+ //#endregion
89
139
  //#region lib/types/index.js
90
140
  /** Durable attachment storage seam (`ctx.attachments`). @module @deepseek-ai/dsh-attachment */
91
141
  /** Immutable binary attachment service. Implementations validate bytes before publishing a reference. */
@@ -101,16 +151,42 @@ var AttachmentStore = class extends Service {
101
151
  * @param inputs - encoded images in their owning message order.
102
152
  * @returns durable references in the exact input order.
103
153
  */
104
- async saveImages(inputs) {
154
+ validateImageBatch(inputs) {
105
155
  const { maxImagesPerMessage, maxMessageImageBytes, mediaTypes } = this.imageLimits;
106
156
  if (inputs.length > maxImagesPerMessage) throw new AttachmentError("Image batch exceeds the configured image-count limit.", "TOO_MANY_IMAGES");
107
157
  if (inputs.reduce((sum, input) => sum + input.data.byteLength, 0) > maxMessageImageBytes) throw new AttachmentError("Image batch exceeds the configured aggregate image-byte limit.", "IMAGES_TOO_LARGE");
108
158
  for (const input of inputs) if (!mediaTypes.includes(input.mediaType)) throw new AttachmentError(`Image type ${input.mediaType} is not accepted by this deployment.`, "UNSUPPORTED_IMAGE_TYPE");
159
+ }
160
+ /**
161
+ * Validate and durably commit one ordered image batch.
162
+ * @param inputs - encoded images in owning-message order.
163
+ * @returns durable normalized attachment references in the same order after every member succeeds.
164
+ */
165
+ async saveImages(inputs) {
166
+ this.validateImageBatch(inputs);
109
167
  for (const input of inputs) await this.validateImage(input);
110
168
  const refs = [];
111
169
  for (const input of inputs) refs.push(await this.saveImage(input));
112
170
  return refs;
113
171
  }
172
+ /**
173
+ * Locate the provider-owned normalized object in the harness host filesystem.
174
+ * @param ref - durable normalized attachment reference.
175
+ * @returns an absolute host path, or undefined when this backend is not host-file-backed.
176
+ * @throws an AttachmentError when the durable reference is invalid.
177
+ */
178
+ imageHostPath(ref) {}
179
+ /**
180
+ * Generate or read one deterministic model-request version from the stored normalized image.
181
+ * @param ref - durable provider-independent normalized attachment reference.
182
+ * @param policy - exact route pixel budget and encoded-byte target; a target no ladder quality meets yields the smallest ladder output.
183
+ * @param signal - optional cancellation.
184
+ * @returns request bytes and the cache/upload identity covering every transform input.
185
+ */
186
+ readImageRequest(ref, policy, signal) {
187
+ signal?.throwIfAborted();
188
+ return Promise.reject(new AttachmentError("The mounted attachment provider cannot derive model-request images.", "ATTACHMENT_PROJECTION_UNSUPPORTED"));
189
+ }
114
190
  };
115
191
  //#endregion
116
- export { AttachmentError, AttachmentId, AttachmentStore, AttachmentStore as default, admitEncodedImages, isImageAdmissionError };
192
+ export { AttachmentError, AttachmentId, AttachmentStore, AttachmentStore as default, ImageVariantId, admitEncodedImages, isImageAdmissionError, requestImageDimensions };
@@ -8,4 +8,12 @@ export type AttachmentId = Branded<'AttachmentId'>;
8
8
  * @returns the branded identifier.
9
9
  */
10
10
  export declare function AttachmentId(value: string): AttachmentId;
11
+ /** Opaque deterministic identity for one request-image transformation. */
12
+ export type ImageVariantId = Branded<'ImageVariantId'>;
13
+ /**
14
+ * Brand a validated request-image transformation identifier.
15
+ * @param value - attachment-provider-produced opaque identifier.
16
+ * @returns the branded identifier.
17
+ */
18
+ export declare function ImageVariantId(value: string): ImageVariantId;
11
19
  //# sourceMappingURL=brand.d.ts.map
@@ -7,4 +7,12 @@
7
7
  export function AttachmentId(value) {
8
8
  return value;
9
9
  }
10
+ /**
11
+ * Brand a validated request-image transformation identifier.
12
+ * @param value - attachment-provider-produced opaque identifier.
13
+ * @returns the branded identifier.
14
+ */
15
+ export function ImageVariantId(value) {
16
+ return value;
17
+ }
10
18
  //# sourceMappingURL=brand.js.map
@@ -3,7 +3,7 @@ declare const IMAGE_ADMISSION_ERROR_CODES: readonly ["TOO_MANY_IMAGES", "IMAGES_
3
3
  /** Caller-correctable attachment failure codes raised while admitting image input. */
4
4
  export type ImageAdmissionErrorCode = typeof IMAGE_ADMISSION_ERROR_CODES[number];
5
5
  /** Stable attachment failure codes used for protocol error routing. */
6
- export type AttachmentErrorCode = ImageAdmissionErrorCode | 'INVALID_ATTACHMENT_REF' | 'ATTACHMENT_CORRUPT' | 'ATTACHMENT_WRITE_FAILED' | 'ATTACHMENT_NOT_FOUND' | 'ATTACHMENT_READ_FAILED';
6
+ export type AttachmentErrorCode = ImageAdmissionErrorCode | 'INVALID_ATTACHMENT_REF' | 'ATTACHMENT_CORRUPT' | 'ATTACHMENT_WRITE_FAILED' | 'ATTACHMENT_NOT_FOUND' | 'ATTACHMENT_READ_FAILED' | 'ATTACHMENT_PROJECTION_UNSUPPORTED';
7
7
  /**
8
8
  * Stable failures suitable for host RPC error mapping.
9
9
  *
@@ -1,11 +1,12 @@
1
1
  /** Durable attachment storage seam (`ctx.attachments`). @module @deepseek-ai/dsh-attachment */
2
2
  import { Context, Service } from '@deepseek-ai/cordis';
3
- import type { ImageAttachmentLimits, ImageAttachmentRef, SaveImageAttachment, StoredImageAttachment } from './types.ts';
4
- export { AttachmentId } from './brand.ts';
3
+ import type { ImageAttachmentLimits, ImageAttachmentRef, ImageRequestPolicy, RequestImageAttachment, SaveImageAttachment, StoredImageAttachment } from './types.ts';
4
+ export { AttachmentId, ImageVariantId } from './brand.ts';
5
5
  export { AttachmentError, isImageAdmissionError } from './error.ts';
6
6
  export type { AttachmentErrorCode, ImageAdmissionErrorCode } from './error.ts';
7
7
  export { admitEncodedImages } from './admission.ts';
8
- export type { AttachmentId as AttachmentIdType, EncodedImageAttachment, ImageAttachmentLimits, ImageAttachmentRef, ImageMediaType, SaveImageAttachment, StoredImageAttachment, } from './types.ts';
8
+ export { requestImageDimensions } from './request-projection.ts';
9
+ export type { AttachmentId as AttachmentIdType, EncodedImageAttachment, ImageAttachmentLimits, ImageAttachmentRef, ImageRequestPolicy, ImageMediaType, RequestImageAttachment, SaveImageAttachment, StoredImageAttachment, } from './types.ts';
9
10
  declare module '@deepseek-ai/cordis' {
10
11
  interface Context {
11
12
  attachments: AttachmentStore;
@@ -31,21 +32,45 @@ export declare abstract class AttachmentStore extends Service {
31
32
  * @param inputs - encoded images in their owning message order.
32
33
  * @returns durable references in the exact input order.
33
34
  */
35
+ protected validateImageBatch(inputs: readonly SaveImageAttachment[]): void;
36
+ /**
37
+ * Validate and durably commit one ordered image batch.
38
+ * @param inputs - encoded images in owning-message order.
39
+ * @returns durable normalized attachment references in the same order after every member succeeds.
40
+ */
34
41
  saveImages(inputs: readonly SaveImageAttachment[]): Promise<readonly ImageAttachmentRef[]>;
35
42
  /**
36
43
  * Validate and durably commit one image before its owning session event is appended.
44
+ * The returned reference describes the persisted normalized image. When
45
+ * normalization reduces the raster, its `originalDimensions` records the
46
+ * orientation-applied input dimensions.
37
47
  * @param input - encoded bytes, declared media type, and optional display name.
38
- * @returns a durable content-addressed reference.
48
+ * @returns the durable content-addressed normalized image reference.
39
49
  */
40
50
  abstract saveImage(input: SaveImageAttachment): Promise<ImageAttachmentRef>;
41
51
  /**
42
52
  * Read one image and verify that bytes still match the recorded reference.
43
53
  * @param ref - durable reference from the session log.
44
54
  * @param signal - optional cancellation for backend read and verification work.
45
- * @returns the verified bytes and canonical reference.
55
+ * @returns the verified bytes and normalized attachment reference.
46
56
  * @throws the signal reason when aborted, or a storage error when verification fails.
47
57
  */
48
58
  abstract readImage(ref: ImageAttachmentRef, signal?: AbortSignal): Promise<StoredImageAttachment>;
59
+ /**
60
+ * Locate the provider-owned normalized object in the harness host filesystem.
61
+ * @param ref - durable normalized attachment reference.
62
+ * @returns an absolute host path, or undefined when this backend is not host-file-backed.
63
+ * @throws an AttachmentError when the durable reference is invalid.
64
+ */
65
+ imageHostPath(ref: ImageAttachmentRef): string | undefined;
66
+ /**
67
+ * Generate or read one deterministic model-request version from the stored normalized image.
68
+ * @param ref - durable provider-independent normalized attachment reference.
69
+ * @param policy - exact route pixel budget and encoded-byte target; a target no ladder quality meets yields the smallest ladder output.
70
+ * @param signal - optional cancellation.
71
+ * @returns request bytes and the cache/upload identity covering every transform input.
72
+ */
73
+ readImageRequest(ref: ImageAttachmentRef, policy: ImageRequestPolicy, signal?: AbortSignal): Promise<RequestImageAttachment>;
49
74
  }
50
75
  export default AttachmentStore;
51
76
  //# sourceMappingURL=index.d.ts.map
@@ -1,9 +1,10 @@
1
1
  /** Durable attachment storage seam (`ctx.attachments`). @module @deepseek-ai/dsh-attachment */
2
2
  import { Service } from '@deepseek-ai/cordis';
3
3
  import { AttachmentError } from "./error.js";
4
- export { AttachmentId } from "./brand.js";
4
+ export { AttachmentId, ImageVariantId } from "./brand.js";
5
5
  export { AttachmentError, isImageAdmissionError } from "./error.js";
6
6
  export { admitEncodedImages } from "./admission.js";
7
+ export { requestImageDimensions } from "./request-projection.js";
7
8
  /** Immutable binary attachment service. Implementations validate bytes before publishing a reference. */
8
9
  export class AttachmentStore extends Service {
9
10
  constructor(ctx) {
@@ -17,7 +18,7 @@ export class AttachmentStore extends Service {
17
18
  * @param inputs - encoded images in their owning message order.
18
19
  * @returns durable references in the exact input order.
19
20
  */
20
- async saveImages(inputs) {
21
+ validateImageBatch(inputs) {
21
22
  const { maxImagesPerMessage, maxMessageImageBytes, mediaTypes } = this.imageLimits;
22
23
  if (inputs.length > maxImagesPerMessage) {
23
24
  throw new AttachmentError('Image batch exceeds the configured image-count limit.', 'TOO_MANY_IMAGES');
@@ -31,6 +32,14 @@ export class AttachmentStore extends Service {
31
32
  throw new AttachmentError(`Image type ${input.mediaType} is not accepted by this deployment.`, 'UNSUPPORTED_IMAGE_TYPE');
32
33
  }
33
34
  }
35
+ }
36
+ /**
37
+ * Validate and durably commit one ordered image batch.
38
+ * @param inputs - encoded images in owning-message order.
39
+ * @returns durable normalized attachment references in the same order after every member succeeds.
40
+ */
41
+ async saveImages(inputs) {
42
+ this.validateImageBatch(inputs);
34
43
  for (const input of inputs)
35
44
  await this.validateImage(input);
36
45
  const refs = [];
@@ -38,6 +47,29 @@ export class AttachmentStore extends Service {
38
47
  refs.push(await this.saveImage(input));
39
48
  return refs;
40
49
  }
50
+ /**
51
+ * Locate the provider-owned normalized object in the harness host filesystem.
52
+ * @param ref - durable normalized attachment reference.
53
+ * @returns an absolute host path, or undefined when this backend is not host-file-backed.
54
+ * @throws an AttachmentError when the durable reference is invalid.
55
+ */
56
+ imageHostPath(ref) {
57
+ void ref;
58
+ return undefined;
59
+ }
60
+ /**
61
+ * Generate or read one deterministic model-request version from the stored normalized image.
62
+ * @param ref - durable provider-independent normalized attachment reference.
63
+ * @param policy - exact route pixel budget and encoded-byte target; a target no ladder quality meets yields the smallest ladder output.
64
+ * @param signal - optional cancellation.
65
+ * @returns request bytes and the cache/upload identity covering every transform input.
66
+ */
67
+ readImageRequest(ref, policy, signal) {
68
+ signal?.throwIfAborted();
69
+ void ref;
70
+ void policy;
71
+ return Promise.reject(new AttachmentError('The mounted attachment provider cannot derive model-request images.', 'ATTACHMENT_PROJECTION_UNSUPPORTED'));
72
+ }
41
73
  }
42
74
  export default AttachmentStore;
43
75
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Pure request-projection geometry shared by attachment providers and
3
+ * provider-side request pricing. @module @deepseek-ai/dsh-attachment/request-projection
4
+ */
5
+ /**
6
+ * Compute aspect-preserving integer dimensions within a hard total-pixel budget.
7
+ * @param width - positive source width.
8
+ * @param height - positive source height.
9
+ * @param maxPixels - positive width-times-height cap.
10
+ * @returns inward-rounded dimensions; small images are not enlarged.
11
+ */
12
+ export declare function requestImageDimensions(width: number, height: number, maxPixels: number): {
13
+ width: number;
14
+ height: number;
15
+ };
16
+ //# sourceMappingURL=request-projection.d.ts.map
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Pure request-projection geometry shared by attachment providers and
3
+ * provider-side request pricing. @module @deepseek-ai/dsh-attachment/request-projection
4
+ */
5
+ /**
6
+ * Compute aspect-preserving integer dimensions within a hard total-pixel budget.
7
+ * @param width - positive source width.
8
+ * @param height - positive source height.
9
+ * @param maxPixels - positive width-times-height cap.
10
+ * @returns inward-rounded dimensions; small images are not enlarged.
11
+ */
12
+ export function requestImageDimensions(width, height, maxPixels) {
13
+ const scale = Math.min(1, Math.sqrt(maxPixels / (width * height)));
14
+ if (scale === 1)
15
+ return { width, height };
16
+ if (width >= height) {
17
+ let projectedWidth = Math.max(1, Math.floor(width * scale));
18
+ let projectedHeight = Math.max(1, Math.round(projectedWidth * height / width));
19
+ while (projectedWidth * projectedHeight > maxPixels && projectedWidth > 1) {
20
+ projectedWidth -= 1;
21
+ projectedHeight = Math.max(1, Math.round(projectedWidth * height / width));
22
+ }
23
+ return { width: projectedWidth, height: projectedHeight };
24
+ }
25
+ let projectedHeight = Math.max(1, Math.floor(height * scale));
26
+ let projectedWidth = Math.max(1, Math.round(projectedHeight * width / height));
27
+ while (projectedWidth * projectedHeight > maxPixels && projectedHeight > 1) {
28
+ projectedHeight -= 1;
29
+ projectedWidth = Math.max(1, Math.round(projectedHeight * width / height));
30
+ }
31
+ return { width: projectedWidth, height: projectedHeight };
32
+ }
33
+ //# sourceMappingURL=request-projection.js.map
@@ -1,9 +1,9 @@
1
1
  /** Durable attachment vocabulary. @module @deepseek-ai/dsh-attachment/types */
2
- import type { AttachmentId } from './brand.ts';
2
+ import type { AttachmentId, ImageVariantId } from './brand.ts';
3
3
  export type { AttachmentId } from './brand.ts';
4
4
  /** Raster image formats accepted by the version-one attachment path. */
5
5
  export type ImageMediaType = 'image/png' | 'image/jpeg' | 'image/webp' | 'image/gif';
6
- /** Durable, serializable metadata for one immutable image object. */
6
+ /** Durable, serializable reference to one immutable normalized image. */
7
7
  export interface ImageAttachmentRef {
8
8
  /** Opaque storage identifier; never a filesystem path or bearer URL. */
9
9
  attachmentId: AttachmentId;
@@ -17,6 +17,14 @@ export interface ImageAttachmentRef {
17
17
  height: number;
18
18
  /** Optional display name stripped of local path information. */
19
19
  name?: string;
20
+ /**
21
+ * Input dimensions after applying EXIF orientation and before normalization
22
+ * scaling. Present only when normalization reduced the image.
23
+ */
24
+ originalDimensions?: {
25
+ width: number;
26
+ height: number;
27
+ };
20
28
  }
21
29
  /** Deployment-resolved limits used by upload admission and request buffering. */
22
30
  export interface ImageAttachmentLimits {
@@ -50,4 +58,30 @@ export interface StoredImageAttachment {
50
58
  ref: ImageAttachmentRef;
51
59
  data: Uint8Array;
52
60
  }
61
+ /** Deterministic request-image policy selected by one exact model route. */
62
+ export interface ImageRequestPolicy {
63
+ /** Maximum width multiplied by height after aspect-preserving projection. */
64
+ maxPixels: number;
65
+ /** Encoded-byte target before base64 expansion or Files API upload; the smallest quality-ladder output is kept when no quality fits. */
66
+ maxBytes: number;
67
+ }
68
+ /** Cached request version derived from one provider-independent normalized attachment. */
69
+ export interface RequestImageAttachment {
70
+ /** Cache and upload-index key over the attachment id, policy, and fixed encoder parameters. */
71
+ variantId: ImageVariantId;
72
+ /** Durable normalized attachment from which this request version was derived. */
73
+ attachment: ImageAttachmentRef;
74
+ /** Encoded request bytes. */
75
+ data: Uint8Array;
76
+ mediaType: ImageMediaType;
77
+ bytes: number;
78
+ width: number;
79
+ height: number;
80
+ /** Provider-compatible sample depth proven after request encoding. */
81
+ depth: 'uchar';
82
+ /** Provider-compatible color space proven after request encoding. */
83
+ space: 'srgb';
84
+ /** Whether the encoded request version retains an alpha channel. */
85
+ hasAlpha: boolean;
86
+ }
53
87
  //# sourceMappingURL=types.d.ts.map
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@deepseek-ai/dsh-attachment",
3
3
  "description": "Durable immutable attachment storage seam for the DeepSeek Harness",
4
- "version": "0.1.1-rc.1",
4
+ "version": "0.1.2-alpha.2",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -37,13 +37,13 @@
37
37
  ],
38
38
  "license": "MIT",
39
39
  "peerDependencies": {
40
- "@deepseek-ai/dsh-brand": "^0.1.1-rc.1",
41
- "@deepseek-ai/cordis": "^4.0.1",
42
- "@deepseek-ai/dsh-invariants": "^0.1.1-rc.1"
40
+ "@deepseek-ai/cordis": "^4.0.2",
41
+ "@deepseek-ai/dsh-brand": "^0.1.2-alpha.2",
42
+ "@deepseek-ai/dsh-invariants": "^0.1.2-alpha.2"
43
43
  },
44
44
  "devDependencies": {
45
- "@deepseek-ai/dsh-brand": "^0.1.1-rc.1",
46
- "@deepseek-ai/dsh-invariants": "^0.1.1-rc.1",
47
- "@deepseek-ai/cordis": "^4.0.1"
45
+ "@deepseek-ai/dsh-brand": "^0.1.2-alpha.2",
46
+ "@deepseek-ai/cordis": "^4.0.2",
47
+ "@deepseek-ai/dsh-invariants": "^0.1.2-alpha.2"
48
48
  }
49
49
  }