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.
- package/README.md +12 -3
- package/README.zh.md +12 -3
- package/docs/architecture/3.0-architecture-plan.md +252 -0
- package/docs/architecture/3.0-r0-contract-ledger.md +137 -0
- package/docs/architecture/3.0-r1-source-artifact-boundary.md +98 -0
- package/docs/architecture/3.0-r10-final-correctness-closure.md +207 -0
- package/docs/architecture/3.0-r11-adversarial-review.md +441 -0
- package/docs/architecture/3.0-r5-browser-go-no-go.md +165 -0
- package/docs/architecture/3.0-r5-browser-typescript-gate.md +105 -0
- package/docs/architecture/3.0-r6-compat-retirement.md +203 -0
- package/docs/architecture/3.0-r7-public-api.md +151 -0
- package/docs/architecture/3.0-r8-ci-release.md +248 -0
- package/docs/architecture/3.0-r9-final-audit.md +370 -0
- package/docs/architecture/dsh-support-window.md +5 -3
- package/docs/releases/v3.0.0.md +85 -0
- package/entry.js +1 -1
- package/index.js +108 -71
- package/lib/agent-request-route-authority.js +1 -20
- package/lib/core-apply-lifecycle.js +37 -43
- package/lib/core-cache.js +87 -0
- package/lib/core-primitives.js +3 -143
- package/lib/core-vision-surface-domain.js +63 -0
- package/lib/core-vision-surface.js +11 -77
- package/lib/delegated-call-config.js +15 -24
- package/lib/doctor-cli.js +0 -0
- package/lib/doctor-runtime.js +4 -1
- package/lib/dsh-contract-compat.js +26 -230
- package/lib/dsh-host-capabilities.js +3 -9
- package/lib/dsh-settings-017-compat.js +17 -10
- package/lib/file-logger.js +8 -3
- package/lib/grounding-coordinate-runtime.js +1 -2
- package/lib/http-body-limit.js +97 -79
- package/lib/image-offload-compat.js +30 -21
- package/lib/image-resource-governor.js +210 -228
- package/lib/legacy-global-proxy-boundary.js +46 -3
- package/lib/live-model-client-prelude.js +12 -4
- package/lib/live-model-discovery.js +134 -43
- package/lib/local-vision-stabilizer.js +31 -17
- package/lib/maintenance-routes.js +1 -3
- package/lib/native-image-coexistence.js +15 -3
- package/lib/ollama-cold-start.js +87 -23
- package/lib/pi-ai-bridge-wire-compat.js +3 -2
- package/lib/proxy-dispatcher-pool.js +151 -92
- package/lib/proxy-routing.js +75 -51
- package/lib/proxy-url-compat.js +3 -10
- package/lib/public-config.js +23 -44
- package/lib/public-entry.d.ts +148 -0
- package/lib/public-entry.js +17 -4
- package/lib/replay-delegation.js +12 -7
- package/lib/replay-envelope-v2-compat.js +12 -7
- package/lib/runtime-composition.js +22 -4
- package/lib/screenshot-source-boundary.js +2 -2
- package/lib/session-affinity-runtime.js +113 -97
- package/lib/session-affinity.js +89 -76
- package/lib/session-event-history-compat.js +30 -0
- package/lib/session-query-readers.js +263 -0
- package/lib/session-surface-compat.js +30 -31
- package/lib/session-surface-policy-domain.js +85 -0
- package/lib/session-surface-policy.js +2 -111
- package/lib/session-turn-resolver.js +2 -6
- package/lib/session-vision-attachment-index.js +143 -0
- package/lib/session-vision-delegation-boundary.js +34 -0
- package/lib/session-vision-delegation.js +22 -0
- package/lib/session-vision-index.js +25 -107
- package/lib/session-vision-mode-authority.js +229 -192
- package/lib/session-vision-mode-boundary.js +65 -22
- package/lib/session-vision-policy-domain.js +304 -0
- package/lib/session-vision-policy.js +104 -0
- package/lib/session-vision-state.js +368 -378
- package/lib/settings-client-017-compat.js +9 -7
- package/lib/structured-flow-hardening.js +22 -4
- package/lib/tesseract-exec-compat.js +9 -1
- package/lib/turn-budget-context.js +47 -41
- package/lib/update-check.js +11 -2
- package/lib/vision-attachment-handle-runtime.js +176 -156
- package/lib/vision-backend-runtime-policy.js +16 -0
- package/lib/vision-background-benchmark.js +41 -4
- package/lib/vision-background-failure-policy.js +41 -41
- package/lib/vision-background-stop-store-core.js +217 -0
- package/lib/vision-background-stop-store.js +8 -179
- package/lib/vision-capability-benchmark.js +11 -61
- package/lib/vision-capability-evidence.js +35 -0
- package/lib/vision-capability-fingerprint.js +72 -0
- package/lib/vision-capability-identity.js +116 -61
- package/lib/vision-capability-router.js +13 -33
- package/lib/vision-chunk-assembler.js +95 -0
- package/lib/vision-diagnostics-routes.js +34 -4
- package/lib/vision-execution-order-apply.js +45 -55
- package/lib/vision-execution-order.js +19 -19
- package/lib/vision-execution-policy.js +12 -7
- package/lib/vision-model-registry.js +30 -17
- package/lib/vision-provider-transport.js +137 -147
- package/lib/vision-routing-authority.js +47 -48
- package/lib/vision-routing-product.js +26 -29
- package/lib/vision-routing-settings-prelude.js +8 -3
- package/lib/vision-runtime-performance-store.js +166 -0
- package/lib/vision-runtime-performance.js +13 -142
- package/lib/vision-tool-runtime-boundary.js +99 -30
- package/lib/web/dsh-settings-017-root-transport.js +27 -12
- package/lib/web/remote-settings-client.js +4 -7
- package/lib/web-capability-boundary.js +17 -7
- package/lib/wrapper-directory.js +14 -23
- 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
|
|
10
|
-
|
|
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
|
-
|
|
17
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
31
|
-
|
|
26
|
+
const normalized = normalizeVisionRoutingPreference(preference);
|
|
27
|
+
return normalized === 'local' ? 'privacy' : normalized;
|
|
32
28
|
}
|
|
33
|
-
|
|
34
29
|
export function resolveVisionRoutingProduct(config = {}) {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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')
|
|
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
|
-
|
|
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
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
262
|
-
if (
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
299
|
-
if (
|
|
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
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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
|
-
|
|
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
|
|