dsh-vision-router 2.2.0 → 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 +3 -3
- package/README.zh.md +3 -3
- package/docs/releases/v2.2.1.md +22 -0
- package/lib/core-primitives.js +11 -0
- package/lib/dsh-contract-compat.js +28 -5
- package/lib/fetch-wrapper-lifecycle.js +56 -0
- package/lib/legacy-global-proxy-boundary.js +4 -3
- package/lib/pi-ai-bridge-wire-compat.js +4 -3
- package/lib/pixel-diff-stream.js +12 -6
- package/lib/session-vision-index.js +238 -10
- package/lib/vision-breaker-shadow-health.js +11 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
</p>
|
|
18
18
|
|
|
19
19
|
<p align="center">
|
|
20
|
-
<a href="https://github.com/ysr666/dsh-vision-router/releases/tag/v2.2.
|
|
20
|
+
<a href="https://github.com/ysr666/dsh-vision-router/releases/tag/v2.2.1"><img src="https://img.shields.io/badge/release-v2.2.1-5B4CF0?style=flat-square" alt="Release v2.2.1" /></a>
|
|
21
21
|
<a href="tests"><img src="https://img.shields.io/badge/verified-Node%2022%20%2B%2024-2EA44F?style=flat-square" alt="Verified: Node 22 + 24" /></a>
|
|
22
22
|
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-2EA44F?style=flat-square" alt="License: MIT" /></a>
|
|
23
23
|
<a href="package.json"><img src="https://img.shields.io/badge/Node.js-%3E%3D22-339933?style=flat-square&logo=nodedotjs&logoColor=white" alt="Node.js >=22" /></a>
|
|
@@ -39,9 +39,9 @@
|
|
|
39
39
|
<p align="center">💬 <strong>QQ community group: 1105463028</strong></p>
|
|
40
40
|
|
|
41
41
|
> [!WARNING]
|
|
42
|
-
> 📌 **Announcement (v2.2.
|
|
42
|
+
> 📌 **Announcement (v2.2.1)**
|
|
43
43
|
>
|
|
44
|
-
> **v2.2.
|
|
44
|
+
> **v2.2.1:** Stability hotfix for large-session repair, accessor-based fetch composition, non-divisible pixel-diff bounds, and local Ollama answer-budget preservation — without raising the rc.8 Host floor. [What’s new →](docs/releases/v2.2.1.md)
|
|
45
45
|
|
|
46
46
|
<p align="center">
|
|
47
47
|
<img src="assets/vision-demo.gif" width="640" alt="Demo: paste an image, the agent locates the send button with vision_ground / vision_crop / vision_pixel_diff and answers with coordinates" />
|
package/README.zh.md
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
</p>
|
|
18
18
|
|
|
19
19
|
<p align="center">
|
|
20
|
-
<a href="https://github.com/ysr666/dsh-vision-router/releases/tag/v2.2.
|
|
20
|
+
<a href="https://github.com/ysr666/dsh-vision-router/releases/tag/v2.2.1"><img src="https://img.shields.io/badge/release-v2.2.1-5B4CF0?style=flat-square" alt="Release v2.2.1" /></a>
|
|
21
21
|
<a href="tests"><img src="https://img.shields.io/badge/verified-Node%2022%20%2B%2024-2EA44F?style=flat-square" alt="已验证 Node 22 + 24" /></a>
|
|
22
22
|
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-2EA44F?style=flat-square" alt="MIT 许可证" /></a>
|
|
23
23
|
<a href="package.json"><img src="https://img.shields.io/badge/Node.js-%3E%3D22-339933?style=flat-square&logo=nodedotjs&logoColor=white" alt="Node.js >=22" /></a>
|
|
@@ -39,9 +39,9 @@
|
|
|
39
39
|
<p align="center">💬 <strong>QQ 用户交流群:1105463028</strong></p>
|
|
40
40
|
|
|
41
41
|
> [!WARNING]
|
|
42
|
-
> 📌 **公告(v2.2.
|
|
42
|
+
> 📌 **公告(v2.2.1)**
|
|
43
43
|
>
|
|
44
|
-
> **v2.2.
|
|
44
|
+
> **v2.2.1:稳定性热修——修复大型 Session repair 卡死、accessor fetch 组合、非整除 pixel diff 边界与本地 Ollama 推理占满答案预算;公开最低 Host 仍保持 `0.1.0-rc.8`。** [查看完整更新 →](docs/releases/v2.2.1.md)
|
|
45
45
|
|
|
46
46
|
<p align="center">
|
|
47
47
|
<img src="assets/vision-demo.gif" width="640" alt="演示:粘贴图片,Agent 用 vision_ground / vision_crop / vision_pixel_diff 定位发送按钮并给出坐标" />
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# v2.2.1
|
|
2
|
+
|
|
3
|
+
DSH Vision Router 2.2.1 is a focused stability and compatibility hotfix for the 2.2.0 line.
|
|
4
|
+
|
|
5
|
+
## Highlights
|
|
6
|
+
|
|
7
|
+
- Fixes the large-session repair regressions in #524 and #532. On supported DSH Hosts, production tool-result and guard-stop repair now consumes bounded post-commit `session/event` entries and performs zero live Session-history reads: no `sessionQuery.readEvent()`, no `observeSession.events` materialization, and no new `eventAt()` / `snapshotEvents()` dependency. This removes the original full-clone amplification and the later `Unable to deserialize cloned data` / Windows Node 24 native-crash path.
|
|
8
|
+
- Fixes global `fetch` compatibility composition with accessor-based Host/plugin pipelines (#519), preventing wrapper cycles while preserving live Host middleware authority.
|
|
9
|
+
- Fixes `vision_pixel_diff` worst-region bounds on non-divisible image/grid dimensions (#520).
|
|
10
|
+
- Fixes the confirmed Ollama portion of #526 by disabling hidden reasoning for generated local Ollama vision providers on the OpenAI-compatible Chat Completions path, preserving the bounded token budget for visible answer text.
|
|
11
|
+
- Completes the release-channel canary fixture so stable/latest and preview/alpha validate the same Session publication family before Host contract smoke.
|
|
12
|
+
|
|
13
|
+
## Compatibility
|
|
14
|
+
|
|
15
|
+
- Node 22 and Node 24 remain supported.
|
|
16
|
+
- Windows, macOS, and Linux remain gated.
|
|
17
|
+
- The public minimum DSH Host remains `0.1.0-rc.8`.
|
|
18
|
+
- Existing 2.2.0 settings require no migration.
|
|
19
|
+
|
|
20
|
+
## Known follow-ups
|
|
21
|
+
|
|
22
|
+
- #526 remains open for the LM Studio reasoning/budget path; 2.2.1 does not send Ollama-specific `reasoning_effort` to LM Studio without a documented LM Studio wire contract.
|
package/lib/core-primitives.js
CHANGED
|
@@ -1858,6 +1858,10 @@ export function localOllamaProvidersOf(config) {
|
|
|
1858
1858
|
model,
|
|
1859
1859
|
apiKeyEnv: '',
|
|
1860
1860
|
maxTokens: 2048,
|
|
1861
|
+
// Ollama's OpenAI-compatible Chat Completions endpoint supports
|
|
1862
|
+
// reasoning_effort='none'. Vision extraction should spend the bounded
|
|
1863
|
+
// completion budget on observable answer text rather than hidden thought.
|
|
1864
|
+
reasoningEffort: 'none',
|
|
1861
1865
|
// 仅显式选择 anthropic 格式时携带(默认 openai 路径保持字节不变)。
|
|
1862
1866
|
...(local.format === 'anthropic' ? { format: 'anthropic' } : {}),
|
|
1863
1867
|
// 建议值透传:温度/top_p 只在显式配置时携带(callOpenAICompatible
|
|
@@ -1978,6 +1982,9 @@ export async function callLocalBackend(provider, messages, options = {}) {
|
|
|
1978
1982
|
...(typeof options.resolveCredential === 'function'
|
|
1979
1983
|
? { resolveCredential: options.resolveCredential }
|
|
1980
1984
|
: {}),
|
|
1985
|
+
...(typeof provider.reasoningEffort === 'string'
|
|
1986
|
+
? { reasoningEffort: provider.reasoningEffort }
|
|
1987
|
+
: {}),
|
|
1981
1988
|
...sampling,
|
|
1982
1989
|
})
|
|
1983
1990
|
}
|
|
@@ -2121,6 +2128,7 @@ export async function callOpenAICompatible(provider, messages, options = {}) {
|
|
|
2121
2128
|
if (resolvedApiKey === '') throw new Error(`http provider "${provider.name}": ${apiKeyEnv} is not set`)
|
|
2122
2129
|
headers.authorization = `Bearer ${resolvedApiKey}`
|
|
2123
2130
|
}
|
|
2131
|
+
const reasoningEffort = options.reasoningEffort ?? provider.reasoningEffort
|
|
2124
2132
|
const body = {
|
|
2125
2133
|
model: provider.model,
|
|
2126
2134
|
messages,
|
|
@@ -2130,6 +2138,9 @@ export async function callOpenAICompatible(provider, messages, options = {}) {
|
|
|
2130
2138
|
// never pass them, so the wire body stays byte-identical for main paths.
|
|
2131
2139
|
...(typeof options.temperature === 'number' ? { temperature: options.temperature } : {}),
|
|
2132
2140
|
...(typeof options.top_p === 'number' ? { top_p: options.top_p } : {}),
|
|
2141
|
+
...(['none', 'low', 'medium', 'high', 'max'].includes(reasoningEffort)
|
|
2142
|
+
? { reasoning_effort: reasoningEffort }
|
|
2143
|
+
: {}),
|
|
2133
2144
|
}
|
|
2134
2145
|
const url = `${provider.baseURL.replace(/\/$/, '')}/chat/completions`
|
|
2135
2146
|
const request = () =>
|
|
@@ -192,17 +192,40 @@ export function createSessionEventTailReader(ctx) {
|
|
|
192
192
|
|
|
193
193
|
|
|
194
194
|
/**
|
|
195
|
-
* Build the
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
*
|
|
195
|
+
* Build the asynchronous Session-log reader used by cold attachment recovery
|
|
196
|
+
* and batched surface repair. Prefer SessionQuery's observation lease when it
|
|
197
|
+
* exists: one exact live cut materializes at most one immutable event snapshot,
|
|
198
|
+
* rather than cloning the whole live corpus once per requested event (#524).
|
|
199
|
+
* Hosts without that capability retain the released readSession() contract.
|
|
199
200
|
*/
|
|
200
201
|
export function createSessionLogReader(ctx) {
|
|
201
202
|
return async function readSessionLog(session) {
|
|
202
203
|
const sessionId = session?.id
|
|
203
204
|
if (sessionId === undefined || sessionId === null) return { supported: false }
|
|
204
205
|
const query = sessionQueryOf(ctx)
|
|
205
|
-
if (!query
|
|
206
|
+
if (!query) return { supported: false }
|
|
207
|
+
|
|
208
|
+
if (typeof query.observeSession === 'function') {
|
|
209
|
+
let observation
|
|
210
|
+
try {
|
|
211
|
+
observation = await query.observeSession(sessionId, { projectionMode: 'none' })
|
|
212
|
+
if (!observation || !Array.isArray(observation.events)) {
|
|
213
|
+
throw new Error('sessionQuery.observeSession returned no event log')
|
|
214
|
+
}
|
|
215
|
+
const observedId = observation.header?.id
|
|
216
|
+
if (observedId !== undefined && observedId !== sessionId) {
|
|
217
|
+
throw new Error(
|
|
218
|
+
`sessionQuery.observeSession returned session ${String(observedId)} for requested ${String(sessionId)}`,
|
|
219
|
+
)
|
|
220
|
+
}
|
|
221
|
+
return { supported: true, events: observation.events }
|
|
222
|
+
} finally {
|
|
223
|
+
const dispose = observation?.[Symbol.dispose]
|
|
224
|
+
if (typeof dispose === 'function') dispose.call(observation)
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (typeof query.readSession !== 'function') return { supported: false }
|
|
206
229
|
const snapshot = await query.readSession(sessionId)
|
|
207
230
|
if (!snapshot || !Array.isArray(snapshot.events)) {
|
|
208
231
|
throw new Error('sessionQuery.readSession returned no event log')
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Lifecycle metadata only: no native fetch snapshot, dispatcher, or egress
|
|
2
|
+
// policy is retained here. Weak keys let abandoned wrapper chains be collected.
|
|
3
|
+
const installations = new WeakMap()
|
|
4
|
+
|
|
5
|
+
function livePreviousDescriptor(descriptor) {
|
|
6
|
+
let previous = descriptor
|
|
7
|
+
while (previous && Object.hasOwn(previous, 'value')) {
|
|
8
|
+
const installation = installations.get(previous.value)
|
|
9
|
+
if (!installation?.disposed) break
|
|
10
|
+
previous = installation.previous
|
|
11
|
+
}
|
|
12
|
+
return previous
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Preserve a live accessor-owned pipeline, including middleware recomposition. */
|
|
16
|
+
export function captureFetchDelegate(target = globalThis) {
|
|
17
|
+
const descriptor = Object.getOwnPropertyDescriptor(target, 'fetch')
|
|
18
|
+
const current = target.fetch
|
|
19
|
+
if (typeof current !== 'function' || typeof descriptor?.get !== 'function') return current
|
|
20
|
+
return (input, init) => Reflect.apply(Reflect.apply(descriptor.get, target, []), target, [input, init])
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Publish an already constructed fetch wrapper without calling a foreign
|
|
25
|
+
* accessor setter. Assignment is unsafe: a pipeline getter may return a
|
|
26
|
+
* function whose setter adopts our wrapper as its own delegate (#519).
|
|
27
|
+
*
|
|
28
|
+
* The caller captures the CURRENT Host chain at installation, never at module
|
|
29
|
+
* load, and disables its own behavior before restoring. Later Host replacements
|
|
30
|
+
* remain authoritative. Restore the exact previous descriptor only while our
|
|
31
|
+
* data property still owns the surface; never feed a wrapper back to a setter.
|
|
32
|
+
* Non-configurable accessors cannot be composed this way and fail before any
|
|
33
|
+
* mutation rather than silently bypassing a Host policy or explicit proxy.
|
|
34
|
+
*/
|
|
35
|
+
export function installFetchWrapper(wrapped, target = globalThis) {
|
|
36
|
+
if (typeof wrapped !== 'function') throw new TypeError('fetch wrapper must be a function')
|
|
37
|
+
const previous = Object.getOwnPropertyDescriptor(target, 'fetch')
|
|
38
|
+
if (previous && Object.hasOwn(previous, 'value') && previous.value === wrapped) return () => {}
|
|
39
|
+
const installed = previous && Object.hasOwn(previous, 'value')
|
|
40
|
+
? { ...previous, value: wrapped }
|
|
41
|
+
: { configurable: true, enumerable: previous?.enumerable ?? true, writable: true, value: wrapped }
|
|
42
|
+
Object.defineProperty(target, 'fetch', installed)
|
|
43
|
+
const installation = { previous, disposed: false }
|
|
44
|
+
installations.set(wrapped, installation)
|
|
45
|
+
return () => {
|
|
46
|
+
if (installation.disposed) return
|
|
47
|
+
installation.disposed = true
|
|
48
|
+
const current = Object.getOwnPropertyDescriptor(target, 'fetch')
|
|
49
|
+
if (!current || !Object.hasOwn(current, 'value') || current.value !== wrapped ||
|
|
50
|
+
current.configurable !== installed.configurable ||
|
|
51
|
+
current.enumerable !== installed.enumerable || current.writable !== installed.writable) return
|
|
52
|
+
const restore = livePreviousDescriptor(previous)
|
|
53
|
+
if (restore) Object.defineProperty(target, 'fetch', restore)
|
|
54
|
+
else delete target.fetch
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { captureFetchDelegate, installFetchWrapper } from './fetch-wrapper-lifecycle.js'
|
|
1
2
|
// H2: retain the legacy Host-owned proxy override as a scoped compatibility
|
|
2
3
|
// transport, not as process-wide routing authority. The wrapper is always
|
|
3
4
|
// transparent unless one DVR-owned visual adapter call is active (or the
|
|
@@ -184,7 +185,7 @@ function proxyHostsOf(config) {
|
|
|
184
185
|
* the narrow legacy direct whole-turn fallback above.
|
|
185
186
|
*/
|
|
186
187
|
export function installLegacyGlobalProxyBoundary(ctx, config = {}, options = {}) {
|
|
187
|
-
const originalFetch = typeof options.originalFetch === 'function' ? options.originalFetch :
|
|
188
|
+
const originalFetch = typeof options.originalFetch === 'function' ? options.originalFetch : captureFetchDelegate()
|
|
188
189
|
const importUndici = typeof options.importUndici === 'function' ? options.importUndici : () => import('undici')
|
|
189
190
|
if (typeof originalFetch !== 'function') return () => {}
|
|
190
191
|
|
|
@@ -227,14 +228,14 @@ export function installLegacyGlobalProxyBoundary(ctx, config = {}, options = {})
|
|
|
227
228
|
})
|
|
228
229
|
}
|
|
229
230
|
|
|
230
|
-
|
|
231
|
+
const restoreFetch = installFetchWrapper(scopedFetch)
|
|
231
232
|
let disposed = false
|
|
232
233
|
const dispose = () => {
|
|
233
234
|
if (disposed) return
|
|
234
235
|
disposed = true
|
|
235
236
|
active = false
|
|
236
237
|
void dispatcherPool.dispose()
|
|
237
|
-
|
|
238
|
+
restoreFetch()
|
|
238
239
|
}
|
|
239
240
|
if (typeof ctx?.effect === 'function') {
|
|
240
241
|
try {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { captureFetchDelegate, installFetchWrapper } from './fetch-wrapper-lifecycle.js'
|
|
1
2
|
import { currentVisionSessionAffinityId } from './session-affinity-runtime.js'
|
|
2
3
|
import { isOfficialOpenCodeGoUrl, openCodeSessionAffinityHeaderForUrl } from './session-affinity.js'
|
|
3
4
|
|
|
@@ -283,7 +284,7 @@ export function applyPiAiBridgeWireFacts(init, body, facts) {
|
|
|
283
284
|
* wrapper even if another later patch sits above it in the fetch chain.
|
|
284
285
|
*/
|
|
285
286
|
export function installPiAiBridgeWireCompat(ctx, logger) {
|
|
286
|
-
const original =
|
|
287
|
+
const original = captureFetchDelegate()
|
|
287
288
|
if (typeof original !== 'function') return () => {}
|
|
288
289
|
let active = true
|
|
289
290
|
const wrapped = async (input, init) => {
|
|
@@ -334,10 +335,10 @@ export function installPiAiBridgeWireCompat(ctx, logger) {
|
|
|
334
335
|
}
|
|
335
336
|
return Reflect.apply(original, globalThis, [input, patchedInit])
|
|
336
337
|
}
|
|
337
|
-
|
|
338
|
+
const restoreFetch = installFetchWrapper(wrapped)
|
|
338
339
|
const cleanup = () => {
|
|
339
340
|
active = false
|
|
340
|
-
|
|
341
|
+
restoreFetch()
|
|
341
342
|
}
|
|
342
343
|
try {
|
|
343
344
|
ctx?.effect?.(() => cleanup, 'vision-router: pi-ai bridge wire compatibility')
|
package/lib/pixel-diff-stream.js
CHANGED
|
@@ -37,12 +37,18 @@ class AsyncByteReader {
|
|
|
37
37
|
function cellBounds(index, width, height, cols, rows) {
|
|
38
38
|
const cx = index % cols
|
|
39
39
|
const cy = Math.floor(index / cols)
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
40
|
+
// Uniform boundaries mirror the counting map floor(x * cells / extent)
|
|
41
|
+
// exactly: with cells <= extent (the grid clamp above guarantees it),
|
|
42
|
+
// ceil(c * extent / cells) rises by at least 1 per cell, so every cell is
|
|
43
|
+
// non-empty and every pixel lands inside the cell the counters credited
|
|
44
|
+
// it to. The previous ceil-stride bounds drifted from that map whenever
|
|
45
|
+
// extent was not a multiple of cells, shifting worst-region boxes and, on
|
|
46
|
+
// tiny extents, dropping counted pixels from cells whose bounds collapsed
|
|
47
|
+
// to zero width.
|
|
48
|
+
const x1 = Math.ceil((cx * width) / cols)
|
|
49
|
+
const y1 = Math.ceil((cy * height) / rows)
|
|
50
|
+
const x2 = Math.ceil(((cx + 1) * width) / cols)
|
|
51
|
+
const y2 = Math.ceil(((cy + 1) * height) / rows)
|
|
46
52
|
return { x1, y1, x2, y2, total: Math.max(0, x2 - x1) * Math.max(0, y2 - y1) }
|
|
47
53
|
}
|
|
48
54
|
|
|
@@ -2,6 +2,8 @@ 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
4
|
|
|
5
|
+
const MAX_PENDING_REPAIR_EVENTS = 256
|
|
6
|
+
|
|
5
7
|
function isObject(value) {
|
|
6
8
|
return value !== null && typeof value === 'object'
|
|
7
9
|
}
|
|
@@ -39,13 +41,23 @@ function boundedMessage(error) {
|
|
|
39
41
|
return String(text ?? '').slice(0, 400)
|
|
40
42
|
}
|
|
41
43
|
|
|
44
|
+
function isReplacementEvent(event) {
|
|
45
|
+
const op = event?.surfaceOp
|
|
46
|
+
return !!op && typeof op === 'object' && !Array.isArray(op) && op.op === 'replace'
|
|
47
|
+
}
|
|
48
|
+
|
|
42
49
|
/**
|
|
43
50
|
* Session visual data-plane index.
|
|
44
51
|
*
|
|
45
52
|
* SessionVisionStateStore remains the bounded storage owner. This object owns
|
|
46
|
-
* target-only durable attachment recovery
|
|
47
|
-
*
|
|
48
|
-
*
|
|
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.
|
|
49
61
|
*
|
|
50
62
|
* The index never replaces stateStore.lookupAttachment(). Cache lookup stays
|
|
51
63
|
* synchronous through lookupAttachment(); cold durable recovery is requested
|
|
@@ -73,16 +85,20 @@ export function createSessionVisionIndex({
|
|
|
73
85
|
: undefined
|
|
74
86
|
const toolSurfaceScans = new WeakMap()
|
|
75
87
|
const guardSurfaceScans = new WeakMap()
|
|
88
|
+
const pendingToolRepairEvents = new WeakMap()
|
|
89
|
+
const pendingGuardRepairEvents = new WeakMap()
|
|
76
90
|
const unsupportedSurfaceContracts = new WeakSet()
|
|
77
91
|
const repairReadWarnings = new WeakMap()
|
|
92
|
+
const repairFeedOverflowWarnings = new WeakSet()
|
|
93
|
+
const repairFeedObserverWarnings = new WeakMap()
|
|
78
94
|
const attachmentRecoveryWarnings = new WeakMap()
|
|
95
|
+
let surfaceEventFeedActive = false
|
|
79
96
|
|
|
80
97
|
const recordAttachments = (session, refs) => {
|
|
81
98
|
if (!session || !Array.isArray(refs) || refs.length === 0) return
|
|
82
99
|
store.recordAttachments(session, refs)
|
|
83
100
|
}
|
|
84
101
|
|
|
85
|
-
|
|
86
102
|
// Synchronous lookup is cache-only. Projected short handles first warm this
|
|
87
103
|
// cache from current derived messages and must not trigger historical I/O.
|
|
88
104
|
const lookupAttachment = (session, id) => {
|
|
@@ -217,13 +233,47 @@ export function createSessionVisionIndex({
|
|
|
217
233
|
}
|
|
218
234
|
|
|
219
235
|
// Isolated/partial Hosts that do not expose SessionQuery retain the released
|
|
220
|
-
// Session-local compatibility path. Supported production Hosts
|
|
221
|
-
//
|
|
236
|
+
// Session-local compatibility path. Supported production Hosts install the
|
|
237
|
+
// event feed and never enter this compatibility reader.
|
|
222
238
|
const events = legacySessionEvents(session)
|
|
223
239
|
if (!events) return { readable: false }
|
|
224
240
|
return { readable: true, event: events[seq] }
|
|
225
241
|
}
|
|
226
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 }
|
|
275
|
+
}
|
|
276
|
+
|
|
227
277
|
const keepToolResultImages = () => {
|
|
228
278
|
const policy = currentSessionSurfacePolicy(
|
|
229
279
|
typeof config === 'function' ? config() : config,
|
|
@@ -244,14 +294,165 @@ export function createSessionVisionIndex({
|
|
|
244
294
|
return undefined
|
|
245
295
|
}
|
|
246
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
|
+
|
|
247
443
|
const repairToolResultSurface = async (session) => {
|
|
444
|
+
if (surfaceEventFeedActive) return repairToolResultSurfaceFromFeed(session)
|
|
248
445
|
if (typeof session?.append !== 'function') return 0
|
|
249
446
|
const scan = pendingSurfaceScan(toolSurfaceScans, session)
|
|
250
447
|
if (!scan || typeof core.planToolResultImageShadows !== 'function') return 0
|
|
251
448
|
const preserve = keepToolResultImages()
|
|
449
|
+
const snapshot = await surfaceRepairSnapshot(session, scan.seqs[0])
|
|
450
|
+
if (snapshot?.readable === false) return 0
|
|
252
451
|
let repaired = 0
|
|
253
452
|
for (const seq of scan.seqs) {
|
|
254
|
-
const read =
|
|
453
|
+
const read = snapshot?.readable === true
|
|
454
|
+
? eventFromRepairSnapshot(session, snapshot.events, seq)
|
|
455
|
+
: await eventForSurfaceRepair(session, seq)
|
|
255
456
|
if (!read.readable) break
|
|
256
457
|
// A readable node is settled for this repair pass even when it needs no
|
|
257
458
|
// replacement or append later fails; only an unread node remains pending.
|
|
@@ -282,12 +483,17 @@ export function createSessionVisionIndex({
|
|
|
282
483
|
}
|
|
283
484
|
|
|
284
485
|
const repairGuardStopSurface = async (session) => {
|
|
486
|
+
if (surfaceEventFeedActive) return repairGuardStopSurfaceFromFeed(session)
|
|
285
487
|
if (typeof session?.append !== 'function') return 0
|
|
286
488
|
const scan = pendingSurfaceScan(guardSurfaceScans, session)
|
|
287
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
|
|
288
492
|
let repaired = 0
|
|
289
493
|
for (const seq of scan.seqs) {
|
|
290
|
-
const read =
|
|
494
|
+
const read = snapshot?.readable === true
|
|
495
|
+
? eventFromRepairSnapshot(session, snapshot.events, seq)
|
|
496
|
+
: await eventForSurfaceRepair(session, seq)
|
|
291
497
|
if (!read.readable) break
|
|
292
498
|
scan.advance()
|
|
293
499
|
const events = []
|
|
@@ -333,6 +539,8 @@ export function createSessionVisionIndex({
|
|
|
333
539
|
|
|
334
540
|
return Object.freeze({
|
|
335
541
|
recordAttachments,
|
|
542
|
+
recordSessionEvent,
|
|
543
|
+
activateSurfaceEventFeed,
|
|
336
544
|
lookupAttachment,
|
|
337
545
|
resolveAttachment,
|
|
338
546
|
resolveAttachments,
|
|
@@ -344,8 +552,9 @@ export function createSessionVisionIndex({
|
|
|
344
552
|
|
|
345
553
|
/**
|
|
346
554
|
* Intercept core's pre-step registration and decorate only its downstream
|
|
347
|
-
* `next()` result. Production
|
|
348
|
-
*
|
|
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.
|
|
349
558
|
*/
|
|
350
559
|
export function installSessionVisionIndexBoundary(ctx, config, core, options = {}) {
|
|
351
560
|
if (!isObject(ctx)) return ctx
|
|
@@ -363,6 +572,25 @@ export function installSessionVisionIndexBoundary(ctx, config, core, options = {
|
|
|
363
572
|
logger: options.logger,
|
|
364
573
|
})
|
|
365
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
|
+
|
|
366
594
|
return new Proxy(ctx, {
|
|
367
595
|
get(target, property) {
|
|
368
596
|
if (property === 'on') {
|
|
@@ -3,7 +3,17 @@ import { createSessionTurnResolver } from './session-turn-resolver.js'
|
|
|
3
3
|
|
|
4
4
|
function legacyTurnNumberOf(session) {
|
|
5
5
|
try {
|
|
6
|
-
|
|
6
|
+
// dsh 0.1.2-alpha.4 removed the bare `session.events` array in favor of
|
|
7
|
+
// `session.snapshotEvents()`; mirror legacySessionEvents
|
|
8
|
+
// (session-vision-index.js), which prefers the snapshot when both surfaces
|
|
9
|
+
// exist — transitional Hosts keep the bare array but leave it stale, so a
|
|
10
|
+
// bare-array-first read would resolve a stale turn on them.
|
|
11
|
+
let events
|
|
12
|
+
if (typeof session?.snapshotEvents === 'function') {
|
|
13
|
+
events = session.snapshotEvents()
|
|
14
|
+
} else {
|
|
15
|
+
events = session?.events
|
|
16
|
+
}
|
|
7
17
|
if (!Array.isArray(events)) return 0
|
|
8
18
|
const last = events.findLast((event) => event && event.type === 'turn/start')
|
|
9
19
|
return last && Number.isInteger(last.data?.turn) ? last.data.turn : 0
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-vision-router",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.1",
|
|
4
4
|
"description": "Eyes for text-only DeepSeek Harness agents: built-in free vision chain (no key) + pixel-level vision tools (Q&A, grounding, crop, pixel diff, colors, OCR, SVG trace, cutout, screenshots). One-command install, no Python, image turns work like ordinary tool-calling turns.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -70,12 +70,12 @@
|
|
|
70
70
|
"sharp": "^0.35.4"
|
|
71
71
|
},
|
|
72
72
|
"scripts": {
|
|
73
|
-
"test": "node --test tests/index-modularization.test.js tests/adapter-prepare-call-compat.test.js tests/core.test.js tests/issue-374-delegated-call-config.test.js tests/grounding-coordinate-frame.test.js tests/grounding-coordinate-runtime.test.js tests/capability-advisory.test.js tests/vision-execution-policy.test.js tests/vision-backend-diagnostics.test.js tests/live-model-discovery.test.js tests/live-model-credential-resolution.test.js tests/vision-model-registry.test.js tests/live-model-client-stability.test.js tests/providers-persistence.test.js tests/provider-directory-fallback.test.js tests/model-catalog-policy.test.js tests/settings-ia-client-prelude.test.js tests/settings-ia-fj.test.js tests/settings-ia-acceptance-regression.test.js tests/settings-migration.test.js tests/settings-section-order.test.js tests/attachment-admission-policy.test.js tests/vision-resilience.test.js tests/degraded-local-evidence.test.js tests/vision-resilience-lifecycle.test.js tests/vision-breaker-readonly.test.js tests/alpha1-browser-lifecycle-integration.test.js tests/issue-367-remote-session-inject.test.js tests/vision-mode-toggle-injection.test.js tests/client.test.js tests/issue-307-regression.test.js tests/issue-284-vision-mode-toggle.test.js tests/issue-284-vision-mode-hardening.test.js tests/issue-284-attachment-id-hint.test.js tests/issue-284-model-visibility.test.js tests/issue-284-vision-selection-effort.test.js tests/client-presentation-boundary.test.js tests/clipboard-image-paste-compat.test.js tests/remote-settings-bridge.test.js tests/remote-settings-v2-contract.test.js tests/remote-settings-risk-confirmation.test.js tests/local-remote-settings-permission.test.js tests/rc6-real-settings-persistence.test.js tests/artifact-path-security.test.js tests/http-compat.test.js tests/catalog-corrections.test.js tests/update-check.test.js tests/update-check-signal-lifecycle.test.js tests/self-update.test.js tests/doctor.test.js tests/doctor-cli.test.js tests/legacy-session-repair.test.js tests/profile-pnpm-diagnostics.test.js tests/doctor-v2.test.js tests/doctor-cli-v2.test.js tests/doctor-runtime.test.js tests/dsh-host-capabilities.test.js tests/doctor-host-capabilities.test.js tests/session-repair-v2.test.js tests/bundle-defaults.test.js tests/file-logger.test.js tests/official-deepseek-catalog.test.js tests/replay-delegation.test.js tests/twin-image-capability-fallback.test.js tests/twin-image-custom-wrapper.test.js tests/adversarial-hardening.test.js tests/vision-adversarial-hardening-v2.test.js tests/vision-capability-benchmark-visual-proof.test.js tests/wrapper-directory.test.js tests/logging-ui.test.js tests/manifest-dependencies.test.js tests/structured-bootstrap.test.js tests/structured-bootstrap-gate.test.js tests/vision-model-guidance-contract.test.js tests/local-ollama.test.js tests/ollama-cold-start.test.js tests/ollama-cold-start-loaded.test.js tests/zero-regression-gate.test.js tests/runtime-e2e.test.js tests/local-vision-stabilizer.test.js tests/local-connection-probe.test.js tests/repetition-guard.test.js tests/guide-scroll-gate.test.js tests/settings-scroll-jank-regression.test.js tests/android-attachment-compat.test.js tests/free-cloud-first.test.js tests/rc6-rc7-compat.test.js tests/native-image-coexistence.test.js tests/issue-276-session-image-ownership.test.js tests/issue-276-entry-policy-bridge.test.js tests/issue-276-policy-hardening.test.js tests/issue-289-native-nonintervention.test.js tests/issue-512-tool-restriction.test.js tests/pi-ai-bridge-wire-compat.test.js tests/mixed-router.test.js tests/depth-tier.test.js tests/depth-quota-behavior.test.js tests/structured-flow-hardening.test.js tests/structured-flow-adversarial.test.js tests/vision-budget-activation.test.js tests/runtime-boundary-fixes.test.js tests/security-resource-boundaries.test.js tests/security-property-fuzz.test.js tests/security-adversarial-fuzz-contract.test.js tests/runtime-i18n.test.js tests/tesseract-adaptive-ocr.test.js tests/tesseract-node24-boot.test.js tests/session-vision-state.test.js tests/session-vision-state-integration.test.js tests/image-resource-governor.test.js tests/resource-retention-lifecycle.test.js tests/pixel-diff-stream.test.js tests/large-image-resource-integration.test.js tests/structured-guard-idempotency.test.js tests/vision-routing-product.test.js tests/vision-runtime-performance.test.js tests/vision-routing-settings-prelude.test.js tests/v2-settings-ia-integration.test.js tests/vision-capability-router.test.js tests/vision-capability-reference.test.js tests/vision-capability-benchmark.test.js tests/vision-capability-grounding-proof-contract.test.js tests/vision-capability-probe.test.js tests/vision-capability-axis-freshness.test.js tests/vision-capability-shadow-health.test.js tests/vision-capability-benchmark-service.test.js tests/vision-capability-preflight-hardening.test.js tests/vision-capability-benchmark-contract.test.js tests/vision-capability-benchmark-client.test.js tests/vision-capability-benchmark-client-productization.test.js tests/vision-capability-adapter-route.test.js tests/vision-background-benchmark.test.js tests/vision-background-lifecycle.test.js tests/vision-background-benchmark-productization.test.js tests/vision-capability-benchmark-budget.test.js tests/vision-image-input-verdict.test.js tests/vision-capability-failure-classification.test.js tests/vision-capability-gate-client.test.js tests/v2-product-review-fixes.test.js tests/qa-top5-reliability.test.js tests/qa-screenshot-runtime.test.js tests/qa-runtime-identity.test.js tests/qa-turn-budget-cancellation.test.js tests/qa-cancellation-publication.test.js tests/qa-reliability-tail.test.js tests/qa-endpoint-route-alias.test.js tests/settings-native-card-semantics.test.js tests/settings-native-card-lazy-data.test.js tests/settings-invalid-staged-dirty.test.js tests/vision-toggle-root-hardening.test.js && node --test tests/vision-backend-runtime-policy.test.js",
|
|
73
|
+
"test": "node --test tests/index-modularization.test.js tests/adapter-prepare-call-compat.test.js tests/core.test.js tests/issue-374-delegated-call-config.test.js tests/grounding-coordinate-frame.test.js tests/grounding-coordinate-runtime.test.js tests/capability-advisory.test.js tests/vision-execution-policy.test.js tests/vision-backend-diagnostics.test.js tests/live-model-discovery.test.js tests/live-model-credential-resolution.test.js tests/vision-model-registry.test.js tests/live-model-client-stability.test.js tests/providers-persistence.test.js tests/provider-directory-fallback.test.js tests/model-catalog-policy.test.js tests/settings-ia-client-prelude.test.js tests/settings-ia-fj.test.js tests/settings-ia-acceptance-regression.test.js tests/settings-migration.test.js tests/settings-section-order.test.js tests/attachment-admission-policy.test.js tests/vision-resilience.test.js tests/degraded-local-evidence.test.js tests/vision-resilience-lifecycle.test.js tests/vision-breaker-readonly.test.js tests/alpha1-browser-lifecycle-integration.test.js tests/issue-367-remote-session-inject.test.js tests/vision-mode-toggle-injection.test.js tests/client.test.js tests/issue-307-regression.test.js tests/issue-284-vision-mode-toggle.test.js tests/issue-284-vision-mode-hardening.test.js tests/issue-284-attachment-id-hint.test.js tests/issue-284-model-visibility.test.js tests/issue-284-vision-selection-effort.test.js tests/client-presentation-boundary.test.js tests/clipboard-image-paste-compat.test.js tests/remote-settings-bridge.test.js tests/remote-settings-v2-contract.test.js tests/remote-settings-risk-confirmation.test.js tests/local-remote-settings-permission.test.js tests/rc6-real-settings-persistence.test.js tests/artifact-path-security.test.js tests/http-compat.test.js tests/catalog-corrections.test.js tests/update-check.test.js tests/update-check-signal-lifecycle.test.js tests/self-update.test.js tests/doctor.test.js tests/doctor-cli.test.js tests/legacy-session-repair.test.js tests/profile-pnpm-diagnostics.test.js tests/doctor-v2.test.js tests/doctor-cli-v2.test.js tests/doctor-runtime.test.js tests/dsh-host-capabilities.test.js tests/doctor-host-capabilities.test.js tests/session-repair-v2.test.js tests/bundle-defaults.test.js tests/file-logger.test.js tests/official-deepseek-catalog.test.js tests/replay-delegation.test.js tests/twin-image-capability-fallback.test.js tests/twin-image-custom-wrapper.test.js tests/adversarial-hardening.test.js tests/vision-adversarial-hardening-v2.test.js tests/vision-capability-benchmark-visual-proof.test.js tests/wrapper-directory.test.js tests/logging-ui.test.js tests/manifest-dependencies.test.js tests/structured-bootstrap.test.js tests/structured-bootstrap-gate.test.js tests/vision-model-guidance-contract.test.js tests/local-ollama.test.js tests/ollama-cold-start.test.js tests/ollama-cold-start-loaded.test.js tests/zero-regression-gate.test.js tests/runtime-e2e.test.js tests/local-vision-stabilizer.test.js tests/local-connection-probe.test.js tests/repetition-guard.test.js tests/guide-scroll-gate.test.js tests/settings-scroll-jank-regression.test.js tests/android-attachment-compat.test.js tests/free-cloud-first.test.js tests/rc6-rc7-compat.test.js tests/native-image-coexistence.test.js tests/issue-276-session-image-ownership.test.js tests/issue-276-entry-policy-bridge.test.js tests/issue-276-policy-hardening.test.js tests/issue-289-native-nonintervention.test.js tests/issue-512-tool-restriction.test.js tests/pi-ai-bridge-wire-compat.test.js tests/fetch-wrapper-lifecycle.test.js tests/fetch-wrapper-composition.test.js tests/mixed-router.test.js tests/depth-tier.test.js tests/depth-quota-behavior.test.js tests/structured-flow-hardening.test.js tests/structured-flow-adversarial.test.js tests/vision-budget-activation.test.js tests/runtime-boundary-fixes.test.js tests/security-resource-boundaries.test.js tests/security-property-fuzz.test.js tests/security-adversarial-fuzz-contract.test.js tests/runtime-i18n.test.js tests/tesseract-adaptive-ocr.test.js tests/tesseract-node24-boot.test.js tests/session-vision-state.test.js tests/session-vision-state-integration.test.js tests/session-vision-event-feed.test.js tests/image-resource-governor.test.js tests/resource-retention-lifecycle.test.js tests/pixel-diff-stream.test.js tests/large-image-resource-integration.test.js tests/structured-guard-idempotency.test.js tests/vision-routing-product.test.js tests/vision-runtime-performance.test.js tests/vision-routing-settings-prelude.test.js tests/v2-settings-ia-integration.test.js tests/vision-capability-router.test.js tests/vision-capability-reference.test.js tests/vision-capability-benchmark.test.js tests/vision-capability-grounding-proof-contract.test.js tests/vision-capability-probe.test.js tests/vision-capability-axis-freshness.test.js tests/vision-capability-shadow-health.test.js tests/vision-capability-benchmark-service.test.js tests/vision-capability-preflight-hardening.test.js tests/vision-capability-benchmark-contract.test.js tests/vision-capability-benchmark-client.test.js tests/vision-capability-benchmark-client-productization.test.js tests/vision-capability-adapter-route.test.js tests/vision-background-benchmark.test.js tests/vision-background-lifecycle.test.js tests/vision-background-benchmark-productization.test.js tests/vision-capability-benchmark-budget.test.js tests/vision-image-input-verdict.test.js tests/vision-capability-failure-classification.test.js tests/vision-capability-gate-client.test.js tests/v2-product-review-fixes.test.js tests/qa-top5-reliability.test.js tests/qa-screenshot-runtime.test.js tests/qa-runtime-identity.test.js tests/qa-turn-budget-cancellation.test.js tests/qa-cancellation-publication.test.js tests/qa-reliability-tail.test.js tests/qa-endpoint-route-alias.test.js tests/settings-native-card-semantics.test.js tests/settings-native-card-lazy-data.test.js tests/settings-invalid-staged-dirty.test.js tests/vision-toggle-root-hardening.test.js && node --test tests/vision-backend-runtime-policy.test.js",
|
|
74
74
|
"test:core": "node --test tests/core.test.js tests/degraded-local-evidence.test.js tests/runtime-e2e.test.js tests/structured-bootstrap.test.js tests/structured-flow-hardening.test.js tests/structured-flow-adversarial.test.js tests/vision-execution-policy.test.js tests/vision-backend-runtime-policy.test.js",
|
|
75
75
|
"test:routing": "node --test tests/issue-374-delegated-call-config.test.js tests/twin-image-capability-fallback.test.js tests/twin-image-custom-wrapper.test.js tests/vision-routing-product.test.js tests/vision-capability-router.test.js tests/vision-capability-probe.test.js tests/vision-capability-shadow-health.test.js tests/vision-runtime-performance.test.js tests/vision-background-benchmark.test.js tests/vision-image-input-verdict.test.js",
|
|
76
76
|
"test:session": "node --test tests/session-vision-state.test.js tests/session-vision-state-integration.test.js tests/replay-delegation.test.js tests/legacy-session-repair.test.js tests/session-repair-v2.test.js tests/issue-276-session-image-ownership.test.js",
|
|
77
77
|
"test:resources": "node --test tests/artifact-path-security.test.js tests/image-resource-governor.test.js tests/resource-retention-lifecycle.test.js tests/pixel-diff-stream.test.js tests/large-image-resource-integration.test.js tests/qa-turn-budget-cancellation.test.js tests/qa-cancellation-publication.test.js",
|
|
78
|
-
"test:compat": "node --test tests/adapter-prepare-call-compat.test.js tests/attachment-admission-policy.test.js tests/rc6-rc7-compat.test.js tests/rc6-real-settings-persistence.test.js tests/android-attachment-compat.test.js tests/http-compat.test.js tests/pi-ai-bridge-wire-compat.test.js tests/native-image-coexistence.test.js tests/tesseract-node24-boot.test.js tests/dsh-host-capabilities.test.js",
|
|
78
|
+
"test:compat": "node --test tests/adapter-prepare-call-compat.test.js tests/attachment-admission-policy.test.js tests/rc6-rc7-compat.test.js tests/rc6-real-settings-persistence.test.js tests/android-attachment-compat.test.js tests/http-compat.test.js tests/pi-ai-bridge-wire-compat.test.js tests/fetch-wrapper-lifecycle.test.js tests/fetch-wrapper-composition.test.js tests/native-image-coexistence.test.js tests/tesseract-node24-boot.test.js tests/dsh-host-capabilities.test.js",
|
|
79
79
|
"test:web": "node --test tests/alpha1-browser-lifecycle-integration.test.js tests/issue-367-remote-session-inject.test.js tests/vision-mode-toggle-injection.test.js tests/client.test.js tests/client-presentation-boundary.test.js tests/clipboard-image-paste-compat.test.js tests/settings-ia-client-prelude.test.js tests/settings-ia-acceptance-regression.test.js tests/remote-settings-bridge.test.js tests/remote-settings-v2-contract.test.js tests/local-remote-settings-permission.test.js tests/v2-settings-ia-integration.test.js tests/logging-ui.test.js",
|
|
80
80
|
"test:contract": "node --test tests/manifest-dependencies.test.js tests/bundle-defaults.test.js tests/zero-regression-gate.test.js tests/issue-289-native-nonintervention.test.js tests/issue-374-delegated-call-config.test.js tests/runtime-boundary-fixes.test.js tests/security-adversarial-fuzz-contract.test.js tests/runtime-i18n.test.js tests/adapter-prepare-call-compat.test.js tests/rc6-rc7-compat.test.js tests/attachment-admission-policy.test.js tests/dsh-host-capabilities.test.js tests/doctor-host-capabilities.test.js tests/structured-bootstrap.test.js tests/structured-bootstrap-gate.test.js",
|
|
81
81
|
"test:grounding": "node --test tests/grounding-coordinate-frame.test.js tests/grounding-coordinate-runtime.test.js tests/vision-capability-grounding-proof-contract.test.js",
|