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,63 @@
1
+ import { resolveSessionSurfacePolicy, } from './session-surface-policy-domain.js';
2
+ export const CORE_VISION_SURFACE_KEYS = [
3
+ 'tool',
4
+ 'rewriteImages',
5
+ 'instantDescribe',
6
+ 'autoActivateOnImage',
7
+ 'structuredVisionBootstrap',
8
+ ];
9
+ function objectRecord(value) {
10
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
11
+ ? value
12
+ : undefined;
13
+ }
14
+ function own(object, key) {
15
+ return Object.prototype.hasOwnProperty.call(object, key);
16
+ }
17
+ function coreFlags(values) {
18
+ return {
19
+ toolAvailable: values.tool !== false,
20
+ rewriteEnabled: values.rewriteImages !== false,
21
+ instantDescribe: values.instantDescribe === true,
22
+ autoActivateOnImage: values.autoActivateOnImage !== false,
23
+ structuredBootstrap: values.structuredVisionBootstrap === true,
24
+ };
25
+ }
26
+ /**
27
+ * Explicit Core-facing projection of SessionSurfacePolicy.
28
+ *
29
+ * Only the five historical Core switches cross this boundary. Unrelated plugin
30
+ * Settings cannot masquerade as a Core config/Settings object.
31
+ */
32
+ export function projectCoreVisionSurface(config = {}, sessionPolicy) {
33
+ const value = objectRecord(config) ?? {};
34
+ const policyRecord = objectRecord(sessionPolicy);
35
+ const policy = policyRecord !== undefined
36
+ ? policyRecord
37
+ : resolveSessionSurfacePolicy({ config: value });
38
+ const overrides = objectRecord(policy.legacyConfigOverrides) ?? {};
39
+ const projected = {};
40
+ for (const key of CORE_VISION_SURFACE_KEYS) {
41
+ projected[key] = own(overrides, key)
42
+ ? overrides[key]
43
+ : value[key];
44
+ }
45
+ const values = Object.freeze(projected);
46
+ return Object.freeze({
47
+ ownership: typeof policy.ownership === 'string'
48
+ ? policy.ownership
49
+ : undefined,
50
+ preserveRawImages: policy.preserveRawImages === true,
51
+ rewriteCurrentImages: policy.rewriteCurrentImages === true,
52
+ ...coreFlags(values),
53
+ values,
54
+ });
55
+ }
56
+ export function resolveCoreVisionSurface({ visionPolicy, config = {}, schemaBootstrapping = false, } = {}) {
57
+ const value = objectRecord(config) ?? {};
58
+ return projectCoreVisionSurface(value, resolveSessionSurfacePolicy({
59
+ visionPolicy,
60
+ config: value,
61
+ schemaBootstrapping,
62
+ }));
63
+ }
@@ -1,82 +1,20 @@
1
+ import { currentSessionSurfacePolicy } from './session-surface-policy.js'
1
2
  import {
2
- currentSessionSurfacePolicy,
3
- resolveSessionSurfacePolicy,
4
- } from './session-surface-policy.js'
3
+ CORE_VISION_SURFACE_KEYS,
4
+ projectCoreVisionSurface,
5
+ resolveCoreVisionSurface,
6
+ } from './core-vision-surface-domain.js'
5
7
 
6
- const CORE_SURFACE_KEYS = Object.freeze([
7
- 'tool',
8
- 'rewriteImages',
9
- 'instantDescribe',
10
- 'autoActivateOnImage',
11
- 'structuredVisionBootstrap',
12
- ])
8
+ export {
9
+ CORE_VISION_SURFACE_KEYS,
10
+ projectCoreVisionSurface,
11
+ resolveCoreVisionSurface,
12
+ } from './core-vision-surface-domain.js'
13
13
 
14
14
  function isObject(value) {
15
15
  return value !== null && typeof value === 'object' && !Array.isArray(value)
16
16
  }
17
17
 
18
- function own(object, key) {
19
- return Object.prototype.hasOwnProperty.call(object, key)
20
- }
21
-
22
- function coreFlags(values) {
23
- return {
24
- toolAvailable: values.tool !== false,
25
- rewriteEnabled: values.rewriteImages !== false,
26
- instantDescribe: values.instantDescribe === true,
27
- autoActivateOnImage: values.autoActivateOnImage !== false,
28
- structuredBootstrap: values.structuredVisionBootstrap === true,
29
- }
30
- }
31
-
32
- /**
33
- * Explicit core-facing projection of the SessionSurfacePolicy.
34
- *
35
- * The returned object carries only the five config values that the historical
36
- * Settings/config projection could override plus explicit booleans consumed by
37
- * Core and the already-resolved image ownership facts. It deliberately cannot
38
- * masquerade as a Settings service or a full plugin config object.
39
- */
40
- export function projectCoreVisionSurface(config = {}, sessionPolicy) {
41
- const value = isObject(config) ? config : {}
42
- const policy = sessionPolicy && typeof sessionPolicy === 'object'
43
- ? sessionPolicy
44
- : resolveSessionSurfacePolicy({ config: value })
45
- const overrides = isObject(policy.legacyConfigOverrides)
46
- ? policy.legacyConfigOverrides
47
- : {}
48
- const projected = {}
49
-
50
- for (const key of CORE_SURFACE_KEYS) {
51
- projected[key] = own(overrides, key) ? overrides[key] : value[key]
52
- }
53
-
54
- return Object.freeze({
55
- ownership: policy.ownership,
56
- preserveRawImages: policy.preserveRawImages === true,
57
- rewriteCurrentImages: policy.rewriteCurrentImages === true,
58
- ...coreFlags(projected),
59
- values: Object.freeze(projected),
60
- })
61
- }
62
-
63
- /** Resolve an explicit core surface from an explicit ownership-policy input. */
64
- export function resolveCoreVisionSurface({
65
- visionPolicy,
66
- config = {},
67
- schemaBootstrapping = false,
68
- } = {}) {
69
- const value = isObject(config) ? config : {}
70
- return projectCoreVisionSurface(
71
- value,
72
- resolveSessionSurfacePolicy({
73
- visionPolicy,
74
- config: value,
75
- schemaBootstrapping,
76
- }),
77
- )
78
- }
79
-
80
18
  /** Resolve the current turn-local surface without exposing a Settings facade. */
81
19
  export function currentCoreVisionSurface(config = {}, options = {}) {
82
20
  const value = isObject(config) ? config : {}
@@ -92,9 +30,7 @@ export function currentCoreVisionSurface(config = {}, options = {}) {
92
30
  * One explicit production seam for Core's five policy-derived switches.
93
31
  *
94
32
  * The config source stays live, but schema bootstrap is an internal lifecycle
95
- * bit rather than a fake Settings/config value. Direct Core callers can keep
96
- * their two-argument compatibility path; composition passes this runtime as
97
- * the authoritative production surface.
33
+ * bit rather than a fake Settings/config value.
98
34
  */
99
35
  export function createCoreVisionSurfaceRuntime({ config = {} } = {}) {
100
36
  let schemaBootstrapping = true
@@ -120,5 +56,3 @@ export function createCoreVisionSurfaceRuntime({ config = {} } = {}) {
120
56
  },
121
57
  })
122
58
  }
123
-
124
- export const CORE_VISION_SURFACE_KEYS = CORE_SURFACE_KEYS
@@ -1,27 +1,18 @@
1
1
  const ROUTE_OWNED_CALL_FIELDS = Object.freeze([
2
- 'reasoningEffort',
3
- 'temperature',
4
- 'maxTokens',
5
- 'stop',
6
- ])
7
-
8
- /**
9
- * Project an internal Vision Router delegation onto a different provider/model.
10
- *
11
- * The delegated route owns its own call defaults and compatibility. Carry the
12
- * request payload and lifecycle through, but never leak source-route sampling,
13
- * reasoning or output-limit state into the target adapter. DSH will materialize
14
- * the target adapter's configured defaults when these fields are absent.
15
- *
16
- * Direct transports owned by Vision Router do not use this projection; their
17
- * wire compatibility remains the Router's responsibility.
18
- */
2
+ 'reasoningEffort',
3
+ 'temperature',
4
+ 'maxTokens',
5
+ 'stop',
6
+ ]);
19
7
  export function projectDelegatedCallConfig(options) {
20
- if (!options || typeof options !== 'object' || Array.isArray(options)) return options
21
- if (!ROUTE_OWNED_CALL_FIELDS.some((field) => Object.hasOwn(options, field))) return options
22
- const next = { ...options }
23
- for (const field of ROUTE_OWNED_CALL_FIELDS) delete next[field]
24
- return next
8
+ if (!options || typeof options !== 'object' || Array.isArray(options))
9
+ return options;
10
+ const record = options;
11
+ if (!ROUTE_OWNED_CALL_FIELDS.some((field) => Object.hasOwn(record, field)))
12
+ return options;
13
+ const next = { ...record };
14
+ for (const field of ROUTE_OWNED_CALL_FIELDS)
15
+ delete next[field];
16
+ return next;
25
17
  }
26
-
27
- export const delegatedRouteOwnedCallFields = ROUTE_OWNED_CALL_FIELDS
18
+ export const delegatedRouteOwnedCallFields = ROUTE_OWNED_CALL_FIELDS;
package/lib/doctor-cli.js CHANGED
File without changes
@@ -52,6 +52,10 @@ export async function probeRuntime({
52
52
  dshHome,
53
53
  applicableProfiles = [],
54
54
  timeoutMs = 800,
55
+ // Doctor probes are a CLI surface: they never install a process fetch
56
+ // wrapper and must observe whatever transport the surrounding process
57
+ // currently provides.
58
+ // diagnostics-lint: sealed-process-global
55
59
  fetchImpl = globalThis.fetch,
56
60
  } = {}) {
57
61
  if (typeof fetchImpl !== 'function') throw new TypeError('runtime probe requires fetch')
@@ -220,7 +224,6 @@ function chromiumCandidates(env = process.env) {
220
224
  }
221
225
 
222
226
  const HOST_PACKAGES = [
223
- '@deepseek-ai/dsh-client-runtime',
224
227
  '@deepseek-ai/dsh-client-ui-settings-plugins',
225
228
  '@deepseek-ai/dsh-attachment-local',
226
229
  '@deepseek-ai/dsh-llm-deepseek',
@@ -31,232 +31,19 @@ const VISION_ROUTER_NORMALIZED_BYTES = LEGACY_VISION_ATTACHMENT_BYTES
31
31
  const HOST_CAPABILITIES_PATH = '/_dsh/vision-router/host-capabilities'
32
32
  const capabilityRouteInstalls = new WeakSet()
33
33
 
34
- function sessionQueryOf(ctx) {
35
- if (!ctx || typeof ctx !== 'object') return undefined
36
- try {
37
- if (typeof ctx.get === 'function') {
38
- const query = ctx.get('sessionQuery')
39
- if (query && typeof query === 'object') return query
40
- }
41
- } catch {
42
- // A partial/legacy Host may not expose SessionQuery through ctx.get().
43
- }
44
- try {
45
- const query = ctx.sessionQuery
46
- return query && typeof query === 'object' ? query : undefined
47
- } catch {
48
- return undefined
49
- }
50
- }
51
-
52
- /**
53
- * Build the narrow bounded Session event reader used by durable surface repair.
54
- * DSH rc.8+ exposes `sessionQuery.readEvent({ sessionId, seq })`; resolve the
55
- * service at call time so Host service replacement/hot reload stays visible.
56
- * Missing capability is explicit and lets isolated/legacy callers use their
57
- * existing Session-local compatibility fallback. Once the Host advertises the
58
- * reader, operational failures propagate instead of silently falling back to
59
- * deprecated synchronous Session history.
60
- */
61
- export function createSessionEventReader(ctx) {
62
- return async function readSessionEvent(session, seq) {
63
- const sessionId = session?.id
64
- if (sessionId === undefined || sessionId === null) return { supported: false }
65
- if (!Number.isSafeInteger(seq) || seq < 0 || Object.is(seq, -0)) {
66
- throw new TypeError(`session event seq must be a non-negative safe integer, got ${String(seq)}`)
67
- }
68
- const query = sessionQueryOf(ctx)
69
- if (!query || typeof query.readEvent !== 'function') return { supported: false }
70
- const window = await query.readEvent({ sessionId, seq })
71
- const event = window?.target
72
- if (!event || typeof event !== 'object') {
73
- throw new Error(`sessionQuery.readEvent returned no target for seq ${seq}`)
74
- }
75
- if (event.seq !== undefined && event.seq !== seq) {
76
- throw new Error(`sessionQuery.readEvent returned seq ${String(event.seq)} for requested seq ${seq}`)
77
- }
78
- return { supported: true, event }
79
- }
80
- }
81
-
82
-
83
- const SESSION_EVENT_TAIL_INITIAL_AFTER = 50
84
- const SESSION_EVENT_TAIL_MAX_EVENTS = 4096
85
- const SESSION_EVENT_TAIL_MAX_READS = 128
86
-
87
- function sessionQueryCode(error) {
88
- return typeof error?.code === 'string' ? error.code : undefined
89
- }
90
-
91
- /**
92
- * Read raw Session events strictly after one known committed seq through the
93
- * Host-owned bounded `sessionQuery.readEvent()` window contract.
94
- *
95
- * The reader adapts to deployments that lower SessionQuery's configurable
96
- * `readWindowMax`, never materializes the full Session log, and caps both
97
- * events and Host reads. Missing capability is explicit. Operational failures
98
- * from an advertised capability propagate to the caller; a cap is reported as
99
- * `truncated` so routing can fail safe without a synchronous-history fallback.
100
- */
101
- export function createSessionEventTailReader(ctx) {
102
- let afterHint = SESSION_EVENT_TAIL_INITIAL_AFTER
103
-
104
- const readWindow = async (query, sessionId, seq) => {
105
- let after = afterHint
106
- while (true) {
107
- try {
108
- const request = after > 0 ? { sessionId, seq, after } : { sessionId, seq }
109
- const window = await query.readEvent(request)
110
- afterHint = after
111
- return { window, after }
112
- } catch (error) {
113
- if (sessionQueryCode(error) !== 'SESSION_QUERY_INVALID_WINDOW' || after <= 0) throw error
114
- after = after <= 1 ? 0 : Math.floor(after / 2)
115
- }
116
- }
117
- }
118
-
119
- return async function readSessionEventTail(session, anchorSeq, options = {}) {
120
- const sessionId = session?.id
121
- if (sessionId === undefined || sessionId === null) return { supported: false }
122
- if (!Number.isSafeInteger(anchorSeq) || anchorSeq < 0 || Object.is(anchorSeq, -0)) {
123
- throw new TypeError(`session tail anchor seq must be a non-negative safe integer, got ${String(anchorSeq)}`)
124
- }
125
- const query = sessionQueryOf(ctx)
126
- if (!query || typeof query.readEvent !== 'function') return { supported: false }
127
-
128
- const collect = options?.collect !== false
129
- const events = []
130
- let capturedThroughSeq = anchorSeq
131
- let cursor = anchorSeq
132
- let inspected = 0
133
- let reads = 0
134
-
135
- while (reads < SESSION_EVENT_TAIL_MAX_READS) {
136
- let result
137
- try {
138
- result = await readWindow(query, sessionId, cursor)
139
- } catch (error) {
140
- if (sessionQueryCode(error) === 'SESSION_QUERY_EVENT_NOT_FOUND' && cursor > anchorSeq) {
141
- return { supported: true, events, capturedThroughSeq, truncated: false }
142
- }
143
- throw error
144
- }
145
- reads += 1
146
-
147
- const window = result.window
148
- const target = window?.target
149
- if (!target || typeof target !== 'object' || target.seq !== cursor) {
150
- throw new Error(`sessionQuery.readEvent returned an invalid target for seq ${cursor}`)
151
- }
152
- if (!Array.isArray(window.events) || window.events.length === 0) {
153
- throw new Error(`sessionQuery.readEvent returned no event window for seq ${cursor}`)
154
- }
155
- const windowEvents = window.events
156
- let expectedSeq = cursor
157
- let endSeq = cursor
158
- for (const event of windowEvents) {
159
- const seq = event?.seq
160
- if (!Number.isSafeInteger(seq) || seq !== expectedSeq) {
161
- throw new Error(
162
- `sessionQuery.readEvent returned non-contiguous seq ${String(seq)}; expected ${expectedSeq}`,
163
- )
164
- }
165
- expectedSeq += 1
166
- endSeq = seq
167
- if (seq <= capturedThroughSeq) continue
168
- capturedThroughSeq = seq
169
- inspected += 1
170
- if (collect) events.push(event)
171
- if (inspected >= SESSION_EVENT_TAIL_MAX_EVENTS) {
172
- return { supported: true, events, capturedThroughSeq, truncated: true }
173
- }
174
- }
175
- if (Number.isSafeInteger(window?.endSeq) && window.endSeq !== endSeq) {
176
- throw new Error(
177
- `sessionQuery.readEvent returned endSeq ${String(window.endSeq)} for window ending at ${endSeq}`,
178
- )
179
- }
180
-
181
- if (result.after > 0) {
182
- if (endSeq <= cursor || windowEvents.length < result.after + 1) {
183
- return { supported: true, events, capturedThroughSeq: Math.max(capturedThroughSeq, endSeq), truncated: false }
184
- }
185
- cursor = endSeq
186
- continue
187
- }
188
-
189
- // `readWindowMax: 0` is a valid Host configuration. Walk exact seqs and
190
- // use the typed EVENT_NOT_FOUND result as the logical tail sentinel.
191
- cursor += 1
192
- }
193
-
194
- return { supported: true, events, capturedThroughSeq, truncated: true }
195
- }
196
- }
197
-
198
-
199
- /**
200
- * Build the asynchronous Session-log reader used by cold attachment recovery
201
- * and batched surface repair. Prefer SessionQuery's observation lease when it
202
- * exists: one exact live cut materializes at most one immutable event snapshot,
203
- * rather than cloning the whole live corpus once per requested event (#524).
204
- * Hosts without that capability retain the released readSession() contract.
205
- */
206
- export function createSessionLogReader(ctx) {
207
- return async function readSessionLog(session) {
208
- const sessionId = session?.id
209
- if (sessionId === undefined || sessionId === null) return { supported: false }
210
- const query = sessionQueryOf(ctx)
211
- if (!query) return { supported: false }
212
-
213
- if (typeof query.observeSession === 'function') {
214
- let observation
215
- try {
216
- observation = await query.observeSession(sessionId, { projectionMode: 'none' })
217
- if (!observation || !Array.isArray(observation.events)) {
218
- throw new Error('sessionQuery.observeSession returned no event log')
219
- }
220
- const observedId = observation.header?.id
221
- if (observedId !== undefined && observedId !== sessionId) {
222
- throw new Error(
223
- `sessionQuery.observeSession returned session ${String(observedId)} for requested ${String(sessionId)}`,
224
- )
225
- }
226
- return { supported: true, events: observation.events }
227
- } finally {
228
- const dispose = observation?.[Symbol.dispose]
229
- if (typeof dispose === 'function') dispose.call(observation)
230
- }
231
- }
232
-
233
- if (typeof query.readSession !== 'function') return { supported: false }
234
- const snapshot = await query.readSession(sessionId)
235
- if (!snapshot || !Array.isArray(snapshot.events)) {
236
- throw new Error('sessionQuery.readSession returned no event log')
237
- }
238
- const observedId = snapshot.session?.id
239
- if (observedId !== undefined && observedId !== sessionId) {
240
- throw new Error(
241
- `sessionQuery.readSession returned session ${String(observedId)} for requested ${String(sessionId)}`,
242
- )
243
- }
244
- return { supported: true, events: snapshot.events }
245
- }
246
- }
34
+ export {
35
+ createSessionEventReader,
36
+ createSessionEventTailReader,
37
+ createSessionLogReader,
38
+ } from './session-query-readers.js'
247
39
 
248
40
  function attachmentStoreOf(ctx) {
249
41
  if (!ctx || typeof ctx !== 'object') return undefined
42
+ // attachments is an optional capability at this boundary. Context#get keeps
43
+ // the probe non-owning; lifecycle ownership is handled separately by inject.
250
44
  try {
251
- if (typeof ctx.get === 'function') {
252
- const store = ctx.get('attachments')
253
- if (store && typeof store === 'object') return store
254
- }
255
- } catch {
256
- // A detached/partial test context may not expose the service yet.
257
- }
258
- try {
259
- const store = ctx.attachments
45
+ if (typeof ctx.get !== 'function') return undefined
46
+ const store = ctx.get('attachments')
260
47
  return store && typeof store === 'object' ? store : undefined
261
48
  } catch {
262
49
  return undefined
@@ -564,14 +351,20 @@ export function installSettingsSectionCompat(ctx, namespace, Config, entryConfig
564
351
  hooks.setSource(() => scope.get())
565
352
  hooks.onChange()
566
353
  const disposeWatch = scope.watch(() => hooks.onChange())
567
- sctx.effect(
568
- () => () => {
569
- if (typeof disposeWatch === 'function') disposeWatch()
570
- hooks.setSource(() => entryConfig)
571
- hooks.onChange()
572
- },
573
- 'vision-router: host settings compatibility source',
574
- )
354
+ const restore = () => {
355
+ if (typeof disposeWatch === 'function') disposeWatch()
356
+ hooks.setSource(() => entryConfig)
357
+ hooks.onChange()
358
+ }
359
+ try {
360
+ sctx.effect(
361
+ () => restore,
362
+ 'vision-router: host settings compatibility source',
363
+ )
364
+ } catch (error) {
365
+ restore()
366
+ throw error
367
+ }
575
368
  })
576
369
  }
577
370
 
@@ -585,6 +378,9 @@ export function installHostSettingsCompatibility(ctx, entryConfig, options = {})
585
378
  const install = options.installSettingsSection ?? installSettingsSectionCompat
586
379
  const ns = options.namespace
587
380
  const Config = options.Config
381
+ if (typeof ns !== 'string' || ns.trim() === '') {
382
+ throw new TypeError('vision-router: host settings compatibility requires namespace')
383
+ }
588
384
  if (Config === undefined) {
589
385
  throw new TypeError('vision-router: host settings compatibility requires Config')
590
386
  }
@@ -2,18 +2,12 @@ const UNKNOWN = 'unknown'
2
2
 
3
3
  function serviceOf(ctx, name) {
4
4
  if (!ctx || (typeof ctx !== 'object' && typeof ctx !== 'function')) return undefined
5
+ if (typeof ctx.get !== 'function') return undefined
5
6
  try {
6
- if (typeof ctx.get === 'function') {
7
- const service = ctx.get(name)
8
- if (service !== undefined && service !== null) return service
9
- }
10
- } catch {
11
- // Capability diagnostics are advisory and must not break plugin startup.
12
- }
13
- try {
14
- const service = ctx[name]
7
+ const service = ctx.get(name)
15
8
  return service === undefined || service === null ? undefined : service
16
9
  } catch {
10
+ // Capability diagnostics are advisory and must not break plugin startup.
17
11
  return undefined
18
12
  }
19
13
  }
@@ -6,8 +6,10 @@ const REVISION_REGISTRY_KEY = Symbol.for('dsh-vision-router.settings-017-revisio
6
6
 
7
7
  function revisionRegistry() {
8
8
  let registry = globalThis[REVISION_REGISTRY_KEY]
9
- if (!(registry instanceof Map)) {
10
- registry = new Map()
9
+ if (!(registry instanceof WeakMap)) {
10
+ // The pre-R11 registry was a process-global Map keyed only by document
11
+ // paths. Replace it rather than retaining stale profile keys forever.
12
+ registry = new WeakMap()
11
13
  Object.defineProperty(globalThis, REVISION_REGISTRY_KEY, {
12
14
  value: registry,
13
15
  configurable: true,
@@ -94,13 +96,20 @@ function revisionKey(editor, namespace) {
94
96
  * resets every browser connection, which forces a fresh descriptor read.
95
97
  */
96
98
  function revisionFor(editor, namespace, current, inherited) {
99
+ const identity = serviceIdentity(editor)
100
+ if (!identity || (typeof identity !== 'object' && typeof identity !== 'function')) return 0
101
+ const registry = revisionRegistry()
102
+ let states = registry.get(identity)
103
+ if (!(states instanceof Map)) {
104
+ states = new Map()
105
+ registry.set(identity, states)
106
+ }
97
107
  const key = revisionKey(editor, namespace)
98
108
  const valueFingerprint = fingerprint({ current, inherited })
99
- const registry = revisionRegistry()
100
- const previous = registry.get(key)
109
+ const previous = states.get(key)
101
110
  if (!previous) {
102
111
  const state = { fingerprint: valueFingerprint, revision: 0 }
103
- registry.set(key, state)
112
+ states.set(key, state)
104
113
  return state.revision
105
114
  }
106
115
  if (previous.fingerprint !== valueFingerprint) {
@@ -123,12 +132,10 @@ function serviceIdentity(value) {
123
132
  }
124
133
 
125
134
  function serviceOf(ctx, name) {
135
+ if (!ctx || (typeof ctx !== 'object' && typeof ctx !== 'function')) return undefined
136
+ if (typeof ctx.get !== 'function') return undefined
126
137
  try {
127
- const value = typeof ctx?.get === 'function' ? ctx.get(name) : undefined
128
- if (value !== undefined && value !== null) return value
129
- } catch {}
130
- try {
131
- const value = ctx?.[name]
138
+ const value = ctx.get(name)
132
139
  return value === undefined || value === null ? undefined : value
133
140
  } catch {
134
141
  return undefined
@@ -504,13 +504,18 @@ export function installVisionRouterFileLogging(ctx, options = {}) {
504
504
  // next apply would reuse an object whose routes are no longer mounted.
505
505
  try {
506
506
  ctx.effect?.(
507
- () => () => {
507
+ () => async () => {
508
508
  if (installs.get(ctx) === installed) installs.delete(ctx)
509
+ await sink.flush()
509
510
  },
510
511
  'vision-router: diagnostics file logger lifecycle',
511
512
  )
512
- } catch {
513
- // Cache expiry is a hardening aid; logging itself must stay non-fatal.
513
+ } catch (error) {
514
+ // Filesystem diagnostics may degrade, but lifecycle ownership may not.
515
+ // Continuing here would publish routes and a file-writing logger after
516
+ // Cordis already rejected this plugin generation.
517
+ if (installs.get(ctx) === installed) installs.delete(ctx)
518
+ throw error
514
519
  }
515
520
  }
516
521
 
@@ -34,11 +34,10 @@ function artifactsDirOf(config) {
34
34
 
35
35
  function contextService(ctx, name) {
36
36
  try {
37
- if (typeof ctx?.get === 'function') return ctx.get(name)
37
+ return typeof ctx?.get === 'function' ? ctx.get(name) : undefined
38
38
  } catch {
39
39
  return undefined
40
40
  }
41
- return ctx?.[name]
42
41
  }
43
42
 
44
43
  async function readSourceBytes(ctx, core, sessionVisionIndex, exec, image) {