dsh-vision-router 2.2.0 → 2.2.2

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 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.0"><img src="https://img.shields.io/badge/release-v2.2.0-5B4CF0?style=flat-square" alt="Release v2.2.0" /></a>
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&amp;logo=nodedotjs&amp;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.0)**
42
+ > 📌 **Announcement (v2.2.1)**
43
43
  >
44
- > **v2.2.0:** Lands Vision Quality Round 2 with adaptive local OCR, bounded degraded-evidence work, stable artifact handoff, successful-evidence convergence, and stronger DSH/session compatibility — without raising the rc.8 Host floor. [What’s new →](docs/releases/v2.2.0.md)
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.0"><img src="https://img.shields.io/badge/release-v2.2.0-5B4CF0?style=flat-square" alt="Release v2.2.0" /></a>
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&amp;logo=nodedotjs&amp;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.0)**
42
+ > 📌 **公告(v2.2.1)**
43
43
  >
44
- > **v2.2.0:Vision Quality Round 2 正式落地——自适应本地OCR、失败证据有界收敛、稳定artifact handoff、成功证据停止重复调用,并进一步收敛DSH/Session兼容;公开最低Host仍保持 `0.1.0-rc.8`。** [查看完整更新 →](docs/releases/v2.2.0.md)
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.
@@ -0,0 +1,21 @@
1
+ # v2.2.2
2
+
3
+ DSH Vision Router 2.2.2 is an urgent browser compatibility hotfix for DSH 0.1.7-rc.1.
4
+
5
+ ## Highlights
6
+
7
+ - Fixes Vision Router being left pending at Web startup on DSH 0.1.7-rc.1 with `waiting for service: settingsScope`.
8
+ - Adapts the legacy `ctx.settingsScope.bind({ namespace })` consumer face to DSH 0.1.7's official `configForms.get(namespace)` service without hard-requiring either service generation.
9
+ - Keeps the real legacy `settingsScope` path preferred on older supported Hosts, so the fix does not raise the existing Host floor.
10
+ - Preserves the existing local settings permission and remote-risk wrappers over both settings backends.
11
+ - Adds loader-level regression coverage for both DSH 0.1.7 `configForms` and legacy `settingsScope` Hosts.
12
+
13
+ ## Validation
14
+
15
+ - Node 22 and Node 24 CI passed on the compatibility fix.
16
+ - Architecture Closure, P1 Routing Parity, P3 Compatibility Convergence, DSH Contract, exact-source, browser smoke, adversarial compatibility, fuzz, dependency review, fetch composition, and native multimodal cold-resume gates passed on PR #538.
17
+ - The public minimum DSH Host remains `0.1.0-rc.8`.
18
+
19
+ ## Upgrade
20
+
21
+ Users affected by the DSH 0.1.7 startup failure should upgrade Vision Router to 2.2.2. No settings migration is required.
package/lib/client.js CHANGED
@@ -865,6 +865,57 @@ window.__ModuleLoader__.load({
865
865
  : stored && settingsValueEqual(item.key, user[item.key], item.run.value)
866
866
  return { ok, stored }
867
867
  }
868
+ const finalize = () => {
869
+ const landed = failures.length === 0
870
+ const nextDrafts = landedFields.length === 0 ? drafts : { ...drafts }
871
+ for (const field of landedFields) delete nextDrafts[field]
872
+ return {
873
+ landed,
874
+ failed: !landed,
875
+ landedFields,
876
+ nextDrafts,
877
+ failures,
878
+ }
879
+ }
880
+ const mismatchFailure = (item, check) => ({
881
+ field: item.key,
882
+ operation: item.run.clear ? 'unset' : 'set',
883
+ reason: 'readback-mismatch',
884
+ detail: item.run.clear
885
+ ? 'field remained present in the user layer'
886
+ : check.stored
887
+ ? 'stored user-layer value differs from the requested value'
888
+ : 'field is absent from the user layer',
889
+ })
890
+ const batchWrite = scope && typeof scope.__visionRouterWritePlan === 'function'
891
+ ? scope.__visionRouterWritePlan.bind(scope)
892
+ : undefined
893
+ // DSH 0.1.7's local compatibility transport is ConfigEditor-backed.
894
+ // ConfigEditor reloads the plugin after a successful edit, so serial POSTs
895
+ // from one Save can cross generations and strand the second mutation.
896
+ // Its private scope seam commits the entire UI plan in one atomic edit.
897
+ if (plan.length > 1 && batchWrite) {
898
+ try {
899
+ await batchWrite(plan)
900
+ } catch (error) {
901
+ for (const item of plan) {
902
+ failures.push({
903
+ field: item.key,
904
+ operation: item.run.clear ? 'unset' : 'set',
905
+ reason: error && error.code === 'settings-conflict' ? 'settings-conflict' : 'write-error',
906
+ detail: settingsSaveErrorMessage(error),
907
+ })
908
+ }
909
+ return finalize()
910
+ }
911
+ for (const item of plan) {
912
+ const check = inspectReadback(item)
913
+ if (check.error) failures.push(check.error)
914
+ else if (check.ok) landedFields.push(item.key)
915
+ else failures.push(mismatchFailure(item, check))
916
+ }
917
+ return finalize()
918
+ }
868
919
  const writeItem = async (item) => {
869
920
  if (item.run.clear) await scope.unset(item.key)
870
921
  else await scope.set(item.key, item.run.value)
@@ -913,16 +964,7 @@ window.__ModuleLoader__.load({
913
964
  }
914
965
  }
915
966
 
916
- terminalFailure = {
917
- field: item.key,
918
- operation,
919
- reason: 'readback-mismatch',
920
- detail: item.run.clear
921
- ? 'field remained present in the user layer'
922
- : check.stored
923
- ? 'stored user-layer value differs from the requested value'
924
- : 'field is absent from the user layer',
925
- }
967
+ terminalFailure = mismatchFailure(item, check)
926
968
  }
927
969
  if (success) landedFields.push(item.key)
928
970
  else failures.push(terminalFailure ?? {
@@ -932,16 +974,7 @@ window.__ModuleLoader__.load({
932
974
  detail: 'write did not become visible in the user layer',
933
975
  })
934
976
  }
935
- const landed = failures.length === 0
936
- const nextDrafts = landedFields.length === 0 ? drafts : { ...drafts }
937
- for (const field of landedFields) delete nextDrafts[field]
938
- return {
939
- landed,
940
- failed: !landed,
941
- landedFields,
942
- nextDrafts,
943
- failures,
944
- }
977
+ return finalize()
945
978
  }
946
979
 
947
980
  const REMOTE_SETTINGS_CHANNEL = '/vision-router-settings'
@@ -1124,16 +1157,23 @@ window.__ModuleLoader__.load({
1124
1157
  }
1125
1158
 
1126
1159
  function shouldUseRemoteSettings(getConnection, locationLike) {
1160
+ // Browser page authority is the security boundary. DSH 0.1.7 can report
1161
+ // Connection.isLoopback=false even for a real 127.0.0.1 page, so letting
1162
+ // that hint win would incorrectly force the local Settings page onto the
1163
+ // trusted-host RPC path. Conversely, a non-loopback browser URL must not
1164
+ // become local merely because a stale Connection says true.
1165
+ const location = locationLike ?? (typeof window !== 'undefined' ? window.location : undefined)
1166
+ const hostname = typeof location?.hostname === 'string' ? location.hostname.toLowerCase().replace(/^\[|\]$/g, '') : ''
1167
+ if (hostname !== '') {
1168
+ if (hostname === 'localhost' || hostname.endsWith('.localhost') || hostname === '::1') return false
1169
+ if (/^127(?:\.\d{1,3}){3}$/.test(hostname)) return false
1170
+ return true
1171
+ }
1127
1172
  try {
1128
1173
  const connection = typeof getConnection === 'function' ? getConnection() : undefined
1129
1174
  if (connection && typeof connection.isLoopback === 'boolean') return connection.isLoopback === false
1130
- } catch { /* fall through to page authority */ }
1131
- const location = locationLike ?? (typeof window !== 'undefined' ? window.location : undefined)
1132
- const hostname = typeof location?.hostname === 'string' ? location.hostname.toLowerCase().replace(/^\[|\]$/g, '') : ''
1133
- if (hostname === '') return false
1134
- if (hostname === 'localhost' || hostname.endsWith('.localhost') || hostname === '::1') return false
1135
- if (/^127(?:\.\d{1,3}){3}$/.test(hostname)) return false
1136
- return true
1175
+ } catch { /* no page authority and no usable Connection: stay local-safe */ }
1176
+ return false
1137
1177
  }
1138
1178
 
1139
1179
  function reportSettingsSaveFailures(failures) {
@@ -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 narrow asynchronous Session-log reader used only for cold
196
- * canonical attachment recovery after the bounded cache evicts a ref.
197
- * Supported Hosts own replay validation through `sessionQuery.readSession()`;
198
- * callers receive detached events and never touch synchronous Session history.
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 || typeof query.readSession !== 'function') return { supported: false }
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')