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 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. Enable the plugin.** `enabled: true` turns the fallback engine on. It defaults to **off** — with no chains configured the plugin is a complete no-op.
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)**: an existing `fallbacks:` section **without an explicit `enabled` key** resolves to `false` after upgrading — add `enabled: true` to keep the plugin active.
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**: 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 delegation to succeed); selecting a real model keeps fallback-only (see [FallbacksChain in the model picker](#fallbackschain-in-the-model-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 no chains configured the plugin behaves exactly like not being installed (`enabled` is off by default — see [Minimal configuration](#minimal-configuration)).
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 (`enabled`, `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.
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
- When `enabled: true`, the plugin 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). The row is visible whenever the plugin is enabled — a legacy or empty all-day chain does NOT hide it (the delegate just refuses to serve it).
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. Disabling the plugin hides the row again (slot-row/chain edits never churn registration).
227
- - **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).
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
- - **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 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.
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. 启用插件。** `enabled: true` 打开降级引擎。默认**关闭(`false`)**——未配置任何链时插件完全 no-op。
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
- > **升级提示(行为变更)**:已有 `fallbacks:` 配置若**未显式写 `enabled` 键**,升级后解析为 `false`——请补上 `enabled: true` 以保持插件继续生效。
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
- - **选择器里把链当主模型**:`enabled` 开启时,宿主模型选择器(web 与 TUI 一致)出现虚拟 `FallbacksChain` / `Auto` 行——选中它即以配置的链作为 root 主模型(需要 all-day 链头合规才能成功委托);选真实模型则保持 fallback-only(见 [模型选择器中的 FallbacksChain](#模型选择器中的-fallbackschain))。
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)**:未配置任何链时行为与未安装插件完全一致——`enabled` 默认关闭(见 [最小配置](#最小配置))。
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 设置卡完全一致**:布尔(`enabled`、`roleAutoMatch`)渲染为开关、下拉(`presets`、`revertPolicy`)为选择器、数值(`cooldownMs`、`maxSwitchesPerStep`、`alwaysModeRetryCap`)为数字输入;复杂结构(`rootChain`、`timeSlots`、`roles.list`、`roles.rules`)为 JSON 文本字段,`triggerCodes` 为逗号分隔文本字段。非法草稿(JSON 解析失败、链尾不合规、分时行畸形)会阻止保存——区块绝不写入损坏配置。
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
- 当 `enabled: true` 时,插件注册一个虚拟 provider **FallbacksChain**,目录中只有一行:**Auto**。web profile 与 dsh-tui 都能看到这一行:两者共享同一个 adapter catalog,无需设置页接线或宿主补丁(它与 `/settings` 的 fallbacks 区块相互独立——区块编辑的是配置,不是选择器目录)。该行只要插件启用就可见——遗留多模型或空的 all-day 链**不会**隐藏它(只是委托会拒绝服务)。
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`,即设置卡的「默认模型」面板);前面的默认降级链先走。禁用插件后该行隐藏(slot/链编辑不会触发注册抖动)。
227
- - **过期选择**:行消失(插件禁用)而会话仍选中 `FallbacksChain / Auto` 时,会话继续把它显示为当前模型,但 `routable: false`——从目录选一个真实模型即可继续(宿主原生目录语义)。
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
- - **设置卡**:主代理区块下分三块——**分时槽设置**(额外行:添加预设 / 添加自定义 / 删除 / 按钮或**拖拽**排序;预设行只读展示窗口摘要、仅可编辑模型链;自定义行带可编辑名称;**时区选择器**在此区块内,只要存在预设行就**锁定 Asia/Shanghai**——预设窗口是冻结的 UTC+8 常量)、**默认降级链**(all-day 链,可配置的 provider/model 选择器列表)与**默认模型**(官方 Flash | Pro 二选一链头)。行可折叠为「名称 + 首个模型」。没有 `timeSlots.enabled` 总开关(添加行即开启),也没有 `rootMode` 控件。
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
- * The card chrome replicates the upstream `PluginCard` contract (self-drawn:
10
- * the upstream client value face exports no reusable card): a collapsible
11
- * `<li>` whose header is a button stacking the plugin name over its
12
- * description, with a dirty "unsaved" pill and a rotating chevron
13
- * (`IconChevronDownOutlineMedium` from ui-primitives — a CLIENT_EXTERNALS value
14
- * import), `aria-expanded`/`aria-label` like the upstream header; a divider
15
- * under the header; then the form content. PR #62 UX round 2: the card
16
- * footer is gone — each big section (主代理 / 子代理 / 高级选项) carries its
17
- * own Save/Discard actions beside its heading (高级选项: inside the expanded
18
- * body) and its own validation / save-error surface. PR #62 UX round 3:
19
- * each section's Save writes ONLY that section's fields — 主代理 owns
20
- * rootChain / timeSlots / tz (+ the card-level `enabled`), 子代理 owns
21
- * roles, 高级选项 owns the advanced scalars; the patch spreads the last
22
- * ACCEPTED config for every other section, so a 主代理 Save can never
23
- * ride along an unsaved 子代理 edit (and vice versa) — and validation /
24
- * the dirty gate apply per section too (a bad role id never blocks 主代理,
25
- * and only the saved section's Discard reverts that section's edits).
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 form body is the two-block editing surface (spec §8): the `enabled`
33
- * checkbox row, the 6 top-level scalar fields (trigger codes / revert
34
- * policy / three numeric fields), the `rootChain` block (block 1 — the
35
- * root agent's single chain, no key input), and the roles block (block 2 —
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
- * The page-only chrome is gone (720px column wrapper, title/intro banners,
51
- * page-bottom status block): the AC-7 read-only status (derived effective
52
- * model + recent-switch summary) is folded into the card body, and the
53
- * plugin-config section owns the column width.
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
- * Degraded/error/loading states keep the same card chrome (KD-U3): the
56
- * header always renders title+description+chevron, and the body carries the
57
- * config-channel notice or the load error. A card that cannot reach the
58
- * `fallbacks/get` gateway channel (`ready && !present`) keeps the USABLE
59
- * skeleton — the form stays writable and saves are attempted (KD-G5) — with
60
- * the `unavailable` notice ALWAYS visible (derived open — the header cannot
61
- * collapse it away), while a healthy card is collapsed until the user
62
- * expands it (AC-1, the documented divergence from upstream whose
63
- * unavailable card renders nothing). A hard load failure (`status ===
64
- * 'error'`) also forces the body open with an error notice and — when the
65
- * form is inert (`!writable`, i.e. the load never landed) — a Retry button;
66
- * a save failure keeps the editable form so the Save action itself is the
67
- * retry. PR #62 UX round 2: the single `state.error` surface is split by
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
- * The degraded derivation is latched in the card (the store stays untouched):
74
- * `present` only ever changes inside the store's `accept()`, so the settled
75
- * `ready` read is authoritative, and a card-local latch carries that value
76
- * through refresh/save windows (`loading`/`saving`) so the notice body can
77
- * never collapse mid-refresh (the advisor's latched `degraded` field,
78
- * implemented without a store change); on a first mount the latch is false,
79
- * so the healthy card starts (and stays) collapsed through its first load.
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
- * replicating the upstream PluginCard chrome (KD-U1). The body carries the
98
- * existing form content unchanged plus the folded-in status block and the
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
- /** ① `enabled: false` or an empty rootChain. */
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): ① disabled / empty
229
- * rootChain → unavailable; ② a recent switch exists → the latest one's `to`;
230
- * ③ otherwise → the config's primary target. A **display value** — never a
231
- * live route probe.
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