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,143 @@
1
+ import { legacySessionEvents } from './session-event-history-compat.js';
2
+ function propertyBag(value) {
3
+ return value !== null && (typeof value === 'object' || typeof value === 'function')
4
+ ? value
5
+ : undefined;
6
+ }
7
+ function weakKey(value) {
8
+ return value !== null && (typeof value === 'object' || typeof value === 'function')
9
+ ? value
10
+ : undefined;
11
+ }
12
+ function attachmentRef(value) {
13
+ return propertyBag(value) ? value : undefined;
14
+ }
15
+ function attachmentIdOf(ref) {
16
+ const record = propertyBag(ref);
17
+ return String(record?.attachmentId ?? record?.id);
18
+ }
19
+ function iterableValues(value) {
20
+ const iterator = propertyBag(value)?.[Symbol.iterator];
21
+ if (typeof iterator !== 'function') {
22
+ throw new TypeError('Session attachment collector returned a non-iterable result');
23
+ }
24
+ return value;
25
+ }
26
+ function boundedMessage(error) {
27
+ const record = propertyBag(error);
28
+ const text = record?.message ?? error;
29
+ return String(text ?? '').slice(0, 400);
30
+ }
31
+ /**
32
+ * Attachment access boundary for one SessionVisionStateStore.
33
+ *
34
+ * The state store remains the sole bounded storage owner. This boundary owns
35
+ * only cache access plus target-only cold recovery from the durable Session
36
+ * event source. Synchronous lookup never performs historical I/O.
37
+ */
38
+ export function createSessionVisionAttachmentIndex({ stateStore, collectEventAttachmentRefs, readSessionLog, logger, }) {
39
+ const runtimeStore = stateStore;
40
+ const lookup = runtimeStore.lookupAttachment;
41
+ const primitiveLookup = typeof lookup === 'function'
42
+ ? (session, attachmentId) => lookup.call(stateStore, session, attachmentId)
43
+ : undefined;
44
+ const attachmentRecoveryWarnings = new WeakMap();
45
+ const recordAttachments = (session, refs) => {
46
+ if (!session || !Array.isArray(refs) || refs.length === 0)
47
+ return;
48
+ stateStore.recordAttachments(session, refs);
49
+ };
50
+ const lookupAttachment = (session, attachmentId) => {
51
+ if (primitiveLookup === undefined)
52
+ return undefined;
53
+ return primitiveLookup(session, attachmentId);
54
+ };
55
+ const recoverAttachmentsFromEvents = (session, ids, events) => {
56
+ const found = new Map();
57
+ if (!Array.isArray(events)
58
+ || events.length === 0
59
+ || typeof collectEventAttachmentRefs !== 'function') {
60
+ return found;
61
+ }
62
+ const wanted = new Set(ids);
63
+ const collected = collectEventAttachmentRefs(events);
64
+ for (const value of iterableValues(collected)) {
65
+ if (!value)
66
+ continue;
67
+ const ref = attachmentRef(value);
68
+ if (ref === undefined)
69
+ continue;
70
+ const id = attachmentIdOf(ref);
71
+ if (wanted.has(id) && !found.has(id))
72
+ found.set(id, ref);
73
+ }
74
+ if (found.size > 0)
75
+ stateStore.recordAttachments(session, [...found.values()]);
76
+ return found;
77
+ };
78
+ const warnAttachmentRecoveryFailure = (session, attachmentId, error) => {
79
+ const message = boundedMessage(error);
80
+ const warningKey = `${String(attachmentId)}:${message}`;
81
+ const key = weakKey(session);
82
+ if (key !== undefined && attachmentRecoveryWarnings.get(key) === warningKey)
83
+ return;
84
+ if (key !== undefined)
85
+ attachmentRecoveryWarnings.set(key, warningKey);
86
+ logger?.warn?.('vision-router: session attachment recovery failed id=%s error=%s', String(attachmentId), message);
87
+ };
88
+ const resolveAttachments = async (session, attachmentIds) => {
89
+ const requested = [
90
+ ...new Set((Array.isArray(attachmentIds) ? attachmentIds : []).map((id) => String(id))),
91
+ ];
92
+ const resolved = new Map();
93
+ const missing = [];
94
+ for (const id of requested) {
95
+ const cached = lookupAttachment(session, id);
96
+ if (cached !== undefined)
97
+ resolved.set(id, cached);
98
+ else
99
+ missing.push(id);
100
+ }
101
+ if (missing.length === 0 || session === undefined)
102
+ return resolved;
103
+ let events;
104
+ if (typeof readSessionLog === 'function') {
105
+ let result;
106
+ try {
107
+ result = await readSessionLog(session);
108
+ }
109
+ catch (error) {
110
+ warnAttachmentRecoveryFailure(session, missing[0], error);
111
+ return resolved;
112
+ }
113
+ const resultRecord = propertyBag(result);
114
+ if (resultRecord?.supported === true) {
115
+ const key = weakKey(session);
116
+ if (key !== undefined)
117
+ attachmentRecoveryWarnings.delete(key);
118
+ events = resultRecord.events;
119
+ }
120
+ else if (resultRecord?.supported !== false) {
121
+ warnAttachmentRecoveryFailure(session, missing[0], new Error('Session log reader returned an invalid capability result'));
122
+ return resolved;
123
+ }
124
+ }
125
+ // Explicitly unsupported SessionQuery keeps the released Session-local
126
+ // compatibility fallback. Advertised reader failures above never fall back.
127
+ if (events === undefined)
128
+ events = legacySessionEvents(session);
129
+ const recovered = recoverAttachmentsFromEvents(session, missing, events);
130
+ for (const [id, ref] of recovered)
131
+ resolved.set(id, ref);
132
+ return resolved;
133
+ };
134
+ const resolveAttachment = async (session, attachmentId) => {
135
+ return (await resolveAttachments(session, [attachmentId])).get(String(attachmentId));
136
+ };
137
+ return Object.freeze({
138
+ recordAttachments,
139
+ lookupAttachment,
140
+ resolveAttachment,
141
+ resolveAttachments,
142
+ });
143
+ }
@@ -0,0 +1,34 @@
1
+ import { materializeDelegatedSessionVisionPolicy, } from './session-vision-delegation.js';
2
+ /**
3
+ * Materialize inherited DVR authority for exact runtime-owned child Agents.
4
+ *
5
+ * DSH awaits serial `agent/created` listeners before releasing queued work,
6
+ * so the snapshot is durable before the child can assemble its first prompt.
7
+ * Runtime ownership is revalidated with `agents.isOwnedBy`; ambient initiator
8
+ * presence alone is never treated as delegation authority.
9
+ */
10
+ export function installSessionVisionDelegationBoundary(ctx, store, resolveParentAuthority) {
11
+ const currentInitiator = ctx.agents?.currentInitiator;
12
+ const isOwnedBy = ctx.agents?.isOwnedBy;
13
+ const on = ctx.on;
14
+ if (typeof currentInitiator !== 'function'
15
+ || typeof isOwnedBy !== 'function'
16
+ || typeof on !== 'function') {
17
+ return false;
18
+ }
19
+ on.call(ctx, 'agent/created', async ({ agent: child }) => {
20
+ if (typeof store.hydrate === 'function') {
21
+ await store.hydrate(child.id);
22
+ }
23
+ const parent = currentInitiator.call(ctx.agents);
24
+ if (parent === undefined || parent.id === child.id)
25
+ return;
26
+ if (!isOwnedBy.call(ctx.agents, child.id, parent))
27
+ return;
28
+ if (typeof store.hydrate === 'function') {
29
+ await store.hydrate(parent.id);
30
+ }
31
+ await materializeDelegatedSessionVisionPolicy(store, parent, child, resolveParentAuthority);
32
+ });
33
+ return true;
34
+ }
@@ -0,0 +1,22 @@
1
+ import { delegatedSessionVisionPolicy, } from './session-vision-policy.js';
2
+ /**
3
+ * Materialize one child-local DVR policy snapshot at the delegation boundary.
4
+ *
5
+ * Child-local durable truth wins when it already exists. Otherwise the direct
6
+ * parent's durable policy wins over route-derived compatibility authority, and
7
+ * the resulting enabled bit is copied into the child as a delegation snapshot.
8
+ * Later parent changes therefore cannot retroactively mutate an existing child.
9
+ */
10
+ export async function materializeDelegatedSessionVisionPolicy(store, parent, child, resolveParentAuthority) {
11
+ if (parent.id === child.id) {
12
+ throw new TypeError('Session Vision delegation requires distinct parent and child ids');
13
+ }
14
+ const existing = store.get(child.id);
15
+ if (existing !== undefined) {
16
+ return Object.freeze({ status: 'existing', policy: existing });
17
+ }
18
+ const parentPolicy = store.get(parent.id);
19
+ const enabled = parentPolicy?.enabled ?? resolveParentAuthority(parent).enabled;
20
+ const policy = await store.set(child.id, delegatedSessionVisionPolicy(enabled, parent.id));
21
+ return Object.freeze({ status: 'materialized', policy });
22
+ }
@@ -1,6 +1,8 @@
1
1
  import { currentSessionSurfacePolicy } from './session-surface-policy.js'
2
2
  import { sessionSurfaceReplacementIntent } from './session-surface-compat.js'
3
3
  import { createSessionVisionStateStore } from './session-vision-state.js'
4
+ import { legacySessionEvents } from './session-event-history-compat.js'
5
+ import { createSessionVisionAttachmentIndex } from './session-vision-attachment-index.js'
4
6
 
5
7
  const MAX_PENDING_REPAIR_EVENTS = 256
6
8
  const MAX_REPAIR_OVERFLOW_RANGES = 32
@@ -11,21 +13,6 @@ function isObject(value) {
11
13
  return value !== null && typeof value === 'object'
12
14
  }
13
15
 
14
- function legacySessionEvents(session) {
15
- try {
16
- // dsh 0.1.2-alpha.4 removed the bare `session.events` array in favor of
17
- // `session.snapshotEvents()`; both are handled so one build runs on both
18
- // harness generations.
19
- if (session && typeof session.snapshotEvents === 'function') {
20
- const events = session.snapshotEvents()
21
- return Array.isArray(events) ? events : undefined
22
- }
23
- return Array.isArray(session?.events) ? session.events : undefined
24
- } catch {
25
- return undefined
26
- }
27
- }
28
-
29
16
  function surfaceNodes(session) {
30
17
  try {
31
18
  return Array.isArray(session?.surface?.nodes) ? session.surface.nodes : undefined
@@ -52,9 +39,10 @@ function isReplacementEvent(event) {
52
39
  /**
53
40
  * Session visual data-plane index.
54
41
  *
55
- * SessionVisionStateStore remains the bounded storage owner. This object owns
56
- * target-only durable attachment recovery, event-feed-backed tool-result image
57
- * surface repair and expired structured guard-stop surface repair.
42
+ * SessionVisionStateStore remains the bounded storage owner. Attachment cache
43
+ * access and target-only durable recovery are delegated to one typed attachment
44
+ * index; this object owns event-feed-backed tool-result image surface repair and
45
+ * expired structured guard-stop surface repair.
58
46
  *
59
47
  * Supported DSH Hosts expose the post-commit `session/event` feed. Production
60
48
  * surface repair consumes only the exact frozen events observed on that feed;
@@ -83,9 +71,24 @@ export function createSessionVisionIndex({
83
71
  if (!store || typeof store !== 'object') {
84
72
  throw new TypeError('session vision index requires a state store')
85
73
  }
86
- const primitiveLookup = typeof store.lookupAttachment === 'function'
87
- ? store.lookupAttachment.bind(store)
88
- : undefined
74
+ const attachmentIndex = createSessionVisionAttachmentIndex({
75
+ stateStore: store,
76
+ collectEventAttachmentRefs:
77
+ typeof core.collectEventAttachmentRefs === 'function'
78
+ ? (events) => core.collectEventAttachmentRefs(events)
79
+ : undefined,
80
+ readSessionLog,
81
+ logger,
82
+ })
83
+ const {
84
+ recordAttachments,
85
+ lookupAttachment,
86
+ resolveAttachment,
87
+ resolveAttachments,
88
+ } = attachmentIndex
89
+
90
+ // Surface repair/feed state remains owned by SessionVisionIndex. Attachment
91
+ // cache/recovery state lives exclusively in SessionVisionAttachmentIndex.
89
92
  const toolSurfaceScans = new WeakMap()
90
93
  const guardSurfaceScans = new WeakMap()
91
94
  const pendingToolRepairEvents = new WeakMap()
@@ -97,93 +100,8 @@ export function createSessionVisionIndex({
97
100
  const repairFeedOverflowWarnings = new WeakSet()
98
101
  const repairFeedObserverWarnings = new WeakMap()
99
102
  const surfaceFeedBackfills = new WeakMap()
100
- const attachmentRecoveryWarnings = new WeakMap()
101
103
  let surfaceEventFeedActive = false
102
104
 
103
- const recordAttachments = (session, refs) => {
104
- if (!session || !Array.isArray(refs) || refs.length === 0) return
105
- store.recordAttachments(session, refs)
106
- }
107
-
108
- // Synchronous lookup is cache-only. Projected short handles first warm this
109
- // cache from current derived messages and must not trigger historical I/O.
110
- const lookupAttachment = (session, id) => {
111
- if (!primitiveLookup) return undefined
112
- return primitiveLookup(session, id)
113
- }
114
-
115
- const recoverAttachmentsFromEvents = (session, ids, events) => {
116
- const found = new Map()
117
- if (!Array.isArray(events) || events.length === 0 || typeof core.collectEventAttachmentRefs !== 'function') {
118
- return found
119
- }
120
- const wanted = new Set(ids.map((id) => String(id)))
121
- for (const ref of core.collectEventAttachmentRefs(events)) {
122
- if (!ref) continue
123
- const id = String(ref.attachmentId ?? ref.id)
124
- if (wanted.has(id) && !found.has(id)) found.set(id, ref)
125
- }
126
- if (found.size > 0) store.recordAttachments(session, [...found.values()])
127
- return found
128
- }
129
-
130
- const warnAttachmentRecoveryFailure = (session, id, error) => {
131
- const message = boundedMessage(error)
132
- const key = `${String(id)}:${message}`
133
- if (attachmentRecoveryWarnings.get(session) === key) return
134
- attachmentRecoveryWarnings.set(session, key)
135
- logger?.warn?.(
136
- 'vision-router: session attachment recovery failed id=%s error=%s',
137
- String(id),
138
- message,
139
- )
140
- }
141
-
142
- const resolveAttachments = async (session, ids) => {
143
- const requested = [...new Set((Array.isArray(ids) ? ids : []).map((id) => String(id)))]
144
- const resolved = new Map()
145
- const missing = []
146
- for (const id of requested) {
147
- const cached = lookupAttachment(session, id)
148
- if (cached !== undefined) resolved.set(id, cached)
149
- else missing.push(id)
150
- }
151
- if (missing.length === 0 || session === undefined) return resolved
152
-
153
- let events
154
- if (typeof readSessionLog === 'function') {
155
- let result
156
- try {
157
- result = await readSessionLog(session)
158
- } catch (error) {
159
- warnAttachmentRecoveryFailure(session, missing[0], error)
160
- return resolved
161
- }
162
- if (result?.supported === true) {
163
- attachmentRecoveryWarnings.delete(session)
164
- events = result.events
165
- } else if (result?.supported !== false) {
166
- warnAttachmentRecoveryFailure(
167
- session,
168
- missing[0],
169
- new Error('Session log reader returned an invalid capability result'),
170
- )
171
- return resolved
172
- }
173
- }
174
-
175
- // Historical/partial Hosts without SessionQuery keep the released fallback.
176
- // Public rc8+ Hosts use readSessionLog above and never touch sync history.
177
- if (events === undefined) events = legacySessionEvents(session)
178
- const recovered = recoverAttachmentsFromEvents(session, missing, events)
179
- for (const [id, ref] of recovered) resolved.set(id, ref)
180
- return resolved
181
- }
182
-
183
- const resolveAttachment = async (session, id) => {
184
- return (await resolveAttachments(session, [id])).get(String(id))
185
- }
186
-
187
105
  const pendingSurfaceScan = (scans, session) => {
188
106
  if (!session) return undefined
189
107
  const nodes = surfaceNodes(session)
@@ -732,7 +650,7 @@ export function createSessionVisionIndex({
732
650
  if (typeof core.rewriteImageBlocks === 'function') {
733
651
  const found = core.rewriteImageBlocks(messages)
734
652
  if (Array.isArray(found?.attachments) && found.attachments.length > 0) {
735
- store.recordAttachments(session, found.attachments)
653
+ recordAttachments(session, found.attachments)
736
654
  }
737
655
  }
738
656
  await ensureSurfaceEventFeedBackfill(session)