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.
- package/README.md +18 -3
- package/README.zh.md +16 -3
- package/docs/architecture/3.0-architecture-plan.md +252 -0
- package/docs/architecture/3.0-r0-contract-ledger.md +137 -0
- package/docs/architecture/3.0-r1-source-artifact-boundary.md +98 -0
- package/docs/architecture/3.0-r10-final-correctness-closure.md +207 -0
- package/docs/architecture/3.0-r11-adversarial-review.md +447 -0
- package/docs/architecture/3.0-r5-browser-go-no-go.md +165 -0
- package/docs/architecture/3.0-r5-browser-typescript-gate.md +105 -0
- package/docs/architecture/3.0-r6-compat-retirement.md +203 -0
- package/docs/architecture/3.0-r7-public-api.md +151 -0
- package/docs/architecture/3.0-r8-ci-release.md +248 -0
- package/docs/architecture/3.0-r9-final-audit.md +370 -0
- package/docs/architecture/dsh-support-window.md +13 -3
- package/docs/releases/v3.0.0.md +85 -0
- package/docs/releases/v3.0.1.md +77 -0
- package/entry.js +1 -1
- package/index.js +110 -71
- package/lib/agent-request-route-authority.js +1 -20
- package/lib/core-apply-lifecycle.js +37 -43
- package/lib/core-cache.js +87 -0
- package/lib/core-primitives.js +11 -144
- package/lib/core-vision-surface-domain.js +63 -0
- package/lib/core-vision-surface.js +11 -77
- package/lib/delegated-call-config.js +15 -24
- package/lib/doctor-cli.js +0 -0
- package/lib/doctor-runtime.js +4 -1
- package/lib/dsh-contract-compat.js +26 -230
- package/lib/dsh-host-capabilities.js +3 -9
- package/lib/dsh-settings-017-compat.js +17 -10
- package/lib/file-logger.js +8 -3
- package/lib/grounding-coordinate-runtime.js +1 -2
- package/lib/http-body-limit.js +97 -79
- package/lib/image-offload-compat.js +30 -21
- package/lib/image-resource-governor.js +210 -228
- package/lib/legacy-global-proxy-boundary.js +46 -3
- package/lib/live-model-client-prelude.js +12 -4
- package/lib/live-model-discovery.js +134 -43
- package/lib/local-vision-stabilizer.js +33 -18
- package/lib/maintenance-routes.js +1 -3
- package/lib/native-image-coexistence.js +15 -3
- package/lib/ollama-cold-start.js +87 -23
- package/lib/pi-ai-bridge-wire-compat.js +3 -2
- package/lib/proxy-dispatcher-pool.js +151 -92
- package/lib/proxy-routing.js +75 -51
- package/lib/proxy-url-compat.js +3 -10
- package/lib/public-config.js +23 -44
- package/lib/public-entry.d.ts +148 -0
- package/lib/public-entry.js +17 -4
- package/lib/replay-delegation.js +12 -7
- package/lib/replay-envelope-v2-compat.js +12 -7
- package/lib/runtime-composition.js +30 -10
- package/lib/screenshot-source-boundary.js +2 -2
- package/lib/session-affinity-runtime.js +113 -97
- package/lib/session-affinity.js +89 -76
- package/lib/session-event-history-compat.js +30 -0
- package/lib/session-query-readers.js +263 -0
- package/lib/session-surface-compat.js +30 -31
- package/lib/session-surface-policy-domain.js +85 -0
- package/lib/session-surface-policy.js +2 -111
- package/lib/session-turn-resolver.js +2 -6
- package/lib/session-vision-attachment-index.js +143 -0
- package/lib/session-vision-delegation-boundary.js +34 -0
- package/lib/session-vision-delegation.js +22 -0
- package/lib/session-vision-index.js +25 -107
- package/lib/session-vision-mode-authority.js +229 -192
- package/lib/session-vision-mode-boundary.js +65 -22
- package/lib/session-vision-policy-domain.js +304 -0
- package/lib/session-vision-policy.js +104 -0
- package/lib/session-vision-state.js +368 -378
- package/lib/settings-client-017-compat.js +16 -10
- package/lib/structured-flow-hardening.js +22 -4
- package/lib/tesseract-exec-compat.js +9 -1
- package/lib/turn-budget-context.js +47 -41
- package/lib/update-check.js +11 -2
- package/lib/vision-attachment-handle-runtime.js +176 -156
- package/lib/vision-backend-runtime-policy.js +16 -0
- package/lib/vision-background-benchmark.js +55 -6
- package/lib/vision-background-failure-policy.js +41 -41
- package/lib/vision-background-stop-store-core.js +217 -0
- package/lib/vision-background-stop-store.js +8 -179
- package/lib/vision-capability-benchmark.js +11 -61
- package/lib/vision-capability-evidence.js +35 -0
- package/lib/vision-capability-fingerprint.js +72 -0
- package/lib/vision-capability-identity.js +116 -61
- package/lib/vision-capability-router.js +13 -33
- package/lib/vision-chunk-assembler.js +95 -0
- package/lib/vision-diagnostics-routes.js +42 -4
- package/lib/vision-execution-order-apply.js +45 -55
- package/lib/vision-execution-order.js +19 -19
- package/lib/vision-execution-policy.js +12 -7
- package/lib/vision-model-registry.js +30 -17
- package/lib/vision-model-visibility-boundary-main.js +15 -16
- package/lib/vision-provider-transport.js +137 -147
- package/lib/vision-routing-authority.js +47 -48
- package/lib/vision-routing-product.js +26 -29
- package/lib/vision-routing-settings-prelude.js +8 -3
- package/lib/vision-runtime-performance-store.js +166 -0
- package/lib/vision-runtime-performance.js +13 -142
- package/lib/vision-tool-runtime-boundary.js +99 -30
- package/lib/web/dsh-settings-017-root-transport.js +27 -12
- package/lib/web/remote-settings-client.js +4 -7
- package/lib/web-capability-boundary.js +17 -7
- package/lib/wrapper-directory.js +14 -23
- package/package.json +37 -11
package/lib/public-entry.js
CHANGED
|
@@ -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 (
|
|
72
|
+
if (transportReleasePromise) return transportReleasePromise
|
|
73
|
+
if (transportReleased) return Promise.resolve()
|
|
62
74
|
transportReleased = true
|
|
63
|
-
|
|
75
|
+
transportReleasePromise = Promise.resolve().then(() => transport.dispose())
|
|
76
|
+
return transportReleasePromise
|
|
64
77
|
}
|
|
65
78
|
try {
|
|
66
|
-
runtimeCtx
|
|
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
|
}
|
package/lib/replay-delegation.js
CHANGED
|
@@ -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
|
-
|
|
555
|
-
|
|
556
|
-
|
|
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
|
-
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
8
|
-
|
|
13
|
+
const state = affinityRuntime.getStore();
|
|
14
|
+
return state?.active === true ? state.affinityId : undefined;
|
|
9
15
|
}
|
|
10
|
-
|
|
11
16
|
export function runWithVisionSessionAffinity(affinityId, callback) {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
}
|
package/lib/session-affinity.js
CHANGED
|
@@ -1,93 +1,106 @@
|
|
|
1
|
-
const MAX_SESSION_AFFINITY_ID_LENGTH = 512
|
|
2
|
-
|
|
3
|
-
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
79
|
+
const source = propertyBag(provider);
|
|
80
|
+
return isOfficialOpenCodeGoUrl(source?.baseURL ?? source?.baseUrl ?? '');
|
|
68
81
|
}
|
|
69
|
-
|
|
70
82
|
function invalidAffinityError(sessionId) {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
}
|