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
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { SESSION_VISION_POLICY_REVISION, parseSessionVisionPolicy, } from './session-vision-policy.js';
|
|
3
|
+
const policySchema = z.discriminatedUnion('source', [
|
|
4
|
+
z.object({
|
|
5
|
+
revision: z.literal(SESSION_VISION_POLICY_REVISION),
|
|
6
|
+
enabled: z.boolean(),
|
|
7
|
+
source: z.literal('user'),
|
|
8
|
+
}).strict(),
|
|
9
|
+
z.object({
|
|
10
|
+
revision: z.literal(SESSION_VISION_POLICY_REVISION),
|
|
11
|
+
enabled: z.boolean(),
|
|
12
|
+
source: z.literal('delegation'),
|
|
13
|
+
inheritedFrom: z.string().trim().min(1),
|
|
14
|
+
}).strict(),
|
|
15
|
+
]);
|
|
16
|
+
/**
|
|
17
|
+
* Plugin-owned Session sidecar. The shape intentionally satisfies DSH's
|
|
18
|
+
* public DomainSpec contract without importing storage-domain at runtime:
|
|
19
|
+
* older Hosts may not mount that optional capability at all.
|
|
20
|
+
*/
|
|
21
|
+
export const sessionVisionPolicyDomainSpec = {
|
|
22
|
+
name: 'vision_router_session_policy',
|
|
23
|
+
version: 1,
|
|
24
|
+
layout: 'per-record',
|
|
25
|
+
invalidRecords: 'backup-and-skip',
|
|
26
|
+
tables: {
|
|
27
|
+
policies: {
|
|
28
|
+
valueSchema: policySchema,
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
function normalizedSessionId(value) {
|
|
33
|
+
if (typeof value !== 'string' || value.trim() === '') {
|
|
34
|
+
throw new TypeError('session vision policy requires a non-empty session id');
|
|
35
|
+
}
|
|
36
|
+
return value.trim();
|
|
37
|
+
}
|
|
38
|
+
function storageDomainOf(ctx) {
|
|
39
|
+
// storageDomain is optional across the supported Host window. Reading the
|
|
40
|
+
// Cordis service property without inject() turns capability detection into a
|
|
41
|
+
// hard dependency; Context#get is the non-owning, live lookup seam here.
|
|
42
|
+
try {
|
|
43
|
+
const candidate = ctx.get?.('storageDomain');
|
|
44
|
+
return candidate && typeof candidate.open === 'function' ? candidate : undefined;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Create a synchronous-read policy store with lazy official DSH sidecar
|
|
52
|
+
* hydration. No storage capability means ordinary volatile operation.
|
|
53
|
+
*
|
|
54
|
+
* Domain ownership is bound to the consumer fiber before any asynchronous open
|
|
55
|
+
* begins. That ordering is deliberate: an HMR/unload racing a slow open must
|
|
56
|
+
* await/close the eventual handle instead of leaving a same-name domain behind
|
|
57
|
+
* in the longer-lived storageDomain facility. Live facility replacement also
|
|
58
|
+
* retires the previous handle before opening against the replacement.
|
|
59
|
+
*
|
|
60
|
+
* Once a storage failure is observed, this store degrades to volatile for the
|
|
61
|
+
* rest of its lifetime. Vision availability must not depend on auxiliary
|
|
62
|
+
* persistence health; a later plugin generation can retry the durable seam.
|
|
63
|
+
*/
|
|
64
|
+
export function createSessionVisionPolicyRuntimeStore(ctx) {
|
|
65
|
+
const cache = new Map();
|
|
66
|
+
const closedDomains = new WeakSet();
|
|
67
|
+
let activeFacility;
|
|
68
|
+
let domainPromise;
|
|
69
|
+
let transition = Promise.resolve();
|
|
70
|
+
const mutationTails = new Map();
|
|
71
|
+
const activeMutations = new Set();
|
|
72
|
+
const hydrationTokens = new Map();
|
|
73
|
+
let durableDisabled = false;
|
|
74
|
+
let lifecycleClosed = false;
|
|
75
|
+
let lifecycleBound = false;
|
|
76
|
+
let warned = false;
|
|
77
|
+
const warnDegraded = (error) => {
|
|
78
|
+
if (warned)
|
|
79
|
+
return;
|
|
80
|
+
warned = true;
|
|
81
|
+
try {
|
|
82
|
+
ctx.logger?.warn?.('vision-router: Session Vision policy persistence unavailable; using process-local fallback: %s', error instanceof Error ? error.message : String(error));
|
|
83
|
+
}
|
|
84
|
+
catch { }
|
|
85
|
+
};
|
|
86
|
+
const markDurabilityFailed = (error) => {
|
|
87
|
+
durableDisabled = true;
|
|
88
|
+
warnDegraded(error);
|
|
89
|
+
};
|
|
90
|
+
const closeDomainOnce = async (domain) => {
|
|
91
|
+
const key = domain;
|
|
92
|
+
if (closedDomains.has(key))
|
|
93
|
+
return;
|
|
94
|
+
closedDomains.add(key);
|
|
95
|
+
try {
|
|
96
|
+
await domain.close();
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
warnDegraded(error);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
const retireActiveDomain = async () => {
|
|
103
|
+
const pending = domainPromise;
|
|
104
|
+
domainPromise = undefined;
|
|
105
|
+
activeFacility = undefined;
|
|
106
|
+
if (pending === undefined)
|
|
107
|
+
return;
|
|
108
|
+
try {
|
|
109
|
+
const domain = await pending;
|
|
110
|
+
if (domain !== undefined)
|
|
111
|
+
await closeDomainOnce(domain);
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
warnDegraded(error);
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
const enqueueRetire = () => {
|
|
118
|
+
transition = transition.then(retireActiveDomain, retireActiveDomain);
|
|
119
|
+
return transition;
|
|
120
|
+
};
|
|
121
|
+
if (typeof ctx.effect === 'function') {
|
|
122
|
+
try {
|
|
123
|
+
ctx.effect(() => {
|
|
124
|
+
lifecycleBound = true;
|
|
125
|
+
return async () => {
|
|
126
|
+
lifecycleClosed = true;
|
|
127
|
+
// No new durable mutation is admitted after lifecycleClosed flips.
|
|
128
|
+
// Wait every mutation that was already admitted before closing the
|
|
129
|
+
// shared domain underneath it.
|
|
130
|
+
await Promise.allSettled([...activeMutations]);
|
|
131
|
+
await enqueueRetire();
|
|
132
|
+
};
|
|
133
|
+
}, 'vision-router: Session Vision policy domain lifecycle');
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
markDurabilityFailed(error);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const ensureDomainFor = async (facility, allowClosed = false) => {
|
|
140
|
+
if ((lifecycleClosed && !allowClosed) || durableDisabled)
|
|
141
|
+
return;
|
|
142
|
+
if (activeFacility === facility && domainPromise !== undefined)
|
|
143
|
+
return;
|
|
144
|
+
if (domainPromise !== undefined || activeFacility !== undefined) {
|
|
145
|
+
await retireActiveDomain();
|
|
146
|
+
}
|
|
147
|
+
if ((lifecycleClosed && !allowClosed) || durableDisabled)
|
|
148
|
+
return;
|
|
149
|
+
activeFacility = facility;
|
|
150
|
+
let opening;
|
|
151
|
+
opening = facility.open(sessionVisionPolicyDomainSpec)
|
|
152
|
+
.then(async (domain) => {
|
|
153
|
+
if ((lifecycleClosed && !allowClosed)
|
|
154
|
+
|| durableDisabled
|
|
155
|
+
|| activeFacility !== facility
|
|
156
|
+
|| domainPromise !== opening) {
|
|
157
|
+
await closeDomainOnce(domain);
|
|
158
|
+
return undefined;
|
|
159
|
+
}
|
|
160
|
+
return domain;
|
|
161
|
+
})
|
|
162
|
+
.catch((error) => {
|
|
163
|
+
if (domainPromise === opening) {
|
|
164
|
+
domainPromise = undefined;
|
|
165
|
+
activeFacility = undefined;
|
|
166
|
+
markDurabilityFailed(error);
|
|
167
|
+
}
|
|
168
|
+
return undefined;
|
|
169
|
+
});
|
|
170
|
+
domainPromise = opening;
|
|
171
|
+
};
|
|
172
|
+
const openDomain = async (allowClosed = false) => {
|
|
173
|
+
if (!lifecycleBound || (lifecycleClosed && !allowClosed) || durableDisabled)
|
|
174
|
+
return undefined;
|
|
175
|
+
const facility = storageDomainOf(ctx);
|
|
176
|
+
if (facility === undefined)
|
|
177
|
+
return undefined;
|
|
178
|
+
transition = transition.then(() => ensureDomainFor(facility, allowClosed), () => ensureDomainFor(facility, allowClosed));
|
|
179
|
+
await transition;
|
|
180
|
+
return await domainPromise;
|
|
181
|
+
};
|
|
182
|
+
const table = async (allowClosed = false) => {
|
|
183
|
+
const domain = await openDomain(allowClosed);
|
|
184
|
+
if (domain === undefined)
|
|
185
|
+
return undefined;
|
|
186
|
+
return domain.table('policies');
|
|
187
|
+
};
|
|
188
|
+
const failDurabilityAndRetire = async (error) => {
|
|
189
|
+
markDurabilityFailed(error);
|
|
190
|
+
await enqueueRetire();
|
|
191
|
+
};
|
|
192
|
+
const enqueueMutation = (key, mutate) => {
|
|
193
|
+
if (lifecycleClosed)
|
|
194
|
+
return Promise.resolve();
|
|
195
|
+
const previous = mutationTails.get(key) ?? Promise.resolve();
|
|
196
|
+
let current;
|
|
197
|
+
// Admission is decided above. Once admitted, a mutation must run even if
|
|
198
|
+
// lifecycle shutdown begins while it is queued behind an earlier mutation;
|
|
199
|
+
// the disposer waits activeMutations before retiring the domain.
|
|
200
|
+
current = previous.then(mutate, mutate);
|
|
201
|
+
mutationTails.set(key, current);
|
|
202
|
+
activeMutations.add(current);
|
|
203
|
+
void current.then(() => {
|
|
204
|
+
activeMutations.delete(current);
|
|
205
|
+
if (mutationTails.get(key) === current)
|
|
206
|
+
mutationTails.delete(key);
|
|
207
|
+
}, () => {
|
|
208
|
+
activeMutations.delete(current);
|
|
209
|
+
if (mutationTails.get(key) === current)
|
|
210
|
+
mutationTails.delete(key);
|
|
211
|
+
});
|
|
212
|
+
return current;
|
|
213
|
+
};
|
|
214
|
+
return {
|
|
215
|
+
get durable() {
|
|
216
|
+
return lifecycleBound
|
|
217
|
+
&& !lifecycleClosed
|
|
218
|
+
&& !durableDisabled
|
|
219
|
+
&& storageDomainOf(ctx) !== undefined;
|
|
220
|
+
},
|
|
221
|
+
get(sessionId) {
|
|
222
|
+
return cache.get(normalizedSessionId(sessionId));
|
|
223
|
+
},
|
|
224
|
+
async hydrate(sessionId) {
|
|
225
|
+
const key = normalizedSessionId(sessionId);
|
|
226
|
+
const existing = cache.get(key);
|
|
227
|
+
if (existing !== undefined)
|
|
228
|
+
return existing;
|
|
229
|
+
const token = {};
|
|
230
|
+
hydrationTokens.set(key, token);
|
|
231
|
+
try {
|
|
232
|
+
const handle = await table();
|
|
233
|
+
if (hydrationTokens.get(key) !== token)
|
|
234
|
+
return cache.get(key);
|
|
235
|
+
if (handle === undefined)
|
|
236
|
+
return undefined;
|
|
237
|
+
const policy = parseSessionVisionPolicy(handle.get(key));
|
|
238
|
+
if (hydrationTokens.get(key) !== token)
|
|
239
|
+
return cache.get(key);
|
|
240
|
+
if (policy !== undefined)
|
|
241
|
+
cache.set(key, policy);
|
|
242
|
+
return policy;
|
|
243
|
+
}
|
|
244
|
+
catch (error) {
|
|
245
|
+
await failDurabilityAndRetire(error);
|
|
246
|
+
return undefined;
|
|
247
|
+
}
|
|
248
|
+
finally {
|
|
249
|
+
if (hydrationTokens.get(key) === token)
|
|
250
|
+
hydrationTokens.delete(key);
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
release(sessionId) {
|
|
254
|
+
const key = normalizedSessionId(sessionId);
|
|
255
|
+
hydrationTokens.delete(key);
|
|
256
|
+
// A durable row can be rehydrated on the next Agent creation. Keep the
|
|
257
|
+
// process-local copy when durability is unavailable so volatile support
|
|
258
|
+
// does not lose the only Session-scoped truth it has.
|
|
259
|
+
if (this.durable)
|
|
260
|
+
cache.delete(key);
|
|
261
|
+
},
|
|
262
|
+
async set(sessionId, policy) {
|
|
263
|
+
const key = normalizedSessionId(sessionId);
|
|
264
|
+
hydrationTokens.delete(key);
|
|
265
|
+
const normalized = parseSessionVisionPolicy(policy);
|
|
266
|
+
if (normalized === undefined)
|
|
267
|
+
throw new TypeError('invalid session vision policy');
|
|
268
|
+
// Publish process-local authority before the first await. A committed
|
|
269
|
+
// model/selection event must affect the next prompt immediately even if
|
|
270
|
+
// sidecar I/O is still settling; delegation still awaits this method and
|
|
271
|
+
// therefore preserves its stronger durability-before-child-release rule.
|
|
272
|
+
cache.set(key, normalized);
|
|
273
|
+
await enqueueMutation(key, async () => {
|
|
274
|
+
try {
|
|
275
|
+
// This mutation was admitted before lifecycle close; the disposer
|
|
276
|
+
// waits it before retiring the domain, so it may finish durable I/O.
|
|
277
|
+
const handle = await table(true);
|
|
278
|
+
if (handle !== undefined)
|
|
279
|
+
await handle.put(key, normalized);
|
|
280
|
+
}
|
|
281
|
+
catch (error) {
|
|
282
|
+
await failDurabilityAndRetire(error);
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
return normalized;
|
|
286
|
+
},
|
|
287
|
+
async delete(sessionId) {
|
|
288
|
+
const key = normalizedSessionId(sessionId);
|
|
289
|
+
hydrationTokens.delete(key);
|
|
290
|
+
cache.delete(key);
|
|
291
|
+
await enqueueMutation(key, async () => {
|
|
292
|
+
try {
|
|
293
|
+
// Same admission rule as set(): pre-close deletes must not be lost.
|
|
294
|
+
const handle = await table(true);
|
|
295
|
+
if (handle !== undefined)
|
|
296
|
+
await handle.delete(key);
|
|
297
|
+
}
|
|
298
|
+
catch (error) {
|
|
299
|
+
await failDurabilityAndRetire(error);
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
},
|
|
303
|
+
};
|
|
304
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
export const SESSION_VISION_POLICY_REVISION = 1;
|
|
2
|
+
function nonEmptyString(value) {
|
|
3
|
+
return typeof value === 'string' && value.trim() !== '' ? value.trim() : undefined;
|
|
4
|
+
}
|
|
5
|
+
function normalizedSessionId(value) {
|
|
6
|
+
const id = nonEmptyString(value);
|
|
7
|
+
if (id === undefined)
|
|
8
|
+
throw new TypeError('session vision policy requires a non-empty session id');
|
|
9
|
+
return id;
|
|
10
|
+
}
|
|
11
|
+
export function parseSessionVisionPolicy(value) {
|
|
12
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value))
|
|
13
|
+
return undefined;
|
|
14
|
+
const record = value;
|
|
15
|
+
if (record.revision !== SESSION_VISION_POLICY_REVISION)
|
|
16
|
+
return undefined;
|
|
17
|
+
if (typeof record.enabled !== 'boolean')
|
|
18
|
+
return undefined;
|
|
19
|
+
if (record.source !== 'user' && record.source !== 'delegation')
|
|
20
|
+
return undefined;
|
|
21
|
+
if (record.source === 'user') {
|
|
22
|
+
if (record.inheritedFrom !== undefined)
|
|
23
|
+
return undefined;
|
|
24
|
+
return Object.freeze({
|
|
25
|
+
revision: SESSION_VISION_POLICY_REVISION,
|
|
26
|
+
enabled: record.enabled,
|
|
27
|
+
source: 'user',
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
const inheritedFrom = nonEmptyString(record.inheritedFrom);
|
|
31
|
+
if (inheritedFrom === undefined)
|
|
32
|
+
return undefined;
|
|
33
|
+
return Object.freeze({
|
|
34
|
+
revision: SESSION_VISION_POLICY_REVISION,
|
|
35
|
+
enabled: record.enabled,
|
|
36
|
+
source: 'delegation',
|
|
37
|
+
inheritedFrom,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
export function userSessionVisionPolicy(enabled) {
|
|
41
|
+
return Object.freeze({
|
|
42
|
+
revision: SESSION_VISION_POLICY_REVISION,
|
|
43
|
+
enabled,
|
|
44
|
+
source: 'user',
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
export function delegatedSessionVisionPolicy(enabled, parentSessionId) {
|
|
48
|
+
return Object.freeze({
|
|
49
|
+
revision: SESSION_VISION_POLICY_REVISION,
|
|
50
|
+
enabled,
|
|
51
|
+
source: 'delegation',
|
|
52
|
+
inheritedFrom: normalizedSessionId(parentSessionId),
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Own Session-scoped DVR intent independently of provider/model routing.
|
|
57
|
+
*
|
|
58
|
+
* The injected table is deliberately narrower than DSH storage-domain. This
|
|
59
|
+
* module owns DVR policy semantics; the Host boundary that opens a durable
|
|
60
|
+
* domain is a separate composition concern. A malformed persisted row fails
|
|
61
|
+
* closed as absent policy so route-derived compatibility can remain the
|
|
62
|
+
* migration fallback until the durable policy rollout is complete.
|
|
63
|
+
*/
|
|
64
|
+
export function createSessionVisionPolicyStore(table) {
|
|
65
|
+
return {
|
|
66
|
+
get(sessionId) {
|
|
67
|
+
const key = normalizedSessionId(sessionId);
|
|
68
|
+
return parseSessionVisionPolicy(table.get(key));
|
|
69
|
+
},
|
|
70
|
+
async set(sessionId, policy) {
|
|
71
|
+
const key = normalizedSessionId(sessionId);
|
|
72
|
+
const normalized = parseSessionVisionPolicy(policy);
|
|
73
|
+
if (normalized === undefined) {
|
|
74
|
+
throw new TypeError('invalid session vision policy');
|
|
75
|
+
}
|
|
76
|
+
await table.put(key, normalized);
|
|
77
|
+
return normalized;
|
|
78
|
+
},
|
|
79
|
+
async delete(sessionId) {
|
|
80
|
+
await table.delete(normalizedSessionId(sessionId));
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Process-scoped policy backend used while no durable Host sidecar is bound.
|
|
86
|
+
*
|
|
87
|
+
* The Map is closure-owned by the returned store, never process-global. It is
|
|
88
|
+
* intentionally replaceable by a durable table without changing policy,
|
|
89
|
+
* delegation, or authority semantics.
|
|
90
|
+
*/
|
|
91
|
+
export function createVolatileSessionVisionPolicyStore() {
|
|
92
|
+
const rows = new Map();
|
|
93
|
+
return createSessionVisionPolicyStore({
|
|
94
|
+
get(key) {
|
|
95
|
+
return rows.get(key);
|
|
96
|
+
},
|
|
97
|
+
async put(key, value) {
|
|
98
|
+
rows.set(key, value);
|
|
99
|
+
},
|
|
100
|
+
async delete(key) {
|
|
101
|
+
rows.delete(key);
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
}
|