dsh-llm-fallbacks 0.3.0 → 0.3.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 CHANGED
@@ -5,17 +5,17 @@
5
5
  [![license](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
6
6
  ![node](https://img.shields.io/badge/node-%3E%3D22-339933.svg)
7
7
  ![pnpm](https://img.shields.io/badge/pnpm-%3E%3D10-f69220.svg)
8
- ![dsh web](https://img.shields.io/badge/dsh%20web-compatible-4B32C3.svg)
9
8
  ![dsh tui](https://img.shields.io/badge/dsh%20tui-compatible-4B32C3.svg)
9
+ ![dsh](https://img.shields.io/badge/DSH-0.1.0--rc.8-4B32C3.svg)
10
10
  [![dshfind](https://dshfind.com/api/badge/omdsh-dev/dsh-llm-fallbacks?lang=en)](https://dshfind.com/zh/plugins/omdsh-dev/dsh-llm-fallbacks?ref=badge)
11
11
 
12
12
  Automatic provider/model fallback chains for dsh (DeepSeek Harness): when an agent's LLM requests keep failing — retries exhausted, auth errors, quota exceeded, rate limiting (429) — the plugin switches provider/model along the fallback chain for the current role, and the current step/turn continues on the target model: tasks are not interrupted by model problems.
13
13
 
14
- Works in both dsh front ends: the **web** profile (Settings → 插件配置 → Fallbacks card) and the **dsh-tui** terminal profile (`/fallbacks` + `/fallbacks config`).
14
+ Works in both dsh front ends: the **web** profile (Settings → Plugins → Fallbacks card) and the **dsh-tui** terminal profile (`/fallbacks` + `/fallbacks config`).
15
15
 
16
16
  ## Time slots
17
17
 
18
- Time slots (分时切换) rotate the **effective root chain** by wall-clock windows: each slot row carries its own fallback chain, and the first row whose window contains the current moment replaces the all-day chain for the next root request — the all-day chain stays as the last resort when no slot matches. Peak and valley windows can therefore use different chains while the failure walk (降级切换) remains untouched.
18
+ Time slots rotate the **effective root chain** by wall-clock windows: each slot row carries its own fallback chain, and the first row whose window contains the current moment replaces the all-day chain for the next root request — the all-day chain stays as the last resort when no slot matches. Peak and valley windows can therefore use different chains while the failure walk (fallback switch) remains untouched.
19
19
 
20
20
  ![Time slots](docs/assets/screenshot-1-en.png)
21
21
 
@@ -30,7 +30,7 @@ Four frozen UTC+8 presets (windows are code constants; preset rows lock `tz` to
30
30
 
31
31
  GLM Peak and GLM Valley are offered in the card picker only when `zai-coding-cn` is configured.
32
32
 
33
- The first extra row whose window contains the current moment (in `fallbacks.tz`, default Asia/Shanghai) wins; no match → the all-day `rootChain`, whose tail (默认模型) must be exactly one official V4 model — `deepseek-official/deepseek-v4-flash` XOR `deepseek-official/deepseek-v4-pro`. Slot rotation is a routing seed, not a failure decision: it applies on the next root request, consumes no cooldown, and is logged as a time-slot switch (分时切换) — failure walks keep 降级切换. Full semantics → [Time-slot presets (分时切换)](#time-slot-presets-分时切换) and [docs/configuration.md](docs/configuration.md).
33
+ The first extra row whose window contains the current moment (in `fallbacks.tz`, default Asia/Shanghai) wins; no match → the all-day `rootChain`, whose tail (Default model) must be exactly one official V4 model — `deepseek-official/deepseek-v4-flash` XOR `deepseek-official/deepseek-v4-pro`. Slot rotation is a routing seed, not a failure decision: it applies on the next root request, consumes no cooldown, and is logged as a time-slot switch — failure walks keep fallback switch. Full semantics → [Time-slot presets](#time-slot-presets) and [docs/configuration.md](docs/configuration.md).
34
34
 
35
35
  ## Quick start
36
36
 
@@ -64,8 +64,8 @@ Add a `fallbacks:` section to the dsh settings document (default `$DSH_HOME/sett
64
64
  ```yaml
65
65
  fallbacks:
66
66
  enabled: true # feature switch; defaults to false — set explicitly to enable
67
- rootChain: # all-day chain: LAST entry is 默认模型 (official V4);
68
- - anthropic/claude-3-5-sonnet # leading entries = 默认降级链 (walked first)
67
+ rootChain: # all-day chain: LAST entry is Default model (official V4);
68
+ - anthropic/claude-3-5-sonnet # leading entries = Default fallback chain (walked first)
69
69
  - deepseek-official/deepseek-v4-flash # last = Flash XOR Pro last-resort fallback
70
70
  timeSlots: # optional: rotate the effective root chain by wall-clock windows
71
71
  - kind: preset # frozen UTC+8 windows; only the model chain is editable (locks tz to Asia/Shanghai)
@@ -103,7 +103,7 @@ Save and restart the session, then type `/fallbacks` — the read-only in-sessio
103
103
  - **Automatic fallback for root and subagents**: any agent switches down the chain to the next available provider/model on model failure — no manual model switching.
104
104
  - **Two-block config**: `rootChain` for the root agent; declared role entities (`roles.list`) referenced by `roles.rules` (or the built-in `inherit`).
105
105
  - **Chain as root primary from the picker**: when `enabled` is on, the host model picker (web and TUI alike) shows a virtual `FallbacksChain` / `Auto` row — selecting it uses the configured chain as the root primary (a conforming all-day head is required for the override to succeed); selecting a real model keeps fallback-only (see [FallbacksChain in the model picker](#fallbackschain-in-the-model-picker)).
106
- - **Time slots (分时切换)**: optional `fallbacks.timeSlots` rows rotate the effective root chain by wall-clock windows in the config-level `tz` timezone (default `Asia/Shanghai`) — four frozen UTC+8 presets (`liang-peak` / `liang-valley` / `glm-peak` / `glm-valley`, windows are code constants, models-only edits) or custom `start`/`end`/`days` windows. The first matching row wins; the all-day row is always last. A slot change applies on the **next** root request and is logged as a **time-slot switch** (分时切换) — a routing seed, never a failure decision: it consumes no cooldown and does not count against `maxSwitchesPerStep`. Failure walks keep the **降级切换** / fallback-switch copy (see [Time-slot presets](#time-slot-presets-分时切换)).
106
+ - **Time slots**: optional `fallbacks.timeSlots` rows rotate the effective root chain by wall-clock windows in the config-level `tz` timezone (default `Asia/Shanghai`) — four frozen UTC+8 presets (`liang-peak` / `liang-valley` / `glm-peak` / `glm-valley`, windows are code constants, models-only edits) or custom `start`/`end`/`days` windows. The first matching row wins; the all-day row is always last. A slot change applies on the **next** root request and is logged as a **time-slot switch** — a routing seed, never a failure decision: it consumes no cooldown and does not count against `maxSwitchesPerStep`. Failure walks keep the **fallback switch** copy (see [Time-slot presets](#time-slot-presets)).
107
107
  - **Dispatch-time role resolution**: on a subagent's first request its role is resolved in three stages — explicit (`agentPreset` matches a declared role id) → deterministic rules (unchanged) → LLM auto-match from the declared role taxonomy (`fallbacks.roleAutoMatch`, default `true`). The resolved role's chain-head model is injected into the first request and recorded via an explicit `role → model` log line (no durable `fallbacks/switch` event is written — issue #52 stop-write); set `roleAutoMatch: false` to disable the LLM auto-match stage (the explicit `agentPreset` stage still applies — with no explicit role this reproduces the previous rules-only behavior). The settings card always renders an **Enable role auto-match** switch (default `true`) to toggle it — the schema default applies even to legacy configs that never declared the key.
108
108
  - **Cooldown and revert**: failed / switched-away models are not re-selected during cooldown; `revertPolicy: cooldown-expiry` returns to the primary model automatically.
109
109
  - **Visible behavior**: every switch is recorded in an info-level log line (from/to/role/reason) — no silent model switching. The plugin deliberately writes **no** durable `fallbacks/switch` session events (issue #52: the apply()-time event-type registration was proven ineffective, and a session containing the event refused to load after a dsh restart). Sessions written by older plugin versions that contain such events are repaired by `scripts/repair-fallbacks-switch-logs.ts`, which marks legacy events ignorable so affected sessions load again.
@@ -122,19 +122,19 @@ Notes:
122
122
 
123
123
  - **Picker label**: the row's catalog `name` (what the composer trigger shows) is live — `Auto: DeepSeek V4 Flash[Liang Peak]` / `Auto: DeepSeek V4 Flash[all-day]` (catalog display name, not the model id); the id stays `Auto`. Bare `Auto` if the all-day tail is not conforming. Refresh by reopening the picker.
124
124
  - **Root only**: the row is about the root agent. Subagent role resolution and injection are unchanged; a subagent session that inherits the selection still routes through the chain head — the virtual row is a thin delegate, never a second routing engine.
125
- - **Conformance gate on the tail**: a successful override/delegate requires the all-day chain to be **tail-conforming** — its last entry must be exactly one official V4 model (`deepseek-official/deepseek-v4-flash` or `deepseek-official/deepseek-v4-pro`, the card's 默认模型 panel); leading entries (默认降级链) are walked first. Disabling the plugin hides the row again (slot-row/chain edits never churn registration).
125
+ - **Conformance gate on the tail**: a successful override/delegate requires the all-day chain to be **tail-conforming** — its last entry must be exactly one official V4 model (`deepseek-official/deepseek-v4-flash` or `deepseek-official/deepseek-v4-pro`, the card's Default model panel); leading entries (Default fallback chain) are walked first. Disabling the plugin hides the row again (slot-row/chain edits never churn registration).
126
126
  - **Stale selection**: if the row disappears (plugin disabled) while `FallbacksChain / Auto` is selected, the session keeps showing it as the current model with `routable: false` — pick a real model from the catalog to continue (host-native catalog semantics).
127
127
  - **Capabilities follow the head**: the row's model metadata (context window, modalities, reasoning) mirrors the current effective head; retry attribution follows the permissive default — retries/failures are accounted to the real head pair, not to the `FallbacksChain` provider. Full semantics → [docs/configuration.md](docs/configuration.md).
128
128
 
129
- ## Time-slot presets (分时切换)
129
+ ## Time-slot presets
130
130
 
131
- Time slots (分时切换) are introduced in the [featured overview](#time-slots) above; this section is the reference. Time-slot rows rotate the **effective root chain** by wall-clock windows — useful for peak/valley pricing without confusing wall-clock rotation with failure fallback. The copy split is strict: slot rotation logs and UI say **分时切换** (time-slot switch); the failure walk keeps **降级切换** (fallback switch); the conversation notice 模型已降级 / Model downgraded stays on the failure path only.
131
+ Time slots are introduced in the [featured overview](#time-slots) above; this section is the reference. Time-slot rows rotate the **effective root chain** by wall-clock windows — useful for peak/valley pricing without confusing wall-clock rotation with failure fallback. The copy split is strict: slot rotation logs and UI say **time-slot switch**; the failure walk keeps **fallback switch**; the conversation notice Model downgraded stays on the failure path only.
132
132
 
133
- - **Match order**: at every root request, the first extra row whose window contains the current moment (in `fallbacks.tz`, default `Asia/Shanghai` / UTC+8) wins — that row's chain **replaces** the all-day chain. No row matches → the all-day `rootChain` is used. The all-day row is always last and **required**: its last entry must be exactly one official V4 model (Flash XOR Pro; leading 默认降级链 entries are walked first).
133
+ - **Match order**: at every root request, the first extra row whose window contains the current moment (in `fallbacks.tz`, default `Asia/Shanghai` / UTC+8) wins — that row's chain **replaces** the all-day chain. No row matches → the all-day `rootChain` is used. The all-day row is always last and **required**: its last entry must be exactly one official V4 model (Flash XOR Pro; leading Default fallback chain entries are walked first).
134
134
  - **Presets** (frozen, not user-editable): `liang-peak` = 09:00–12:00 **and** 14:00–18:00 every day; `liang-valley` = every other UTC+8 time; `glm-peak` = Monday–Friday 14:00–18:00; `glm-valley` = every other time. One preset id = one row; the card picker never offers a duplicate.
135
135
  - **Custom rows**: `start` / `end` (`HH:mm`, may wrap midnight) + optional `days` (0=Sunday…6=Saturday; omitted/empty = every day) + models.
136
136
  - **Next-request apply**: a slot boundary crossing never preempts an in-flight step — the new row takes effect on the next root request. Rotation is mount-only: info log + card/`/fallbacks` status line, no durable switch event.
137
- - **Settings card**: the 主代理 section groups 分时槽设置 (extra rows — add preset / add custom / remove / reorder by buttons or **drag**; preset rows show a read-only window summary and edit models only; custom rows carry an editable name; the **timezone picker** lives here and **locks to Asia/Shanghai while any preset row exists**, since preset windows are frozen UTC+8 constants), 默认降级链 (walked first when no slot matches) and 默认模型 (the official V4 Flash | Pro last-resort fallback). Rows are collapsible to name + first model. There is no `timeSlots.enabled` master switch (adding a row is the opt-in) and no `rootMode` control.
137
+ - **Settings card**: the Main agent section groups Time slots (extra rows — add preset / add custom / remove / reorder by buttons or **drag**; preset rows show a read-only window summary and edit models only; custom rows carry an editable name; the **timezone picker** lives here and **locks to Asia/Shanghai while any preset row exists**, since preset windows are frozen UTC+8 constants), Default fallback chain (walked first when no slot matches) and Default model (the official V4 Flash | Pro last-resort fallback). Rows are collapsible to name + first model. There is no `timeSlots.enabled` master switch (adding a row is the opt-in) and no `rootMode` control.
138
138
 
139
139
  ## Preset roles
140
140
 
package/README.zh-CN.md CHANGED
@@ -5,8 +5,8 @@
5
5
  [![license](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
6
6
  ![node](https://img.shields.io/badge/node-%3E%3D22-339933.svg)
7
7
  ![pnpm](https://img.shields.io/badge/pnpm-%3E%3D10-f69220.svg)
8
- ![dsh web](https://img.shields.io/badge/dsh%20web-compatible-4B32C3.svg)
9
8
  ![dsh tui](https://img.shields.io/badge/dsh%20tui-compatible-4B32C3.svg)
9
+ ![dsh](https://img.shields.io/badge/DSH-0.1.0--rc.8-4B32C3.svg)
10
10
  [![dshfind](https://dshfind.com/api/badge/omdsh-dev/dsh-llm-fallbacks?lang=zh)](https://dshfind.com/zh/plugins/omdsh-dev/dsh-llm-fallbacks?ref=badge)
11
11
 
12
12
  dsh(DeepSeek Harness)的自动模型降级插件:当 root agent 或 subagent 的模型请求持续失败(重试耗尽、权限、配额超限、限流 429)时,按角色/模型 fallback 链自动切换 provider/model,当前 step/turn 在目标模型上继续完成——任务不因模型问题中断。
@@ -80,7 +80,7 @@
80
80
  */
81
81
  import type { ReactNode } from 'react';
82
82
  import type { PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
83
- import type { SnapshotSelectorHook } from '@deepseek-ai/dsh-client-web-react';
83
+ import type { SnapshotSelectorHook } from '@deepseek-ai/dsh-client-ui-slots';
84
84
  import { FallbacksSettingsController, type FallbacksSettingsState } from './fallbacks-store.ts';
85
85
  /** Injected dependencies of {@link FallbacksCard} (slot `inject`). */
86
86
  export interface FallbacksCardInjected {
@@ -1,6 +1,6 @@
1
1
  import type { ReactNode } from 'react';
2
2
  import type { PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
3
- import type { SnapshotSelectorHook } from '@deepseek-ai/dsh-client-web-react';
3
+ import type { SnapshotSelectorHook } from '@deepseek-ai/dsh-client-ui-slots';
4
4
  import type { FallbacksSettingsController, FallbacksSettingsState } from './fallbacks-store.ts';
5
5
  /** Injected dependencies of {@link GeneralFallbacksRow} (slot `inject`). */
6
6
  export interface GeneralFallbacksRowInjected {
@@ -4,11 +4,170 @@ window.__ModuleLoader__.load({
4
4
  var module = { exports: {} };
5
5
  var exports = module.exports;
6
6
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
- let _deepseek_ai_dsh_client_web_react = require("@deepseek-ai/dsh-client-web-react");
7
+ //#region \0rolldown/runtime.js
8
+ var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);
9
+ //#endregion
8
10
  let react = require("react");
9
11
  let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
10
12
  let _deepseek_ai_dsh_client_runtime_client = require("@deepseek-ai/dsh-client-runtime/client");
11
13
  let react_jsx_runtime = require("react/jsx-runtime");
14
+ //#region node_modules/use-sync-external-store/cjs/use-sync-external-store-shim.production.js
15
+ /**
16
+ * @license React
17
+ * use-sync-external-store-shim.production.js
18
+ *
19
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
20
+ *
21
+ * This source code is licensed under the MIT license found in the
22
+ * LICENSE file in the root directory of this source tree.
23
+ */
24
+ var require_use_sync_external_store_shim_production = /* @__PURE__ */ __commonJSMin(((exports) => {
25
+ var React$1 = require("react");
26
+ function is(x, y) {
27
+ return x === y && (0 !== x || 1 / x === 1 / y) || x !== x && y !== y;
28
+ }
29
+ var objectIs = "function" === typeof Object.is ? Object.is : is;
30
+ var useState = React$1.useState;
31
+ var useEffect = React$1.useEffect;
32
+ var useLayoutEffect = React$1.useLayoutEffect;
33
+ var useDebugValue = React$1.useDebugValue;
34
+ function useSyncExternalStore$2(subscribe, getSnapshot) {
35
+ var value = getSnapshot(), _useState = useState({ inst: {
36
+ value,
37
+ getSnapshot
38
+ } }), inst = _useState[0].inst, forceUpdate = _useState[1];
39
+ useLayoutEffect(function() {
40
+ inst.value = value;
41
+ inst.getSnapshot = getSnapshot;
42
+ checkIfSnapshotChanged(inst) && forceUpdate({ inst });
43
+ }, [
44
+ subscribe,
45
+ value,
46
+ getSnapshot
47
+ ]);
48
+ useEffect(function() {
49
+ checkIfSnapshotChanged(inst) && forceUpdate({ inst });
50
+ return subscribe(function() {
51
+ checkIfSnapshotChanged(inst) && forceUpdate({ inst });
52
+ });
53
+ }, [subscribe]);
54
+ useDebugValue(value);
55
+ return value;
56
+ }
57
+ function checkIfSnapshotChanged(inst) {
58
+ var latestGetSnapshot = inst.getSnapshot;
59
+ inst = inst.value;
60
+ try {
61
+ var nextValue = latestGetSnapshot();
62
+ return !objectIs(inst, nextValue);
63
+ } catch (error) {
64
+ return !0;
65
+ }
66
+ }
67
+ function useSyncExternalStore$1(subscribe, getSnapshot) {
68
+ return getSnapshot();
69
+ }
70
+ var shim = "undefined" === typeof window || "undefined" === typeof window.document || "undefined" === typeof window.document.createElement ? useSyncExternalStore$1 : useSyncExternalStore$2;
71
+ exports.useSyncExternalStore = void 0 !== React$1.useSyncExternalStore ? React$1.useSyncExternalStore : shim;
72
+ }));
73
+ //#endregion
74
+ //#region node_modules/use-sync-external-store/shim/index.js
75
+ var require_shim = /* @__PURE__ */ __commonJSMin(((exports, module) => {
76
+ module.exports = require_use_sync_external_store_shim_production();
77
+ }));
78
+ //#endregion
79
+ //#region node_modules/use-sync-external-store/cjs/use-sync-external-store-shim/with-selector.production.js
80
+ /**
81
+ * @license React
82
+ * use-sync-external-store-shim/with-selector.production.js
83
+ *
84
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
85
+ *
86
+ * This source code is licensed under the MIT license found in the
87
+ * LICENSE file in the root directory of this source tree.
88
+ */
89
+ var require_with_selector_production = /* @__PURE__ */ __commonJSMin(((exports) => {
90
+ var React = require("react");
91
+ var shim = require_shim();
92
+ function is(x, y) {
93
+ return x === y && (0 !== x || 1 / x === 1 / y) || x !== x && y !== y;
94
+ }
95
+ var objectIs = "function" === typeof Object.is ? Object.is : is;
96
+ var useSyncExternalStore = shim.useSyncExternalStore;
97
+ var useRef = React.useRef;
98
+ var useEffect = React.useEffect;
99
+ var useMemo = React.useMemo;
100
+ var useDebugValue = React.useDebugValue;
101
+ exports.useSyncExternalStoreWithSelector = function(subscribe, getSnapshot, getServerSnapshot, selector, isEqual) {
102
+ var instRef = useRef(null);
103
+ if (null === instRef.current) {
104
+ var inst = {
105
+ hasValue: !1,
106
+ value: null
107
+ };
108
+ instRef.current = inst;
109
+ } else inst = instRef.current;
110
+ instRef = useMemo(function() {
111
+ function memoizedSelector(nextSnapshot) {
112
+ if (!hasMemo) {
113
+ hasMemo = !0;
114
+ memoizedSnapshot = nextSnapshot;
115
+ nextSnapshot = selector(nextSnapshot);
116
+ if (void 0 !== isEqual && inst.hasValue) {
117
+ var currentSelection = inst.value;
118
+ if (isEqual(currentSelection, nextSnapshot)) return memoizedSelection = currentSelection;
119
+ }
120
+ return memoizedSelection = nextSnapshot;
121
+ }
122
+ currentSelection = memoizedSelection;
123
+ if (objectIs(memoizedSnapshot, nextSnapshot)) return currentSelection;
124
+ var nextSelection = selector(nextSnapshot);
125
+ if (void 0 !== isEqual && isEqual(currentSelection, nextSelection)) return memoizedSnapshot = nextSnapshot, currentSelection;
126
+ memoizedSnapshot = nextSnapshot;
127
+ return memoizedSelection = nextSelection;
128
+ }
129
+ var hasMemo = !1, memoizedSnapshot, memoizedSelection, maybeGetServerSnapshot = void 0 === getServerSnapshot ? null : getServerSnapshot;
130
+ return [function() {
131
+ return memoizedSelector(getSnapshot());
132
+ }, null === maybeGetServerSnapshot ? void 0 : function() {
133
+ return memoizedSelector(maybeGetServerSnapshot());
134
+ }];
135
+ }, [
136
+ getSnapshot,
137
+ getServerSnapshot,
138
+ selector,
139
+ isEqual
140
+ ]);
141
+ var value = useSyncExternalStore(subscribe, instRef[0], instRef[1]);
142
+ useEffect(function() {
143
+ inst.hasValue = !0;
144
+ inst.value = value;
145
+ }, [value]);
146
+ useDebugValue(value);
147
+ return value;
148
+ };
149
+ }));
150
+ //#endregion
151
+ //#region src/client/use-snapshot.ts
152
+ var import_with_selector = (/* @__PURE__ */ __commonJSMin(((exports, module) => {
153
+ module.exports = require_with_selector_production();
154
+ })))();
155
+ /**
156
+ * Bind a bare observable source to a typed uSES selector hook.
157
+ * subscribe/getSnapshot are captured once per source into stable closures
158
+ * (also re-binds `this` for method-based sources), so components never
159
+ * resubscribe across renders. Equality defaults to Object.is.
160
+ * @param w - snapshot source (engine store, Session object, store instance).
161
+ * @returns the selector hook.
162
+ */
163
+ function bindSnapshotSelector(w) {
164
+ const subscribe = (fn) => w.subscribe(fn);
165
+ const getSnapshot = () => w.getSnapshot();
166
+ return function useSelector(sel, eq) {
167
+ return (0, import_with_selector.useSyncExternalStoreWithSelector)(subscribe, getSnapshot, void 0, sel, eq);
168
+ };
169
+ }
170
+ //#endregion
12
171
  //#region src/selectors.ts
13
172
  /** Catchable error for illegal/unknown selectors (config-warning path). */
14
173
  var SelectorError = class extends Error {
@@ -318,7 +477,7 @@ window.__ModuleLoader__.load({
318
477
  return typeof value === "object" && value !== null && !Array.isArray(value);
319
478
  }
320
479
  /**
321
- * Read a nested value by path — the `@deepseek-ai/dsh-client-schema-form`
480
+ * Read a nested value by path — the upstream `dsh-client-schema-form`
322
481
  * `getPath` semantics, copied locally so the provider-configured join needs no
323
482
  * new dependency (array indexes as numeric keys, `undefined` along a missing
324
483
  * branch).
@@ -4187,7 +4346,7 @@ window.__ModuleLoader__.load({
4187
4346
  const connection = ctx.get("connection");
4188
4347
  const sessions = ctx.get("sessions");
4189
4348
  const controller = new FallbacksSettingsController(connection.api, connection.rpc);
4190
- const useSnapshot = (0, _deepseek_ai_dsh_client_web_react.bindSnapshotSelector)(controller.store);
4349
+ const useSnapshot = bindSnapshotSelector(controller.store);
4191
4350
  ctx.effect(() => {
4192
4351
  const syncSession = () => {
4193
4352
  controller.setCurrentSession(sessions?.list.getSnapshot().current);
@@ -0,0 +1,11 @@
1
+ import type { HostObservable, SnapshotSelectorHook } from '@deepseek-ai/dsh-client-ui-slots';
2
+ export type { SnapshotSelectorHook } from '@deepseek-ai/dsh-client-ui-slots';
3
+ /**
4
+ * Bind a bare observable source to a typed uSES selector hook.
5
+ * subscribe/getSnapshot are captured once per source into stable closures
6
+ * (also re-binds `this` for method-based sources), so components never
7
+ * resubscribe across renders. Equality defaults to Object.is.
8
+ * @param w - snapshot source (engine store, Session object, store instance).
9
+ * @returns the selector hook.
10
+ */
11
+ export declare function bindSnapshotSelector<T>(w: HostObservable<T>): SnapshotSelectorHook<T>;
package/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "dsh",
6
6
  "dsh-plugin"
7
7
  ],
8
- "version": "0.3.0",
8
+ "version": "0.3.1",
9
9
  "type": "module",
10
10
  "main": "./dist/index.js",
11
11
  "types": "./dist/index.d.ts",
@@ -56,28 +56,30 @@
56
56
  "engines": {
57
57
  "node": ">=22"
58
58
  },
59
+ "dependencies": {
60
+ "use-sync-external-store": "^1.6.0"
61
+ },
59
62
  "peerDependencies": {
60
63
  "@deepseek-ai/cordis": "^4.0.1",
61
- "@deepseek-ai/dsh-agent": "^0.1.0-rc.7",
62
- "@deepseek-ai/dsh-api-gateway": "^0.1.0-rc.7",
63
- "@deepseek-ai/dsh-api-remotes": "^0.1.0-rc.7",
64
- "@deepseek-ai/dsh-client-connection": "^0.1.0-rc.7",
65
- "@deepseek-ai/dsh-client-locale": "^0.1.0-rc.7",
66
- "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.7",
67
- "@deepseek-ai/dsh-client-ui-settings-plugins": "^0.1.0-rc.7",
68
- "@deepseek-ai/dsh-client-ui-settings": "^0.1.0-rc.7",
69
- "@deepseek-ai/dsh-client-ui-conversation": "^0.1.0-rc.7",
70
- "@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.7",
71
- "@deepseek-ai/dsh-client-web-react": "^0.1.0-rc.7",
72
- "@deepseek-ai/dsh-client-ui-primitives": "^0.1.0-rc.7",
73
- "@deepseek-ai/dsh-commands": "^0.1.0-rc.7",
74
- "@deepseek-ai/dsh-llm": "^0.1.0-rc.7",
75
- "@deepseek-ai/dsh-session": "^0.1.0-rc.7",
76
- "@deepseek-ai/dsh-settings": "^0.1.0-rc.7",
77
- "@deepseek-ai/dsh-typert-protocol": "^0.1.0-rc.7",
78
- "@deepseek-ai/dsh-typert-registry": "^0.1.0-rc.7",
79
- "react": "^18.2.0",
80
- "@deepseek-ai/schemastery": "^3.18.1"
64
+ "@deepseek-ai/dsh-agent": "^0.1.0-rc.8",
65
+ "@deepseek-ai/dsh-api-gateway": "^0.1.0-rc.8",
66
+ "@deepseek-ai/dsh-api-remotes": "^0.1.0-rc.8",
67
+ "@deepseek-ai/dsh-client-connection": "^0.1.0-rc.8",
68
+ "@deepseek-ai/dsh-client-locale": "^0.1.0-rc.8",
69
+ "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.8",
70
+ "@deepseek-ai/dsh-client-ui-conversation": "^0.1.0-rc.8",
71
+ "@deepseek-ai/dsh-client-ui-primitives": "^0.1.0-rc.8",
72
+ "@deepseek-ai/dsh-client-ui-settings": "^0.1.0-rc.8",
73
+ "@deepseek-ai/dsh-client-ui-settings-plugins": "^0.1.0-rc.8",
74
+ "@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.8",
75
+ "@deepseek-ai/dsh-commands": "^0.1.0-rc.8",
76
+ "@deepseek-ai/dsh-llm": "^0.1.0-rc.8",
77
+ "@deepseek-ai/dsh-session": "^0.1.0-rc.8",
78
+ "@deepseek-ai/dsh-settings": "^0.1.0-rc.8",
79
+ "@deepseek-ai/dsh-typert-protocol": "^0.1.0-rc.8",
80
+ "@deepseek-ai/dsh-typert-registry": "^0.1.0-rc.8",
81
+ "@deepseek-ai/schemastery": "^3.18.1",
82
+ "react": "^18.2.0"
81
83
  },
82
84
  "devDependencies": {
83
85
  "@testing-library/dom": "^10.4.1",
@@ -85,13 +87,14 @@
85
87
  "@types/node": "^22.20.0",
86
88
  "@types/react": "~18.3.31",
87
89
  "@types/react-dom": "~18.3.7",
90
+ "@types/use-sync-external-store": "^1.5.0",
88
91
  "jsdom": "29.1.1",
89
92
  "react": "^18.3.1",
90
93
  "react-dom": "^18.3.1",
91
- "vitest": "^4.1.8",
92
94
  "tsdown": "^0.22.2",
93
95
  "tsx": "^4.22.4",
94
- "typescript": "^5.9.3"
96
+ "typescript": "^5.9.3",
97
+ "vitest": "^4.1.8"
95
98
  },
96
99
  "repository": {
97
100
  "type": "git",
@@ -47,12 +47,15 @@ const OUT_FILE = 'index.js'
47
47
 
48
48
  /**
49
49
  * Frozen loader module table (dsh mechanism-guide): the plan-documented
50
- * composition = PLATFORM_MODULES (10 entries, `web/src/platform.ts:8-15`)
51
- * + the RUNTIME_STORE_EXEMPTION (`@deepseek-ai/dsh-client-runtime/client`,
52
- * `tsdown.client.ts:62-65`). `@deepseek-ai/cordis` is the in-box cordis
53
- * framework (dsh-advisor alignment): the client graph resolves it through
54
- * the generated shim, so it stays on the table as a real loader-table
55
- * specifier for the frozen-composition claim.
50
+ * composition = PLATFORM_MODULES (7 entries, `web/src/platform.ts:8-12`)
51
+ * + the PRELOADED_CLIENT_EXTERNALS (`@deepseek-ai/dsh-client-runtime/client`,
52
+ * `web/src/platform.ts:15-17`). rc.8 dropped `dsh-client-web-react` (deleted
53
+ * package — the uSES bridge is vendored in `src/client/use-snapshot.ts`),
54
+ * `dsh-client-ui-attachment` and `dsh-client-schema-form` from the platform
55
+ * table. `@deepseek-ai/cordis` is the in-box cordis framework (dsh-advisor
56
+ * alignment): the client graph resolves it through the generated shim, so it
57
+ * stays on the table as a real loader-table specifier for the frozen-
58
+ * composition claim.
56
59
  */
57
60
  export const CLIENT_EXTERNALS: readonly string[] = [
58
61
  'react',
@@ -61,10 +64,7 @@ export const CLIENT_EXTERNALS: readonly string[] = [
61
64
  'react-dom/client',
62
65
  '@deepseek-ai/cordis',
63
66
  '@deepseek-ai/dsh-client-ui-slots',
64
- '@deepseek-ai/dsh-client-web-react',
65
67
  '@deepseek-ai/dsh-client-ui-primitives',
66
- '@deepseek-ai/dsh-client-ui-attachment',
67
- '@deepseek-ai/dsh-client-schema-form',
68
68
  '@deepseek-ai/dsh-client-runtime/client',
69
69
  ]
70
70