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
@@ -14,14 +14,11 @@ export const SETTINGS_017_CLIENT_PRELUDE = String.raw`(function(){
14
14
  var connectionCache = typeof WeakMap === 'function' ? new WeakMap() : undefined;
15
15
 
16
16
  function safeGet(ctx, name) {
17
- if (!ctx) return undefined;
17
+ if (!ctx || typeof ctx.get !== 'function') return undefined;
18
18
  try {
19
- if (typeof ctx.get === 'function') {
20
- var value = ctx.get(name);
21
- if (value !== undefined && value !== null) return value;
22
- }
23
- } catch (_) {}
24
- try { return ctx[name]; } catch (_) { return undefined; }
19
+ var value = ctx.get(name);
20
+ return value === undefined || value === null ? undefined : value;
21
+ } catch (_) { return undefined; }
25
22
  }
26
23
 
27
24
  function isLoopbackLocation(locationLike) {
@@ -212,6 +209,11 @@ export const SETTINGS_017_CLIENT_PRELUDE = String.raw`(function(){
212
209
  var getter = Reflect.get(target, property, target);
213
210
  if (typeof getter !== 'function') return getter;
214
211
  return function(name) {
212
+ if (name === 'settingsScope') {
213
+ var original = safeGet(target, 'settingsScope');
214
+ if (original && typeof original.bind === 'function') return original;
215
+ return syntheticBinder(ctx, configFormsBinder(target));
216
+ }
215
217
  var value = getter.call(target, name);
216
218
  return name === 'connection' ? normalizeConnection(value) : value;
217
219
  };
@@ -0,0 +1,129 @@
1
+ import { injectClientCarrierHtml, installClientScriptCarriers } from './client-script-carrier.js'
2
+ import {
3
+ LOCAL_PERMISSION_CLIENT_BOUNDARY_SOURCE,
4
+ LOCAL_REMOTE_SETTINGS_PERMISSION_FIELD,
5
+ LOCAL_REMOTE_SETTINGS_PERMISSION_PATH,
6
+ } from './local-remote-settings-permission.js'
7
+ import { REMOTE_SETTINGS_RISK_CLIENT_BOUNDARY_SOURCE } from './remote-settings-risk-client-boundary.js'
8
+
9
+ // Keep the legacy marker value so mixed-generation/HMR pages do not install
10
+ // a second lifecycle wrapper after the source/API rename.
11
+ const SETTINGS_CLIENT_LOADER_LIFECYCLE_MARK = 'data-vision-router-settings-rc8-lifecycle'
12
+
13
+ /**
14
+ * Supported Hosts can replace ModuleLoader.load() inside loader.create(). This
15
+ * compatibility owner only re-attaches the current local-permission and
16
+ * remote-risk product boundaries after that queue-to-live transition; product
17
+ * semantics stay in their capability owners.
18
+ */
19
+ export const SETTINGS_CLIENT_LOADER_LIFECYCLE_PRELUDE = String.raw`(function(){
20
+ 'use strict';
21
+ var TARGET = 'dsh-vision-router';
22
+ ${LOCAL_PERMISSION_CLIENT_BOUNDARY_SOURCE}
23
+ var localPermissionBoundary = createLocalRemoteSettingsPermissionClientBoundary({
24
+ endpoint: '${LOCAL_REMOTE_SETTINGS_PERMISSION_PATH}',
25
+ field: '${LOCAL_REMOTE_SETTINGS_PERMISSION_FIELD}',
26
+ fetchImpl: typeof fetch === 'function'
27
+ ? function(url, options) { return fetch(url, options); }
28
+ : undefined
29
+ });
30
+
31
+ ${REMOTE_SETTINGS_RISK_CLIENT_BOUNDARY_SOURCE}
32
+ var riskBoundary = createRemoteSettingsRiskClientBoundary({
33
+ confirmImpl: function(message) {
34
+ return typeof window.confirm === 'function' && window.confirm(message) === true;
35
+ },
36
+ alertImpl: function(message) {
37
+ try { if (typeof window.alert === 'function') window.alert(message); } catch (_) {}
38
+ },
39
+ locale: function() {
40
+ try {
41
+ return String((document && document.documentElement && document.documentElement.lang)
42
+ || (navigator && navigator.language) || '');
43
+ } catch (_) { return ''; }
44
+ },
45
+ location: function() {
46
+ try { return window && window.location; } catch (_) { return undefined; }
47
+ },
48
+ normalizeLoopback: true
49
+ });
50
+
51
+ function patchLiveLoader(loader) {
52
+ if (!loader || typeof loader.load !== 'function' || loader.load.__visionRouterSettingsRc8Lifecycle) return;
53
+ var original = loader.load;
54
+ function load(spec) {
55
+ if (spec && spec.id === TARGET && typeof spec.factory === 'function') {
56
+ var factory = spec.factory;
57
+ spec = Object.assign({}, spec, {
58
+ factory: function(require) {
59
+ var exports = factory(require);
60
+ if (exports && typeof exports.apply === 'function' && !exports.apply.__visionRouterSettingsRc8Lifecycle) {
61
+ var apply = exports.apply;
62
+ var wrappedApply = function(ctx) {
63
+ var rest = Array.prototype.slice.call(arguments, 1);
64
+ var nextCtx = riskBoundary.wrapContext(localPermissionBoundary.wrapContext(ctx));
65
+ return apply.apply(exports, [nextCtx].concat(rest));
66
+ };
67
+ Object.defineProperty(wrappedApply, '__visionRouterSettingsRc8Lifecycle', { value: true });
68
+ exports.apply = wrappedApply;
69
+ }
70
+ return exports;
71
+ }
72
+ });
73
+ }
74
+ return original.call(this, spec);
75
+ }
76
+ Object.defineProperty(load, '__visionRouterSettingsRc8Lifecycle', { value: true });
77
+ loader.load = load;
78
+ }
79
+
80
+ function patchCreate(loader) {
81
+ if (!loader || typeof loader.create !== 'function' || loader.create.__visionRouterSettingsRc8Lifecycle) return;
82
+ var originalCreate = loader.create;
83
+ function create() {
84
+ var result = originalCreate.apply(this, arguments);
85
+ patchLiveLoader(loader);
86
+ return result;
87
+ }
88
+ Object.defineProperty(create, '__visionRouterSettingsRc8Lifecycle', { value: true });
89
+ loader.create = create;
90
+ if (loader.mode === 'live') patchLiveLoader(loader);
91
+ }
92
+
93
+ function install() {
94
+ if (window.__ModuleLoader__) {
95
+ patchCreate(window.__ModuleLoader__);
96
+ return;
97
+ }
98
+ var descriptor = Object.getOwnPropertyDescriptor(window, '__ModuleLoader__');
99
+ if (descriptor && descriptor.configurable === false) return;
100
+ var previousGet = descriptor && descriptor.get;
101
+ var previousSet = descriptor && descriptor.set;
102
+ var stored = descriptor && Object.prototype.hasOwnProperty.call(descriptor, 'value') ? descriptor.value : undefined;
103
+ Object.defineProperty(window, '__ModuleLoader__', {
104
+ configurable: true,
105
+ enumerable: !descriptor || descriptor.enumerable !== false,
106
+ get: function(){ return previousGet ? previousGet.call(window) : stored; },
107
+ set: function(value) {
108
+ if (previousSet) previousSet.call(window, value); else stored = value;
109
+ try { patchCreate(previousGet ? previousGet.call(window) : value); } catch (_) {}
110
+ }
111
+ });
112
+ if (stored) patchCreate(stored);
113
+ }
114
+
115
+ try { install(); } catch (_) {}
116
+ })();`
117
+
118
+ export function injectSettingsClientLoaderLifecyclePrelude(html) {
119
+ return injectClientCarrierHtml(html, SETTINGS_CLIENT_LOADER_LIFECYCLE_MARK, SETTINGS_CLIENT_LOADER_LIFECYCLE_PRELUDE)
120
+ }
121
+
122
+ export function installSettingsClientLoaderLifecycle(ctx) {
123
+ installClientScriptCarriers(ctx, {
124
+ marker: SETTINGS_CLIENT_LOADER_LIFECYCLE_MARK,
125
+ prelude: SETTINGS_CLIENT_LOADER_LIFECYCLE_PRELUDE,
126
+ label: 'vision-router: settings client loader lifecycle',
127
+ injectHtml: injectSettingsClientLoaderLifecyclePrelude,
128
+ })
129
+ }
@@ -10,7 +10,6 @@ import { Worker } from 'node:worker_threads'
10
10
  import { createRequire } from 'node:module'
11
11
  import { pathToFileURL } from 'node:url'
12
12
  import { promisify } from 'node:util'
13
- import { appendPromptToImageOnlyMessage, fetchWithOpenAICompatibility } from './http-compat.js'
14
13
  import {
15
14
  directSessionAffinityHeaders,
16
15
  isOfficialOpenCodeGoUrl,
@@ -19,12 +18,6 @@ import {
19
18
  sessionIdentityOf,
20
19
  } from './session-affinity.js'
21
20
  import { runWithVisionSessionAffinity, streamWithVisionSessionAffinity } from './session-affinity-runtime.js'
22
- import {
23
- routingCorrectionFor,
24
- toAnthropicMessages,
25
- callAnthropicCompatible,
26
- anthropicMediaType,
27
- } from './catalog-corrections.js'
28
21
  import { createCachedUpdateChecker } from './update-check.js'
29
22
  import { probeLocalBackends } from './local-connection-probe.js'
30
23
  import { detectDshSelfUpdatePlan, runDshPluginUpdate } from './self-update.js'
@@ -4,6 +4,7 @@ import {
4
4
  } from './turn-budget-context.js'
5
5
  import { runWithDepthConfig } from './depth-guidance.js'
6
6
  import { runtimeLanguageFor } from './runtime-i18n.js'
7
+ import { visionRouterMessageSource } from './session-message-source-compat.js'
7
8
 
8
9
  const STRUCTURED_EVIDENCE_TOOLS = new Set([
9
10
  'vision_describe',
@@ -440,7 +441,7 @@ function appendGuardMessage(decision, payload, state, config) {
440
441
  ? '已达到本轮设置的深挖次数上限。不要再调用视觉工具;请基于已经获得的证据作答,并明确仍存在的不确定性。'
441
442
  : '连续多次后续视觉调用都没有产出可用证据。不要继续重复调用视觉工具;请基于已经获得的证据作答,并明确仍存在的不确定性。',
442
443
  }],
443
- source: { kind: 'plugin', plugin: 'dsh-vision-router' },
444
+ source: visionRouterMessageSource(payload?.agent?.session),
444
445
  }
445
446
  return appendSyntheticGuardOnce(decision, baseMessages, state, message, true)
446
447
  }
@@ -461,7 +462,7 @@ function appendGuardMessage(decision, payload, state, config) {
461
462
  ? 'The structured bootstrap is complete, but no usable task-directed visual evidence has been produced yet. Call at least one vision tool that can add or verify evidence, then answer after it succeeds.'
462
463
  : '结构化预识别已经完成,但目前还没有产出可用的任务定向视觉证据。请至少调用一个能新增或验证证据的视觉工具,成功后再作答。',
463
464
  }],
464
- source: { kind: 'plugin', plugin: 'dsh-vision-router' },
465
+ source: visionRouterMessageSource(payload?.agent?.session),
465
466
  }
466
467
  return appendSyntheticGuardOnce(decision, baseMessages, state, message)
467
468
  }
@@ -629,7 +630,7 @@ function wrapSettingsScope(scope) {
629
630
  })
630
631
  }
631
632
 
632
- function wrapSettingsService(settings, setScope) {
633
+ function wrapSettingsService(settings, bindScope) {
633
634
  if (!settings || typeof settings !== 'object') return settings
634
635
  return new Proxy(settings, {
635
636
  get(target, property) {
@@ -642,7 +643,7 @@ function wrapSettingsService(settings, setScope) {
642
643
  return (namespace, ...args) => {
643
644
  const scope = register.call(target, namespace, ...args)
644
645
  if (namespace !== 'vision-router') return scope
645
- setScope(scope)
646
+ if (bindScope(scope) !== true) return scope
646
647
  return wrapSettingsScope(scope)
647
648
  }
648
649
  },
@@ -651,7 +652,8 @@ function wrapSettingsService(settings, setScope) {
651
652
 
652
653
  function wrapSettingsContext(childCtx, setScope) {
653
654
  if (!childCtx || typeof childCtx !== 'object' || !childCtx.settings) return childCtx
654
- const settings = wrapSettingsService(childCtx.settings, setScope)
655
+ const bindScope = (scope) => setScope(scope, childCtx)
656
+ const settings = wrapSettingsService(childCtx.settings, bindScope)
655
657
  return new Proxy(childCtx, {
656
658
  get(target, property) {
657
659
  if (property === 'settings') return settings
@@ -665,7 +667,24 @@ export function installStructuredFlowHardening(ctx, config = {}) {
665
667
  if (!ctx || typeof ctx !== 'object') return ctx
666
668
  const states = new WeakMap()
667
669
  let settingsScope
668
- const setSettingsScope = (scope) => { settingsScope = scope }
670
+ const setSettingsScope = (scope, ownerCtx) => {
671
+ if (!scope || (typeof scope !== 'object' && typeof scope !== 'function')) return false
672
+ // Settings is a dynamic child service. Never retain a scope unless the
673
+ // child fiber owns its lifetime; otherwise an HMR gap can read stale config.
674
+ try {
675
+ if (typeof ownerCtx?.effect !== 'function') return false
676
+ ownerCtx.effect(
677
+ () => () => {
678
+ if (settingsScope === scope) settingsScope = undefined
679
+ },
680
+ 'vision-router: structured flow settings lifecycle',
681
+ )
682
+ } catch {
683
+ return false
684
+ }
685
+ settingsScope = scope
686
+ return true
687
+ }
669
688
  const activeConfig = () => {
670
689
  try {
671
690
  const value = settingsScope && typeof settingsScope.get === 'function' ? settingsScope.get() : config
@@ -326,7 +326,15 @@ export function installVisionRouterExecFileCompat(ctx, options = {}) {
326
326
  }
327
327
 
328
328
  if (ctx && typeof ctx.effect === 'function') {
329
- ctx.effect(() => dispose, 'vision-router: execFile compatibility')
329
+ try {
330
+ ctx.effect(() => dispose, 'vision-router: execFile compatibility')
331
+ } catch (error) {
332
+ // The process-global execFile seam was already mutated above. If Cordis
333
+ // cannot own this generation, undo exactly this installation before the
334
+ // failure escapes so no orphan patch survives HMR/startup failure.
335
+ dispose()
336
+ throw error
337
+ }
330
338
  }
331
339
  return dispose
332
340
  }
@@ -1,58 +1,64 @@
1
- import { AsyncLocalStorage } from 'node:async_hooks'
2
-
3
- const turnBudgetScope = new AsyncLocalStorage()
4
-
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+ const turnBudgetScope = new AsyncLocalStorage();
5
3
  function usableSignal(value) {
6
- return value && typeof value === 'object' && typeof value.aborted === 'boolean' ? value : undefined
4
+ return value !== null
5
+ && typeof value === 'object'
6
+ && typeof value.aborted === 'boolean'
7
+ ? value
8
+ : undefined;
7
9
  }
8
-
9
10
  function finiteDeadline(value) {
10
- const number = Number(value)
11
- return Number.isFinite(number) ? number : undefined
11
+ const number = Number(value);
12
+ return Number.isFinite(number) ? number : undefined;
13
+ }
14
+ function objectBudget(value) {
15
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
16
+ ? value
17
+ : undefined;
12
18
  }
13
-
14
19
  /**
15
20
  * Run one async tool execution inside an ambient turn-level budget.
16
21
  * Nested budgets may tighten the parent, but may not discard an earlier parent
17
22
  * deadline, cancellation signal or artifact-run identity.
18
23
  */
19
24
  export function runWithVisionTurnBudget(budget, fn) {
20
- if (typeof fn !== 'function') throw new TypeError('runWithVisionTurnBudget: fn must be a function')
21
- const parent = turnBudgetScope.getStore()
22
- const parentSignal = usableSignal(parent?.signal)
23
- const childSignal = usableSignal(budget?.signal)
24
- const parentDeadline = finiteDeadline(parent?.deadlineAt)
25
- const childDeadline = finiteDeadline(budget?.deadlineAt)
26
- let next = budget
27
- if (parentSignal && childSignal && parentSignal !== childSignal) {
28
- next = { ...(next ?? {}), signal: AbortSignal.any([parentSignal, childSignal]) }
29
- } else if (parentSignal && !childSignal) {
30
- next = { ...(next ?? {}), signal: parentSignal }
31
- }
32
- if (parentDeadline !== undefined && (childDeadline === undefined || parentDeadline < childDeadline)) {
33
- next = { ...(next ?? {}), deadlineAt: parentDeadline }
34
- }
35
- if (
36
- typeof parent?.artifactRunId === 'string' &&
37
- parent.artifactRunId !== '' &&
38
- !(typeof next?.artifactRunId === 'string' && next.artifactRunId !== '')
39
- ) {
40
- next = { ...(next ?? {}), artifactRunId: parent.artifactRunId }
41
- }
42
- return turnBudgetScope.run(next, fn)
25
+ if (typeof fn !== 'function')
26
+ throw new TypeError('runWithVisionTurnBudget: fn must be a function');
27
+ const parent = turnBudgetScope.getStore();
28
+ const parentSignal = usableSignal(parent?.signal);
29
+ const childSignal = usableSignal(budget?.signal);
30
+ const parentDeadline = finiteDeadline(parent?.deadlineAt);
31
+ const childDeadline = finiteDeadline(budget?.deadlineAt);
32
+ let next = objectBudget(budget);
33
+ if (parentSignal && childSignal && parentSignal !== childSignal) {
34
+ next = { ...(next ?? {}), signal: AbortSignal.any([parentSignal, childSignal]) };
35
+ }
36
+ else if (parentSignal && !childSignal) {
37
+ next = { ...(next ?? {}), signal: parentSignal };
38
+ }
39
+ if (parentDeadline !== undefined && (childDeadline === undefined || parentDeadline < childDeadline)) {
40
+ next = { ...(next ?? {}), deadlineAt: parentDeadline };
41
+ }
42
+ if (typeof parent?.artifactRunId === 'string'
43
+ && parent.artifactRunId !== ''
44
+ && !(typeof next?.artifactRunId === 'string' && next.artifactRunId !== '')) {
45
+ next = { ...(next ?? {}), artifactRunId: parent.artifactRunId };
46
+ }
47
+ return turnBudgetScope.run(next, fn);
43
48
  }
44
-
45
49
  export function currentVisionTurnBudget() {
46
- return turnBudgetScope.getStore()
50
+ return turnBudgetScope.getStore();
47
51
  }
48
-
49
52
  export function currentVisionTurnBudgetSignal() {
50
- return turnBudgetScope.getStore()?.signal
53
+ return usableSignal(turnBudgetScope.getStore()?.signal);
51
54
  }
52
-
53
55
  export function remainingVisionTurnBudgetMs(now = Date.now) {
54
- const budget = turnBudgetScope.getStore()
55
- if (!budget || !Number.isFinite(Number(budget.deadlineAt))) return undefined
56
- const clock = typeof now === 'function' ? now : Date.now
57
- return Math.max(0, Number(budget.deadlineAt) - Number(clock()))
56
+ const budget = turnBudgetScope.getStore();
57
+ const deadlineAt = finiteDeadline(budget?.deadlineAt);
58
+ if (deadlineAt === undefined)
59
+ return undefined;
60
+ const clock = typeof now === 'function'
61
+ ? now
62
+ : Date.now;
63
+ return Math.max(0, deadlineAt - Number(clock()));
58
64
  }
@@ -5,6 +5,15 @@ import { stripTrailingSlashes } from './string-normalization.js'
5
5
  const require = createRequire(import.meta.url)
6
6
  const packageJson = require('../package.json')
7
7
 
8
+ // Capture the process fetch once, at module evaluation, exactly like the
9
+ // Router-owned provider transport. Reading `globalThis.fetch` per call would
10
+ // re-admit DVR's own later-installed process-wide wrappers (legacy proxy,
11
+ // i18n/pi-ai bridge) into DVR's product-metadata requests, and would make the
12
+ // transport used for a request depend on when the request happens to run.
13
+ const moduleFetch = typeof globalThis.fetch === 'function'
14
+ ? globalThis.fetch.bind(globalThis)
15
+ : undefined
16
+
8
17
  export const PACKAGE_NAME = 'dsh-vision-router'
9
18
  export const CURRENT_VERSION = String(packageJson.version ?? '')
10
19
  export const DEFAULT_NPM_REGISTRY = 'https://registry.npmjs.org'
@@ -149,7 +158,7 @@ async function fetchLatestReleaseVersion({ fetchImpl, releaseApi, signal, timeou
149
158
  * read-only metadata request against npmjs instead of showing a false failure.
150
159
  */
151
160
  export async function checkPackageUpdate({
152
- fetchImpl = globalThis.fetch,
161
+ fetchImpl = moduleFetch,
153
162
  currentVersion = CURRENT_VERSION,
154
163
  registry = registryBaseFromEnv(),
155
164
  fallbackRegistry = DEFAULT_NPM_REGISTRY,
@@ -247,7 +256,7 @@ export async function checkPackageUpdate({
247
256
  * settings card reuses that result, and the manual button can force a refresh.
248
257
  */
249
258
  export function createCachedUpdateChecker({
250
- fetchImpl = (...args) => globalThis.fetch(...args),
259
+ fetchImpl = moduleFetch,
251
260
  currentVersion = CURRENT_VERSION,
252
261
  registry = registryBaseFromEnv(),
253
262
  fallbackRegistry = DEFAULT_NPM_REGISTRY,