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.
- package/README.md +12 -3
- package/README.zh.md +12 -3
- package/docs/architecture/3.0-architecture-plan.md +252 -0
- package/docs/architecture/3.0-r0-contract-ledger.md +137 -0
- package/docs/architecture/3.0-r1-source-artifact-boundary.md +98 -0
- package/docs/architecture/3.0-r10-final-correctness-closure.md +207 -0
- package/docs/architecture/3.0-r11-adversarial-review.md +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/dsh-support-window.md +5 -3
- package/docs/releases/v3.0.0.md +85 -0
- package/entry.js +1 -1
- package/index.js +108 -71
- package/lib/agent-request-route-authority.js +1 -20
- package/lib/core-apply-lifecycle.js +37 -43
- package/lib/core-cache.js +87 -0
- package/lib/core-primitives.js +3 -143
- package/lib/core-vision-surface-domain.js +63 -0
- package/lib/core-vision-surface.js +11 -77
- package/lib/delegated-call-config.js +15 -24
- package/lib/doctor-cli.js +0 -0
- package/lib/doctor-runtime.js +4 -1
- package/lib/dsh-contract-compat.js +26 -230
- package/lib/dsh-host-capabilities.js +3 -9
- package/lib/dsh-settings-017-compat.js +17 -10
- package/lib/file-logger.js +8 -3
- package/lib/grounding-coordinate-runtime.js +1 -2
- package/lib/http-body-limit.js +97 -79
- package/lib/image-offload-compat.js +30 -21
- package/lib/image-resource-governor.js +210 -228
- package/lib/legacy-global-proxy-boundary.js +46 -3
- package/lib/live-model-client-prelude.js +12 -4
- package/lib/live-model-discovery.js +134 -43
- package/lib/local-vision-stabilizer.js +31 -17
- package/lib/maintenance-routes.js +1 -3
- package/lib/native-image-coexistence.js +15 -3
- package/lib/ollama-cold-start.js +87 -23
- package/lib/pi-ai-bridge-wire-compat.js +3 -2
- package/lib/proxy-dispatcher-pool.js +151 -92
- package/lib/proxy-routing.js +75 -51
- package/lib/proxy-url-compat.js +3 -10
- package/lib/public-config.js +23 -44
- package/lib/public-entry.d.ts +148 -0
- package/lib/public-entry.js +17 -4
- package/lib/replay-delegation.js +12 -7
- package/lib/replay-envelope-v2-compat.js +12 -7
- package/lib/runtime-composition.js +22 -4
- package/lib/screenshot-source-boundary.js +2 -2
- package/lib/session-affinity-runtime.js +113 -97
- package/lib/session-affinity.js +89 -76
- package/lib/session-event-history-compat.js +30 -0
- package/lib/session-query-readers.js +263 -0
- package/lib/session-surface-compat.js +30 -31
- package/lib/session-surface-policy-domain.js +85 -0
- package/lib/session-surface-policy.js +2 -111
- package/lib/session-turn-resolver.js +2 -6
- package/lib/session-vision-attachment-index.js +143 -0
- package/lib/session-vision-delegation-boundary.js +34 -0
- package/lib/session-vision-delegation.js +22 -0
- package/lib/session-vision-index.js +25 -107
- package/lib/session-vision-mode-authority.js +229 -192
- package/lib/session-vision-mode-boundary.js +65 -22
- package/lib/session-vision-policy-domain.js +304 -0
- package/lib/session-vision-policy.js +104 -0
- package/lib/session-vision-state.js +368 -378
- package/lib/settings-client-017-compat.js +9 -7
- package/lib/structured-flow-hardening.js +22 -4
- package/lib/tesseract-exec-compat.js +9 -1
- package/lib/turn-budget-context.js +47 -41
- package/lib/update-check.js +11 -2
- package/lib/vision-attachment-handle-runtime.js +176 -156
- package/lib/vision-backend-runtime-policy.js +16 -0
- package/lib/vision-background-benchmark.js +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.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 +34 -4
- package/lib/vision-execution-order-apply.js +45 -55
- package/lib/vision-execution-order.js +19 -19
- package/lib/vision-execution-policy.js +12 -7
- package/lib/vision-model-registry.js +30 -17
- package/lib/vision-provider-transport.js +137 -147
- package/lib/vision-routing-authority.js +47 -48
- package/lib/vision-routing-product.js +26 -29
- package/lib/vision-routing-settings-prelude.js +8 -3
- package/lib/vision-runtime-performance-store.js +166 -0
- package/lib/vision-runtime-performance.js +13 -142
- package/lib/vision-tool-runtime-boundary.js +99 -30
- package/lib/web/dsh-settings-017-root-transport.js +27 -12
- package/lib/web/remote-settings-client.js +4 -7
- package/lib/web-capability-boundary.js +17 -7
- package/lib/wrapper-directory.js +14 -23
- package/package.json +37 -11
|
@@ -0,0 +1,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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
CORE_VISION_SURFACE_KEYS,
|
|
4
|
+
projectCoreVisionSurface,
|
|
5
|
+
resolveCoreVisionSurface,
|
|
6
|
+
} from './core-vision-surface-domain.js'
|
|
5
7
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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.
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
package/lib/doctor-runtime.js
CHANGED
|
@@ -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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
|
252
|
-
|
|
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
|
-
|
|
568
|
-
()
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
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
|
-
|
|
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
|
|
10
|
-
registry
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
package/lib/file-logger.js
CHANGED
|
@@ -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
|
-
//
|
|
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
|
-
|
|
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) {
|