dsh-vision-router 2.2.7 → 2.3.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 (69) hide show
  1. package/README.md +8 -8
  2. package/README.zh.md +8 -8
  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/compat-inventory.md +26 -34
  8. package/docs/architecture/dsh-020-rc2-adaptation.md +23 -0
  9. package/docs/architecture/dsh-compatibility-matrix.md +65 -54
  10. package/docs/architecture/dsh-support-window.md +29 -21
  11. package/docs/architecture/final-closure-audit.md +2 -0
  12. package/docs/architecture/host-first-proxy-convergence.md +1 -1
  13. package/docs/releases/v2.2.8.md +25 -0
  14. package/docs/releases/v2.3.0.md +53 -0
  15. package/entry.js +8 -56
  16. package/index.js +161 -691
  17. package/lib/catalog-corrections.js +1 -2
  18. package/lib/client-host-compat-prelude.js +7 -12
  19. package/lib/client-maintenance-actions.js +145 -0
  20. package/lib/client-presentation-boundary-main.js +82 -32
  21. package/lib/client-presentation-boundary.js +60 -40
  22. package/lib/client-script-carrier.js +90 -0
  23. package/lib/client.js +248 -146
  24. package/lib/core-apply-lifecycle.js +47 -0
  25. package/lib/core-primitives.js +88 -135
  26. package/lib/desktop-screenshot-tool.js +262 -0
  27. package/lib/dsh-contract-compat.js +11 -26
  28. package/lib/dsh-support-window.js +4 -4
  29. package/lib/guide-vision-toggle-highlight.js +7 -10
  30. package/lib/http-compat.js +1 -2
  31. package/lib/live-model-client-prelude.js +7 -14
  32. package/lib/local-remote-settings-permission.js +41 -16
  33. package/lib/maintenance-routes.js +136 -0
  34. package/lib/public-config.js +57 -0
  35. package/lib/public-entry.js +2 -7
  36. package/lib/remote-settings-bridge.js +11 -0
  37. package/lib/remote-settings-risk-client-boundary.js +155 -0
  38. package/lib/remote-settings-risk-confirmation.js +17 -105
  39. package/lib/runtime-composition.js +105 -30
  40. package/lib/runtime-i18n-boundary.js +10 -11
  41. package/lib/session-message-source-compat.js +7 -3
  42. package/lib/session-vision-index.js +154 -9
  43. package/lib/session-vision-mode-boundary.js +43 -8
  44. package/lib/settings-client-loader-lifecycle.js +129 -0
  45. package/lib/settings-factory-lifecycle.js +25 -14
  46. package/lib/settings-ia-client-prelude.js +11 -11
  47. package/lib/settings-limit-client-prelude.js +7 -9
  48. package/lib/sharp-runtime.js +0 -7
  49. package/lib/strict-live-model-client-prelude.js +7 -10
  50. package/lib/structured-flow-hardening.js +3 -2
  51. package/lib/v2-settings-ia-integration.js +21 -6
  52. package/lib/vision-backend-runtime-policy.js +3 -2
  53. package/lib/vision-background-benchmark.js +3 -5
  54. package/lib/vision-capability-benchmark-client.js +7 -10
  55. package/lib/vision-capability-benchmark-service.js +1 -0
  56. package/lib/vision-diagnostics-routes.js +216 -0
  57. package/lib/vision-evidence-guidance.js +3 -1
  58. package/lib/vision-exact-check-client.js +11 -6
  59. package/lib/vision-model-visibility-boundary-main.js +7 -11
  60. package/lib/vision-model-visibility-boundary.js +7 -12
  61. package/lib/vision-provider-transport.js +0 -29
  62. package/lib/vision-routing-settings-prelude.js +7 -11
  63. package/lib/vision-toggle-root-hardening.js +50 -8
  64. package/lib/vision-tool-runtime-boundary.js +5 -1
  65. package/lib/vision-turn-budget-client-prelude.js +7 -10
  66. package/lib/web/remote-settings-client.js +9 -14
  67. package/package.json +23 -13
  68. package/lib/android-attachment-compat.js +0 -244
  69. package/lib/settings-client-rc8-lifecycle.js +0 -402
@@ -1,113 +1,25 @@
1
1
  import { htmlHasScriptMarker } from './html-script-marker.js'
2
+ import { REMOTE_SETTINGS_RISK_CLIENT_BOUNDARY_SOURCE } from './remote-settings-risk-client-boundary.js'
2
3
 
3
4
  const RISK_PRELUDE_MARK = 'data-vision-router-remote-settings-risk-confirmation'
4
5
 
5
6
  export const REMOTE_SETTINGS_RISK_CLIENT_PRELUDE = String.raw`(function(){
6
7
  'use strict';
7
- var CHANNEL = '/vision-router-settings';
8
- var AUTHORIZE_ENDPOINT = 'authorize';
9
- var connectionCache = typeof WeakMap === 'function' ? new WeakMap() : undefined;
10
- var rpcCache = typeof WeakMap === 'function' ? new WeakMap() : undefined;
11
- var contextCache = typeof WeakMap === 'function' ? new WeakMap() : undefined;
12
- var authorizationPromise;
13
-
14
- function chineseLocale() {
15
- var lang = '';
16
- try {
17
- lang = String((document && document.documentElement && document.documentElement.lang) || (navigator && navigator.language) || '').toLowerCase();
18
- } catch (_) {}
19
- return lang === '' || lang.indexOf('zh') === 0;
20
- }
21
-
22
- function riskMessage() {
23
- return chineseLocale()
24
- ? '启用远程设置?\n\n当前页面通过远程地址访问 DSH。启用后,能够访问此 trusted host 的客户端可以修改 Vision Router 已开放的设置。\n\nDSH trustedHosts 不是身份认证,请仅在你信任当前网络和访问者时继续。API Key、HTTP Provider 凭据、本地 Ollama / LM Studio、产物路径等未列入远程白名单的敏感配置仍不会开放。\n\n确定要允许远程修改设置吗?'
25
- : 'Enable remote settings?\n\nThis page is accessing DSH through a remote address. After enabling this, clients that can reach this trusted host can change the Vision Router settings exposed by the remote allow-list.\n\nDSH trustedHosts is not authentication. Continue only if you trust the current network and its users. Sensitive settings outside the remote allow-list, including API keys, HTTP-provider credentials, local Ollama / LM Studio, and artifact paths, remain unavailable remotely.\n\nAllow remote settings?';
26
- }
27
-
28
- function isPermissionDisabledDescribe(channel, endpoint, result) {
29
- return channel === CHANNEL
30
- && endpoint === 'describe'
31
- && result && result.ok === true
32
- && result.value && result.value.enabled !== true
33
- && result.value.reason === 'permission-disabled';
34
- }
35
-
36
- async function authorizeAndRefresh(target, call) {
37
- if (authorizationPromise) return authorizationPromise;
38
- authorizationPromise = (async function(){
39
- if (typeof window.confirm !== 'function' || window.confirm(riskMessage()) !== true) return undefined;
40
- var authorized = await call.call(target, CHANNEL, AUTHORIZE_ENDPOINT, { acceptedRisk: true });
41
- if (!authorized || authorized.ok !== true) {
42
- var message = authorized && authorized.error && authorized.error.message
43
- ? authorized.error.message
44
- : 'Vision Router could not enable remote settings.';
45
- try { if (typeof window.alert === 'function') window.alert(message); } catch (_) {}
46
- return undefined;
47
- }
48
- return call.call(target, CHANNEL, 'describe', {});
49
- })();
50
- try { return await authorizationPromise; }
51
- finally { authorizationPromise = undefined; }
52
- }
53
-
54
- function wrapRpc(rpc) {
55
- if (!rpc || (typeof rpc !== 'object' && typeof rpc !== 'function')) return rpc;
56
- if (rpcCache && rpcCache.has(rpc)) return rpcCache.get(rpc);
57
- var wrapped = new Proxy(rpc, {
58
- get: function(target, property) {
59
- if (property === 'call') {
60
- var call = Reflect.get(target, property, target);
61
- if (typeof call !== 'function') return call;
62
- return async function(channel, endpoint) {
63
- var result = await call.apply(target, arguments);
64
- if (!isPermissionDisabledDescribe(channel, endpoint, result)) return result;
65
- var refreshed = await authorizeAndRefresh(target, call);
66
- return refreshed || result;
67
- };
68
- }
69
- var value = Reflect.get(target, property, target);
70
- return typeof value === 'function' ? value.bind(target) : value;
71
- }
72
- });
73
- if (rpcCache) rpcCache.set(rpc, wrapped);
74
- return wrapped;
75
- }
76
-
77
- function wrapConnection(connection) {
78
- if (!connection || (typeof connection !== 'object' && typeof connection !== 'function')) return connection;
79
- if (connectionCache && connectionCache.has(connection)) return connectionCache.get(connection);
80
- var wrapped = new Proxy(connection, {
81
- get: function(target, property) {
82
- if (property === 'rpc') return wrapRpc(Reflect.get(target, property, target));
83
- var value = Reflect.get(target, property, target);
84
- return typeof value === 'function' ? value.bind(target) : value;
85
- }
86
- });
87
- if (connectionCache) connectionCache.set(connection, wrapped);
88
- return wrapped;
89
- }
90
-
91
- function wrapContext(ctx) {
92
- if (!ctx || typeof ctx !== 'object') return ctx;
93
- if (contextCache && contextCache.has(ctx)) return contextCache.get(ctx);
94
- var wrapped = new Proxy(ctx, {
95
- get: function(target, property) {
96
- if (property === 'get') {
97
- var get = Reflect.get(target, property, target);
98
- if (typeof get !== 'function') return get;
99
- return function(name) {
100
- var value = get.apply(target, arguments);
101
- return name === 'connection' ? wrapConnection(value) : value;
102
- };
103
- }
104
- var value = Reflect.get(target, property, target);
105
- return typeof value === 'function' ? value.bind(target) : value;
106
- }
107
- });
108
- if (contextCache) contextCache.set(ctx, wrapped);
109
- return wrapped;
110
- }
8
+ ${REMOTE_SETTINGS_RISK_CLIENT_BOUNDARY_SOURCE}
9
+ var riskBoundary = createRemoteSettingsRiskClientBoundary({
10
+ confirmImpl: function(message) {
11
+ return typeof window.confirm === 'function' && window.confirm(message) === true;
12
+ },
13
+ alertImpl: function(message) {
14
+ try { if (typeof window.alert === 'function') window.alert(message); } catch (_) {}
15
+ },
16
+ locale: function() {
17
+ try {
18
+ return String((document && document.documentElement && document.documentElement.lang)
19
+ || (navigator && navigator.language) || '');
20
+ } catch (_) { return ''; }
21
+ }
22
+ });
111
23
 
112
24
  function patchLoader(loader) {
113
25
  if (!loader || typeof loader.load !== 'function' || loader.load.__visionRouterRemoteRiskConfirmation) return;
@@ -122,7 +34,7 @@ export const REMOTE_SETTINGS_RISK_CLIENT_PRELUDE = String.raw`(function(){
122
34
  var apply = exports.apply;
123
35
  var wrappedApply = function(ctx) {
124
36
  var rest = Array.prototype.slice.call(arguments, 1);
125
- return apply.apply(exports, [wrapContext(ctx)].concat(rest));
37
+ return apply.apply(exports, [riskBoundary.wrapContext(ctx)].concat(rest));
126
38
  };
127
39
  Object.defineProperty(wrappedApply, '__visionRouterRemoteRiskConfirmation', { value: true });
128
40
  exports.apply = wrappedApply;
@@ -14,6 +14,7 @@ import { contextWithNativeImageCoexistence } from './native-image-coexistence.js
14
14
  import { createCoreVisionSurfaceRuntime } from './core-vision-surface.js'
15
15
  import { installSessionVisionIndexBoundary } from './session-vision-index.js'
16
16
  import { createSessionVisionRuntime } from './session-vision-runtime.js'
17
+ import { runCoreApplyLifecycle } from './core-apply-lifecycle.js'
17
18
  import { installLegacyCoreVisionPolicyBridge } from './legacy-core-vision-policy-bridge.js'
18
19
  import { installSessionVisionModeBoundary } from './session-vision-mode-boundary.js'
19
20
  import { installPiAiBridgeWireCompat } from './pi-ai-bridge-wire-compat.js'
@@ -23,7 +24,6 @@ import { installAdversarialHardening } from './adversarial-hardening.js'
23
24
  import { installOllamaColdStartGuard } from './ollama-cold-start.js'
24
25
  import { installLocalVisionStabilizer } from './local-vision-stabilizer.js'
25
26
  import { installWrapperDirectoryAlias } from './wrapper-directory.js'
26
- import { installAndroidAttachmentCompat } from './android-attachment-compat.js'
27
27
  import { contextWithCoalescedAdapterUpdates } from './adapter-update-coalescer.js'
28
28
  import { installTesseractExecFileCompat } from './tesseract-exec-compat.js'
29
29
  import { installLocalMutationRouteBoundary } from './web-capability-boundary.js'
@@ -57,7 +57,7 @@ import {
57
57
  } from './structured-flow-hardening.js'
58
58
  import { installVisionLimitDiagnostics } from './vision-limit-diagnostics.js'
59
59
  import {
60
- attachmentContextForContract,
60
+ installDshHostCapabilityDiagnostics,
61
61
  createSessionEventReader,
62
62
  createSessionEventTailReader,
63
63
  createSessionLogReader,
@@ -73,6 +73,7 @@ import {
73
73
  } from './web/index.js'
74
74
  import { createRuntimeI18nCoreFacade } from './runtime-i18n-core.js'
75
75
  import { createRuntimeI18nCoreScope } from './runtime-i18n-core-scope.js'
76
+ import { installVisionMaintenanceRoutes } from './maintenance-routes.js'
76
77
 
77
78
  function liveVisionSettings(ctx, fallback) {
78
79
  try {
@@ -95,7 +96,7 @@ function legacyCoreVisionSettings(ctx, fallback) {
95
96
  return { ...current, autoActivateOnImage: false }
96
97
  }
97
98
 
98
- export function applyVisionRuntimeComposition(ctx, config = {}, core) {
99
+ function installHostAndSecurityBoundaries(ctx, config, core) {
99
100
  // SessionVisionIndex calls planGuardStopShadows before Core.apply. Keep that
100
101
  // one helper behind the live-locale facade; the final Core itself still gets
101
102
  // the ordinary composition identity and is localized by the scoped context.
@@ -174,10 +175,29 @@ export function applyVisionRuntimeComposition(ctx, config = {}, core) {
174
175
  Config: core.Config,
175
176
  })
176
177
  : ownershipCtx
177
- const attachmentCompatCtx = attachmentContextForContract(settingsCtx, logging.logger, {
178
- installAndroidAttachmentCompat,
179
- })
178
+ // DVR 2.3 supports DSH from the 0.1.5 train, whose attachment
179
+ // contract already owns batch persistence. The old no-batch Android wrapper
180
+ // is therefore unreachable; keep capability diagnostics as an explicit
181
+ // composition responsibility instead of hiding that side effect in a shim.
182
+ installDshHostCapabilityDiagnostics(settingsCtx)
183
+ const attachmentCompatCtx = settingsCtx
184
+
185
+ return {
186
+ attachmentCompatCtx,
187
+ batchAttachmentHost,
188
+ capabilityStore,
189
+ hardenedConfig,
190
+ hostOwnsOfficialDeepSeek,
191
+ logging,
192
+ runtimeConfig,
193
+ runtimeI18nCore,
194
+ runtimeI18nCoreScope,
195
+ runtimePerformanceStore,
196
+ }
197
+ }
180
198
 
199
+ function createRuntimeOwners(host, core) {
200
+ const { attachmentCompatCtx, logging, runtimeConfig, runtimeI18nCore } = host
181
201
  const toolRuntimeCtx = installVisionToolRuntimeBoundary(attachmentCompatCtx, runtimeConfig)
182
202
  const nativeImageCompat = contextWithNativeImageCoexistence(toolRuntimeCtx, runtimeConfig)
183
203
  const coreVisionSurfaceRuntime = createCoreVisionSurfaceRuntime({
@@ -213,6 +233,17 @@ export function applyVisionRuntimeComposition(ctx, config = {}, core) {
213
233
  legacyCoreCompat.config,
214
234
  )
215
235
 
236
+ return {
237
+ coreVisionSurfaceRuntime,
238
+ nativeImageCompat,
239
+ sessionVisionModeCompat,
240
+ sessionVisionRuntime,
241
+ }
242
+ }
243
+
244
+ function installRoutingAndHostProducts(host, owners, core) {
245
+ const { capabilityStore, logging, runtimeConfig, runtimePerformanceStore } = host
246
+ const { sessionVisionModeCompat } = owners
216
247
  // Final structured-flow guard sits closest to core.apply so it sees the
217
248
  // actual tool registrations and pre-step listener. The diagnostic observer
218
249
  // remains immediately inside it so existing timeout/budget semantics stay
@@ -266,6 +297,19 @@ export function applyVisionRuntimeComposition(ctx, config = {}, core) {
266
297
  })
267
298
  installPiAiBridgeWireCompat(reconciledCtx, logging.logger)
268
299
 
300
+ return {
301
+ breakerShadowHealth,
302
+ liveDiscovery,
303
+ reconciledCtx,
304
+ sessionEventTailReader,
305
+ sessionTurnResolver,
306
+ }
307
+ }
308
+
309
+ function installExecutionBoundaries(host, owners, routing, core, providerTransport) {
310
+ const { capabilityStore, logging, runtimeConfig, runtimeI18nCoreScope, runtimePerformanceStore } = host
311
+ const { sessionVisionRuntime } = owners
312
+ const { breakerShadowHealth, liveDiscovery, reconciledCtx } = routing
269
313
  const executionCtx = contextWithVisionExecutionPolicy(reconciledCtx, {
270
314
  isBridgeEvidence: (provider, model) => liveDiscovery.hasModel(provider, model),
271
315
  evidenceSource: (provider, model) => liveDiscovery.evidenceSource?.(provider, model),
@@ -314,11 +358,13 @@ export function applyVisionRuntimeComposition(ctx, config = {}, core) {
314
358
  core,
315
359
  evidenceSource: (provider, model) => liveDiscovery.evidenceSource?.(provider, model),
316
360
  logger: logging.logger,
361
+ providerTransport,
317
362
  finalContextDecorator: runtimeI18nCoreScope.decorate,
318
363
  })
319
364
  const capabilityBenchmark = installCapabilityBenchmarkService(backendRuntimeCtx, runtimeConfig, core, {
320
365
  logger: logging.logger,
321
366
  store: capabilityStore,
367
+ providerTransport,
322
368
  })
323
369
  attachCapabilityBenchmarkPresentation(capabilityBenchmark, {
324
370
  ctx: backendRuntimeCtx,
@@ -338,6 +384,22 @@ export function applyVisionRuntimeComposition(ctx, config = {}, core) {
338
384
  })
339
385
  const coreRequestAuthorityCtx = contextWithAgentRequestRouteAuthority(backendRuntimeCtx)
340
386
 
387
+ return { coreRequestAuthorityCtx }
388
+ }
389
+
390
+ function applyMatureCoreBridge(host, owners, routing, execution, core, runtime) {
391
+ const {
392
+ attachmentCompatCtx,
393
+ batchAttachmentHost,
394
+ hardenedConfig,
395
+ hostOwnsOfficialDeepSeek,
396
+ logging,
397
+ runtimeConfig,
398
+ runtimeI18nCoreScope,
399
+ } = host
400
+ const { coreVisionSurfaceRuntime, sessionVisionModeCompat, sessionVisionRuntime } = owners
401
+ const { breakerShadowHealth, sessionEventTailReader, sessionTurnResolver } = routing
402
+ const { coreRequestAuthorityCtx } = execution
341
403
  try {
342
404
  const c = hardenedConfig && typeof hardenedConfig === 'object' ? hardenedConfig : {}
343
405
  const local = c.localOllama && typeof c.localOllama === 'object' ? c.localOllama : {}
@@ -354,8 +416,27 @@ export function applyVisionRuntimeComposition(ctx, config = {}, core) {
354
416
  /* diagnostics must never break apply */
355
417
  }
356
418
 
357
- try {
358
- const result = runtimeI18nCoreScope.run(() =>
419
+ let schemaBootstrapFinished = false
420
+ const finishSchemaBootstrapOnce = () => {
421
+ if (schemaBootstrapFinished) return
422
+ schemaBootstrapFinished = true
423
+ coreVisionSurfaceRuntime.finishSchemaBootstrap()
424
+ }
425
+ const installPostCoreRuntime = () => {
426
+ installVisionMaintenanceRoutes(coreRequestAuthorityCtx, { logger: logging.logger })
427
+ finishSchemaBootstrapOnce()
428
+ installWrapperDirectoryAlias(attachmentCompatCtx, runtimeConfig, logging.logger)
429
+ }
430
+ const failCoreApply = (error) => {
431
+ finishSchemaBootstrapOnce()
432
+ logging.logger.error(
433
+ 'vision-router: plugin apply failed: %s',
434
+ error && error.stack ? error.stack : error && error.message ? error.message : String(error),
435
+ )
436
+ }
437
+
438
+ return runCoreApplyLifecycle(
439
+ () => runtimeI18nCoreScope.run(() =>
359
440
  withVisionCircuitBreakerObserver(
360
441
  breakerShadowHealth.capture,
361
442
  () => core.apply(
@@ -367,28 +448,22 @@ export function applyVisionRuntimeComposition(ctx, config = {}, core) {
367
448
  sessionTurnResolver,
368
449
  sessionEventTailReader,
369
450
  hostOwnsOfficialDeepSeek,
451
+ providerTransport: runtime?.providerTransport,
370
452
  },
371
453
  ),
372
454
  ),
373
- )
374
- coreVisionSurfaceRuntime.finishSchemaBootstrap()
375
- installWrapperDirectoryAlias(attachmentCompatCtx, runtimeConfig, logging.logger)
376
- if (result && typeof result.then === 'function') {
377
- return result.catch((error) => {
378
- logging.logger.error(
379
- 'vision-router: plugin apply failed: %s',
380
- error && error.stack ? error.stack : error && error.message ? error.message : String(error),
381
- )
382
- throw error
383
- })
384
- }
385
- return result
386
- } catch (error) {
387
- coreVisionSurfaceRuntime.finishSchemaBootstrap()
388
- logging.logger.error(
389
- 'vision-router: plugin apply failed: %s',
390
- error && error.stack ? error.stack : error && error.message ? error.message : String(error),
391
- )
392
- throw error
393
- }
394
- }
455
+ ),
456
+ {
457
+ onSuccess: installPostCoreRuntime,
458
+ onFailure: failCoreApply,
459
+ },
460
+ )
461
+ }
462
+
463
+ export function applyVisionRuntimeComposition(ctx, config = {}, core, runtime = {}) {
464
+ const host = installHostAndSecurityBoundaries(ctx, config, core)
465
+ const owners = createRuntimeOwners(host, core)
466
+ const routing = installRoutingAndHostProducts(host, owners, core)
467
+ const execution = installExecutionBoundaries(host, owners, routing, core, runtime?.providerTransport)
468
+ return applyMatureCoreBridge(host, owners, routing, execution, core, runtime)
469
+ }
@@ -1,6 +1,8 @@
1
1
  import { createRuntimeI18n, DEEP_TOOL_MOUNT_STATE } from './runtime-i18n.js'
2
2
  import { depthCopyFor } from './depth-guidance.js'
3
3
  import { blocksHaveRetainedImage } from './image-offload-compat.js'
4
+ import { captureFetchDelegate, installFetchWrapper } from './fetch-wrapper-lifecycle.js'
5
+ import { visionRouterMessageSource } from './session-message-source-compat.js'
4
6
 
5
7
  const PLUGIN_NAME = 'dsh-vision-router'
6
8
  const DEEP_TOOL_NAMES = new Set([
@@ -432,7 +434,7 @@ function addAutoMountReminder(decision, payload, i18n) {
432
434
  : `${Date.now()}-${Math.floor(Math.random() * 1e9)}`
433
435
  }`,
434
436
  content: [{ type: 'text', text: i18n.t('autoMountReminder') }],
435
- source: { kind: 'plugin', plugin: PLUGIN_NAME },
437
+ source: visionRouterMessageSource(payload?.agent?.session),
436
438
  }
437
439
  const structuredIndex = base.findIndex((message) =>
438
440
  typeof message?.id === 'string' && message.id.startsWith('vision-router-structured-'),
@@ -520,9 +522,9 @@ function localizeRequestBody(value, i18n) {
520
522
  function installPromptFetchBoundary(ctx, i18n) {
521
523
  if (typeof ctx?.effect !== 'function' || typeof globalThis.fetch !== 'function') return
522
524
  ctx.effect(() => {
523
- const originalFetch = globalThis.fetch
525
+ const delegateFetch = captureFetchDelegate()
524
526
  const localizedFetch = (input, init) => {
525
- if (i18n.language() !== 'en' || typeof init?.body !== 'string') return originalFetch(input, init)
527
+ if (i18n.language() !== 'en' || typeof init?.body !== 'string') return delegateFetch(input, init)
526
528
  const body = init.body
527
529
  if (
528
530
  !body.includes('请按以下结构识别这张图片') &&
@@ -530,21 +532,18 @@ function installPromptFetchBoundary(ctx, i18n) {
530
532
  !body.includes(OCR_FALLBACK_PROMPT_ZH) &&
531
533
  !body.includes(LONG_SCREENSHOT_OCR_PROMPT_ZH)
532
534
  ) {
533
- return originalFetch(input, init)
535
+ return delegateFetch(input, init)
534
536
  }
535
537
  try {
536
538
  const parsed = JSON.parse(body)
537
539
  const localized = localizeRequestBody(parsed, i18n)
538
- if (localized === parsed) return originalFetch(input, init)
539
- return originalFetch(input, { ...init, body: JSON.stringify(localized) })
540
+ if (localized === parsed) return delegateFetch(input, init)
541
+ return delegateFetch(input, { ...init, body: JSON.stringify(localized) })
540
542
  } catch {
541
- return originalFetch(input, init)
543
+ return delegateFetch(input, init)
542
544
  }
543
545
  }
544
- globalThis.fetch = localizedFetch
545
- return () => {
546
- if (globalThis.fetch === localizedFetch) globalThis.fetch = originalFetch
547
- }
546
+ return installFetchWrapper(localizedFetch)
548
547
  }, 'vision-router: runtime i18n prompt boundary')
549
548
  }
550
549
 
@@ -62,9 +62,13 @@ function normalizeHookResult(event, args, result) {
62
62
 
63
63
  /**
64
64
  * DSH Session V3 requires the historical {kind:'plugin', plugin:...} source,
65
- * while V4 rejects that wrapper in favor of producer-owned source kinds. Keep
66
- * every existing DVR message producer unchanged and normalize at the final
67
- * hook publication boundary using the Session's own durable header version.
65
+ * while V4 rejects that wrapper in favor of producer-owned source kinds.
66
+ *
67
+ * Session-facing DVR producers are responsible for calling
68
+ * visionRouterMessageSource(session) when they create a message. This final
69
+ * publication boundary is deliberately defense-in-depth: it protects legacy
70
+ * or future producers that accidentally reach a hook with the wrong DVR source,
71
+ * but producer correctness must not depend on this proxy being the sole owner.
68
72
  */
69
73
  export function installVisionRouterMessageSourceBoundary(ctx) {
70
74
  if (!ctx || typeof ctx !== 'object') return ctx
@@ -3,6 +3,9 @@ import { sessionSurfaceReplacementIntent } from './session-surface-compat.js'
3
3
  import { createSessionVisionStateStore } from './session-vision-state.js'
4
4
 
5
5
  const MAX_PENDING_REPAIR_EVENTS = 256
6
+ const MAX_REPAIR_OVERFLOW_RANGES = 32
7
+ const MAX_REPAIR_OVERFLOW_INSPECTIONS_PER_PASS = 512
8
+ const MAX_REPAIR_OVERFLOW_READS_PER_PASS = 32
6
9
 
7
10
  function isObject(value) {
8
11
  return value !== null && typeof value === 'object'
@@ -87,6 +90,8 @@ export function createSessionVisionIndex({
87
90
  const guardSurfaceScans = new WeakMap()
88
91
  const pendingToolRepairEvents = new WeakMap()
89
92
  const pendingGuardRepairEvents = new WeakMap()
93
+ const toolRepairOverflowRanges = new WeakMap()
94
+ const guardRepairOverflowRanges = new WeakMap()
90
95
  const unsupportedSurfaceContracts = new WeakSet()
91
96
  const repairReadWarnings = new WeakMap()
92
97
  const repairFeedOverflowWarnings = new WeakSet()
@@ -295,7 +300,132 @@ export function createSessionVisionIndex({
295
300
  return undefined
296
301
  }
297
302
 
298
- const cachePendingRepairEvent = (pendingBySession, session, event) => {
303
+ // Feed overflow stores only dropped sequence ranges, never frozen event
304
+ // objects. Adjacent ranges coalesce; pathological sparse overflow is
305
+ // conservatively collapsed once the range count reaches a fixed bound. The
306
+ // recovery loop then advances that range in bounded numeric/read batches and
307
+ // consults only Host-owned readSessionEvent(), so memory remains bounded
308
+ // without returning to whole-Session history materialization.
309
+ const addRepairOverflowSeq = (overflowBySession, session, seq) => {
310
+ if (!Number.isSafeInteger(seq) || seq < 0 || Object.is(seq, -0)) return
311
+ let ranges = overflowBySession.get(session)
312
+ if (!ranges) {
313
+ ranges = []
314
+ overflowBySession.set(session, ranges)
315
+ }
316
+
317
+ let start = seq
318
+ let end = seq
319
+ let insertAt = 0
320
+ while (insertAt < ranges.length && ranges[insertAt].end + 1 < start) insertAt += 1
321
+ while (insertAt < ranges.length && ranges[insertAt].start - 1 <= end) {
322
+ start = Math.min(start, ranges[insertAt].start)
323
+ end = Math.max(end, ranges[insertAt].end)
324
+ ranges.splice(insertAt, 1)
325
+ }
326
+ ranges.splice(insertAt, 0, { start, end })
327
+
328
+ if (ranges.length > MAX_REPAIR_OVERFLOW_RANGES) {
329
+ overflowBySession.set(session, [{
330
+ start: ranges[0].start,
331
+ end: ranges[ranges.length - 1].end,
332
+ }])
333
+ }
334
+ }
335
+
336
+ const advanceRepairOverflowRange = (ranges) => {
337
+ const range = ranges?.[0]
338
+ if (!range) return
339
+ if (range.start >= range.end) ranges.shift()
340
+ else range.start += 1
341
+ }
342
+
343
+ const recoverRepairOverflowEvents = async (overflowBySession, pendingBySession, session) => {
344
+ const ranges = overflowBySession.get(session)
345
+ if (!ranges || ranges.length === 0) return []
346
+
347
+ const nodes = surfaceNodes(session)
348
+ if (!nodes || nodes.length === 0) {
349
+ overflowBySession.delete(session)
350
+ repairFeedOverflowWarnings.delete(session)
351
+ return []
352
+ }
353
+ const current = new Set(nodes)
354
+ const pending = pendingBySession.get(session)
355
+ const recovered = []
356
+ let inspections = 0
357
+ let reads = 0
358
+
359
+ while (
360
+ ranges.length > 0
361
+ && inspections < MAX_REPAIR_OVERFLOW_INSPECTIONS_PER_PASS
362
+ && reads < MAX_REPAIR_OVERFLOW_READS_PER_PASS
363
+ ) {
364
+ const seq = ranges[0].start
365
+ inspections += 1
366
+
367
+ // A dropped repair that is no longer on the live surface is settled. A
368
+ // seq still present in the fast-path map will be consumed normally.
369
+ if (!current.has(seq) || pending?.has(seq)) {
370
+ advanceRepairOverflowRange(ranges)
371
+ continue
372
+ }
373
+
374
+ if (typeof readSessionEvent !== 'function') {
375
+ warnRepairReadFailure(
376
+ session,
377
+ seq,
378
+ new Error('Session event reader is unavailable during feed overflow recovery'),
379
+ )
380
+ break
381
+ }
382
+
383
+ let result
384
+ try {
385
+ result = await readSessionEvent(session, seq)
386
+ } catch (error) {
387
+ warnRepairReadFailure(session, seq, error)
388
+ break
389
+ }
390
+ if (result?.supported !== true) {
391
+ warnRepairReadFailure(
392
+ session,
393
+ seq,
394
+ result?.supported === false
395
+ ? new Error('Session event reader is unsupported during feed overflow recovery')
396
+ : new Error('Session event reader returned an invalid capability result during feed overflow recovery'),
397
+ )
398
+ break
399
+ }
400
+ const event = result.event
401
+ if (!event || typeof event !== 'object' || (event.seq !== undefined && event.seq !== seq)) {
402
+ warnRepairReadFailure(
403
+ session,
404
+ seq,
405
+ new Error(`Session event reader returned no matching event for overflow seq ${seq}`),
406
+ )
407
+ break
408
+ }
409
+
410
+ reads += 1
411
+ advanceRepairOverflowRange(ranges)
412
+ repairReadWarnings.delete(session)
413
+ // The live feed may deliver the same committed event while the exact read
414
+ // is in flight. In that case the fast-path map owns it; do not return a
415
+ // second copy from recovery.
416
+ if (!pendingBySession.get(session)?.has(seq) && !isReplacementEvent(event)) {
417
+ recovered.push({ seq, event })
418
+ }
419
+ }
420
+
421
+ if (ranges.length === 0) {
422
+ overflowBySession.delete(session)
423
+ repairFeedOverflowWarnings.delete(session)
424
+ }
425
+ return recovered
426
+ }
427
+
428
+ const cachePendingRepairEvent = (pendingBySession, overflowBySession, session, event) => {
299
429
  if (!session || !Number.isSafeInteger(event?.seq) || event.seq < 0 || Object.is(event.seq, -0)) return
300
430
  let pending = pendingBySession.get(session)
301
431
  if (!pending) {
@@ -304,11 +434,14 @@ export function createSessionVisionIndex({
304
434
  }
305
435
  if (!pending.has(event.seq) && pending.size >= MAX_PENDING_REPAIR_EVENTS) {
306
436
  const oldest = pending.keys().next().value
307
- if (oldest !== undefined) pending.delete(oldest)
437
+ if (oldest !== undefined) {
438
+ pending.delete(oldest)
439
+ addRepairOverflowSeq(overflowBySession, session, oldest)
440
+ }
308
441
  if (!repairFeedOverflowWarnings.has(session)) {
309
442
  repairFeedOverflowWarnings.add(session)
310
443
  logger?.warn?.(
311
- 'vision-router: session surface repair event feed exceeded %s pending events; dropping oldest entry',
444
+ 'vision-router: session surface repair event feed exceeded %s pending events; scheduling bounded exact-event recovery',
312
445
  MAX_PENDING_REPAIR_EVENTS,
313
446
  )
314
447
  }
@@ -340,10 +473,10 @@ export function createSessionVisionIndex({
340
473
  event,
341
474
  (events, seqs) => core.planToolResultImageShadows(events, seqs, () => true),
342
475
  )
343
- if (plans.length > 0) cachePendingRepairEvent(pendingToolRepairEvents, session, event)
476
+ if (plans.length > 0) cachePendingRepairEvent(pendingToolRepairEvents, toolRepairOverflowRanges, session, event)
344
477
  } else if (event.type === 'user/message' && typeof core.planGuardStopShadows === 'function') {
345
478
  const plans = planAgainstOneEvent(event, (events, seqs) => core.planGuardStopShadows(events, seqs))
346
- if (plans.length > 0) cachePendingRepairEvent(pendingGuardRepairEvents, session, event)
479
+ if (plans.length > 0) cachePendingRepairEvent(pendingGuardRepairEvents, guardRepairOverflowRanges, session, event)
347
480
  }
348
481
  repairFeedObserverWarnings.delete(session)
349
482
  } catch (error) {
@@ -439,11 +572,17 @@ export function createSessionVisionIndex({
439
572
 
440
573
  const repairToolResultSurfaceFromFeed = async (session) => {
441
574
  if (typeof session?.append !== 'function' || typeof core.planToolResultImageShadows !== 'function') return 0
575
+ const recovered = await recoverRepairOverflowEvents(
576
+ toolRepairOverflowRanges,
577
+ pendingToolRepairEvents,
578
+ session,
579
+ )
442
580
  const pending = consumePendingRepairEvents(pendingToolRepairEvents, session)
443
- if (pending.length === 0) return 0
581
+ const entries = [...recovered, ...pending]
582
+ if (entries.length === 0) return 0
444
583
  const preserve = keepToolResultImages()
445
584
  let repaired = 0
446
- for (const { seq, event } of pending) {
585
+ for (const { seq, event } of entries) {
447
586
  const events = { [seq]: event }
448
587
  const plans = core.planToolResultImageShadows(events, [seq], () => !preserve)
449
588
  for (const plan of plans) {
@@ -470,10 +609,16 @@ export function createSessionVisionIndex({
470
609
 
471
610
  const repairGuardStopSurfaceFromFeed = async (session) => {
472
611
  if (typeof session?.append !== 'function' || typeof core.planGuardStopShadows !== 'function') return 0
612
+ const recovered = await recoverRepairOverflowEvents(
613
+ guardRepairOverflowRanges,
614
+ pendingGuardRepairEvents,
615
+ session,
616
+ )
473
617
  const pending = consumePendingRepairEvents(pendingGuardRepairEvents, session)
474
- if (pending.length === 0) return 0
618
+ const entries = [...recovered, ...pending]
619
+ if (entries.length === 0) return 0
475
620
  let repaired = 0
476
- for (const { seq, event } of pending) {
621
+ for (const { seq, event } of entries) {
477
622
  const events = { [seq]: event }
478
623
  const plans = core.planGuardStopShadows(events, [seq])
479
624
  for (const plan of plans) {