dsh-vision-router 2.2.8 → 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 +16 -7
- package/README.zh.md +16 -7
- package/docs/architecture/2.3.0-compat-retirement.md +147 -0
- package/docs/architecture/2.3.0-execution-plan.md +410 -0
- package/docs/architecture/2.3.0-final-audit.md +279 -0
- package/docs/architecture/2.3.0-reopened-plan.md +720 -0
- 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/compat-inventory.md +26 -34
- package/docs/architecture/dsh-compatibility-matrix.md +65 -54
- package/docs/architecture/dsh-support-window.md +30 -22
- package/docs/architecture/final-closure-audit.md +2 -0
- package/docs/releases/v2.3.0.md +53 -0
- package/docs/releases/v3.0.0.md +85 -0
- package/entry.js +9 -57
- package/index.js +268 -761
- package/lib/agent-request-route-authority.js +1 -20
- package/lib/catalog-corrections.js +1 -2
- package/lib/client-maintenance-actions.js +145 -0
- package/lib/client-presentation-boundary-main.js +75 -17
- package/lib/client-presentation-boundary.js +53 -28
- package/lib/client.js +246 -144
- package/lib/core-apply-lifecycle.js +41 -0
- package/lib/core-cache.js +87 -0
- package/lib/core-primitives.js +91 -278
- 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/desktop-screenshot-tool.js +262 -0
- package/lib/doctor-cli.js +0 -0
- package/lib/doctor-runtime.js +4 -1
- package/lib/dsh-contract-compat.js +37 -256
- package/lib/dsh-host-capabilities.js +3 -9
- package/lib/dsh-settings-017-compat.js +17 -10
- package/lib/dsh-support-window.js +3 -3
- 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/http-compat.js +1 -2
- 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-remote-settings-permission.js +33 -7
- package/lib/local-vision-stabilizer.js +31 -17
- package/lib/maintenance-routes.js +134 -0
- 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 +36 -0
- package/lib/public-entry.d.ts +148 -0
- package/lib/public-entry.js +19 -11
- package/lib/remote-settings-bridge.js +11 -0
- package/lib/remote-settings-risk-client-boundary.js +155 -0
- package/lib/remote-settings-risk-confirmation.js +17 -105
- package/lib/replay-delegation.js +12 -7
- package/lib/replay-envelope-v2-compat.js +12 -7
- package/lib/runtime-composition.js +127 -34
- package/lib/runtime-i18n-boundary.js +10 -11
- 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-message-source-compat.js +7 -3
- 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 +179 -116
- 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/settings-client-loader-lifecycle.js +129 -0
- package/lib/sharp-runtime.js +0 -7
- package/lib/structured-flow-hardening.js +25 -6
- 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 +19 -2
- 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-service.js +1 -0
- 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 +246 -0
- package/lib/vision-evidence-guidance.js +3 -1
- 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 -176
- 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-toggle-root-hardening.js +22 -5
- package/lib/vision-tool-runtime-boundary.js +104 -31
- package/lib/web/dsh-settings-017-root-transport.js +27 -12
- package/lib/web/remote-settings-client.js +6 -9
- package/lib/web-capability-boundary.js +17 -7
- package/lib/wrapper-directory.js +14 -23
- package/package.json +56 -20
- package/lib/android-attachment-compat.js +0 -244
- package/lib/settings-client-rc8-lifecycle.js +0 -397
|
@@ -1,84 +1,91 @@
|
|
|
1
|
-
const SHA256_HANDLE_PREFIX = /^sha256:/i
|
|
2
|
-
const CANONICAL_SHA256_ID = /^sha256:[0-9a-f]{64}$/i
|
|
3
|
-
const PROJECTED_SHA256_HANDLE = /^sha256:[0-9a-f]{8,63}$/i
|
|
4
|
-
|
|
1
|
+
const SHA256_HANDLE_PREFIX = /^sha256:/i;
|
|
2
|
+
const CANONICAL_SHA256_ID = /^sha256:[0-9a-f]{64}$/i;
|
|
3
|
+
const PROJECTED_SHA256_HANDLE = /^sha256:[0-9a-f]{8,63}$/i;
|
|
5
4
|
const TOOL_FIELDS = Object.freeze({
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
})
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
5
|
+
vision_describe: { arrays: ['attachmentIds', 'paths'] },
|
|
6
|
+
vision_bootstrap: { arrays: ['attachmentIds', 'paths'] },
|
|
7
|
+
vision_materialize: { scalars: ['image'] },
|
|
8
|
+
vision_ground: { scalars: ['image'] },
|
|
9
|
+
vision_detect: { scalars: ['image'] },
|
|
10
|
+
vision_crop: { scalars: ['image'] },
|
|
11
|
+
vision_present: { scalars: ['image'] },
|
|
12
|
+
vision_pixel_diff: { scalars: ['original', 'rebuilt'] },
|
|
13
|
+
vision_colors: { scalars: ['image'] },
|
|
14
|
+
vision_ocr: { scalars: ['image'] },
|
|
15
|
+
vision_trace: { scalars: ['image'] },
|
|
16
|
+
vision_extract_foreground: { scalars: ['image'] },
|
|
17
|
+
});
|
|
18
|
+
function objectRecord(value) {
|
|
19
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
20
|
+
? value
|
|
21
|
+
: undefined;
|
|
22
22
|
}
|
|
23
|
-
|
|
24
23
|
function attachmentIdOf(ref) {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
24
|
+
const value = objectRecord(ref);
|
|
25
|
+
if (value === undefined)
|
|
26
|
+
return undefined;
|
|
27
|
+
const raw = value.attachmentId ?? value.id;
|
|
28
|
+
if (raw === undefined || raw === null)
|
|
29
|
+
return undefined;
|
|
30
|
+
const id = String(raw).trim();
|
|
31
|
+
return id === '' ? undefined : id;
|
|
30
32
|
}
|
|
31
|
-
|
|
32
33
|
function sessionMessages(session) {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
34
|
+
try {
|
|
35
|
+
if (!session || typeof session.deriveMessages !== 'function')
|
|
36
|
+
return [];
|
|
37
|
+
const messages = session.deriveMessages();
|
|
38
|
+
return Array.isArray(messages) ? messages : [];
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
40
43
|
}
|
|
41
|
-
|
|
42
44
|
function collectImageRefsFromBlocks(blocks, out) {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
45
|
+
if (!Array.isArray(blocks))
|
|
46
|
+
return;
|
|
47
|
+
for (const block of blocks) {
|
|
48
|
+
const value = objectRecord(block);
|
|
49
|
+
if (value === undefined)
|
|
50
|
+
continue;
|
|
51
|
+
if (value.type === 'image') {
|
|
52
|
+
const attachment = objectRecord(value.attachment);
|
|
53
|
+
if (attachment !== undefined)
|
|
54
|
+
out.push(attachment);
|
|
55
|
+
}
|
|
56
|
+
if (Array.isArray(value.content))
|
|
57
|
+
collectImageRefsFromBlocks(value.content, out);
|
|
58
|
+
}
|
|
49
59
|
}
|
|
50
|
-
|
|
51
60
|
function collectImageRefsFromMessages(messages, out) {
|
|
52
|
-
|
|
53
|
-
|
|
61
|
+
if (!Array.isArray(messages))
|
|
62
|
+
return;
|
|
63
|
+
for (const message of messages) {
|
|
64
|
+
collectImageRefsFromBlocks(objectRecord(message)?.content, out);
|
|
65
|
+
}
|
|
54
66
|
}
|
|
55
|
-
|
|
56
67
|
function authorizedRefs(agent) {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
+
const refs = [];
|
|
69
|
+
collectImageRefsFromMessages(sessionMessages(agent?.session), refs);
|
|
70
|
+
collectImageRefsFromMessages(agent?.inbox?.nextTurn, refs);
|
|
71
|
+
collectImageRefsFromMessages(agent?.inbox?.nextStep, refs);
|
|
72
|
+
const unique = new Map();
|
|
73
|
+
for (const ref of refs) {
|
|
74
|
+
const id = attachmentIdOf(ref);
|
|
75
|
+
if (id !== undefined && !unique.has(id))
|
|
76
|
+
unique.set(id, ref);
|
|
77
|
+
}
|
|
78
|
+
return [...unique.values()];
|
|
68
79
|
}
|
|
69
|
-
|
|
70
80
|
export function isProjectedAttachmentHandle(value) {
|
|
71
|
-
|
|
81
|
+
return typeof value === 'string' && PROJECTED_SHA256_HANDLE.test(value.trim());
|
|
72
82
|
}
|
|
73
|
-
|
|
74
83
|
function isSha256HandleLike(value) {
|
|
75
|
-
|
|
84
|
+
return typeof value === 'string' && SHA256_HANDLE_PREFIX.test(value.trim());
|
|
76
85
|
}
|
|
77
|
-
|
|
78
86
|
function isCanonicalSha256Id(value) {
|
|
79
|
-
|
|
87
|
+
return CANONICAL_SHA256_ID.test(value.trim());
|
|
80
88
|
}
|
|
81
|
-
|
|
82
89
|
/**
|
|
83
90
|
* Resolve DSH's model-facing text-only image alias back to one canonical
|
|
84
91
|
* attachment ref without changing durable attachment identity semantics.
|
|
@@ -89,91 +96,95 @@ function isCanonicalSha256Id(value) {
|
|
|
89
96
|
* prefix. Zero or multiple matches fail closed.
|
|
90
97
|
*/
|
|
91
98
|
export function resolveProjectedAttachmentHandle(handle, { sessionVisionIndex, agent } = {}) {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
99
|
+
const token = typeof handle === 'string' ? handle.trim() : '';
|
|
100
|
+
if (!isProjectedAttachmentHandle(token))
|
|
101
|
+
return { kind: 'not-projected', value: handle };
|
|
102
|
+
const session = agent?.session;
|
|
103
|
+
const refs = authorizedRefs(agent);
|
|
104
|
+
const wanted = token.toLowerCase();
|
|
105
|
+
const matches = refs.filter((ref) => {
|
|
106
|
+
const id = attachmentIdOf(ref);
|
|
107
|
+
return id !== undefined && id.toLowerCase().startsWith(wanted);
|
|
108
|
+
});
|
|
109
|
+
if (matches.length === 0)
|
|
110
|
+
return { kind: 'unknown', handle: token };
|
|
111
|
+
if (matches.length > 1)
|
|
112
|
+
return { kind: 'ambiguous', handle: token };
|
|
113
|
+
const candidate = matches[0];
|
|
114
|
+
const canonicalId = attachmentIdOf(candidate);
|
|
115
|
+
if (candidate === undefined || canonicalId === undefined)
|
|
116
|
+
return { kind: 'unknown', handle: token };
|
|
117
|
+
// The Session event/inbox proves authorization; warm the canonical bounded
|
|
118
|
+
// index with that exact Host-owned ref, then require the index to hand it
|
|
119
|
+
// back. This keeps all downstream tools on the same recovery/identity seam.
|
|
120
|
+
try {
|
|
121
|
+
sessionVisionIndex?.recordAttachments?.(session, [candidate]);
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
// Authorization is still fail-closed if the bounded cache cannot be warmed.
|
|
125
|
+
}
|
|
126
|
+
if (!sessionVisionIndex || typeof sessionVisionIndex.lookupAttachment !== 'function') {
|
|
127
|
+
return { kind: 'unknown', handle: token };
|
|
128
|
+
}
|
|
129
|
+
const ref = sessionVisionIndex.lookupAttachment(session, canonicalId);
|
|
130
|
+
if (ref === undefined)
|
|
131
|
+
return { kind: 'unknown', handle: token };
|
|
132
|
+
return { kind: 'resolved', handle: token, canonicalId, ref };
|
|
124
133
|
}
|
|
125
|
-
|
|
126
134
|
function canonicalizeValue(value, context) {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
135
|
+
if (!isSha256HandleLike(value))
|
|
136
|
+
return value;
|
|
137
|
+
if (isCanonicalSha256Id(value))
|
|
138
|
+
return value;
|
|
139
|
+
if (!isProjectedAttachmentHandle(value)) {
|
|
140
|
+
throw new Error(`${context.toolName}: invalid attachment handle "${value.trim()}" ` +
|
|
141
|
+
'(sha256 attachment handles must be a canonical id or a DSH-projected 8+ hex prefix)');
|
|
142
|
+
}
|
|
143
|
+
const result = resolveProjectedAttachmentHandle(value, context);
|
|
144
|
+
if (result.kind === 'resolved')
|
|
145
|
+
return result.canonicalId;
|
|
146
|
+
if (result.kind === 'ambiguous') {
|
|
147
|
+
throw new Error(`${context.toolName}: ambiguous attachment handle "${result.handle}" ` +
|
|
148
|
+
'(multiple images in this conversation share that prefix; use a full attachment id or attach the image again)');
|
|
149
|
+
}
|
|
150
|
+
if (result.kind === 'unknown') {
|
|
151
|
+
throw new Error(`${context.toolName}: unknown attachment handle "${result.handle}" ` +
|
|
152
|
+
'(it is not authorized by an image in this conversation; attach the image again if needed)');
|
|
153
|
+
}
|
|
154
|
+
return value;
|
|
147
155
|
}
|
|
148
|
-
|
|
149
156
|
function canonicalizeArgs(args, context, fields) {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
157
|
+
const record = objectRecord(args);
|
|
158
|
+
if (record === undefined)
|
|
159
|
+
return args;
|
|
160
|
+
let next;
|
|
161
|
+
for (const field of fields.scalars ?? []) {
|
|
162
|
+
if (!Object.hasOwn(record, field))
|
|
163
|
+
continue;
|
|
164
|
+
const value = canonicalizeValue(record[field], context);
|
|
165
|
+
if (value !== record[field]) {
|
|
166
|
+
next ??= { ...record };
|
|
167
|
+
next[field] = value;
|
|
168
|
+
}
|
|
158
169
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
170
|
+
for (const field of fields.arrays ?? []) {
|
|
171
|
+
const values = record[field];
|
|
172
|
+
if (!Array.isArray(values))
|
|
173
|
+
continue;
|
|
174
|
+
let changed = false;
|
|
175
|
+
const mapped = values.map((value) => {
|
|
176
|
+
const canonical = canonicalizeValue(value, context);
|
|
177
|
+
if (canonical !== value)
|
|
178
|
+
changed = true;
|
|
179
|
+
return canonical;
|
|
180
|
+
});
|
|
181
|
+
if (changed) {
|
|
182
|
+
next ??= { ...record };
|
|
183
|
+
next[field] = mapped;
|
|
184
|
+
}
|
|
172
185
|
}
|
|
173
|
-
|
|
174
|
-
return next ?? args
|
|
186
|
+
return next ?? args;
|
|
175
187
|
}
|
|
176
|
-
|
|
177
188
|
/**
|
|
178
189
|
* Canonicalize only image-source arguments of Vision Router tools. Ordinary
|
|
179
190
|
* prose fields are never scanned, so a user mentioning `sha256:deadbeef` in a
|
|
@@ -182,19 +193,28 @@ function canonicalizeArgs(args, context, fields) {
|
|
|
182
193
|
* fails closed rather than falling through to cwd-relative filesystem lookup.
|
|
183
194
|
*/
|
|
184
195
|
export function wrapVisionAttachmentHandleDefinition(def, options = {}) {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
196
|
+
// Tool definitions arrive from the Host registry at runtime. Static generic
|
|
197
|
+
// constraints cannot replace this boundary guard: partial/legacy Hosts may
|
|
198
|
+
// still pass nullish or malformed values before their own validation runs.
|
|
199
|
+
if (def === null || typeof def !== 'object' || typeof def.execute !== 'function')
|
|
200
|
+
return def;
|
|
201
|
+
const toolName = def.name;
|
|
202
|
+
const fields = typeof toolName === 'string'
|
|
203
|
+
? TOOL_FIELDS[toolName]
|
|
204
|
+
: undefined;
|
|
205
|
+
const sessionVisionIndex = options.sessionVisionIndex;
|
|
206
|
+
if (!fields || !sessionVisionIndex || typeof toolName !== 'string')
|
|
207
|
+
return def;
|
|
208
|
+
const execute = def.execute;
|
|
209
|
+
return {
|
|
210
|
+
...def,
|
|
211
|
+
execute(args, exec) {
|
|
212
|
+
const nextArgs = canonicalizeArgs(args, {
|
|
213
|
+
sessionVisionIndex,
|
|
214
|
+
...(exec?.agent === undefined ? {} : { agent: exec.agent }),
|
|
215
|
+
toolName,
|
|
216
|
+
}, fields);
|
|
217
|
+
return execute.call(def, nextArgs, exec);
|
|
218
|
+
},
|
|
219
|
+
};
|
|
200
220
|
}
|
|
@@ -215,7 +215,7 @@ function failureStream(message, code = 'VISION_IMAGE_DELIVERY_UNAVAILABLE') {
|
|
|
215
215
|
}
|
|
216
216
|
}
|
|
217
217
|
|
|
218
|
-
function directStream(ctx, core, plan, call, logger, source) {
|
|
218
|
+
function directStream(ctx, core, plan, call, logger, source, providerTransport) {
|
|
219
219
|
return {
|
|
220
220
|
async *[Symbol.asyncIterator]() {
|
|
221
221
|
const started = Date.now()
|
|
@@ -252,6 +252,7 @@ function directStream(ctx, core, plan, call, logger, source) {
|
|
|
252
252
|
maxTokens: call.maxTokens ?? 4096,
|
|
253
253
|
signal: call.signal,
|
|
254
254
|
sessionId: call.sessionId,
|
|
255
|
+
providerTransport,
|
|
255
256
|
resolveCredential: async (ref) => ref === keyRef ? apiKey : undefined,
|
|
256
257
|
},
|
|
257
258
|
)
|
|
@@ -349,7 +350,7 @@ function llmWithRuntimePolicy(ctx, llm, options) {
|
|
|
349
350
|
)
|
|
350
351
|
return
|
|
351
352
|
}
|
|
352
|
-
yield* directStream(ctx, core, plan, call, logger, source)
|
|
353
|
+
yield* directStream(ctx, core, plan, call, logger, source, options.providerTransport)
|
|
353
354
|
return
|
|
354
355
|
}
|
|
355
356
|
|
|
@@ -414,5 +415,21 @@ export function contextWithVisionBackendRuntimePolicy(ctx, options = {}) {
|
|
|
414
415
|
? finalContextDecorator(wrapped)
|
|
415
416
|
: wrapped
|
|
416
417
|
wrappedContexts.set(ctx, result)
|
|
418
|
+
let lifecycleOwned = false
|
|
419
|
+
try {
|
|
420
|
+
if (typeof ctx.effect === 'function') {
|
|
421
|
+
ctx.effect(
|
|
422
|
+
() => () => {
|
|
423
|
+
if (wrappedContexts.get(ctx) === result) wrappedContexts.delete(ctx)
|
|
424
|
+
},
|
|
425
|
+
'vision-router: backend runtime policy context lifecycle',
|
|
426
|
+
)
|
|
427
|
+
lifecycleOwned = true
|
|
428
|
+
}
|
|
429
|
+
} catch {
|
|
430
|
+
// The current caller can still use this one-shot policy view.
|
|
431
|
+
}
|
|
432
|
+
// options contains generation-owned config/transport/evidence closures.
|
|
433
|
+
if (!lifecycleOwned && wrappedContexts.get(ctx) === result) wrappedContexts.delete(ctx)
|
|
417
434
|
return result
|
|
418
435
|
}
|
|
@@ -552,6 +552,7 @@ export function createBackgroundCapabilityProfiler({
|
|
|
552
552
|
let lastForegroundAt = Number(now())
|
|
553
553
|
let runningController
|
|
554
554
|
let runningWork
|
|
555
|
+
let tickTask
|
|
555
556
|
let topologyDirty = false
|
|
556
557
|
let lastAuthority = resolveVisionRoutingAuthority(activeSettings(ctx, config))
|
|
557
558
|
const backoff = new Map()
|
|
@@ -689,7 +690,7 @@ export function createBackgroundCapabilityProfiler({
|
|
|
689
690
|
}
|
|
690
691
|
}
|
|
691
692
|
|
|
692
|
-
const
|
|
693
|
+
const runTick = async () => {
|
|
693
694
|
if (stopped || runningController) return
|
|
694
695
|
const currentNow = Number(now())
|
|
695
696
|
const current = activeSettings(ctx, config)
|
|
@@ -864,6 +865,32 @@ export function createBackgroundCapabilityProfiler({
|
|
|
864
865
|
}
|
|
865
866
|
}
|
|
866
867
|
|
|
868
|
+
const tick = () => {
|
|
869
|
+
if (tickTask) return tickTask
|
|
870
|
+
let tracked
|
|
871
|
+
// `tracked` is assigned before any rejection can be observed: the timer
|
|
872
|
+
// callback and the lifecycle disposer both call tick(), and a background
|
|
873
|
+
// scan that throws must degrade to a logged failure instead of an
|
|
874
|
+
// unhandled process-level rejection.
|
|
875
|
+
tracked = Promise.resolve()
|
|
876
|
+
.then(runTick)
|
|
877
|
+
.catch((error) => {
|
|
878
|
+
try {
|
|
879
|
+
logger?.warn?.(
|
|
880
|
+
'vision-router: background benchmark tick failed: %s',
|
|
881
|
+
bounded(error?.message ?? error),
|
|
882
|
+
)
|
|
883
|
+
} catch {
|
|
884
|
+
/* diagnostics only */
|
|
885
|
+
}
|
|
886
|
+
})
|
|
887
|
+
.finally(() => {
|
|
888
|
+
if (tickTask === tracked) tickTask = undefined
|
|
889
|
+
})
|
|
890
|
+
tickTask = tracked
|
|
891
|
+
return tracked
|
|
892
|
+
}
|
|
893
|
+
|
|
867
894
|
const foregroundStart = () => {
|
|
868
895
|
activeForeground += 1
|
|
869
896
|
lastForegroundAt = Number(now())
|
|
@@ -893,6 +920,7 @@ export function createBackgroundCapabilityProfiler({
|
|
|
893
920
|
if (timer !== undefined) clearTimer(timer)
|
|
894
921
|
timer = undefined
|
|
895
922
|
yieldBackground('background profiler stopped')
|
|
923
|
+
return tickTask ?? Promise.resolve()
|
|
896
924
|
}
|
|
897
925
|
|
|
898
926
|
schedule(idleMs)
|
|
@@ -1022,17 +1050,26 @@ export function installBackgroundCapabilityProfiling(ctx, config, core, store, o
|
|
|
1022
1050
|
// Event-driven wakeups are an optimization; the short authority scan remains.
|
|
1023
1051
|
}
|
|
1024
1052
|
|
|
1053
|
+
let lifecycleOwned = true
|
|
1025
1054
|
try {
|
|
1026
|
-
ctx?.effect?.(() => () => {
|
|
1055
|
+
ctx?.effect?.(() => async () => {
|
|
1027
1056
|
for (const dispose of eventDisposers.splice(0)) {
|
|
1028
1057
|
try { dispose() } catch {}
|
|
1029
1058
|
}
|
|
1030
|
-
profiler.stop()
|
|
1059
|
+
await profiler.stop()
|
|
1031
1060
|
}, 'vision-router: background capability profiler')
|
|
1032
1061
|
} catch {
|
|
1033
|
-
|
|
1062
|
+
lifecycleOwned = false
|
|
1063
|
+
for (const dispose of eventDisposers.splice(0)) {
|
|
1064
|
+
try { dispose() } catch {}
|
|
1065
|
+
}
|
|
1066
|
+
void profiler.stop().catch(() => {})
|
|
1034
1067
|
}
|
|
1035
1068
|
|
|
1069
|
+
// An inactive/unowned generation must not continue admitting client scripts,
|
|
1070
|
+
// routes, or tool wrappers after its profiler ownership was rejected.
|
|
1071
|
+
if (!lifecycleOwned) return { ctx, profiler }
|
|
1072
|
+
|
|
1036
1073
|
installVisionExactCheckClient(ctx)
|
|
1037
1074
|
|
|
1038
1075
|
try {
|
|
@@ -1,47 +1,47 @@
|
|
|
1
|
-
export const BACKGROUND_AUTH_STOP_TTL_MS = 10 * 60 * 1000
|
|
2
|
-
export const BACKGROUND_ENDPOINT_STOP_TTL_MS = 6 * 60 * 60 * 1000
|
|
3
|
-
export const BACKGROUND_TRANSIENT_RETRY_MS = 30 * 60 * 1000
|
|
4
|
-
|
|
1
|
+
export const BACKGROUND_AUTH_STOP_TTL_MS = 10 * 60 * 1000;
|
|
2
|
+
export const BACKGROUND_ENDPOINT_STOP_TTL_MS = 6 * 60 * 60 * 1000;
|
|
3
|
+
export const BACKGROUND_TRANSIENT_RETRY_MS = 30 * 60 * 1000;
|
|
5
4
|
const DEFAULT_POLICY = Object.freeze({
|
|
6
|
-
retryable: true,
|
|
7
|
-
persist: false,
|
|
8
|
-
retryAfterMs: undefined,
|
|
9
|
-
ttlMs: undefined,
|
|
10
|
-
})
|
|
11
|
-
|
|
12
|
-
const POLICIES = Object.freeze({
|
|
13
|
-
auth: Object.freeze({
|
|
14
|
-
retryable: false,
|
|
15
|
-
persist: false,
|
|
16
|
-
ttlMs: BACKGROUND_AUTH_STOP_TTL_MS,
|
|
17
|
-
}),
|
|
18
|
-
protocol: Object.freeze({
|
|
19
|
-
retryable: false,
|
|
20
|
-
persist: true,
|
|
21
|
-
ttlMs: BACKGROUND_ENDPOINT_STOP_TTL_MS,
|
|
22
|
-
}),
|
|
23
|
-
unavailable: Object.freeze({
|
|
24
|
-
retryable: false,
|
|
25
|
-
persist: true,
|
|
26
|
-
ttlMs: BACKGROUND_ENDPOINT_STOP_TTL_MS,
|
|
27
|
-
}),
|
|
28
|
-
'unsupported-image': Object.freeze({
|
|
29
|
-
retryable: false,
|
|
30
|
-
persist: false,
|
|
31
|
-
}),
|
|
32
|
-
'visual-proof': Object.freeze({
|
|
33
5
|
retryable: true,
|
|
34
6
|
persist: false,
|
|
35
|
-
retryAfterMs:
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
7
|
+
retryAfterMs: undefined,
|
|
8
|
+
ttlMs: undefined,
|
|
9
|
+
});
|
|
10
|
+
const POLICIES = Object.freeze({
|
|
11
|
+
auth: Object.freeze({
|
|
12
|
+
retryable: false,
|
|
13
|
+
persist: false,
|
|
14
|
+
ttlMs: BACKGROUND_AUTH_STOP_TTL_MS,
|
|
15
|
+
}),
|
|
16
|
+
protocol: Object.freeze({
|
|
17
|
+
retryable: false,
|
|
18
|
+
persist: true,
|
|
19
|
+
ttlMs: BACKGROUND_ENDPOINT_STOP_TTL_MS,
|
|
20
|
+
}),
|
|
21
|
+
unavailable: Object.freeze({
|
|
22
|
+
retryable: false,
|
|
23
|
+
persist: true,
|
|
24
|
+
ttlMs: BACKGROUND_ENDPOINT_STOP_TTL_MS,
|
|
25
|
+
}),
|
|
26
|
+
'unsupported-image': Object.freeze({
|
|
27
|
+
retryable: false,
|
|
28
|
+
persist: false,
|
|
29
|
+
}),
|
|
30
|
+
'visual-proof': Object.freeze({
|
|
31
|
+
retryable: true,
|
|
32
|
+
persist: false,
|
|
33
|
+
retryAfterMs: BACKGROUND_TRANSIENT_RETRY_MS,
|
|
34
|
+
}),
|
|
35
|
+
infrastructure: Object.freeze({
|
|
36
|
+
retryable: true,
|
|
37
|
+
persist: false,
|
|
38
|
+
retryAfterMs: BACKGROUND_TRANSIENT_RETRY_MS,
|
|
39
|
+
}),
|
|
40
|
+
});
|
|
44
41
|
/** Failure lifetime is separate from failure classification. */
|
|
45
42
|
export function backgroundFailurePolicy(errorClass) {
|
|
46
|
-
|
|
43
|
+
const key = String(errorClass ?? '');
|
|
44
|
+
return Object.prototype.hasOwnProperty.call(POLICIES, key)
|
|
45
|
+
? POLICIES[key]
|
|
46
|
+
: DEFAULT_POLICY;
|
|
47
47
|
}
|