dsh-vision-router 2.2.8 → 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 +16 -7
- package/README.zh.md +16 -7
- package/docs/architecture/2.3.0-compat-retirement.md +147 -0
- package/docs/architecture/2.3.0-execution-plan.md +410 -0
- package/docs/architecture/2.3.0-final-audit.md +279 -0
- package/docs/architecture/2.3.0-reopened-plan.md +720 -0
- 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/compat-inventory.md +26 -34
- package/docs/architecture/dsh-compatibility-matrix.md +65 -54
- package/docs/architecture/dsh-support-window.md +30 -22
- package/docs/architecture/final-closure-audit.md +2 -0
- package/docs/releases/v2.3.0.md +53 -0
- package/docs/releases/v3.0.0.md +85 -0
- package/entry.js +9 -57
- package/index.js +268 -761
- package/lib/agent-request-route-authority.js +1 -20
- package/lib/catalog-corrections.js +1 -2
- package/lib/client-maintenance-actions.js +145 -0
- package/lib/client-presentation-boundary-main.js +75 -17
- package/lib/client-presentation-boundary.js +53 -28
- package/lib/client.js +246 -144
- package/lib/core-apply-lifecycle.js +41 -0
- package/lib/core-cache.js +87 -0
- package/lib/core-primitives.js +91 -278
- 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/desktop-screenshot-tool.js +262 -0
- package/lib/doctor-cli.js +0 -0
- package/lib/doctor-runtime.js +4 -1
- package/lib/dsh-contract-compat.js +37 -256
- package/lib/dsh-host-capabilities.js +3 -9
- package/lib/dsh-settings-017-compat.js +17 -10
- package/lib/dsh-support-window.js +3 -3
- 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/http-compat.js +1 -2
- 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-remote-settings-permission.js +33 -7
- package/lib/local-vision-stabilizer.js +31 -17
- package/lib/maintenance-routes.js +134 -0
- 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 +36 -0
- package/lib/public-entry.d.ts +148 -0
- package/lib/public-entry.js +19 -11
- package/lib/remote-settings-bridge.js +11 -0
- package/lib/remote-settings-risk-client-boundary.js +155 -0
- package/lib/remote-settings-risk-confirmation.js +17 -105
- package/lib/replay-delegation.js +12 -7
- package/lib/replay-envelope-v2-compat.js +12 -7
- package/lib/runtime-composition.js +127 -34
- package/lib/runtime-i18n-boundary.js +10 -11
- 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-message-source-compat.js +7 -3
- 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 +179 -116
- 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/settings-client-loader-lifecycle.js +129 -0
- package/lib/sharp-runtime.js +0 -7
- package/lib/structured-flow-hardening.js +25 -6
- 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 +19 -2
- 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-service.js +1 -0
- 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 +246 -0
- package/lib/vision-evidence-guidance.js +3 -1
- 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 -176
- 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-toggle-root-hardening.js +22 -5
- package/lib/vision-tool-runtime-boundary.js +104 -31
- package/lib/web/dsh-settings-017-root-transport.js +27 -12
- package/lib/web/remote-settings-client.js +6 -9
- package/lib/web-capability-boundary.js +17 -7
- package/lib/wrapper-directory.js +14 -23
- package/package.json +56 -20
- package/lib/android-attachment-compat.js +0 -244
- package/lib/settings-client-rc8-lifecycle.js +0 -397
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
|
+
}
|
|
@@ -62,9 +62,13 @@ function normalizeHookResult(event, args, result) {
|
|
|
62
62
|
|
|
63
63
|
/**
|
|
64
64
|
* DSH Session V3 requires the historical {kind:'plugin', plugin:...} source,
|
|
65
|
-
* while V4 rejects that wrapper in favor of producer-owned source kinds.
|
|
66
|
-
*
|
|
67
|
-
*
|
|
65
|
+
* while V4 rejects that wrapper in favor of producer-owned source kinds.
|
|
66
|
+
*
|
|
67
|
+
* Session-facing DVR producers are responsible for calling
|
|
68
|
+
* visionRouterMessageSource(session) when they create a message. This final
|
|
69
|
+
* publication boundary is deliberately defense-in-depth: it protects legacy
|
|
70
|
+
* or future producers that accidentally reach a hook with the wrong DVR source,
|
|
71
|
+
* but producer correctness must not depend on this proxy being the sole owner.
|
|
68
72
|
*/
|
|
69
73
|
export function installVisionRouterMessageSourceBoundary(ctx) {
|
|
70
74
|
if (!ctx || typeof ctx !== 'object') return ctx
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
const disposeSymbol = Symbol.dispose;
|
|
2
|
+
const SESSION_EVENT_TAIL_INITIAL_AFTER = 50;
|
|
3
|
+
const SESSION_EVENT_TAIL_MAX_EVENTS = 4096;
|
|
4
|
+
const SESSION_EVENT_TAIL_MAX_READS = 128;
|
|
5
|
+
function objectRecord(value) {
|
|
6
|
+
return value !== null && typeof value === 'object'
|
|
7
|
+
? value
|
|
8
|
+
: undefined;
|
|
9
|
+
}
|
|
10
|
+
function propertyBag(value) {
|
|
11
|
+
return value !== null && (typeof value === 'object' || typeof value === 'function')
|
|
12
|
+
? value
|
|
13
|
+
: undefined;
|
|
14
|
+
}
|
|
15
|
+
function sessionIdOf(session) {
|
|
16
|
+
return propertyBag(session)?.id;
|
|
17
|
+
}
|
|
18
|
+
function sessionQueryOf(ctx) {
|
|
19
|
+
const context = objectRecord(ctx);
|
|
20
|
+
if (context === undefined)
|
|
21
|
+
return undefined;
|
|
22
|
+
// sessionQuery is an optional capability. Cordis service property reads may
|
|
23
|
+
// register a hard pending dependency outside inject(); Context#get is the
|
|
24
|
+
// non-owning capability probe and is the only supported lookup here.
|
|
25
|
+
try {
|
|
26
|
+
const get = context.get;
|
|
27
|
+
if (typeof get !== 'function')
|
|
28
|
+
return undefined;
|
|
29
|
+
return objectRecord(get.call(ctx, 'sessionQuery'));
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function sessionQueryCode(error) {
|
|
36
|
+
const code = propertyBag(error)?.code;
|
|
37
|
+
return typeof code === 'string' ? code : undefined;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Build the narrow bounded Session event reader used by durable surface repair.
|
|
41
|
+
*
|
|
42
|
+
* The Host SessionQuery service is intentionally discovered by capability at
|
|
43
|
+
* runtime instead of imported as a concrete service graph. DVR owns only the
|
|
44
|
+
* small supported/unsupported reader port returned from this adapter.
|
|
45
|
+
*/
|
|
46
|
+
export function createSessionEventReader(ctx) {
|
|
47
|
+
return async function readSessionEvent(session, seqValue) {
|
|
48
|
+
const sessionId = sessionIdOf(session);
|
|
49
|
+
if (sessionId === undefined || sessionId === null)
|
|
50
|
+
return { supported: false };
|
|
51
|
+
if (typeof seqValue !== 'number'
|
|
52
|
+
|| !Number.isSafeInteger(seqValue)
|
|
53
|
+
|| seqValue < 0
|
|
54
|
+
|| Object.is(seqValue, -0)) {
|
|
55
|
+
throw new TypeError(`session event seq must be a non-negative safe integer, got ${String(seqValue)}`);
|
|
56
|
+
}
|
|
57
|
+
const seq = seqValue;
|
|
58
|
+
const query = sessionQueryOf(ctx);
|
|
59
|
+
const readEvent = query?.readEvent;
|
|
60
|
+
if (query === undefined || typeof readEvent !== 'function') {
|
|
61
|
+
return { supported: false };
|
|
62
|
+
}
|
|
63
|
+
const window = await readEvent.call(query, { sessionId, seq });
|
|
64
|
+
const event = propertyBag(window)?.target;
|
|
65
|
+
const eventRecord = objectRecord(event);
|
|
66
|
+
if (eventRecord === undefined) {
|
|
67
|
+
throw new Error(`sessionQuery.readEvent returned no target for seq ${seq}`);
|
|
68
|
+
}
|
|
69
|
+
if (eventRecord.seq !== undefined && eventRecord.seq !== seq) {
|
|
70
|
+
throw new Error(`sessionQuery.readEvent returned seq ${String(eventRecord.seq)} for requested seq ${seq}`);
|
|
71
|
+
}
|
|
72
|
+
return { supported: true, event };
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Read raw Session events strictly after one known committed seq through the
|
|
77
|
+
* Host-owned bounded SessionQuery window contract.
|
|
78
|
+
*/
|
|
79
|
+
export function createSessionEventTailReader(ctx) {
|
|
80
|
+
let afterHint = SESSION_EVENT_TAIL_INITIAL_AFTER;
|
|
81
|
+
const readWindow = async (query, sessionId, seq) => {
|
|
82
|
+
let after = afterHint;
|
|
83
|
+
while (true) {
|
|
84
|
+
try {
|
|
85
|
+
const request = after > 0 ? { sessionId, seq, after } : { sessionId, seq };
|
|
86
|
+
const readEvent = query.readEvent;
|
|
87
|
+
if (typeof readEvent !== 'function') {
|
|
88
|
+
throw new TypeError('sessionQuery.readEvent is unavailable');
|
|
89
|
+
}
|
|
90
|
+
const window = await readEvent.call(query, request);
|
|
91
|
+
afterHint = after;
|
|
92
|
+
return { window, after };
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
if (sessionQueryCode(error) !== 'SESSION_QUERY_INVALID_WINDOW' || after <= 0) {
|
|
96
|
+
throw error;
|
|
97
|
+
}
|
|
98
|
+
after = after <= 1 ? 0 : Math.floor(after / 2);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
return async function readSessionEventTail(session, anchorSeqValue, options = {}) {
|
|
103
|
+
const sessionId = sessionIdOf(session);
|
|
104
|
+
if (sessionId === undefined || sessionId === null)
|
|
105
|
+
return { supported: false };
|
|
106
|
+
if (typeof anchorSeqValue !== 'number'
|
|
107
|
+
|| !Number.isSafeInteger(anchorSeqValue)
|
|
108
|
+
|| anchorSeqValue < 0
|
|
109
|
+
|| Object.is(anchorSeqValue, -0)) {
|
|
110
|
+
throw new TypeError(`session tail anchor seq must be a non-negative safe integer, got ${String(anchorSeqValue)}`);
|
|
111
|
+
}
|
|
112
|
+
const anchorSeq = anchorSeqValue;
|
|
113
|
+
const query = sessionQueryOf(ctx);
|
|
114
|
+
if (query === undefined || typeof query.readEvent !== 'function') {
|
|
115
|
+
return { supported: false };
|
|
116
|
+
}
|
|
117
|
+
const collect = propertyBag(options)?.collect !== false;
|
|
118
|
+
const events = [];
|
|
119
|
+
let capturedThroughSeq = anchorSeq;
|
|
120
|
+
let cursor = anchorSeq;
|
|
121
|
+
let inspected = 0;
|
|
122
|
+
let reads = 0;
|
|
123
|
+
while (reads < SESSION_EVENT_TAIL_MAX_READS) {
|
|
124
|
+
let result;
|
|
125
|
+
try {
|
|
126
|
+
result = await readWindow(query, sessionId, cursor);
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
if (sessionQueryCode(error) === 'SESSION_QUERY_EVENT_NOT_FOUND'
|
|
130
|
+
&& cursor > anchorSeq) {
|
|
131
|
+
return {
|
|
132
|
+
supported: true,
|
|
133
|
+
events,
|
|
134
|
+
capturedThroughSeq,
|
|
135
|
+
truncated: false,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
throw error;
|
|
139
|
+
}
|
|
140
|
+
reads += 1;
|
|
141
|
+
const window = propertyBag(result.window);
|
|
142
|
+
const target = window?.target;
|
|
143
|
+
const targetRecord = objectRecord(target);
|
|
144
|
+
if (targetRecord === undefined || targetRecord.seq !== cursor) {
|
|
145
|
+
throw new Error(`sessionQuery.readEvent returned an invalid target for seq ${cursor}`);
|
|
146
|
+
}
|
|
147
|
+
const windowEvents = window?.events;
|
|
148
|
+
if (!Array.isArray(windowEvents) || windowEvents.length === 0) {
|
|
149
|
+
throw new Error(`sessionQuery.readEvent returned no event window for seq ${cursor}`);
|
|
150
|
+
}
|
|
151
|
+
let expectedSeq = cursor;
|
|
152
|
+
let endSeq = cursor;
|
|
153
|
+
for (const event of windowEvents) {
|
|
154
|
+
const seqValue = propertyBag(event)?.seq;
|
|
155
|
+
if (typeof seqValue !== 'number'
|
|
156
|
+
|| !Number.isSafeInteger(seqValue)
|
|
157
|
+
|| seqValue !== expectedSeq) {
|
|
158
|
+
throw new Error(`sessionQuery.readEvent returned non-contiguous seq ${String(seqValue)}; expected ${expectedSeq}`);
|
|
159
|
+
}
|
|
160
|
+
const seq = seqValue;
|
|
161
|
+
expectedSeq += 1;
|
|
162
|
+
endSeq = seq;
|
|
163
|
+
if (seq <= capturedThroughSeq)
|
|
164
|
+
continue;
|
|
165
|
+
capturedThroughSeq = seq;
|
|
166
|
+
inspected += 1;
|
|
167
|
+
if (collect)
|
|
168
|
+
events.push(event);
|
|
169
|
+
if (inspected >= SESSION_EVENT_TAIL_MAX_EVENTS) {
|
|
170
|
+
return {
|
|
171
|
+
supported: true,
|
|
172
|
+
events,
|
|
173
|
+
capturedThroughSeq,
|
|
174
|
+
truncated: true,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
const windowEndSeq = window?.endSeq;
|
|
179
|
+
if (typeof windowEndSeq === 'number'
|
|
180
|
+
&& Number.isSafeInteger(windowEndSeq)
|
|
181
|
+
&& windowEndSeq !== endSeq) {
|
|
182
|
+
throw new Error(`sessionQuery.readEvent returned endSeq ${String(windowEndSeq)} for window ending at ${endSeq}`);
|
|
183
|
+
}
|
|
184
|
+
if (result.after > 0) {
|
|
185
|
+
if (endSeq <= cursor || windowEvents.length < result.after + 1) {
|
|
186
|
+
return {
|
|
187
|
+
supported: true,
|
|
188
|
+
events,
|
|
189
|
+
capturedThroughSeq: Math.max(capturedThroughSeq, endSeq),
|
|
190
|
+
truncated: false,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
cursor = endSeq;
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
// readWindowMax: 0 is valid. Walk exact seqs and use EVENT_NOT_FOUND as
|
|
197
|
+
// the logical tail sentinel.
|
|
198
|
+
cursor += 1;
|
|
199
|
+
}
|
|
200
|
+
return {
|
|
201
|
+
supported: true,
|
|
202
|
+
events,
|
|
203
|
+
capturedThroughSeq,
|
|
204
|
+
truncated: true,
|
|
205
|
+
};
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Build the asynchronous Session-log reader used by cold attachment recovery
|
|
210
|
+
* and batched surface repair.
|
|
211
|
+
*
|
|
212
|
+
* Prefer the Host observation lease when available and always dispose the lease
|
|
213
|
+
* after copying out the raw event array. Older supported capability shapes keep
|
|
214
|
+
* the released readSession() path.
|
|
215
|
+
*/
|
|
216
|
+
export function createSessionLogReader(ctx) {
|
|
217
|
+
return async function readSessionLog(session) {
|
|
218
|
+
const sessionId = sessionIdOf(session);
|
|
219
|
+
if (sessionId === undefined || sessionId === null)
|
|
220
|
+
return { supported: false };
|
|
221
|
+
const query = sessionQueryOf(ctx);
|
|
222
|
+
if (query === undefined)
|
|
223
|
+
return { supported: false };
|
|
224
|
+
const observeSession = query.observeSession;
|
|
225
|
+
if (typeof observeSession === 'function') {
|
|
226
|
+
let observation;
|
|
227
|
+
try {
|
|
228
|
+
observation = await observeSession.call(query, sessionId, { projectionMode: 'none' });
|
|
229
|
+
const observationBag = propertyBag(observation);
|
|
230
|
+
const events = observationBag?.events;
|
|
231
|
+
if (!Array.isArray(events)) {
|
|
232
|
+
throw new Error('sessionQuery.observeSession returned no event log');
|
|
233
|
+
}
|
|
234
|
+
const observedId = propertyBag(observationBag?.header)?.id;
|
|
235
|
+
if (observedId !== undefined && observedId !== sessionId) {
|
|
236
|
+
throw new Error(`sessionQuery.observeSession returned session ${String(observedId)} for requested ${String(sessionId)}`);
|
|
237
|
+
}
|
|
238
|
+
return { supported: true, events };
|
|
239
|
+
}
|
|
240
|
+
finally {
|
|
241
|
+
const dispose = disposeSymbol === undefined
|
|
242
|
+
? undefined
|
|
243
|
+
: propertyBag(observation)?.[disposeSymbol];
|
|
244
|
+
if (typeof dispose === 'function')
|
|
245
|
+
dispose.call(observation);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
const readSession = query.readSession;
|
|
249
|
+
if (typeof readSession !== 'function')
|
|
250
|
+
return { supported: false };
|
|
251
|
+
const snapshot = await readSession.call(query, sessionId);
|
|
252
|
+
const snapshotBag = propertyBag(snapshot);
|
|
253
|
+
const events = snapshotBag?.events;
|
|
254
|
+
if (!Array.isArray(events)) {
|
|
255
|
+
throw new Error('sessionQuery.readSession returned no event log');
|
|
256
|
+
}
|
|
257
|
+
const observedId = propertyBag(snapshotBag?.session)?.id;
|
|
258
|
+
if (observedId !== undefined && observedId !== sessionId) {
|
|
259
|
+
throw new Error(`sessionQuery.readSession returned session ${String(observedId)} for requested ${String(sessionId)}`);
|
|
260
|
+
}
|
|
261
|
+
return { supported: true, events };
|
|
262
|
+
};
|
|
263
|
+
}
|
|
@@ -1,41 +1,40 @@
|
|
|
1
|
+
function propertyBag(value) {
|
|
2
|
+
return value !== null && typeof value === 'object'
|
|
3
|
+
? value
|
|
4
|
+
: undefined;
|
|
5
|
+
}
|
|
1
6
|
function nonNegativeSafeInteger(value) {
|
|
2
|
-
|
|
7
|
+
return (typeof value === 'number'
|
|
8
|
+
&& Number.isSafeInteger(value)
|
|
9
|
+
&& value >= 0
|
|
10
|
+
&& !Object.is(value, -0));
|
|
3
11
|
}
|
|
4
|
-
|
|
5
12
|
/**
|
|
6
13
|
* Build the Host-owned Session surface replacement intent for one exact node.
|
|
7
14
|
*
|
|
8
15
|
* DSH Session format v3 renamed replacement endpoints from start/end to
|
|
9
|
-
* startSeq/endSeq, and reviewed format v4 keeps that
|
|
10
|
-
*
|
|
11
|
-
* not to a DSH package-version heuristic, so dispatch on session.header.version
|
|
12
|
-
* and refuse unknown future formats instead of guessing forward compatibility.
|
|
13
|
-
*
|
|
14
|
-
* Returns undefined when the Host does not expose a supported logical Session
|
|
15
|
-
* format. Callers can then keep request-local safety behavior while skipping
|
|
16
|
-
* optional durable surface hygiene.
|
|
16
|
+
* startSeq/endSeq, and reviewed format v4 keeps that shape. Unknown future
|
|
17
|
+
* formats fail closed instead of being guessed from package version.
|
|
17
18
|
*/
|
|
18
|
-
export function sessionSurfaceReplacementIntent(session,
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
19
|
+
export function sessionSurfaceReplacementIntent(session, seqValue) {
|
|
20
|
+
if (!nonNegativeSafeInteger(seqValue)) {
|
|
21
|
+
throw new TypeError('session surface replacement seq must be a non-negative safe integer');
|
|
22
|
+
}
|
|
23
|
+
const seq = seqValue;
|
|
24
|
+
const version = propertyBag(propertyBag(session)?.header)?.version;
|
|
25
|
+
if (!nonNegativeSafeInteger(version))
|
|
26
|
+
return undefined;
|
|
27
|
+
if (version <= 2) {
|
|
28
|
+
return {
|
|
29
|
+
surfaceOp: { op: 'replace', start: seq, end: seq },
|
|
30
|
+
sourceEventSeqs: [seq],
|
|
31
|
+
};
|
|
30
32
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
sourceEventSeqs: [seq],
|
|
33
|
+
if (version === 3 || version === 4) {
|
|
34
|
+
return {
|
|
35
|
+
surfaceOp: { op: 'replace', startSeq: seq, endSeq: seq },
|
|
36
|
+
sourceEventSeqs: [seq],
|
|
37
|
+
};
|
|
37
38
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
return undefined
|
|
39
|
+
return undefined;
|
|
41
40
|
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
const OWNERSHIP = Object.freeze({
|
|
2
|
+
PLUGIN_OWNED: 'vision-router-owned',
|
|
3
|
+
NATIVE: 'native-image',
|
|
4
|
+
TEXT_ONLY: 'text-only',
|
|
5
|
+
UNKNOWN: 'unknown',
|
|
6
|
+
});
|
|
7
|
+
const KNOWN_OWNERSHIP = new Set(Object.values(OWNERSHIP));
|
|
8
|
+
function objectRecord(value) {
|
|
9
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
10
|
+
? value
|
|
11
|
+
: undefined;
|
|
12
|
+
}
|
|
13
|
+
function normalizedOwnership(policy) {
|
|
14
|
+
const source = objectRecord(policy);
|
|
15
|
+
if (source === undefined)
|
|
16
|
+
return undefined;
|
|
17
|
+
return typeof source.ownership === 'string'
|
|
18
|
+
&& KNOWN_OWNERSHIP.has(source.ownership)
|
|
19
|
+
? source.ownership
|
|
20
|
+
: OWNERSHIP.UNKNOWN;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Resolve one immutable Core-facing capability snapshot for the current Session.
|
|
24
|
+
*
|
|
25
|
+
* Image ownership and Vision-mode authority remain separate facts. Durable
|
|
26
|
+
* transcript preservation is fail-safe: every real Session policy preserves
|
|
27
|
+
* the user-owned image, while the step authority alone decides whether DVR
|
|
28
|
+
* automatic/tool surfaces are active.
|
|
29
|
+
*/
|
|
30
|
+
export function resolveSessionSurfacePolicy({ visionPolicy, visionModeAuthority, config = {}, schemaBootstrapping = false, } = {}) {
|
|
31
|
+
const source = objectRecord(visionPolicy);
|
|
32
|
+
const authority = objectRecord(visionModeAuthority);
|
|
33
|
+
const value = objectRecord(config) ?? {};
|
|
34
|
+
const ownership = normalizedOwnership(source);
|
|
35
|
+
const pluginOwned = ownership === OWNERSHIP.PLUGIN_OWNED;
|
|
36
|
+
const visionModeEnabled = authority !== undefined
|
|
37
|
+
? authority.enabled === true
|
|
38
|
+
: source === undefined
|
|
39
|
+
? true
|
|
40
|
+
: pluginOwned;
|
|
41
|
+
const preserveRawImages = source !== undefined;
|
|
42
|
+
const rewriteCurrentImages = false;
|
|
43
|
+
const allowStructuredBootstrap = visionModeEnabled;
|
|
44
|
+
const allowGenericAutoMount = visionModeEnabled;
|
|
45
|
+
const surface = Object.freeze({
|
|
46
|
+
preserveRawImages,
|
|
47
|
+
rewriteCurrentImages,
|
|
48
|
+
visionTools: value.tool !== false && visionModeEnabled,
|
|
49
|
+
structuredBootstrap: value.structuredVisionBootstrap === true && visionModeEnabled,
|
|
50
|
+
genericAutoMount: value.autoActivateOnImage !== false && visionModeEnabled,
|
|
51
|
+
instantDescribe: value.instantDescribe !== false && visionModeEnabled,
|
|
52
|
+
});
|
|
53
|
+
const overrides = {};
|
|
54
|
+
if (schemaBootstrapping === true
|
|
55
|
+
&& source === undefined
|
|
56
|
+
&& value.tool === false) {
|
|
57
|
+
overrides.tool = true;
|
|
58
|
+
}
|
|
59
|
+
if (source !== undefined && value.rewriteImages !== false) {
|
|
60
|
+
overrides.rewriteImages = false;
|
|
61
|
+
}
|
|
62
|
+
if (source !== undefined && !visionModeEnabled) {
|
|
63
|
+
if (value.tool !== false)
|
|
64
|
+
overrides.tool = false;
|
|
65
|
+
if (value.instantDescribe !== false)
|
|
66
|
+
overrides.instantDescribe = false;
|
|
67
|
+
if (value.autoActivateOnImage !== false) {
|
|
68
|
+
overrides.autoActivateOnImage = false;
|
|
69
|
+
}
|
|
70
|
+
if (value.structuredVisionBootstrap !== false) {
|
|
71
|
+
overrides.structuredVisionBootstrap = false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return Object.freeze({
|
|
75
|
+
ownership,
|
|
76
|
+
visionModeEnabled,
|
|
77
|
+
participates: source !== undefined,
|
|
78
|
+
preserveRawImages,
|
|
79
|
+
rewriteCurrentImages,
|
|
80
|
+
allowStructuredBootstrap,
|
|
81
|
+
allowGenericAutoMount,
|
|
82
|
+
surface,
|
|
83
|
+
legacyConfigOverrides: Object.freeze(overrides),
|
|
84
|
+
});
|
|
85
|
+
}
|