dsh-vision-router 2.3.0 → 3.0.1

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 (105) hide show
  1. package/README.md +18 -3
  2. package/README.zh.md +16 -3
  3. package/docs/architecture/3.0-architecture-plan.md +252 -0
  4. package/docs/architecture/3.0-r0-contract-ledger.md +137 -0
  5. package/docs/architecture/3.0-r1-source-artifact-boundary.md +98 -0
  6. package/docs/architecture/3.0-r10-final-correctness-closure.md +207 -0
  7. package/docs/architecture/3.0-r11-adversarial-review.md +447 -0
  8. package/docs/architecture/3.0-r5-browser-go-no-go.md +165 -0
  9. package/docs/architecture/3.0-r5-browser-typescript-gate.md +105 -0
  10. package/docs/architecture/3.0-r6-compat-retirement.md +203 -0
  11. package/docs/architecture/3.0-r7-public-api.md +151 -0
  12. package/docs/architecture/3.0-r8-ci-release.md +248 -0
  13. package/docs/architecture/3.0-r9-final-audit.md +370 -0
  14. package/docs/architecture/dsh-support-window.md +13 -3
  15. package/docs/releases/v3.0.0.md +85 -0
  16. package/docs/releases/v3.0.1.md +77 -0
  17. package/entry.js +1 -1
  18. package/index.js +110 -71
  19. package/lib/agent-request-route-authority.js +1 -20
  20. package/lib/core-apply-lifecycle.js +37 -43
  21. package/lib/core-cache.js +87 -0
  22. package/lib/core-primitives.js +11 -144
  23. package/lib/core-vision-surface-domain.js +63 -0
  24. package/lib/core-vision-surface.js +11 -77
  25. package/lib/delegated-call-config.js +15 -24
  26. package/lib/doctor-cli.js +0 -0
  27. package/lib/doctor-runtime.js +4 -1
  28. package/lib/dsh-contract-compat.js +26 -230
  29. package/lib/dsh-host-capabilities.js +3 -9
  30. package/lib/dsh-settings-017-compat.js +17 -10
  31. package/lib/file-logger.js +8 -3
  32. package/lib/grounding-coordinate-runtime.js +1 -2
  33. package/lib/http-body-limit.js +97 -79
  34. package/lib/image-offload-compat.js +30 -21
  35. package/lib/image-resource-governor.js +210 -228
  36. package/lib/legacy-global-proxy-boundary.js +46 -3
  37. package/lib/live-model-client-prelude.js +12 -4
  38. package/lib/live-model-discovery.js +134 -43
  39. package/lib/local-vision-stabilizer.js +33 -18
  40. package/lib/maintenance-routes.js +1 -3
  41. package/lib/native-image-coexistence.js +15 -3
  42. package/lib/ollama-cold-start.js +87 -23
  43. package/lib/pi-ai-bridge-wire-compat.js +3 -2
  44. package/lib/proxy-dispatcher-pool.js +151 -92
  45. package/lib/proxy-routing.js +75 -51
  46. package/lib/proxy-url-compat.js +3 -10
  47. package/lib/public-config.js +23 -44
  48. package/lib/public-entry.d.ts +148 -0
  49. package/lib/public-entry.js +17 -4
  50. package/lib/replay-delegation.js +12 -7
  51. package/lib/replay-envelope-v2-compat.js +12 -7
  52. package/lib/runtime-composition.js +30 -10
  53. package/lib/screenshot-source-boundary.js +2 -2
  54. package/lib/session-affinity-runtime.js +113 -97
  55. package/lib/session-affinity.js +89 -76
  56. package/lib/session-event-history-compat.js +30 -0
  57. package/lib/session-query-readers.js +263 -0
  58. package/lib/session-surface-compat.js +30 -31
  59. package/lib/session-surface-policy-domain.js +85 -0
  60. package/lib/session-surface-policy.js +2 -111
  61. package/lib/session-turn-resolver.js +2 -6
  62. package/lib/session-vision-attachment-index.js +143 -0
  63. package/lib/session-vision-delegation-boundary.js +34 -0
  64. package/lib/session-vision-delegation.js +22 -0
  65. package/lib/session-vision-index.js +25 -107
  66. package/lib/session-vision-mode-authority.js +229 -192
  67. package/lib/session-vision-mode-boundary.js +65 -22
  68. package/lib/session-vision-policy-domain.js +304 -0
  69. package/lib/session-vision-policy.js +104 -0
  70. package/lib/session-vision-state.js +368 -378
  71. package/lib/settings-client-017-compat.js +16 -10
  72. package/lib/structured-flow-hardening.js +22 -4
  73. package/lib/tesseract-exec-compat.js +9 -1
  74. package/lib/turn-budget-context.js +47 -41
  75. package/lib/update-check.js +11 -2
  76. package/lib/vision-attachment-handle-runtime.js +176 -156
  77. package/lib/vision-backend-runtime-policy.js +16 -0
  78. package/lib/vision-background-benchmark.js +55 -6
  79. package/lib/vision-background-failure-policy.js +41 -41
  80. package/lib/vision-background-stop-store-core.js +217 -0
  81. package/lib/vision-background-stop-store.js +8 -179
  82. package/lib/vision-capability-benchmark.js +11 -61
  83. package/lib/vision-capability-evidence.js +35 -0
  84. package/lib/vision-capability-fingerprint.js +72 -0
  85. package/lib/vision-capability-identity.js +116 -61
  86. package/lib/vision-capability-router.js +13 -33
  87. package/lib/vision-chunk-assembler.js +95 -0
  88. package/lib/vision-diagnostics-routes.js +42 -4
  89. package/lib/vision-execution-order-apply.js +45 -55
  90. package/lib/vision-execution-order.js +19 -19
  91. package/lib/vision-execution-policy.js +12 -7
  92. package/lib/vision-model-registry.js +30 -17
  93. package/lib/vision-model-visibility-boundary-main.js +15 -16
  94. package/lib/vision-provider-transport.js +137 -147
  95. package/lib/vision-routing-authority.js +47 -48
  96. package/lib/vision-routing-product.js +26 -29
  97. package/lib/vision-routing-settings-prelude.js +8 -3
  98. package/lib/vision-runtime-performance-store.js +166 -0
  99. package/lib/vision-runtime-performance.js +13 -142
  100. package/lib/vision-tool-runtime-boundary.js +99 -30
  101. package/lib/web/dsh-settings-017-root-transport.js +27 -12
  102. package/lib/web/remote-settings-client.js +4 -7
  103. package/lib/web-capability-boundary.js +17 -7
  104. package/lib/wrapper-directory.js +14 -23
  105. package/package.json +37 -11
@@ -1,404 +1,394 @@
1
1
  const DEFAULTS = Object.freeze({
2
- maxSessions: 64,
3
- idleTtlMs: 60 * 60 * 1000,
4
- descriptionMaxEntries: 64,
5
- descriptionMaxChars: 256 * 1024,
6
- attachmentMaxEntries: 256,
7
- })
8
-
9
- // Exact Session-object bridge for entry-layer compatibility policy. The value
10
- // remains a view over the owning store rather than a copied Map, so eviction,
11
- // forgetSession(), and bounded-LRU semantics stay authoritative in one place.
12
- // Weak keys prevent a finished Session from being retained by this seam.
13
- const knownSessionMemoryViews = new WeakMap()
14
-
2
+ maxSessions: 64,
3
+ idleTtlMs: 60 * 60 * 1000,
4
+ descriptionMaxEntries: 64,
5
+ descriptionMaxChars: 256 * 1024,
6
+ attachmentMaxEntries: 256,
7
+ });
8
+ const knownSessionMemoryViews = new WeakMap();
9
+ function objectRecord(value) {
10
+ return value !== null && (typeof value === 'object' || typeof value === 'function')
11
+ ? value
12
+ : undefined;
13
+ }
15
14
  function isSessionKey(value) {
16
- return value !== null && (typeof value === 'object' || typeof value === 'function')
15
+ return value !== null && (typeof value === 'object' || typeof value === 'function');
16
+ }
17
+ function attachmentRef(value) {
18
+ return objectRecord(value) ? value : undefined;
19
+ }
20
+ function attachmentIdOf(value) {
21
+ const record = objectRecord(value);
22
+ return record?.attachmentId ?? record?.id;
17
23
  }
18
-
19
24
  export function knownSessionVisionMemory(session) {
20
- return isSessionKey(session) ? knownSessionMemoryViews.get(session) : undefined
25
+ return isSessionKey(session) ? knownSessionMemoryViews.get(session) : undefined;
21
26
  }
22
-
23
27
  class WeightedLruMap {
24
- constructor({ maxEntries, maxWeight = Infinity, weightOf = () => 1 } = {}) {
25
- this.maxEntries = Math.max(1, Math.floor(Number(maxEntries) || 1))
26
- this.maxWeight = Number.isFinite(maxWeight) && maxWeight >= 0 ? maxWeight : Infinity
27
- this.weightOf = typeof weightOf === 'function' ? weightOf : () => 1
28
- this.entries = new Map()
29
- this.weight = 0
30
- }
31
-
32
- _weight(value, key) {
33
- const weight = Number(this.weightOf(value, key))
34
- return Number.isFinite(weight) && weight > 0 ? weight : 0
35
- }
36
-
37
- get(key) {
38
- const entry = this.entries.get(key)
39
- if (entry === undefined) return undefined
40
- this.entries.delete(key)
41
- this.entries.set(key, entry)
42
- return entry.value
43
- }
44
-
45
- peek(key) {
46
- return this.entries.get(key)?.value
47
- }
48
-
49
- has(key) {
50
- return this.entries.has(key)
51
- }
52
-
53
- set(key, value) {
54
- const old = this.entries.get(key)
55
- if (old !== undefined) {
56
- this.weight -= old.weight
57
- this.entries.delete(key)
28
+ maxEntries;
29
+ maxWeight;
30
+ weightOf;
31
+ entries = new Map();
32
+ weight = 0;
33
+ constructor({ maxEntries, maxWeight = Infinity, weightOf = () => 1, } = {}) {
34
+ this.maxEntries = Math.max(1, Math.floor(Number(maxEntries) || 1));
35
+ this.maxWeight = Number.isFinite(Number(maxWeight)) && Number(maxWeight) >= 0
36
+ ? Number(maxWeight)
37
+ : Infinity;
38
+ this.weightOf = typeof weightOf === 'function' ? weightOf : () => 1;
39
+ }
40
+ _weight(value, key) {
41
+ const weight = Number(this.weightOf(value, key));
42
+ return Number.isFinite(weight) && weight > 0 ? weight : 0;
43
+ }
44
+ get(key) {
45
+ const entry = this.entries.get(key);
46
+ if (entry === undefined)
47
+ return undefined;
48
+ this.entries.delete(key);
49
+ this.entries.set(key, entry);
50
+ return entry.value;
51
+ }
52
+ peek(key) {
53
+ return this.entries.get(key)?.value;
54
+ }
55
+ has(key) {
56
+ return this.entries.has(key);
57
+ }
58
+ set(key, value) {
59
+ const old = this.entries.get(key);
60
+ if (old !== undefined) {
61
+ this.weight -= old.weight;
62
+ this.entries.delete(key);
63
+ }
64
+ const weight = this._weight(value, key);
65
+ this.entries.set(key, { value, weight });
66
+ this.weight += weight;
67
+ this._trim();
68
+ return this;
69
+ }
70
+ delete(key) {
71
+ const entry = this.entries.get(key);
72
+ if (entry === undefined)
73
+ return false;
74
+ this.weight -= entry.weight;
75
+ return this.entries.delete(key);
76
+ }
77
+ clear() {
78
+ this.entries.clear();
79
+ this.weight = 0;
80
+ }
81
+ _trim() {
82
+ while (this.entries.size > this.maxEntries || this.weight > this.maxWeight) {
83
+ const oldest = this.entries.keys().next().value;
84
+ if (oldest === undefined)
85
+ break;
86
+ this.delete(oldest);
87
+ }
88
+ }
89
+ get size() {
90
+ return this.entries.size;
91
+ }
92
+ keys() {
93
+ return this.entries.keys();
58
94
  }
59
- const weight = this._weight(value, key)
60
- this.entries.set(key, { value, weight })
61
- this.weight += weight
62
- this._trim()
63
- return this
64
- }
65
-
66
- delete(key) {
67
- const entry = this.entries.get(key)
68
- if (entry === undefined) return false
69
- this.weight -= entry.weight
70
- return this.entries.delete(key)
71
- }
72
-
73
- clear() {
74
- this.entries.clear()
75
- this.weight = 0
76
- }
77
-
78
- _trim() {
79
- while (this.entries.size > this.maxEntries || this.weight > this.maxWeight) {
80
- const oldest = this.entries.keys().next().value
81
- if (oldest === undefined) break
82
- this.delete(oldest)
95
+ values() {
96
+ return [...this.entries.values()].map((entry) => entry.value).values();
97
+ }
98
+ entriesIterator() {
99
+ return [...this.entries.entries()].map(([key, entry]) => [key, entry.value]).values();
83
100
  }
84
- }
85
-
86
- get size() {
87
- return this.entries.size
88
- }
89
-
90
- keys() {
91
- return this.entries.keys()
92
- }
93
-
94
- values() {
95
- return [...this.entries.values()].map((entry) => entry.value).values()
96
- }
97
-
98
- entriesIterator() {
99
- return [...this.entries.entries()].map(([key, entry]) => [key, entry.value]).values()
100
- }
101
101
  }
102
-
103
102
  function normalizeId(value) {
104
- if (value === undefined || value === null) return undefined
105
- const id = String(value)
106
- return id === '' ? undefined : id
103
+ if (value === undefined || value === null)
104
+ return undefined;
105
+ const id = String(value);
106
+ return id === '' ? undefined : id;
107
107
  }
108
-
109
108
  function textWeight(value) {
110
- return typeof value === 'string' ? value.length : String(value ?? '').length
109
+ return typeof value === 'string' ? value.length : String(value ?? '').length;
111
110
  }
112
-
113
111
  function createState(key, stable, options, now) {
114
- return {
115
- key,
116
- stable,
117
- lastAccessAt: now,
118
- descriptions: new WeightedLruMap({
119
- maxEntries: options.descriptionMaxEntries,
120
- maxWeight: options.descriptionMaxChars,
121
- weightOf: textWeight,
122
- }),
123
- attachments: new WeightedLruMap({
124
- maxEntries: options.attachmentMaxEntries,
125
- }),
126
- }
112
+ return {
113
+ key,
114
+ stable,
115
+ lastAccessAt: now,
116
+ descriptions: new WeightedLruMap({
117
+ maxEntries: options.descriptionMaxEntries,
118
+ maxWeight: options.descriptionMaxChars,
119
+ weightOf: textWeight,
120
+ }),
121
+ attachments: new WeightedLruMap({
122
+ maxEntries: options.attachmentMaxEntries,
123
+ }),
124
+ };
127
125
  }
128
-
129
126
  class SessionMemoryView extends Map {
130
- constructor(store, session) {
131
- super()
132
- this.store = store
133
- this.session = session
134
- }
135
-
136
- get(key) {
137
- return this.store.getDescription(this.session, key)
138
- }
139
-
140
- has(key) {
141
- return this.store.hasDescription(this.session, key)
142
- }
143
-
144
- set(key, value) {
145
- this.store.setDescription(this.session, key, value)
146
- return this
147
- }
148
-
149
- delete(key) {
150
- return this.store.deleteDescription(this.session, key)
151
- }
152
-
153
- clear() {
154
- this.store.clearDescriptions(this.session)
155
- }
156
-
157
- get size() {
158
- return this.store.stateStats(this.session)?.descriptions ?? 0
159
- }
127
+ store;
128
+ session;
129
+ constructor(store, session) {
130
+ super();
131
+ this.store = store;
132
+ this.session = session;
133
+ }
134
+ get(key) {
135
+ return this.store.getDescription(this.session, key);
136
+ }
137
+ has(key) {
138
+ return this.store.hasDescription(this.session, key);
139
+ }
140
+ set(key, value) {
141
+ this.store.setDescription(this.session, key, value);
142
+ return this;
143
+ }
144
+ delete(key) {
145
+ return this.store.deleteDescription(this.session, key);
146
+ }
147
+ clear() {
148
+ this.store.clearDescriptions(this.session);
149
+ }
150
+ get size() {
151
+ return this.store.stateStats(this.session)?.descriptions ?? 0;
152
+ }
160
153
  }
161
-
162
154
  class DescriptionFacade extends Map {
163
- constructor(store) {
164
- super()
165
- this.store = store
166
- }
167
-
168
- get(key) {
169
- const state = this.store.uniqueStableOwner(key)
170
- return state?.descriptions.get(String(key))
171
- }
172
-
173
- has(key) {
174
- const state = this.store.uniqueStableOwner(key)
175
- return state?.descriptions.has(String(key)) === true
176
- }
177
-
178
- set(key, value) {
179
- const state = this.store.uniqueStableOwner(key)
180
- if (state !== undefined) {
181
- state.descriptions.set(String(key), value)
182
- this.store.touchState(state)
155
+ store;
156
+ constructor(store) {
157
+ super();
158
+ this.store = store;
183
159
  }
184
- return this
185
- }
186
-
187
- delete(key) {
188
- const state = this.store.uniqueStableOwner(key)
189
- if (state === undefined) return false
190
- this.store.touchState(state)
191
- return state.descriptions.delete(String(key))
192
- }
193
-
194
- clear() {
195
- for (const state of this.store.stableStates()) state.descriptions.clear()
196
- }
197
-
198
- get size() {
199
- let size = 0
200
- for (const state of this.store.stableStates()) size += state.descriptions.size
201
- return size
202
- }
203
- }
204
-
205
- export function createSessionVisionStateStore(config = {}) {
206
- const options = {
207
- maxSessions: Math.max(1, Math.floor(Number(config.maxSessions) || DEFAULTS.maxSessions)),
208
- idleTtlMs:
209
- Number.isFinite(Number(config.idleTtlMs)) && Number(config.idleTtlMs) >= 0
210
- ? Number(config.idleTtlMs)
211
- : DEFAULTS.idleTtlMs,
212
- descriptionMaxEntries: Math.max(
213
- 1,
214
- Math.floor(Number(config.descriptionMaxEntries) || DEFAULTS.descriptionMaxEntries),
215
- ),
216
- descriptionMaxChars: Math.max(
217
- 1,
218
- Math.floor(Number(config.descriptionMaxChars) || DEFAULTS.descriptionMaxChars),
219
- ),
220
- attachmentMaxEntries: Math.max(
221
- 1,
222
- Math.floor(Number(config.attachmentMaxEntries) || DEFAULTS.attachmentMaxEntries),
223
- ),
224
- }
225
- const now = typeof config.now === 'function' ? config.now : Date.now
226
- const statesById = new Map()
227
- const weakStates = new WeakMap()
228
-
229
- const prune = () => {
230
- const cutoff = options.idleTtlMs <= 0 ? -Infinity : now() - options.idleTtlMs
231
- for (const [key, state] of statesById) {
232
- if (state.lastAccessAt < cutoff) statesById.delete(key)
160
+ get(key) {
161
+ const state = this.store.uniqueStableOwner(key);
162
+ return state?.descriptions.get(String(key));
233
163
  }
234
- while (statesById.size > options.maxSessions) {
235
- const oldest = statesById.keys().next().value
236
- if (oldest === undefined) break
237
- statesById.delete(oldest)
164
+ has(key) {
165
+ const state = this.store.uniqueStableOwner(key);
166
+ return state?.descriptions.has(String(key)) === true;
238
167
  }
239
- }
240
-
241
- const touchState = (state) => {
242
- state.lastAccessAt = now()
243
- if (!state.stable) return
244
- if (statesById.get(state.key) === state) {
245
- statesById.delete(state.key)
246
- statesById.set(state.key, state)
168
+ set(key, value) {
169
+ const state = this.store.uniqueStableOwner(key);
170
+ if (state !== undefined) {
171
+ state.descriptions.set(String(key), value);
172
+ this.store.touchState(state);
173
+ }
174
+ return this;
247
175
  }
248
- prune()
249
- }
250
-
251
- const stateFor = (session, create = true) => {
252
- if (!isSessionKey(session)) return undefined
253
- prune()
254
- const id = normalizeId(session.id)
255
- if (id !== undefined) {
256
- let state = statesById.get(id)
257
- if (state === undefined && create) {
258
- state = createState(id, true, options, now())
259
- statesById.set(id, state)
260
- prune()
261
- }
262
- if (state !== undefined) touchState(state)
263
- return state
176
+ delete(key) {
177
+ const state = this.store.uniqueStableOwner(key);
178
+ if (state === undefined)
179
+ return false;
180
+ this.store.touchState(state);
181
+ return state.descriptions.delete(String(key));
264
182
  }
265
- let state = weakStates.get(session)
266
- if (state === undefined && create) {
267
- state = createState(undefined, false, options, now())
268
- weakStates.set(session, state)
183
+ clear() {
184
+ for (const state of this.store.stableStates())
185
+ state.descriptions.clear();
269
186
  }
270
- if (state !== undefined) touchState(state)
271
- return state
272
- }
273
-
274
- const stableStates = () => {
275
- prune()
276
- return [...statesById.values()]
277
- }
278
-
279
- const uniqueStableOwner = (attachmentId) => {
280
- const id = normalizeId(attachmentId)
281
- if (id === undefined) return undefined
282
- let owner
283
- for (const state of stableStates()) {
284
- if (!state.attachments.has(id) && !state.descriptions.has(id)) continue
285
- if (owner !== undefined && owner !== state) return undefined
286
- owner = state
187
+ get size() {
188
+ let size = 0;
189
+ for (const state of this.store.stableStates())
190
+ size += state.descriptions.size;
191
+ return size;
287
192
  }
288
- if (owner !== undefined) touchState(owner)
289
- return owner
290
- }
291
-
292
- const store = {
293
- options,
294
- stateFor,
295
- stableStates,
296
- uniqueStableOwner,
297
- touchState,
298
-
299
- memoryForSession(session) {
300
- const memory = new SessionMemoryView(store, session)
301
- if (isSessionKey(session)) knownSessionMemoryViews.set(session, memory)
302
- return memory
303
- },
304
-
305
- getDescription(session, attachmentId) {
306
- const id = normalizeId(attachmentId)
307
- if (id === undefined) return undefined
308
- return stateFor(session, false)?.descriptions.get(id)
309
- },
310
-
311
- hasDescription(session, attachmentId) {
312
- const id = normalizeId(attachmentId)
313
- if (id === undefined) return false
314
- return stateFor(session, false)?.descriptions.has(id) === true
315
- },
316
-
317
- setDescription(session, attachmentId, description) {
318
- const id = normalizeId(attachmentId)
319
- if (id === undefined) return false
320
- const state = stateFor(session, true)
321
- if (state === undefined) return false
322
- state.descriptions.set(id, description)
323
- touchState(state)
324
- return state.descriptions.has(id)
325
- },
326
-
327
- deleteDescription(session, attachmentId) {
328
- const id = normalizeId(attachmentId)
329
- if (id === undefined) return false
330
- const state = stateFor(session, false)
331
- if (state === undefined) return false
332
- touchState(state)
333
- return state.descriptions.delete(id)
334
- },
335
-
336
- clearDescriptions(session) {
337
- const state = stateFor(session, false)
338
- if (state !== undefined) state.descriptions.clear()
339
- },
340
-
341
- recordAttachments(session, refs) {
342
- if (!Array.isArray(refs) || refs.length === 0) return
343
- const state = stateFor(session, true)
344
- if (state === undefined) return
345
- for (const ref of refs) {
346
- const id = normalizeId(ref && (ref.attachmentId ?? ref.id))
347
- if (id !== undefined) state.attachments.set(id, ref)
348
- }
349
- touchState(state)
350
- },
351
-
352
- lookupAttachment(session, attachmentId) {
353
- const id = normalizeId(attachmentId)
354
- if (id === undefined) return undefined
355
- return stateFor(session, false)?.attachments.get(id)
356
- },
357
-
358
-
359
- forgetSession(sessionOrId) {
360
- const id =
361
- typeof sessionOrId === 'string' || typeof sessionOrId === 'number'
362
- ? normalizeId(sessionOrId)
363
- : normalizeId(sessionOrId && sessionOrId.id)
364
- if (id !== undefined) return statesById.delete(id)
365
- if (isSessionKey(sessionOrId)) {
366
- knownSessionMemoryViews.delete(sessionOrId)
367
- return weakStates.delete(sessionOrId)
368
- }
369
- return false
370
- },
371
-
372
- stateStats(session) {
373
- const state = stateFor(session, false)
374
- if (state === undefined) return undefined
375
- return {
376
- stable: state.stable,
377
- descriptions: state.descriptions.size,
378
- descriptionChars: state.descriptions.weight,
379
- attachments: state.attachments.size,
380
- }
381
- },
382
-
383
- stats() {
384
- prune()
385
- let descriptions = 0
386
- let descriptionChars = 0
387
- let attachments = 0
388
- for (const state of statesById.values()) {
389
- descriptions += state.descriptions.size
390
- descriptionChars += state.descriptions.weight
391
- attachments += state.attachments.size
392
- }
393
- return {
394
- stableSessions: statesById.size,
395
- descriptions,
396
- descriptionChars,
397
- attachments,
398
- }
399
- },
400
- }
401
-
402
- store.descriptionFacade = new DescriptionFacade(store)
403
- return store
193
+ }
194
+ export function createSessionVisionStateStore(config = {}) {
195
+ const source = objectRecord(config) ?? {};
196
+ const options = {
197
+ maxSessions: Math.max(1, Math.floor(Number(source.maxSessions) || DEFAULTS.maxSessions)),
198
+ idleTtlMs: Number.isFinite(Number(source.idleTtlMs)) && Number(source.idleTtlMs) >= 0
199
+ ? Number(source.idleTtlMs)
200
+ : DEFAULTS.idleTtlMs,
201
+ descriptionMaxEntries: Math.max(1, Math.floor(Number(source.descriptionMaxEntries) || DEFAULTS.descriptionMaxEntries)),
202
+ descriptionMaxChars: Math.max(1, Math.floor(Number(source.descriptionMaxChars) || DEFAULTS.descriptionMaxChars)),
203
+ attachmentMaxEntries: Math.max(1, Math.floor(Number(source.attachmentMaxEntries) || DEFAULTS.attachmentMaxEntries)),
204
+ };
205
+ const now = typeof source.now === 'function'
206
+ ? source.now
207
+ : Date.now;
208
+ const statesById = new Map();
209
+ const weakStates = new WeakMap();
210
+ const prune = () => {
211
+ const cutoff = options.idleTtlMs <= 0 ? -Infinity : now() - options.idleTtlMs;
212
+ for (const [key, state] of statesById) {
213
+ if (state.lastAccessAt < cutoff)
214
+ statesById.delete(key);
215
+ }
216
+ while (statesById.size > options.maxSessions) {
217
+ const oldest = statesById.keys().next().value;
218
+ if (oldest === undefined)
219
+ break;
220
+ statesById.delete(oldest);
221
+ }
222
+ };
223
+ const touchState = (state) => {
224
+ state.lastAccessAt = now();
225
+ if (!state.stable || state.key === undefined)
226
+ return;
227
+ if (statesById.get(state.key) === state) {
228
+ statesById.delete(state.key);
229
+ statesById.set(state.key, state);
230
+ }
231
+ prune();
232
+ };
233
+ const stateFor = (session, create = true) => {
234
+ if (!isSessionKey(session))
235
+ return undefined;
236
+ prune();
237
+ const id = normalizeId(objectRecord(session)?.id);
238
+ if (id !== undefined) {
239
+ let state = statesById.get(id);
240
+ if (state === undefined && create) {
241
+ state = createState(id, true, options, now());
242
+ statesById.set(id, state);
243
+ prune();
244
+ }
245
+ if (state !== undefined)
246
+ touchState(state);
247
+ return state;
248
+ }
249
+ let state = weakStates.get(session);
250
+ if (state === undefined && create) {
251
+ state = createState(undefined, false, options, now());
252
+ weakStates.set(session, state);
253
+ }
254
+ if (state !== undefined)
255
+ touchState(state);
256
+ return state;
257
+ };
258
+ const stableStates = () => {
259
+ prune();
260
+ return [...statesById.values()];
261
+ };
262
+ const uniqueStableOwner = (attachmentId) => {
263
+ const id = normalizeId(attachmentId);
264
+ if (id === undefined)
265
+ return undefined;
266
+ let owner;
267
+ for (const state of stableStates()) {
268
+ if (!state.attachments.has(id) && !state.descriptions.has(id))
269
+ continue;
270
+ if (owner !== undefined && owner !== state)
271
+ return undefined;
272
+ owner = state;
273
+ }
274
+ if (owner !== undefined)
275
+ touchState(owner);
276
+ return owner;
277
+ };
278
+ const store = {
279
+ options,
280
+ stateFor,
281
+ stableStates,
282
+ uniqueStableOwner,
283
+ touchState,
284
+ memoryForSession(session) {
285
+ const memory = new SessionMemoryView(store, session);
286
+ if (isSessionKey(session))
287
+ knownSessionMemoryViews.set(session, memory);
288
+ return memory;
289
+ },
290
+ getDescription(session, attachmentId) {
291
+ const id = normalizeId(attachmentId);
292
+ if (id === undefined)
293
+ return undefined;
294
+ return stateFor(session, false)?.descriptions.get(id);
295
+ },
296
+ hasDescription(session, attachmentId) {
297
+ const id = normalizeId(attachmentId);
298
+ if (id === undefined)
299
+ return false;
300
+ return stateFor(session, false)?.descriptions.has(id) === true;
301
+ },
302
+ setDescription(session, attachmentId, description) {
303
+ const id = normalizeId(attachmentId);
304
+ if (id === undefined)
305
+ return false;
306
+ const state = stateFor(session, true);
307
+ if (state === undefined)
308
+ return false;
309
+ state.descriptions.set(id, description);
310
+ touchState(state);
311
+ return state.descriptions.has(id);
312
+ },
313
+ deleteDescription(session, attachmentId) {
314
+ const id = normalizeId(attachmentId);
315
+ if (id === undefined)
316
+ return false;
317
+ const state = stateFor(session, false);
318
+ if (state === undefined)
319
+ return false;
320
+ touchState(state);
321
+ return state.descriptions.delete(id);
322
+ },
323
+ clearDescriptions(session) {
324
+ const state = stateFor(session, false);
325
+ if (state !== undefined)
326
+ state.descriptions.clear();
327
+ },
328
+ recordAttachments(session, refs) {
329
+ if (!Array.isArray(refs) || refs.length === 0)
330
+ return;
331
+ const state = stateFor(session, true);
332
+ if (state === undefined)
333
+ return;
334
+ for (const value of refs) {
335
+ const ref = attachmentRef(value);
336
+ if (ref === undefined)
337
+ continue;
338
+ const id = normalizeId(attachmentIdOf(ref));
339
+ if (id !== undefined)
340
+ state.attachments.set(id, ref);
341
+ }
342
+ touchState(state);
343
+ },
344
+ lookupAttachment(session, attachmentId) {
345
+ const id = normalizeId(attachmentId);
346
+ if (id === undefined)
347
+ return undefined;
348
+ return stateFor(session, false)?.attachments.get(id);
349
+ },
350
+ forgetSession(sessionOrId) {
351
+ const id = typeof sessionOrId === 'string' || typeof sessionOrId === 'number'
352
+ ? normalizeId(sessionOrId)
353
+ : normalizeId(objectRecord(sessionOrId)?.id);
354
+ if (id !== undefined)
355
+ return statesById.delete(id);
356
+ if (isSessionKey(sessionOrId)) {
357
+ knownSessionMemoryViews.delete(sessionOrId);
358
+ return weakStates.delete(sessionOrId);
359
+ }
360
+ return false;
361
+ },
362
+ stateStats(session) {
363
+ const state = stateFor(session, false);
364
+ if (state === undefined)
365
+ return undefined;
366
+ return {
367
+ stable: state.stable,
368
+ descriptions: state.descriptions.size,
369
+ descriptionChars: state.descriptions.weight,
370
+ attachments: state.attachments.size,
371
+ };
372
+ },
373
+ stats() {
374
+ prune();
375
+ let descriptions = 0;
376
+ let descriptionChars = 0;
377
+ let attachments = 0;
378
+ for (const state of statesById.values()) {
379
+ descriptions += state.descriptions.size;
380
+ descriptionChars += state.descriptions.weight;
381
+ attachments += state.attachments.size;
382
+ }
383
+ return {
384
+ stableSessions: statesById.size,
385
+ descriptions,
386
+ descriptionChars,
387
+ attachments,
388
+ };
389
+ },
390
+ descriptionFacade: undefined,
391
+ };
392
+ store.descriptionFacade = new DescriptionFacade(store);
393
+ return store;
404
394
  }