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.
Files changed (103) hide show
  1. package/README.md +12 -3
  2. package/README.zh.md +12 -3
  3. package/docs/architecture/3.0-architecture-plan.md +252 -0
  4. package/docs/architecture/3.0-r0-contract-ledger.md +137 -0
  5. package/docs/architecture/3.0-r1-source-artifact-boundary.md +98 -0
  6. package/docs/architecture/3.0-r10-final-correctness-closure.md +207 -0
  7. package/docs/architecture/3.0-r11-adversarial-review.md +441 -0
  8. package/docs/architecture/3.0-r5-browser-go-no-go.md +165 -0
  9. package/docs/architecture/3.0-r5-browser-typescript-gate.md +105 -0
  10. package/docs/architecture/3.0-r6-compat-retirement.md +203 -0
  11. package/docs/architecture/3.0-r7-public-api.md +151 -0
  12. package/docs/architecture/3.0-r8-ci-release.md +248 -0
  13. package/docs/architecture/3.0-r9-final-audit.md +370 -0
  14. package/docs/architecture/dsh-support-window.md +5 -3
  15. package/docs/releases/v3.0.0.md +85 -0
  16. package/entry.js +1 -1
  17. package/index.js +108 -71
  18. package/lib/agent-request-route-authority.js +1 -20
  19. package/lib/core-apply-lifecycle.js +37 -43
  20. package/lib/core-cache.js +87 -0
  21. package/lib/core-primitives.js +3 -143
  22. package/lib/core-vision-surface-domain.js +63 -0
  23. package/lib/core-vision-surface.js +11 -77
  24. package/lib/delegated-call-config.js +15 -24
  25. package/lib/doctor-cli.js +0 -0
  26. package/lib/doctor-runtime.js +4 -1
  27. package/lib/dsh-contract-compat.js +26 -230
  28. package/lib/dsh-host-capabilities.js +3 -9
  29. package/lib/dsh-settings-017-compat.js +17 -10
  30. package/lib/file-logger.js +8 -3
  31. package/lib/grounding-coordinate-runtime.js +1 -2
  32. package/lib/http-body-limit.js +97 -79
  33. package/lib/image-offload-compat.js +30 -21
  34. package/lib/image-resource-governor.js +210 -228
  35. package/lib/legacy-global-proxy-boundary.js +46 -3
  36. package/lib/live-model-client-prelude.js +12 -4
  37. package/lib/live-model-discovery.js +134 -43
  38. package/lib/local-vision-stabilizer.js +31 -17
  39. package/lib/maintenance-routes.js +1 -3
  40. package/lib/native-image-coexistence.js +15 -3
  41. package/lib/ollama-cold-start.js +87 -23
  42. package/lib/pi-ai-bridge-wire-compat.js +3 -2
  43. package/lib/proxy-dispatcher-pool.js +151 -92
  44. package/lib/proxy-routing.js +75 -51
  45. package/lib/proxy-url-compat.js +3 -10
  46. package/lib/public-config.js +23 -44
  47. package/lib/public-entry.d.ts +148 -0
  48. package/lib/public-entry.js +17 -4
  49. package/lib/replay-delegation.js +12 -7
  50. package/lib/replay-envelope-v2-compat.js +12 -7
  51. package/lib/runtime-composition.js +22 -4
  52. package/lib/screenshot-source-boundary.js +2 -2
  53. package/lib/session-affinity-runtime.js +113 -97
  54. package/lib/session-affinity.js +89 -76
  55. package/lib/session-event-history-compat.js +30 -0
  56. package/lib/session-query-readers.js +263 -0
  57. package/lib/session-surface-compat.js +30 -31
  58. package/lib/session-surface-policy-domain.js +85 -0
  59. package/lib/session-surface-policy.js +2 -111
  60. package/lib/session-turn-resolver.js +2 -6
  61. package/lib/session-vision-attachment-index.js +143 -0
  62. package/lib/session-vision-delegation-boundary.js +34 -0
  63. package/lib/session-vision-delegation.js +22 -0
  64. package/lib/session-vision-index.js +25 -107
  65. package/lib/session-vision-mode-authority.js +229 -192
  66. package/lib/session-vision-mode-boundary.js +65 -22
  67. package/lib/session-vision-policy-domain.js +304 -0
  68. package/lib/session-vision-policy.js +104 -0
  69. package/lib/session-vision-state.js +368 -378
  70. package/lib/settings-client-017-compat.js +9 -7
  71. package/lib/structured-flow-hardening.js +22 -4
  72. package/lib/tesseract-exec-compat.js +9 -1
  73. package/lib/turn-budget-context.js +47 -41
  74. package/lib/update-check.js +11 -2
  75. package/lib/vision-attachment-handle-runtime.js +176 -156
  76. package/lib/vision-backend-runtime-policy.js +16 -0
  77. package/lib/vision-background-benchmark.js +41 -4
  78. package/lib/vision-background-failure-policy.js +41 -41
  79. package/lib/vision-background-stop-store-core.js +217 -0
  80. package/lib/vision-background-stop-store.js +8 -179
  81. package/lib/vision-capability-benchmark.js +11 -61
  82. package/lib/vision-capability-evidence.js +35 -0
  83. package/lib/vision-capability-fingerprint.js +72 -0
  84. package/lib/vision-capability-identity.js +116 -61
  85. package/lib/vision-capability-router.js +13 -33
  86. package/lib/vision-chunk-assembler.js +95 -0
  87. package/lib/vision-diagnostics-routes.js +34 -4
  88. package/lib/vision-execution-order-apply.js +45 -55
  89. package/lib/vision-execution-order.js +19 -19
  90. package/lib/vision-execution-policy.js +12 -7
  91. package/lib/vision-model-registry.js +30 -17
  92. package/lib/vision-provider-transport.js +137 -147
  93. package/lib/vision-routing-authority.js +47 -48
  94. package/lib/vision-routing-product.js +26 -29
  95. package/lib/vision-routing-settings-prelude.js +8 -3
  96. package/lib/vision-runtime-performance-store.js +166 -0
  97. package/lib/vision-runtime-performance.js +13 -142
  98. package/lib/vision-tool-runtime-boundary.js +99 -30
  99. package/lib/web/dsh-settings-017-root-transport.js +27 -12
  100. package/lib/web/remote-settings-client.js +4 -7
  101. package/lib/web-capability-boundary.js +17 -7
  102. package/lib/wrapper-directory.js +14 -23
  103. package/package.json +37 -11
@@ -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
+ }
@@ -1,117 +1,8 @@
1
1
  import { currentSessionVisionPolicy } from './native-image-coexistence.js'
2
2
  import { currentSessionVisionModeAuthority } from './session-vision-mode-authority.js'
3
+ import { resolveSessionSurfacePolicy } from './session-surface-policy-domain.js'
3
4
 
4
- const OWNERSHIP = Object.freeze({
5
- PLUGIN_OWNED: 'vision-router-owned',
6
- NATIVE: 'native-image',
7
- TEXT_ONLY: 'text-only',
8
- UNKNOWN: 'unknown',
9
- })
10
-
11
- const KNOWN_OWNERSHIP = new Set(Object.values(OWNERSHIP))
12
-
13
- function isObject(value) {
14
- return value !== null && typeof value === 'object' && !Array.isArray(value)
15
- }
16
-
17
- function normalizedOwnership(policy) {
18
- if (!isObject(policy)) return undefined
19
- return KNOWN_OWNERSHIP.has(policy.ownership) ? policy.ownership : OWNERSHIP.UNKNOWN
20
- }
21
-
22
- /**
23
- * Resolve one immutable, read-only snapshot of the Vision Router capability
24
- * surface for the current session.
25
- *
26
- * Image ownership and Vision-mode authority are intentionally distinct. The
27
- * native-image policy answers who can consume raw pixels; the mode authority
28
- * answers whether this Session explicitly selected a Vision Router-owned
29
- * wrapper/twin for the current step. Only the latter can grant plugin tools or
30
- * automatic visual work. This prevents a historical image, a native multimodal
31
- * source model, or a stale request header from silently turning Vision back on.
32
- *
33
- * Durable transcript invariant: once a real session policy exists, Core may
34
- * observe/index image blocks but may never replace a user-owned image message
35
- * before the Agent loop appends it to the Session log. Image projection belongs
36
- * at the adapter/request boundary. The historical `rewriteCurrentImages` bit is
37
- * therefore intentionally ignored even when an older policy producer still
38
- * exposes it; the compatibility field below is permanently false.
39
- */
40
- export function resolveSessionSurfacePolicy({
41
- visionPolicy,
42
- visionModeAuthority,
43
- config = {},
44
- schemaBootstrapping = false,
45
- } = {}) {
46
- const source = isObject(visionPolicy) ? visionPolicy : undefined
47
- const authority = isObject(visionModeAuthority) ? visionModeAuthority : undefined
48
- const value = isObject(config) ? config : {}
49
- const ownership = normalizedOwnership(source)
50
- const pluginOwned = ownership === OWNERSHIP.PLUGIN_OWNED
51
-
52
- // During schema bootstrap there is no real Session policy, so global Settings
53
- // still decide which definitions Core registers. During a real step, an
54
- // explicit authority snapshot wins; direct helper callers fall back to the
55
- // already-classified plugin ownership instead of inventing an enabled mode.
56
- const visionModeEnabled = authority !== undefined
57
- ? authority.enabled === true
58
- : source === undefined
59
- ? true
60
- : pluginOwned
61
-
62
- // No active session policy means no session-specific preservation authority.
63
- // During a real pre-step, every ownership result — including explicit
64
- // text-only and unknown — preserves the durable user message unchanged.
65
- const preserveRawImages = source !== undefined
66
- const rewriteCurrentImages = false
67
- const allowStructuredBootstrap = visionModeEnabled
68
- const allowGenericAutoMount = visionModeEnabled
69
-
70
- const surface = Object.freeze({
71
- preserveRawImages,
72
- rewriteCurrentImages,
73
- visionTools: value.tool !== false && visionModeEnabled,
74
- structuredBootstrap:
75
- value.structuredVisionBootstrap === true && visionModeEnabled,
76
- genericAutoMount:
77
- value.autoActivateOnImage !== false && visionModeEnabled,
78
- instantDescribe:
79
- value.instantDescribe !== false && visionModeEnabled,
80
- })
81
-
82
- const overrides = {}
83
- if (schemaBootstrapping === true && source === undefined && value.tool === false) {
84
- overrides.tool = true
85
- }
86
- // Core's historical rewriteImages switch controls an agent/pre-step message
87
- // transform. Disable it for every real session policy so neither current nor
88
- // historical user image blocks can be persisted as internal attachment text.
89
- // Vision Router-owned adapters still perform their private request projection.
90
- if (source !== undefined && value.rewriteImages !== false) overrides.rewriteImages = false
91
-
92
- // The composer/model selection is the Session authority. When it is OFF,
93
- // suppress every automatic/plugin-owned visual surface while preserving the
94
- // durable image itself. Settings remain unchanged; these are turn-local Core
95
- // projections only.
96
- if (source !== undefined && !visionModeEnabled) {
97
- if (value.tool !== false) overrides.tool = false
98
- if (value.instantDescribe !== false) overrides.instantDescribe = false
99
- if (value.autoActivateOnImage !== false) overrides.autoActivateOnImage = false
100
- if (value.structuredVisionBootstrap !== false) overrides.structuredVisionBootstrap = false
101
- }
102
-
103
- return Object.freeze({
104
- ownership,
105
- visionModeEnabled,
106
- participates: source !== undefined,
107
- preserveRawImages,
108
- rewriteCurrentImages,
109
- allowStructuredBootstrap,
110
- allowGenericAutoMount,
111
- surface,
112
- legacyConfigOverrides: Object.freeze(overrides),
113
- })
114
- }
5
+ export { resolveSessionSurfacePolicy } from './session-surface-policy-domain.js'
115
6
 
116
7
  /** Read the turn-local ownership + mode snapshots and resolve the Core surface. */
117
8
  export function currentSessionSurfacePolicy(config = {}, options = {}) {
@@ -1,10 +1,6 @@
1
1
  function projectionServiceOf(ctx) {
2
- try {
3
- const service = ctx?.sessionProjections
4
- if (service && typeof service.stateOf === 'function') return service
5
- } catch {
6
- // Some compatibility contexts expose services only through get().
7
- }
2
+ // sessionProjections is optional here; probing the Cordis service property
3
+ // outside inject() would turn this reader into a hard pending dependency.
8
4
  try {
9
5
  const service = ctx?.get?.('sessionProjections')
10
6
  return service && typeof service.stateOf === 'function' ? service : undefined