@anionex/dsh-vision-toolkit 0.1.37 → 0.1.38

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.
@@ -0,0 +1,350 @@
1
+ /** Durable, Session-scoped cache for image descriptions used by model variants. */
2
+ import { createHash } from 'node:crypto';
3
+ import { defineDomain, domainTable } from '@deepseek-ai/dsh-storage-domain';
4
+ import { z } from 'zod';
5
+ import { UPSTREAM_COMMIT } from "./version.js";
6
+ /** Bump only when the model-visible evidence contract changes incompatibly. */
7
+ export const EVIDENCE_CONTRACT_VERSION = 1;
8
+ /** Persistent cache bounds keep the Profile storage proportional and predictable. */
9
+ const DEFAULT_PERSISTED_ENTRY_LIMIT = 512;
10
+ const DEFAULT_PERSISTED_BYTE_LIMIT = 8 * 1024 * 1024;
11
+ const DEFAULT_PERSISTED_ENTRY_BYTE_LIMIT = 64 * 1024;
12
+ const MAX_SCHEMA_TEXT_CHARS = 256 * 1024;
13
+ const MAX_SCHEMA_RECORD_CHARS = 512 * 1024;
14
+ const hexDigestSchema = z.string().regex(/^[0-9a-f]{64}$/u);
15
+ const sessionIdentitySchema = z.object({
16
+ createdAt: z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER),
17
+ cwd: z.string().optional(),
18
+ });
19
+ const evidenceRecordSchema = z.object({
20
+ contractVersion: z.number().int().nonnegative(),
21
+ sessionId: z.string().min(1),
22
+ session: sessionIdentitySchema,
23
+ attachmentId: z.string().min(1),
24
+ promptHash: hexDigestSchema,
25
+ runtimeHash: hexDigestSchema,
26
+ text: z.string().min(1).max(MAX_SCHEMA_TEXT_CHARS),
27
+ storedAt: z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER),
28
+ });
29
+ /** Plugin-owned sidecar domain; the host backend handles atomicity and file safety. */
30
+ export const evidenceCacheDomainSpec = defineDomain({
31
+ name: 'vision_toolkit_evidence',
32
+ version: 0,
33
+ tables: {
34
+ // Keep the durable boundary tolerant of one damaged cache payload: each
35
+ // record is parsed and validated independently below, so corruption causes
36
+ // a miss instead of preventing the entire optional domain from opening.
37
+ evidence: domainTable(z.string().max(MAX_SCHEMA_RECORD_CHARS)),
38
+ },
39
+ });
40
+ function hash(value) {
41
+ return createHash('sha256').update(value).digest('hex');
42
+ }
43
+ /** Fingerprint every runtime setting that can change the generated description. */
44
+ export function evidenceRuntimeFingerprint(config, credentialSha256, sslVerify) {
45
+ return hash(JSON.stringify({
46
+ upstreamCommit: UPSTREAM_COMMIT,
47
+ provider: {
48
+ baseUrl: config.provider.baseUrl,
49
+ credential: {
50
+ ref: String(config.provider.credential),
51
+ sha256: credentialSha256 ?? null,
52
+ },
53
+ model: config.provider.model,
54
+ protocol: config.provider.protocol,
55
+ anthropicThinking: config.provider.anthropicThinking,
56
+ sslVerify: sslVerify ?? null,
57
+ userAgent: config.provider.userAgent,
58
+ },
59
+ language: config.language,
60
+ timeoutMs: config.timeoutMs,
61
+ concurrency: config.concurrency,
62
+ maxImageBytes: config.maxImageBytes,
63
+ maxImagePixels: config.maxImagePixels,
64
+ runtime: config.runtime,
65
+ }));
66
+ }
67
+ /** Build a non-secret cache key from the Session, attachment, focus, and runtime contract. */
68
+ export function createEvidenceCacheKey(input) {
69
+ const promptHash = hash(input.prompt);
70
+ const digest = hash(JSON.stringify([
71
+ EVIDENCE_CONTRACT_VERSION,
72
+ input.sessionId ?? '',
73
+ input.sessionIdentity?.createdAt ?? '',
74
+ input.sessionIdentity?.cwd ?? '',
75
+ input.attachmentId,
76
+ promptHash,
77
+ input.runtimeHash,
78
+ ]));
79
+ return Object.freeze({
80
+ digest,
81
+ contractVersion: EVIDENCE_CONTRACT_VERSION,
82
+ ...(input.sessionId === undefined ? {} : { sessionId: input.sessionId }),
83
+ ...(input.sessionIdentity === undefined ? {} : { sessionCreatedAt: input.sessionIdentity.createdAt }),
84
+ ...(input.sessionIdentity?.cwd === undefined ? {} : { sessionCwd: input.sessionIdentity.cwd }),
85
+ attachmentId: input.attachmentId,
86
+ promptHash,
87
+ runtimeHash: input.runtimeHash,
88
+ });
89
+ }
90
+ /** Bounded promise cache; concurrent readers join one load and rejected loads are evicted. */
91
+ export class EvidenceCache {
92
+ limit;
93
+ persistence;
94
+ entries = new Map();
95
+ constructor(limit, persistence) {
96
+ this.limit = limit;
97
+ this.persistence = persistence;
98
+ }
99
+ /** Read a memory/durable hit or compute and persist one model-visible result. */
100
+ read(key, load) {
101
+ const memoryKey = typeof key === 'string' ? key : key.digest;
102
+ const existing = this.entries.get(memoryKey);
103
+ if (existing !== undefined) {
104
+ // Refresh recency: Map iteration order is insertion order.
105
+ this.entries.delete(memoryKey);
106
+ this.entries.set(memoryKey, existing);
107
+ return existing;
108
+ }
109
+ const pending = (async () => {
110
+ if (typeof key !== 'string' && this.persistence !== undefined) {
111
+ try {
112
+ const persisted = await this.persistence.read(key);
113
+ if (persisted !== undefined)
114
+ return persisted;
115
+ }
116
+ catch {
117
+ // Persistence is an optimization; a damaged/unavailable sidecar must
118
+ // never block the model request from recomputing evidence.
119
+ }
120
+ }
121
+ const block = await load();
122
+ if (typeof key !== 'string' && this.persistence !== undefined) {
123
+ try {
124
+ await this.persistence.write(key, block);
125
+ }
126
+ catch {
127
+ // Keep the process-local result even when durability fails.
128
+ }
129
+ }
130
+ return block;
131
+ })().then(block => block, (error) => {
132
+ // Only evict our own entry: this promise may have been LRU-evicted and
133
+ // the key re-populated by a newer read meanwhile.
134
+ if (this.entries.get(memoryKey) === pending) {
135
+ this.entries.delete(memoryKey);
136
+ }
137
+ throw error;
138
+ });
139
+ this.entries.set(memoryKey, pending);
140
+ while (this.entries.size > this.limit) {
141
+ const oldest = this.entries.keys().next().value;
142
+ if (oldest === undefined)
143
+ break;
144
+ this.entries.delete(oldest);
145
+ }
146
+ return pending;
147
+ }
148
+ /** Drop process-local descriptions; durable rows stay versioned by their runtime fingerprint. */
149
+ clear() {
150
+ this.entries.clear();
151
+ }
152
+ }
153
+ function identityOf(header) {
154
+ return Object.freeze({
155
+ createdAt: header.createdAt,
156
+ ...(header.cwd === undefined ? {} : { cwd: header.cwd }),
157
+ });
158
+ }
159
+ function sameIdentity(record, header) {
160
+ return record.session.createdAt === header.createdAt && record.session.cwd === header.cwd;
161
+ }
162
+ function matchesKey(record, key) {
163
+ return record.contractVersion === key.contractVersion
164
+ && record.sessionId === key.sessionId
165
+ && record.attachmentId === key.attachmentId
166
+ && record.promptHash === key.promptHash
167
+ && record.runtimeHash === key.runtimeHash;
168
+ }
169
+ function byteLength(text) {
170
+ return Buffer.byteLength(text, 'utf8');
171
+ }
172
+ function messageOf(error) {
173
+ return error instanceof Error ? error.message : String(error);
174
+ }
175
+ function parseRecord(value) {
176
+ try {
177
+ const parsed = JSON.parse(value);
178
+ const result = evidenceRecordSchema.safeParse(parsed);
179
+ return result.success ? result.data : undefined;
180
+ }
181
+ catch {
182
+ return undefined;
183
+ }
184
+ }
185
+ /** Official DSH storage-domain sidecar used to survive Profile restarts. */
186
+ export class SessionEvidenceStore {
187
+ ctx;
188
+ storage;
189
+ storageFiber;
190
+ storageReady;
191
+ mutationTail = Promise.resolve();
192
+ flushes = new Map();
193
+ warned = false;
194
+ maxEntries;
195
+ maxBytes;
196
+ maxEntryBytes;
197
+ now;
198
+ constructor(ctx, options = {}) {
199
+ this.ctx = ctx;
200
+ this.maxEntries = options.maxEntries ?? DEFAULT_PERSISTED_ENTRY_LIMIT;
201
+ this.maxBytes = options.maxBytes ?? DEFAULT_PERSISTED_BYTE_LIMIT;
202
+ this.maxEntryBytes = options.maxEntryBytes ?? DEFAULT_PERSISTED_ENTRY_BYTE_LIMIT;
203
+ this.now = options.now ?? Date.now;
204
+ if (typeof ctx.inject !== 'function')
205
+ return;
206
+ this.storageFiber = ctx.inject(['storageDomain'], async (storageCtx) => {
207
+ const domain = await storageCtx.storageDomain.open(evidenceCacheDomainSpec);
208
+ const binding = { table: domain.table('evidence') };
209
+ try {
210
+ await this.enqueueMutation(async () => { await this.prune(binding.table); });
211
+ this.storage = binding;
212
+ }
213
+ catch (error) {
214
+ await domain.close();
215
+ throw error;
216
+ }
217
+ return async () => {
218
+ if (this.storage === binding)
219
+ this.storage = undefined;
220
+ await this.mutationTail;
221
+ await domain.close();
222
+ };
223
+ });
224
+ this.storageReady = Promise.resolve(this.storageFiber).then(() => undefined, (error) => { this.warnOnce(error); });
225
+ }
226
+ /** Release the optional storage binding with the owning variant lifecycle. */
227
+ dispose() {
228
+ const fiber = this.storageFiber;
229
+ this.storageFiber = undefined;
230
+ this.storageReady = undefined;
231
+ if (fiber !== undefined)
232
+ void fiber.dispose().catch(error => { this.warnOnce(error); });
233
+ }
234
+ async read(key) {
235
+ const session = this.sessionFor(key);
236
+ if (session === undefined)
237
+ return undefined;
238
+ const binding = await this.prepareStorage();
239
+ if (binding === undefined)
240
+ return undefined;
241
+ const stored = binding.table.get(key.digest);
242
+ const record = stored === undefined ? undefined : parseRecord(stored);
243
+ if (record === undefined || !matchesKey(record, key) || !sameIdentity(record, session.header)) {
244
+ return undefined;
245
+ }
246
+ if (byteLength(record.text) > this.maxEntryBytes)
247
+ return undefined;
248
+ return { type: 'text', text: record.text };
249
+ }
250
+ async write(key, block) {
251
+ if (block.type !== 'text' || block.text.length === 0 || byteLength(block.text) > this.maxEntryBytes)
252
+ return;
253
+ const session = this.sessionFor(key);
254
+ if (session === undefined)
255
+ return;
256
+ const binding = await this.prepareStorage();
257
+ if (binding === undefined)
258
+ return;
259
+ try {
260
+ const participated = await this.flushSession(session);
261
+ if (!participated)
262
+ return;
263
+ const record = Object.freeze({
264
+ contractVersion: key.contractVersion,
265
+ sessionId: key.sessionId,
266
+ session: identityOf(session.header),
267
+ attachmentId: key.attachmentId,
268
+ promptHash: key.promptHash,
269
+ runtimeHash: key.runtimeHash,
270
+ text: block.text,
271
+ storedAt: this.now(),
272
+ });
273
+ await this.enqueueMutation(async () => {
274
+ await binding.table.put(key.digest, JSON.stringify(record));
275
+ await this.prune(binding.table);
276
+ });
277
+ }
278
+ catch (error) {
279
+ this.warnOnce(error);
280
+ }
281
+ }
282
+ sessionFor(key) {
283
+ if (key.sessionId === undefined)
284
+ return undefined;
285
+ const session = this.ctx.sessions.get(key.sessionId);
286
+ if (session === undefined)
287
+ return undefined;
288
+ if (key.sessionCreatedAt !== undefined
289
+ && (session.header.createdAt !== key.sessionCreatedAt || session.header.cwd !== key.sessionCwd)) {
290
+ return undefined;
291
+ }
292
+ return session;
293
+ }
294
+ async prepareStorage() {
295
+ if (this.storage !== undefined)
296
+ return this.storage;
297
+ if (this.ctx.get('storageDomain') === undefined)
298
+ return undefined;
299
+ await this.storageReady;
300
+ return this.storage;
301
+ }
302
+ enqueueMutation(operation) {
303
+ const result = this.mutationTail.then(operation);
304
+ this.mutationTail = result.then(() => undefined, () => undefined);
305
+ return result;
306
+ }
307
+ flushSession(session) {
308
+ const key = `${session.id}\u0000${session.header.createdAt}\u0000${session.header.cwd ?? ''}`;
309
+ const existing = this.flushes.get(key);
310
+ if (existing !== undefined)
311
+ return existing;
312
+ const pending = this.ctx.sessions.flush(session).finally(() => {
313
+ if (this.flushes.get(key) === pending)
314
+ this.flushes.delete(key);
315
+ });
316
+ this.flushes.set(key, pending);
317
+ return pending;
318
+ }
319
+ async prune(table) {
320
+ const records = [...table.entries()].map(([key, stored]) => {
321
+ const record = parseRecord(stored);
322
+ return {
323
+ key,
324
+ bytes: byteLength(stored),
325
+ storedAt: record?.storedAt ?? -1,
326
+ valid: record !== undefined && byteLength(record.text) <= this.maxEntryBytes,
327
+ };
328
+ });
329
+ let totalBytes = records.reduce((sum, record) => sum + record.bytes, 0);
330
+ let totalEntries = records.length;
331
+ records.sort((left, right) => Number(left.valid) - Number(right.valid)
332
+ || left.storedAt - right.storedAt
333
+ || left.key.localeCompare(right.key));
334
+ for (const record of records) {
335
+ if (record.valid && totalEntries <= this.maxEntries && totalBytes <= this.maxBytes)
336
+ break;
337
+ if (await table.delete(record.key)) {
338
+ totalEntries -= 1;
339
+ totalBytes -= record.bytes;
340
+ }
341
+ }
342
+ }
343
+ warnOnce(error) {
344
+ if (this.warned)
345
+ return;
346
+ this.warned = true;
347
+ this.ctx.logger?.warn('dsh-vision-toolkit: persistent image evidence cache is unavailable; using the process cache only. %s', messageOf(error).slice(0, 500));
348
+ }
349
+ }
350
+ //# sourceMappingURL=evidence-cache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"evidence-cache.js","sourceRoot":"","sources":["../src/evidence-cache.ts"],"names":[],"mappings":"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"}
@@ -13,7 +13,9 @@ import { mkdtemp, rm, writeFile } from 'node:fs/promises';
13
13
  import { tmpdir } from 'node:os';
14
14
  import { isAbsolute, join, resolve } from 'node:path';
15
15
  import { LlmAdapter, contentHasImage } from '@deepseek-ai/dsh-llm';
16
+ import { createEvidenceCacheKey, EvidenceCache, SessionEvidenceStore, } from "./evidence-cache.js";
16
17
  import { sessionPasteRoot } from "./paste-images.js";
18
+ export { EvidenceCache } from "./evidence-cache.js";
17
19
  /** Provider-id prefix for the variant routes this plugin registers. */
18
20
  export const VARIANT_PROVIDER_PREFIX = 'vision-toolkit-';
19
21
  /** Display suffix shared by variant provider names and variant model names. */
@@ -138,63 +140,10 @@ function assistantMessageText(message) {
138
140
  .filter(text => text.trim().length > 0)
139
141
  .join('\n\n');
140
142
  }
141
- function cacheKey(attachmentId, prompt, sessionId) {
142
- return `${sessionId ?? ''}\u0000${attachmentId}\u0000${prompt}`;
143
- }
144
143
  function contentHasText(blocks, text) {
145
144
  return blocks.some(block => (block.type === 'text' && block.text === text)
146
145
  || (block.type === 'tool-result' && contentHasText(block.content, text)));
147
146
  }
148
- /** Bounded promise cache for one attachment's description; failed reads are not retained. */
149
- export class EvidenceCache {
150
- limit;
151
- entries = new Map();
152
- constructor(limit) {
153
- this.limit = limit;
154
- }
155
- /**
156
- * Read one attachment-and-prompt key's entry or compute it. Concurrent readers join the in-flight
157
- * computation; a settled failure is evicted so a fixed configuration gets a
158
- * fresh chance.
159
- * @param key - the attachment identity plus the exact focus prompt.
160
- * @param load - computes the description; must resolve `{ ok, block }` and never reject.
161
- * @returns the cached or computed block.
162
- */
163
- read(key, load) {
164
- const existing = this.entries.get(key);
165
- if (existing !== undefined) {
166
- // Refresh recency: Map iteration order is insertion order.
167
- this.entries.delete(key);
168
- this.entries.set(key, existing);
169
- return existing;
170
- }
171
- const pending = load().then((result) => {
172
- // Only evict our own entry: this promise may have been LRU-evicted and
173
- // the key re-populated by a newer read meanwhile.
174
- if (!result.ok && this.entries.get(key) === pending) {
175
- this.entries.delete(key);
176
- }
177
- return result.block;
178
- }, (error) => {
179
- if (this.entries.get(key) === pending) {
180
- this.entries.delete(key);
181
- }
182
- throw error;
183
- });
184
- this.entries.set(key, pending);
185
- while (this.entries.size > this.limit) {
186
- const oldest = this.entries.keys().next().value;
187
- if (oldest === undefined)
188
- break;
189
- this.entries.delete(oldest);
190
- }
191
- return pending;
192
- }
193
- /** Drop every cached description (runtime reconfiguration invalidates provider-specific reads). */
194
- clear() {
195
- this.entries.clear();
196
- }
197
- }
198
147
  /**
199
148
  * Wait on a shared promise without inheriting its lifetime: the caller's
200
149
  * abort rejects this wait immediately, while the underlying read keeps
@@ -309,24 +258,24 @@ function createLimiter(limit) {
309
258
  }
310
259
  /**
311
260
  * Read one image block into a Vision Toolkit description text block. Never
312
- * throws: failures degrade to an explanatory block with `ok: false`, so the
313
- * caller can decide what a failure means (the cache refuses to memoize it).
261
+ * throws: failures become model-visible explanatory blocks and are cached like
262
+ * successful descriptions so replayed history stays byte-identical.
314
263
  * @param ctx - plugin context; reads the optional `attachments` service.
315
- * @param runtime - the currently serving Vision Toolkit runtime, if ready.
264
+ * @param runtime - the immutable runtime snapshot captured for this conversion.
316
265
  * @param block - the image block to describe.
317
266
  * @param query - the exact focus-hinted prompt sent to the vision model.
318
267
  * @param sessionId - the live Session identity, used to keep a model-visible copy.
319
- * @returns the outcome and its model-facing replacement block.
268
+ * @returns the model-facing replacement block.
320
269
  */
321
270
  async function readImageBlock(ctx, runtime, block, query, sessionId) {
322
271
  const attachments = ctx.get('attachments');
323
272
  const current = runtime();
324
273
  if (attachments === undefined) {
325
- return { ok: false, block: { type: 'text', text: `${UNAVAILABLE_PREFIX}the DSH attachment service is not ready] The vision tool is temporarily unavailable; let the user know.` } };
274
+ return { type: 'text', text: `${UNAVAILABLE_PREFIX}the DSH attachment service is not ready] The vision tool is temporarily unavailable; let the user know.` };
326
275
  }
327
276
  const extension = MEDIA_EXTENSIONS[block.attachment.mediaType];
328
277
  if (extension === undefined) {
329
- return { ok: false, block: { type: 'text', text: `${UNAVAILABLE_PREFIX}unsupported image media type ${block.attachment.mediaType}] The vision tool is temporarily unavailable; let the user know.` } };
278
+ return { type: 'text', text: `${UNAVAILABLE_PREFIX}unsupported image media type ${block.attachment.mediaType}] The vision tool is temporarily unavailable; let the user know.` };
330
279
  }
331
280
  let temporaryDirectory;
332
281
  let pathEvidence = '';
@@ -340,10 +289,7 @@ async function readImageBlock(ctx, runtime, block, query, sessionId) {
340
289
  // can still use the path with a later visual-tool call if the bridge is
341
290
  // temporarily unavailable on this turn.
342
291
  if (current === undefined) {
343
- return {
344
- ok: false,
345
- block: { type: 'text', text: `${pathEvidence}${pathEvidence === '' ? '' : '\n'}${UNAVAILABLE_PREFIX}the Vision Toolkit runtime is not ready] The vision tool is temporarily unavailable; let the user know.` },
346
- };
292
+ return { type: 'text', text: `${pathEvidence}${pathEvidence === '' ? '' : '\n'}${UNAVAILABLE_PREFIX}the Vision Toolkit runtime is not ready] The vision tool is temporarily unavailable; let the user know.` };
347
293
  }
348
294
  // A fresh signal on purpose: the cached run must not die with its first
349
295
  // caller (their abort used to cancel every concurrent joiner); the runtime
@@ -352,15 +298,12 @@ async function readImageBlock(ctx, runtime, block, query, sessionId) {
352
298
  const answer = result.answer.trim();
353
299
  if (answer.length === 0)
354
300
  throw new Error('the Vision Toolkit returned an empty description');
355
- return { ok: true, block: { type: 'text', text: `${pathEvidence}${pathEvidence === '' ? '' : '\n'}${DESCRIPTION_PREFIX}${answer}` } };
301
+ return { type: 'text', text: `${pathEvidence}${pathEvidence === '' ? '' : '\n'}${DESCRIPTION_PREFIX}${answer}` };
356
302
  }
357
303
  catch (error) {
358
304
  return {
359
- ok: false,
360
- block: {
361
- type: 'text',
362
- text: `${pathEvidence}${pathEvidence === '' ? '' : '\n'}${UNAVAILABLE_PREFIX}${messageOf(error).slice(0, 300)}] The vision tool is temporarily unavailable; let the user know.`,
363
- },
305
+ type: 'text',
306
+ text: `${pathEvidence}${pathEvidence === '' ? '' : '\n'}${UNAVAILABLE_PREFIX}${messageOf(error).slice(0, 300)}] The vision tool is temporarily unavailable; let the user know.`,
364
307
  };
365
308
  }
366
309
  finally {
@@ -374,14 +317,22 @@ async function readImageBlock(ctx, runtime, block, query, sessionId) {
374
317
  * The original messages are returned untouched when nothing carries an image;
375
318
  * converted messages are new objects, so the durable request stays immutable.
376
319
  * @param ctx - plugin context for the attachments service.
377
- * @param runtime - the currently serving runtime (lazily read per conversion).
378
- * @param cache - shared per-adapter description cache.
320
+ * @param runtime - the immutable runtime snapshot captured for this conversion.
321
+ * @param cache - shared process/durable description cache.
379
322
  * @param messages - the assembled request messages.
380
323
  * @param signal - the caller's cancellation for this conversion pass.
381
324
  * @param sessionId - the live Session identity, when available.
325
+ * @param runtimeHash - stable fingerprint of the vision provider and evidence runtime.
382
326
  * @returns the rewritten message list.
383
327
  */
384
- export async function convertImagesToEvidence(ctx, runtime, cache, messages, signal, sessionId) {
328
+ export async function convertImagesToEvidence(ctx, runtime, cache, messages, signal, sessionId, runtimeHash = 'process-only-runtime') {
329
+ const session = sessionId === undefined ? undefined : ctx.sessions.get(sessionId);
330
+ const sessionIdentity = session === undefined
331
+ ? undefined
332
+ : {
333
+ createdAt: session.header.createdAt,
334
+ ...(session.header.cwd === undefined ? {} : { cwd: session.header.cwd }),
335
+ };
385
336
  const plans = [];
386
337
  let lastUserText = '';
387
338
  let lastAssistantText = '';
@@ -426,7 +377,13 @@ export async function convertImagesToEvidence(ctx, runtime, cache, messages, sig
426
377
  const converted = await Promise.all(plans.map(async ({ message, query }) => {
427
378
  if (query === undefined)
428
379
  return message;
429
- const content = await convertBlocks(message.content, (block) => abortableWait(limit(() => cache.read(cacheKey(String(block.attachment.attachmentId), query, sessionId), () => readImageBlock(ctx, runtime, block, query, sessionId)), signal), signal));
380
+ const content = await convertBlocks(message.content, (block) => abortableWait(limit(() => cache.read(createEvidenceCacheKey({
381
+ ...(sessionId === undefined ? {} : { sessionId }),
382
+ ...(sessionIdentity === undefined ? {} : { sessionIdentity }),
383
+ attachmentId: String(block.attachment.attachmentId),
384
+ prompt: query,
385
+ runtimeHash,
386
+ }), () => readImageBlock(ctx, runtime, block, query, sessionId)), signal), signal));
430
387
  return { ...message, content };
431
388
  }));
432
389
  if (!messages.some(message => contentHasText(message.content, CHANNEL_NOTE))) {
@@ -456,7 +413,6 @@ export class ImageInputVariantAdapter extends LlmAdapter {
456
413
  runtime;
457
414
  cache;
458
415
  hidden;
459
- lastRuntime;
460
416
  constructor(ctx, llm, upstream, upstreamName, runtime, cache, hidden = () => false) {
461
417
  super();
462
418
  this.ctx = ctx;
@@ -503,14 +459,23 @@ export class ImageInputVariantAdapter extends LlmAdapter {
503
459
  };
504
460
  }
505
461
  async *stream(options) {
506
- // A reconfigured runtime is a NEW instance; descriptions read through the
507
- // previous provider must not be replayed for the new one.
508
462
  const current = this.runtime();
509
- if (current !== this.lastRuntime) {
510
- this.cache.clear();
511
- this.lastRuntime = current;
463
+ let captured;
464
+ if (current !== undefined && options.messages.some(message => contentHasImage(message.content))) {
465
+ try {
466
+ captured = await current.captureEvidenceRuntime();
467
+ }
468
+ catch (error) {
469
+ // Keep the established graceful-degradation path. The unavailable block
470
+ // is cached under this fallback fingerprint so historical replay stays
471
+ // stable; a later successful credential capture produces a different key.
472
+ captured = Object.freeze({
473
+ evidenceFingerprint: current.evidenceFingerprint,
474
+ glance: async () => { throw error; },
475
+ });
476
+ }
512
477
  }
513
- const messages = await convertImagesToEvidence(this.ctx, this.runtime, this.cache, options.messages, options.signal, options.sessionId === undefined ? undefined : String(options.sessionId));
478
+ const messages = await convertImagesToEvidence(this.ctx, () => captured, this.cache, options.messages, options.signal, options.sessionId === undefined ? undefined : String(options.sessionId), captured?.evidenceFingerprint ?? 'process-only-runtime');
514
479
  // Delegate through the host service under the upstream route: the variant
515
480
  // is a wire-only facade, and the upstream route owns retry and replay.
516
481
  yield* this.llm.stream({ ...options, provider: this.upstream, messages });
@@ -739,6 +704,8 @@ export function createPasteTakeoverResolver(ctx, getConfig) {
739
704
  * @returns the disposer and a manual re-sweep trigger (settings changes).
740
705
  */
741
706
  export function installImageInputVariants(ctx, getConfig, getRuntime) {
707
+ const evidenceStore = new SessionEvidenceStore(ctx);
708
+ const evidenceCache = new EvidenceCache(EVIDENCE_CACHE_LIMIT, evidenceStore);
742
709
  const registrations = new Map();
743
710
  // The host snapshots adapter provider metadata (including the group display
744
711
  // name) at registration time, so a transparent-routing toggle must rebuild
@@ -868,7 +835,7 @@ export function installImageInputVariants(ctx, getConfig, getRuntime) {
868
835
  if (disposed)
869
836
  return;
870
837
  try {
871
- const dispose = llm.registerAdapter([variantId], new ImageInputVariantAdapter(ctx, llm, upstream, provider.name, getRuntime, new EvidenceCache(EVIDENCE_CACHE_LIMIT), () => getConfig().imageInputVariants.hidden));
838
+ const dispose = llm.registerAdapter([variantId], new ImageInputVariantAdapter(ctx, llm, upstream, provider.name, getRuntime, evidenceCache, () => getConfig().imageInputVariants.hidden));
872
839
  registrations.set(upstream, dispose);
873
840
  stale.delete(upstream);
874
841
  }
@@ -894,6 +861,7 @@ export function installImageInputVariants(ctx, getConfig, getRuntime) {
894
861
  disposed = true;
895
862
  clearInterval(timer);
896
863
  releaseAll();
864
+ evidenceStore.dispose();
897
865
  },
898
866
  reconcile: () => { sweep(); },
899
867
  };