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.
Files changed (132) hide show
  1. package/README.md +16 -7
  2. package/README.zh.md +16 -7
  3. package/docs/architecture/2.3.0-compat-retirement.md +147 -0
  4. package/docs/architecture/2.3.0-execution-plan.md +410 -0
  5. package/docs/architecture/2.3.0-final-audit.md +279 -0
  6. package/docs/architecture/2.3.0-reopened-plan.md +720 -0
  7. package/docs/architecture/3.0-architecture-plan.md +252 -0
  8. package/docs/architecture/3.0-r0-contract-ledger.md +137 -0
  9. package/docs/architecture/3.0-r1-source-artifact-boundary.md +98 -0
  10. package/docs/architecture/3.0-r10-final-correctness-closure.md +207 -0
  11. package/docs/architecture/3.0-r11-adversarial-review.md +441 -0
  12. package/docs/architecture/3.0-r5-browser-go-no-go.md +165 -0
  13. package/docs/architecture/3.0-r5-browser-typescript-gate.md +105 -0
  14. package/docs/architecture/3.0-r6-compat-retirement.md +203 -0
  15. package/docs/architecture/3.0-r7-public-api.md +151 -0
  16. package/docs/architecture/3.0-r8-ci-release.md +248 -0
  17. package/docs/architecture/3.0-r9-final-audit.md +370 -0
  18. package/docs/architecture/compat-inventory.md +26 -34
  19. package/docs/architecture/dsh-compatibility-matrix.md +65 -54
  20. package/docs/architecture/dsh-support-window.md +30 -22
  21. package/docs/architecture/final-closure-audit.md +2 -0
  22. package/docs/releases/v2.3.0.md +53 -0
  23. package/docs/releases/v3.0.0.md +85 -0
  24. package/entry.js +9 -57
  25. package/index.js +268 -761
  26. package/lib/agent-request-route-authority.js +1 -20
  27. package/lib/catalog-corrections.js +1 -2
  28. package/lib/client-maintenance-actions.js +145 -0
  29. package/lib/client-presentation-boundary-main.js +75 -17
  30. package/lib/client-presentation-boundary.js +53 -28
  31. package/lib/client.js +246 -144
  32. package/lib/core-apply-lifecycle.js +41 -0
  33. package/lib/core-cache.js +87 -0
  34. package/lib/core-primitives.js +91 -278
  35. package/lib/core-vision-surface-domain.js +63 -0
  36. package/lib/core-vision-surface.js +11 -77
  37. package/lib/delegated-call-config.js +15 -24
  38. package/lib/desktop-screenshot-tool.js +262 -0
  39. package/lib/doctor-cli.js +0 -0
  40. package/lib/doctor-runtime.js +4 -1
  41. package/lib/dsh-contract-compat.js +37 -256
  42. package/lib/dsh-host-capabilities.js +3 -9
  43. package/lib/dsh-settings-017-compat.js +17 -10
  44. package/lib/dsh-support-window.js +3 -3
  45. package/lib/file-logger.js +8 -3
  46. package/lib/grounding-coordinate-runtime.js +1 -2
  47. package/lib/http-body-limit.js +97 -79
  48. package/lib/http-compat.js +1 -2
  49. package/lib/image-offload-compat.js +30 -21
  50. package/lib/image-resource-governor.js +210 -228
  51. package/lib/legacy-global-proxy-boundary.js +46 -3
  52. package/lib/live-model-client-prelude.js +12 -4
  53. package/lib/live-model-discovery.js +134 -43
  54. package/lib/local-remote-settings-permission.js +33 -7
  55. package/lib/local-vision-stabilizer.js +31 -17
  56. package/lib/maintenance-routes.js +134 -0
  57. package/lib/native-image-coexistence.js +15 -3
  58. package/lib/ollama-cold-start.js +87 -23
  59. package/lib/pi-ai-bridge-wire-compat.js +3 -2
  60. package/lib/proxy-dispatcher-pool.js +151 -92
  61. package/lib/proxy-routing.js +75 -51
  62. package/lib/proxy-url-compat.js +3 -10
  63. package/lib/public-config.js +36 -0
  64. package/lib/public-entry.d.ts +148 -0
  65. package/lib/public-entry.js +19 -11
  66. package/lib/remote-settings-bridge.js +11 -0
  67. package/lib/remote-settings-risk-client-boundary.js +155 -0
  68. package/lib/remote-settings-risk-confirmation.js +17 -105
  69. package/lib/replay-delegation.js +12 -7
  70. package/lib/replay-envelope-v2-compat.js +12 -7
  71. package/lib/runtime-composition.js +127 -34
  72. package/lib/runtime-i18n-boundary.js +10 -11
  73. package/lib/screenshot-source-boundary.js +2 -2
  74. package/lib/session-affinity-runtime.js +113 -97
  75. package/lib/session-affinity.js +89 -76
  76. package/lib/session-event-history-compat.js +30 -0
  77. package/lib/session-message-source-compat.js +7 -3
  78. package/lib/session-query-readers.js +263 -0
  79. package/lib/session-surface-compat.js +30 -31
  80. package/lib/session-surface-policy-domain.js +85 -0
  81. package/lib/session-surface-policy.js +2 -111
  82. package/lib/session-turn-resolver.js +2 -6
  83. package/lib/session-vision-attachment-index.js +143 -0
  84. package/lib/session-vision-delegation-boundary.js +34 -0
  85. package/lib/session-vision-delegation.js +22 -0
  86. package/lib/session-vision-index.js +179 -116
  87. package/lib/session-vision-mode-authority.js +229 -192
  88. package/lib/session-vision-mode-boundary.js +65 -22
  89. package/lib/session-vision-policy-domain.js +304 -0
  90. package/lib/session-vision-policy.js +104 -0
  91. package/lib/session-vision-state.js +368 -378
  92. package/lib/settings-client-017-compat.js +9 -7
  93. package/lib/settings-client-loader-lifecycle.js +129 -0
  94. package/lib/sharp-runtime.js +0 -7
  95. package/lib/structured-flow-hardening.js +25 -6
  96. package/lib/tesseract-exec-compat.js +9 -1
  97. package/lib/turn-budget-context.js +47 -41
  98. package/lib/update-check.js +11 -2
  99. package/lib/vision-attachment-handle-runtime.js +176 -156
  100. package/lib/vision-backend-runtime-policy.js +19 -2
  101. package/lib/vision-background-benchmark.js +41 -4
  102. package/lib/vision-background-failure-policy.js +41 -41
  103. package/lib/vision-background-stop-store-core.js +217 -0
  104. package/lib/vision-background-stop-store.js +8 -179
  105. package/lib/vision-capability-benchmark-service.js +1 -0
  106. package/lib/vision-capability-benchmark.js +11 -61
  107. package/lib/vision-capability-evidence.js +35 -0
  108. package/lib/vision-capability-fingerprint.js +72 -0
  109. package/lib/vision-capability-identity.js +116 -61
  110. package/lib/vision-capability-router.js +13 -33
  111. package/lib/vision-chunk-assembler.js +95 -0
  112. package/lib/vision-diagnostics-routes.js +246 -0
  113. package/lib/vision-evidence-guidance.js +3 -1
  114. package/lib/vision-execution-order-apply.js +45 -55
  115. package/lib/vision-execution-order.js +19 -19
  116. package/lib/vision-execution-policy.js +12 -7
  117. package/lib/vision-model-registry.js +30 -17
  118. package/lib/vision-provider-transport.js +137 -176
  119. package/lib/vision-routing-authority.js +47 -48
  120. package/lib/vision-routing-product.js +26 -29
  121. package/lib/vision-routing-settings-prelude.js +8 -3
  122. package/lib/vision-runtime-performance-store.js +166 -0
  123. package/lib/vision-runtime-performance.js +13 -142
  124. package/lib/vision-toggle-root-hardening.js +22 -5
  125. package/lib/vision-tool-runtime-boundary.js +104 -31
  126. package/lib/web/dsh-settings-017-root-transport.js +27 -12
  127. package/lib/web/remote-settings-client.js +6 -9
  128. package/lib/web-capability-boundary.js +17 -7
  129. package/lib/wrapper-directory.js +14 -23
  130. package/package.json +56 -20
  131. package/lib/android-attachment-compat.js +0 -244
  132. 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
- vision_describe: { arrays: ['attachmentIds', 'paths'] },
7
- vision_bootstrap: { arrays: ['attachmentIds', 'paths'] },
8
- vision_materialize: { scalars: ['image'] },
9
- vision_ground: { scalars: ['image'] },
10
- vision_detect: { scalars: ['image'] },
11
- vision_crop: { scalars: ['image'] },
12
- vision_present: { scalars: ['image'] },
13
- vision_pixel_diff: { scalars: ['original', 'rebuilt'] },
14
- vision_colors: { scalars: ['image'] },
15
- vision_ocr: { scalars: ['image'] },
16
- vision_trace: { scalars: ['image'] },
17
- vision_extract_foreground: { scalars: ['image'] },
18
- })
19
-
20
- function isObject(value) {
21
- return value !== null && typeof value === 'object'
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
- if (!ref || typeof ref !== 'object') return undefined
26
- const value = ref.attachmentId ?? ref.id
27
- if (value === undefined || value === null) return undefined
28
- const id = String(value).trim()
29
- return id === '' ? undefined : id
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
- try {
34
- if (!session || typeof session.deriveMessages !== 'function') return []
35
- const messages = session.deriveMessages()
36
- return Array.isArray(messages) ? messages : []
37
- } catch {
38
- return []
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
- if (!Array.isArray(blocks)) return
44
- for (const block of blocks) {
45
- if (!block || typeof block !== 'object') continue
46
- if (block.type === 'image' && block.attachment) out.push(block.attachment)
47
- if (Array.isArray(block.content)) collectImageRefsFromBlocks(block.content, out)
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
- if (!Array.isArray(messages)) return
53
- for (const message of messages) collectImageRefsFromBlocks(message?.content, out)
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
- const refs = []
58
- collectImageRefsFromMessages(sessionMessages(agent?.session), refs)
59
- collectImageRefsFromMessages(agent?.inbox?.nextTurn, refs)
60
- collectImageRefsFromMessages(agent?.inbox?.nextStep, refs)
61
-
62
- const unique = new Map()
63
- for (const ref of refs) {
64
- const id = attachmentIdOf(ref)
65
- if (id !== undefined && !unique.has(id)) unique.set(id, ref)
66
- }
67
- return [...unique.values()]
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
- return typeof value === 'string' && PROJECTED_SHA256_HANDLE.test(value.trim())
81
+ return typeof value === 'string' && PROJECTED_SHA256_HANDLE.test(value.trim());
72
82
  }
73
-
74
83
  function isSha256HandleLike(value) {
75
- return typeof value === 'string' && SHA256_HANDLE_PREFIX.test(value.trim())
84
+ return typeof value === 'string' && SHA256_HANDLE_PREFIX.test(value.trim());
76
85
  }
77
-
78
86
  function isCanonicalSha256Id(value) {
79
- return typeof value === 'string' && CANONICAL_SHA256_ID.test(value.trim())
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
- const token = typeof handle === 'string' ? handle.trim() : ''
93
- if (!isProjectedAttachmentHandle(token)) return { kind: 'not-projected', value: handle }
94
-
95
- const session = agent?.session
96
- const refs = authorizedRefs(agent)
97
- const wanted = token.toLowerCase()
98
- const matches = refs.filter((ref) => {
99
- const id = attachmentIdOf(ref)
100
- return id !== undefined && id.toLowerCase().startsWith(wanted)
101
- })
102
-
103
- if (matches.length === 0) return { kind: 'unknown', handle: token }
104
- if (matches.length > 1) return { kind: 'ambiguous', handle: token }
105
-
106
- const candidate = matches[0]
107
- const canonicalId = attachmentIdOf(candidate)
108
- if (canonicalId === undefined) return { kind: 'unknown', handle: token }
109
-
110
- // The Session event/inbox proves authorization; warm the canonical bounded
111
- // index with that exact Host-owned ref, then require the index to hand it
112
- // back. This keeps all downstream tools on the same recovery/identity seam.
113
- try {
114
- sessionVisionIndex?.recordAttachments?.(session, [candidate])
115
- } catch {
116
- // Authorization is still fail-closed if the bounded cache cannot be warmed.
117
- }
118
- if (!sessionVisionIndex || typeof sessionVisionIndex.lookupAttachment !== 'function') {
119
- return { kind: 'unknown', handle: token }
120
- }
121
- const ref = sessionVisionIndex.lookupAttachment(session, canonicalId)
122
- if (ref === undefined) return { kind: 'unknown', handle: token }
123
- return { kind: 'resolved', handle: token, canonicalId, ref }
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
- if (!isSha256HandleLike(value)) return value
128
- if (isCanonicalSha256Id(value)) return value
129
- if (!isProjectedAttachmentHandle(value)) {
130
- throw new Error(
131
- `${context.toolName}: invalid attachment handle "${String(value).trim()}" ` +
132
- '(sha256 attachment handles must be a canonical id or a DSH-projected 8+ hex prefix)',
133
- )
134
- }
135
- const result = resolveProjectedAttachmentHandle(value, context)
136
- if (result.kind === 'resolved') return result.canonicalId
137
- if (result.kind === 'ambiguous') {
138
- throw new Error(
139
- `${context.toolName}: ambiguous attachment handle "${result.handle}" ` +
140
- '(multiple images in this conversation share that prefix; use a full attachment id or attach the image again)',
141
- )
142
- }
143
- throw new Error(
144
- `${context.toolName}: unknown attachment handle "${result.handle}" ` +
145
- '(it is not authorized by an image in this conversation; attach the image again if needed)',
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
- if (!isObject(args) || Array.isArray(args)) return args
151
- let next
152
- for (const field of fields.scalars ?? []) {
153
- if (!Object.hasOwn(args, field)) continue
154
- const value = canonicalizeValue(args[field], context)
155
- if (value !== args[field]) {
156
- next ??= { ...args }
157
- next[field] = value
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
- for (const field of fields.arrays ?? []) {
161
- const values = args[field]
162
- if (!Array.isArray(values)) continue
163
- let changed = false
164
- const mapped = values.map((value) => {
165
- const canonical = canonicalizeValue(value, context)
166
- if (canonical !== value) changed = true
167
- return canonical
168
- })
169
- if (changed) {
170
- next ??= { ...args }
171
- next[field] = mapped
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
- if (!def || typeof def !== 'object' || typeof def.execute !== 'function') return def
186
- const fields = TOOL_FIELDS[def.name]
187
- if (!fields || !options.sessionVisionIndex) return def
188
- const execute = def.execute
189
- return {
190
- ...def,
191
- execute(args, exec) {
192
- const nextArgs = canonicalizeArgs(args, {
193
- sessionVisionIndex: options.sessionVisionIndex,
194
- agent: exec?.agent,
195
- toolName: def.name,
196
- }, fields)
197
- return execute.call(def, nextArgs, exec)
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 tick = async () => {
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
- // Cleanup registration is best-effort; the profiler remains process-scoped.
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: BACKGROUND_TRANSIENT_RETRY_MS,
36
- }),
37
- infrastructure: Object.freeze({
38
- retryable: true,
39
- persist: false,
40
- retryAfterMs: BACKGROUND_TRANSIENT_RETRY_MS,
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
- return POLICIES[String(errorClass ?? '')] ?? DEFAULT_POLICY
43
+ const key = String(errorClass ?? '');
44
+ return Object.prototype.hasOwnProperty.call(POLICIES, key)
45
+ ? POLICIES[key]
46
+ : DEFAULT_POLICY;
47
47
  }