dsh-vision-router 2.3.0 → 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 (103) hide show
  1. package/README.md +12 -3
  2. package/README.zh.md +12 -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 +441 -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 +5 -3
  15. package/docs/releases/v3.0.0.md +85 -0
  16. package/entry.js +1 -1
  17. package/index.js +108 -71
  18. package/lib/agent-request-route-authority.js +1 -20
  19. package/lib/core-apply-lifecycle.js +37 -43
  20. package/lib/core-cache.js +87 -0
  21. package/lib/core-primitives.js +3 -143
  22. package/lib/core-vision-surface-domain.js +63 -0
  23. package/lib/core-vision-surface.js +11 -77
  24. package/lib/delegated-call-config.js +15 -24
  25. package/lib/doctor-cli.js +0 -0
  26. package/lib/doctor-runtime.js +4 -1
  27. package/lib/dsh-contract-compat.js +26 -230
  28. package/lib/dsh-host-capabilities.js +3 -9
  29. package/lib/dsh-settings-017-compat.js +17 -10
  30. package/lib/file-logger.js +8 -3
  31. package/lib/grounding-coordinate-runtime.js +1 -2
  32. package/lib/http-body-limit.js +97 -79
  33. package/lib/image-offload-compat.js +30 -21
  34. package/lib/image-resource-governor.js +210 -228
  35. package/lib/legacy-global-proxy-boundary.js +46 -3
  36. package/lib/live-model-client-prelude.js +12 -4
  37. package/lib/live-model-discovery.js +134 -43
  38. package/lib/local-vision-stabilizer.js +31 -17
  39. package/lib/maintenance-routes.js +1 -3
  40. package/lib/native-image-coexistence.js +15 -3
  41. package/lib/ollama-cold-start.js +87 -23
  42. package/lib/pi-ai-bridge-wire-compat.js +3 -2
  43. package/lib/proxy-dispatcher-pool.js +151 -92
  44. package/lib/proxy-routing.js +75 -51
  45. package/lib/proxy-url-compat.js +3 -10
  46. package/lib/public-config.js +23 -44
  47. package/lib/public-entry.d.ts +148 -0
  48. package/lib/public-entry.js +17 -4
  49. package/lib/replay-delegation.js +12 -7
  50. package/lib/replay-envelope-v2-compat.js +12 -7
  51. package/lib/runtime-composition.js +22 -4
  52. package/lib/screenshot-source-boundary.js +2 -2
  53. package/lib/session-affinity-runtime.js +113 -97
  54. package/lib/session-affinity.js +89 -76
  55. package/lib/session-event-history-compat.js +30 -0
  56. package/lib/session-query-readers.js +263 -0
  57. package/lib/session-surface-compat.js +30 -31
  58. package/lib/session-surface-policy-domain.js +85 -0
  59. package/lib/session-surface-policy.js +2 -111
  60. package/lib/session-turn-resolver.js +2 -6
  61. package/lib/session-vision-attachment-index.js +143 -0
  62. package/lib/session-vision-delegation-boundary.js +34 -0
  63. package/lib/session-vision-delegation.js +22 -0
  64. package/lib/session-vision-index.js +25 -107
  65. package/lib/session-vision-mode-authority.js +229 -192
  66. package/lib/session-vision-mode-boundary.js +65 -22
  67. package/lib/session-vision-policy-domain.js +304 -0
  68. package/lib/session-vision-policy.js +104 -0
  69. package/lib/session-vision-state.js +368 -378
  70. package/lib/settings-client-017-compat.js +9 -7
  71. package/lib/structured-flow-hardening.js +22 -4
  72. package/lib/tesseract-exec-compat.js +9 -1
  73. package/lib/turn-budget-context.js +47 -41
  74. package/lib/update-check.js +11 -2
  75. package/lib/vision-attachment-handle-runtime.js +176 -156
  76. package/lib/vision-backend-runtime-policy.js +16 -0
  77. package/lib/vision-background-benchmark.js +41 -4
  78. package/lib/vision-background-failure-policy.js +41 -41
  79. package/lib/vision-background-stop-store-core.js +217 -0
  80. package/lib/vision-background-stop-store.js +8 -179
  81. package/lib/vision-capability-benchmark.js +11 -61
  82. package/lib/vision-capability-evidence.js +35 -0
  83. package/lib/vision-capability-fingerprint.js +72 -0
  84. package/lib/vision-capability-identity.js +116 -61
  85. package/lib/vision-capability-router.js +13 -33
  86. package/lib/vision-chunk-assembler.js +95 -0
  87. package/lib/vision-diagnostics-routes.js +34 -4
  88. package/lib/vision-execution-order-apply.js +45 -55
  89. package/lib/vision-execution-order.js +19 -19
  90. package/lib/vision-execution-policy.js +12 -7
  91. package/lib/vision-model-registry.js +30 -17
  92. package/lib/vision-provider-transport.js +137 -147
  93. package/lib/vision-routing-authority.js +47 -48
  94. package/lib/vision-routing-product.js +26 -29
  95. package/lib/vision-routing-settings-prelude.js +8 -3
  96. package/lib/vision-runtime-performance-store.js +166 -0
  97. package/lib/vision-runtime-performance.js +13 -142
  98. package/lib/vision-tool-runtime-boundary.js +99 -30
  99. package/lib/web/dsh-settings-017-root-transport.js +27 -12
  100. package/lib/web/remote-settings-client.js +4 -7
  101. package/lib/web-capability-boundary.js +17 -7
  102. package/lib/wrapper-directory.js +14 -23
  103. 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'
@@ -135,7 +137,9 @@ 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,
@@ -228,15 +232,30 @@ function createRuntimeOwners(host, core) {
228
232
  nativeImageCompat.config,
229
233
  { rewriteHistoryImages: core.rewriteHistoryImages },
230
234
  )
235
+ const sessionVisionPolicyStore = createSessionVisionPolicyRuntimeStore(legacyCoreCompat.ctx)
236
+ legacyCoreCompat.ctx.inject?.(['agents'], (agentsCtx) => {
237
+ installSessionVisionDelegationBoundary(
238
+ agentsCtx,
239
+ sessionVisionPolicyStore,
240
+ (parent) => resolveSessionVisionModeAuthority(
241
+ agentsCtx,
242
+ parent,
243
+ legacyCoreCompat.config,
244
+ { sessionPolicy: sessionVisionPolicyStore.get(String(parent.id)) },
245
+ ),
246
+ )
247
+ })
231
248
  const sessionVisionModeCompat = installSessionVisionModeBoundary(
232
249
  legacyCoreCompat.ctx,
233
250
  legacyCoreCompat.config,
251
+ { sessionVisionPolicyStore },
234
252
  )
235
253
 
236
254
  return {
237
255
  coreVisionSurfaceRuntime,
238
256
  nativeImageCompat,
239
257
  sessionVisionModeCompat,
258
+ sessionVisionPolicyStore,
240
259
  sessionVisionRuntime,
241
260
  }
242
261
  }
@@ -377,12 +396,11 @@ function installExecutionBoundaries(host, owners, routing, core, providerTranspo
377
396
  // Core owns exactly one agent/request routing hook. Protect the completed
378
397
  // provider/model handoff at that event boundary so future route-switch logic
379
398
  // cannot accidentally carry source-model call defaults into the target.
380
- configureAgentRequestRouteAuthority(backendRuntimeCtx, {
399
+ const coreRequestAuthorityCtx = contextWithAgentRequestRouteAuthority(backendRuntimeCtx, {
381
400
  wrapperRoute: runtimeConfig.wrapperRoute,
382
401
  chainRoute: runtimeConfig.chainRoute,
383
402
  logger: logging.logger,
384
403
  })
385
- const coreRequestAuthorityCtx = contextWithAgentRequestRouteAuthority(backendRuntimeCtx)
386
404
 
387
405
  return { coreRequestAuthorityCtx }
388
406
  }
@@ -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
  }
@@ -0,0 +1,30 @@
1
+ function propertyBag(value) {
2
+ return value !== null && (typeof value === 'object' || typeof value === 'function')
3
+ ? value
4
+ : undefined;
5
+ }
6
+ /**
7
+ * Read the released Session-local event snapshot used only when the Host does
8
+ * not advertise the asynchronous SessionQuery capability.
9
+ *
10
+ * Durable-data compatibility is intentionally separate from the current Host
11
+ * floor: this helper feature-detects historical Session shapes at runtime and
12
+ * never claims that either shape exists through a static Host type.
13
+ */
14
+ export function legacySessionEvents(session) {
15
+ const source = propertyBag(session);
16
+ if (source === undefined)
17
+ return undefined;
18
+ try {
19
+ const snapshotEvents = source.snapshotEvents;
20
+ if (typeof snapshotEvents === 'function') {
21
+ const events = snapshotEvents.call(session);
22
+ return Array.isArray(events) ? events : undefined;
23
+ }
24
+ const events = source.events;
25
+ return Array.isArray(events) ? events : undefined;
26
+ }
27
+ catch {
28
+ return undefined;
29
+ }
30
+ }