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.
- package/README.md +12 -3
- package/README.zh.md +12 -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 +441 -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 +5 -3
- package/docs/releases/v3.0.0.md +85 -0
- package/entry.js +1 -1
- package/index.js +108 -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 +3 -143
- 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 +31 -17
- 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 +22 -4
- 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 +9 -7
- 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 +41 -4
- 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 +34 -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-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'
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
}
|
|
@@ -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
|
+
}
|