dsh-vision-router 2.1.3 → 2.1.4

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
@@ -413,7 +413,7 @@ ollama pull qwen2.5vl
413
413
  ## Requirements
414
414
 
415
415
  - DeepSeek Harness Web profile. Normal installs can use `npx @deepseek-ai/dsh ...`; source checkouts use `pnpm dsh ...`. A bare `dsh ...` command only works when the CLI is already on your shell `PATH`.
416
- - **DSH Host support window:** DVR 2.1.x supports DSH `0.1.0-rc.8` (minimum), `0.1.1-rc.1` (previous released train), and current `0.1.1-rc.2`; DSH `0.1.2-alpha.4` is canary-only evidence. DVR 2.0.x was the final train with public support for rc.6/rc.7. See [DSH Host support window](docs/architecture/dsh-support-window.md).
416
+ - **DSH Host support policy:** DVR 2.1.x keeps DSH `0.1.0-rc.8` as the public minimum and currently supports the released stable channel through `0.1.2-rc.1`. Exact `0.1.3-alpha.2` coverage is **verification evidence only**, not a preview support promise; scheduled `latest`/`alpha` canaries monitor drift without changing the support policy. DVR 2.0.x was the final train with public support for rc.6/rc.7. See [DSH Host support window](docs/architecture/dsh-support-window.md).
417
417
  - Node ≥ 22 (host side).
418
418
  - No API key for the default free chain; a credential reference (`apiKeyEnv`) only for paid `httpProviders`.
419
419
  - Chrome / Chromium / Edge is needed only for `vision_html_screenshot`; every other tool works without a browser.
package/README.zh.md CHANGED
@@ -411,7 +411,7 @@ ollama pull qwen2.5vl
411
411
  ## 环境要求
412
412
 
413
413
  - DeepSeek Harness 的 Web profile。普通安装可用 `npx @deepseek-ai/dsh ...`;从源码仓库运行时用 `pnpm dsh ...`。只有 CLI 已经进入系统 `PATH` 时才能直接写 `dsh ...`。
414
- - **DSH Host 支持窗口:** DVR 2.1.x 支持 DSH `0.1.0-rc.8`(最低)、`0.1.1-rc.1`(上一正式支持线)以及当前 `0.1.1-rc.2`;DSH `0.1.2-alpha.4` 仅作为 canary 兼容证据。DVR 2.0.x 是最后公开支持 rc.6/rc.7 的版本线。详见 [DSH Host 支持窗口](docs/architecture/dsh-support-window.md)。
414
+ - **DSH Host 支持策略:** DVR 2.1.x 的公开最低 Host 仍为 DSH `0.1.0-rc.8`,当前正式发布通道已验证并支持到 `0.1.2-rc.1`。对 `0.1.3-alpha.2` 的精确覆盖**只属于验证证据**,不代表对 preview 的公开支持承诺;定时 `latest`/`alpha` canary 只负责发现上游漂移,也不会自动改变支持策略。DVR 2.0.x 是最后公开支持 rc.6/rc.7 的版本线。详见 [DSH Host 支持窗口](docs/architecture/dsh-support-window.md)。
415
415
  - Node ≥ 22(宿主侧)。
416
416
  - 默认免费链路无需 API Key;付费 `httpProviders` 只需一个凭据引用(`apiKeyEnv`)。
417
417
  - 只有 `vision_html_screenshot` 需要 Chrome / Chromium / Edge;其余工具无浏览器也能用。
@@ -40,12 +40,12 @@ P0 records why each major compatibility seam exists and the condition that permi
40
40
 
41
41
  ## `lib/pi-ai-bridge-wire-compat.js`
42
42
 
43
- - **Reason:** make the legacy direct image bridge transport-equivalent to pi-ai declared wire compatibility without reading credentials or changing provider priority.
44
- - **Host gap:** the direct bridge predates route/model wire metadata such as max-token fields and route-owned headers.
45
- - **First needed for:** DSH 0.1.1 pi-ai declared provider/wire compatibility.
46
- - **Feature detection:** exact non-streaming OpenAI-compatible image bridge fingerprint plus resolved route/model facts; normal streaming Host traffic does not match.
47
- - **Removal condition:** the direct bridge is removed or every supported Host executes this image path through the native pi-ai wire seam.
48
- - **Tests:** `pi-ai-bridge-wire-compat`, native process-restart/cold-resume contract.
43
+ - **Reason:** keep the legacy direct image bridge transport-equivalent to pi-ai declared wire compatibility and, while upstream lacks the OpenCode-specific carrier, project Vision Router-owned session affinity onto the final Go wire without taking ownership of ordinary Host traffic. `lib/session-affinity-runtime.js` is the scoped AsyncLocalStorage companion; it carries affinity only while a Router-owned lazy stream is executing.
44
+ - **Host gap:** the direct bridge predates route/model wire metadata such as max-token fields and route-owned headers; current DSH/pi-ai accepts `GenerateOptions.sessionId` but emits generic affinity carriers rather than OpenCode Go's required `x-opencode-session`.
45
+ - **First needed for:** DSH 0.1.1 pi-ai declared provider/wire compatibility; OpenCode Go's September 2026 required session header.
46
+ - **Feature detection:** legacy wire recovery still requires the exact non-streaming OpenAI-compatible image bridge fingerprint plus resolved route/model facts. OpenCode affinity requires an active Vision Router AsyncLocalStorage scope, the exact official HTTPS `opencode.ai/zen/go...` endpoint, and absence of an upstream-native `x-opencode-session`; unrelated Host/plugin traffic is inert.
47
+ - **Removal condition:** remove legacy bridge recovery when every supported Host executes that image path through native pi-ai wire facts; remove the scoped OpenCode projection independently once every supported DSH/pi-ai path emits `x-opencode-session` from `GenerateOptions.sessionId`. The native header must win before deletion so the compatibility rule self-retires.
48
+ - **Tests:** `pi-ai-bridge-wire-compat`, real `opencode-session-wire-contract`, native process-restart/cold-resume contract.
49
49
 
50
50
  ## `lib/settings-client-rc8-lifecycle.js`
51
51
 
@@ -8,10 +8,11 @@ The matrix is capability-based. Runtime code must feature-detect the seam it nee
8
8
 
9
9
  | CI fixture | DSH package line | Role |
10
10
  | --- | --- | --- |
11
- | `minimum-contract` | `0.1.0-rc.6` | Minimum supported Host contract. Must remain green. |
12
- | `legacy-contract` | `0.1.0-rc.8` | Legacy contract carrying batch attachments and dimension policy. |
13
- | `current-contract` | `0.1.1-rc.2` | Current contract baseline for 2.x convergence. Must be green before P1. |
14
- | `latest-dsh` | resolved dynamically from npm | Scheduled canary only. Never a normal PR required check. |
11
+ | `minimum-contract` | `0.1.0-rc.6` | Historical compatibility-retention fixture. The name is legacy; DVR 2.1.x public support starts at rc.8. |
12
+ | `legacy-contract` | `0.1.0-rc.8` | Public support-floor fixture carrying batch attachments and dimension policy. |
13
+ | `current-contract` | `0.1.2-rc.1` | Current stable Host contract baseline. Must remain green. |
14
+ | exact preview gates | `0.1.3-alpha.2` | Required verification evidence only; not a public preview-support claim. |
15
+ | `latest-dsh` / alpha canaries | resolved dynamically from npm dist-tags | Scheduled drift surveillance only. Never changes support policy by itself. |
15
16
 
16
17
  Node 22 and Node 24 remain the general required runtime matrix. The Host contract jobs are additive; they do not replace the normal test matrix.
17
18
 
@@ -19,7 +20,7 @@ Node 22 and Node 24 remain the general required runtime matrix. The Host contrac
19
20
 
20
21
  `yes` means the fixture has a direct positive test or feature probe. `no` means a direct negative probe exists. `compat` means the fixture proves Vision Router can safely carry the newer input/config through that Host, but does **not** claim the Host owns that capability. `probe` means the capability is intentionally not inferred from the version label and is verified at runtime/contract-test time.
21
22
 
22
- | Capability | minimum-contract rc.6 | legacy-contract rc.8 | current-contract rc.2 | Evidence / detection |
23
+ | Capability | minimum-contract rc.6 | legacy-contract rc.8 | current-contract rc.1 (0.1.2) | Evidence / detection |
23
24
  | --- | --- | --- | --- | --- |
24
25
  | Batch attachment save | no | yes | yes | `hasBatchAttachmentContract()` checks the released `attachments.saveImages` prototype; `tests/rc6-rc7-compat.test.js`; contract CI. |
25
26
  | Max image dimension policy | compat | yes | yes | All fixtures parse the complete attachment-local row; rc.8/current positively retain the field and the established admission tests exercise the 10000/10001 boundary. Older Schemastery passthrough is not treated as ownership evidence. |
@@ -53,5 +54,5 @@ Every compatibility seam must answer the same six questions: **Reason**, **Host
53
54
 
54
55
  1. A new version-specific branch requires evidence that no stable capability probe exists.
55
56
  2. A compatibility layer may narrow behavior to preserve an existing product contract; it may not expand routing authority.
56
- 3. `latest-dsh` can reveal upstream drift, but a canary failure must not silently redefine the supported contract.
57
+ 3. Exact preview and dynamic canary evidence can reveal upstream drift, but neither may silently redefine the public support policy.
57
58
  4. P1 may begin only when all three gating fixtures and the existing cold-resume/resource baselines are green.
@@ -1,21 +1,41 @@
1
1
  # DSH Host support window
2
2
 
3
- Status: normative for the DVR 2.x compatibility program.
3
+ Status: normative for the current DVR 2.x compatibility program.
4
4
 
5
- ## Current DVR 2.1.x window
5
+ ## Public support policy
6
+
7
+ The public support policy contains only released Host semantics. Preview/canary versions are intentionally excluded from this table.
6
8
 
7
9
  | Role | DSH train | Meaning |
8
10
  |---|---|---|
9
11
  | Minimum Supported Host | `0.1.0-rc.8` | Oldest Host generation that DVR 2.1.x publicly supports. |
10
- | Previous Supported Train | `0.1.1-rc.1` | Previous released Host train kept in the compatibility matrix. |
11
- | Current Supported Train | `0.1.1-rc.2` | Current released train used by the required current-contract gate. |
12
- | Canary only | `0.1.2-alpha.4` | Latest upstream prerelease evidence at v2.1.0 release preparation time. It is not a released support-floor claim and does not authorize compat deletion by itself. |
12
+ | Current Stable Host | `0.1.2-rc.1` | Current npm stable-channel release covered by required exact Host and browser evidence. |
13
+
14
+ DVR `2.1.x` therefore keeps `0.1.0-rc.8` as its public floor and supports released Host trains through the current stable channel. Runtime branching remains capability-based rather than version-string-driven.
15
+
16
+ No later support-floor increase is currently announced.
17
+
18
+ ## Verification evidence — not support policy
19
+
20
+ Compatibility evidence answers a different question: what exact upstream releases and moving channels have current CI proof? It must never be interpreted as a public support-floor change.
21
+
22
+ | Evidence role | DSH source | Meaning |
23
+ |---|---|---|
24
+ | Exact stable evidence | `0.1.2-rc.1` | Required Host/wire and real Host + Chromium coverage for the current stable release. |
25
+ | Exact preview evidence | `0.1.3-alpha.2` | Required preview Host/wire/lifecycle/browser evidence. This is not a preview support promise. |
26
+ | Stable drift canary | npm dist-tag `latest` | Scheduled, dynamically resolved surveillance. A failure starts compatibility investigation; it does not rewrite support policy. |
27
+ | Preview drift canary | npm dist-tag `alpha` | Scheduled, dynamically resolved surveillance with preview-specific lifecycle coverage. A failure does not rewrite support policy. |
28
+
29
+ The exact evidence values may move in a patch-level maintenance PR when CI proof advances. The public minimum may move only under the support-floor protocol below.
30
+
31
+ Historical release notes under `docs/releases/` are release-time snapshots and are not rewritten when later evidence advances.
32
+
13
33
 
14
- DVR `2.1.x` therefore supports DSH `0.1.0-rc.8` and newer released trains covered by the published matrix. Runtime branching remains capability-based rather than version-string-driven.
34
+ The optional peer-dependency range may admit an exact preview version so CI/users can install a verified preview Host without peer-resolution noise. That install admission is compatibility evidence, not a public preview support promise.
15
35
 
16
36
  ## Floor transition from DVR 2.0.x
17
37
 
18
- DVR 2.0.x was released with DSH `0.1.0-rc.6` as its minimum Host. The 2.1.0 boundary was announced in advance and raises the public minimum to DSH `0.1.0-rc.8`.
38
+ DVR 2.0.x was released with DSH `0.1.0-rc.6` as its minimum Host. The 2.1.0 boundary was announced in advance and raised the public minimum to DSH `0.1.0-rc.8`.
19
39
 
20
40
  ```text
21
41
  DVR 2.0.x minimum: DSH 0.1.0-rc.6
@@ -26,24 +46,24 @@ Users still on rc.6/rc.7 should upgrade DSH before upgrading to DVR 2.1.x.
26
46
 
27
47
  This support-floor transition does **not** require deleting every rc.6-era compatibility seam in the same release. Compatibility code is retired only after a separate proof shows it is unreachable or unnecessary on every supported Host and durable-history path.
28
48
 
29
- No later support-floor increase is currently announced.
30
-
31
- ## Support-window change protocol
49
+ ## Support-policy change protocol
32
50
 
33
- A Host support-floor change is valid only when all of the following are true:
51
+ A public Host support-floor change is valid only when all of the following are true:
34
52
 
35
- 1. the change is announced in a DVR minor or major release, never only in a patch release;
53
+ 1. the floor change is announced in a DVR minor or major release, never only in a patch release;
36
54
  2. README / support documentation and release notes state the old and new floors;
37
- 3. Doctor reports the effective support window and gives a capability-based upgrade result for Hosts below the active floor;
38
- 4. required CI has stable minimum, previous, current, and canary evidence for the declared window;
55
+ 3. Doctor reports the effective public support policy and gives a capability-based upgrade result for Hosts below the active floor;
56
+ 4. required CI proves the public floor and current stable Host, while preview and dynamic canaries remain separately labelled verification evidence;
39
57
  5. compatibility seams are removed only after the new minimum Host proves the replacement capability;
40
58
  6. removal PRs keep restart, settings, native-image coexistence, tool execution, Node 22/24 and supported-platform regressions green.
41
59
 
60
+ Advancing an exact stable/preview evidence version or a moving canary target does **not** by itself change the public support floor.
61
+
42
62
  ## Capability-first rule
43
63
 
44
- Version labels describe the public support window; runtime branching still uses capabilities.
64
+ Version labels describe support policy and CI evidence; runtime branching still uses capabilities.
45
65
 
46
- DVR must not turn this table into widespread version-string conditionals. Runtime compatibility continues to feature-detect the concrete Host seam it needs. If a capability cannot be proven safely, the compatibility path fails open or reports an explicit unsupported/unknown state according to that seam's contract.
66
+ DVR must not turn these tables into widespread version-string conditionals. Runtime compatibility continues to feature-detect the concrete Host seam it needs. If a capability cannot be proven safely, the compatibility path fails open or reports an explicit unsupported/unknown state according to that seam's contract.
47
67
 
48
68
  ## Compatibility-retirement rule
49
69
 
@@ -2,15 +2,17 @@
2
2
 
3
3
  Decision date: 2026-08-28
4
4
 
5
- Current DVR train: `2.0.x`
5
+ Status: historical P3-B audit snapshot. It records the 2.0.x support policy in force at the decision date; it is **not** the current Host support policy. See [`dsh-support-window.md`](dsh-support-window.md) for the normative current policy and verification evidence.
6
6
 
7
- Current minimum supported Host: DSH `0.1.0-rc.6`
7
+ DVR train at decision time: `2.0.x`
8
+
9
+ Minimum supported Host at decision time: DSH `0.1.0-rc.6`
8
10
 
9
11
  ## Result
10
12
 
11
13
  **NO COMPAT DELETION IS CURRENTLY AUTHORIZED.**
12
14
 
13
- P3-B is intentionally a retirement audit, not a quota to delete files. Under the current 2.0.x support window, every Host-generation compatibility seam that materially exists for rc.6/rc.8 users still has a reachable support case.
15
+ P3-B is intentionally a retirement audit, not a quota to delete files. Under the 2.0.x support window in force at the decision date, every Host-generation compatibility seam that materially exists for rc.6/rc.8 users still has a reachable support case.
14
16
 
15
17
  ## Seam review
16
18
 
@@ -18,7 +20,7 @@ P3-B is intentionally a retirement audit, not a quota to delete files. Under the
18
20
  |---|---|---|
19
21
  | attachment contract / Android attachment fallback | rc.6 remains the minimum and does not provide the later batch-attachment contract used by the modern path | after the released minimum Host no longer needs the single-attachment/permission fallback and the replacement is proven on the new minimum |
20
22
  | Host settings compatibility | rc.6 remains supported; live settings/client behavior differs across the support window | after the new minimum exposes the stable settings seam used by DVR without compatibility wrapping |
21
- | rc.8 browser/client lifecycle compatibility | rc.8 is still inside the declared support window and remains the Previous Supported Train | only after rc.8 itself leaves the support window or the same path becomes unreachable by capability proof |
23
+ | rc.8 browser/client lifecycle compatibility | rc.8 is still inside the declared support window and was the Previous Supported Train at the decision date | only after rc.8 itself leaves the support window or the same path becomes unreachable by capability proof |
22
24
  | replay envelope v2 compatibility | old durable histories remain valid inputs even when the live Host is newer | only when the supported history/runtime window no longer needs producer rebinding or Host provides an equivalent native replay identity seam |
23
25
  | adapter prepareCall/coalescing compatibility | the support matrix still spans Host generations with different adapter-registration/update behavior | only when the minimum Host and every DVR-owned adapter satisfy one stable registration/update contract |
24
26
  | pi-ai bridge wire compatibility | legacy direct-bridge traffic remains a supported route shape | only after the direct bridge is retired or all supported Hosts execute that path through an equivalent native wire seam |
@@ -41,6 +43,6 @@ Each deletion still requires proof on:
41
43
 
42
44
  ## P3-B verdict
43
45
 
44
- `PASS — no expired seam under the current 2.0.x support window.`
46
+ `PASS — no expired seam under the 2.0.x support window in force at the decision date.`
45
47
 
46
48
  Deleting a still-supported rc.6 seam merely to make the compatibility inventory smaller would violate the P3 plan and the published 2.0.x compatibility contract.
@@ -2,7 +2,9 @@
2
2
 
3
3
  Decision date: 2026-08-28
4
4
 
5
- Support window: DVR `2.0.x` / minimum DSH `0.1.0-rc.6` / previous `0.1.0-rc.8` / current `0.1.1-rc.2`.
5
+ Status: historical P3-D audit snapshot. It records the support policy in force when this decision was made; it is **not** the current Host support policy. See [`dsh-support-window.md`](dsh-support-window.md) for the normative current policy and verification evidence.
6
+
7
+ Support window at decision time: DVR `2.0.x` / minimum DSH `0.1.0-rc.6` / previous `0.1.0-rc.8` / current `0.1.1-rc.2`.
6
8
 
7
9
  P3-D permits a migration only when all four conditions are true:
8
10
 
package/docs/doctor.md CHANGED
@@ -79,7 +79,7 @@ or `DSH_WEB_URL`. To perform offline checks only:
79
79
  npx dsh-vision-router doctor --no-runtime
80
80
  ```
81
81
 
82
- An unreachable DSH process is advisory rather than a doctor failure; offline checks still complete. When `--profile` is used against a reachable runtime, doctor does not attribute green route health to that profile unless the runtime exposes a verified Vision Router profile identity. If ownership cannot be proven, the human report shows `? runtime profile ownership unknown` instead of a false green binding.
82
+ An unreachable DSH process is advisory rather than a doctor failure; offline checks still complete. DSH Web may also require its signed browser-session cookie before any `/api` route is visible. If every side-effect-free route probe is rejected with `401`, Doctor reports the runtime as **reachable but authentication-required** and keeps plugin route health/ownership unknown instead of printing false route failures or bypassing Host authentication. The JSON report exposes `runtime.authenticationRequired: true`; capability support advice remains unknown because no Host seam was actually observed. When `--profile` is used against an authenticated reachable runtime, Doctor still does not attribute green route health to that profile unless the runtime exposes a verified Vision Router profile identity. If ownership cannot be proven, the human report shows `? runtime profile ownership unknown` instead of a false green binding.
83
83
 
84
84
  ### Local capability diagnostics
85
85
 
@@ -91,6 +91,9 @@ Doctor reports the local platform, Node version, and whether it can find:
91
91
 
92
92
  Tesseract, Chromium and profile-local Sharp are advisory because the corresponding optional tool may be unused or supplied by the host through another resolution path.
93
93
 
94
+
95
+ Doctor also keeps **public Host support policy** separate from **compatibility verification evidence**. Human output lists the DVR support floor/current stable Host in the support section, then lists exact stable/preview CI evidence and moving npm canary channels in a separate verification section. In `--json`, these are separate `hostSupportWindow` and `hostVerificationEvidence` objects; preview/canary values never appear inside `hostSupportWindow`.
96
+
94
97
  ### Scan historical sessions
95
98
 
96
99
  Session scanning is opt-in because a large session store can take longer to read/decompress:
@@ -0,0 +1,34 @@
1
+ # v2.1.4
2
+
3
+ DVR 2.1.4 is a compatibility and reliability patch for the current DSH stable/preview trains. It hardens Windows capture, OpenCode Go session affinity, attachment/timeout lifecycle boundaries, live-model credentials, background capability measurement, and Doctor diagnostics without changing the public 2.1.x routing model.
4
+
5
+ ## Runtime reliability
6
+
7
+ - Replaces the legacy Windows DPI screenshot shim with a dedicated PMv2 → PMv1 DPI-aware desktop capture path. Compilation uses an isolated ASCII temp directory so non-ASCII Windows user profiles no longer break `Add-Type`, while incorrect DPI contexts still fail closed instead of returning a cropped desktop.
8
+ - Propagates the real DSH session identity through vision child calls and isolates OpenCode Go wire affinity from internal Session identity. Direct Go requests without a stable session fail closed, existing upstream `x-opencode-session` headers remain authoritative, and benchmark traffic uses isolated synthetic affinity rather than contaminating user sessions.
9
+ - Bounds the user-facing lifetime of vision tools across Host attachment saves by combining the ambient turn signal, Host execution signal, and live `visionTaskTimeoutMs` deadline. Late plugin work observes the spent signal even though the Host attachment store itself has no physical cancellation primitive.
10
+ - Keeps `visionTaskTimeoutMs` hot: Settings changes are read on the next invocation without restarting DSH or re-registering tools.
11
+
12
+ ## DSH stable / preview compatibility
13
+
14
+ - Adds exact compatibility evidence for DSH `0.1.2-rc.1` and `0.1.3-alpha.2`, including real Host contracts, pi-ai wire behavior, Session-v2 restart coverage, cold Vision-toggle browser smoke, and alpha mixed generic-file paste lifecycle.
15
+ - Splits scheduled release canaries into npm `latest` (stable) and npm `alpha` (preview). These moving canaries automatically test newly published DSH trains while fixed exact-version gates preserve reproducible compatibility evidence.
16
+ - Hardens bitmap normalization and alpha mixed paste so MIME/extension mismatches, text+image+generic-file combinations, and duplicate lifecycle delivery cannot silently corrupt attachment intake.
17
+
18
+ ## Security, discovery, and diagnostics
19
+
20
+ - Separates public Host support policy from compatibility verification evidence. DVR 2.1.x keeps `0.1.0-rc.8` as its public minimum and `0.1.2-rc.1` as the current stable Host, while exact preview evidence and moving npm canaries are reported separately and never become an implicit support promise.
21
+ - Removes secret-derived data from live-model route fingerprints and from persisted background AUTH stops. Credential rotation is handled by the current `credentials/reference-updated` event with legacy-event compatibility, immediately invalidating stale evidence and revalidating with the new credential.
22
+ - Makes unattended background failures quieter and more accurate: authentication, protocol, and rate-limit failures share bounded transport-scope cooldowns instead of rotating through sibling models/axes every short gap, while ordinary network/timeout failures remain narrowly axis-scoped.
23
+ - Treats token-authenticated DSH Web correctly in Doctor. A blanket `401` now means “runtime reachable, browser authentication required, route health unknown” instead of a false route failure; Doctor does not bypass the Host’s signed-cookie boundary.
24
+ - Closes the remaining CodeQL findings without dismissing them; current Code Scanning is clean.
25
+
26
+ ## Validation
27
+
28
+ - Covered by Node 22/24, DSH rc.6/rc.7/rc.8 contracts, exact stable `0.1.2-rc.1`, exact preview `0.1.3-alpha.2`, Windows Node 24 screenshot runtime, provider transport, routing parity, Session surfaces, real browser lifecycle gates, and CodeQL.
29
+ - Local release-candidate verification completed with the full test suite at zero failures, contract tests at zero failures, and backend runtime policy coverage green.
30
+ - No Settings migration is required. Restart DSH Web/Desktop after upgrading so the new runtime and browser-side compatibility logic are loaded.
31
+
32
+ ## Upgrade
33
+
34
+ Upgrade to 2.1.4 and restart DSH Web/Desktop. Existing Vision Router settings remain compatible.
@@ -156,17 +156,26 @@ intentional: standing background authority is the user's permission to verify
156
156
  capability within the selected local/free/paid cost boundary, not permission to
157
157
  trust possibly stale Host modality metadata as ground truth.
158
158
 
159
- Transient failure state is scoped to the exact deployment fingerprint plus model
160
- and axis. Network, timeout, and rate-limit conditions receive retry backoff.
161
- Clear deployment-level non-retryable failures such as authentication failure,
162
- unavailable model, and unsupported protocol are persisted for the same exact
163
- fingerprint and stop unattended measurement across axes, including after process
164
- restart. Visual-proof and Benchmark-infrastructure failures remain axis-scoped;
165
- they do not manufacture a text-only verdict or disable other axes. An explicit
166
- image-input rejection is persisted separately as a measured text-only verdict.
167
- Ordinary settings refreshes, adapter notifications, and process restart do not
168
- silently clear these same-fingerprint stops. A changed deployment fingerprint
169
- creates a new evidence scope, and an explicit successful Test Vision clears the
159
+ Failure lifetime follows the narrowest authority that can explain the failure.
160
+ Timeout, network, visual-proof, and Benchmark-infrastructure failures remain
161
+ model/axis-scoped, so one failed axis does not suppress another axis or an
162
+ independent route. Authentication and unsupported-protocol failures apply to the
163
+ same transport scope (provider + endpoint + protocol + credential reference), and
164
+ rate-limit backoff is shared by sibling models on that same scope, preventing an
165
+ invalid or throttled account/endpoint from rotating through every model and axis
166
+ at the normal 15-second background gap.
167
+
168
+ Authentication stops are process-local and bounded; Vision Router never persists
169
+ an API-key value or deterministic derivative just to recognize a later key
170
+ rotation. Current `credentials/reference-updated` and legacy
171
+ `credentials/updated` notifications synchronously revoke the in-memory AUTH stop
172
+ and rescan with the new credential. Unsupported-protocol and unavailable-model
173
+ stops remain bounded persistent evidence (the former also suppresses sibling
174
+ models on the same transport; the latter stays model-specific). Visual-proof and
175
+ Benchmark-infrastructure failures do not manufacture a text-only verdict or
176
+ disable other axes. An explicit image-input rejection is persisted separately as
177
+ a measured text-only verdict. A changed deployment fingerprint creates a new
178
+ evidence scope, and an explicit successful Test Vision clears the corresponding
170
179
  same-fingerprint stop. Public background status exposes only a sanitized failure
171
180
  class/code; raw provider responses and credentials are not published to the
172
181
  browser.
package/index.js CHANGED
@@ -44,6 +44,14 @@ import { createRequire } from 'node:module'
44
44
  import { pathToFileURL } from 'node:url'
45
45
  import { promisify } from 'node:util'
46
46
  import { appendPromptToImageOnlyMessage, fetchWithOpenAICompatibility } from './lib/http-compat.js'
47
+ import {
48
+ directSessionAffinityHeaders,
49
+ isOfficialOpenCodeGoUrl,
50
+ openCodeSessionAffinityHeaderForUrl,
51
+ rawSessionIdentity,
52
+ sessionIdentityOf,
53
+ } from './lib/session-affinity.js'
54
+ import { runWithVisionSessionAffinity, streamWithVisionSessionAffinity } from './lib/session-affinity-runtime.js'
47
55
  import {
48
56
  routingCorrectionFor,
49
57
  toAnthropicMessages,
@@ -98,6 +106,7 @@ import {
98
106
  import { writeArtifactFile } from './lib/artifact-boundary.js'
99
107
  import { stripTrailingSlashes } from './lib/string-normalization.js'
100
108
  import { parseVersionComparator } from './lib/version-range.js'
109
+ import { captureWindowsDesktop } from './lib/windows-desktop-capture.js'
101
110
 
102
111
  // sharp is a native module with platform-specific prebuilt binaries. It used
103
112
  // to be imported statically, so a missing, broken, or conflicting install
@@ -2213,6 +2222,9 @@ export async function callLocalBackend(provider, messages, options = {}) {
2213
2222
  signal: options.signal,
2214
2223
  allowKeyless: true,
2215
2224
  system: system.join('\n').trim(),
2225
+ ...(rawSessionIdentity(options.sessionId) === undefined
2226
+ ? {}
2227
+ : { sessionId: rawSessionIdentity(options.sessionId) }),
2216
2228
  ...(typeof options.resolveCredential === 'function'
2217
2229
  ? { resolveCredential: options.resolveCredential }
2218
2230
  : {}),
@@ -2223,6 +2235,9 @@ export async function callLocalBackend(provider, messages, options = {}) {
2223
2235
  return callOpenAICompatible(provider, messages, {
2224
2236
  maxTokens,
2225
2237
  signal: options.signal,
2238
+ ...(rawSessionIdentity(options.sessionId) === undefined
2239
+ ? {}
2240
+ : { sessionId: rawSessionIdentity(options.sessionId) }),
2226
2241
  ...(typeof options.resolveCredential === 'function'
2227
2242
  ? { resolveCredential: options.resolveCredential }
2228
2243
  : {}),
@@ -2349,7 +2364,10 @@ export function toAnthropicContent(content) {
2349
2364
  }
2350
2365
 
2351
2366
  export async function callOpenAICompatible(provider, messages, options = {}) {
2352
- const headers = { 'content-type': 'application/json' }
2367
+ const headers = {
2368
+ 'content-type': 'application/json',
2369
+ ...directSessionAffinityHeaders(provider, options.affinityId ?? options.sessionId),
2370
+ }
2353
2371
  const apiKeyEnv = typeof provider.apiKeyEnv === 'string' ? provider.apiKeyEnv : ''
2354
2372
  let resolvedApiKey = ''
2355
2373
  if (apiKeyEnv !== '') {
@@ -2493,11 +2511,13 @@ export function createChunkAssembler() {
2493
2511
  }
2494
2512
 
2495
2513
  async function visionAnswer(llm, options) {
2496
- const assembler = createChunkAssembler()
2497
- for await (const chunk of llm.stream(options)) {
2498
- assembler.push(chunk)
2499
- }
2500
- return assembler.finish()
2514
+ return runWithVisionSessionAffinity(options?.sessionId, async () => {
2515
+ const assembler = createChunkAssembler()
2516
+ for await (const chunk of llm.stream(options)) {
2517
+ assembler.push(chunk)
2518
+ }
2519
+ return assembler.finish()
2520
+ })
2501
2521
  }
2502
2522
 
2503
2523
  /** Environment shim for `resolveAdapterOptions`: `{ get: (name) => ({ value }) }`. */
@@ -3280,13 +3300,7 @@ export function apply(ctx, config = {}, runtime = {}) {
3280
3300
  return 0
3281
3301
  }
3282
3302
  }
3283
- const sessionIdOf = (session) => {
3284
- try {
3285
- return session && session.id !== undefined ? String(session.id) : 'anon'
3286
- } catch {
3287
- return 'anon'
3288
- }
3289
- }
3303
+ const sessionIdOf = (session) => sessionIdentityOf(session) ?? 'anon'
3290
3304
  const visionScopeOf = (session) => `${sessionIdOf(session)}:${turnNumberOf(session)}`
3291
3305
 
3292
3306
  /** Stable, never-logged fingerprint of the credential a backend will use. */
@@ -3653,11 +3667,13 @@ export function apply(ctx, config = {}, runtime = {}) {
3653
3667
  ? callLocalBackend(entry.provider, openAIMessages, {
3654
3668
  maxTokens: entry.provider.maxTokens ?? 4096,
3655
3669
  signal: options.signal,
3670
+ sessionId: options.sessionId,
3656
3671
  resolveCredential,
3657
3672
  })
3658
3673
  : callOpenAICompatible(entry.provider, openAIMessages, {
3659
3674
  maxTokens: entry.provider.maxTokens ?? 4096,
3660
3675
  signal: options.signal,
3676
+ sessionId: options.sessionId,
3661
3677
  resolveCredential,
3662
3678
  }))
3663
3679
  } catch (error) {
@@ -4132,6 +4148,16 @@ export function apply(ctx, config = {}, runtime = {}) {
4132
4148
  }
4133
4149
  return { ok: true, rawProfile, resolvedProfile, transport }
4134
4150
  }
4151
+ const assertOpenCodeGoAffinityForPair = (pair, sessionId) => {
4152
+ const plan = channelBridgePlan(pair.provider, pair.model)
4153
+ const baseURL = plan?.transport?.baseURL
4154
+ if (!isOfficialOpenCodeGoUrl(baseURL)) return
4155
+ // Validation only: Host receives the unmodified DSH sessionId, while the
4156
+ // scoped final-wire compatibility layer owns x-opencode-session. Fail here
4157
+ // before pi-ai can turn a non-ByteString id into an opaque SDK error.
4158
+ openCodeSessionAffinityHeaderForUrl(baseURL, sessionId)
4159
+ }
4160
+
4135
4161
  const resolveChannelApiKey = async (plan) => {
4136
4162
  const ref = plan && plan.transport && plan.transport.apiKeyEnv
4137
4163
  if (typeof ref === 'string' && ref !== '') {
@@ -4163,7 +4189,7 @@ export function apply(ctx, config = {}, runtime = {}) {
4163
4189
  }
4164
4190
  return undefined
4165
4191
  }
4166
- const directChannelVisionAnswer = async (provider, model, blocks, instruction, signal) => {
4192
+ const directChannelVisionAnswer = async (provider, model, blocks, instruction, options = {}) => {
4167
4193
  const plan = channelBridgePlan(provider, model)
4168
4194
  if (!plan.ok) throw new Error(`vision bridge unavailable: ${plan.reason}`)
4169
4195
  const apiKey = await resolveChannelApiKey(plan)
@@ -4187,7 +4213,12 @@ export function apply(ctx, config = {}, runtime = {}) {
4187
4213
  apiKeyEnv: '__vision-router-channel__',
4188
4214
  },
4189
4215
  [{ role: 'user', content: [...content, { type: 'text', text: instruction }] }],
4190
- { maxTokens: 4096, signal, resolveCredential: () => apiKey },
4216
+ {
4217
+ maxTokens: 4096,
4218
+ signal: options.signal,
4219
+ sessionId: options.sessionId,
4220
+ resolveCredential: () => apiKey,
4221
+ },
4191
4222
  )
4192
4223
  }
4193
4224
 
@@ -4266,6 +4297,7 @@ export function apply(ctx, config = {}, runtime = {}) {
4266
4297
  system: anthropic.system,
4267
4298
  maxTokens: options.maxTokens ?? 4096,
4268
4299
  signal: options.signal,
4300
+ sessionId: options.sessionId,
4269
4301
  apiKey,
4270
4302
  },
4271
4303
  )
@@ -4275,12 +4307,16 @@ export function apply(ctx, config = {}, runtime = {}) {
4275
4307
  const callVisionPair = async (pair, messages, options = {}) => {
4276
4308
  const corrected = await correctedVisionAnswer(pair, messages, options)
4277
4309
  if (corrected !== undefined) return corrected
4310
+ assertOpenCodeGoAffinityForPair(pair, options.sessionId)
4278
4311
  return visionAnswer(ctx.llm, {
4279
4312
  provider: pair.provider,
4280
4313
  model: pair.model,
4281
4314
  messages,
4282
4315
  maxTokens: options.maxTokens ?? 4096,
4283
4316
  signal: options.signal,
4317
+ ...(rawSessionIdentity(options.sessionId) === undefined
4318
+ ? {}
4319
+ : { sessionId: rawSessionIdentity(options.sessionId) }),
4284
4320
  })
4285
4321
  }
4286
4322
 
@@ -4327,7 +4363,7 @@ export function apply(ctx, config = {}, runtime = {}) {
4327
4363
  pair.model,
4328
4364
  options.bridgeBlocks,
4329
4365
  options.bridgeInstruction,
4330
- options.signal,
4366
+ { signal: options.signal, sessionId: options.sessionId },
4331
4367
  )
4332
4368
  }
4333
4369
  throw error
@@ -4582,16 +4618,18 @@ export function apply(ctx, config = {}, runtime = {}) {
4582
4618
  const text = await correctedVisionAnswer(pair, messages, {
4583
4619
  maxTokens: options.maxTokens ?? 65536,
4584
4620
  signal: attemptSignal,
4621
+ sessionId: options.sessionId,
4585
4622
  })
4586
4623
  if (text === undefined) {
4587
- yield* ctx.llm.stream({
4624
+ assertOpenCodeGoAffinityForPair(pair, options.sessionId)
4625
+ yield* streamWithVisionSessionAffinity(options.sessionId, () => ctx.llm.stream({
4588
4626
  ...options,
4589
4627
  provider: pair.provider,
4590
4628
  model: pair.model,
4591
4629
  reasoningEffort: undefined,
4592
4630
  messages,
4593
4631
  signal: attemptSignal,
4594
- })
4632
+ }))
4595
4633
  return
4596
4634
  }
4597
4635
  if (text !== '') {
@@ -5310,6 +5348,7 @@ export function apply(ctx, config = {}, runtime = {}) {
5310
5348
  // later calls answer instantly — no network, no re-hitting a tripped
5311
5349
  // 401 provider, no minutes of "deep diving".
5312
5350
  const session = exec && exec.agent && exec.agent.session
5351
+ const sessionId = sessionIdentityOf(session)
5313
5352
  const scope = visionScopeOf(session)
5314
5353
  if (visionTurnMemory.allFailed(scope)) {
5315
5354
  return JSON.stringify(
@@ -5383,6 +5422,7 @@ export function apply(ctx, config = {}, runtime = {}) {
5383
5422
  let text = await callVisionPairWithOptionalBridge(pair, messages, {
5384
5423
  maxTokens: 4096,
5385
5424
  signal,
5425
+ sessionId,
5386
5426
  capability,
5387
5427
  bridgeBlocks: blocks,
5388
5428
  bridgeInstruction: promptText,
@@ -5421,6 +5461,7 @@ ctx.logger?.info(
5421
5461
  text = await callVisionPairWithOptionalBridge(pair, messages, {
5422
5462
  maxTokens: 4096,
5423
5463
  signal,
5464
+ sessionId,
5424
5465
  capability,
5425
5466
  bridgeBlocks: blocks,
5426
5467
  bridgeInstruction:
@@ -5524,6 +5565,7 @@ ctx.logger?.info(
5524
5565
  {
5525
5566
  maxTokens: provider.maxTokens ?? 4096,
5526
5567
  signal: attemptSignal,
5568
+ sessionId,
5527
5569
  resolveCredential,
5528
5570
  },
5529
5571
  )
@@ -5966,6 +6008,7 @@ ctx.logger?.info(
5966
6008
  {
5967
6009
  maxTokens: 4096,
5968
6010
  signal: attemptSignal,
6011
+ sessionId: options.sessionId,
5969
6012
  capability: pairCapability,
5970
6013
  bridgeBlocks: [block],
5971
6014
  bridgeInstruction: instruction,
@@ -6004,6 +6047,7 @@ ctx.logger?.info(
6004
6047
  deadline.signal(),
6005
6048
  AbortSignal.timeout(timeoutMs()),
6006
6049
  ),
6050
+ sessionId: options.sessionId,
6007
6051
  resolveCredential,
6008
6052
  },
6009
6053
  )
@@ -6020,11 +6064,14 @@ ctx.logger?.info(
6020
6064
 
6021
6065
  // Tool-facing wrapper: binds the caller's session+turn scope so the
6022
6066
  // breaker and the turn memory act per conversation turn.
6023
- const answerVisionForTool = (exec, imageBytes, mediaType, instruction, options = {}) =>
6024
- answerVision(imageBytes, mediaType, instruction, {
6025
- scope: visionScopeOf(exec && exec.agent && exec.agent.session),
6067
+ const answerVisionForTool = (exec, imageBytes, mediaType, instruction, options = {}) => {
6068
+ const session = exec && exec.agent && exec.agent.session
6069
+ return answerVision(imageBytes, mediaType, instruction, {
6026
6070
  ...options,
6071
+ scope: visionScopeOf(session),
6072
+ sessionId: sessionIdentityOf(session),
6027
6073
  })
6074
+ }
6028
6075
 
6029
6076
  deepToolDefs.push({
6030
6077
  name: 'vision_ground',
@@ -6960,7 +7007,7 @@ ctx.logger?.info(
6960
7007
  })
6961
7008
 
6962
7009
  // ── dsh-vision 并入:屏幕截图(vision_screenshot)───────────────────────
6963
- // 截取用户桌面。平台命令:Windows PowerShell CopyFromScreen(虚拟屏幕)、
7010
+ // 截取用户桌面。平台命令:Windows PMv2-aware PowerShell helper(虚拟屏幕)、
6964
7011
  // macOS screencapture(主显示器)、Linux ImageMagick import(回退 scrot,
6965
7012
  // 两者均为系统外部依赖)。产物写入工作区 artifacts 目录。
6966
7013
  // Boot-time opt-in: the tool is registered ONLY when desktopScreenshot is
@@ -6971,7 +7018,7 @@ ctx.logger?.info(
6971
7018
  name: 'vision_screenshot',
6972
7019
  description:
6973
7020
  'Capture the user\'s desktop screen as a PNG artifact (the virtual screen on Windows; the main display on macOS; the root display on Linux). ' +
6974
- 'Windows: PowerShell CopyFromScreen; macOS: screencapture; Linux: ImageMagick import (falls back to scrot; either command must be installed). ' +
7021
+ 'Windows: per-monitor-DPI-aware PowerShell capture; macOS: screencapture; Linux: ImageMagick import (falls back to scrot; either command must be installed). ' +
6975
7022
  'This privacy-sensitive tool is disabled by default and works only after the user explicitly enables Desktop screenshot in Vision Router settings. ' +
6976
7023
  'Use it when you need to see what is on the user\'s screen right now — e.g. their current GUI, an app, or a page outside this browser. ' +
6977
7024
  'Optional identify=true also runs local recognition on the capture using the enabled local backends (Ollama, then LM Studio) and returns the description alongside the path.',
@@ -7000,18 +7047,13 @@ ctx.logger?.info(
7000
7047
  const platform = process.platform
7001
7048
  try {
7002
7049
  if (platform === 'win32') {
7003
- const script = [
7004
- 'Add-Type -AssemblyName System.Windows.Forms,System.Drawing',
7005
- '$b=[System.Windows.Forms.SystemInformation]::VirtualScreen',
7006
- '$bmp=New-Object System.Drawing.Bitmap($b.Width,$b.Height)',
7007
- '$g=[System.Drawing.Graphics]::FromImage($bmp)',
7008
- '$g.CopyFromScreen($b.X,$b.Y,0,0,$bmp.Size)',
7009
- `$bmp.Save('${tmp.replace(/'/g, "''")}')`,
7010
- '$g.Dispose();$bmp.Dispose()',
7011
- ].join('; ')
7012
- await promisify(execFile)('powershell.exe', ['-NoProfile', '-STA', '-Command', script], {
7013
- timeout: timeoutMs(),
7014
- windowsHide: true,
7050
+ // #409: own the DPI-aware capture here instead of emitting the
7051
+ // known-broken logical-coordinate script and hoping a global
7052
+ // promisify(execFile) shim rewrites it later. The helper also
7053
+ // isolates CodeDom TEMP/TMP to a writable ASCII path.
7054
+ await captureWindowsDesktop(tmp, {
7055
+ timeoutMs: timeoutMs(),
7056
+ signal: exec?.signal,
7015
7057
  })
7016
7058
  } else if (platform === 'darwin') {
7017
7059
  // Without -m, screencapture writes one file per display. The code