dsh-vision-router 2.1.7 → 2.2.1
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 +8 -14
- package/README.zh.md +8 -14
- package/docs/architecture/compat-inventory.md +1 -1
- package/docs/architecture/dsh-compatibility-matrix.md +4 -1
- package/docs/architecture/dsh-support-window.md +1 -2
- package/docs/releases/v2.2.0.md +25 -0
- package/docs/releases/v2.2.1.md +22 -0
- package/docs/remote-settings.md +2 -0
- package/entry.js +2 -0
- package/index.js +355 -64
- package/lib/artifact-boundary.js +11 -0
- package/lib/artifact-io.js +60 -7
- package/lib/catalog-corrections.js +5 -0
- package/lib/client.js +22 -17
- package/lib/core-primitives.js +126 -5
- package/lib/degraded-local-evidence.js +39 -0
- package/lib/dsh-contract-compat.js +225 -0
- package/lib/dsh-support-window.js +1 -1
- package/lib/fetch-wrapper-lifecycle.js +56 -0
- package/lib/image-offload-compat.js +38 -0
- package/lib/legacy-global-proxy-boundary.js +4 -3
- package/lib/local-vision-stabilizer.js +15 -6
- package/lib/official-deepseek-catalog.js +120 -0
- package/lib/ollama-cold-start.js +3 -15
- package/lib/pi-ai-bridge-wire-compat.js +4 -3
- package/lib/pixel-diff-stream.js +12 -6
- package/lib/remote-settings-bridge.js +19 -1
- package/lib/replay-delegation.js +41 -21
- package/lib/runtime-composition.js +15 -2
- package/lib/runtime-i18n-boundary.js +4 -13
- package/lib/runtime-i18n.js +2 -2
- package/lib/session-surface-compat.js +40 -0
- package/lib/session-turn-resolver.js +57 -0
- package/lib/session-vision-index.js +438 -97
- package/lib/session-vision-mode-boundary.js +83 -6
- package/lib/session-vision-runtime.js +8 -2
- package/lib/session-vision-state.js +0 -13
- package/lib/tesseract-exec-compat.js +16 -2
- package/lib/twin-image-capability-fallback.js +2 -11
- package/lib/vision-artifact-store.js +16 -3
- package/lib/vision-attachment-handle-runtime.js +9 -21
- package/lib/vision-backend-runtime-policy.js +7 -6
- package/lib/vision-breaker-shadow-health.js +26 -6
- package/lib/vision-evidence-guidance.js +39 -0
- package/lib/vision-resilience.js +73 -1
- package/package.json +12 -7
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { currentSessionSurfacePolicy } from './session-surface-policy.js'
|
|
2
|
+
import { sessionSurfaceReplacementIntent } from './session-surface-compat.js'
|
|
2
3
|
import { createSessionVisionStateStore } from './session-vision-state.js'
|
|
3
4
|
|
|
5
|
+
const MAX_PENDING_REPAIR_EVENTS = 256
|
|
6
|
+
|
|
4
7
|
function isObject(value) {
|
|
5
8
|
return value !== null && typeof value === 'object'
|
|
6
9
|
}
|
|
7
10
|
|
|
8
|
-
function
|
|
11
|
+
function legacySessionEvents(session) {
|
|
9
12
|
try {
|
|
10
13
|
// dsh 0.1.2-alpha.4 removed the bare `session.events` array in favor of
|
|
11
14
|
// `session.snapshotEvents()`; both are handled so one build runs on both
|
|
@@ -38,23 +41,35 @@ function boundedMessage(error) {
|
|
|
38
41
|
return String(text ?? '').slice(0, 400)
|
|
39
42
|
}
|
|
40
43
|
|
|
44
|
+
function isReplacementEvent(event) {
|
|
45
|
+
const op = event?.surfaceOp
|
|
46
|
+
return !!op && typeof op === 'object' && !Array.isArray(op) && op.op === 'replace'
|
|
47
|
+
}
|
|
48
|
+
|
|
41
49
|
/**
|
|
42
50
|
* Session visual data-plane index.
|
|
43
51
|
*
|
|
44
52
|
* SessionVisionStateStore remains the bounded storage owner. This object owns
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
53
|
+
* target-only durable attachment recovery, event-feed-backed tool-result image
|
|
54
|
+
* surface repair and expired structured guard-stop surface repair.
|
|
55
|
+
*
|
|
56
|
+
* Supported DSH Hosts expose the post-commit `session/event` feed. Production
|
|
57
|
+
* surface repair consumes only the exact frozen events observed on that feed;
|
|
58
|
+
* it never asks SessionQuery to materialize or clone the live Session history.
|
|
59
|
+
* The older reader path remains only for isolated/partial callers that do not
|
|
60
|
+
* install the event feed boundary.
|
|
48
61
|
*
|
|
49
|
-
* The index never replaces stateStore.lookupAttachment().
|
|
50
|
-
*
|
|
51
|
-
*
|
|
62
|
+
* The index never replaces stateStore.lookupAttachment(). Cache lookup stays
|
|
63
|
+
* synchronous through lookupAttachment(); cold durable recovery is requested
|
|
64
|
+
* explicitly through async resolveAttachment(), so I/O ownership remains visible.
|
|
52
65
|
*/
|
|
53
66
|
export function createSessionVisionIndex({
|
|
54
67
|
stateStore,
|
|
55
68
|
core,
|
|
56
69
|
config = {},
|
|
57
70
|
logger,
|
|
71
|
+
readSessionEvent,
|
|
72
|
+
readSessionLog,
|
|
58
73
|
} = {}) {
|
|
59
74
|
if (!core || typeof core !== 'object') throw new TypeError('session vision index requires core helpers')
|
|
60
75
|
|
|
@@ -70,59 +85,103 @@ export function createSessionVisionIndex({
|
|
|
70
85
|
: undefined
|
|
71
86
|
const toolSurfaceScans = new WeakMap()
|
|
72
87
|
const guardSurfaceScans = new WeakMap()
|
|
88
|
+
const pendingToolRepairEvents = new WeakMap()
|
|
89
|
+
const pendingGuardRepairEvents = new WeakMap()
|
|
90
|
+
const unsupportedSurfaceContracts = new WeakSet()
|
|
91
|
+
const repairReadWarnings = new WeakMap()
|
|
92
|
+
const repairFeedOverflowWarnings = new WeakSet()
|
|
93
|
+
const repairFeedObserverWarnings = new WeakMap()
|
|
94
|
+
const attachmentRecoveryWarnings = new WeakMap()
|
|
95
|
+
let surfaceEventFeedActive = false
|
|
73
96
|
|
|
74
97
|
const recordAttachments = (session, refs) => {
|
|
75
98
|
if (!session || !Array.isArray(refs) || refs.length === 0) return
|
|
76
99
|
store.recordAttachments(session, refs)
|
|
77
100
|
}
|
|
78
101
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const events = sessionEvents(session)
|
|
82
|
-
if (!events || events.length === 0) return
|
|
83
|
-
const last = store.getScannedEventSeq(session)
|
|
84
|
-
if (last >= events.length) return
|
|
85
|
-
const refs = typeof core.collectEventAttachmentRefs === 'function'
|
|
86
|
-
? core.collectEventAttachmentRefs(events.slice(last))
|
|
87
|
-
: []
|
|
88
|
-
// Advance even when no refs are present so irrelevant durable history is
|
|
89
|
-
// never rescanned indefinitely.
|
|
90
|
-
store.setScannedEventSeq(session, events.length)
|
|
91
|
-
if (Array.isArray(refs) && refs.length > 0) store.recordAttachments(session, refs)
|
|
92
|
-
}
|
|
93
|
-
|
|
102
|
+
// Synchronous lookup is cache-only. Projected short handles first warm this
|
|
103
|
+
// cache from current derived messages and must not trigger historical I/O.
|
|
94
104
|
const lookupAttachment = (session, id) => {
|
|
95
105
|
if (!primitiveLookup) return undefined
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
// Bounded cache eviction is a performance event, never a correctness
|
|
105
|
-
// event. Recover only the requested durable ref instead of rebuilding an
|
|
106
|
-
// unbounded attachment index.
|
|
107
|
-
const events = sessionEvents(session)
|
|
108
|
-
if (!events || events.length === 0 || typeof core.collectEventAttachmentRefs !== 'function') {
|
|
109
|
-
return undefined
|
|
106
|
+
return primitiveLookup(session, id)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const recoverAttachmentsFromEvents = (session, ids, events) => {
|
|
110
|
+
const found = new Map()
|
|
111
|
+
if (!Array.isArray(events) || events.length === 0 || typeof core.collectEventAttachmentRefs !== 'function') {
|
|
112
|
+
return found
|
|
110
113
|
}
|
|
111
|
-
const wanted = String(id)
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
+
const wanted = new Set(ids.map((id) => String(id)))
|
|
115
|
+
for (const ref of core.collectEventAttachmentRefs(events)) {
|
|
116
|
+
if (!ref) continue
|
|
117
|
+
const id = String(ref.attachmentId ?? ref.id)
|
|
118
|
+
if (wanted.has(id) && !found.has(id)) found.set(id, ref)
|
|
119
|
+
}
|
|
120
|
+
if (found.size > 0) store.recordAttachments(session, [...found.values()])
|
|
121
|
+
return found
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const warnAttachmentRecoveryFailure = (session, id, error) => {
|
|
125
|
+
const message = boundedMessage(error)
|
|
126
|
+
const key = `${String(id)}:${message}`
|
|
127
|
+
if (attachmentRecoveryWarnings.get(session) === key) return
|
|
128
|
+
attachmentRecoveryWarnings.set(session, key)
|
|
129
|
+
logger?.warn?.(
|
|
130
|
+
'vision-router: session attachment recovery failed id=%s error=%s',
|
|
131
|
+
String(id),
|
|
132
|
+
message,
|
|
114
133
|
)
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const resolveAttachments = async (session, ids) => {
|
|
137
|
+
const requested = [...new Set((Array.isArray(ids) ? ids : []).map((id) => String(id)))]
|
|
138
|
+
const resolved = new Map()
|
|
139
|
+
const missing = []
|
|
140
|
+
for (const id of requested) {
|
|
141
|
+
const cached = lookupAttachment(session, id)
|
|
142
|
+
if (cached !== undefined) resolved.set(id, cached)
|
|
143
|
+
else missing.push(id)
|
|
118
144
|
}
|
|
119
|
-
return
|
|
145
|
+
if (missing.length === 0 || session === undefined) return resolved
|
|
146
|
+
|
|
147
|
+
let events
|
|
148
|
+
if (typeof readSessionLog === 'function') {
|
|
149
|
+
let result
|
|
150
|
+
try {
|
|
151
|
+
result = await readSessionLog(session)
|
|
152
|
+
} catch (error) {
|
|
153
|
+
warnAttachmentRecoveryFailure(session, missing[0], error)
|
|
154
|
+
return resolved
|
|
155
|
+
}
|
|
156
|
+
if (result?.supported === true) {
|
|
157
|
+
attachmentRecoveryWarnings.delete(session)
|
|
158
|
+
events = result.events
|
|
159
|
+
} else if (result?.supported !== false) {
|
|
160
|
+
warnAttachmentRecoveryFailure(
|
|
161
|
+
session,
|
|
162
|
+
missing[0],
|
|
163
|
+
new Error('Session log reader returned an invalid capability result'),
|
|
164
|
+
)
|
|
165
|
+
return resolved
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Historical/partial Hosts without SessionQuery keep the released fallback.
|
|
170
|
+
// Public rc8+ Hosts use readSessionLog above and never touch sync history.
|
|
171
|
+
if (events === undefined) events = legacySessionEvents(session)
|
|
172
|
+
const recovered = recoverAttachmentsFromEvents(session, missing, events)
|
|
173
|
+
for (const [id, ref] of recovered) resolved.set(id, ref)
|
|
174
|
+
return resolved
|
|
120
175
|
}
|
|
121
176
|
|
|
122
|
-
const
|
|
123
|
-
|
|
177
|
+
const resolveAttachment = async (session, id) => {
|
|
178
|
+
return (await resolveAttachments(session, [id])).get(String(id))
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const pendingSurfaceScan = (scans, session) => {
|
|
182
|
+
if (!session) return undefined
|
|
124
183
|
const nodes = surfaceNodes(session)
|
|
125
|
-
if (!nodes || nodes.length === 0) return
|
|
184
|
+
if (!nodes || nodes.length === 0) return undefined
|
|
126
185
|
let scan = scans.get(session)
|
|
127
186
|
if (!scan) {
|
|
128
187
|
scan = { count: 0 }
|
|
@@ -131,9 +190,88 @@ export function createSessionVisionIndex({
|
|
|
131
190
|
// Compaction/resume may shrink/rebuild the surface. Reset the incremental
|
|
132
191
|
// cursor rather than assuming old indices still refer to the new surface.
|
|
133
192
|
if (scan.count > nodes.length) scan.count = 0
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
return
|
|
193
|
+
const count = nodes.length
|
|
194
|
+
const seqs = nodes.slice(scan.count)
|
|
195
|
+
if (seqs.length === 0) return undefined
|
|
196
|
+
return {
|
|
197
|
+
seqs,
|
|
198
|
+
advance() {
|
|
199
|
+
if (scan.count < count) scan.count += 1
|
|
200
|
+
},
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const warnRepairReadFailure = (session, seq, error) => {
|
|
205
|
+
const message = boundedMessage(error)
|
|
206
|
+
const previous = repairReadWarnings.get(session)
|
|
207
|
+
if (previous?.seq === seq && previous.message === message) return
|
|
208
|
+
repairReadWarnings.set(session, { seq, message })
|
|
209
|
+
logger?.warn?.(
|
|
210
|
+
'vision-router: session surface event read failed seq=%s error=%s',
|
|
211
|
+
seq,
|
|
212
|
+
message,
|
|
213
|
+
)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const eventForSurfaceRepair = async (session, seq) => {
|
|
217
|
+
if (typeof readSessionEvent === 'function') {
|
|
218
|
+
let result
|
|
219
|
+
try {
|
|
220
|
+
result = await readSessionEvent(session, seq)
|
|
221
|
+
} catch (error) {
|
|
222
|
+
warnRepairReadFailure(session, seq, error)
|
|
223
|
+
return { readable: false }
|
|
224
|
+
}
|
|
225
|
+
if (result?.supported === true) {
|
|
226
|
+
repairReadWarnings.delete(session)
|
|
227
|
+
return { readable: true, event: result.event }
|
|
228
|
+
}
|
|
229
|
+
if (result?.supported !== false) {
|
|
230
|
+
warnRepairReadFailure(session, seq, new Error('Session event reader returned an invalid capability result'))
|
|
231
|
+
return { readable: false }
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Isolated/partial Hosts that do not expose SessionQuery retain the released
|
|
236
|
+
// Session-local compatibility path. Supported production Hosts install the
|
|
237
|
+
// event feed and never enter this compatibility reader.
|
|
238
|
+
const events = legacySessionEvents(session)
|
|
239
|
+
if (!events) return { readable: false }
|
|
240
|
+
return { readable: true, event: events[seq] }
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const surfaceRepairSnapshot = async (session, seq) => {
|
|
244
|
+
if (typeof readSessionLog !== 'function') return undefined
|
|
245
|
+
let result
|
|
246
|
+
try {
|
|
247
|
+
result = await readSessionLog(session)
|
|
248
|
+
} catch (error) {
|
|
249
|
+
warnRepairReadFailure(session, seq, error)
|
|
250
|
+
return { readable: false }
|
|
251
|
+
}
|
|
252
|
+
if (result?.supported === true) {
|
|
253
|
+
if (!Array.isArray(result.events)) {
|
|
254
|
+
warnRepairReadFailure(session, seq, new Error('Session log reader returned no event log'))
|
|
255
|
+
return { readable: false }
|
|
256
|
+
}
|
|
257
|
+
repairReadWarnings.delete(session)
|
|
258
|
+
return { readable: true, events: result.events }
|
|
259
|
+
}
|
|
260
|
+
if (result?.supported !== false) {
|
|
261
|
+
warnRepairReadFailure(session, seq, new Error('Session log reader returned an invalid capability result'))
|
|
262
|
+
return { readable: false }
|
|
263
|
+
}
|
|
264
|
+
return undefined
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const eventFromRepairSnapshot = (session, events, seq) => {
|
|
268
|
+
const event = events?.[seq]
|
|
269
|
+
if (!event || typeof event !== 'object' || (event.seq !== undefined && event.seq !== seq)) {
|
|
270
|
+
warnRepairReadFailure(session, seq, new Error(`Session repair snapshot has no event at seq ${seq}`))
|
|
271
|
+
return { readable: false }
|
|
272
|
+
}
|
|
273
|
+
repairReadWarnings.delete(session)
|
|
274
|
+
return { readable: true, event }
|
|
137
275
|
}
|
|
138
276
|
|
|
139
277
|
const keepToolResultImages = () => {
|
|
@@ -143,60 +281,241 @@ export function createSessionVisionIndex({
|
|
|
143
281
|
return policy.ownership === 'native-image' || policy.ownership === 'vision-router-owned'
|
|
144
282
|
}
|
|
145
283
|
|
|
284
|
+
const replacementIntent = (session, seq) => {
|
|
285
|
+
const intent = sessionSurfaceReplacementIntent(session, seq)
|
|
286
|
+
if (intent !== undefined) return intent
|
|
287
|
+
if (!unsupportedSurfaceContracts.has(session)) {
|
|
288
|
+
unsupportedSurfaceContracts.add(session)
|
|
289
|
+
logger?.warn?.(
|
|
290
|
+
'vision-router: skipping durable session surface repair for unsupported Session format version=%s',
|
|
291
|
+
session?.header?.version ?? 'unknown',
|
|
292
|
+
)
|
|
293
|
+
}
|
|
294
|
+
return undefined
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const cachePendingRepairEvent = (pendingBySession, session, event) => {
|
|
298
|
+
if (!session || !Number.isSafeInteger(event?.seq) || event.seq < 0 || Object.is(event.seq, -0)) return
|
|
299
|
+
let pending = pendingBySession.get(session)
|
|
300
|
+
if (!pending) {
|
|
301
|
+
pending = new Map()
|
|
302
|
+
pendingBySession.set(session, pending)
|
|
303
|
+
}
|
|
304
|
+
if (!pending.has(event.seq) && pending.size >= MAX_PENDING_REPAIR_EVENTS) {
|
|
305
|
+
const oldest = pending.keys().next().value
|
|
306
|
+
if (oldest !== undefined) pending.delete(oldest)
|
|
307
|
+
if (!repairFeedOverflowWarnings.has(session)) {
|
|
308
|
+
repairFeedOverflowWarnings.add(session)
|
|
309
|
+
logger?.warn?.(
|
|
310
|
+
'vision-router: session surface repair event feed exceeded %s pending events; dropping oldest entry',
|
|
311
|
+
MAX_PENDING_REPAIR_EVENTS,
|
|
312
|
+
)
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
pending.set(event.seq, event)
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const planAgainstOneEvent = (event, planner) => {
|
|
319
|
+
if (!Number.isSafeInteger(event?.seq) || event.seq < 0 || Object.is(event.seq, -0)) return []
|
|
320
|
+
const events = { [event.seq]: event }
|
|
321
|
+
return planner(events, [event.seq])
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const recordSessionEvent = (session, event) => {
|
|
325
|
+
if (!session || !event || typeof event !== 'object') return
|
|
326
|
+
try {
|
|
327
|
+
if (typeof core.collectEventAttachmentRefs === 'function') {
|
|
328
|
+
const refs = core.collectEventAttachmentRefs([event])
|
|
329
|
+
if (Array.isArray(refs) && refs.length > 0) recordAttachments(session, refs)
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Replacement events are consequences of this repair path and must never
|
|
333
|
+
// be re-enqueued, otherwise a guard-stop replacement would shadow itself
|
|
334
|
+
// forever because its stable plugin-owned id is intentionally preserved.
|
|
335
|
+
if (isReplacementEvent(event)) return
|
|
336
|
+
|
|
337
|
+
if (event.type === 'tool/result' && typeof core.planToolResultImageShadows === 'function') {
|
|
338
|
+
const plans = planAgainstOneEvent(
|
|
339
|
+
event,
|
|
340
|
+
(events, seqs) => core.planToolResultImageShadows(events, seqs, () => true),
|
|
341
|
+
)
|
|
342
|
+
if (plans.length > 0) cachePendingRepairEvent(pendingToolRepairEvents, session, event)
|
|
343
|
+
} else if (event.type === 'user/message' && typeof core.planGuardStopShadows === 'function') {
|
|
344
|
+
const plans = planAgainstOneEvent(event, (events, seqs) => core.planGuardStopShadows(events, seqs))
|
|
345
|
+
if (plans.length > 0) cachePendingRepairEvent(pendingGuardRepairEvents, session, event)
|
|
346
|
+
}
|
|
347
|
+
repairFeedObserverWarnings.delete(session)
|
|
348
|
+
} catch (error) {
|
|
349
|
+
const message = boundedMessage(error)
|
|
350
|
+
if (repairFeedObserverWarnings.get(session) === message) return
|
|
351
|
+
repairFeedObserverWarnings.set(session, message)
|
|
352
|
+
logger?.warn?.('vision-router: session event feed observation failed error=%s', message)
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const activateSurfaceEventFeed = () => {
|
|
357
|
+
surfaceEventFeedActive = true
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const consumePendingRepairEvents = (pendingBySession, session) => {
|
|
361
|
+
const pending = pendingBySession.get(session)
|
|
362
|
+
if (!pending || pending.size === 0) return []
|
|
363
|
+
const nodes = surfaceNodes(session)
|
|
364
|
+
if (!nodes || nodes.length === 0) {
|
|
365
|
+
pending.clear()
|
|
366
|
+
return []
|
|
367
|
+
}
|
|
368
|
+
const current = new Set(nodes)
|
|
369
|
+
for (const seq of pending.keys()) {
|
|
370
|
+
if (!current.has(seq)) pending.delete(seq)
|
|
371
|
+
}
|
|
372
|
+
const entries = []
|
|
373
|
+
for (const seq of nodes) {
|
|
374
|
+
const event = pending.get(seq)
|
|
375
|
+
if (event === undefined) continue
|
|
376
|
+
pending.delete(seq)
|
|
377
|
+
entries.push({ seq, event })
|
|
378
|
+
}
|
|
379
|
+
return entries
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const repairToolResultSurfaceFromFeed = async (session) => {
|
|
383
|
+
if (typeof session?.append !== 'function' || typeof core.planToolResultImageShadows !== 'function') return 0
|
|
384
|
+
const pending = consumePendingRepairEvents(pendingToolRepairEvents, session)
|
|
385
|
+
if (pending.length === 0) return 0
|
|
386
|
+
const preserve = keepToolResultImages()
|
|
387
|
+
let repaired = 0
|
|
388
|
+
for (const { seq, event } of pending) {
|
|
389
|
+
const events = { [seq]: event }
|
|
390
|
+
const plans = core.planToolResultImageShadows(events, [seq], () => !preserve)
|
|
391
|
+
for (const plan of plans) {
|
|
392
|
+
try {
|
|
393
|
+
const intent = replacementIntent(session, plan.seq)
|
|
394
|
+
if (intent === undefined) continue
|
|
395
|
+
await session.append(
|
|
396
|
+
'tool/result',
|
|
397
|
+
{ ...plan.event.data, message: plan.message },
|
|
398
|
+
intent,
|
|
399
|
+
)
|
|
400
|
+
repaired += 1
|
|
401
|
+
} catch (error) {
|
|
402
|
+
logger?.warn?.(
|
|
403
|
+
'vision-router: session tool-result surface repair failed seq=%s error=%s',
|
|
404
|
+
plan.seq,
|
|
405
|
+
boundedMessage(error),
|
|
406
|
+
)
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
return repaired
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const repairGuardStopSurfaceFromFeed = async (session) => {
|
|
414
|
+
if (typeof session?.append !== 'function' || typeof core.planGuardStopShadows !== 'function') return 0
|
|
415
|
+
const pending = consumePendingRepairEvents(pendingGuardRepairEvents, session)
|
|
416
|
+
if (pending.length === 0) return 0
|
|
417
|
+
let repaired = 0
|
|
418
|
+
for (const { seq, event } of pending) {
|
|
419
|
+
const events = { [seq]: event }
|
|
420
|
+
const plans = core.planGuardStopShadows(events, [seq])
|
|
421
|
+
for (const plan of plans) {
|
|
422
|
+
try {
|
|
423
|
+
const intent = replacementIntent(session, plan.seq)
|
|
424
|
+
if (intent === undefined) continue
|
|
425
|
+
await session.append(
|
|
426
|
+
'user/message',
|
|
427
|
+
plan.data,
|
|
428
|
+
intent,
|
|
429
|
+
)
|
|
430
|
+
repaired += 1
|
|
431
|
+
} catch (error) {
|
|
432
|
+
logger?.warn?.(
|
|
433
|
+
'vision-router: session guard-stop surface repair failed seq=%s error=%s',
|
|
434
|
+
plan.seq,
|
|
435
|
+
boundedMessage(error),
|
|
436
|
+
)
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
return repaired
|
|
441
|
+
}
|
|
442
|
+
|
|
146
443
|
const repairToolResultSurface = async (session) => {
|
|
147
|
-
|
|
148
|
-
if (
|
|
149
|
-
const
|
|
150
|
-
if (
|
|
444
|
+
if (surfaceEventFeedActive) return repairToolResultSurfaceFromFeed(session)
|
|
445
|
+
if (typeof session?.append !== 'function') return 0
|
|
446
|
+
const scan = pendingSurfaceScan(toolSurfaceScans, session)
|
|
447
|
+
if (!scan || typeof core.planToolResultImageShadows !== 'function') return 0
|
|
151
448
|
const preserve = keepToolResultImages()
|
|
152
|
-
const
|
|
449
|
+
const snapshot = await surfaceRepairSnapshot(session, scan.seqs[0])
|
|
450
|
+
if (snapshot?.readable === false) return 0
|
|
153
451
|
let repaired = 0
|
|
154
|
-
for (const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
452
|
+
for (const seq of scan.seqs) {
|
|
453
|
+
const read = snapshot?.readable === true
|
|
454
|
+
? eventFromRepairSnapshot(session, snapshot.events, seq)
|
|
455
|
+
: await eventForSurfaceRepair(session, seq)
|
|
456
|
+
if (!read.readable) break
|
|
457
|
+
// A readable node is settled for this repair pass even when it needs no
|
|
458
|
+
// replacement or append later fails; only an unread node remains pending.
|
|
459
|
+
scan.advance()
|
|
460
|
+
const events = []
|
|
461
|
+
events[seq] = read.event
|
|
462
|
+
const plans = core.planToolResultImageShadows(events, [seq], () => !preserve)
|
|
463
|
+
for (const plan of plans) {
|
|
464
|
+
try {
|
|
465
|
+
const intent = replacementIntent(session, plan.seq)
|
|
466
|
+
if (intent === undefined) continue
|
|
467
|
+
await session.append(
|
|
468
|
+
'tool/result',
|
|
469
|
+
{ ...plan.event.data, message: plan.message },
|
|
470
|
+
intent,
|
|
471
|
+
)
|
|
472
|
+
repaired += 1
|
|
473
|
+
} catch (error) {
|
|
474
|
+
logger?.warn?.(
|
|
475
|
+
'vision-router: session tool-result surface repair failed seq=%s error=%s',
|
|
476
|
+
plan.seq,
|
|
477
|
+
boundedMessage(error),
|
|
478
|
+
)
|
|
479
|
+
}
|
|
171
480
|
}
|
|
172
481
|
}
|
|
173
482
|
return repaired
|
|
174
483
|
}
|
|
175
484
|
|
|
176
485
|
const repairGuardStopSurface = async (session) => {
|
|
177
|
-
|
|
178
|
-
if (
|
|
179
|
-
const
|
|
180
|
-
if (
|
|
181
|
-
const
|
|
486
|
+
if (surfaceEventFeedActive) return repairGuardStopSurfaceFromFeed(session)
|
|
487
|
+
if (typeof session?.append !== 'function') return 0
|
|
488
|
+
const scan = pendingSurfaceScan(guardSurfaceScans, session)
|
|
489
|
+
if (!scan || typeof core.planGuardStopShadows !== 'function') return 0
|
|
490
|
+
const snapshot = await surfaceRepairSnapshot(session, scan.seqs[0])
|
|
491
|
+
if (snapshot?.readable === false) return 0
|
|
182
492
|
let repaired = 0
|
|
183
|
-
for (const
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
493
|
+
for (const seq of scan.seqs) {
|
|
494
|
+
const read = snapshot?.readable === true
|
|
495
|
+
? eventFromRepairSnapshot(session, snapshot.events, seq)
|
|
496
|
+
: await eventForSurfaceRepair(session, seq)
|
|
497
|
+
if (!read.readable) break
|
|
498
|
+
scan.advance()
|
|
499
|
+
const events = []
|
|
500
|
+
events[seq] = read.event
|
|
501
|
+
const plans = core.planGuardStopShadows(events, [seq])
|
|
502
|
+
for (const plan of plans) {
|
|
503
|
+
try {
|
|
504
|
+
const intent = replacementIntent(session, plan.seq)
|
|
505
|
+
if (intent === undefined) continue
|
|
506
|
+
await session.append(
|
|
507
|
+
'user/message',
|
|
508
|
+
plan.data,
|
|
509
|
+
intent,
|
|
510
|
+
)
|
|
511
|
+
repaired += 1
|
|
512
|
+
} catch (error) {
|
|
513
|
+
logger?.warn?.(
|
|
514
|
+
'vision-router: session guard-stop surface repair failed seq=%s error=%s',
|
|
515
|
+
plan.seq,
|
|
516
|
+
boundedMessage(error),
|
|
517
|
+
)
|
|
518
|
+
}
|
|
200
519
|
}
|
|
201
520
|
}
|
|
202
521
|
return repaired
|
|
@@ -213,7 +532,6 @@ export function createSessionVisionIndex({
|
|
|
213
532
|
store.recordAttachments(session, found.attachments)
|
|
214
533
|
}
|
|
215
534
|
}
|
|
216
|
-
scanEventLog(session)
|
|
217
535
|
await repairToolResultSurface(session)
|
|
218
536
|
await repairGuardStopSurface(session)
|
|
219
537
|
return decision
|
|
@@ -221,8 +539,11 @@ export function createSessionVisionIndex({
|
|
|
221
539
|
|
|
222
540
|
return Object.freeze({
|
|
223
541
|
recordAttachments,
|
|
224
|
-
|
|
542
|
+
recordSessionEvent,
|
|
543
|
+
activateSurfaceEventFeed,
|
|
225
544
|
lookupAttachment,
|
|
545
|
+
resolveAttachment,
|
|
546
|
+
resolveAttachments,
|
|
226
547
|
repairToolResultSurface,
|
|
227
548
|
repairGuardStopSurface,
|
|
228
549
|
prepareDecision,
|
|
@@ -231,8 +552,9 @@ export function createSessionVisionIndex({
|
|
|
231
552
|
|
|
232
553
|
/**
|
|
233
554
|
* Intercept core's pre-step registration and decorate only its downstream
|
|
234
|
-
* `next()` result. Production
|
|
235
|
-
*
|
|
555
|
+
* `next()` result. Production also subscribes the index to DSH's post-commit
|
|
556
|
+
* `session/event` feed before activating feed-backed surface repair. Direct/test
|
|
557
|
+
* callers without a usable event feed retain the compatibility reader path.
|
|
236
558
|
*/
|
|
237
559
|
export function installSessionVisionIndexBoundary(ctx, config, core, options = {}) {
|
|
238
560
|
if (!isObject(ctx)) return ctx
|
|
@@ -250,6 +572,25 @@ export function installSessionVisionIndexBoundary(ctx, config, core, options = {
|
|
|
250
572
|
logger: options.logger,
|
|
251
573
|
})
|
|
252
574
|
|
|
575
|
+
try {
|
|
576
|
+
const on = Reflect.get(ctx, 'on', ctx)
|
|
577
|
+
if (
|
|
578
|
+
typeof on === 'function'
|
|
579
|
+
&& typeof index.recordSessionEvent === 'function'
|
|
580
|
+
&& typeof index.activateSurfaceEventFeed === 'function'
|
|
581
|
+
) {
|
|
582
|
+
on.call(ctx, 'session/event', (session, event) => {
|
|
583
|
+
index.recordSessionEvent(session, event)
|
|
584
|
+
})
|
|
585
|
+
index.activateSurfaceEventFeed()
|
|
586
|
+
}
|
|
587
|
+
} catch (error) {
|
|
588
|
+
options.logger?.warn?.(
|
|
589
|
+
'vision-router: session event feed unavailable; retaining compatibility surface reader error=%s',
|
|
590
|
+
boundedMessage(error),
|
|
591
|
+
)
|
|
592
|
+
}
|
|
593
|
+
|
|
253
594
|
return new Proxy(ctx, {
|
|
254
595
|
get(target, property) {
|
|
255
596
|
if (property === 'on') {
|