dsh-llm-fallbacks 0.6.1 → 0.6.3
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 +19 -12
- package/README.zh-CN.md +19 -12
- package/dist/client/FallbacksCard.d.ts +71 -71
- package/dist/client/fallbacks-store.d.ts +6 -5
- package/dist/client/index.js +1447 -1561
- package/dist/client/locales.d.ts +0 -18
- package/dist/commands.d.ts +6 -1
- package/dist/config.d.ts +24 -7
- package/dist/gateway.d.ts +8 -2
- package/dist/index.d.ts +2 -1
- package/dist/index.js +14 -23
- package/dist/tui-settings.d.ts +5 -2
- package/dist/virtual-adapter.d.ts +15 -10
- package/icon.svg +16 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +5 -2
- package/scripts/build-client.ts +0 -1
package/README.md
CHANGED
|
@@ -42,7 +42,7 @@ dsh plugin --profile web add dsh-llm-fallbacks # web profile (Settings →
|
|
|
42
42
|
dsh plugin --profile dsh-tui add dsh-llm-fallbacks # dsh-tui terminal profile
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
-
Same plugin, either front end — the only difference is the `--profile` flag. Pin a version with `@<version>`. A registry install fetches the **built package** (`dist/`), nothing builds on the target machine. Registry / git / local-directory variants, uninstall, and `--dump-config` verification → [docs/install.md](docs/install.md).
|
|
45
|
+
Same plugin, either front end — the only difference is the `--profile` flag. Pin a version with `@<version>`. A registry install fetches the **built package** (`dist/`), nothing builds on the target machine. Registry / git / local-directory variants, uninstall, and `--dump-config` verification → [docs/install.md](docs/install.md). The plugin ships **localized metadata** (title/description for the Plugins-page detail view, English + 简体中文) and an **icon** — both declared in the package (`locale/*.json` + `icon.svg`) and picked up by the host automatically.
|
|
46
46
|
|
|
47
47
|
### Configuration surfaces
|
|
48
48
|
|
|
@@ -62,7 +62,6 @@ Add a `fallbacks:` section to the shared settings document (`$DSH_HOME/settings.
|
|
|
62
62
|
|
|
63
63
|
```yaml
|
|
64
64
|
fallbacks:
|
|
65
|
-
enabled: true # feature switch — defaults to off (plugin is a no-op otherwise)
|
|
66
65
|
rootChain: # all-day chain: leading entries = fallback walk, last = Default model (official)
|
|
67
66
|
- anthropic/claude-3-5-sonnet # walked first
|
|
68
67
|
- deepseek-official/deepseek-flash # last resort (Flash or Pro)
|
|
@@ -91,7 +90,7 @@ fallbacks:
|
|
|
91
90
|
|
|
92
91
|
Build the section up in four steps:
|
|
93
92
|
|
|
94
|
-
**1.
|
|
93
|
+
**1. Configure content.** There is **no feature switch** in the section — the plugin intervenes exactly when something is configured: a non-empty `rootChain`, at least one `timeSlots` row, or one declared role. An empty section is a complete no-op, and the Plugins-page row toggle is the master switch (disabling the plugin row stops it entirely).
|
|
95
94
|
|
|
96
95
|
**2. Set the all-day `rootChain`.** Leading entries are the fallback chain, walked first when a request fails; the **last** entry is the Default model.
|
|
97
96
|
|
|
@@ -103,7 +102,7 @@ Build the section up in four steps:
|
|
|
103
102
|
|
|
104
103
|
Full reference (role entities, fallback strategies, rules, selectors, preset roles, time-slot presets) → [docs/configuration.md](docs/configuration.md).
|
|
105
104
|
|
|
106
|
-
> **Upgrade note (behavior change)**:
|
|
105
|
+
> **Upgrade note (breaking, behavior change)**: the config-level `enabled` switch is **removed** — a stored section still carrying `enabled: true/false` loads cleanly (the key is ignored and stripped on the next save), but the switch no longer gates anything. If you relied on `enabled: false` to keep the plugin inert, empty the section (or disable the plugin row on the Plugins page) instead.
|
|
107
106
|
|
|
108
107
|
### Verify
|
|
109
108
|
|
|
@@ -185,7 +184,7 @@ The durable fix belongs upstream at the migration edges (the frozen V0→V1 edge
|
|
|
185
184
|
|
|
186
185
|
- **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.
|
|
187
186
|
- **Two-block config**: `rootChain` for the root agent; declared role entities (`roles.list`) referenced by `roles.rules` (or the built-in `inherit`).
|
|
188
|
-
- **Chain as root primary from the picker**:
|
|
187
|
+
- **Chain as root primary from the picker**: whenever the plugin has content configured, 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 delegation to succeed); selecting a real model keeps fallback-only (see [FallbacksChain in the model picker](#fallbackschain-in-the-model-picker)).
|
|
189
188
|
- **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)).
|
|
190
189
|
- **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 (matching either the recorded pair or the served head — see [FallbacksChain in the model picker](#fallbackschain-in-the-model-picker)) → 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.
|
|
191
190
|
- **Role persona on the child (chain-independent)**: when a subagent's dispatch declares a role this plugin knows (the `**Execute as**: <id>` field of its Assignment), that role's `persona` is installed on the child as its own persona — the role is the child's identity, not only a routing decision, and delivery does not depend on routing: a role with an empty `chain` gets its persona exactly like a chained one (the chain only picks the model). A persona the caller already set is never overwritten; a provider that cannot carry a persona skips it and the dispatch runs natively.
|
|
@@ -197,7 +196,7 @@ The durable fix belongs upstream at the migration edges (the frozen V0→V1 edge
|
|
|
197
196
|
- **Half-open recovery (opt-in)**: `recovery: half-open` makes recovery evidence-driven — an expired cooldown leaves the route half-open for one logged probe instead of restoring the preference; consecutive failures escalate the suppression duration (×2 per failure, capped at 1 h); an observed completion closes the circuit and fully restores the preference. `revertPolicy: 'never'` keeps the mechanism inert; state is session-scoped in-memory (a restart resets). YAML-only — the default `timer` keeps every existing behavior byte-identical (see [docs/configuration.md](docs/configuration.md#recovery-mode-recovery-key)).
|
|
198
197
|
- **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 **cannot** be repaired by an `ignorable` flag — the released session-format migration chain (v0→v1) refuses unknown event types even when marked ignorable — so `pnpm repair:session-logs` reports them (and recovers them only with the opt-in lossy `--drop-legacy-events` — see [Repair existing sessions](#repair-existing-sessions)).
|
|
199
198
|
- **Safety valves**: `maxSwitchesPerStep` caps switches per step and `alwaysModeRetryCap` caps always-mode retries — chain loops cannot amplify latency.
|
|
200
|
-
- **No-config no-op**: with
|
|
199
|
+
- **No-config no-op**: with nothing configured the plugin behaves exactly like not being installed — content presence is the gate (see [Minimal configuration](#minimal-configuration)).
|
|
201
200
|
|
|
202
201
|
## dsh-tui profile (terminal)
|
|
203
202
|
|
|
@@ -205,7 +204,7 @@ In a dsh-tui profile the plugin has three operator surfaces, with a strict duty
|
|
|
205
204
|
|
|
206
205
|
- **`/fallbacks`** — what happened this session: origin, resolved role, effective chain, recent fallback switches, cooldown status (half-open marker rows when `recovery: half-open` is active). Read-only.
|
|
207
206
|
- **`/fallbacks config`** — what is configured: composed-config readback (trigger codes, root chain, time slots, timezone, roles, role rules, cooldown, revert policy, safety valves, presets, role auto-match). Read-only apart from the one action command **`/fallbacks config revert-seed <role-id>`**, which restores a seeded role's persona to its declared seed default — the web settings card presents seeded personas read-only (no revert affordance), so the command is that action's only surface.
|
|
208
|
-
- **`/settings`** — the edit surface. The plugin registers a **fallbacks** section with full parity to the web settings card: booleans (`
|
|
207
|
+
- **`/settings`** — the edit surface. The plugin registers a **fallbacks** section with full parity to the web settings card: booleans (`roleAutoMatch`) render as toggles, selects (`presets`, `revertPolicy`) as pickers, and numbers (`cooldownMs`, `maxSwitchesPerStep`, `alwaysModeRetryCap`) as numeric inputs; complex structures (`rootChain`, `timeSlots`, `roles.list`, `roles.rules`) are JSON text fields and `triggerCodes` a comma-separated text field. Invalid drafts (bad JSON, non-conforming chains, malformed time-slot rows) block the save — the section never corrupts the config.
|
|
209
208
|
|
|
210
209
|
**Requirements**: the `/settings` fallbacks section needs **dsh-tui ≥ v0.8.5** (commit `c51661f` or later on `main`; the settings seam shipped in v0.8.0, the groups shape + validation in v0.8.5). On an older dsh-tui the section is absent, and file editing remains the only TUI edit surface.
|
|
211
210
|
|
|
@@ -213,7 +212,7 @@ File editing still works everywhere: the shared `$DSH_HOME/settings.yaml` (`fall
|
|
|
213
212
|
|
|
214
213
|
## FallbacksChain in the model picker
|
|
215
214
|
|
|
216
|
-
|
|
215
|
+
Whenever the plugin has content configured (a non-empty `rootChain`, at least one `timeSlots` row, or one declared role), it registers a virtual provider, **FallbacksChain**, with a single catalog row: **Auto**. The web profile and dsh-tui both see the row: they share the same adapter catalog, so the row needs no settings-page wiring or host patch (it is independent of the `/settings` fallbacks section, which edits configuration rather than the picker catalog). A legacy or empty all-day chain does NOT hide the row as long as other content is configured (the delegate just refuses to serve it); with nothing configured there is no row.
|
|
217
216
|
|
|
218
217
|
Selecting **FallbacksChain / Auto** uses the configured chain as the root **primary**: the request stays on the virtual pair and the adapter's thin delegate dispatches the effective chain's first exact `provider/model` at request time, so the fallback engine degrades from that head as usual. Serving the selection unchanged (instead of rewriting the route to the head) is also what keeps the host's model-change notice quiet: the recorded route equals the session selection, so the notice appears once on a genuine selection change rather than being re-armed on every step. Selecting any real catalog model keeps the v0.2.2 fallback-only behavior — the session model is primary and the chain engages only after it fails.
|
|
219
218
|
|
|
@@ -223,8 +222,8 @@ Notes:
|
|
|
223
222
|
|
|
224
223
|
- **Picker label**: the row's catalog `name` (what the composer trigger shows) is live — `Auto: DeepSeek Flash[Liang Peak]` / `Auto: DeepSeek 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.
|
|
225
224
|
- **Thin delegate on every origin**: the row is served by the same `stream()` delegate for the root agent and for a subagent session that inherits the selection — subagent role resolution and injection keep their semantics apart from one deliberate widening: dispatch-time rule matching accepts **either** the recorded pair or the served head (a superset of the previous matching, so a rule keyed on the real head keeps matching), and the virtual row is never a second routing engine. A subagent that inherits the selection still routes through the chain head.
|
|
226
|
-
- **Conformance gate on the tail**: a successful delegation requires the all-day chain to be **tail-conforming** — its last entry must be exactly one official model (`deepseek-official/deepseek-flash` or `deepseek-official/deepseek-v4-pro`, the card's Default model panel); leading entries (Default fallback chain) are walked first.
|
|
227
|
-
- **Stale selection**: if the row disappears (plugin
|
|
225
|
+
- **Conformance gate on the tail**: a successful delegation requires the all-day chain to be **tail-conforming** — its last entry must be exactly one official model (`deepseek-official/deepseek-flash` or `deepseek-official/deepseek-v4-pro`, the card's Default model panel); leading entries (Default fallback chain) are walked first. Deactivating the plugin (the Plugins-page row switch, or clearing all configured content) hides the row again (slot-row/chain edits never churn registration).
|
|
226
|
+
- **Stale selection**: if the row disappears (plugin deactivated) 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).
|
|
228
227
|
- **Capabilities and retry policy follow the head**: the row's model metadata (context window, modalities, reasoning) mirrors the current effective head, and `providerRetryPolicy` returns **that head's** policy — so a user's `llm-deepseek.retryPolicy` applies on this route too, not the permissive default. The host captures the policy once at registration, so a later policy edit or a slot-driven head-provider rotation shows up only after the plugin re-registers. Retry events are keyed by the virtual provider — the route the runtime actually sees. Full semantics → [docs/configuration.md](docs/configuration.md).
|
|
229
228
|
|
|
230
229
|
## Time-slot presets
|
|
@@ -235,7 +234,7 @@ Time slots are introduced in the [featured overview](#time-slots) above; this se
|
|
|
235
234
|
- **Presets** (frozen, not user-editable): `liang-peak` = Monday–Friday 09:00–12:00 **and** 14:00–18:00; `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.
|
|
236
235
|
- **Custom rows**: `start` / `end` (`HH:mm`, may wrap midnight) + optional `days` (0=Sunday…6=Saturday; omitted/empty = every day) + models.
|
|
237
236
|
- **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.
|
|
238
|
-
- **
|
|
237
|
+
- **Flat settings card** (the official Plugins-page form language): the card is always open — no collapsible chrome and no feature switch — and one footer **Save/Discard** pair writes the whole validated draft. 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 Flash | Pro last-resort fallback). Rows are collapsible to name + first model (content disclosure). There is no `timeSlots.enabled` master switch (adding a row is the opt-in) and no `rootMode` control.
|
|
239
238
|
|
|
240
239
|
## Preset roles
|
|
241
240
|
|
|
@@ -244,6 +243,15 @@ The plugin ships **5 bundled generic subagent roles** out of the box — `review
|
|
|
244
243
|
- **Switch**: `fallbacks.presets` — `'bundled'` (default) declares the preset roles on apply; `'none'` disables the automatic declaration (already-materialized rows stay).
|
|
245
244
|
- Full semantics (upgrade behavior, conflict handling, library reuse of `presetRoles`) → [docs/configuration.md](docs/configuration.md).
|
|
246
245
|
|
|
246
|
+
## Agent presets (plugin composition)
|
|
247
|
+
|
|
248
|
+
Agent **presets** are a dsh feature for composing plugins — they are declarative child-plugin lists a user can apply as a set, and they carry **no** model fields of their own. The fallbacks plugin needs no preset-specific configuration: because a preset composes plugins, the fallback applies **wherever the plugin's row is mounted** — a preset that includes the `dsh-llm-fallbacks` row gives that preset's sessions the same chains and roles configured for the plugin.
|
|
249
|
+
|
|
250
|
+
Two composition caveats:
|
|
251
|
+
|
|
252
|
+
- **A user preset override replaces the entire child list** (no merge): a preset that omits the fallbacks row silently unmounts the plugin for sessions using that preset. Restate every plugin row you want to keep.
|
|
253
|
+
- **Preset-scoped chain configs are possible by mounting the row inside a preset**: the plugin row's `config:` overrides in a preset composition produce a chain configuration that applies to that preset's sessions only (patch rows replace the whole `config`, so restate every field).
|
|
254
|
+
|
|
247
255
|
## Fallback triggers (`triggerCodes`)
|
|
248
256
|
|
|
249
257
|
`fallbacks.triggerCodes` is a plain list of dsh failure codes, and **any** code the harness can report is accepted — not only the three defaults (`AUTH` / `QUOTA` / `RATE_LIMIT`). A failure whose code is not listed passes straight through to llm-retry or to the original error, exactly as if the plugin were not installed. Retryable failures (5xx / `TRANSPORT` / `TIMEOUT` / `EMPTY_RESPONSE`) need no extra entries: llm-retry backs them off first and the failure enters the chain decision the same way once that budget is exhausted.
|
|
@@ -254,7 +262,6 @@ A request larger than the model's context window is reported as `CONTEXT_WINDOW_
|
|
|
254
262
|
|
|
255
263
|
```yaml
|
|
256
264
|
fallbacks:
|
|
257
|
-
enabled: true
|
|
258
265
|
triggerCodes:
|
|
259
266
|
- AUTH
|
|
260
267
|
- QUOTA
|
package/README.zh-CN.md
CHANGED
|
@@ -42,7 +42,7 @@ dsh plugin --profile web add dsh-llm-fallbacks # web profile(设置 → F
|
|
|
42
42
|
dsh plugin --profile dsh-tui add dsh-llm-fallbacks # dsh-tui 终端 profile
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
-
同一个插件、两个前端——区别只在 `--profile` 参数。钉版本:加 `@<version>`。registry 安装拉取的是**已构建产物**(`dist/`),目标机无需构建。registry / git / 本地目录变体、卸载与 `--dump-config` 验证 → [docs/install.md](docs/install.md)
|
|
45
|
+
同一个插件、两个前端——区别只在 `--profile` 参数。钉版本:加 `@<version>`。registry 安装拉取的是**已构建产物**(`dist/`),目标机无需构建。registry / git / 本地目录变体、卸载与 `--dump-config` 验证 → [docs/install.md](docs/install.md)。插件自带**本地化元数据**(插件页详情视图的标题/描述,英文 + 简体中文)与**图标**——均以包内声明(`locale/*.json` + `icon.svg`)交由宿主自动读取。
|
|
46
46
|
|
|
47
47
|
### 配置界面
|
|
48
48
|
|
|
@@ -62,7 +62,6 @@ dsh plugin --profile dsh-tui add dsh-llm-fallbacks # dsh-tui 终端 profile
|
|
|
62
62
|
|
|
63
63
|
```yaml
|
|
64
64
|
fallbacks:
|
|
65
|
-
enabled: true # 功能开关——默认关闭(否则插件完全 no-op)
|
|
66
65
|
rootChain: # 全时段链:前面的条目 = 降级路径,最后一项 = 默认模型(官方模型)
|
|
67
66
|
- anthropic/claude-3-5-sonnet # 先走
|
|
68
67
|
- deepseek-official/deepseek-flash # 最后一档(Flash 或 Pro)
|
|
@@ -91,7 +90,7 @@ fallbacks:
|
|
|
91
90
|
|
|
92
91
|
按四个步骤逐步构建:
|
|
93
92
|
|
|
94
|
-
**1.
|
|
93
|
+
**1. 配置内容。** 本分节**没有功能开关**——只要配置了内容(非空 `rootChain`、至少一条 `timeSlots` 行、或一个已声明角色),插件即介入;空分节完全 no-op。插件页的行开关是总开关(停用插件行即完全停止)。
|
|
95
94
|
|
|
96
95
|
**2. 配置全时段 `rootChain`。** 前面的条目是降级链,请求失败时先走;**最后**一项是默认模型。
|
|
97
96
|
|
|
@@ -103,7 +102,7 @@ fallbacks:
|
|
|
103
102
|
|
|
104
103
|
完整参考(角色实体、fallback 策略、规则、selector、预设角色、分时槽预设)→ [docs/configuration.md](docs/configuration.md)。
|
|
105
104
|
|
|
106
|
-
>
|
|
105
|
+
> **升级提示(破坏性变更)**:配置级 `enabled` 开关已**移除**——存量配置仍携带 `enabled: true/false` 时可正常加载(该键被忽略,下次保存时剥离),但不再起任何门控作用。如果你靠 `enabled: false` 保持插件惰性,请改为清空分节(或在插件页停用该行)。
|
|
107
106
|
|
|
108
107
|
### 验证
|
|
109
108
|
|
|
@@ -185,7 +184,7 @@ pnpm repair:session-logs -- --drop-legacy-events --apply --backup # 此处必
|
|
|
185
184
|
|
|
186
185
|
- **root / subagent 自动降级**:任意 agent 在模型故障下按链切换到下一个可用 provider/model,无需手动换模型。
|
|
187
186
|
- **两块制配置**:`rootChain` 管 root 代理;声明式角色实体(`roles.list`)供 `roles.rules` 引用(或内置 `inherit`)。
|
|
188
|
-
-
|
|
187
|
+
- **选择器里把链当主模型**:只要插件配置了内容,宿主模型选择器(web 与 TUI 一致)即出现虚拟 `FallbacksChain` / `Auto` 行——选中它即以配置的链作为 root 主模型(需要 all-day 链头合规才能成功委托);选真实模型则保持 fallback-only(见 [模型选择器中的 FallbacksChain](#模型选择器中的-fallbackschain))。
|
|
189
188
|
- **峰谷无忧(分时切换)**:可选的 `fallbacks.timeSlots` 行按墙钟窗口(配置级 `tz` 时区,默认 `Asia/Shanghai`)轮换 root 生效链——四个冻结的 UTC+8 预设(`liang-peak` / `liang-valley` / `glm-peak` / `glm-valley`,窗口为代码常量、仅模型链可编辑),或自定义 `start`/`end`/`days` 窗口。第一条命中的行生效;全时段行固定最后。时段切换在**下一个** root 请求生效,日志记为**分时切换**——路由种子而非失败决策:不消耗冷却、不计入 `maxSwitchesPerStep`。失败降级保留**降级切换**文案(见 [分时槽预设(分时切换)](#分时槽预设分时切换))。
|
|
190
189
|
- **派发时角色解析**:在 subagent 的首次请求上,其角色按三个阶段解析——显式(`agentPreset` 匹配已声明角色 id)→ 确定性规则(匹配记录 pair 或实际服务的链头——见 [模型选择器中的 FallbacksChain](#模型选择器中的-fallbackschain))→ LLM 自动匹配(从已声明角色体系中选择,`fallbacks.roleAutoMatch` 默认 `true`)。解析出的角色的链头模型注入首次请求,并以显式 `role → model` 日志行记录(不写 durable `fallbacks/switch` 事件——issue #52 停写);设 `roleAutoMatch: false` 仅关闭 LLM 自动匹配阶段(显式 `agentPreset` 阶段仍生效——无显式角色时即复现原有仅规则行为)。设置卡总是渲染「启用角色自动匹配」开关(默认 `true`)以切换之——即使是从未声明过该键的旧配置,schema 默认值同样生效。
|
|
191
190
|
- **角色人格注入到子代理(与链无关)**:当 subagent 的派发声明了本插件已知的角色(Assignment 的 `**Execute as**: <id>` 字段)时,该角色的 `persona` 会安装为子代理自身的人格——角色就是子代理的身份,而不只是路由决策;注入与路由无关:`chain` 为空的角色与带链角色的人格注入完全一致(链只负责选模型)。调用方已设置的人格永不覆盖;宿主 provider 无法承载人格时跳过注入,派发照常原生运行。
|
|
@@ -197,7 +196,7 @@ pnpm repair:session-logs -- --drop-legacy-events --apply --backup # 此处必
|
|
|
197
196
|
- **半开恢复(可选)**:`recovery: half-open` 让恢复以证据驱动——冷却到期后路由进入 **half-open**,以一次记录探针(logged probe)放行,而不是直接恢复首选;连续失败使抑制时长按 **×2** 逐次升级、**1 小时**封顶;观察到完成即闭合回路、完全恢复首选。`revertPolicy: 'never'` 使该机制完全失效;状态为会话级内存态(重启即重置)。仅 YAML 配置——默认 `timer` 保持所有既有行为逐字节一致(见 [docs/configuration.md](docs/configuration.md#recovery-mode-recovery-key))。
|
|
198
197
|
- **行为可见**:每次切换以 info 级日志行(from/to/role/reason)记录——无静默换模型。插件**刻意不写** durable `fallbacks/switch` 会话事件(issue #52——apply() 时的事件类型注册被证伪无效,含该事件的会话在 dsh 重启后拒绝加载)。由旧版插件写入、含此类事件的会话**无法**通过 `ignorable` 标记修复——已发布的 session-format 迁移链(v0→v1)即使事件带 `ignorable` 也拒绝未知事件类型——因此 `pnpm repair:session-logs` 会报告此类日志(只有显式 opt-in 的有损 `--drop-legacy-events` 才会恢复它们——见 [修复已有会话](#修复已有会话))。
|
|
199
198
|
- **安全阀**:`maxSwitchesPerStep` 限制每 step 切换次数、`alwaysModeRetryCap` 限制 always 模式重试——链循环不会放大延迟。
|
|
200
|
-
- **无配置回归(no-op
|
|
199
|
+
- **无配置回归(no-op)**:未配置任何内容时行为与未安装插件完全一致——内容在否即门控(见 [最小配置](#最小配置))。
|
|
201
200
|
|
|
202
201
|
## dsh-tui profile(终端)
|
|
203
202
|
|
|
@@ -205,7 +204,7 @@ pnpm repair:session-logs -- --drop-legacy-events --apply --backup # 此处必
|
|
|
205
204
|
|
|
206
205
|
- **`/fallbacks`** —— 本次会话发生了什么:来源、解析角色、生效链、最近降级切换、冷却状态(`recovery: half-open` 生效时显示 half-open 标记行)。只读。
|
|
207
206
|
- **`/fallbacks config`** —— 配置了什么:组合配置回读(触发码、根链、分时槽、时区、角色、角色规则、冷却、回主策略、安全阀、预置、角色自动匹配)。除唯一的动作命令 **`/fallbacks config revert-seed <role-id>`** 外只读——该命令把某个 seed 角色的 persona 还原为已声明的默认(Web 设置卡将 seed 角色的 persona 呈现为只读、不提供还原入口,此命令是该动作的唯一入口)。
|
|
208
|
-
- **`/settings`** —— 编辑界面。插件注册 **fallbacks** 区块,与 **Web 设置卡完全一致**:布尔(`
|
|
207
|
+
- **`/settings`** —— 编辑界面。插件注册 **fallbacks** 区块,与 **Web 设置卡完全一致**:布尔(`roleAutoMatch`)渲染为开关、下拉(`presets`、`revertPolicy`)为选择器、数值(`cooldownMs`、`maxSwitchesPerStep`、`alwaysModeRetryCap`)为数字输入;复杂结构(`rootChain`、`timeSlots`、`roles.list`、`roles.rules`)为 JSON 文本字段,`triggerCodes` 为逗号分隔文本字段。非法草稿(JSON 解析失败、链尾不合规、分时行畸形)会阻止保存——区块绝不写入损坏配置。
|
|
209
208
|
|
|
210
209
|
**版本要求**:`/settings` 的 fallbacks 区块需要 **dsh-tui ≥ v0.8.5**(`main` 上 commit `c51661f` 及以后;settings seam 于 v0.8.0 引入,groups 结构与校验于 v0.8.5 引入)。更旧的 dsh-tui 没有该区块,文件编辑仍是 TUI 唯一编辑面。
|
|
211
210
|
|
|
@@ -213,7 +212,7 @@ pnpm repair:session-logs -- --drop-legacy-events --apply --backup # 此处必
|
|
|
213
212
|
|
|
214
213
|
## 模型选择器中的 FallbacksChain
|
|
215
214
|
|
|
216
|
-
|
|
215
|
+
只要插件配置了内容(非空 `rootChain`、至少一条 `timeSlots` 行、或一个已声明角色),它就注册一个虚拟 provider **FallbacksChain**,目录中只有一行:**Auto**。web profile 与 dsh-tui 都能看到这一行:两者共享同一个 adapter catalog,无需设置页接线或宿主补丁(它与 `/settings` 的 fallbacks 区块相互独立——区块编辑的是配置,不是选择器目录)。只要其它内容仍在,遗留多模型或空的 all-day 链**不会**隐藏它(只是委托会拒绝服务);无任何内容时则没有这一行。
|
|
217
216
|
|
|
218
217
|
选择 **FallbacksChain / Auto** = 把配置的链作为 root **主模型**:请求保留在虚拟对上,由 adapter 的薄委托在请求时刻派发到生效链的第一个精确 `provider/model`,失败后由降级引擎从该链头照常沿链切换。之所以让选择原样下发(而不是把路由改写成链头),也正是为了不让宿主的模型变更提示被反复触发:落盘的路由与会话选择一致,因此该提示只在**选择真的发生变化**时出现一次,而不再每一步重新注入。选择任何真实目录模型则保持 v0.2.2 的 fallback-only 行为——会话模型为主,链只在它失败后介入。
|
|
219
218
|
|
|
@@ -223,8 +222,8 @@ pnpm repair:session-logs -- --drop-legacy-events --apply --backup # 此处必
|
|
|
223
222
|
|
|
224
223
|
- **选择器文案**:目录行的 `name`(composer 触发器显示)是动态的——`Auto: DeepSeek Flash[Liang Peak]` / `Auto: DeepSeek Flash[all-day]`(用 catalog 显示名,不是 model id);id 仍是 `Auto`。all-day 尾巴不合规则只显示 `Auto`。重新打开选择器即可刷新。
|
|
225
224
|
- **全来源同一个薄委托**:root 代理与继承了该选择的 subagent 会话由同一个 `stream()` 薄委托服务——subagent 的角色解析与注入语义不变,只有一处刻意的放宽:派发时规则匹配接受**记录 pair 或实际服务的链头**(此前匹配的超集,因此以真实链头为键的规则仍能命中),虚拟行绝不是第二个路由引擎。继承了该选择的 subagent 仍经链头路由。
|
|
226
|
-
- **链尾合规门槛**:委托成功要求 all-day 链**尾巴合规**——最后一项必须是恰好一个官方模型(`deepseek-official/deepseek-flash` 或 `deepseek-official/deepseek-v4-pro
|
|
227
|
-
-
|
|
225
|
+
- **链尾合规门槛**:委托成功要求 all-day 链**尾巴合规**——最后一项必须是恰好一个官方模型(`deepseek-official/deepseek-flash` 或 `deepseek-official/deepseek-v4-pro`,即设置卡的「默认模型」面板);前面的默认降级链先走。停用插件(插件页行开关,或清空全部已配置内容)后该行隐藏(slot/链编辑不会触发注册抖动)。
|
|
226
|
+
- **过期选择**:行消失(插件停用)而会话仍选中 `FallbacksChain / Auto` 时,会话继续把它显示为当前模型,但 `routable: false`——从目录选一个真实模型即可继续(宿主原生目录语义)。
|
|
228
227
|
- **能力与重试策略跟随链头**:该行的模型元数据(上下文窗口、模态、推理)镜像当前生效链头,`providerRetryPolicy` 返回**该链头的**策略——因此用户配置的 `llm-deepseek.retryPolicy` 在这条路由上同样生效,不再退回宽松默认。宿主在注册时一次性捕获该策略,因此之后修改策略、或分时槽导致链头 provider 轮换,都只有在插件重新注册后才会反映。重试事件按虚拟 provider 记账——即运行时实际看到的路由。完整语义 → [docs/configuration.md](docs/configuration.md)。
|
|
229
228
|
|
|
230
229
|
## 分时槽预设(分时切换)
|
|
@@ -235,7 +234,7 @@ pnpm repair:session-logs -- --drop-legacy-events --apply --backup # 此处必
|
|
|
235
234
|
- **预设**(冻结,不可编辑窗口):`liang-peak` = 周一至周五 09:00–12:00 **与** 14:00–18:00;`liang-valley` = 其它所有 UTC+8 时间;`glm-peak` = 周一至周五 14:00–18:00;`glm-valley` = 其余时间。一个预设 id 对应一行;设置卡的选择器不会重复提供已添加的预设。
|
|
236
235
|
- **自定义行**:`start` / `end`(`HH:mm`,可跨午夜)+ 可选 `days`(0=周日…6=周六;缺省/空 = 每天)+ 模型。
|
|
237
236
|
- **下一请求生效**:时段边界跨越绝不打断进行中的 step——新行在下一个 root 请求生效。轮换仅挂载生效:info 日志 + 设置卡/`/fallbacks` 状态行,无 durable 切换事件。
|
|
238
|
-
-
|
|
237
|
+
- **平铺设置卡**(官方插件页表单语言):卡片始终展开——无可折叠外壳、无功能开关——由唯一的底部 **保存/放弃** 按钮对写入整份通过校验的草稿。主代理区块下分三块——**分时槽设置**(额外行:添加预设 / 添加自定义 / 删除 / 按钮或**拖拽**排序;预设行只读展示窗口摘要、仅可编辑模型链;自定义行带可编辑名称;**时区选择器**在此区块内,只要存在预设行就**锁定 Asia/Shanghai**——预设窗口是冻结的 UTC+8 常量)、**默认降级链**(all-day 链,可配置的 provider/model 选择器列表)与**默认模型**(官方 Flash | Pro 二选一链头)。行可折叠为「名称 + 首个模型」(内容级折叠)。没有 `timeSlots.enabled` 总开关(添加行即开启),也没有 `rootMode` 控件。
|
|
239
238
|
|
|
240
239
|
## 预设角色(Preset roles)
|
|
241
240
|
|
|
@@ -244,6 +243,15 @@ pnpm repair:session-logs -- --drop-legacy-events --apply --backup # 此处必
|
|
|
244
243
|
- **开关**:`fallbacks.presets`——`'bundled'`(默认)在 apply 时声明预设角色;`'none'` 关闭自动声明(已物化行保留)。
|
|
245
244
|
- 完整语义(升级行为、冲突处理、`presetRoles` 库复用)→ [docs/configuration.md](docs/configuration.md)。
|
|
246
245
|
|
|
246
|
+
## Agent 预设(插件组合)
|
|
247
|
+
|
|
248
|
+
Agent **preset** 是 dsh 的插件组合机制——一份声明式的子插件行清单,用户可整组启用;preset 本身**不携带任何模型字段**。本插件无需 preset 专属配置:preset 组合的是插件,因此 fallback 生效于**插件行被挂载的每一处**——包含 `dsh-llm-fallbacks` 行的 preset,其会话即享有同一套链与角色配置。
|
|
249
|
+
|
|
250
|
+
两条组合注意事项:
|
|
251
|
+
|
|
252
|
+
- **用户 preset 覆盖会整体替换子插件清单**(无合并):遗漏了 fallbacks 行的 preset 会在使用该 preset 的会话中静默卸载本插件。想保留的插件行都要重新写全。
|
|
253
|
+
- **preset 级链配置可通过把插件行挂进 preset 实现**:preset 组合中插件行的 `config:` 覆盖会形成仅作用于该 preset 会话的链配置(patch 行整体替换 `config`,字段要写全)。
|
|
254
|
+
|
|
247
255
|
## 降级触发码(`triggerCodes`)
|
|
248
256
|
|
|
249
257
|
`fallbacks.triggerCodes` 是一份 dsh 失败码列表,**任何**宿主可上报的失败码都被接受——不限于三个默认值(`AUTH` / `QUOTA` / `RATE_LIMIT`)。未列出的失败码原样透传给 llm-retry 或原始错误,与未安装插件完全一致。可重试型失败(5xx / `TRANSPORT` / `TIMEOUT` / `EMPTY_RESPONSE`)无需额外条目:llm-retry 先行退避,预算耗尽后同样进入链决策。
|
|
@@ -254,7 +262,6 @@ pnpm repair:session-logs -- --drop-legacy-events --apply --backup # 此处必
|
|
|
254
262
|
|
|
255
263
|
```yaml
|
|
256
264
|
fallbacks:
|
|
257
|
-
enabled: true
|
|
258
265
|
triggerCodes:
|
|
259
266
|
- AUTH
|
|
260
267
|
- QUOTA
|
|
@@ -6,77 +6,78 @@
|
|
|
6
6
|
* registration order); owner props are empty and all data flows
|
|
7
7
|
* through {@link FallbacksSettingsController}.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
* the
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* (`
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* Save/discard disabled terms: save = `!sectionDirty || saving ||
|
|
27
|
-
* !writable`, discard = `!sectionDirty || saving` (KD-U1). Disclosure is
|
|
28
|
-
* card-local state:
|
|
29
|
-
* which card a user has open is a reading gesture, and staged edits outlive
|
|
30
|
-
* collapsing — the pill rides the header (upstream rationale).
|
|
9
|
+
* FLAT rebuild (plan fallbacks-web-ux-alignment T3, the advisor PR #98
|
|
10
|
+
* pattern): the collapsible card chrome is gone — no header button, no
|
|
11
|
+
* rotating chevron, no dirty "unsaved" pill, no card-local open state, no
|
|
12
|
+
* bordered `<li>` box. The page already renders the plugin title/description
|
|
13
|
+
* above the card (the `locale/*.json` meta files the host resolves per UI
|
|
14
|
+
* language), so the notices, the form, and the section actions directly
|
|
15
|
+
* beneath it in the official flat settings-form language (reference §3):
|
|
16
|
+
* fields at
|
|
17
|
+
* 12px vertical padding with 0.5px hairline separators, 34px inputs on the
|
|
18
|
+
* 0.5px l4 border, per-section Discard + Save pairs on the section
|
|
19
|
+
* headings. The card is ALWAYS open —
|
|
20
|
+
* nothing needs card-level disclosure state; the degraded /
|
|
21
|
+
* error / read-only / migration notices render flat atop the form (the
|
|
22
|
+
* unavailable notice stays always-visible per KD-G5 — the documented
|
|
23
|
+
* divergence from the advisor, whose unavailable card renders nothing: the
|
|
24
|
+
* fallbacks skeleton stays the USABLE last-known form and saves are
|
|
25
|
+
* attempted).
|
|
31
26
|
*
|
|
32
|
-
* The
|
|
33
|
-
* checkbox row
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* declared role entity cards from `roles.list` plus the rule rows from
|
|
37
|
-
* `roles.rules`, whose role field is a dropdown bound to the declared ids
|
|
38
|
-
* + the built-in `inherit`, same-page live). Saving runs `validateDraft`
|
|
39
|
-
* first — id format/reserved word/duplicates, undeclared rule role
|
|
40
|
-
* references, illegal selectors, and a role with no chain entries (no
|
|
41
|
-
* model config) block the write with a validation banner + inline red
|
|
42
|
-
* borders / hints (never touching the store error path); a
|
|
43
|
-
* non-empty `state.legacyKeys` renders the migration banner at the top of
|
|
44
|
-
* the card body. The row editors keep their filled editorCard surface
|
|
45
|
-
* inside the card, with `--dsw-alias-*` tokens throughout. The reset-
|
|
46
|
-
* to-defaults affordance is GONE from the card (PR #62 UX round 3) — the
|
|
47
|
-
* gateway RPC `fallbacks/reset` and the store `resetToDefaults()` stay as
|
|
48
|
-
* host APIs (store/gateway tests unchanged), only the card UI was removed.
|
|
27
|
+
* The config-level `enabled` switch is REMOVED (T2, breaking): there is no
|
|
28
|
+
* checkbox row and no `enabled.off` hiding — all fields render
|
|
29
|
+
* unconditionally; the Plugins-page row toggle is the master switch and the
|
|
30
|
+
* runtime gates read content presence (`isFallbackActive`).
|
|
49
31
|
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
32
|
+
* PER-SECTION Save/Discard return beside each section heading (plan
|
|
33
|
+
* fallbacks-card-section-ux, the maintainer's live-host feedback reversing
|
|
34
|
+
* the flat plan's one-document model): the scroll-to-save cost of ONE
|
|
35
|
+
* bottom Save outweighed it. Each section's Save writes ONLY that section's
|
|
36
|
+
* fields (patch = the last ACCEPTED config with the section's fields
|
|
37
|
+
* replaced — the ride-along protection, PR #62 UX round 3), gated by that
|
|
38
|
+
* section's dirty term and validation bucket only (KD-U1: save =
|
|
39
|
+
* `!sectionDirty || saving || !writable`, discard = `!sectionDirty ||
|
|
40
|
+
* saving`; Save additionally carries `!writable` — W-1, qc2 fix wave: the
|
|
41
|
+
* dirty-implies-writable assumption does not hold for this in-place-draft
|
|
42
|
+
* store). Violations render live under their OWNING section heading, and a
|
|
43
|
+
* store write failure renders under the section whose Save was last
|
|
44
|
+
* clicked (`lastSaveSection`). Discard is kept — our staged edits survive
|
|
45
|
+
* refresh, the advisor's documented divergence from the official
|
|
46
|
+
* no-discard form — a pure client-side revert of THAT section's editors to
|
|
47
|
+
* the last accepted config.
|
|
54
48
|
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
* origin — a LOAD failure keeps the card-top notice (with Retry when
|
|
69
|
-
* inert), while a WRITE failure renders under the section whose Save was
|
|
70
|
-
* last clicked (`lastSaveSection`), unlike the advisor's separate
|
|
71
|
-
* apply-failure hints.
|
|
49
|
+
* The form body is the two-block editing surface (spec §8): the 6 top-level
|
|
50
|
+
* scalar fields (trigger codes / revert policy / three numeric fields), the
|
|
51
|
+
* `rootChain` block (block 1 — the root agent's single chain, no key input),
|
|
52
|
+
* and the roles block (block 2 — declared role entity cards from
|
|
53
|
+
* `roles.list` plus the rule rows from `roles.rules`, whose role field is a
|
|
54
|
+
* dropdown bound to the declared ids + the built-in `inherit`, same-page
|
|
55
|
+
* live). Row-level disclosures that are CONTENT keep their expand/collapse:
|
|
56
|
+
* the time-slot row cards and the role entity cards (with the seeded
|
|
57
|
+
* persona briefs) stay collapsible — a read-only view forces them open so
|
|
58
|
+
* the configs stay reachable. The 主代理 / 子代理 / 高级选项 group labels
|
|
59
|
+
* are semantic `<h2>` headings (T1); 高级选项 is a content-level disclosure
|
|
60
|
+
* collapsed by default (T3 — same expand/collapse contract as the rows, a
|
|
61
|
+
* read-only view forces it open).
|
|
72
62
|
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
63
|
+
* Validation: id format/reserved word/duplicates, undeclared rule role
|
|
64
|
+
* references, illegal selectors, and a role with no chain entries (no model
|
|
65
|
+
* config) block the write (the Save disabled gate) with per-section banners
|
|
66
|
+
* + inline red borders / hints (never touching the store error path); a
|
|
67
|
+
* non-empty `state.legacyKeys` renders the migration banner at the top.
|
|
68
|
+
* The reset-to-defaults affordance is GONE from the card (PR #62 UX
|
|
69
|
+
* round 3) — the gateway RPC `fallbacks/reset` and the store
|
|
70
|
+
* `resetToDefaults()` stay as host APIs, only the card UI was removed.
|
|
71
|
+
*
|
|
72
|
+
* The AC-7 read-only status block (recent-switch summary, D-5) stays at the
|
|
73
|
+
* bottom, flat.
|
|
74
|
+
*
|
|
75
|
+
* Presentation is ui-primitives-free (T4, advisor parity + reference §4
|
|
76
|
+
* risk): Button/Tooltip and the five icon imports are replaced by
|
|
77
|
+
* self-drawn styled elements — inline SVG icons (plus / trash / chevron /
|
|
78
|
+
* grip) and the CSS `data-tip` tooltip bubble (the card's existing
|
|
79
|
+
* icon-button pattern); every color resolves through a `--dsw-alias-*`
|
|
80
|
+
* token so the card adapts to the light/dark theme.
|
|
80
81
|
*/
|
|
81
82
|
import type { ReactNode } from 'react';
|
|
82
83
|
import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
|
|
@@ -93,10 +94,9 @@ export interface FallbacksCardInjected {
|
|
|
93
94
|
/** Props delivered by the slot outlet: runtime share + locale seat + inject face. */
|
|
94
95
|
export type FallbacksCardProps = PropsRuntime<'plugins.bundle.config'> & PropsLocale<'fallbacks'> & InjectFace<FallbacksCardInjected>;
|
|
95
96
|
/**
|
|
96
|
-
* Render the Fallbacks settings card inside the plugin-config section
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
* footer actions (Discard / Reset / Save).
|
|
97
|
+
* Render the Fallbacks settings card inside the plugin-config section —
|
|
98
|
+
* flat: the notices, the form, and the section action pairs directly under
|
|
99
|
+
* the page's plugin title/description, with no chrome of our own.
|
|
100
100
|
* @param props - slot-delivered injected dependencies and the synthesized t seat.
|
|
101
101
|
* @returns the card.
|
|
102
102
|
*/
|
|
@@ -56,7 +56,7 @@ export interface FallbacksSwitchSnapshot extends FallbacksSwitchEventData {
|
|
|
56
56
|
* live route probe.
|
|
57
57
|
*/
|
|
58
58
|
export type EffectiveModelView =
|
|
59
|
-
/** ①
|
|
59
|
+
/** ① An inactive config (nothing configured) or an empty rootChain. */
|
|
60
60
|
{
|
|
61
61
|
kind: 'unavailable';
|
|
62
62
|
}
|
|
@@ -225,10 +225,11 @@ export declare function classifyModel(provider: string, raw: string, catalog: Ca
|
|
|
225
225
|
*/
|
|
226
226
|
export declare function extractRecentSwitches(entries: readonly SessionEventLikeEntry[], limit?: number): FallbacksSwitchSnapshot[];
|
|
227
227
|
/**
|
|
228
|
-
* Derive the "current effective model" (spec §2.5 D-6): ①
|
|
229
|
-
*
|
|
230
|
-
*
|
|
231
|
-
*
|
|
228
|
+
* Derive the "current effective model" (spec §2.5 D-6): ① inactive (nothing
|
|
229
|
+
* configured — the removed `enabled` switch re-keyed to `isFallbackActive`)
|
|
230
|
+
* / empty rootChain → unavailable; ② a recent switch exists → the latest
|
|
231
|
+
* one's `to`; ③ otherwise → the config's primary target. A **display
|
|
232
|
+
* value** — never a live route probe.
|
|
232
233
|
*
|
|
233
234
|
* INTENTIONAL D-6 CONTRACT RETENTION: after the AC-2 trim (plan
|
|
234
235
|
* fallbacks-settings-visibility Task 2) the settings card's status block no
|