dsh-vision-router 2.3.0 → 3.0.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 (103) hide show
  1. package/README.md +12 -3
  2. package/README.zh.md +12 -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 +441 -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 +5 -3
  15. package/docs/releases/v3.0.0.md +85 -0
  16. package/entry.js +1 -1
  17. package/index.js +108 -71
  18. package/lib/agent-request-route-authority.js +1 -20
  19. package/lib/core-apply-lifecycle.js +37 -43
  20. package/lib/core-cache.js +87 -0
  21. package/lib/core-primitives.js +3 -143
  22. package/lib/core-vision-surface-domain.js +63 -0
  23. package/lib/core-vision-surface.js +11 -77
  24. package/lib/delegated-call-config.js +15 -24
  25. package/lib/doctor-cli.js +0 -0
  26. package/lib/doctor-runtime.js +4 -1
  27. package/lib/dsh-contract-compat.js +26 -230
  28. package/lib/dsh-host-capabilities.js +3 -9
  29. package/lib/dsh-settings-017-compat.js +17 -10
  30. package/lib/file-logger.js +8 -3
  31. package/lib/grounding-coordinate-runtime.js +1 -2
  32. package/lib/http-body-limit.js +97 -79
  33. package/lib/image-offload-compat.js +30 -21
  34. package/lib/image-resource-governor.js +210 -228
  35. package/lib/legacy-global-proxy-boundary.js +46 -3
  36. package/lib/live-model-client-prelude.js +12 -4
  37. package/lib/live-model-discovery.js +134 -43
  38. package/lib/local-vision-stabilizer.js +31 -17
  39. package/lib/maintenance-routes.js +1 -3
  40. package/lib/native-image-coexistence.js +15 -3
  41. package/lib/ollama-cold-start.js +87 -23
  42. package/lib/pi-ai-bridge-wire-compat.js +3 -2
  43. package/lib/proxy-dispatcher-pool.js +151 -92
  44. package/lib/proxy-routing.js +75 -51
  45. package/lib/proxy-url-compat.js +3 -10
  46. package/lib/public-config.js +23 -44
  47. package/lib/public-entry.d.ts +148 -0
  48. package/lib/public-entry.js +17 -4
  49. package/lib/replay-delegation.js +12 -7
  50. package/lib/replay-envelope-v2-compat.js +12 -7
  51. package/lib/runtime-composition.js +22 -4
  52. package/lib/screenshot-source-boundary.js +2 -2
  53. package/lib/session-affinity-runtime.js +113 -97
  54. package/lib/session-affinity.js +89 -76
  55. package/lib/session-event-history-compat.js +30 -0
  56. package/lib/session-query-readers.js +263 -0
  57. package/lib/session-surface-compat.js +30 -31
  58. package/lib/session-surface-policy-domain.js +85 -0
  59. package/lib/session-surface-policy.js +2 -111
  60. package/lib/session-turn-resolver.js +2 -6
  61. package/lib/session-vision-attachment-index.js +143 -0
  62. package/lib/session-vision-delegation-boundary.js +34 -0
  63. package/lib/session-vision-delegation.js +22 -0
  64. package/lib/session-vision-index.js +25 -107
  65. package/lib/session-vision-mode-authority.js +229 -192
  66. package/lib/session-vision-mode-boundary.js +65 -22
  67. package/lib/session-vision-policy-domain.js +304 -0
  68. package/lib/session-vision-policy.js +104 -0
  69. package/lib/session-vision-state.js +368 -378
  70. package/lib/settings-client-017-compat.js +9 -7
  71. package/lib/structured-flow-hardening.js +22 -4
  72. package/lib/tesseract-exec-compat.js +9 -1
  73. package/lib/turn-budget-context.js +47 -41
  74. package/lib/update-check.js +11 -2
  75. package/lib/vision-attachment-handle-runtime.js +176 -156
  76. package/lib/vision-backend-runtime-policy.js +16 -0
  77. package/lib/vision-background-benchmark.js +41 -4
  78. package/lib/vision-background-failure-policy.js +41 -41
  79. package/lib/vision-background-stop-store-core.js +217 -0
  80. package/lib/vision-background-stop-store.js +8 -179
  81. package/lib/vision-capability-benchmark.js +11 -61
  82. package/lib/vision-capability-evidence.js +35 -0
  83. package/lib/vision-capability-fingerprint.js +72 -0
  84. package/lib/vision-capability-identity.js +116 -61
  85. package/lib/vision-capability-router.js +13 -33
  86. package/lib/vision-chunk-assembler.js +95 -0
  87. package/lib/vision-diagnostics-routes.js +34 -4
  88. package/lib/vision-execution-order-apply.js +45 -55
  89. package/lib/vision-execution-order.js +19 -19
  90. package/lib/vision-execution-policy.js +12 -7
  91. package/lib/vision-model-registry.js +30 -17
  92. package/lib/vision-provider-transport.js +137 -147
  93. package/lib/vision-routing-authority.js +47 -48
  94. package/lib/vision-routing-product.js +26 -29
  95. package/lib/vision-routing-settings-prelude.js +8 -3
  96. package/lib/vision-runtime-performance-store.js +166 -0
  97. package/lib/vision-runtime-performance.js +13 -142
  98. package/lib/vision-tool-runtime-boundary.js +99 -30
  99. package/lib/web/dsh-settings-017-root-transport.js +27 -12
  100. package/lib/web/remote-settings-client.js +4 -7
  101. package/lib/web-capability-boundary.js +17 -7
  102. package/lib/wrapper-directory.js +14 -23
  103. package/package.json +37 -11
@@ -5,40 +5,37 @@
5
5
  // A user chooses whether routing is automatic or strictly ordered, then chooses
6
6
  // a plain-language preference. The actual execution-changing auto mode is
7
7
  // enabled only when the runtime is ready to honor this contract.
8
-
9
- export const VISION_ROUTING_MODES = Object.freeze(['ordered', 'auto'])
10
- export const VISION_ROUTING_PREFERENCES = Object.freeze(['balanced', 'quality', 'speed', 'local'])
11
-
12
- const MODE_SET = new Set(VISION_ROUTING_MODES)
13
- const PREFERENCE_SET = new Set(VISION_ROUTING_PREFERENCES)
14
-
8
+ export const VISION_ROUTING_MODES = Object.freeze(['ordered', 'auto']);
9
+ export const VISION_ROUTING_PREFERENCES = Object.freeze(['balanced', 'quality', 'speed', 'local']);
10
+ const MODE_SET = new Set(VISION_ROUTING_MODES);
11
+ const PREFERENCE_SET = new Set(VISION_ROUTING_PREFERENCES);
15
12
  export function normalizeVisionRoutingMode(value, fallback = 'ordered') {
16
- const normalizedFallback = MODE_SET.has(fallback) ? fallback : 'ordered'
17
- return typeof value === 'string' && MODE_SET.has(value.trim()) ? value.trim() : normalizedFallback
13
+ const normalizedFallback = typeof fallback === 'string' && MODE_SET.has(fallback) ? fallback : 'ordered';
14
+ const candidate = typeof value === 'string' ? value.trim() : '';
15
+ return MODE_SET.has(candidate) ? candidate : normalizedFallback;
18
16
  }
19
-
20
17
  export function normalizeVisionRoutingPreference(value) {
21
- if (typeof value === 'string') {
22
- const candidate = value.trim()
23
- if (PREFERENCE_SET.has(candidate)) return candidate
24
- }
25
-
26
- return 'balanced'
18
+ if (typeof value === 'string') {
19
+ const candidate = value.trim();
20
+ if (PREFERENCE_SET.has(candidate))
21
+ return candidate;
22
+ }
23
+ return 'balanced';
27
24
  }
28
-
29
25
  export function routingPreferenceToCapabilityStrategy(preference) {
30
- const normalized = normalizeVisionRoutingPreference(preference)
31
- return normalized === 'local' ? 'privacy' : normalized
26
+ const normalized = normalizeVisionRoutingPreference(preference);
27
+ return normalized === 'local' ? 'privacy' : normalized;
32
28
  }
33
-
34
29
  export function resolveVisionRoutingProduct(config = {}) {
35
- const source = config && typeof config === 'object' && !Array.isArray(config) ? config : {}
36
- const mode = normalizeVisionRoutingMode(source.routingMode)
37
- const preference = normalizeVisionRoutingPreference(source.routingPreference)
38
- return {
39
- mode,
40
- preference,
41
- strategy: routingPreferenceToCapabilityStrategy(preference),
42
- automatic: mode === 'auto',
43
- }
30
+ const source = config && typeof config === 'object' && !Array.isArray(config)
31
+ ? config
32
+ : {};
33
+ const mode = normalizeVisionRoutingMode(source.routingMode);
34
+ const preference = normalizeVisionRoutingPreference(source.routingPreference);
35
+ return {
36
+ mode,
37
+ preference,
38
+ strategy: routingPreferenceToCapabilityStrategy(preference),
39
+ automatic: mode === 'auto',
40
+ };
44
41
  }
@@ -446,7 +446,7 @@ export const VISION_ROUTING_SETTINGS_PRELUDE = String.raw`(function(){
446
446
  try { state.backgroundTimer && state.backgroundTimer.unref && state.backgroundTimer.unref(); } catch (_) {}
447
447
  schedule();
448
448
 
449
- var dispose = function() {
449
+ var dispose = async function() {
450
450
  state.disposed = true;
451
451
  if (state.scanTimer !== undefined) clearTimeout(state.scanTimer);
452
452
  if (state.backgroundTimer !== undefined) clearInterval(state.backgroundTimer);
@@ -456,13 +456,18 @@ export const VISION_ROUTING_SETTINGS_PRELUDE = String.raw`(function(){
456
456
  try { disposeRemote(); } catch (_) {}
457
457
  });
458
458
  state.remoteDisposers.length = 0;
459
- if (remote && scope && typeof scope.dispose === 'function') void scope.dispose();
460
459
  if (state.panel && state.panel.parentNode) state.panel.parentNode.removeChild(state.panel);
461
460
  if (states) states.delete(ctx);
461
+ if (remote && scope && typeof scope.dispose === 'function') await scope.dispose();
462
462
  };
463
+ var lifecycleOwned = false;
463
464
  try {
464
- if (typeof ctx.effect === 'function') ctx.effect(function(){ return dispose; }, 'vision-router: routing settings product panel');
465
+ if (typeof ctx.effect === 'function') {
466
+ ctx.effect(function(){ return dispose; }, 'vision-router: routing settings product panel');
467
+ lifecycleOwned = true;
468
+ }
465
469
  } catch (_) {}
470
+ if (!lifecycleOwned) void dispose().catch(function(){});
466
471
  }
467
472
 
468
473
  function patchLoader(loader) {
@@ -0,0 +1,166 @@
1
+ import { BENCHMARK_AXES, } from './vision-capability-evidence.js';
2
+ export const DEFAULT_RUNTIME_PERFORMANCE_MAX_AGE_MS = 60 * 60 * 1000;
3
+ export const DEFAULT_RUNTIME_PERFORMANCE_MAX_SAMPLES = 8;
4
+ export const DEFAULT_RUNTIME_PERFORMANCE_MIN_SAMPLES = 2;
5
+ export const DEFAULT_RUNTIME_PERFORMANCE_MAX_BACKENDS = 128;
6
+ function finiteLatency(value) {
7
+ const number = Number(value);
8
+ return Number.isFinite(number) && number >= 0 ? number : undefined;
9
+ }
10
+ function median(values) {
11
+ const sorted = values.filter(Number.isFinite).sort((a, b) => a - b);
12
+ if (sorted.length === 0)
13
+ return undefined;
14
+ const middle = Math.floor(sorted.length / 2);
15
+ const right = sorted[middle];
16
+ if (right === undefined)
17
+ return undefined;
18
+ if (sorted.length % 2 === 1)
19
+ return right;
20
+ const left = sorted[middle - 1];
21
+ return left === undefined ? right : (left + right) / 2;
22
+ }
23
+ function cleanBackendKey(value) {
24
+ const text = typeof value === 'string' ? value.trim() : '';
25
+ return text === '' ? undefined : text.slice(0, 512);
26
+ }
27
+ function isBenchmarkAxis(value) {
28
+ return typeof value === 'string'
29
+ && BENCHMARK_AXES.includes(value);
30
+ }
31
+ export function createVisionRuntimePerformanceSampleStore(options = {}) {
32
+ const now = typeof options.now === 'function' ? options.now : Date.now;
33
+ const maxAgeMs = Math.max(1_000, Number(options.maxAgeMs) || DEFAULT_RUNTIME_PERFORMANCE_MAX_AGE_MS);
34
+ const maxSamples = Math.max(1, Math.min(64, Math.floor(Number(options.maxSamples) || DEFAULT_RUNTIME_PERFORMANCE_MAX_SAMPLES)));
35
+ const minSamples = Math.max(1, Math.min(maxSamples, Math.floor(Number(options.minSamples) || DEFAULT_RUNTIME_PERFORMANCE_MIN_SAMPLES)));
36
+ const maxBackends = Math.max(1, Math.min(1024, Math.floor(Number(options.maxBackends) || DEFAULT_RUNTIME_PERFORMANCE_MAX_BACKENDS)));
37
+ const records = new Map();
38
+ let identityContext = options.context;
39
+ const identityResolver = options.identityResolver;
40
+ const resolvedStorageKey = (backendKey) => {
41
+ const key = cleanBackendKey(backendKey);
42
+ if (key === undefined)
43
+ return undefined;
44
+ let identity;
45
+ try {
46
+ identity = identityContext && identityResolver !== undefined
47
+ ? identityResolver(key, identityContext)
48
+ : undefined;
49
+ }
50
+ catch {
51
+ identity = undefined;
52
+ }
53
+ return identity ? `${key}\u0000${String(identity)}` : key;
54
+ };
55
+ const pruneAxis = (samples, at) => samples.filter((sample) => at - sample.at <= maxAgeMs);
56
+ const pruneBackend = (key, at) => {
57
+ const axes = records.get(key);
58
+ if (axes === undefined)
59
+ return undefined;
60
+ for (const [axis, samples] of axes) {
61
+ const current = pruneAxis(samples, at);
62
+ if (current.length === 0)
63
+ axes.delete(axis);
64
+ else if (current.length !== samples.length)
65
+ axes.set(axis, current);
66
+ }
67
+ if (axes.size === 0) {
68
+ records.delete(key);
69
+ return undefined;
70
+ }
71
+ records.delete(key);
72
+ records.set(key, axes);
73
+ return axes;
74
+ };
75
+ const bound = () => {
76
+ while (records.size > maxBackends) {
77
+ const oldest = records.keys().next().value;
78
+ if (oldest === undefined)
79
+ break;
80
+ records.delete(oldest);
81
+ }
82
+ };
83
+ return {
84
+ maxAgeMs,
85
+ maxSamples,
86
+ minSamples,
87
+ bindContext(context) {
88
+ identityContext = context;
89
+ },
90
+ record(backendKey, axisValue, latencyMsValue, atValue = now()) {
91
+ const key = resolvedStorageKey(backendKey);
92
+ const latencyMs = finiteLatency(latencyMsValue);
93
+ const at = Number(atValue);
94
+ if (key === undefined
95
+ || !isBenchmarkAxis(axisValue)
96
+ || latencyMs === undefined
97
+ || !Number.isFinite(at)) {
98
+ return false;
99
+ }
100
+ let axes = pruneBackend(key, at);
101
+ if (axes === undefined) {
102
+ axes = new Map();
103
+ records.set(key, axes);
104
+ }
105
+ const current = pruneAxis(axes.get(axisValue) ?? [], at);
106
+ current.push({ at, latencyMs });
107
+ if (current.length > maxSamples) {
108
+ current.splice(0, current.length - maxSamples);
109
+ }
110
+ axes.set(axisValue, current);
111
+ records.delete(key);
112
+ records.set(key, axes);
113
+ bound();
114
+ return true;
115
+ },
116
+ get(backendKey, atValue = now()) {
117
+ const key = resolvedStorageKey(backendKey);
118
+ const at = Number(atValue);
119
+ if (key === undefined || !Number.isFinite(at))
120
+ return undefined;
121
+ const axes = pruneBackend(key, at);
122
+ if (axes === undefined)
123
+ return undefined;
124
+ const observedLatencyMsByAxis = {};
125
+ const runtimeLatencyMsByAxis = {};
126
+ const sampleCountByAxis = {};
127
+ const observedAtByAxis = {};
128
+ for (const [axis, samples] of axes) {
129
+ const latencyMs = median(samples.map((sample) => sample.latencyMs));
130
+ if (latencyMs === undefined)
131
+ continue;
132
+ observedLatencyMsByAxis[axis] = latencyMs;
133
+ sampleCountByAxis[axis] = samples.length;
134
+ observedAtByAxis[axis] = Math.max(...samples.map((sample) => sample.at));
135
+ if (samples.length >= minSamples)
136
+ runtimeLatencyMsByAxis[axis] = latencyMs;
137
+ }
138
+ return {
139
+ runtimeLatencyMsByAxis,
140
+ observedLatencyMsByAxis,
141
+ sampleCountByAxis,
142
+ observedAtByAxis,
143
+ maxAgeMs,
144
+ minSamples,
145
+ };
146
+ },
147
+ clear(backendKey) {
148
+ if (backendKey === undefined) {
149
+ records.clear();
150
+ return;
151
+ }
152
+ const base = cleanBackendKey(backendKey);
153
+ if (base === undefined)
154
+ return;
155
+ records.delete(base);
156
+ const prefix = `${base}\u0000`;
157
+ for (const key of [...records.keys()]) {
158
+ if (key.startsWith(prefix))
159
+ records.delete(key);
160
+ }
161
+ },
162
+ size() {
163
+ return records.size;
164
+ },
165
+ };
166
+ }
@@ -1,37 +1,19 @@
1
1
  import { AsyncLocalStorage } from 'node:async_hooks'
2
- import {
3
- benchmarkAxisForVisionIntent,
4
- inferToolVisionIntent,
5
- } from './vision-capability-router.js'
2
+ import { benchmarkAxisForVisionIntent } from './vision-capability-evidence.js'
3
+ import { inferToolVisionIntent } from './vision-capability-router.js'
6
4
  import { capabilityEvidenceFingerprint } from './vision-capability-identity.js'
7
5
  import { providerTransportFor } from './live-model-discovery.js'
6
+ import { createVisionRuntimePerformanceSampleStore } from './vision-runtime-performance-store.js'
8
7
 
9
- export const DEFAULT_RUNTIME_PERFORMANCE_MAX_AGE_MS = 60 * 60 * 1000
10
- export const DEFAULT_RUNTIME_PERFORMANCE_MAX_SAMPLES = 8
11
- export const DEFAULT_RUNTIME_PERFORMANCE_MIN_SAMPLES = 2
12
- export const DEFAULT_RUNTIME_PERFORMANCE_MAX_BACKENDS = 128
8
+ export {
9
+ DEFAULT_RUNTIME_PERFORMANCE_MAX_AGE_MS,
10
+ DEFAULT_RUNTIME_PERFORMANCE_MAX_SAMPLES,
11
+ DEFAULT_RUNTIME_PERFORMANCE_MIN_SAMPLES,
12
+ DEFAULT_RUNTIME_PERFORMANCE_MAX_BACKENDS,
13
+ } from './vision-runtime-performance-store.js'
13
14
 
14
15
  const runtimeScope = new AsyncLocalStorage()
15
16
 
16
- function finiteLatency(value) {
17
- const number = Number(value)
18
- return Number.isFinite(number) && number >= 0 ? number : undefined
19
- }
20
-
21
- function median(values) {
22
- const sorted = values.filter(Number.isFinite).sort((a, b) => a - b)
23
- if (sorted.length === 0) return undefined
24
- const middle = Math.floor(sorted.length / 2)
25
- return sorted.length % 2 === 1
26
- ? sorted[middle]
27
- : (sorted[middle - 1] + sorted[middle]) / 2
28
- }
29
-
30
- function cleanBackendKey(value) {
31
- const text = typeof value === 'string' ? value.trim() : ''
32
- return text === '' ? undefined : text.slice(0, 512)
33
- }
34
-
35
17
  function backendParts(backendKey) {
36
18
  const key = cleanBackendKey(backendKey)
37
19
  if (!key) return undefined
@@ -91,124 +73,13 @@ function failureFinish(chunk) {
91
73
  }
92
74
 
93
75
  export function createVisionRuntimePerformanceStore(options = {}) {
94
- const now = typeof options.now === 'function' ? options.now : Date.now
95
- const maxAgeMs = Math.max(1_000, Number(options.maxAgeMs) || DEFAULT_RUNTIME_PERFORMANCE_MAX_AGE_MS)
96
- const maxSamples = Math.max(1, Math.min(64, Math.floor(Number(options.maxSamples) || DEFAULT_RUNTIME_PERFORMANCE_MAX_SAMPLES)))
97
- const minSamples = Math.max(1, Math.min(maxSamples, Math.floor(Number(options.minSamples) || DEFAULT_RUNTIME_PERFORMANCE_MIN_SAMPLES)))
98
- const maxBackends = Math.max(1, Math.min(1024, Math.floor(Number(options.maxBackends) || DEFAULT_RUNTIME_PERFORMANCE_MAX_BACKENDS)))
99
- const records = new Map()
100
- let identityContext = options.context
101
76
  const identityResolver = typeof options.identityResolver === 'function'
102
77
  ? options.identityResolver
103
78
  : (backendKey, ctx) => runtimePerformanceIdentityFor(ctx, backendKey)
104
-
105
- const resolvedStorageKey = (backendKey) => {
106
- const key = cleanBackendKey(backendKey)
107
- if (!key) return undefined
108
- let identity
109
- try { identity = identityContext ? identityResolver(key, identityContext) : undefined } catch { identity = undefined }
110
- return identity ? `${key}\u0000${identity}` : key
111
- }
112
-
113
- const pruneAxis = (samples, at) => samples.filter((sample) => at - sample.at <= maxAgeMs)
114
-
115
- const pruneBackend = (key, at) => {
116
- const axes = records.get(key)
117
- if (!axes) return undefined
118
- for (const [axis, samples] of axes) {
119
- const current = pruneAxis(samples, at)
120
- if (current.length === 0) axes.delete(axis)
121
- else if (current.length !== samples.length) axes.set(axis, current)
122
- }
123
- if (axes.size === 0) {
124
- records.delete(key)
125
- return undefined
126
- }
127
- // LRU touch only on runtime-observation reads/writes; routing diagnostics
128
- // are allowed to read this performance store because it is not the v1
129
- // breaker and has no execution side effects.
130
- records.delete(key)
131
- records.set(key, axes)
132
- return axes
133
- }
134
-
135
- const bound = () => {
136
- while (records.size > maxBackends) {
137
- const oldest = records.keys().next().value
138
- if (oldest === undefined) break
139
- records.delete(oldest)
140
- }
141
- }
142
-
143
- return {
144
- maxAgeMs,
145
- maxSamples,
146
- minSamples,
147
- bindContext(ctx) {
148
- identityContext = ctx
149
- },
150
- record(backendKey, axis, latencyMs, at = now()) {
151
- const key = resolvedStorageKey(backendKey)
152
- const latency = finiteLatency(latencyMs)
153
- const timestamp = Number(at)
154
- if (!key || !benchmarkAxisForVisionIntent(axis) || latency === undefined || !Number.isFinite(timestamp)) return false
155
- let axes = pruneBackend(key, timestamp)
156
- if (!axes) {
157
- axes = new Map()
158
- records.set(key, axes)
159
- }
160
- const current = pruneAxis(axes.get(axis) ?? [], timestamp)
161
- current.push({ at: timestamp, latencyMs: latency })
162
- if (current.length > maxSamples) current.splice(0, current.length - maxSamples)
163
- axes.set(axis, current)
164
- records.delete(key)
165
- records.set(key, axes)
166
- bound()
167
- return true
168
- },
169
- get(backendKey, at = now()) {
170
- const key = resolvedStorageKey(backendKey)
171
- const timestamp = Number(at)
172
- if (!key || !Number.isFinite(timestamp)) return undefined
173
- const axes = pruneBackend(key, timestamp)
174
- if (!axes) return undefined
175
- const observedLatencyMsByAxis = {}
176
- const runtimeLatencyMsByAxis = {}
177
- const sampleCountByAxis = {}
178
- const observedAtByAxis = {}
179
- for (const [axis, samples] of axes) {
180
- const latencies = samples.map((sample) => sample.latencyMs)
181
- const value = median(latencies)
182
- if (value === undefined) continue
183
- observedLatencyMsByAxis[axis] = value
184
- sampleCountByAxis[axis] = samples.length
185
- observedAtByAxis[axis] = Math.max(...samples.map((sample) => sample.at))
186
- if (samples.length >= minSamples) runtimeLatencyMsByAxis[axis] = value
187
- }
188
- return {
189
- runtimeLatencyMsByAxis,
190
- observedLatencyMsByAxis,
191
- sampleCountByAxis,
192
- observedAtByAxis,
193
- maxAgeMs,
194
- minSamples,
195
- }
196
- },
197
- clear(backendKey) {
198
- if (backendKey === undefined) {
199
- records.clear()
200
- return
201
- }
202
- const base = cleanBackendKey(backendKey)
203
- if (!base) return
204
- records.delete(base)
205
- const prefix = `${base}\u0000`
206
- for (const key of [...records.keys()]) if (key.startsWith(prefix)) records.delete(key)
207
- },
208
- size() {
209
- return records.size
210
- },
211
- }
79
+ return createVisionRuntimePerformanceSampleStore({
80
+ ...options,
81
+ identityResolver,
82
+ })
212
83
  }
213
84
 
214
85
  export function withVisionRuntimePerformanceScope(toolName, args, fn) {
@@ -17,6 +17,9 @@ const toolRuntime = new AsyncLocalStorage()
17
17
  const wrappedContexts = new WeakMap()
18
18
  const runtimeStates = new WeakMap()
19
19
  const wrappedFileSystems = new WeakMap()
20
+ // Settings proxies are cached per underlying service/scope and per runtime
21
+ // state. Re-creating them on every `ctx.settings` read would re-run scope
22
+ // activation and register a second lifecycle effect for the same generation.
20
23
  const wrappedSettings = new WeakMap()
21
24
  const wrappedScopes = new WeakMap()
22
25
  const wrappedLlms = new WeakMap()
@@ -104,7 +107,7 @@ function createRuntimeState(initialConfig = {}) {
104
107
  const cache = new LiveDescribeCache()
105
108
  const endpoints = new HttpEndpointRevisionTracker()
106
109
  const transparentReasoning = new Map()
107
- let config = {
110
+ const fallbackConfig = {
108
111
  cache: true,
109
112
  cacheMaxEntries: 200,
110
113
  cacheTtlSeconds: 3600,
@@ -115,8 +118,13 @@ function createRuntimeState(initialConfig = {}) {
115
118
  ? initialConfig
116
119
  : {}),
117
120
  }
121
+ let config = fallbackConfig
118
122
  let signature = stableRuntimeConfigSignature(config)
119
123
  let revision = 0
124
+ let activeSettingsGeneration
125
+ // scope -> { runtimeState, token, ownsLifecycle }: the generation registry
126
+ // that makes repeated activation idempotent per runtime.
127
+ const wiredScopes = new WeakMap()
120
128
 
121
129
  const noteConfig = (value) => {
122
130
  if (!value || typeof value !== 'object' || Array.isArray(value)) return config
@@ -135,6 +143,59 @@ function createRuntimeState(initialConfig = {}) {
135
143
  return config
136
144
  }
137
145
 
146
+ const activateSettingsScope = (scope, ownerCtx, runtimeState) => {
147
+ if (!scope || (typeof scope !== 'object' && typeof scope !== 'function')) return undefined
148
+ // Registering the same scope twice (repeated `ctx.settings` reads, a
149
+ // generation that re-registers its namespace) must not stack lifecycle
150
+ // owners or mint a competing generation: reuse the existing one.
151
+ const known = wiredScopes.get(scope)
152
+ if (known && known.runtimeState === runtimeState) {
153
+ if (activeSettingsGeneration?.token !== known.token) {
154
+ activeSettingsGeneration = { token: known.token, scope, ownsLifecycle: known.ownsLifecycle }
155
+ }
156
+ return known.token
157
+ }
158
+ const token = {}
159
+ // Lifecycle ownership is an optimisation, not a precondition: a context
160
+ // without `effect()` (harness contexts, non-Cordis embeddings) still needs
161
+ // live settings to reach the runtime, but the generation must then be the
162
+ // most recently registered scope so a stale scope cannot outrank it.
163
+ let ownsLifecycle = false
164
+ try {
165
+ if (typeof ownerCtx?.effect === 'function') {
166
+ ownerCtx.effect(
167
+ () => () => {
168
+ if (activeSettingsGeneration?.token !== token) return
169
+ activeSettingsGeneration = undefined
170
+ noteConfig(fallbackConfig)
171
+ },
172
+ 'vision-router: tool runtime settings lifecycle',
173
+ )
174
+ ownsLifecycle = true
175
+ }
176
+ } catch {
177
+ ownsLifecycle = false
178
+ }
179
+ wiredScopes.set(scope, { runtimeState, token, ownsLifecycle })
180
+ activeSettingsGeneration = { token, scope, ownsLifecycle }
181
+ try {
182
+ if (typeof scope.get === 'function') noteConfig(scope.get())
183
+ } catch {
184
+ // Boot config remains authoritative until the scope becomes readable.
185
+ }
186
+ return token
187
+ }
188
+
189
+ const noteSettingsConfig = (token, value) => {
190
+ // Only the active generation may write config. An unowned generation is
191
+ // still "the active one" while it is the latest registration, so a
192
+ // lifecycle-less context keeps its watch path exactly as before the token
193
+ // existed, while a retired generation can no longer resurrect stale config.
194
+ if (token === undefined || activeSettingsGeneration?.token !== token) return false
195
+ noteConfig(value)
196
+ return true
197
+ }
198
+
138
199
  const rememberTransparentReasoning = (key, value) => {
139
200
  if (transparentReasoning.size >= MAX_TRANSPARENT_REASONING_MEMORY && !transparentReasoning.has(key)) {
140
201
  const oldest = transparentReasoning.keys().next().value
@@ -156,6 +217,8 @@ function createRuntimeState(initialConfig = {}) {
156
217
  cache,
157
218
  endpoints,
158
219
  noteConfig,
220
+ activateSettingsScope,
221
+ noteSettingsConfig,
159
222
  config: () => config,
160
223
  revision: () => revision,
161
224
  rememberTransparentReasoning,
@@ -242,9 +305,12 @@ function wrapFileSystem(fs) {
242
305
  return wrapped
243
306
  }
244
307
 
245
- function projectLiveSettings(value, runtimeState) {
308
+ function projectLiveSettings(value, runtimeState, settingsToken) {
246
309
  if (!value || typeof value !== 'object' || Array.isArray(value)) return value
247
- runtimeState.noteConfig(value)
310
+ if (settingsToken !== undefined && runtimeState.noteSettingsConfig(settingsToken, value) !== true) {
311
+ return value
312
+ }
313
+ if (settingsToken === undefined) runtimeState.noteConfig(value)
248
314
  const toolState = toolRuntime.getStore()
249
315
  const httpProviders = runtimeState.endpoints.project(value.httpProviders)
250
316
  const suppressCoreCache = toolState?.disableCoreCache === true && value.cache !== false
@@ -256,21 +322,16 @@ function projectLiveSettings(value, runtimeState) {
256
322
  }
257
323
  }
258
324
 
259
- function wrapSettingsScope(scope, runtimeState) {
325
+ function wrapSettingsScope(scope, runtimeState, settingsToken) {
260
326
  if (!scope || (typeof scope !== 'object' && typeof scope !== 'function')) return scope
261
- let byRuntime = wrappedScopes.get(scope)
262
- if (!byRuntime) {
263
- byRuntime = new WeakMap()
264
- wrappedScopes.set(scope, byRuntime)
265
- }
266
- const cached = byRuntime.get(runtimeState)
267
- if (cached) return cached
327
+ const cached = wrappedScopes.get(scope)
328
+ if (cached && cached.runtimeState === runtimeState && cached.token === settingsToken) return cached.wrapped
268
329
  const wrapped = new Proxy(scope, {
269
330
  get(target, property) {
270
331
  if (property === 'get') {
271
332
  const get = Reflect.get(target, property, target)
272
333
  if (typeof get !== 'function') return get
273
- return (...args) => projectLiveSettings(get.apply(target, args), runtimeState)
334
+ return (...args) => projectLiveSettings(get.apply(target, args), runtimeState, settingsToken)
274
335
  }
275
336
  if (property === 'watch') {
276
337
  const watch = Reflect.get(target, property, target)
@@ -278,7 +339,7 @@ function wrapSettingsScope(scope, runtimeState) {
278
339
  return (callback, ...rest) => watch.call(target, (...args) => {
279
340
  try {
280
341
  const get = Reflect.get(target, 'get', target)
281
- if (typeof get === 'function') runtimeState.noteConfig(get.call(target))
342
+ if (typeof get === 'function') runtimeState.noteSettingsConfig(settingsToken, get.call(target))
282
343
  } catch {
283
344
  // Diagnostics refresh is best effort.
284
345
  }
@@ -289,19 +350,14 @@ function wrapSettingsScope(scope, runtimeState) {
289
350
  return typeof value === 'function' ? value.bind(target) : value
290
351
  },
291
352
  })
292
- byRuntime.set(runtimeState, wrapped)
353
+ wrappedScopes.set(scope, { runtimeState, token: settingsToken, wrapped })
293
354
  return wrapped
294
355
  }
295
356
 
296
- function wrapSettingsService(settings, runtimeState) {
357
+ function wrapSettingsService(settings, runtimeState, ownerCtx) {
297
358
  if (!settings || (typeof settings !== 'object' && typeof settings !== 'function')) return settings
298
- let byRuntime = wrappedSettings.get(settings)
299
- if (!byRuntime) {
300
- byRuntime = new WeakMap()
301
- wrappedSettings.set(settings, byRuntime)
302
- }
303
- const cached = byRuntime.get(runtimeState)
304
- if (cached) return cached
359
+ const cached = wrappedSettings.get(settings)
360
+ if (cached && cached.runtimeState === runtimeState) return cached.wrapped
305
361
  const wrapped = new Proxy(settings, {
306
362
  get(target, property) {
307
363
  if (property === 'register') {
@@ -310,25 +366,22 @@ function wrapSettingsService(settings, runtimeState) {
310
366
  return (namespace, ...args) => {
311
367
  const scope = register.call(target, namespace, ...args)
312
368
  if (namespace !== 'vision-router') return scope
313
- try {
314
- if (typeof scope?.get === 'function') runtimeState.noteConfig(scope.get())
315
- } catch {
316
- // Core can still run from boot config if settings are late.
317
- }
318
- return wrapSettingsScope(scope, runtimeState)
369
+ const settingsToken = runtimeState.activateSettingsScope(scope, ownerCtx, runtimeState)
370
+ if (settingsToken === undefined) return scope
371
+ return wrapSettingsScope(scope, runtimeState, settingsToken)
319
372
  }
320
373
  }
321
374
  const value = Reflect.get(target, property, target)
322
375
  return typeof value === 'function' ? value.bind(target) : value
323
376
  },
324
377
  })
325
- byRuntime.set(runtimeState, wrapped)
378
+ wrappedSettings.set(settings, { runtimeState, wrapped })
326
379
  return wrapped
327
380
  }
328
381
 
329
382
  function wrapSettingsContext(ctx, runtimeState) {
330
383
  if (!ctx || (typeof ctx !== 'object' && typeof ctx !== 'function') || !ctx.settings) return ctx
331
- const settings = wrapSettingsService(ctx.settings, runtimeState)
384
+ const settings = wrapSettingsService(ctx.settings, runtimeState, ctx)
332
385
  return new Proxy(ctx, {
333
386
  get(target, property) {
334
387
  if (property === 'settings') return settings
@@ -716,6 +769,22 @@ export function installVisionToolRuntimeBoundary(ctx, initialConfig = {}) {
716
769
  })
717
770
  wrappedContexts.set(ctx, wrapped)
718
771
  runtimeStates.set(wrapped, runtimeState)
772
+ let lifecycleOwned = false
773
+ try {
774
+ if (typeof ctx.effect === 'function') {
775
+ ctx.effect(
776
+ () => () => {
777
+ if (wrappedContexts.get(ctx) === wrapped) wrappedContexts.delete(ctx)
778
+ },
779
+ 'vision-router: vision tool runtime context lifecycle',
780
+ )
781
+ lifecycleOwned = true
782
+ }
783
+ } catch {
784
+ // Keep the current returned view usable as a one-shot boundary.
785
+ }
786
+ // runtimeState captures this generation's config/cache/endpoint revision.
787
+ if (!lifecycleOwned && wrappedContexts.get(ctx) === wrapped) wrappedContexts.delete(ctx)
719
788
  return wrapped
720
789
  }
721
790