dsh-plugin-dev-kb 1.0.9 → 1.1.0

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.
Files changed (120) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.en.md +6 -6
  3. package/README.md +6 -6
  4. package/kb/INDEX.md +4 -2
  5. package/kb/README.md +4 -4
  6. package/kb/extra/AGENTS.md +1 -1
  7. package/kb/extra/cookbook/adding-a-remote-api.md +197 -0
  8. package/kb/extra/cookbook/adding-a-remote-api.zh.md +197 -0
  9. package/kb/extra/deepseek-llm-api-wire-extensions.md +18 -14
  10. package/kb/extra/deepseek-llm-api-wire-extensions.zh.md +18 -14
  11. package/kb/extra/event-producer-consumer.md +44 -43
  12. package/kb/extra/event-producer-consumer.zh.md +47 -46
  13. package/kb/extra/i18n/style-samples.md +2 -2
  14. package/kb/extra/module-graph.md +444 -991
  15. package/kb/extra/module-graph.zh.md +446 -993
  16. package/kb/extra/subsystems/agent-team.md +4 -3
  17. package/kb/extra/subsystems/agent-team.zh.md +4 -3
  18. package/kb/extra/subsystems/attachment.md +157 -4
  19. package/kb/extra/subsystems/attachment.zh.md +157 -4
  20. package/kb/extra/subsystems/feedback.md +2 -2
  21. package/kb/extra/subsystems/feedback.zh.md +2 -2
  22. package/kb/extra/testing.md +6 -2
  23. package/kb/extra/testing.zh.md +6 -2
  24. package/kb/extra/web-styling.md +4 -0
  25. package/kb/extra/web-styling.zh.md +4 -0
  26. package/kb/meta/search-index.json +135 -111
  27. package/kb/meta/site-pages.txt +184 -182
  28. package/kb/meta/source.json +5 -5
  29. package/kb/meta/topics.md +4 -4
  30. package/kb/site/develop/cordis-tutorial/07-into-the-harness.md +3 -2
  31. package/kb/site/develop/practice/llm-adapter.md +4 -3
  32. package/kb/site/en/develop/cordis-tutorial/07-into-the-harness.md +3 -2
  33. package/kb/site/en/develop/practice/llm-adapter.md +4 -3
  34. package/kb/site/en/guide/network-proxy.md +87 -0
  35. package/kb/site/en/guide/providers.md +70 -17
  36. package/kb/site/en/guide/schedule.md +2 -0
  37. package/kb/site/en/reference/agent-lifecycle.md +5 -3
  38. package/kb/site/en/reference/api-gateway.md +2 -2
  39. package/kb/site/en/reference/capability-seams.md +14 -7
  40. package/kb/site/en/reference/config-catalog.md +157 -230
  41. package/kb/site/en/reference/cookbook/adding-a-package.md +1 -2
  42. package/kb/site/en/reference/cookbook/adding-a-settings-card.md +10 -8
  43. package/kb/site/en/reference/cookbook/extension-cookbook.md +15 -12
  44. package/kb/site/en/reference/index.md +11 -4
  45. package/kb/site/en/reference/persistence-catalog.md +91 -70
  46. package/kb/site/en/reference/subsystems/code-runtime.md +7 -3
  47. package/kb/site/en/reference/subsystems/commands.md +25 -16
  48. package/kb/site/en/reference/subsystems/compaction.md +9 -9
  49. package/kb/site/en/reference/subsystems/conversation.md +11 -4
  50. package/kb/site/en/reference/subsystems/core.md +96 -15
  51. package/kb/site/en/reference/subsystems/credentials.md +5 -4
  52. package/kb/site/en/reference/subsystems/goal.md +1 -1
  53. package/kb/site/en/reference/subsystems/index.md +1 -1
  54. package/kb/site/en/reference/subsystems/llm-streaming.md +24 -3
  55. package/kb/site/en/reference/subsystems/permission-presets.md +5 -5
  56. package/kb/site/en/reference/subsystems/persistence.md +185 -194
  57. package/kb/site/en/reference/subsystems/plan.md +3 -3
  58. package/kb/site/en/reference/subsystems/sandbox.md +2 -0
  59. package/kb/site/en/reference/subsystems/schedule.md +9 -3
  60. package/kb/site/en/reference/subsystems/session-projection.md +50 -24
  61. package/kb/site/en/reference/subsystems/session-query.md +19 -13
  62. package/kb/site/en/reference/subsystems/session-reference.md +25 -1
  63. package/kb/site/en/reference/subsystems/session-telemetry.md +6 -5
  64. package/kb/site/en/reference/subsystems/session-title.md +6 -6
  65. package/kb/site/en/reference/subsystems/session.md +156 -73
  66. package/kb/site/en/reference/subsystems/settings.md +30 -12
  67. package/kb/site/en/reference/subsystems/skills.md +1 -1
  68. package/kb/site/en/reference/subsystems/slots.md +1 -0
  69. package/kb/site/en/reference/subsystems/storage.md +28 -5
  70. package/kb/site/en/reference/subsystems/subagent.md +43 -97
  71. package/kb/site/en/reference/subsystems/system-prompt.md +16 -3
  72. package/kb/site/en/reference/subsystems/token-meter.md +2 -2
  73. package/kb/site/en/reference/subsystems/typert.md +18 -18
  74. package/kb/site/en/reference/subsystems/web-client.md +1 -1
  75. package/kb/site/en/reference/subsystems/workspace.md +8 -8
  76. package/kb/site/en/reference/tool-catalog.md +10 -64
  77. package/kb/site/guide/network-proxy.md +87 -0
  78. package/kb/site/guide/providers.md +70 -17
  79. package/kb/site/guide/schedule.md +2 -0
  80. package/kb/site/reference/agent-lifecycle.md +5 -3
  81. package/kb/site/reference/api-gateway.md +2 -2
  82. package/kb/site/reference/capability-seams.md +14 -7
  83. package/kb/site/reference/config-catalog.md +156 -229
  84. package/kb/site/reference/cookbook/adding-a-package.md +1 -2
  85. package/kb/site/reference/cookbook/adding-a-settings-card.md +10 -8
  86. package/kb/site/reference/cookbook/extension-cookbook.md +15 -12
  87. package/kb/site/reference/index.md +11 -4
  88. package/kb/site/reference/persistence-catalog.md +91 -70
  89. package/kb/site/reference/subsystems/code-runtime.md +7 -3
  90. package/kb/site/reference/subsystems/commands.md +25 -16
  91. package/kb/site/reference/subsystems/compaction.md +9 -9
  92. package/kb/site/reference/subsystems/conversation.md +11 -4
  93. package/kb/site/reference/subsystems/core.md +96 -15
  94. package/kb/site/reference/subsystems/credentials.md +5 -4
  95. package/kb/site/reference/subsystems/goal.md +1 -1
  96. package/kb/site/reference/subsystems/index.md +1 -1
  97. package/kb/site/reference/subsystems/llm-streaming.md +24 -3
  98. package/kb/site/reference/subsystems/permission-presets.md +5 -5
  99. package/kb/site/reference/subsystems/persistence.md +184 -193
  100. package/kb/site/reference/subsystems/plan.md +3 -3
  101. package/kb/site/reference/subsystems/schedule.md +9 -3
  102. package/kb/site/reference/subsystems/session-projection.md +50 -24
  103. package/kb/site/reference/subsystems/session-query.md +19 -13
  104. package/kb/site/reference/subsystems/session-reference.md +25 -1
  105. package/kb/site/reference/subsystems/session-telemetry.md +6 -5
  106. package/kb/site/reference/subsystems/session-title.md +6 -6
  107. package/kb/site/reference/subsystems/session.md +156 -73
  108. package/kb/site/reference/subsystems/settings.md +30 -12
  109. package/kb/site/reference/subsystems/skills.md +1 -1
  110. package/kb/site/reference/subsystems/slots.md +1 -0
  111. package/kb/site/reference/subsystems/storage.md +28 -5
  112. package/kb/site/reference/subsystems/subagent.md +43 -97
  113. package/kb/site/reference/subsystems/system-prompt.md +16 -3
  114. package/kb/site/reference/subsystems/token-meter.md +2 -2
  115. package/kb/site/reference/subsystems/typert.md +18 -18
  116. package/kb/site/reference/subsystems/web-client.md +1 -1
  117. package/kb/site/reference/subsystems/workspace.md +8 -8
  118. package/kb/site/reference/tool-catalog.md +9 -63
  119. package/package.json +2 -2
  120. package/skills/dsh-plugin-dev-kb.md +4 -4
@@ -2,7 +2,7 @@
2
2
 
3
3
  English | [中文](agent-team.zh.md)
4
4
 
5
- Types shared by the experimental implicit-root Team domain, model tools, and host adapters. The [Agent Teams Agent Note](../../.agents/notes/implemented/feature/2026-08-05-agent-teams.md) owns identity, mailbox, task, and shared-checkout decisions; this page records the literal durable forms from [`packages/experimental/agent-team/src/types.ts`](../../packages/experimental/agent-team/src/types.ts).
5
+ Types shared by the experimental implicit-root Team domain, model tools, and host adapters. The [Agent Teams Agent Note](../../.agents/notes/implemented/feature/2026-08-05-agent-teams.md) owns identity, mailbox, task, and shared-checkout decisions; the [Team Steer messaging Agent Note](../../.agents/notes/implemented/simplification/2026-08-30-team-send-message-steer.md) owns message scheduling; this page records the literal durable forms from [`packages/experimental/agent-team/src/types.ts`](../../packages/experimental/agent-team/src/types.ts).
6
6
 
7
7
  ## Identity and roster
8
8
 
@@ -34,11 +34,12 @@ interface TeamMessageSnapshot {
34
34
  readonly senderId: SessionId
35
35
  readonly senderName: string
36
36
  readonly targetId: SessionId
37
- readonly delivery: 'quiet' | 'wakeup'
38
37
  readonly content: ContentBlock[]
39
38
  }
40
39
  ```
41
40
 
41
+ Every message attempts Steer delivery. A running target receives it at the nearest step boundary, an idle target starts a turn, and an inactive teammate cold-resumes. Scheduling is not stored in the durable record because callers cannot select another mode.
42
+
42
43
  The target Session keeps message identity and sender attribution on both the pending inbox item and the eventual user message. Folding that source across inbox and history is the target-side de-duplication key; the model-visible framing repeats the id and sender.
43
44
 
44
45
  ```ts type-equiv
@@ -116,7 +117,7 @@ async spawnTeammate(caller: Agent, request: SpawnTeammateRequest): Promise<Spawn
116
117
  /**
117
118
  * Queue one durable peer message, then attempt immediate delivery.
118
119
  * @param caller - exact live sending Team member.
119
- * @param request - target name, content, scheduling mode, and pre-queue cancellation.
120
+ * @param request - target name, content, and pre-queue cancellation.
120
121
  * @returns durable message identity and immediate-delivery observation.
121
122
  */
122
123
  async sendMessage(caller: Agent, request: SendTeamMessageRequest): Promise<SendTeamMessageResult>
@@ -2,7 +2,7 @@
2
2
 
3
3
  [English](agent-team.md) | 中文
4
4
 
5
- 实验性隐式 Root Team 领域、模型工具与宿主适配器共享的类型。[Agent Teams Agent Note](../../.agents/notes/implemented/feature/2026-08-05-agent-teams.zh.md)负责身份、mailbox、task 与共享 checkout 决策;本页记录 [`packages/experimental/agent-team/src/types.ts`](../../packages/experimental/agent-team/src/types.ts) 中的字面持久形式。
5
+ 实验性隐式 Root Team 领域、模型工具与宿主适配器共享的类型。[Agent Teams Agent Note](../../.agents/notes/implemented/feature/2026-08-05-agent-teams.zh.md)负责身份、mailbox、task 与共享 checkout 决策;[Team Steer 消息 Agent Note](../../.agents/notes/implemented/simplification/2026-08-30-team-send-message-steer.zh.md)负责消息调度;本页记录 [`packages/experimental/agent-team/src/types.ts`](../../packages/experimental/agent-team/src/types.ts) 中的字面持久形式。
6
6
 
7
7
  ## 身份与 roster
8
8
 
@@ -34,11 +34,12 @@ interface TeamMessageSnapshot {
34
34
  readonly senderId: SessionId
35
35
  readonly senderName: string
36
36
  readonly targetId: SessionId
37
- readonly delivery: 'quiet' | 'wakeup'
38
37
  readonly content: ContentBlock[]
39
38
  }
40
39
  ```
41
40
 
41
+ 每条消息都会尝试 Steer 投递。running target 在最近的步骤边界收到消息,idle target 启动一个轮次,inactive teammate 则冷恢复。调用方不能选择其他模式,因此持久记录不存储调度方式。
42
+
42
43
  target Session 会在 pending inbox 条目和最终用户消息上保留消息身份与发送者归因。跨 inbox 与历史折叠该 source 构成 target 侧去重键;模型可见的 framing 会重复 id 和发送者。
43
44
 
44
45
  ```ts type-equiv
@@ -116,7 +117,7 @@ async spawnTeammate(caller: Agent, request: SpawnTeammateRequest): Promise<Spawn
116
117
  /**
117
118
  * Queue one durable peer message, then attempt immediate delivery.
118
119
  * @param caller - exact live sending Team member.
119
- * @param request - target name, content, scheduling mode, and pre-queue cancellation.
120
+ * @param request - target name, content, and pre-queue cancellation.
120
121
  * @returns durable message identity and immediate-delivery observation.
121
122
  */
122
123
  async sendMessage(caller: Agent, request: SendTeamMessageRequest): Promise<SendTeamMessageResult>
@@ -1,10 +1,10 @@
1
- # Durable Image Attachments
1
+ # Durable Attachments
2
2
 
3
3
  English | [中文](attachment.zh.md)
4
4
 
5
- The attachment seam separates binary image ownership from the session log. A producer gives validated encoded bytes to [`ctx.attachments`](#ctxattachments--attachmentstore-abstract-seam); the service publishes an immutable content-addressed reference only after the object is durable. Session events and model-visible `ImageBlock`s contain that reference and metadata, never a browser object URL, host temporary path, provider URL, or base64 payload.
5
+ The attachment seam separates binary image and generic-file ownership from the session log. A producer gives bytes to [`ctx.attachments`](#ctxattachments--attachmentstore-abstract-seam); the service publishes an immutable content-addressed reference only after the object is durable. Session events and model-visible attachment blocks contain that reference and metadata, never a browser object URL, host temporary path, provider URL, or base64 payload. The independent [`ctx.fileUploads`](#ctxfileuploads--fileuploads) service binds browser file transfers and staged receipts to the receiving Agent.
6
6
 
7
- Unsent browser drafts may stay in memory and native clients may stage them in operating-system temporary storage. Once the host accepts a user message, its images move below `<DSH_HOME>/attachments/v1` before the user event is appended. Structured model image output follows the same persist-before-event rule.
7
+ Unsent browser drafts may stay in memory and native clients may stage them in operating-system temporary storage. Browser generic files become durable before they receive a staged prompt receipt. Once the host accepts a user message, its images move below `<DSH_HOME>/attachments/v1` before the user event is appended. Structured model image output follows the same persist-before-event rule.
8
8
 
9
9
  Source: [`packages/attachment/attachment/src/types.ts`](../../packages/attachment/attachment/src/types.ts)
10
10
 
@@ -62,6 +62,38 @@ The reference records intrinsic dimensions and encoded length so clients can lay
62
62
 
63
63
  ## Commit and verified-read payloads
64
64
 
65
+ ```ts type-equiv
66
+ /**
67
+ * Browser-submitted prompt content accepted by Host prompt endpoints; the
68
+ * accepting Host promotes image parts to durable references through
69
+ * `ctx.attachments.admitPromptContent()` before any message is created, so a wire caller can
70
+ * never cite an attachment it did not upload.
71
+ */
72
+ type PromptContentPart =
73
+ | { readonly type: 'text'; readonly text: string }
74
+ | {
75
+ readonly type: 'image'
76
+ readonly mediaType: ImageMediaType
77
+ readonly data: string
78
+ readonly name?: string
79
+ }
80
+ ```
81
+
82
+ ```ts type-equiv
83
+ /** Host prompt content whose file receipts are resolved and whose image bytes await admission. */
84
+ type AttachmentAdmissionPart =
85
+ | PromptContentPart
86
+ | { readonly type: 'file'; readonly attachment: FileAttachmentRef }
87
+ ```
88
+
89
+ ```ts type-equiv
90
+ /** Host-admitted prompt content with every attachment represented by its durable reference. */
91
+ type AdmittedPromptContentPart =
92
+ | { readonly type: 'text'; readonly text: string }
93
+ | { readonly type: 'image'; readonly attachment: ImageAttachmentRef }
94
+ | { readonly type: 'file'; readonly attachment: FileAttachmentRef }
95
+ ```
96
+
65
97
  ```ts type-equiv
66
98
  /** Base64-encoded image upload accompanying one wire request. */
67
99
  interface EncodedImageAttachment {
@@ -125,7 +157,7 @@ interface RequestImageAttachment {
125
157
  }
126
158
  ```
127
159
 
128
- `saveImage()` prepares and atomically commits a provider-independent normalized attachment before returning its `ImageAttachmentRef`. `saveImages()` prepares every validated attachment once before publishing the batch, so validation rejection leaves no partial objects and publication does not repeat decoding or quality selection. `admitEncodedImages()` is the wire entry for base64 uploads and delegates count, aggregate-byte, and ordered batch admission to `saveImages()`. `readImage()` verifies a normalized attachment from an authorized session path. `imageHostPath()` exposes only the provider-owned host object location; it does not decide whether the current tool execution world can read it. `readImageRequest()` derives and caches one deterministic request version under an exact route pixel and byte budget. That version contains encoded bytes and metadata but no execution-world path. New entries are fully decoded before publication, while cache hits use a bounded metadata probe. Callers use `Promise.all` over the singular method when they need an ordered batch. The local implementation lazily encodes preferred candidates, singleflights equal request identities, lets each waiter cancel independently, stops shared work when no waiter remains, and bounds all transforms with its instance-level limiter, which defaults to two simultaneous transformations. The service is retention-neutral: resumed and forked sessions may share objects, so reference-aware garbage collection is deferred rather than tied to one session's deletion.
160
+ `saveImage()` prepares and atomically commits a provider-independent normalized attachment before returning its `ImageAttachmentRef`. `saveImages()` prepares every validated attachment once before publishing the batch, so validation rejection leaves no partial objects and publication does not repeat decoding or quality selection. `admitPromptContent()` accepts the complete ordered Host prompt after file receipt resolution, replaces base64 image uploads with durable references, and passes durable file references unchanged. `admitEncodedImages()` supports other wire entries and delegates count, aggregate-byte, and ordered batch admission to `saveImages()`. `admitEncodedFile()` gives encoded protocol adapters the same service-owned canonical-base64 admission, and `isAttachmentError()` lets those adapters recognize stable attachment failures without importing implementation helpers. `readImage()` verifies a normalized attachment from an authorized session path. `imageHostPath()` exposes only the provider-owned host object location; it does not decide whether the current tool execution world can read it. `readImageRequest()` derives and caches one deterministic request version under an exact route pixel and byte budget. That version contains encoded bytes and metadata but no execution-world path. New entries are fully decoded before publication, while cache hits use a bounded metadata probe. Callers use `Promise.all` over the singular method when they need an ordered batch. The local implementation lazily encodes preferred candidates, singleflights equal request identities, lets each waiter cancel independently, stops shared work when no waiter remains, and bounds all transforms with its instance-level limiter, which defaults to two simultaneous transformations. The service is retention-neutral: resumed and forked sessions may share objects, so reference-aware garbage collection is deferred rather than tied to one session's deletion.
129
161
 
130
162
  <!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
131
163
 
@@ -157,6 +189,30 @@ abstract validateImage(input: SaveImageAttachment): Promise<void>
157
189
  */
158
190
  async saveImages(inputs: readonly SaveImageAttachment[]): Promise<readonly ImageAttachmentRef[]>
159
191
 
192
+ /**
193
+ * Admit one Host prompt and replace each uploaded image with its durable reference.
194
+ * Text and durable file references pass through unchanged. A prompt without image parts performs no storage operation.
195
+ * @param content - prompt parts in message order after file receipt resolution.
196
+ * @returns admitted prompt parts in the same order as `content`.
197
+ * @throws AttachmentError when the image batch is refused.
198
+ */
199
+ async admitPromptContent( content: readonly AttachmentAdmissionPart[], ): Promise<AdmittedPromptContentPart[]>
200
+
201
+ /**
202
+ * Decode and durably commit one canonical base64 file upload.
203
+ * @param input - canonical base64 bytes and optional display name.
204
+ * @returns the durable content-addressed file reference.
205
+ * @throws AttachmentError when the encoding or storage operation is refused.
206
+ */
207
+ admitEncodedFile(input: EncodedFileAttachment): Promise<FileAttachmentRef>
208
+
209
+ /**
210
+ * Identify a failure emitted by this attachment capability by its stable code.
211
+ * @param error - value caught from an attachment operation.
212
+ * @returns whether the value is an attachment failure.
213
+ */
214
+ isAttachmentError(error: unknown): error is AttachmentError
215
+
160
216
  /**
161
217
  * Validate and durably commit one image before its owning session event is appended.
162
218
  * The returned reference describes the persisted normalized image. When
@@ -184,6 +240,43 @@ abstract readImage(ref: ImageAttachmentRef, signal?: AbortSignal): Promise<Store
184
240
  */
185
241
  imageHostPath(ref: ImageAttachmentRef): string | undefined
186
242
 
243
+ /**
244
+ * Durably commit one file byte-for-byte before its owning session event is
245
+ * appended. Files carry no admission limits: any byte content and length is
246
+ * accepted, and the stored object is the exact submitted bytes. Backends
247
+ * without verbatim file storage keep this default rejection.
248
+ * @param input - exact bytes and optional display name.
249
+ * @returns the durable content-addressed file reference.
250
+ */
251
+ saveFile(input: SaveFileAttachment): Promise<FileAttachmentRef>
252
+
253
+ /**
254
+ * Durably commit one file byte-for-byte from bounded chunks. Providers must
255
+ * apply backpressure and must not collect the complete file in memory.
256
+ * Backends without streamed verbatim storage keep this default rejection.
257
+ * @param input - ordered exact bytes, optional cancellation, and display name.
258
+ * @returns the durable content-addressed file reference.
259
+ */
260
+ saveFileStream(input: SaveFileStreamAttachment): Promise<FileAttachmentRef>
261
+
262
+ /**
263
+ * Read and verify one verbatim stored file as bounded chunks. Providers must
264
+ * not collect the complete file in memory. Backends without verbatim file
265
+ * reads keep this default rejection.
266
+ * @param ref - durable reference from the session log.
267
+ * @param signal - optional cancellation for backend reads and verification work.
268
+ * @returns exact file bytes in order; integrity failures reject the iteration.
269
+ */
270
+ async *readFileStream( ref: FileAttachmentRef, signal?: AbortSignal, ): AsyncIterable<Uint8Array>
271
+
272
+ /**
273
+ * Locate the verbatim stored file object in the harness host filesystem.
274
+ * @param ref - durable file reference.
275
+ * @returns an absolute host path, or undefined when this backend is not host-file-backed.
276
+ * @throws an AttachmentError when the durable reference is invalid.
277
+ */
278
+ fileHostPath(ref: FileAttachmentRef): string | undefined
279
+
187
280
  /**
188
281
  * Generate or read one deterministic model-request version from the stored normalized image.
189
282
  * @param ref - durable provider-independent normalized attachment reference.
@@ -195,4 +288,64 @@ readImageRequest( ref: ImageAttachmentRef, policy: ImageRequestPolicy, signal?:
195
288
  ```
196
289
 
197
290
  Source: [`packages/attachment/attachment/src/index.ts`](../../packages/attachment/attachment/src/index.ts)
291
+
292
+ <a id="ctxfileuploads--fileuploads"></a>
293
+
294
+ ### `ctx.fileUploads` — `FileUploads`
295
+
296
+ Host service owning upload storage and Agent-scoped staged receipts.
297
+
298
+ ```ts cordis-catalog
299
+ /**
300
+ * Register the ordinary-Session resolver used when a raw upload addresses a cold Session.
301
+ * @param resolve - resolver that returns the exact live Agent or throws a Remote error.
302
+ * @returns disposer removing this resolver.
303
+ */
304
+ registerAgentResolver(resolve: AgentResolver): () => void
305
+
306
+ /**
307
+ * Persist one encoded upload and stage it under the Agent receiver selected by Typert.
308
+ * @param agent - receiving Agent resolved from the Remote Agent scope.
309
+ * @param request - canonical base64 bytes and optional display name.
310
+ * @param signal - caller cancellation before storage begins.
311
+ * @returns the staged receipt and durable file reference.
312
+ */
313
+ @Remote('upload') upload(agent: Agent, request: EncodedFileUploadRequest, signal: AbortSignal): Promise<FileUploadValue>
314
+
315
+ /**
316
+ * Persist raw chunks for one Session without aggregating the upload.
317
+ * @param request - Session identity, ordered bytes, cancellation, and optional display name.
318
+ * @returns the staged receipt and durable file reference.
319
+ */
320
+ async uploadStream(request: { readonly sessionId: SessionId readonly data: AsyncIterable<Uint8Array> readonly signal?: AbortSignal readonly name?: string }): Promise<FileUploadValue>
321
+
322
+ /**
323
+ * Resolve one staged receipt inside its receiving Agent scope.
324
+ * @param agent - receiving Agent.
325
+ * @param receiptId - opaque receipt minted for one completed upload.
326
+ * @returns durable file reference, or `undefined` for an unknown or foreign receipt.
327
+ */
328
+ resolve(agent: Agent, receiptId: FileUploadReceiptId): FileAttachmentRef | undefined
329
+
330
+ /**
331
+ * Bind receipts while one prompt enters an Agent inbox.
332
+ * Disposal restores every prior binding unless the caller commits successful delivery.
333
+ * @param agent - receiving Agent.
334
+ * @param receiptIds - distinct staged receipts referenced by the prompt.
335
+ * @param requestId - prompt identity later observed in queue or history.
336
+ * @returns binding kept after commit until queue or history observation retires its receipts.
337
+ */
338
+ bindPrompt( agent: Agent, receiptIds: readonly FileUploadReceiptId[], requestId: string, ): PromptFileBinding
339
+
340
+ /**
341
+ * Retire every receipt accepted by one removed queue occurrence.
342
+ * @param agent - receiving Agent.
343
+ * @param requestId - prompt identity carried by the queue occurrence.
344
+ */
345
+ retirePrompt(agent: Agent, requestId: string): void
346
+ ```
347
+
348
+ Types: [Agent](core.md) · [SessionId](core.md)
349
+
350
+ Source: [`packages/client/file-upload/src/index.ts`](../../packages/client/file-upload/src/index.ts)
198
351
  <!-- END GENERATED cordis-surface -->
@@ -1,10 +1,10 @@
1
- # 持久图片附件
1
+ # 持久附件
2
2
 
3
3
  [English](attachment.md) | 中文
4
4
 
5
- 附件 seam 将二进制图片的所有权与会话日志分离。生产方把经过校验的编码字节交给 [`ctx.attachments`](#ctxattachments--attachmentstore-abstract-seam);只有对象完成持久化后,该服务才会发布不可变的内容寻址引用。会话事件和模型可见的 `ImageBlock` 包含该引用及其元数据,绝不包含浏览器对象 URL、宿主临时路径、提供方 URL 或 base64 数据。
5
+ 附件 seam 将二进制图片和通用文件的所有权与会话日志分离。生产方把字节交给 [`ctx.attachments`](#ctxattachments--attachmentstore-abstract-seam);只有对象完成持久化后,该服务才会发布不可变的内容寻址引用。会话事件和模型可见的附件块包含该引用及其元数据,绝不包含浏览器对象 URL、宿主临时路径、提供方 URL 或 base64 数据。独立的 [`ctx.fileUploads`](#ctxfileuploads--fileuploads) 服务把浏览器文件传输与暂存凭证绑定到接收方 Agent。
6
6
 
7
- 未发送的浏览器草稿可以保留在内存中,原生客户端也可以将其暂存于操作系统临时存储。宿主接受用户消息后,会先把消息中的图片移到 `<DSH_HOME>/attachments/v1` 下,再追加用户事件。结构化模型图片输出遵循同样的先持久化、后追加事件规则。
7
+ 未发送的浏览器草稿可以保留在内存中,原生客户端也可以将其暂存于操作系统临时存储。浏览器通用文件取得暂存 prompt 凭证前会完成持久化。宿主接受用户消息后,会先把消息中的图片移到 `<DSH_HOME>/attachments/v1` 下,再追加用户事件。结构化模型图片输出遵循同样的先持久化、后追加事件规则。
8
8
 
9
9
  来源:[`packages/attachment/attachment/src/types.ts`](../../packages/attachment/attachment/src/types.ts)
10
10
 
@@ -62,6 +62,38 @@ interface ImageAttachmentLimits {
62
62
 
63
63
  ## 提交与经校验读取的数据
64
64
 
65
+ ```ts type-equiv
66
+ /**
67
+ * Browser-submitted prompt content accepted by Host prompt endpoints; the
68
+ * accepting Host promotes image parts to durable references through
69
+ * `ctx.attachments.admitPromptContent()` before any message is created, so a wire caller can
70
+ * never cite an attachment it did not upload.
71
+ */
72
+ type PromptContentPart =
73
+ | { readonly type: 'text'; readonly text: string }
74
+ | {
75
+ readonly type: 'image'
76
+ readonly mediaType: ImageMediaType
77
+ readonly data: string
78
+ readonly name?: string
79
+ }
80
+ ```
81
+
82
+ ```ts type-equiv
83
+ /** Host prompt content whose file receipts are resolved and whose image bytes await admission. */
84
+ type AttachmentAdmissionPart =
85
+ | PromptContentPart
86
+ | { readonly type: 'file'; readonly attachment: FileAttachmentRef }
87
+ ```
88
+
89
+ ```ts type-equiv
90
+ /** Host-admitted prompt content with every attachment represented by its durable reference. */
91
+ type AdmittedPromptContentPart =
92
+ | { readonly type: 'text'; readonly text: string }
93
+ | { readonly type: 'image'; readonly attachment: ImageAttachmentRef }
94
+ | { readonly type: 'file'; readonly attachment: FileAttachmentRef }
95
+ ```
96
+
65
97
  ```ts type-equiv
66
98
  /** Base64-encoded image upload accompanying one wire request. */
67
99
  interface EncodedImageAttachment {
@@ -125,7 +157,7 @@ interface RequestImageAttachment {
125
157
  }
126
158
  ```
127
159
 
128
- `saveImage()` 准备并原子提交提供方无关的规范化附件,然后直接返回 `ImageAttachmentRef`。`saveImages()` 在发布批次前为每个成员各准备一次经过验证的附件,因此校验拒绝不会留下部分对象,发布也不会重复解码或选择质量。`admitEncodedImages()` 是面向 base64 上传的 wire 入口,把张数、聚合字节和有序批量准入交给 `saveImages()`。`readImage()` 校验来自已授权会话路径的规范化附件。`imageHostPath()` 只公开提供方所持对象的宿主位置,不判断当前工具执行环境能否读取它。`readImageRequest()` 按确切路由的像素和字节预算派生并缓存确定性请求版本。该版本包含编码字节和元数据,不包含执行环境路径。新条目在发布前完整解码,缓存命中只做有界元数据探测。调用方需要有序批次时,对单数方法使用 `Promise.all`。本地实现按需编码首选候选、合并相同请求身份的并发任务、允许每个等待方单独取消、没有等待方时停止共享任务,并通过实例级限流器限制全部变换,默认同时执行两项。该服务不规定保留策略:恢复和 fork 后的会话可能共享对象,因此基于引用的垃圾回收会延期实现,不与单个会话的删除绑定。
160
+ `saveImage()` 准备并原子提交提供方无关的规范化附件,然后直接返回 `ImageAttachmentRef`。`saveImages()` 在发布批次前为每个成员各准备一次经过验证的附件,因此校验拒绝不会留下部分对象,发布也不会重复解码或选择质量。`admitPromptContent()` 在文件凭证解析后接收完整且有序的 Host prompt,把 base64 图片上传替换为持久引用,并让持久文件引用原样通过。`admitEncodedImages()` 支持其他 wire 入口,把张数、聚合字节和有序批量准入交给 `saveImages()`。`admitEncodedFile()` 让编码协议适配器使用服务拥有的规范 base64 准入,`isAttachmentError()` 让这些适配器无需导入实现辅助函数即可识别稳定的附件错误。`readImage()` 校验来自已授权会话路径的规范化附件。`imageHostPath()` 只公开提供方所持对象的宿主位置,不判断当前工具执行环境能否读取它。`readImageRequest()` 按确切路由的像素和字节预算派生并缓存确定性请求版本。该版本包含编码字节和元数据,不包含执行环境路径。新条目在发布前完整解码,缓存命中只做有界元数据探测。调用方需要有序批次时,对单数方法使用 `Promise.all`。本地实现按需编码首选候选、合并相同请求身份的并发任务、允许每个等待方单独取消、没有等待方时停止共享任务,并通过实例级限流器限制全部变换,默认同时执行两项。该服务不规定保留策略:恢复和 fork 后的会话可能共享对象,因此基于引用的垃圾回收会延期实现,不与单个会话的删除绑定。
129
161
 
130
162
  <!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
131
163
 
@@ -157,6 +189,30 @@ abstract validateImage(input: SaveImageAttachment): Promise<void>
157
189
  */
158
190
  async saveImages(inputs: readonly SaveImageAttachment[]): Promise<readonly ImageAttachmentRef[]>
159
191
 
192
+ /**
193
+ * Admit one Host prompt and replace each uploaded image with its durable reference.
194
+ * Text and durable file references pass through unchanged. A prompt without image parts performs no storage operation.
195
+ * @param content - prompt parts in message order after file receipt resolution.
196
+ * @returns admitted prompt parts in the same order as `content`.
197
+ * @throws AttachmentError when the image batch is refused.
198
+ */
199
+ async admitPromptContent( content: readonly AttachmentAdmissionPart[], ): Promise<AdmittedPromptContentPart[]>
200
+
201
+ /**
202
+ * Decode and durably commit one canonical base64 file upload.
203
+ * @param input - canonical base64 bytes and optional display name.
204
+ * @returns the durable content-addressed file reference.
205
+ * @throws AttachmentError when the encoding or storage operation is refused.
206
+ */
207
+ admitEncodedFile(input: EncodedFileAttachment): Promise<FileAttachmentRef>
208
+
209
+ /**
210
+ * Identify a failure emitted by this attachment capability by its stable code.
211
+ * @param error - value caught from an attachment operation.
212
+ * @returns whether the value is an attachment failure.
213
+ */
214
+ isAttachmentError(error: unknown): error is AttachmentError
215
+
160
216
  /**
161
217
  * Validate and durably commit one image before its owning session event is appended.
162
218
  * The returned reference describes the persisted normalized image. When
@@ -184,6 +240,43 @@ abstract readImage(ref: ImageAttachmentRef, signal?: AbortSignal): Promise<Store
184
240
  */
185
241
  imageHostPath(ref: ImageAttachmentRef): string | undefined
186
242
 
243
+ /**
244
+ * Durably commit one file byte-for-byte before its owning session event is
245
+ * appended. Files carry no admission limits: any byte content and length is
246
+ * accepted, and the stored object is the exact submitted bytes. Backends
247
+ * without verbatim file storage keep this default rejection.
248
+ * @param input - exact bytes and optional display name.
249
+ * @returns the durable content-addressed file reference.
250
+ */
251
+ saveFile(input: SaveFileAttachment): Promise<FileAttachmentRef>
252
+
253
+ /**
254
+ * Durably commit one file byte-for-byte from bounded chunks. Providers must
255
+ * apply backpressure and must not collect the complete file in memory.
256
+ * Backends without streamed verbatim storage keep this default rejection.
257
+ * @param input - ordered exact bytes, optional cancellation, and display name.
258
+ * @returns the durable content-addressed file reference.
259
+ */
260
+ saveFileStream(input: SaveFileStreamAttachment): Promise<FileAttachmentRef>
261
+
262
+ /**
263
+ * Read and verify one verbatim stored file as bounded chunks. Providers must
264
+ * not collect the complete file in memory. Backends without verbatim file
265
+ * reads keep this default rejection.
266
+ * @param ref - durable reference from the session log.
267
+ * @param signal - optional cancellation for backend reads and verification work.
268
+ * @returns exact file bytes in order; integrity failures reject the iteration.
269
+ */
270
+ async *readFileStream( ref: FileAttachmentRef, signal?: AbortSignal, ): AsyncIterable<Uint8Array>
271
+
272
+ /**
273
+ * Locate the verbatim stored file object in the harness host filesystem.
274
+ * @param ref - durable file reference.
275
+ * @returns an absolute host path, or undefined when this backend is not host-file-backed.
276
+ * @throws an AttachmentError when the durable reference is invalid.
277
+ */
278
+ fileHostPath(ref: FileAttachmentRef): string | undefined
279
+
187
280
  /**
188
281
  * Generate or read one deterministic model-request version from the stored normalized image.
189
282
  * @param ref - durable provider-independent normalized attachment reference.
@@ -195,4 +288,64 @@ readImageRequest( ref: ImageAttachmentRef, policy: ImageRequestPolicy, signal?:
195
288
  ```
196
289
 
197
290
  Source: [`packages/attachment/attachment/src/index.ts`](../../packages/attachment/attachment/src/index.ts)
291
+
292
+ <a id="ctxfileuploads--fileuploads"></a>
293
+
294
+ ### `ctx.fileUploads` — `FileUploads`
295
+
296
+ Host service owning upload storage and Agent-scoped staged receipts.
297
+
298
+ ```ts cordis-catalog
299
+ /**
300
+ * Register the ordinary-Session resolver used when a raw upload addresses a cold Session.
301
+ * @param resolve - resolver that returns the exact live Agent or throws a Remote error.
302
+ * @returns disposer removing this resolver.
303
+ */
304
+ registerAgentResolver(resolve: AgentResolver): () => void
305
+
306
+ /**
307
+ * Persist one encoded upload and stage it under the Agent receiver selected by Typert.
308
+ * @param agent - receiving Agent resolved from the Remote Agent scope.
309
+ * @param request - canonical base64 bytes and optional display name.
310
+ * @param signal - caller cancellation before storage begins.
311
+ * @returns the staged receipt and durable file reference.
312
+ */
313
+ @Remote('upload') upload(agent: Agent, request: EncodedFileUploadRequest, signal: AbortSignal): Promise<FileUploadValue>
314
+
315
+ /**
316
+ * Persist raw chunks for one Session without aggregating the upload.
317
+ * @param request - Session identity, ordered bytes, cancellation, and optional display name.
318
+ * @returns the staged receipt and durable file reference.
319
+ */
320
+ async uploadStream(request: { readonly sessionId: SessionId readonly data: AsyncIterable<Uint8Array> readonly signal?: AbortSignal readonly name?: string }): Promise<FileUploadValue>
321
+
322
+ /**
323
+ * Resolve one staged receipt inside its receiving Agent scope.
324
+ * @param agent - receiving Agent.
325
+ * @param receiptId - opaque receipt minted for one completed upload.
326
+ * @returns durable file reference, or `undefined` for an unknown or foreign receipt.
327
+ */
328
+ resolve(agent: Agent, receiptId: FileUploadReceiptId): FileAttachmentRef | undefined
329
+
330
+ /**
331
+ * Bind receipts while one prompt enters an Agent inbox.
332
+ * Disposal restores every prior binding unless the caller commits successful delivery.
333
+ * @param agent - receiving Agent.
334
+ * @param receiptIds - distinct staged receipts referenced by the prompt.
335
+ * @param requestId - prompt identity later observed in queue or history.
336
+ * @returns binding kept after commit until queue or history observation retires its receipts.
337
+ */
338
+ bindPrompt( agent: Agent, receiptIds: readonly FileUploadReceiptId[], requestId: string, ): PromptFileBinding
339
+
340
+ /**
341
+ * Retire every receipt accepted by one removed queue occurrence.
342
+ * @param agent - receiving Agent.
343
+ * @param requestId - prompt identity carried by the queue occurrence.
344
+ */
345
+ retirePrompt(agent: Agent, requestId: string): void
346
+ ```
347
+
348
+ Types: [Agent](core.zh.md) · [SessionId](core.zh.md)
349
+
350
+ Source: [`packages/client/file-upload/src/index.ts`](../../packages/client/file-upload/src/index.ts)
198
351
  <!-- END GENERATED cordis-surface -->
@@ -191,13 +191,13 @@ One Session sidecar row contains its header identity `{createdAt, cwd}` and feed
191
191
 
192
192
  ## Target and lifecycle authority
193
193
 
194
- `SessionPersistence.inspect()` supplies the target Session observation without publishing or resuming an Agent and without committing cold repair. A cold `listSnapshots()` preflight classifies definite absence; inspection failure for a catalogued Session propagates as infrastructure failure. `put` accepts only a non-empty, append-origin `assistant/message` with the requested `MessageId`; replacement-origin, usage-only empty, and non-assistant records are not feedback targets.
194
+ A live owner's in-memory log supplies the target Session observation directly; a cold target is read through a `SessionPersistence.open(id, 'read')` handle without publishing or resuming an Agent and without writing recovery. A `stat(id)` preflight classifies definite absence; a read failure for a Session `stat` confirmed propagates as infrastructure failure. `put` accepts only a non-empty, append-origin `assistant/message` with the requested `MessageId`; replacement-origin, usage-only empty, and non-assistant records are not feedback targets.
195
195
 
196
196
  The stored `{createdAt, cwd}` identity must match the inspected header. A mismatch is treated as absence: `list` returns no items, while `put` may replace the stale row with one bound to the current header identity. Forks use a new Session identity and receive no sidecar copy even when their seed contains the same messages.
197
197
 
198
198
  ## Persistence and Remote contract
199
199
 
200
- The service stores whole Session rows in the `message_feedback` storage domain through `ctx.storageDomain`. Before `put` commits a row that references a target message, a matching live target passes through the canonical `ctx.sessions.flush` checkpoint; both live and cold paths are then physically read from sequence zero through `SessionPersistence.readFrom`. The resulting observation is revalidated before the sidecar write, so the durable target log always precedes its sidecar commit. `maxNoteBytes` is required and bounds note text by UTF-8 bytes; the Web Host composition sets `8192`. The package publishes the Host `messageFeedback.list`, `messageFeedback.put`, and `messageFeedback.delete` unary Remote contract through `TypertRemoteService` and `@Remote`; the generated Cordis API below is the method-level authority.
200
+ The service stores whole Session rows in the `message_feedback` storage domain through `ctx.storageDomain`. Before `put` commits a row that references a target message, a matching live target passes through the canonical `ctx.sessions.flush` checkpoint; both live and cold paths are then physically read from sequence zero through a fresh read handle, which observes at least the flushed prefix by the seam's freshness guarantee. The resulting observation is revalidated before the sidecar write, so the durable target log always precedes its sidecar commit. `maxNoteBytes` is required and bounds note text by UTF-8 bytes; the Web Host composition sets `8192`. The package publishes the Host `messageFeedback.list`, `messageFeedback.put`, and `messageFeedback.delete` unary Remote contract through `TypertRemoteService` and `@Remote`; the generated Cordis API below is the method-level authority.
201
201
 
202
202
  Plugin disposal closes mutation admission, drains accepted per-Session queue work, and then closes the storage domain.
203
203
 
@@ -191,13 +191,13 @@ type MessageFeedbackDeleteResult =
191
191
 
192
192
  ## 目标与生命周期权威
193
193
 
194
- `SessionPersistence.inspect()` 提供目标 Session 的观测,且不会发布或恢复 Agent,也不会提交 cold repair。cold 路径先由 `listSnapshots()` 预检明确不存在;已进入目录的 Session 若检查失败,会按基础设施故障原样传播。`put` 只接受具有指定 `MessageId` 的非空、append-origin `assistant/message`;replacement-origin、仅承载 usage 的空记录和非 assistant 记录都不是反馈目标。
194
+ live 持有者的内存日志直接提供目标 Session 的观测;cold 目标则通过 `SessionPersistence.open(id, 'read')` 句柄读取,既不会发布或恢复 Agent,也不会写入恢复内容。先由 `stat(id)` 预检明确不存在;`stat` 已确认存在的 Session 若读取失败,会按基础设施故障原样传播。`put` 只接受具有指定 `MessageId` 的非空、append-origin `assistant/message`;replacement-origin、仅承载 usage 的空记录和非 assistant 记录都不是反馈目标。
195
195
 
196
196
  存储的 `{createdAt, cwd}` 身份必须与检查所得 header 匹配。不匹配按不存在处理:`list` 返回空条目,`put` 则可用绑定当前 header 身份的新记录替换陈旧行。fork 使用新的 Session 身份,即使种子包含相同消息,也不获得伴随记录副本。
197
197
 
198
198
  ## 持久化与 Remote 约定
199
199
 
200
- 服务通过 `ctx.storageDomain` 在 `message_feedback` 存储域中保存完整 Session 行。`put` 提交引用目标消息的伴随记录前,身份匹配的 live 目标先经过权威 `ctx.sessions.flush` checkpoint;随后 live 与 cold 路径都会通过 `SessionPersistence.readFrom` 从序列零做物理复读。写入伴随记录前会再次校验所得观测,因此目标日志的持久提交始终先于其伴随记录。`maxNoteBytes` 为必填项,按 UTF-8 字节限制备注文本;Web Host 组合将其设为 `8192`。该包通过 `TypertRemoteService` 与 `@Remote` 发布 Host `messageFeedback.list`、`messageFeedback.put` 和 `messageFeedback.delete` 一元 Remote 约定;下方生成的 Cordis API 是方法级权威。
200
+ 服务通过 `ctx.storageDomain` 在 `message_feedback` 存储域中保存完整 Session 行。`put` 提交引用目标消息的伴随记录前,身份匹配的 live 目标先经过权威 `ctx.sessions.flush` checkpoint;随后 live 与 cold 路径都会通过一个新开的读句柄从序列零做物理复读,依据该 seam 的新鲜度保证,它至少能观察到已 flush 的前缀。写入伴随记录前会再次校验所得观测,因此目标日志的持久提交始终先于其伴随记录。`maxNoteBytes` 为必填项,按 UTF-8 字节限制备注文本;Web Host 组合将其设为 `8192`。该包通过 `TypertRemoteService` 与 `@Remote` 发布 Host `messageFeedback.list`、`messageFeedback.put` 和 `messageFeedback.delete` 一元 Remote 约定;下方生成的 Cordis API 是方法级权威。
201
201
 
202
202
  Plugin disposal 会先关闭变更接纳,排空已进入各 Session 队列的工作,然后才关闭 storage domain。
203
203
 
@@ -10,10 +10,14 @@ How this repo tests, tier by tier, and the rules that keep a green suite meaning
10
10
  - **Coverage gate** (`pnpm run test:coverage`): the gating run, per-file 100% on `packages/*/*/src`. An uncovered line is often dead code the gate flags for deletion, not a missing test to bolt on. Line coverage is necessary, never sufficient — it proves lines ran, not that the feature works as shipped. Per-file 100% on `packages/shell/pwsh-local/src` needs a real `pwsh`: without one its executor suites self-skip and `vitest.config.ts` exempts the file so pwsh-less hosts stay green, while CI runners ship pwsh and enforce the full bar.
11
11
  - **Real-API e2e** (`pnpm run test:e2e`): with-key tests against live provider APIs — the DeepSeek model plus provider-specific smokes that gate on their own keys (`EXA_API_KEY`, `PERPLEXITY_API_KEY`, …); each suite self-skips without its key so keyless CI stays green ([real-API e2e Agent Note](../.agents/notes/implemented/testing/2026-06-19-real-api-e2e-ci.md)).
12
12
  - **Owner-local expected output** (`pnpm run test:expected`): keyless assembled CLI/process expectations without a recorded-session round trip. Drivers use `*.expected.e2e.ts` beside `tests/expected/`; CI runs built exports. Package/script expectations use `test`, while browser expectations use `test:web`.
13
- - **Snapshot** (`pnpm run test:snapshot`): a top-level scenario's recorded `session.jsonl` supplies user input and model replay, then serves as the expected persisted result. Process scenarios start through `dsh`: headless owns one-shot behavior, the SDK owns persistent control, ACP owns automation-protocol behavior, and Web retains browser/ARIA evidence beside the same session. `snapshot.yml` declares the profile, composition/header class, recording policy, exceptional replay or input metadata, and workspace facts. Typed tokens preserve parent/child identity relationships; only header pins own prompt/schema sidecars. A mutating scenario independently compares the complete `workspace.expected/` tree, which record and refresh never rewrite. Use `test:snapshot:record` when a model transcript changes and `test:snapshot:refresh` when replay input remains valid; review every resulting diff.
13
+ - **Snapshot** (`pnpm run test:snapshot`): a top-level scenario's highest recorded parent generation supplies user input and model replay, then serves as the expected persisted result. Parent filenames are `session[.vN].jsonl`; child roles are `session.<ordinal>[.vN].jsonl`; v0 omits `.v0`, positive versions require lowercase `.vN`, and each filename must agree with its header. Process scenarios start through `dsh`: headless owns one-shot behavior, the SDK owns persistent control, ACP owns automation-protocol behavior, and Web retains browser/ARIA evidence beside the same Session. `snapshot.yml` declares the profile, composition/header class, recording policy, exceptional replay or input metadata, and workspace facts. Typed tokens preserve parent/child identity relationships; only header pins own prompt/schema sidecars. A mutating scenario independently compares the complete `workspace.expected/` tree, which record and refresh never rewrite. Use `test:snapshot:record` when a model transcript changes and `test:snapshot:refresh` when replay input remains valid; review every resulting diff.
14
14
  - **Web browser snapshot** (`pnpm run test:web`; required Linux PR gate): Chromium compares session-driven output under `snapshots/web/` and UI-only output under `apps/web/tests/expected/`. CI forces read-only `DSH_SNAPSHOT=replay`, never writing expected outputs; record/refresh stay local and every diff is reviewed ([web e2e lane](../.agents/notes/implemented/testing/2026-07-24-web-gui-browser-e2e-lane.md), [CI gate decision](../.agents/notes/implemented/testing/2026-07-30-web-browser-snapshot-ci-gate.md)). `test:web` [builds first](../.agents/notes/implemented/bug-fix/2026-07-28-themed-scrollbars-and-reserved-gutter.md) for plugin CSS.
15
15
 
16
- Session fixtures keep headers and payloads but omit body sequence/time envelopes. Replay synthesizes them. Fixtures use canonical packed rows; [the migrator](../scripts/migrate-packed-session-fixtures.ts) rewrites old layouts.
16
+ Session fixtures retain headers and payloads but omit body sequence/time envelopes; replay synthesizes them. Replay, record, and refresh select each parent/child role's highest generation. Current v2 uses `.v2`, one row per event, and embedded compact Assistant streams; retained v0 (suffixless) and v1 (`.v1`) may keep canonical packed rows for migration coverage. [The migrator](../scripts/migrate-packed-session-fixtures.ts) rewrites older historical layouts.
17
+
18
+ ## How specs execute
19
+
20
+ Forked workers run several spec files at once, the coverage gate splits into concurrent partitions beside the other gates in its job, and the self-hosted runners share one host and one volume. Only the process is isolated: ports, predictable paths, external namespaces, and inherited children are not. Own each acquired resource through its teardown, and read a spec that passes only when it runs alone as a defect in the spec rather than an unstable runner. [dsh-ci-test-reliability](../.agents/skills/dsh-ci-test-reliability/SKILL.md) owns the allocation, restoration, synchronization, timeout-budget, platform, and teardown rules; its [flake diagnosis workflow](../.agents/skills/dsh-ci-test-reliability/references/ci-flake-diagnosis.md) classifies an existing probabilistic failure.
17
21
 
18
22
  ## The with-key policy: inference is cheap here
19
23
 
@@ -10,10 +10,14 @@
10
10
  - **覆盖率门禁**(`pnpm run test:coverage`):门禁级运行,对 `packages/*/*/src` 按文件 100% 覆盖。未覆盖的行往往是门禁正确标记出的死代码(应删除),而非需要补写的测试。行覆盖率是必要条件,但永远不是充分条件:它证明行被执行过,不证明功能按交付预期工作。`packages/shell/pwsh-local/src` 的按文件 100% 覆盖需要真实的 `pwsh`:缺少它时其执行器套件会自动跳过,`vitest.config.ts` 会豁免该文件以使无 pwsh 的主机保持绿色,而 CI runner 自带 pwsh,仍按完整标准执行门禁。
11
11
  - **真实 API e2e**(`pnpm run test:e2e`):带密钥测试调用真实提供方 API,包括 DeepSeek 模型以及各提供方特有的冒烟测试;这些测试各自由自己的密钥控制(`EXA_API_KEY`、`PERPLEXITY_API_KEY` 等),缺少密钥时套件会自动跳过,使 keyless CI 保持绿色([真实 API e2e Agent Note](../.agents/notes/implemented/testing/2026-06-19-real-api-e2e-ci.zh.md))。
12
12
  - **所属位置的预期输出**(`pnpm run test:expected`):无录制会话往返的无密钥组装 CLI/进程预期。驱动使用 `*.expected.e2e.ts`,并与 `tests/expected/` 同属一处;CI 针对构建产物运行。包/脚本预期使用 `test`,浏览器预期使用 `test:web`。
13
- - **快照**(`pnpm run test:snapshot`):顶层场景的录制 `session.jsonl` 同时提供用户输入和模型回放,并作为持久化结果的预期值。进程级场景都通过 `dsh` 启动:headless 负责一次性行为,SDK 负责持久控制,ACP 负责自动化协议行为,Web 在同一会话旁保留浏览器与 ARIA 证据。`snapshot.yml` 声明 profile、组合与请求头类别、录制策略、例外回放或输入元数据以及工作区事实。带类型的 token 保留父子身份关系;只有请求头 pin 拥有提示词/schema sidecar。变更工作区的场景会独立比较完整的 `workspace.expected/` 目录,record 与 refresh 绝不改写该目录。当模型 transcript(文本记录)变化时使用 `test:snapshot:record`,回放输入仍有效时使用 `test:snapshot:refresh`;请审查所有结果差异。
13
+ - **快照**(`pnpm run test:snapshot`):顶层场景数值最高的已录制 parent generation 同时提供用户输入和模型回放,并作为持久化结果的预期值。parent 文件名是 `session[.vN].jsonl`;child 角色使用 `session.<ordinal>[.vN].jsonl`;v0 省略 `.v0`,正版本必须使用小写 `.vN`,且每个文件名必须与其 header 一致。进程级场景都通过 `dsh` 启动:headless 负责一次性行为,SDK 负责持久控制,ACP 负责自动化协议行为,Web 在同一 Session 旁保留浏览器与 ARIA 证据。`snapshot.yml` 声明 profile、组合与请求头类别、录制策略、例外回放或输入元数据以及 workspace 事实。带类型的 token 保留父子身份关系;只有请求头 pin 拥有 prompt/schema sidecar。变更 workspace 的场景会独立比较完整的 `workspace.expected/` 目录,record 与 refresh 绝不改写该目录。当模型 transcript(文本记录)变化时使用 `test:snapshot:record`,回放输入仍有效时使用 `test:snapshot:refresh`;请审查所有结果差异。
14
14
  - **Web 浏览器快照**(`pnpm run test:web`;必需的 Linux PR(Pull Request)门禁):Chromium 比较 `snapshots/web/` 下由会话驱动的输出,以及 `apps/web/tests/expected/` 下仅含 UI 的输出。CI 强制只读的 `DSH_SNAPSHOT=replay`,绝不写入预期输出;record/refresh 留在本地,每处 diff 都须评审([web e2e 车道](../.agents/notes/implemented/testing/2026-07-24-web-gui-browser-e2e-lane.zh.md)、[CI 门禁决策](../.agents/notes/implemented/testing/2026-07-30-web-browser-snapshot-ci-gate.zh.md))。`test:web` 会[先构建](../.agents/notes/implemented/bug-fix/2026-07-28-themed-scrollbars-and-reserved-gutter.zh.md)以交付插件 CSS。
15
15
 
16
- 会话 fixture 保留 header 与 payload,但省略正文序号/时间 envelope。回放会合成这些字段;运行时持久化不变。fixture 使用规范打包行;[迁移器](../scripts/migrate-packed-session-fixtures.ts)会改写旧布局。
16
+ Session fixture 保留 header 与 payload,但省略正文 seq/time envelope;replay 会合成这些 envelope。Replay、record 与 refresh 会选择每个 parent/child 角色的最高 generation。当前 v2 使用 `.v2`、每个事件一行,并嵌入紧凑 Assistant stream;保留的 v0(无后缀)与 v1(`.v1`)可以为迁移覆盖保留规范 packed row。[迁移器](../scripts/migrate-packed-session-fixtures.ts)会改写更旧的历史布局。
17
+
18
+ ## spec 如何被执行
19
+
20
+ fork 出的 worker 会同时运行多个 spec 文件,coverage gate 会拆成并发的 partition,与同一个 job 中的其它 gate 并排运行,而自托管 runner 共用同一台宿主机和同一个卷。被隔离的只有进程:端口、可预测路径、外部命名空间和继承而来的子进程都不隔离。为每个占用的资源负责到它的 teardown,并把「只有单独运行时才通过」的 spec 读作该 spec 的缺陷,而不是 runner 不稳定。[dsh-ci-test-reliability](../.agents/skills/dsh-ci-test-reliability/SKILL.md) 负责资源分配、状态恢复、同步、超时预算、平台差异与 teardown 规则;它的 [flake 诊断流程](../.agents/skills/dsh-ci-test-reliability/references/ci-flake-diagnosis.md)用于归类已经存在的概率性失败。
17
21
 
18
22
  ## 带密钥策略:推理(inference)在这里很便宜
19
23
 
@@ -19,6 +19,10 @@ Global style sheets belong in `ui-theme/src/styles/`. Component styles live besi
19
19
  - Keep source text, terminal output, and diff lines unwrapped when their component contract requires column preservation; use the shared scrollbar styles rather than component-specific scrollbar selectors.
20
20
  - Put presentation in CSS. Inline React styles may pass component-local custom-property values but must not encode theme branches.
21
21
  - Preserve keyboard focus visibility and reduced-motion behavior when adding transitions or hover-only controls.
22
+ - Rounded corners inherit the global superellipse smoothing from ui-theme's `corner-shape.css` on supporting engines. Pair `corner-shape: round` with every full-round `border-radius` (`50%`, `100%`, or a pill radius) so circles and capsules keep circular arcs; the ui-theme corner-shape spec enforces the pairing.
23
+ - Elevated surfaces (menus, popovers, modals, panels, floating buttons, the composer) set `border: 0` and take `box-shadow: var(--dsw-elevation-panel)`, `var(--dsw-elevation-prominent)`, or the composer's `var(--dsw-elevation-soft)` (larger blur at lower alpha): the 0.5px hairline stroke is the first shadow layer, and `--dsw-elevation-stroke-color` rebinds or suppresses it per surface or state. Never pair a `--dsw-alias-border-*` border with an lv/elevation shadow — the ui-theme elevation spec rejects the pairing; state-colored borders (warn panels) stay real borders.
24
+ - Flat borders and separators that use a neutral `--dsw-alias-border-*` token draw at `0.5px` — buttons, inputs, cards, row dividers, and separators drawn as filled boxes (menu separators, the conversation header seam, markdown `hr`, vertical rails) share the hairline weight, which Chromium paints as one device pixel. Dashed affordances and state-colored borders keep 1px; spinner ring tracks keep their width through the spec's explicit allowlist. The ui-theme elevation spec rejects wider neutral solid borders.
25
+ - Clickable artifact links (markdown anchors, prose file mentions, web source and fetch links, produced-file chips, workflow member links) color through `--dsw-alias-link` at `font-weight: 500`, with no underline at rest and a dotted 3px-offset underline on hover/focus. Text-leading anchors also lead with the ui-primitives `LinkIcon` category glyph riding `currentColor`; workflow member links and image-only anchors carry no glyph, and tool-row file links keep their grey dotted affordance ([clickable-link Agent Note](../.agents/notes/implemented/feature/2026-09-04-web-clickable-link-styles.md)).
22
26
 
23
27
  ## Changing the system
24
28
 
@@ -19,6 +19,10 @@
19
19
  - 当组件约定要求保留列结构时,源码文本、终端输出和 diff 行不得换行;使用共享滚动条样式,不得定义组件专用滚动条选择器。
20
20
  - 呈现规则写在 CSS 中。React 内联样式可以传递组件局部自定义属性值,但不得编码主题分支。
21
21
  - 添加过渡动画或仅悬停可见的控件时,保留清晰可见的键盘焦点和减少动态效果行为。
22
+ - 支持的引擎上,圆角继承 ui-theme `corner-shape.css` 的全局超级椭圆平滑。每个正圆 `border-radius`(`50%`、`100%` 或胶囊半径)必须配对 `corner-shape: round`,使圆形与胶囊保持圆弧;ui-theme 的 corner-shape spec 强制这一配对。
23
+ - 高层级表面(菜单、浮层、对话框、面板、悬浮按钮、输入框)设 `border: 0` 并使用 `box-shadow: var(--dsw-elevation-panel)`、`var(--dsw-elevation-prominent)` 或输入框专用的 `var(--dsw-elevation-soft)`(更大模糊、更低透明度):0.5px 发丝描边是第一层投影,`--dsw-elevation-stroke-color` 可按表面或状态重绑或抑制描边。不得将 `--dsw-alias-border-*` border 与 lv/elevation 投影配对——ui-theme 的 elevation spec 会拒绝;状态色 border(warn 面板)保持真 border。
24
+ - 使用中性 `--dsw-alias-border-*` token 的平面边框与分割线一律 `0.5px`——按钮、输入框、卡片、行分割线,以及以填充盒绘制的分隔线(菜单分隔、对话标题栏接缝、markdown `hr`、竖向轨道线)共用发丝线粗细,Chromium 将其绘制为一个设备像素。dashed 记号与状态色 border 保持 1px;spinner 圆环经 spec 的显式豁免保留原宽度。更宽的中性 solid border 会被 ui-theme elevation spec 拒绝。
25
+ - 可点击产物链接(Markdown 锚点、正文文件引用、网页来源与抓取链接、产物 chips、workflow 成员链接)经 `--dsw-alias-link` 着色、`font-weight: 500`,默认无下划线,hover/focus 时为 3px offset 的点状下划线。带文字的锚点另以 ui-primitives 的 `LinkIcon` 分类图形(随 `currentColor`)作前置;workflow 成员链接与只包图片的锚点不带图形,工具行文件链接保持其灰色点线示能([可点击链接 Agent Note](../.agents/notes/implemented/feature/2026-09-04-web-clickable-link-styles.zh.md))。
22
26
 
23
27
  ## 变更系统
24
28