dsh-vision-router 2.2.8 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/README.md +16 -7
  2. package/README.zh.md +16 -7
  3. package/docs/architecture/2.3.0-compat-retirement.md +147 -0
  4. package/docs/architecture/2.3.0-execution-plan.md +410 -0
  5. package/docs/architecture/2.3.0-final-audit.md +279 -0
  6. package/docs/architecture/2.3.0-reopened-plan.md +720 -0
  7. package/docs/architecture/3.0-architecture-plan.md +252 -0
  8. package/docs/architecture/3.0-r0-contract-ledger.md +137 -0
  9. package/docs/architecture/3.0-r1-source-artifact-boundary.md +98 -0
  10. package/docs/architecture/3.0-r10-final-correctness-closure.md +207 -0
  11. package/docs/architecture/3.0-r11-adversarial-review.md +441 -0
  12. package/docs/architecture/3.0-r5-browser-go-no-go.md +165 -0
  13. package/docs/architecture/3.0-r5-browser-typescript-gate.md +105 -0
  14. package/docs/architecture/3.0-r6-compat-retirement.md +203 -0
  15. package/docs/architecture/3.0-r7-public-api.md +151 -0
  16. package/docs/architecture/3.0-r8-ci-release.md +248 -0
  17. package/docs/architecture/3.0-r9-final-audit.md +370 -0
  18. package/docs/architecture/compat-inventory.md +26 -34
  19. package/docs/architecture/dsh-compatibility-matrix.md +65 -54
  20. package/docs/architecture/dsh-support-window.md +30 -22
  21. package/docs/architecture/final-closure-audit.md +2 -0
  22. package/docs/releases/v2.3.0.md +53 -0
  23. package/docs/releases/v3.0.0.md +85 -0
  24. package/entry.js +9 -57
  25. package/index.js +268 -761
  26. package/lib/agent-request-route-authority.js +1 -20
  27. package/lib/catalog-corrections.js +1 -2
  28. package/lib/client-maintenance-actions.js +145 -0
  29. package/lib/client-presentation-boundary-main.js +75 -17
  30. package/lib/client-presentation-boundary.js +53 -28
  31. package/lib/client.js +246 -144
  32. package/lib/core-apply-lifecycle.js +41 -0
  33. package/lib/core-cache.js +87 -0
  34. package/lib/core-primitives.js +91 -278
  35. package/lib/core-vision-surface-domain.js +63 -0
  36. package/lib/core-vision-surface.js +11 -77
  37. package/lib/delegated-call-config.js +15 -24
  38. package/lib/desktop-screenshot-tool.js +262 -0
  39. package/lib/doctor-cli.js +0 -0
  40. package/lib/doctor-runtime.js +4 -1
  41. package/lib/dsh-contract-compat.js +37 -256
  42. package/lib/dsh-host-capabilities.js +3 -9
  43. package/lib/dsh-settings-017-compat.js +17 -10
  44. package/lib/dsh-support-window.js +3 -3
  45. package/lib/file-logger.js +8 -3
  46. package/lib/grounding-coordinate-runtime.js +1 -2
  47. package/lib/http-body-limit.js +97 -79
  48. package/lib/http-compat.js +1 -2
  49. package/lib/image-offload-compat.js +30 -21
  50. package/lib/image-resource-governor.js +210 -228
  51. package/lib/legacy-global-proxy-boundary.js +46 -3
  52. package/lib/live-model-client-prelude.js +12 -4
  53. package/lib/live-model-discovery.js +134 -43
  54. package/lib/local-remote-settings-permission.js +33 -7
  55. package/lib/local-vision-stabilizer.js +31 -17
  56. package/lib/maintenance-routes.js +134 -0
  57. package/lib/native-image-coexistence.js +15 -3
  58. package/lib/ollama-cold-start.js +87 -23
  59. package/lib/pi-ai-bridge-wire-compat.js +3 -2
  60. package/lib/proxy-dispatcher-pool.js +151 -92
  61. package/lib/proxy-routing.js +75 -51
  62. package/lib/proxy-url-compat.js +3 -10
  63. package/lib/public-config.js +36 -0
  64. package/lib/public-entry.d.ts +148 -0
  65. package/lib/public-entry.js +19 -11
  66. package/lib/remote-settings-bridge.js +11 -0
  67. package/lib/remote-settings-risk-client-boundary.js +155 -0
  68. package/lib/remote-settings-risk-confirmation.js +17 -105
  69. package/lib/replay-delegation.js +12 -7
  70. package/lib/replay-envelope-v2-compat.js +12 -7
  71. package/lib/runtime-composition.js +127 -34
  72. package/lib/runtime-i18n-boundary.js +10 -11
  73. package/lib/screenshot-source-boundary.js +2 -2
  74. package/lib/session-affinity-runtime.js +113 -97
  75. package/lib/session-affinity.js +89 -76
  76. package/lib/session-event-history-compat.js +30 -0
  77. package/lib/session-message-source-compat.js +7 -3
  78. package/lib/session-query-readers.js +263 -0
  79. package/lib/session-surface-compat.js +30 -31
  80. package/lib/session-surface-policy-domain.js +85 -0
  81. package/lib/session-surface-policy.js +2 -111
  82. package/lib/session-turn-resolver.js +2 -6
  83. package/lib/session-vision-attachment-index.js +143 -0
  84. package/lib/session-vision-delegation-boundary.js +34 -0
  85. package/lib/session-vision-delegation.js +22 -0
  86. package/lib/session-vision-index.js +179 -116
  87. package/lib/session-vision-mode-authority.js +229 -192
  88. package/lib/session-vision-mode-boundary.js +65 -22
  89. package/lib/session-vision-policy-domain.js +304 -0
  90. package/lib/session-vision-policy.js +104 -0
  91. package/lib/session-vision-state.js +368 -378
  92. package/lib/settings-client-017-compat.js +9 -7
  93. package/lib/settings-client-loader-lifecycle.js +129 -0
  94. package/lib/sharp-runtime.js +0 -7
  95. package/lib/structured-flow-hardening.js +25 -6
  96. package/lib/tesseract-exec-compat.js +9 -1
  97. package/lib/turn-budget-context.js +47 -41
  98. package/lib/update-check.js +11 -2
  99. package/lib/vision-attachment-handle-runtime.js +176 -156
  100. package/lib/vision-backend-runtime-policy.js +19 -2
  101. package/lib/vision-background-benchmark.js +41 -4
  102. package/lib/vision-background-failure-policy.js +41 -41
  103. package/lib/vision-background-stop-store-core.js +217 -0
  104. package/lib/vision-background-stop-store.js +8 -179
  105. package/lib/vision-capability-benchmark-service.js +1 -0
  106. package/lib/vision-capability-benchmark.js +11 -61
  107. package/lib/vision-capability-evidence.js +35 -0
  108. package/lib/vision-capability-fingerprint.js +72 -0
  109. package/lib/vision-capability-identity.js +116 -61
  110. package/lib/vision-capability-router.js +13 -33
  111. package/lib/vision-chunk-assembler.js +95 -0
  112. package/lib/vision-diagnostics-routes.js +246 -0
  113. package/lib/vision-evidence-guidance.js +3 -1
  114. package/lib/vision-execution-order-apply.js +45 -55
  115. package/lib/vision-execution-order.js +19 -19
  116. package/lib/vision-execution-policy.js +12 -7
  117. package/lib/vision-model-registry.js +30 -17
  118. package/lib/vision-provider-transport.js +137 -176
  119. package/lib/vision-routing-authority.js +47 -48
  120. package/lib/vision-routing-product.js +26 -29
  121. package/lib/vision-routing-settings-prelude.js +8 -3
  122. package/lib/vision-runtime-performance-store.js +166 -0
  123. package/lib/vision-runtime-performance.js +13 -142
  124. package/lib/vision-toggle-root-hardening.js +22 -5
  125. package/lib/vision-tool-runtime-boundary.js +104 -31
  126. package/lib/web/dsh-settings-017-root-transport.js +27 -12
  127. package/lib/web/remote-settings-client.js +6 -9
  128. package/lib/web-capability-boundary.js +17 -7
  129. package/lib/wrapper-directory.js +14 -23
  130. package/package.json +56 -20
  131. package/lib/android-attachment-compat.js +0 -244
  132. package/lib/settings-client-rc8-lifecycle.js +0 -397
@@ -1,28 +1,18 @@
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
8
+ const MAX_REPAIR_OVERFLOW_RANGES = 32
9
+ const MAX_REPAIR_OVERFLOW_INSPECTIONS_PER_PASS = 512
10
+ const MAX_REPAIR_OVERFLOW_READS_PER_PASS = 32
6
11
 
7
12
  function isObject(value) {
8
13
  return value !== null && typeof value === 'object'
9
14
  }
10
15
 
11
- function legacySessionEvents(session) {
12
- try {
13
- // dsh 0.1.2-alpha.4 removed the bare `session.events` array in favor of
14
- // `session.snapshotEvents()`; both are handled so one build runs on both
15
- // harness generations.
16
- if (session && typeof session.snapshotEvents === 'function') {
17
- const events = session.snapshotEvents()
18
- return Array.isArray(events) ? events : undefined
19
- }
20
- return Array.isArray(session?.events) ? session.events : undefined
21
- } catch {
22
- return undefined
23
- }
24
- }
25
-
26
16
  function surfaceNodes(session) {
27
17
  try {
28
18
  return Array.isArray(session?.surface?.nodes) ? session.surface.nodes : undefined
@@ -49,9 +39,10 @@ function isReplacementEvent(event) {
49
39
  /**
50
40
  * Session visual data-plane index.
51
41
  *
52
- * SessionVisionStateStore remains the bounded storage owner. This object owns
53
- * target-only durable attachment recovery, event-feed-backed tool-result image
54
- * 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.
55
46
  *
56
47
  * Supported DSH Hosts expose the post-commit `session/event` feed. Production
57
48
  * surface repair consumes only the exact frozen events observed on that feed;
@@ -80,105 +71,37 @@ export function createSessionVisionIndex({
80
71
  if (!store || typeof store !== 'object') {
81
72
  throw new TypeError('session vision index requires a state store')
82
73
  }
83
- const primitiveLookup = typeof store.lookupAttachment === 'function'
84
- ? store.lookupAttachment.bind(store)
85
- : 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.
86
92
  const toolSurfaceScans = new WeakMap()
87
93
  const guardSurfaceScans = new WeakMap()
88
94
  const pendingToolRepairEvents = new WeakMap()
89
95
  const pendingGuardRepairEvents = new WeakMap()
96
+ const toolRepairOverflowRanges = new WeakMap()
97
+ const guardRepairOverflowRanges = new WeakMap()
90
98
  const unsupportedSurfaceContracts = new WeakSet()
91
99
  const repairReadWarnings = new WeakMap()
92
100
  const repairFeedOverflowWarnings = new WeakSet()
93
101
  const repairFeedObserverWarnings = new WeakMap()
94
102
  const surfaceFeedBackfills = new WeakMap()
95
- const attachmentRecoveryWarnings = new WeakMap()
96
103
  let surfaceEventFeedActive = false
97
104
 
98
- const recordAttachments = (session, refs) => {
99
- if (!session || !Array.isArray(refs) || refs.length === 0) return
100
- store.recordAttachments(session, refs)
101
- }
102
-
103
- // Synchronous lookup is cache-only. Projected short handles first warm this
104
- // cache from current derived messages and must not trigger historical I/O.
105
- const lookupAttachment = (session, id) => {
106
- if (!primitiveLookup) return undefined
107
- return primitiveLookup(session, id)
108
- }
109
-
110
- const recoverAttachmentsFromEvents = (session, ids, events) => {
111
- const found = new Map()
112
- if (!Array.isArray(events) || events.length === 0 || typeof core.collectEventAttachmentRefs !== 'function') {
113
- return found
114
- }
115
- const wanted = new Set(ids.map((id) => String(id)))
116
- for (const ref of core.collectEventAttachmentRefs(events)) {
117
- if (!ref) continue
118
- const id = String(ref.attachmentId ?? ref.id)
119
- if (wanted.has(id) && !found.has(id)) found.set(id, ref)
120
- }
121
- if (found.size > 0) store.recordAttachments(session, [...found.values()])
122
- return found
123
- }
124
-
125
- const warnAttachmentRecoveryFailure = (session, id, error) => {
126
- const message = boundedMessage(error)
127
- const key = `${String(id)}:${message}`
128
- if (attachmentRecoveryWarnings.get(session) === key) return
129
- attachmentRecoveryWarnings.set(session, key)
130
- logger?.warn?.(
131
- 'vision-router: session attachment recovery failed id=%s error=%s',
132
- String(id),
133
- message,
134
- )
135
- }
136
-
137
- const resolveAttachments = async (session, ids) => {
138
- const requested = [...new Set((Array.isArray(ids) ? ids : []).map((id) => String(id)))]
139
- const resolved = new Map()
140
- const missing = []
141
- for (const id of requested) {
142
- const cached = lookupAttachment(session, id)
143
- if (cached !== undefined) resolved.set(id, cached)
144
- else missing.push(id)
145
- }
146
- if (missing.length === 0 || session === undefined) return resolved
147
-
148
- let events
149
- if (typeof readSessionLog === 'function') {
150
- let result
151
- try {
152
- result = await readSessionLog(session)
153
- } catch (error) {
154
- warnAttachmentRecoveryFailure(session, missing[0], error)
155
- return resolved
156
- }
157
- if (result?.supported === true) {
158
- attachmentRecoveryWarnings.delete(session)
159
- events = result.events
160
- } else if (result?.supported !== false) {
161
- warnAttachmentRecoveryFailure(
162
- session,
163
- missing[0],
164
- new Error('Session log reader returned an invalid capability result'),
165
- )
166
- return resolved
167
- }
168
- }
169
-
170
- // Historical/partial Hosts without SessionQuery keep the released fallback.
171
- // Public rc8+ Hosts use readSessionLog above and never touch sync history.
172
- if (events === undefined) events = legacySessionEvents(session)
173
- const recovered = recoverAttachmentsFromEvents(session, missing, events)
174
- for (const [id, ref] of recovered) resolved.set(id, ref)
175
- return resolved
176
- }
177
-
178
- const resolveAttachment = async (session, id) => {
179
- return (await resolveAttachments(session, [id])).get(String(id))
180
- }
181
-
182
105
  const pendingSurfaceScan = (scans, session) => {
183
106
  if (!session) return undefined
184
107
  const nodes = surfaceNodes(session)
@@ -295,7 +218,132 @@ export function createSessionVisionIndex({
295
218
  return undefined
296
219
  }
297
220
 
298
- const cachePendingRepairEvent = (pendingBySession, session, event) => {
221
+ // Feed overflow stores only dropped sequence ranges, never frozen event
222
+ // objects. Adjacent ranges coalesce; pathological sparse overflow is
223
+ // conservatively collapsed once the range count reaches a fixed bound. The
224
+ // recovery loop then advances that range in bounded numeric/read batches and
225
+ // consults only Host-owned readSessionEvent(), so memory remains bounded
226
+ // without returning to whole-Session history materialization.
227
+ const addRepairOverflowSeq = (overflowBySession, session, seq) => {
228
+ if (!Number.isSafeInteger(seq) || seq < 0 || Object.is(seq, -0)) return
229
+ let ranges = overflowBySession.get(session)
230
+ if (!ranges) {
231
+ ranges = []
232
+ overflowBySession.set(session, ranges)
233
+ }
234
+
235
+ let start = seq
236
+ let end = seq
237
+ let insertAt = 0
238
+ while (insertAt < ranges.length && ranges[insertAt].end + 1 < start) insertAt += 1
239
+ while (insertAt < ranges.length && ranges[insertAt].start - 1 <= end) {
240
+ start = Math.min(start, ranges[insertAt].start)
241
+ end = Math.max(end, ranges[insertAt].end)
242
+ ranges.splice(insertAt, 1)
243
+ }
244
+ ranges.splice(insertAt, 0, { start, end })
245
+
246
+ if (ranges.length > MAX_REPAIR_OVERFLOW_RANGES) {
247
+ overflowBySession.set(session, [{
248
+ start: ranges[0].start,
249
+ end: ranges[ranges.length - 1].end,
250
+ }])
251
+ }
252
+ }
253
+
254
+ const advanceRepairOverflowRange = (ranges) => {
255
+ const range = ranges?.[0]
256
+ if (!range) return
257
+ if (range.start >= range.end) ranges.shift()
258
+ else range.start += 1
259
+ }
260
+
261
+ const recoverRepairOverflowEvents = async (overflowBySession, pendingBySession, session) => {
262
+ const ranges = overflowBySession.get(session)
263
+ if (!ranges || ranges.length === 0) return []
264
+
265
+ const nodes = surfaceNodes(session)
266
+ if (!nodes || nodes.length === 0) {
267
+ overflowBySession.delete(session)
268
+ repairFeedOverflowWarnings.delete(session)
269
+ return []
270
+ }
271
+ const current = new Set(nodes)
272
+ const pending = pendingBySession.get(session)
273
+ const recovered = []
274
+ let inspections = 0
275
+ let reads = 0
276
+
277
+ while (
278
+ ranges.length > 0
279
+ && inspections < MAX_REPAIR_OVERFLOW_INSPECTIONS_PER_PASS
280
+ && reads < MAX_REPAIR_OVERFLOW_READS_PER_PASS
281
+ ) {
282
+ const seq = ranges[0].start
283
+ inspections += 1
284
+
285
+ // A dropped repair that is no longer on the live surface is settled. A
286
+ // seq still present in the fast-path map will be consumed normally.
287
+ if (!current.has(seq) || pending?.has(seq)) {
288
+ advanceRepairOverflowRange(ranges)
289
+ continue
290
+ }
291
+
292
+ if (typeof readSessionEvent !== 'function') {
293
+ warnRepairReadFailure(
294
+ session,
295
+ seq,
296
+ new Error('Session event reader is unavailable during feed overflow recovery'),
297
+ )
298
+ break
299
+ }
300
+
301
+ let result
302
+ try {
303
+ result = await readSessionEvent(session, seq)
304
+ } catch (error) {
305
+ warnRepairReadFailure(session, seq, error)
306
+ break
307
+ }
308
+ if (result?.supported !== true) {
309
+ warnRepairReadFailure(
310
+ session,
311
+ seq,
312
+ result?.supported === false
313
+ ? new Error('Session event reader is unsupported during feed overflow recovery')
314
+ : new Error('Session event reader returned an invalid capability result during feed overflow recovery'),
315
+ )
316
+ break
317
+ }
318
+ const event = result.event
319
+ if (!event || typeof event !== 'object' || (event.seq !== undefined && event.seq !== seq)) {
320
+ warnRepairReadFailure(
321
+ session,
322
+ seq,
323
+ new Error(`Session event reader returned no matching event for overflow seq ${seq}`),
324
+ )
325
+ break
326
+ }
327
+
328
+ reads += 1
329
+ advanceRepairOverflowRange(ranges)
330
+ repairReadWarnings.delete(session)
331
+ // The live feed may deliver the same committed event while the exact read
332
+ // is in flight. In that case the fast-path map owns it; do not return a
333
+ // second copy from recovery.
334
+ if (!pendingBySession.get(session)?.has(seq) && !isReplacementEvent(event)) {
335
+ recovered.push({ seq, event })
336
+ }
337
+ }
338
+
339
+ if (ranges.length === 0) {
340
+ overflowBySession.delete(session)
341
+ repairFeedOverflowWarnings.delete(session)
342
+ }
343
+ return recovered
344
+ }
345
+
346
+ const cachePendingRepairEvent = (pendingBySession, overflowBySession, session, event) => {
299
347
  if (!session || !Number.isSafeInteger(event?.seq) || event.seq < 0 || Object.is(event.seq, -0)) return
300
348
  let pending = pendingBySession.get(session)
301
349
  if (!pending) {
@@ -304,11 +352,14 @@ export function createSessionVisionIndex({
304
352
  }
305
353
  if (!pending.has(event.seq) && pending.size >= MAX_PENDING_REPAIR_EVENTS) {
306
354
  const oldest = pending.keys().next().value
307
- if (oldest !== undefined) pending.delete(oldest)
355
+ if (oldest !== undefined) {
356
+ pending.delete(oldest)
357
+ addRepairOverflowSeq(overflowBySession, session, oldest)
358
+ }
308
359
  if (!repairFeedOverflowWarnings.has(session)) {
309
360
  repairFeedOverflowWarnings.add(session)
310
361
  logger?.warn?.(
311
- 'vision-router: session surface repair event feed exceeded %s pending events; dropping oldest entry',
362
+ 'vision-router: session surface repair event feed exceeded %s pending events; scheduling bounded exact-event recovery',
312
363
  MAX_PENDING_REPAIR_EVENTS,
313
364
  )
314
365
  }
@@ -340,10 +391,10 @@ export function createSessionVisionIndex({
340
391
  event,
341
392
  (events, seqs) => core.planToolResultImageShadows(events, seqs, () => true),
342
393
  )
343
- if (plans.length > 0) cachePendingRepairEvent(pendingToolRepairEvents, session, event)
394
+ if (plans.length > 0) cachePendingRepairEvent(pendingToolRepairEvents, toolRepairOverflowRanges, session, event)
344
395
  } else if (event.type === 'user/message' && typeof core.planGuardStopShadows === 'function') {
345
396
  const plans = planAgainstOneEvent(event, (events, seqs) => core.planGuardStopShadows(events, seqs))
346
- if (plans.length > 0) cachePendingRepairEvent(pendingGuardRepairEvents, session, event)
397
+ if (plans.length > 0) cachePendingRepairEvent(pendingGuardRepairEvents, guardRepairOverflowRanges, session, event)
347
398
  }
348
399
  repairFeedObserverWarnings.delete(session)
349
400
  } catch (error) {
@@ -439,11 +490,17 @@ export function createSessionVisionIndex({
439
490
 
440
491
  const repairToolResultSurfaceFromFeed = async (session) => {
441
492
  if (typeof session?.append !== 'function' || typeof core.planToolResultImageShadows !== 'function') return 0
493
+ const recovered = await recoverRepairOverflowEvents(
494
+ toolRepairOverflowRanges,
495
+ pendingToolRepairEvents,
496
+ session,
497
+ )
442
498
  const pending = consumePendingRepairEvents(pendingToolRepairEvents, session)
443
- if (pending.length === 0) return 0
499
+ const entries = [...recovered, ...pending]
500
+ if (entries.length === 0) return 0
444
501
  const preserve = keepToolResultImages()
445
502
  let repaired = 0
446
- for (const { seq, event } of pending) {
503
+ for (const { seq, event } of entries) {
447
504
  const events = { [seq]: event }
448
505
  const plans = core.planToolResultImageShadows(events, [seq], () => !preserve)
449
506
  for (const plan of plans) {
@@ -470,10 +527,16 @@ export function createSessionVisionIndex({
470
527
 
471
528
  const repairGuardStopSurfaceFromFeed = async (session) => {
472
529
  if (typeof session?.append !== 'function' || typeof core.planGuardStopShadows !== 'function') return 0
530
+ const recovered = await recoverRepairOverflowEvents(
531
+ guardRepairOverflowRanges,
532
+ pendingGuardRepairEvents,
533
+ session,
534
+ )
473
535
  const pending = consumePendingRepairEvents(pendingGuardRepairEvents, session)
474
- if (pending.length === 0) return 0
536
+ const entries = [...recovered, ...pending]
537
+ if (entries.length === 0) return 0
475
538
  let repaired = 0
476
- for (const { seq, event } of pending) {
539
+ for (const { seq, event } of entries) {
477
540
  const events = { [seq]: event }
478
541
  const plans = core.planGuardStopShadows(events, [seq])
479
542
  for (const plan of plans) {
@@ -587,7 +650,7 @@ export function createSessionVisionIndex({
587
650
  if (typeof core.rewriteImageBlocks === 'function') {
588
651
  const found = core.rewriteImageBlocks(messages)
589
652
  if (Array.isArray(found?.attachments) && found.attachments.length > 0) {
590
- store.recordAttachments(session, found.attachments)
653
+ recordAttachments(session, found.attachments)
591
654
  }
592
655
  }
593
656
  await ensureSurfaceEventFeedBackfill(session)