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.
Files changed (132) hide show
  1. package/README.md +16 -7
  2. package/README.zh.md +16 -7
  3. package/docs/architecture/2.3.0-compat-retirement.md +147 -0
  4. package/docs/architecture/2.3.0-execution-plan.md +410 -0
  5. package/docs/architecture/2.3.0-final-audit.md +279 -0
  6. package/docs/architecture/2.3.0-reopened-plan.md +720 -0
  7. package/docs/architecture/3.0-architecture-plan.md +252 -0
  8. package/docs/architecture/3.0-r0-contract-ledger.md +137 -0
  9. package/docs/architecture/3.0-r1-source-artifact-boundary.md +98 -0
  10. package/docs/architecture/3.0-r10-final-correctness-closure.md +207 -0
  11. package/docs/architecture/3.0-r11-adversarial-review.md +441 -0
  12. package/docs/architecture/3.0-r5-browser-go-no-go.md +165 -0
  13. package/docs/architecture/3.0-r5-browser-typescript-gate.md +105 -0
  14. package/docs/architecture/3.0-r6-compat-retirement.md +203 -0
  15. package/docs/architecture/3.0-r7-public-api.md +151 -0
  16. package/docs/architecture/3.0-r8-ci-release.md +248 -0
  17. package/docs/architecture/3.0-r9-final-audit.md +370 -0
  18. package/docs/architecture/compat-inventory.md +26 -34
  19. package/docs/architecture/dsh-compatibility-matrix.md +65 -54
  20. package/docs/architecture/dsh-support-window.md +30 -22
  21. package/docs/architecture/final-closure-audit.md +2 -0
  22. package/docs/releases/v2.3.0.md +53 -0
  23. package/docs/releases/v3.0.0.md +85 -0
  24. package/entry.js +9 -57
  25. package/index.js +268 -761
  26. package/lib/agent-request-route-authority.js +1 -20
  27. package/lib/catalog-corrections.js +1 -2
  28. package/lib/client-maintenance-actions.js +145 -0
  29. package/lib/client-presentation-boundary-main.js +75 -17
  30. package/lib/client-presentation-boundary.js +53 -28
  31. package/lib/client.js +246 -144
  32. package/lib/core-apply-lifecycle.js +41 -0
  33. package/lib/core-cache.js +87 -0
  34. package/lib/core-primitives.js +91 -278
  35. package/lib/core-vision-surface-domain.js +63 -0
  36. package/lib/core-vision-surface.js +11 -77
  37. package/lib/delegated-call-config.js +15 -24
  38. package/lib/desktop-screenshot-tool.js +262 -0
  39. package/lib/doctor-cli.js +0 -0
  40. package/lib/doctor-runtime.js +4 -1
  41. package/lib/dsh-contract-compat.js +37 -256
  42. package/lib/dsh-host-capabilities.js +3 -9
  43. package/lib/dsh-settings-017-compat.js +17 -10
  44. package/lib/dsh-support-window.js +3 -3
  45. package/lib/file-logger.js +8 -3
  46. package/lib/grounding-coordinate-runtime.js +1 -2
  47. package/lib/http-body-limit.js +97 -79
  48. package/lib/http-compat.js +1 -2
  49. package/lib/image-offload-compat.js +30 -21
  50. package/lib/image-resource-governor.js +210 -228
  51. package/lib/legacy-global-proxy-boundary.js +46 -3
  52. package/lib/live-model-client-prelude.js +12 -4
  53. package/lib/live-model-discovery.js +134 -43
  54. package/lib/local-remote-settings-permission.js +33 -7
  55. package/lib/local-vision-stabilizer.js +31 -17
  56. package/lib/maintenance-routes.js +134 -0
  57. package/lib/native-image-coexistence.js +15 -3
  58. package/lib/ollama-cold-start.js +87 -23
  59. package/lib/pi-ai-bridge-wire-compat.js +3 -2
  60. package/lib/proxy-dispatcher-pool.js +151 -92
  61. package/lib/proxy-routing.js +75 -51
  62. package/lib/proxy-url-compat.js +3 -10
  63. package/lib/public-config.js +36 -0
  64. package/lib/public-entry.d.ts +148 -0
  65. package/lib/public-entry.js +19 -11
  66. package/lib/remote-settings-bridge.js +11 -0
  67. package/lib/remote-settings-risk-client-boundary.js +155 -0
  68. package/lib/remote-settings-risk-confirmation.js +17 -105
  69. package/lib/replay-delegation.js +12 -7
  70. package/lib/replay-envelope-v2-compat.js +12 -7
  71. package/lib/runtime-composition.js +127 -34
  72. package/lib/runtime-i18n-boundary.js +10 -11
  73. package/lib/screenshot-source-boundary.js +2 -2
  74. package/lib/session-affinity-runtime.js +113 -97
  75. package/lib/session-affinity.js +89 -76
  76. package/lib/session-event-history-compat.js +30 -0
  77. package/lib/session-message-source-compat.js +7 -3
  78. package/lib/session-query-readers.js +263 -0
  79. package/lib/session-surface-compat.js +30 -31
  80. package/lib/session-surface-policy-domain.js +85 -0
  81. package/lib/session-surface-policy.js +2 -111
  82. package/lib/session-turn-resolver.js +2 -6
  83. package/lib/session-vision-attachment-index.js +143 -0
  84. package/lib/session-vision-delegation-boundary.js +34 -0
  85. package/lib/session-vision-delegation.js +22 -0
  86. package/lib/session-vision-index.js +179 -116
  87. package/lib/session-vision-mode-authority.js +229 -192
  88. package/lib/session-vision-mode-boundary.js +65 -22
  89. package/lib/session-vision-policy-domain.js +304 -0
  90. package/lib/session-vision-policy.js +104 -0
  91. package/lib/session-vision-state.js +368 -378
  92. package/lib/settings-client-017-compat.js +9 -7
  93. package/lib/settings-client-loader-lifecycle.js +129 -0
  94. package/lib/sharp-runtime.js +0 -7
  95. package/lib/structured-flow-hardening.js +25 -6
  96. package/lib/tesseract-exec-compat.js +9 -1
  97. package/lib/turn-budget-context.js +47 -41
  98. package/lib/update-check.js +11 -2
  99. package/lib/vision-attachment-handle-runtime.js +176 -156
  100. package/lib/vision-backend-runtime-policy.js +19 -2
  101. package/lib/vision-background-benchmark.js +41 -4
  102. package/lib/vision-background-failure-policy.js +41 -41
  103. package/lib/vision-background-stop-store-core.js +217 -0
  104. package/lib/vision-background-stop-store.js +8 -179
  105. package/lib/vision-capability-benchmark-service.js +1 -0
  106. package/lib/vision-capability-benchmark.js +11 -61
  107. package/lib/vision-capability-evidence.js +35 -0
  108. package/lib/vision-capability-fingerprint.js +72 -0
  109. package/lib/vision-capability-identity.js +116 -61
  110. package/lib/vision-capability-router.js +13 -33
  111. package/lib/vision-chunk-assembler.js +95 -0
  112. package/lib/vision-diagnostics-routes.js +246 -0
  113. package/lib/vision-evidence-guidance.js +3 -1
  114. package/lib/vision-execution-order-apply.js +45 -55
  115. package/lib/vision-execution-order.js +19 -19
  116. package/lib/vision-execution-policy.js +12 -7
  117. package/lib/vision-model-registry.js +30 -17
  118. package/lib/vision-provider-transport.js +137 -176
  119. package/lib/vision-routing-authority.js +47 -48
  120. package/lib/vision-routing-product.js +26 -29
  121. package/lib/vision-routing-settings-prelude.js +8 -3
  122. package/lib/vision-runtime-performance-store.js +166 -0
  123. package/lib/vision-runtime-performance.js +13 -142
  124. package/lib/vision-toggle-root-hardening.js +22 -5
  125. package/lib/vision-tool-runtime-boundary.js +104 -31
  126. package/lib/web/dsh-settings-017-root-transport.js +27 -12
  127. package/lib/web/remote-settings-client.js +6 -9
  128. package/lib/web-capability-boundary.js +17 -7
  129. package/lib/wrapper-directory.js +14 -23
  130. package/package.json +56 -20
  131. package/lib/android-attachment-compat.js +0 -244
  132. package/lib/settings-client-rc8-lifecycle.js +0 -397
@@ -1,93 +1,106 @@
1
- const MAX_SESSION_AFFINITY_ID_LENGTH = 512
2
-
3
- /** Preserve DSH's in-process session identity exactly; no wire rules belong here. */
1
+ const MAX_SESSION_AFFINITY_ID_LENGTH = 512;
2
+ function propertyBag(value) {
3
+ return value !== null && (typeof value === 'object' || typeof value === 'function')
4
+ ? value
5
+ : undefined;
6
+ }
7
+ function wireAsciiIdentity(text) {
8
+ for (let index = 0; index < text.length; index += 1) {
9
+ const code = text.charCodeAt(index);
10
+ const edge = index === 0 || index === text.length - 1;
11
+ if (edge) {
12
+ if (code < 33 || code > 126)
13
+ return false;
14
+ }
15
+ else if (code < 32 || code > 126) {
16
+ return false;
17
+ }
18
+ }
19
+ return true;
20
+ }
4
21
  export function rawSessionIdentity(value) {
5
- if (value === undefined || value === null) return undefined
6
- try {
7
- return String(value)
8
- } catch {
9
- return undefined
10
- }
22
+ if (value === undefined || value === null)
23
+ return undefined;
24
+ try {
25
+ return String(value);
26
+ }
27
+ catch {
28
+ return undefined;
29
+ }
11
30
  }
12
-
13
- /** Read the live DSH session id across released/alpha session shapes without rewriting it. */
14
31
  export function sessionIdentityOf(session) {
15
- if (!session) return undefined
16
- try {
17
- const direct = rawSessionIdentity(session.id)
18
- if (direct !== undefined) return direct
19
- } catch {
20
- /* continue to persisted/header shapes */
21
- }
22
- try {
23
- const header = rawSessionIdentity(session.header?.id)
24
- if (header !== undefined) return header
25
- } catch {
26
- /* continue to requestHeader() */
27
- }
28
- try {
29
- if (typeof session.requestHeader === 'function') {
30
- return rawSessionIdentity(session.requestHeader()?.id)
32
+ const source = propertyBag(session);
33
+ if (source === undefined)
34
+ return undefined;
35
+ try {
36
+ const direct = rawSessionIdentity(source.id);
37
+ if (direct !== undefined)
38
+ return direct;
31
39
  }
32
- } catch {
33
- /* no usable session identity */
34
- }
35
- return undefined
40
+ catch { }
41
+ try {
42
+ const header = rawSessionIdentity(propertyBag(source.header)?.id);
43
+ if (header !== undefined)
44
+ return header;
45
+ }
46
+ catch { }
47
+ try {
48
+ const requestHeader = source.requestHeader;
49
+ if (typeof requestHeader === 'function') {
50
+ return rawSessionIdentity(propertyBag(requestHeader.call(session))?.id);
51
+ }
52
+ }
53
+ catch { }
54
+ return undefined;
36
55
  }
37
-
38
- /**
39
- * Admit an opaque identity to an HTTP header without changing it. Fetch/undici
40
- * requires ByteString-compatible header values; use visible ASCII so behavior
41
- * is identical across Node 22/24 and HTTP implementations. Leading/trailing
42
- * whitespace is rejected because Headers would normalize it, violating the
43
- * requirement to preserve the DSH identity unchanged.
44
- */
45
56
  export function wireSessionAffinityId(value) {
46
- const text = rawSessionIdentity(value)
47
- if (text === undefined || text === '' || text.length > MAX_SESSION_AFFINITY_ID_LENGTH) return undefined
48
- if (text.trim() !== text) return undefined
49
- if (!/^[\x21-\x7e](?:[\x20-\x7e]*[\x21-\x7e])?$/.test(text)) return undefined
50
- return text
57
+ const text = rawSessionIdentity(value);
58
+ if (text === undefined
59
+ || text === ''
60
+ || text.length > MAX_SESSION_AFFINITY_ID_LENGTH) {
61
+ return undefined;
62
+ }
63
+ if (text.trim() !== text)
64
+ return undefined;
65
+ return wireAsciiIdentity(text) ? text : undefined;
51
66
  }
52
-
53
67
  export function isOfficialOpenCodeGoUrl(value) {
54
- try {
55
- const url = new URL(String(value))
56
- return (
57
- url.protocol === 'https:' &&
58
- url.hostname.toLowerCase() === 'opencode.ai' &&
59
- /^\/zen\/go(?:\/|$)/i.test(url.pathname)
60
- )
61
- } catch {
62
- return false
63
- }
68
+ try {
69
+ const url = new URL(String(value));
70
+ return (url.protocol === 'https:'
71
+ && url.hostname.toLowerCase() === 'opencode.ai'
72
+ && /^\/zen\/go(?:\/|$)/i.test(url.pathname));
73
+ }
74
+ catch {
75
+ return false;
76
+ }
64
77
  }
65
-
66
78
  export function isOpenCodeGoProvider(provider) {
67
- return isOfficialOpenCodeGoUrl(provider?.baseURL ?? provider?.baseUrl ?? '')
79
+ const source = propertyBag(provider);
80
+ return isOfficialOpenCodeGoUrl(source?.baseURL ?? source?.baseUrl ?? '');
68
81
  }
69
-
70
82
  function invalidAffinityError(sessionId) {
71
- const raw = rawSessionIdentity(sessionId)
72
- const error = new Error(
73
- raw === undefined || raw === ''
74
- ? 'OpenCode Go requires a stable session identity; refusing a direct request without one'
75
- : 'OpenCode Go session identity is not safe for an HTTP header; refusing to rewrite or truncate it',
76
- )
77
- error.code = raw === undefined || raw === '' ? 'OPENCODE_SESSION_REQUIRED' : 'OPENCODE_SESSION_INVALID'
78
- return error
83
+ const raw = rawSessionIdentity(sessionId);
84
+ const code = raw === undefined || raw === ''
85
+ ? 'OPENCODE_SESSION_REQUIRED'
86
+ : 'OPENCODE_SESSION_INVALID';
87
+ return Object.assign(new Error(code === 'OPENCODE_SESSION_REQUIRED'
88
+ ? 'OpenCode Go requires a stable session identity; refusing a direct request without one'
89
+ : 'OpenCode Go session identity is not safe for an HTTP header; refusing to rewrite or truncate it'), { code });
79
90
  }
80
-
81
91
  export function directSessionAffinityHeaders(provider, sessionId) {
82
- if (!isOpenCodeGoProvider(provider)) return {}
83
- const wire = wireSessionAffinityId(sessionId)
84
- if (wire === undefined) throw invalidAffinityError(sessionId)
85
- return { 'x-opencode-session': wire }
92
+ if (!isOpenCodeGoProvider(provider))
93
+ return {};
94
+ const wire = wireSessionAffinityId(sessionId);
95
+ if (wire === undefined)
96
+ throw invalidAffinityError(sessionId);
97
+ return { 'x-opencode-session': wire };
86
98
  }
87
-
88
99
  export function openCodeSessionAffinityHeaderForUrl(url, sessionId) {
89
- if (!isOfficialOpenCodeGoUrl(url)) return undefined
90
- const wire = wireSessionAffinityId(sessionId)
91
- if (wire === undefined) throw invalidAffinityError(sessionId)
92
- return wire
100
+ if (!isOfficialOpenCodeGoUrl(url))
101
+ return undefined;
102
+ const wire = wireSessionAffinityId(sessionId);
103
+ if (wire === undefined)
104
+ throw invalidAffinityError(sessionId);
105
+ return wire;
93
106
  }
@@ -0,0 +1,30 @@
1
+ function propertyBag(value) {
2
+ return value !== null && (typeof value === 'object' || typeof value === 'function')
3
+ ? value
4
+ : undefined;
5
+ }
6
+ /**
7
+ * Read the released Session-local event snapshot used only when the Host does
8
+ * not advertise the asynchronous SessionQuery capability.
9
+ *
10
+ * Durable-data compatibility is intentionally separate from the current Host
11
+ * floor: this helper feature-detects historical Session shapes at runtime and
12
+ * never claims that either shape exists through a static Host type.
13
+ */
14
+ export function legacySessionEvents(session) {
15
+ const source = propertyBag(session);
16
+ if (source === undefined)
17
+ return undefined;
18
+ try {
19
+ const snapshotEvents = source.snapshotEvents;
20
+ if (typeof snapshotEvents === 'function') {
21
+ const events = snapshotEvents.call(session);
22
+ return Array.isArray(events) ? events : undefined;
23
+ }
24
+ const events = source.events;
25
+ return Array.isArray(events) ? events : undefined;
26
+ }
27
+ catch {
28
+ return undefined;
29
+ }
30
+ }
@@ -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. Keep
66
- * every existing DVR message producer unchanged and normalize at the final
67
- * hook publication boundary using the Session's own durable header version.
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
- return Number.isSafeInteger(value) && value >= 0 && !Object.is(value, -0)
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 replacement shape. The
10
- * field change belongs to the Session format contract,
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, seq) {
19
- if (!nonNegativeSafeInteger(seq)) {
20
- throw new TypeError('session surface replacement seq must be a non-negative safe integer')
21
- }
22
-
23
- const version = session?.header?.version
24
- if (!nonNegativeSafeInteger(version)) return undefined
25
-
26
- if (version <= 2) {
27
- return {
28
- surfaceOp: { op: 'replace', start: seq, end: seq },
29
- sourceEventSeqs: [seq],
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
- if (version === 3 || version === 4) {
34
- return {
35
- surfaceOp: { op: 'replace', startSeq: seq, endSeq: seq },
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
+ }