dsh-vision-router 2.3.0 → 3.0.1

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 (105) hide show
  1. package/README.md +18 -3
  2. package/README.zh.md +16 -3
  3. package/docs/architecture/3.0-architecture-plan.md +252 -0
  4. package/docs/architecture/3.0-r0-contract-ledger.md +137 -0
  5. package/docs/architecture/3.0-r1-source-artifact-boundary.md +98 -0
  6. package/docs/architecture/3.0-r10-final-correctness-closure.md +207 -0
  7. package/docs/architecture/3.0-r11-adversarial-review.md +447 -0
  8. package/docs/architecture/3.0-r5-browser-go-no-go.md +165 -0
  9. package/docs/architecture/3.0-r5-browser-typescript-gate.md +105 -0
  10. package/docs/architecture/3.0-r6-compat-retirement.md +203 -0
  11. package/docs/architecture/3.0-r7-public-api.md +151 -0
  12. package/docs/architecture/3.0-r8-ci-release.md +248 -0
  13. package/docs/architecture/3.0-r9-final-audit.md +370 -0
  14. package/docs/architecture/dsh-support-window.md +13 -3
  15. package/docs/releases/v3.0.0.md +85 -0
  16. package/docs/releases/v3.0.1.md +77 -0
  17. package/entry.js +1 -1
  18. package/index.js +110 -71
  19. package/lib/agent-request-route-authority.js +1 -20
  20. package/lib/core-apply-lifecycle.js +37 -43
  21. package/lib/core-cache.js +87 -0
  22. package/lib/core-primitives.js +11 -144
  23. package/lib/core-vision-surface-domain.js +63 -0
  24. package/lib/core-vision-surface.js +11 -77
  25. package/lib/delegated-call-config.js +15 -24
  26. package/lib/doctor-cli.js +0 -0
  27. package/lib/doctor-runtime.js +4 -1
  28. package/lib/dsh-contract-compat.js +26 -230
  29. package/lib/dsh-host-capabilities.js +3 -9
  30. package/lib/dsh-settings-017-compat.js +17 -10
  31. package/lib/file-logger.js +8 -3
  32. package/lib/grounding-coordinate-runtime.js +1 -2
  33. package/lib/http-body-limit.js +97 -79
  34. package/lib/image-offload-compat.js +30 -21
  35. package/lib/image-resource-governor.js +210 -228
  36. package/lib/legacy-global-proxy-boundary.js +46 -3
  37. package/lib/live-model-client-prelude.js +12 -4
  38. package/lib/live-model-discovery.js +134 -43
  39. package/lib/local-vision-stabilizer.js +33 -18
  40. package/lib/maintenance-routes.js +1 -3
  41. package/lib/native-image-coexistence.js +15 -3
  42. package/lib/ollama-cold-start.js +87 -23
  43. package/lib/pi-ai-bridge-wire-compat.js +3 -2
  44. package/lib/proxy-dispatcher-pool.js +151 -92
  45. package/lib/proxy-routing.js +75 -51
  46. package/lib/proxy-url-compat.js +3 -10
  47. package/lib/public-config.js +23 -44
  48. package/lib/public-entry.d.ts +148 -0
  49. package/lib/public-entry.js +17 -4
  50. package/lib/replay-delegation.js +12 -7
  51. package/lib/replay-envelope-v2-compat.js +12 -7
  52. package/lib/runtime-composition.js +30 -10
  53. package/lib/screenshot-source-boundary.js +2 -2
  54. package/lib/session-affinity-runtime.js +113 -97
  55. package/lib/session-affinity.js +89 -76
  56. package/lib/session-event-history-compat.js +30 -0
  57. package/lib/session-query-readers.js +263 -0
  58. package/lib/session-surface-compat.js +30 -31
  59. package/lib/session-surface-policy-domain.js +85 -0
  60. package/lib/session-surface-policy.js +2 -111
  61. package/lib/session-turn-resolver.js +2 -6
  62. package/lib/session-vision-attachment-index.js +143 -0
  63. package/lib/session-vision-delegation-boundary.js +34 -0
  64. package/lib/session-vision-delegation.js +22 -0
  65. package/lib/session-vision-index.js +25 -107
  66. package/lib/session-vision-mode-authority.js +229 -192
  67. package/lib/session-vision-mode-boundary.js +65 -22
  68. package/lib/session-vision-policy-domain.js +304 -0
  69. package/lib/session-vision-policy.js +104 -0
  70. package/lib/session-vision-state.js +368 -378
  71. package/lib/settings-client-017-compat.js +16 -10
  72. package/lib/structured-flow-hardening.js +22 -4
  73. package/lib/tesseract-exec-compat.js +9 -1
  74. package/lib/turn-budget-context.js +47 -41
  75. package/lib/update-check.js +11 -2
  76. package/lib/vision-attachment-handle-runtime.js +176 -156
  77. package/lib/vision-backend-runtime-policy.js +16 -0
  78. package/lib/vision-background-benchmark.js +55 -6
  79. package/lib/vision-background-failure-policy.js +41 -41
  80. package/lib/vision-background-stop-store-core.js +217 -0
  81. package/lib/vision-background-stop-store.js +8 -179
  82. package/lib/vision-capability-benchmark.js +11 -61
  83. package/lib/vision-capability-evidence.js +35 -0
  84. package/lib/vision-capability-fingerprint.js +72 -0
  85. package/lib/vision-capability-identity.js +116 -61
  86. package/lib/vision-capability-router.js +13 -33
  87. package/lib/vision-chunk-assembler.js +95 -0
  88. package/lib/vision-diagnostics-routes.js +42 -4
  89. package/lib/vision-execution-order-apply.js +45 -55
  90. package/lib/vision-execution-order.js +19 -19
  91. package/lib/vision-execution-policy.js +12 -7
  92. package/lib/vision-model-registry.js +30 -17
  93. package/lib/vision-model-visibility-boundary-main.js +15 -16
  94. package/lib/vision-provider-transport.js +137 -147
  95. package/lib/vision-routing-authority.js +47 -48
  96. package/lib/vision-routing-product.js +26 -29
  97. package/lib/vision-routing-settings-prelude.js +8 -3
  98. package/lib/vision-runtime-performance-store.js +166 -0
  99. package/lib/vision-runtime-performance.js +13 -142
  100. package/lib/vision-tool-runtime-boundary.js +99 -30
  101. package/lib/web/dsh-settings-017-root-transport.js +27 -12
  102. package/lib/web/remote-settings-client.js +4 -7
  103. package/lib/web-capability-boundary.js +17 -7
  104. package/lib/wrapper-directory.js +14 -23
  105. package/package.json +37 -11
@@ -34,6 +34,16 @@ function liveVisionConfig(ctx, fallback) {
34
34
  * settings therefore leave DSH/Host as the sole network authority.
35
35
  */
36
36
  export function apply(ctx, config = {}) {
37
+ // Cordis effect() is a required plugin-fiber primitive across the entire
38
+ // supported DSH window, not an optional capability. Refuse malformed/partial
39
+ // Hosts before any DVR route, wrapper, transport, watcher or global mutation
40
+ // can be published without a lifecycle owner.
41
+ let effect
42
+ try { effect = ctx?.effect } catch (error) { throw error }
43
+ if (typeof effect !== 'function') {
44
+ throw new TypeError('vision-router requires a Cordis lifecycle context with effect()')
45
+ }
46
+
37
47
  // DSH 0.1.7 replaces SettingsProvider.register() with profile-owned
38
48
  // ConfigEditor writes for ordinary fields. Feature-detect that exact contract
39
49
  // and present DVR's mature settings face without changing older Hosts.
@@ -57,13 +67,16 @@ export function apply(ctx, config = {}) {
57
67
  config: () => liveVisionConfig(hardening.ctx, config),
58
68
  })
59
69
  let transportReleased = false
70
+ let transportReleasePromise
60
71
  const releaseTransport = () => {
61
- if (transportReleased) return
72
+ if (transportReleasePromise) return transportReleasePromise
73
+ if (transportReleased) return Promise.resolve()
62
74
  transportReleased = true
63
- void transport.dispose()
75
+ transportReleasePromise = Promise.resolve().then(() => transport.dispose())
76
+ return transportReleasePromise
64
77
  }
65
78
  try {
66
- runtimeCtx?.effect?.(
79
+ runtimeCtx.effect(
67
80
  () => releaseTransport,
68
81
  'vision-router: provider transport',
69
82
  )
@@ -72,7 +85,7 @@ export function apply(ctx, config = {}) {
72
85
  hardening.installClientBoundary()
73
86
  return result
74
87
  } catch (error) {
75
- releaseTransport()
88
+ void releaseTransport().catch(() => {})
76
89
  throw error
77
90
  }
78
91
  }
@@ -548,15 +548,20 @@ export function contextWithDelegatedReplay(ctx, options = {}) {
548
548
  },
549
549
  })
550
550
  wrappedContexts.set(ctx, wrapped)
551
+ let lifecycleOwned = false
551
552
  try {
552
- ctx.effect?.(
553
- () => () => {
554
- if (wrappedContexts.get(ctx) === wrapped) wrappedContexts.delete(ctx)
555
- },
556
- 'vision-router: delegated replay context lifecycle',
557
- )
553
+ if (typeof ctx.effect === 'function') {
554
+ ctx.effect(
555
+ () => () => {
556
+ if (wrappedContexts.get(ctx) === wrapped) wrappedContexts.delete(ctx)
557
+ },
558
+ 'vision-router: delegated replay context lifecycle',
559
+ )
560
+ lifecycleOwned = true
561
+ }
558
562
  } catch {
559
- /* lifecycle hardening must not block plugin apply */
563
+ // The current caller may still use this private view.
560
564
  }
565
+ if (!lifecycleOwned && wrappedContexts.get(ctx) === wrapped) wrappedContexts.delete(ctx)
561
566
  return wrapped
562
567
  }
@@ -88,15 +88,20 @@ export function contextWithReplayEnvelopeV2Compat(ctx) {
88
88
  },
89
89
  })
90
90
  wrappedContexts.set(ctx, wrapped)
91
+ let lifecycleOwned = false
91
92
  try {
92
- ctx.effect?.(
93
- () => () => {
94
- if (wrappedContexts.get(ctx) === wrapped) wrappedContexts.delete(ctx)
95
- },
96
- 'vision-router: rc7 replay envelope compatibility lifecycle',
97
- )
93
+ if (typeof ctx.effect === 'function') {
94
+ ctx.effect(
95
+ () => () => {
96
+ if (wrappedContexts.get(ctx) === wrapped) wrappedContexts.delete(ctx)
97
+ },
98
+ 'vision-router: rc7 replay envelope compatibility lifecycle',
99
+ )
100
+ lifecycleOwned = true
101
+ }
98
102
  } catch {
99
- /* lifecycle hardening must not block plugin apply */
103
+ // The current caller can still use this one-shot compatibility view.
100
104
  }
105
+ if (!lifecycleOwned && wrappedContexts.get(ctx) === wrapped) wrappedContexts.delete(ctx)
101
106
  return wrapped
102
107
  }
@@ -17,6 +17,9 @@ import { createSessionVisionRuntime } from './session-vision-runtime.js'
17
17
  import { runCoreApplyLifecycle } from './core-apply-lifecycle.js'
18
18
  import { installLegacyCoreVisionPolicyBridge } from './legacy-core-vision-policy-bridge.js'
19
19
  import { installSessionVisionModeBoundary } from './session-vision-mode-boundary.js'
20
+ import { resolveSessionVisionModeAuthority } from './session-vision-mode-authority.js'
21
+ import { createSessionVisionPolicyRuntimeStore } from './session-vision-policy-domain.js'
22
+ import { installSessionVisionDelegationBoundary } from './session-vision-delegation-boundary.js'
20
23
  import { installPiAiBridgeWireCompat } from './pi-ai-bridge-wire-compat.js'
21
24
  import { installLiveModelDiscovery } from './live-model-discovery.js'
22
25
  import { installVisionModelRegistry } from './vision-model-registry.js'
@@ -30,7 +33,6 @@ import { installLocalMutationRouteBoundary } from './web-capability-boundary.js'
30
33
  import { installScreenshotSourceBoundary } from './screenshot-source-boundary.js'
31
34
  import { installVisionToolRuntimeBoundary } from './vision-tool-runtime-boundary.js'
32
35
  import {
33
- configureAgentRequestRouteAuthority,
34
36
  contextWithAgentRequestRouteAuthority,
35
37
  } from './agent-request-route-authority.js'
36
38
  import { contextWithGroundingCoordinateFrame } from './grounding-coordinate-runtime.js'
@@ -96,7 +98,7 @@ function legacyCoreVisionSettings(ctx, fallback) {
96
98
  return { ...current, autoActivateOnImage: false }
97
99
  }
98
100
 
99
- function installHostAndSecurityBoundaries(ctx, config, core) {
101
+ function installHostAndSecurityBoundaries(ctx, config, core, providerTransport) {
100
102
  // SessionVisionIndex calls planGuardStopShadows before Core.apply. Keep that
101
103
  // one helper behind the live-locale facade; the final Core itself still gets
102
104
  // the ordinary composition identity and is localized by the scoped context.
@@ -135,11 +137,14 @@ function installHostAndSecurityBoundaries(ctx, config, core) {
135
137
  config,
136
138
  core,
137
139
  )
138
- const ollamaColdStartCtx = installOllamaColdStartGuard(hardenedCtx, hardenedConfig, core)
140
+ const ollamaColdStartCtx = installOllamaColdStartGuard(hardenedCtx, hardenedConfig, core, {
141
+ logger: logging.logger,
142
+ })
139
143
  const { ctx: stabilizedCtx, bootConfig } = installLocalVisionStabilizer(
140
144
  ollamaColdStartCtx,
141
145
  hardenedConfig,
142
146
  core,
147
+ { providerTransport },
143
148
  )
144
149
 
145
150
  const routingProduct = resolveVisionRoutingProduct(bootConfig)
@@ -228,20 +233,35 @@ function createRuntimeOwners(host, core) {
228
233
  nativeImageCompat.config,
229
234
  { rewriteHistoryImages: core.rewriteHistoryImages },
230
235
  )
236
+ const sessionVisionPolicyStore = createSessionVisionPolicyRuntimeStore(legacyCoreCompat.ctx)
237
+ legacyCoreCompat.ctx.inject?.(['agents'], (agentsCtx) => {
238
+ installSessionVisionDelegationBoundary(
239
+ agentsCtx,
240
+ sessionVisionPolicyStore,
241
+ (parent) => resolveSessionVisionModeAuthority(
242
+ agentsCtx,
243
+ parent,
244
+ legacyCoreCompat.config,
245
+ { sessionPolicy: sessionVisionPolicyStore.get(String(parent.id)) },
246
+ ),
247
+ )
248
+ })
231
249
  const sessionVisionModeCompat = installSessionVisionModeBoundary(
232
250
  legacyCoreCompat.ctx,
233
251
  legacyCoreCompat.config,
252
+ { sessionVisionPolicyStore },
234
253
  )
235
254
 
236
255
  return {
237
256
  coreVisionSurfaceRuntime,
238
257
  nativeImageCompat,
239
258
  sessionVisionModeCompat,
259
+ sessionVisionPolicyStore,
240
260
  sessionVisionRuntime,
241
261
  }
242
262
  }
243
263
 
244
- function installRoutingAndHostProducts(host, owners, core) {
264
+ function installRoutingAndHostProducts(host, owners, core, providerTransport) {
245
265
  const { capabilityStore, logging, runtimeConfig, runtimePerformanceStore } = host
246
266
  const { sessionVisionModeCompat } = owners
247
267
  // Final structured-flow guard sits closest to core.apply so it sees the
@@ -262,7 +282,7 @@ function installRoutingAndHostProducts(host, owners, core) {
262
282
  runtimeConfig,
263
283
  core,
264
284
  capabilityStore,
265
- { logger: logging.logger },
285
+ { logger: logging.logger, providerTransport },
266
286
  )
267
287
  const sessionTurnResolver = createSessionTurnResolver(backgroundProfiling.ctx)
268
288
  const sessionEventTailReader = createSessionEventTailReader(backgroundProfiling.ctx)
@@ -377,12 +397,11 @@ function installExecutionBoundaries(host, owners, routing, core, providerTranspo
377
397
  // Core owns exactly one agent/request routing hook. Protect the completed
378
398
  // provider/model handoff at that event boundary so future route-switch logic
379
399
  // cannot accidentally carry source-model call defaults into the target.
380
- configureAgentRequestRouteAuthority(backendRuntimeCtx, {
400
+ const coreRequestAuthorityCtx = contextWithAgentRequestRouteAuthority(backendRuntimeCtx, {
381
401
  wrapperRoute: runtimeConfig.wrapperRoute,
382
402
  chainRoute: runtimeConfig.chainRoute,
383
403
  logger: logging.logger,
384
404
  })
385
- const coreRequestAuthorityCtx = contextWithAgentRequestRouteAuthority(backendRuntimeCtx)
386
405
 
387
406
  return { coreRequestAuthorityCtx }
388
407
  }
@@ -461,9 +480,10 @@ function applyMatureCoreBridge(host, owners, routing, execution, core, runtime)
461
480
  }
462
481
 
463
482
  export function applyVisionRuntimeComposition(ctx, config = {}, core, runtime = {}) {
464
- const host = installHostAndSecurityBoundaries(ctx, config, core)
483
+ const providerTransport = runtime?.providerTransport
484
+ const host = installHostAndSecurityBoundaries(ctx, config, core, providerTransport)
465
485
  const owners = createRuntimeOwners(host, core)
466
- const routing = installRoutingAndHostProducts(host, owners, core)
467
- const execution = installExecutionBoundaries(host, owners, routing, core, runtime?.providerTransport)
486
+ const routing = installRoutingAndHostProducts(host, owners, core, providerTransport)
487
+ const execution = installExecutionBoundaries(host, owners, routing, core, providerTransport)
468
488
  return applyMatureCoreBridge(host, owners, routing, execution, core, runtime)
469
489
  }
@@ -110,7 +110,7 @@ function wrapTools(tools, ctx, core) {
110
110
  export function installScreenshotSourceBoundary(ctx, core) {
111
111
  if (!ctx || (typeof ctx !== 'object' && typeof ctx !== 'function')) return ctx
112
112
  const cached = wrappedContexts.get(ctx)
113
- if (cached) return cached
113
+ if (cached?.core === core) return cached.wrapped
114
114
  let wrapped
115
115
  wrapped = new Proxy(ctx, {
116
116
  get(target, property) {
@@ -119,6 +119,6 @@ export function installScreenshotSourceBoundary(ctx, core) {
119
119
  return typeof value === 'function' ? value.bind(target) : value
120
120
  },
121
121
  })
122
- wrappedContexts.set(ctx, wrapped)
122
+ wrappedContexts.set(ctx, { core, wrapped })
123
123
  return wrapped
124
124
  }
@@ -1,104 +1,120 @@
1
- import { AsyncLocalStorage } from 'node:async_hooks'
2
- import { rawSessionIdentity } from './session-affinity.js'
3
-
4
- const affinityRuntime = new AsyncLocalStorage()
5
-
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+ import { rawSessionIdentity } from './session-affinity.js';
3
+ const affinityRuntime = new AsyncLocalStorage();
4
+ function propertyBag(value) {
5
+ return value !== null && (typeof value === 'object' || typeof value === 'function')
6
+ ? value
7
+ : undefined;
8
+ }
9
+ function isPromiseLike(value) {
10
+ return typeof propertyBag(value)?.then === 'function';
11
+ }
6
12
  export function currentVisionSessionAffinityId() {
7
- const state = affinityRuntime.getStore()
8
- return state?.active === true ? state.affinityId : undefined
13
+ const state = affinityRuntime.getStore();
14
+ return state?.active === true ? state.affinityId : undefined;
9
15
  }
10
-
11
16
  export function runWithVisionSessionAffinity(affinityId, callback) {
12
- const raw = rawSessionIdentity(affinityId)
13
- if (raw === undefined) return callback()
14
- const state = { affinityId: raw, active: true }
15
- let result
16
- try {
17
- result = affinityRuntime.run(state, callback)
18
- } catch (error) {
19
- state.active = false
20
- throw error
21
- }
22
- if (result && typeof result.then === 'function') {
23
- return Promise.resolve(result).finally(() => { state.active = false })
24
- }
25
- state.active = false
26
- return result
17
+ const raw = rawSessionIdentity(affinityId);
18
+ if (raw === undefined)
19
+ return callback();
20
+ const state = {
21
+ affinityId: raw,
22
+ active: true,
23
+ };
24
+ let result;
25
+ try {
26
+ result = affinityRuntime.run(state, callback);
27
+ }
28
+ catch (error) {
29
+ state.active = false;
30
+ throw error;
31
+ }
32
+ if (isPromiseLike(result)) {
33
+ return Promise.resolve(result).finally(() => {
34
+ state.active = false;
35
+ });
36
+ }
37
+ state.active = false;
38
+ return result;
27
39
  }
28
-
29
- /**
30
- * Keep AsyncLocalStorage active across lazy AsyncIterable creation and every
31
- * iterator operation. Adapter/network work often begins on next(), not when
32
- * ctx.llm.stream() returns the iterable. Promise-returning test/compat streams
33
- * are supported too because some older seams resolve the iterable lazily.
34
- * One mutable state object is retired at stream completion so async work that
35
- * outlives the model request cannot keep projecting the conversation id.
36
- */
37
40
  export function streamWithVisionSessionAffinity(affinityId, streamFactory) {
38
- const raw = rawSessionIdentity(affinityId)
39
- if (raw === undefined) return streamFactory()
40
- return {
41
- [Symbol.asyncIterator]() {
42
- const state = { affinityId: raw, active: true }
43
- let iteratorPromise
44
- const retire = () => { state.active = false }
45
- const ensure = () => {
46
- if (iteratorPromise === undefined) {
47
- iteratorPromise = affinityRuntime.run(state, async () => {
48
- const stream = await streamFactory()
49
- if (!stream || typeof stream[Symbol.asyncIterator] !== 'function') {
50
- throw new TypeError('scoped vision stream is not async iterable')
51
- }
52
- return stream[Symbol.asyncIterator]()
53
- })
54
- }
55
- return iteratorPromise
56
- }
57
- return {
58
- next(value) {
59
- return affinityRuntime.run(state, async () => {
60
- try {
61
- const result = await (await ensure()).next(value)
62
- if (result?.done === true) retire()
63
- return result
64
- } catch (error) {
65
- retire()
66
- throw error
67
- }
68
- })
69
- },
70
- return(value) {
71
- if (iteratorPromise === undefined) {
72
- retire()
73
- return Promise.resolve({ done: true, value })
74
- }
75
- return affinityRuntime.run(state, async () => {
76
- try {
77
- const active = await iteratorPromise
78
- return typeof active.return === 'function'
79
- ? await active.return(value)
80
- : { done: true, value }
81
- } finally {
82
- retire()
83
- }
84
- })
85
- },
86
- throw(error) {
87
- if (iteratorPromise === undefined) {
88
- retire()
89
- return Promise.reject(error)
90
- }
91
- return affinityRuntime.run(state, async () => {
92
- try {
93
- const active = await iteratorPromise
94
- if (typeof active.throw === 'function') return await active.throw(error)
95
- throw error
96
- } finally {
97
- retire()
98
- }
99
- })
41
+ const raw = rawSessionIdentity(affinityId);
42
+ if (raw === undefined)
43
+ return streamFactory();
44
+ return {
45
+ [Symbol.asyncIterator]() {
46
+ const state = {
47
+ affinityId: raw,
48
+ active: true,
49
+ };
50
+ let iteratorPromise;
51
+ const retire = () => {
52
+ state.active = false;
53
+ };
54
+ const ensure = () => {
55
+ if (iteratorPromise === undefined) {
56
+ iteratorPromise = affinityRuntime.run(state, async () => {
57
+ const stream = await streamFactory();
58
+ if (stream === null
59
+ || (typeof stream !== 'object' && typeof stream !== 'function')
60
+ || typeof stream[Symbol.asyncIterator] !== 'function') {
61
+ throw new TypeError('scoped vision stream is not async iterable');
62
+ }
63
+ return stream[Symbol.asyncIterator]();
64
+ });
65
+ }
66
+ return iteratorPromise;
67
+ };
68
+ return {
69
+ next(value) {
70
+ return affinityRuntime.run(state, async () => {
71
+ try {
72
+ const result = await (await ensure()).next(value);
73
+ if (result?.done === true)
74
+ retire();
75
+ return result;
76
+ }
77
+ catch (error) {
78
+ retire();
79
+ throw error;
80
+ }
81
+ });
82
+ },
83
+ return(value) {
84
+ if (iteratorPromise === undefined) {
85
+ retire();
86
+ return Promise.resolve({ done: true, value });
87
+ }
88
+ return affinityRuntime.run(state, async () => {
89
+ try {
90
+ const active = await ensure();
91
+ return typeof active.return === 'function'
92
+ ? await active.return(value)
93
+ : { done: true, value };
94
+ }
95
+ finally {
96
+ retire();
97
+ }
98
+ });
99
+ },
100
+ throw(error) {
101
+ if (iteratorPromise === undefined) {
102
+ retire();
103
+ return Promise.reject(error);
104
+ }
105
+ return affinityRuntime.run(state, async () => {
106
+ try {
107
+ const active = await ensure();
108
+ if (typeof active.throw === 'function')
109
+ return await active.throw(error);
110
+ throw error;
111
+ }
112
+ finally {
113
+ retire();
114
+ }
115
+ });
116
+ },
117
+ };
100
118
  },
101
- }
102
- },
103
- }
119
+ };
104
120
  }
@@ -1,93 +1,106 @@
1
- const MAX_SESSION_AFFINITY_ID_LENGTH = 512
2
-
3
- /** Preserve DSH's in-process session identity exactly; no wire rules belong here. */
1
+ const MAX_SESSION_AFFINITY_ID_LENGTH = 512;
2
+ function propertyBag(value) {
3
+ return value !== null && (typeof value === 'object' || typeof value === 'function')
4
+ ? value
5
+ : undefined;
6
+ }
7
+ function wireAsciiIdentity(text) {
8
+ for (let index = 0; index < text.length; index += 1) {
9
+ const code = text.charCodeAt(index);
10
+ const edge = index === 0 || index === text.length - 1;
11
+ if (edge) {
12
+ if (code < 33 || code > 126)
13
+ return false;
14
+ }
15
+ else if (code < 32 || code > 126) {
16
+ return false;
17
+ }
18
+ }
19
+ return true;
20
+ }
4
21
  export function rawSessionIdentity(value) {
5
- if (value === undefined || value === null) return undefined
6
- try {
7
- return String(value)
8
- } catch {
9
- return undefined
10
- }
22
+ if (value === undefined || value === null)
23
+ return undefined;
24
+ try {
25
+ return String(value);
26
+ }
27
+ catch {
28
+ return undefined;
29
+ }
11
30
  }
12
-
13
- /** Read the live DSH session id across released/alpha session shapes without rewriting it. */
14
31
  export function sessionIdentityOf(session) {
15
- if (!session) return undefined
16
- try {
17
- const direct = rawSessionIdentity(session.id)
18
- if (direct !== undefined) return direct
19
- } catch {
20
- /* continue to persisted/header shapes */
21
- }
22
- try {
23
- const header = rawSessionIdentity(session.header?.id)
24
- if (header !== undefined) return header
25
- } catch {
26
- /* continue to requestHeader() */
27
- }
28
- try {
29
- if (typeof session.requestHeader === 'function') {
30
- return rawSessionIdentity(session.requestHeader()?.id)
32
+ const source = propertyBag(session);
33
+ if (source === undefined)
34
+ return undefined;
35
+ try {
36
+ const direct = rawSessionIdentity(source.id);
37
+ if (direct !== undefined)
38
+ return direct;
31
39
  }
32
- } catch {
33
- /* no usable session identity */
34
- }
35
- return undefined
40
+ catch { }
41
+ try {
42
+ const header = rawSessionIdentity(propertyBag(source.header)?.id);
43
+ if (header !== undefined)
44
+ return header;
45
+ }
46
+ catch { }
47
+ try {
48
+ const requestHeader = source.requestHeader;
49
+ if (typeof requestHeader === 'function') {
50
+ return rawSessionIdentity(propertyBag(requestHeader.call(session))?.id);
51
+ }
52
+ }
53
+ catch { }
54
+ return undefined;
36
55
  }
37
-
38
- /**
39
- * Admit an opaque identity to an HTTP header without changing it. Fetch/undici
40
- * requires ByteString-compatible header values; use visible ASCII so behavior
41
- * is identical across Node 22/24 and HTTP implementations. Leading/trailing
42
- * whitespace is rejected because Headers would normalize it, violating the
43
- * requirement to preserve the DSH identity unchanged.
44
- */
45
56
  export function wireSessionAffinityId(value) {
46
- const text = rawSessionIdentity(value)
47
- if (text === undefined || text === '' || text.length > MAX_SESSION_AFFINITY_ID_LENGTH) return undefined
48
- if (text.trim() !== text) return undefined
49
- if (!/^[\x21-\x7e](?:[\x20-\x7e]*[\x21-\x7e])?$/.test(text)) return undefined
50
- return text
57
+ const text = rawSessionIdentity(value);
58
+ if (text === undefined
59
+ || text === ''
60
+ || text.length > MAX_SESSION_AFFINITY_ID_LENGTH) {
61
+ return undefined;
62
+ }
63
+ if (text.trim() !== text)
64
+ return undefined;
65
+ return wireAsciiIdentity(text) ? text : undefined;
51
66
  }
52
-
53
67
  export function isOfficialOpenCodeGoUrl(value) {
54
- try {
55
- const url = new URL(String(value))
56
- return (
57
- url.protocol === 'https:' &&
58
- url.hostname.toLowerCase() === 'opencode.ai' &&
59
- /^\/zen\/go(?:\/|$)/i.test(url.pathname)
60
- )
61
- } catch {
62
- return false
63
- }
68
+ try {
69
+ const url = new URL(String(value));
70
+ return (url.protocol === 'https:'
71
+ && url.hostname.toLowerCase() === 'opencode.ai'
72
+ && /^\/zen\/go(?:\/|$)/i.test(url.pathname));
73
+ }
74
+ catch {
75
+ return false;
76
+ }
64
77
  }
65
-
66
78
  export function isOpenCodeGoProvider(provider) {
67
- return isOfficialOpenCodeGoUrl(provider?.baseURL ?? provider?.baseUrl ?? '')
79
+ const source = propertyBag(provider);
80
+ return isOfficialOpenCodeGoUrl(source?.baseURL ?? source?.baseUrl ?? '');
68
81
  }
69
-
70
82
  function invalidAffinityError(sessionId) {
71
- const raw = rawSessionIdentity(sessionId)
72
- const error = new Error(
73
- raw === undefined || raw === ''
74
- ? 'OpenCode Go requires a stable session identity; refusing a direct request without one'
75
- : 'OpenCode Go session identity is not safe for an HTTP header; refusing to rewrite or truncate it',
76
- )
77
- error.code = raw === undefined || raw === '' ? 'OPENCODE_SESSION_REQUIRED' : 'OPENCODE_SESSION_INVALID'
78
- return error
83
+ const raw = rawSessionIdentity(sessionId);
84
+ const code = raw === undefined || raw === ''
85
+ ? 'OPENCODE_SESSION_REQUIRED'
86
+ : 'OPENCODE_SESSION_INVALID';
87
+ return Object.assign(new Error(code === 'OPENCODE_SESSION_REQUIRED'
88
+ ? 'OpenCode Go requires a stable session identity; refusing a direct request without one'
89
+ : 'OpenCode Go session identity is not safe for an HTTP header; refusing to rewrite or truncate it'), { code });
79
90
  }
80
-
81
91
  export function directSessionAffinityHeaders(provider, sessionId) {
82
- if (!isOpenCodeGoProvider(provider)) return {}
83
- const wire = wireSessionAffinityId(sessionId)
84
- if (wire === undefined) throw invalidAffinityError(sessionId)
85
- return { 'x-opencode-session': wire }
92
+ if (!isOpenCodeGoProvider(provider))
93
+ return {};
94
+ const wire = wireSessionAffinityId(sessionId);
95
+ if (wire === undefined)
96
+ throw invalidAffinityError(sessionId);
97
+ return { 'x-opencode-session': wire };
86
98
  }
87
-
88
99
  export function openCodeSessionAffinityHeaderForUrl(url, sessionId) {
89
- if (!isOfficialOpenCodeGoUrl(url)) return undefined
90
- const wire = wireSessionAffinityId(sessionId)
91
- if (wire === undefined) throw invalidAffinityError(sessionId)
92
- return wire
100
+ if (!isOfficialOpenCodeGoUrl(url))
101
+ return undefined;
102
+ const wire = wireSessionAffinityId(sessionId);
103
+ if (wire === undefined)
104
+ throw invalidAffinityError(sessionId);
105
+ return wire;
93
106
  }