@mengruo/dsh-vision-toolkit 0.1.3 → 0.1.5

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 (101) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +15 -70
  3. package/README.zh.md +15 -69
  4. package/docs/requirements-traceability/README.i18n.yaml +2 -2
  5. package/docs/requirements-traceability/README.md +1 -1
  6. package/docs/requirements-traceability/README.zh.md +1 -1
  7. package/lib/artifact-access.js +20 -2
  8. package/lib/artifact-access.js.map +1 -1
  9. package/lib/client.js +128 -12
  10. package/lib/client.js.map +1 -1
  11. package/lib/config.js +69 -0
  12. package/lib/config.js.map +1 -1
  13. package/lib/errors.js +25 -2
  14. package/lib/errors.js.map +1 -1
  15. package/lib/evidence-cache.js +2 -1
  16. package/lib/evidence-cache.js.map +1 -1
  17. package/lib/exposure.js +14 -1
  18. package/lib/exposure.js.map +1 -1
  19. package/lib/image-input-variants.js +22 -12
  20. package/lib/image-input-variants.js.map +1 -1
  21. package/lib/index.js +53 -6
  22. package/lib/index.js.map +1 -1
  23. package/lib/object-storage.js +141 -0
  24. package/lib/object-storage.js.map +1 -0
  25. package/lib/paste-images.js +67 -19
  26. package/lib/paste-images.js.map +1 -1
  27. package/lib/paths.js +214 -28
  28. package/lib/paths.js.map +1 -1
  29. package/lib/runtime-manager.js +76 -10
  30. package/lib/runtime-manager.js.map +1 -1
  31. package/lib/runtime.js +219 -38
  32. package/lib/runtime.js.map +1 -1
  33. package/lib/storage-history.js +154 -0
  34. package/lib/storage-history.js.map +1 -0
  35. package/lib/types/artifact-access.d.ts.map +1 -1
  36. package/lib/types/client/index.d.ts +45 -2
  37. package/lib/types/client/index.d.ts.map +1 -1
  38. package/lib/types/client/paste-images.d.ts +2 -0
  39. package/lib/types/client/paste-images.d.ts.map +1 -1
  40. package/lib/types/config.d.ts +55 -0
  41. package/lib/types/config.d.ts.map +1 -1
  42. package/lib/types/errors.d.ts +18 -2
  43. package/lib/types/errors.d.ts.map +1 -1
  44. package/lib/types/evidence-cache.d.ts +1 -1
  45. package/lib/types/evidence-cache.d.ts.map +1 -1
  46. package/lib/types/exposure.d.ts.map +1 -1
  47. package/lib/types/image-input-variants.d.ts +5 -3
  48. package/lib/types/image-input-variants.d.ts.map +1 -1
  49. package/lib/types/index.d.ts.map +1 -1
  50. package/lib/types/object-storage.d.ts +54 -0
  51. package/lib/types/object-storage.d.ts.map +1 -0
  52. package/lib/types/paste-images.d.ts +12 -4
  53. package/lib/types/paste-images.d.ts.map +1 -1
  54. package/lib/types/paths.d.ts +31 -5
  55. package/lib/types/paths.d.ts.map +1 -1
  56. package/lib/types/runtime-manager.d.ts +28 -4
  57. package/lib/types/runtime-manager.d.ts.map +1 -1
  58. package/lib/types/runtime.d.ts +31 -1
  59. package/lib/types/runtime.d.ts.map +1 -1
  60. package/lib/types/storage-history.d.ts +63 -0
  61. package/lib/types/storage-history.d.ts.map +1 -0
  62. package/lib/types/upstream.d.ts +1 -0
  63. package/lib/types/upstream.d.ts.map +1 -1
  64. package/lib/types/web.d.ts +7 -0
  65. package/lib/types/web.d.ts.map +1 -1
  66. package/lib/upstream.js +34 -8
  67. package/lib/upstream.js.map +1 -1
  68. package/lib/web.js +44 -9
  69. package/lib/web.js.map +1 -1
  70. package/package.json +3 -1
  71. package/src/artifact-access.ts +22 -2
  72. package/src/client/index.tsx +169 -9
  73. package/src/client/paste-images.tsx +14 -4
  74. package/src/config.ts +128 -0
  75. package/src/errors.ts +25 -2
  76. package/src/evidence-cache.ts +2 -1
  77. package/src/exposure.ts +16 -2
  78. package/src/image-input-variants.ts +21 -6
  79. package/src/index.ts +65 -6
  80. package/src/object-storage.ts +174 -0
  81. package/src/paste-images.ts +81 -19
  82. package/src/paths.ts +249 -28
  83. package/src/runtime-manager.ts +93 -10
  84. package/src/runtime.ts +215 -36
  85. package/src/storage-history.ts +172 -0
  86. package/src/upstream.ts +36 -7
  87. package/src/web.ts +54 -9
  88. package/vendor/agent-vision-toolkit/UPSTREAM_MANIFEST.json +11 -11
  89. package/vendor/agent-vision-toolkit/__pycache__/detect.cpython-314.pyc +0 -0
  90. package/vendor/agent-vision-toolkit/__pycache__/ground.cpython-314.pyc +0 -0
  91. package/vendor/agent-vision-toolkit/__pycache__/vision_client.cpython-314.pyc +0 -0
  92. package/vendor/agent-vision-toolkit/bin/__pycache__/glancecpython-314.pyc +0 -0
  93. package/vendor/agent-vision-toolkit/bin/glance +8 -1
  94. package/vendor/agent-vision-toolkit/detect.py +13 -7
  95. package/vendor/agent-vision-toolkit/ground.py +43 -18
  96. package/vendor/agent-vision-toolkit/tests/test_vision_client.py +88 -0
  97. package/vendor/agent-vision-toolkit/vision_client.py +84 -6
  98. package/assets/community-group-qr.png +0 -0
  99. package/assets/logo_aihubmix.png +0 -0
  100. package/assets/logo_eapi_dark.png +0 -0
  101. package/assets/wechat-reward.png +0 -0
package/src/config.ts CHANGED
@@ -77,6 +77,10 @@ export interface VisionProviderConfig {
77
77
  anthropicThinking?: 'omit' | 'disabled' | 'adaptive'
78
78
  /** Outbound User-Agent for provider requests and connection tests. */
79
79
  userAgent?: string
80
+ /** Whether to request a streamed (SSE) completion instead of one JSON response (default false). */
81
+ stream?: boolean
82
+ /** Whether to upload images to object storage and send the model a URL instead of base64 (default false). */
83
+ uploadViaUrl?: boolean
80
84
  /** t1: per-request hedge threshold in seconds. A single request exceeding t1 keeps running while the next provider starts in parallel. */
81
85
  t1Seconds?: number
82
86
  /** t2: per-provider cumulative cutoff in seconds. Total accumulated request time reaching t2 terminates the provider. */
@@ -106,6 +110,10 @@ export interface VisionToolkitConfig {
106
110
  anthropicThinking?: 'omit' | 'disabled' | 'adaptive'
107
111
  /** Outbound User-Agent for provider requests and connection tests. */
108
112
  userAgent?: string
113
+ /** Whether to request a streamed (SSE) completion instead of one JSON response (default false). */
114
+ stream?: boolean
115
+ /** Whether to upload images to object storage and send the model a URL instead of base64 (default false). */
116
+ uploadViaUrl?: boolean
109
117
  }
110
118
  /** Ordered online vision providers; array order is the failover priority. */
111
119
  providers?: VisionProviderConfig[]
@@ -123,6 +131,21 @@ export interface VisionToolkitConfig {
123
131
  maxImagePixels?: number
124
132
  /** Default per-model in-flight request cap inherited by providers that do not set their own. */
125
133
  concurrency?: number
134
+ /**
135
+ * Optional S3-compatible object storage used by the URL image-transfer path.
136
+ * `endpoint`, `bucket`, and `credential` are required to enable URL transfer;
137
+ * `publicBase` is optional and falls back to presigned URLs when unset.
138
+ */
139
+ objectStorage?: {
140
+ /** S3-compatible API endpoint (e.g. R2, MinIO, Tencent COS). */
141
+ endpoint?: string
142
+ /** Bucket name. */
143
+ bucket?: string
144
+ /** DSH Credential reference holding "accessKeyId:secretAccessKey". */
145
+ credential?: string
146
+ /** Public base URL (custom domain / r2.dev); when unset, presigned URLs are used. */
147
+ publicBase?: string
148
+ }
126
149
  runtime?: {
127
150
  /** `managed` uses the packaged snapshot and isolated venv; `external` uses a clean pinned checkout. */
128
151
  mode?: 'managed' | 'external'
@@ -131,6 +154,14 @@ export interface VisionToolkitConfig {
131
154
  /** Optional Python 3.11+ bootstrap/interpreter override. */
132
155
  python?: string
133
156
  }
157
+ /**
158
+ * Optional shared storage root. When set, every workspace gets an isolated,
159
+ * automatically generated child directory below this root instead of writing
160
+ * `.dsh-vision-toolkit` into the workspace.
161
+ */
162
+ storageDir?: string
163
+ /** Internal read-only history used to keep persisted paths valid after storage moves. */
164
+ storageHistory?: string[]
134
165
  /** Extra directories (besides the workspace) inputs may come from. */
135
166
  allowedDirs?: string[]
136
167
  /**
@@ -174,6 +205,7 @@ export const Config: Schema<VisionToolkitConfig> = z.object({
174
205
  protocol: z.union(['openai', 'anthropic'] as const).default('openai'),
175
206
  anthropicThinking: z.union(['omit', 'disabled', 'adaptive'] as const).default('omit'),
176
207
  userAgent: z.string().default(DEFAULT_VISION_USER_AGENT),
208
+ stream: z.boolean().default(false),
177
209
  }),
178
210
  providers: z.array(z.object({
179
211
  name: z.string(),
@@ -185,6 +217,8 @@ export const Config: Schema<VisionToolkitConfig> = z.object({
185
217
  protocol: z.union(['openai', 'anthropic'] as const).default('openai'),
186
218
  anthropicThinking: z.union(['omit', 'disabled', 'adaptive'] as const).default('omit'),
187
219
  userAgent: z.string(),
220
+ stream: z.boolean().default(false),
221
+ uploadViaUrl: z.boolean().default(false),
188
222
  t1Seconds: z.number(),
189
223
  t2Seconds: z.number(),
190
224
  maxImageBytes: z.number(),
@@ -199,11 +233,19 @@ export const Config: Schema<VisionToolkitConfig> = z.object({
199
233
  maxImageBytes: z.number().default(4194304),
200
234
  maxImagePixels: z.number().default(20000000),
201
235
  concurrency: z.number().default(4),
236
+ objectStorage: z.object({
237
+ endpoint: z.string().default(''),
238
+ bucket: z.string().default(''),
239
+ credential: z.string().default(''),
240
+ publicBase: z.string().default(''),
241
+ }),
202
242
  runtime: z.object({
203
243
  mode: z.union(['managed', 'external'] as const).default('managed'),
204
244
  agentVisionToolkitPath: z.string(),
205
245
  python: z.string(),
206
246
  }),
247
+ storageDir: z.string(),
248
+ storageHistory: z.array(z.string()).default([]),
207
249
  allowedDirs: z.array(z.string()).default([]),
208
250
  imageInputVariants: z.object({
209
251
  enabled: z.boolean().default(true),
@@ -225,6 +267,8 @@ export interface ResolvedProvider {
225
267
  protocol: 'openai' | 'anthropic'
226
268
  anthropicThinking: 'omit' | 'disabled' | 'adaptive'
227
269
  userAgent: string
270
+ stream: boolean
271
+ uploadViaUrl: boolean
228
272
  t1Seconds: number
229
273
  t2Seconds: number
230
274
  maxImageBytes: number
@@ -242,6 +286,8 @@ export interface ResolvedVisionToolkitConfig {
242
286
  protocol: 'openai' | 'anthropic'
243
287
  anthropicThinking: 'omit' | 'disabled' | 'adaptive'
244
288
  userAgent: string
289
+ stream: boolean
290
+ uploadViaUrl: boolean
245
291
  }
246
292
  /** Ordered failover pool; array order is the priority, highest first. */
247
293
  providers: ResolvedProvider[]
@@ -252,11 +298,19 @@ export interface ResolvedVisionToolkitConfig {
252
298
  maxImageBytes: number
253
299
  maxImagePixels: number
254
300
  concurrency: number
301
+ objectStorage: {
302
+ endpoint: string
303
+ bucket: string
304
+ credential?: CredentialRef
305
+ publicBase?: string
306
+ }
255
307
  runtime: {
256
308
  mode: 'managed' | 'external'
257
309
  agentVisionToolkitPath?: string
258
310
  python?: string
259
311
  }
312
+ storageDir?: string
313
+ storageHistory: string[]
260
314
  allowedDirs: string[]
261
315
  imageInputVariants: {
262
316
  enabled: boolean
@@ -354,6 +408,8 @@ function resolveProvider(
354
408
  if (userAgent.length === 0) {
355
409
  throw new VisionToolkitError('config', `${label}.userAgent must not be empty`)
356
410
  }
411
+ const stream = input.stream === true
412
+ const uploadViaUrl = input.uploadViaUrl === true
357
413
  const t1Seconds = input.t1Seconds ?? 90
358
414
  if (!Number.isInteger(t1Seconds) || t1Seconds < 1 || t1Seconds > MAX_TIMEOUT_SECONDS) {
359
415
  throw new VisionToolkitError('config', `${label}.t1Seconds must be an integer between 1 and ${MAX_TIMEOUT_SECONDS}`)
@@ -390,6 +446,8 @@ function resolveProvider(
390
446
  protocol,
391
447
  anthropicThinking,
392
448
  userAgent,
449
+ stream,
450
+ uploadViaUrl,
393
451
  t1Seconds,
394
452
  t2Seconds,
395
453
  maxImageBytes,
@@ -455,7 +513,24 @@ export function resolveConfig(config: VisionToolkitConfig = {}): ResolvedVisionT
455
513
  if (python !== undefined && python.length === 0) {
456
514
  throw new VisionToolkitError('config', 'runtime.python must not be empty')
457
515
  }
516
+ const storageDir = config.storageDir?.trim()
517
+ const storageHistory = [...new Set((config.storageHistory ?? [])
518
+ .map(dir => dir.trim())
519
+ .filter(dir => dir.length > 0 && dir !== storageDir))]
458
520
  const allowedDirs = (config.allowedDirs ?? []).map(dir => dir.trim()).filter(dir => dir.length > 0)
521
+ const objectStorageInput = config.objectStorage ?? {}
522
+ const objectStorageEndpoint = objectStorageInput.endpoint?.trim() ?? ''
523
+ const objectStorageBucket = objectStorageInput.bucket?.trim() ?? ''
524
+ const objectStoragePublicBase = objectStorageInput.publicBase?.trim()
525
+ let objectStorageCredential: CredentialRef | undefined
526
+ const objectStorageCredentialSource = objectStorageInput.credential?.trim()
527
+ if (objectStorageCredentialSource !== undefined && objectStorageCredentialSource.length > 0) {
528
+ try {
529
+ objectStorageCredential = credentialRef(objectStorageCredentialSource)
530
+ } catch (error) {
531
+ throw new VisionToolkitError('config', `objectStorage.credential "${objectStorageCredentialSource}" is not a valid credential reference`, { cause: error })
532
+ }
533
+ }
459
534
  const imageInputVariants = config.imageInputVariants ?? {}
460
535
  const variantProviders = (imageInputVariants.providers ?? [])
461
536
  .map(provider => provider.trim())
@@ -478,6 +553,8 @@ export function resolveConfig(config: VisionToolkitConfig = {}): ResolvedVisionT
478
553
  protocol: primary.protocol,
479
554
  anthropicThinking: primary.anthropicThinking,
480
555
  userAgent: primary.userAgent,
556
+ stream: primary.stream,
557
+ uploadViaUrl: primary.uploadViaUrl,
481
558
  },
482
559
  providers,
483
560
  language,
@@ -487,11 +564,19 @@ export function resolveConfig(config: VisionToolkitConfig = {}): ResolvedVisionT
487
564
  maxImageBytes,
488
565
  maxImagePixels,
489
566
  concurrency,
567
+ objectStorage: {
568
+ endpoint: objectStorageEndpoint,
569
+ bucket: objectStorageBucket,
570
+ ...(objectStorageCredential === undefined ? {} : { credential: objectStorageCredential }),
571
+ ...(objectStoragePublicBase === undefined || objectStoragePublicBase.length === 0 ? {} : { publicBase: objectStoragePublicBase }),
572
+ },
490
573
  runtime: {
491
574
  mode,
492
575
  ...(toolkitPath !== undefined ? { agentVisionToolkitPath: toolkitPath } : {}),
493
576
  ...(python !== undefined ? { python } : {}),
494
577
  },
578
+ ...(storageDir === undefined || storageDir.length === 0 ? {} : { storageDir }),
579
+ storageHistory,
495
580
  allowedDirs,
496
581
  imageInputVariants: {
497
582
  enabled: imageInputVariants.enabled ?? true,
@@ -502,6 +587,49 @@ export function resolveConfig(config: VisionToolkitConfig = {}): ResolvedVisionT
502
587
  }
503
588
  }
504
589
 
590
+ /** Merge prior storage roots into the next resolved generation's read-only history. */
591
+ export function retainedStorageHistory(
592
+ next: VisionToolkitConfig,
593
+ previous: VisionToolkitConfig,
594
+ ): string[] {
595
+ const resolvedNext = resolveConfig(next)
596
+ const resolvedPrevious = resolveConfig(previous)
597
+ return [...new Set([
598
+ ...resolvedPrevious.storageHistory,
599
+ ...resolvedNext.storageHistory,
600
+ ...(resolvedPrevious.storageDir === undefined ? [] : [resolvedPrevious.storageDir]),
601
+ ])].filter(storageDir => storageDir !== resolvedNext.storageDir)
602
+ }
603
+
604
+ export interface WatchedSettingsGeneration {
605
+ /** Configuration to activate now; omitted after a successful history writeback. */
606
+ config?: VisionToolkitConfig
607
+ /** Whether the derived history still needs plugin-owned durable persistence. */
608
+ requiresDurableStorageHistory?: boolean
609
+ /** Non-fatal internal-history persistence error. */
610
+ persistenceError?: unknown
611
+ }
612
+
613
+ /** Prepare one live Settings generation without letting internal history writeback block activation. */
614
+ export async function prepareWatchedSettingsGeneration(
615
+ next: VisionToolkitConfig,
616
+ previous: VisionToolkitConfig,
617
+ writable: boolean,
618
+ persistStorageHistory: (storageHistory: string[]) => Promise<void>,
619
+ ): Promise<WatchedSettingsGeneration> {
620
+ const storageHistory = retainedStorageHistory(next, previous)
621
+ if (JSON.stringify(storageHistory) === JSON.stringify(resolveConfig(next).storageHistory)) return { config: next }
622
+
623
+ const config = { ...next, storageHistory }
624
+ if (!writable) return { config, requiresDurableStorageHistory: true }
625
+ try {
626
+ await persistStorageHistory(storageHistory)
627
+ return {}
628
+ } catch (persistenceError) {
629
+ return { config, requiresDurableStorageHistory: true, persistenceError }
630
+ }
631
+ }
632
+
505
633
  /** Whether a resolved provider should use the bundled public key instead of DSH credentials. */
506
634
  export function isBuiltInFreeVisionProvider(provider: ResolvedVisionToolkitConfig['provider']): boolean {
507
635
  return String(provider.credential) === BUILT_IN_FREE_VISION_CREDENTIAL
package/src/errors.ts CHANGED
@@ -5,18 +5,41 @@
5
5
  * @module dsh-vision-toolkit/errors
6
6
  */
7
7
 
8
- /** Discriminant tag for every Vision Toolkit failure. */
8
+ /**
9
+ * Discriminant tag for every Vision Toolkit failure.
10
+ *
11
+ * The remote vision-provider failures are split by a machine-routable taxonomy
12
+ * so the failover loop can decide, per error, whether to retry the SAME
13
+ * provider or advance to the next one:
14
+ *
15
+ * - `auth` 401/403 — credential is wrong or unauthorized. Never retry.
16
+ * - `quota` 402 — account out of quota/unpaid. Never retry.
17
+ * - `rate_limit` 429 — throttled. Park and revisit, never retry in place.
18
+ * - `server` 5xx — transient provider fault. Worth a same-provider retry.
19
+ * - `network` connection refused / DNS / socket — transient. Worth a retry.
20
+ * - `region` provider unavailable in this region. Never retry.
21
+ * - `tos` rejected by content/safety policy. Never retry.
22
+ * - `invalid_request` 400/404/422 — bad request or unknown model. Never retry.
23
+ * - `service` fallback for unclassifiable remote failures. Never retry.
24
+ */
9
25
  export const VISION_TOOLKIT_ERROR_CODES = [
10
26
  'config',
11
27
  'input',
12
28
  'capacity',
29
+ 'auth',
30
+ 'quota',
31
+ 'rate_limit',
32
+ 'server',
33
+ 'network',
34
+ 'region',
35
+ 'tos',
36
+ 'invalid_request',
13
37
  'service',
14
38
  'runtime',
15
39
  'output',
16
40
  'timeout',
17
41
  'cancelled',
18
42
  'path',
19
- 'rate_limit',
20
43
  ] as const
21
44
 
22
45
  /** Stable machine-readable error category. */
@@ -73,7 +73,7 @@ function hash(value: string): string {
73
73
  return createHash('sha256').update(value).digest('hex')
74
74
  }
75
75
 
76
- /** Fingerprint every runtime setting that can change the generated description. */
76
+ /** Fingerprint every runtime setting that can change generated evidence or its embedded path. */
77
77
  export function evidenceRuntimeFingerprint(
78
78
  config: ResolvedVisionToolkitConfig,
79
79
  credentialSha256?: string,
@@ -115,6 +115,7 @@ export function evidenceRuntimeFingerprint(
115
115
  maxImageBytes: config.maxImageBytes,
116
116
  maxImagePixels: config.maxImagePixels,
117
117
  runtime: config.runtime,
118
+ storageDir: config.storageDir ?? null,
118
119
  }))
119
120
  }
120
121
 
package/src/exposure.ts CHANGED
@@ -8,7 +8,7 @@
8
8
 
9
9
  import type { Agent } from '@deepseek-ai/dsh-agent'
10
10
  import type { ContentBlock } from '@deepseek-ai/dsh-llm'
11
- import type { Session } from '@deepseek-ai/dsh-session'
11
+ import type { Session, SessionEvent } from '@deepseek-ai/dsh-session'
12
12
  import { defineTool, type ToolDefinition } from '@deepseek-ai/dsh-tools'
13
13
  import type { Context } from '@deepseek-ai/cordis'
14
14
  import { VISION_SKILLS_CONTENT, VISION_SKILLS_NAME } from './skill.ts'
@@ -78,10 +78,24 @@ function isBundledSkillResult(value: unknown): boolean {
78
78
  && isBundledSkillContent(value.content)
79
79
  }
80
80
 
81
+ /**
82
+ * Iterate a Session's durable event log across runtime lines. dsh 0.1.2-alpha
83
+ * replaced the `events` getter with `snapshotEvents()` (session-log-read-intent):
84
+ * iterating the removed `session.events` throws `session.events is not iterable`
85
+ * and takes down `agent/created` → session restore. Prefer the snapshot accessor
86
+ * and fall back to the legacy rc-line `events` getter so both lines work.
87
+ */
88
+ function sessionEventLog(session: Session): readonly SessionEvent[] {
89
+ const snapshot = (session as { snapshotEvents?: () => readonly SessionEvent[] }).snapshotEvents
90
+ return typeof snapshot === 'function'
91
+ ? snapshot.call(session)
92
+ : (session as { events: readonly SessionEvent[] }).events
93
+ }
94
+
81
95
  /** Whether durable history proves that this Session loaded the bundled Skill. */
82
96
  function hasLoadedVisionSkill(session: Session): boolean {
83
97
  const nativeCalls = new Set<string>()
84
- for (const event of session.events) {
98
+ for (const event of sessionEventLog(session)) {
85
99
  if (event.type === 'user/message') {
86
100
  const source = event.data.source
87
101
  if (source.kind === 'skill-invocation'
@@ -142,18 +142,21 @@ async function materializeImage(
142
142
  data: Uint8Array,
143
143
  extension: string,
144
144
  sessionId: string | undefined,
145
+ storageDir: string | undefined,
145
146
  ): Promise<MaterializedImage> {
146
147
  const session = sessionId === undefined ? undefined : ctx.sessions.get(sessionId as never)
147
148
  const cwd = session?.header.cwd
148
149
  if (sessionId !== undefined && cwd !== undefined && isAbsolute(cwd)) {
149
- const root = await sessionPasteRoot(ctx, sessionId)
150
+ const root = await sessionPasteRoot(ctx, sessionId, storageDir)
150
151
  const identity = createHash('sha256')
151
152
  .update(`${sessionId}\u0000${String(block.attachment.attachmentId)}`)
152
153
  .digest('hex')
153
154
  .slice(0, 32)
154
- const file = join(root.visibleRoot, `attachment-${identity}${extension}`)
155
+ const filename = `attachment-${identity}${extension}`
156
+ const writePath = join(root.writeRoot, filename)
157
+ const file = join(root.visibleRoot, filename)
155
158
  try {
156
- await writeFile(file, Buffer.from(data), { mode: 0o600, flag: 'wx' })
159
+ await writeFile(writePath, Buffer.from(data), { mode: 0o600, flag: 'wx' })
157
160
  } catch (error) {
158
161
  if (!(error instanceof Error && 'code' in error && error.code === 'EEXIST')) throw error
159
162
  }
@@ -357,6 +360,7 @@ async function readImageBlock(
357
360
  block: ImageBlock,
358
361
  query: string,
359
362
  sessionId?: string,
363
+ storageDir?: string,
360
364
  ): Promise<ContentBlock> {
361
365
  const attachments = ctx.get('attachments')
362
366
  const current = runtime()
@@ -371,7 +375,7 @@ async function readImageBlock(
371
375
  let pathEvidence = ''
372
376
  try {
373
377
  const stored = await attachments.readImage(block.attachment)
374
- const materialized = await materializeImage(ctx, block, stored.data, extension, sessionId)
378
+ const materialized = await materializeImage(ctx, block, stored.data, extension, sessionId, storageDir)
375
379
  temporaryDirectory = materialized.temporaryDirectory
376
380
  if (materialized.persistent) pathEvidence = imagePathEvidence(materialized.file)
377
381
 
@@ -414,6 +418,7 @@ async function readImageBlock(
414
418
  * @param signal - the caller's cancellation for this conversion pass.
415
419
  * @param sessionId - the live Session identity, when available.
416
420
  * @param runtimeHash - stable fingerprint of the vision provider and evidence runtime.
421
+ * @param storageDir - optional shared plugin storage root.
417
422
  * @returns the rewritten message list.
418
423
  */
419
424
  export async function convertImagesToEvidence(
@@ -424,6 +429,7 @@ export async function convertImagesToEvidence(
424
429
  signal?: AbortSignal,
425
430
  sessionId?: string,
426
431
  runtimeHash = 'process-only-runtime',
432
+ storageDir?: string,
427
433
  ): Promise<Message[]> {
428
434
  const session = sessionId === undefined ? undefined : ctx.sessions.get(sessionId as never)
429
435
  const sessionIdentity = session === undefined
@@ -480,7 +486,7 @@ export async function convertImagesToEvidence(
480
486
  prompt: query,
481
487
  runtimeHash,
482
488
  }), () =>
483
- readImageBlock(ctx, runtime, block, query, sessionId)),
489
+ readImageBlock(ctx, runtime, block, query, sessionId, storageDir)),
484
490
  signal,
485
491
  ),
486
492
  signal,
@@ -517,6 +523,7 @@ export class ImageInputVariantAdapter extends LlmAdapter {
517
523
  private readonly runtime: () => VisionToolkitRuntime | undefined,
518
524
  private readonly cache: EvidenceCache,
519
525
  private readonly hidden: () => boolean = () => false,
526
+ private readonly startupStorageDirectory: () => string | undefined = () => undefined,
520
527
  ) {
521
528
  super()
522
529
  }
@@ -589,6 +596,7 @@ export class ImageInputVariantAdapter extends LlmAdapter {
589
596
 
590
597
  override async *stream(options: GenerateOptions): AsyncGenerator<StreamChunk> {
591
598
  const current = this.runtime()
599
+ const storageDir = current === undefined ? this.startupStorageDirectory() : current.storageDirectory
592
600
  let captured: CapturedEvidenceRuntime | undefined
593
601
  if (current !== undefined && options.messages.some(message => contentHasImage(message.content))) {
594
602
  try {
@@ -610,7 +618,12 @@ export class ImageInputVariantAdapter extends LlmAdapter {
610
618
  options.messages,
611
619
  options.signal,
612
620
  options.sessionId === undefined ? undefined : String(options.sessionId),
613
- captured?.evidenceFingerprint ?? 'process-only-runtime',
621
+ captured?.evidenceFingerprint ?? createHash('sha256')
622
+ .update('process-only-runtime')
623
+ .update('\0')
624
+ .update(storageDir ?? '')
625
+ .digest('hex'),
626
+ storageDir,
614
627
  )
615
628
  // Delegate through the host service under the upstream route: the variant
616
629
  // is a wire-only facade, and the upstream route owns retry and replay.
@@ -846,6 +859,7 @@ export function installImageInputVariants(
846
859
  ctx: Context,
847
860
  getConfig: () => ResolvedVisionToolkitConfig,
848
861
  getRuntime: () => VisionToolkitRuntime | undefined,
862
+ getStartupStorageDirectory: () => string | undefined = () => undefined,
849
863
  ): { dispose: () => void; reconcile: () => void } {
850
864
  const evidenceStore = new SessionEvidenceStore(ctx)
851
865
  const evidenceCache = new EvidenceCache(EVIDENCE_CACHE_LIMIT, evidenceStore)
@@ -991,6 +1005,7 @@ export function installImageInputVariants(
991
1005
  getRuntime,
992
1006
  evidenceCache,
993
1007
  () => getConfig().imageInputVariants.hidden,
1008
+ getStartupStorageDirectory,
994
1009
  ),
995
1010
  )
996
1011
  registrations.set(upstream, { dispose, retryPolicyKey: upstreamRetryPolicyKey })
package/src/index.ts CHANGED
@@ -18,13 +18,16 @@ import { ArtifactAccessController, prepareArtifactAccessKey } from './artifact-a
18
18
  import {
19
19
  Config,
20
20
  VISION_TOOLKIT_SETTINGS_NAMESPACE,
21
+ prepareWatchedSettingsGeneration,
21
22
  resolveConfig,
23
+ type ResolvedVisionToolkitConfig,
22
24
  type VisionToolkitConfig,
23
25
  } from './config.ts'
24
26
  import { VisionToolExposure } from './exposure.ts'
25
27
  import { createPasteTakeoverResolver, installImageInputVariants } from './image-input-variants.ts'
26
28
  import { VisionToolkitRuntimeManager } from './runtime-manager.ts'
27
29
  import { VISION_SKILLS_SKILL } from './skill.ts'
30
+ import { StorageHistoryStore } from './storage-history.ts'
28
31
  import { createVisionTools } from './tools.ts'
29
32
  import { PLUGIN_VERSION } from './version.ts'
30
33
  import { installVisionToolkitWeb, VisionToolkitWebBackend } from './web.ts'
@@ -51,8 +54,33 @@ export async function apply(ctx: Context, config: VisionToolkitConfig = {}): Pro
51
54
  const artifacts = new ArtifactAccessController(await prepareArtifactAccessKey())
52
55
  const lifecycle = new AbortController()
53
56
  const disposers: Array<() => void> = []
57
+ const storageHistory = new StorageHistoryStore(ctx)
58
+ disposers.push(() => { storageHistory.dispose() })
59
+ let storageHistoryWarningReported = false
54
60
  let operationalDisposers: { activationTool: () => void; exposure: () => void; skill: () => void } | undefined
55
61
 
62
+ const persistStorageHistory = async (candidate: VisionToolkitConfig, required: boolean): Promise<void> => {
63
+ try {
64
+ const persisted = await storageHistory.persist(candidate)
65
+ if (persisted) return
66
+ const error = new Error(
67
+ 'configured storage history requires @deepseek-ai/dsh-storage-domain when Settings cannot persist it',
68
+ )
69
+ if (required) throw error
70
+ if (!storageHistoryWarningReported) {
71
+ storageHistoryWarningReported = true
72
+ ctx.logger.warn('dsh-vision-toolkit: %s', error.message)
73
+ }
74
+ } catch (error) {
75
+ if (required) throw error
76
+ if (!storageHistoryWarningReported) {
77
+ storageHistoryWarningReported = true
78
+ const message = error instanceof Error ? error.message : String(error)
79
+ ctx.logger.warn('dsh-vision-toolkit: configured storage history was not persisted. %s', message)
80
+ }
81
+ }
82
+ }
83
+
56
84
  const ensureOperational = (): void => {
57
85
  if (!manager.ready || operationalDisposers !== undefined) return
58
86
  const exposure = new VisionToolExposure(ctx, () => createVisionTools(
@@ -84,10 +112,16 @@ export async function apply(ctx: Context, config: VisionToolkitConfig = {}): Pro
84
112
  }
85
113
  }
86
114
 
115
+ const initialConfig = await storageHistory.restore(settings.get())
87
116
  try {
88
- await manager.initialize(settings.get())
117
+ await manager.initialize(initialConfig, candidate => persistStorageHistory(candidate.config, false))
89
118
  ensureOperational()
90
119
  } catch (error) {
120
+ const resolvedInitial = resolveConfig(initialConfig)
121
+ if (resolvedInitial.storageDir === undefined
122
+ || manager.validatedStorageDirectory() === resolvedInitial.storageDir) {
123
+ await persistStorageHistory(initialConfig, false)
124
+ }
91
125
  const message = error instanceof Error ? error.message : String(error)
92
126
  ctx.logger.error(
93
127
  'dsh-vision-toolkit %s: runtime not ready; the vision-skills skill, activation bootstrap, and Agent-scoped visual tools are NOT registered. Settings remain available for repair. %s',
@@ -97,29 +131,54 @@ export async function apply(ctx: Context, config: VisionToolkitConfig = {}): Pro
97
131
  }
98
132
 
99
133
  const backend = new VisionToolkitWebBackend(ctx, manager, artifacts, ensureOperational)
134
+ const currentConfig = (): ResolvedVisionToolkitConfig => manager.ready
135
+ ? manager.currentConfig()
136
+ : resolveConfig(settings.get())
100
137
  const pastedImages = new PastedImageBackend(ctx, {
101
138
  maxUploadBytes: () => MAX_PASTE_IMAGE_BYTES,
139
+ storageGeneration: () => manager.storageGeneration(),
102
140
  })
103
141
  // Image-input variants register asynchronously once eligible routes exist;
104
142
  // the runtime getter stays lazy so variants appear even when the runtime
105
143
  // becomes ready after the first sweep.
106
144
  const variants = installImageInputVariants(
107
145
  ctx,
108
- () => resolveConfig(settings.get()),
146
+ currentConfig,
109
147
  () => manager.ready ? manager.current() : undefined,
148
+ () => manager.validatedStorageDirectory(),
110
149
  )
111
150
  installVisionToolkitWeb(
112
151
  ctx,
113
152
  backend,
114
153
  artifacts,
115
154
  pastedImages,
116
- createPasteTakeoverResolver(ctx, () => resolveConfig(settings.get())),
117
- () => ({ hidden: resolveConfig(settings.get()).imageInputVariants.hidden }),
155
+ createPasteTakeoverResolver(ctx, currentConfig),
156
+ () => ({ hidden: currentConfig().imageInputVariants.hidden }),
118
157
  )
119
158
  disposers.push(variants.dispose)
120
- disposers.push(settings.watch(async (next) => {
159
+ disposers.push(settings.watch(async (next, previous) => {
121
160
  try {
122
- await manager.reconfigure(next)
161
+ const prepared = await prepareWatchedSettingsGeneration(
162
+ next,
163
+ previous,
164
+ ctx.settings.writable,
165
+ storageHistory => settings.update({ storageHistory }),
166
+ )
167
+ if (prepared.persistenceError !== undefined) {
168
+ const message = prepared.persistenceError instanceof Error
169
+ ? prepared.persistenceError.message
170
+ : String(prepared.persistenceError)
171
+ ctx.logger.warn('dsh-vision-toolkit: activating Settings without persisting internal storage history. %s', message)
172
+ }
173
+ if (prepared.config === undefined) return
174
+ const candidate = await storageHistory.restore(prepared.config)
175
+ await manager.reconfigure(
176
+ candidate,
177
+ generation => persistStorageHistory(
178
+ generation.config,
179
+ prepared.requiresDurableStorageHistory === true,
180
+ ),
181
+ )
123
182
  ensureOperational()
124
183
  variants.reconcile()
125
184
  } catch (error) {