dsh-llm-fallbacks 0.2.2 → 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,13 +5,32 @@
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
+
16
+ ## Time slots
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 (fallback switch) remains untouched.
19
+
20
+ ![Time slots](docs/assets/screenshot-1-en.png)
21
+
22
+ Four frozen UTC+8 presets (windows are code constants; preset rows lock `tz` to Asia/Shanghai):
23
+
24
+ | Preset | Window |
25
+ |---|---|
26
+ | `liang-peak` | 09:00–12:00 and 14:00–18:00, every day |
27
+ | `liang-valley` | every other UTC+8 time (complement of Liang Peak) |
28
+ | `glm-peak` | Monday–Friday 14:00–18:00 |
29
+ | `glm-valley` | every other time (complement of GLM Peak) |
30
+
31
+ GLM Peak and GLM Valley are offered in the card picker only when `zai-coding-cn` is configured.
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 (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).
15
34
 
16
35
  ## Quick start
17
36
 
@@ -45,9 +64,21 @@ Add a `fallbacks:` section to the dsh settings document (default `$DSH_HOME/sett
45
64
  ```yaml
46
65
  fallbacks:
47
66
  enabled: true # feature switch; defaults to false — set explicitly to enable
48
- rootChain: # block 1: root agent's chain, tried in order after the primary model fails
49
- - anthropic/claude-3-5-sonnet
50
- - openai/*
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
+ - deepseek-official/deepseek-v4-flash # last = Flash XOR Pro last-resort fallback
70
+ timeSlots: # optional: rotate the effective root chain by wall-clock windows
71
+ - kind: preset # frozen UTC+8 windows; only the model chain is editable (locks tz to Asia/Shanghai)
72
+ preset: liang-peak # 09:00–12:00 and 14:00–18:00, every day
73
+ chain:
74
+ - anthropic/claude-3-5-sonnet
75
+ - kind: custom # custom window (may wrap midnight)
76
+ name: evening # optional display name
77
+ start: '22:00'
78
+ end: '02:00'
79
+ days: [1, 5] # optional; omitted/empty = every day (0=Sunday…6=Saturday)
80
+ chain:
81
+ - openai/gpt-4o
51
82
  roles: # block 2: declare role entities first, then let rules reference them
52
83
  list:
53
84
  - id: reviewer # unique id (/^[a-z0-9-]{1,32}$/); "inherit" is reserved
@@ -55,12 +86,11 @@ fallbacks:
55
86
  chain:
56
87
  - openai/gpt-4o-mini
57
88
  fallback: inherit-root # default: role chain, then append rootChain
58
- rules:
59
- - origin: subagent # all subagents → reviewer role (own chain + inherited root)
60
- role: reviewer
89
+ rules: # subagent-only: rules never match root requests
90
+ - role: reviewer # all subagents → reviewer role (own chain + inherited root)
61
91
  ```
62
92
 
63
- No rule match → the built-in `inherit` → `rootChain`. `enabled` defaults to **off** — with no chains configured the plugin is a complete no-op. Full reference (role entities, fallback strategies, rules, selectors, preset roles) → [docs/configuration.md](docs/configuration.md).
93
+ No rule match (or a root request) → the built-in `inherit` → `rootChain`. `enabled` defaults to **off** — with no chains configured the plugin is a complete no-op. The all-day `rootChain` must END with exactly one official V4 model (`deepseek-official/deepseek-v4-flash` or `deepseek-official/deepseek-v4-pro`) — the settings card and gateway reject any other tail on save (a legacy non-official-tail chain warns at startup and keeps working as a fallback-only walk, but cannot be saved as-is). Full reference (role entities, fallback strategies, rules, selectors, preset roles, time-slot presets) → [docs/configuration.md](docs/configuration.md).
64
94
 
65
95
  > **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.
66
96
 
@@ -72,12 +102,40 @@ Save and restart the session, then type `/fallbacks` — the read-only in-sessio
72
102
 
73
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.
74
104
  - **Two-block config**: `rootChain` for the root agent; declared role entities (`roles.list`) referenced by `roles.rules` (or the built-in `inherit`).
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)).
75
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.
76
108
  - **Cooldown and revert**: failed / switched-away models are not re-selected during cooldown; `revertPolicy: cooldown-expiry` returns to the primary model automatically.
77
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.
78
110
  - **Safety valves**: `maxSwitchesPerStep` caps switches per step and `alwaysModeRetryCap` caps always-mode retries — chain loops cannot amplify latency.
79
111
  - **No-config no-op**: `enabled` defaults to off; with no chains configured the plugin behaves exactly like not being installed.
80
112
 
113
+ ## FallbacksChain in the model picker
114
+
115
+ 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 no TUI settings page or host patch is involved. The row is visible whenever the plugin is enabled — a legacy or empty all-day chain does NOT hide it (the override just refuses to fire).
116
+
117
+ Selecting **FallbacksChain / Auto** uses the configured chain as the root **primary**: root requests route to the effective chain's first exact `provider/model` at request time, and the fallback engine degrades from that head as usual. 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.
118
+
119
+ There is **no `rootMode` switch** — no config key, YAML field, settings toggle, or gateway flag. The mode is the session's `{provider, model}` selection itself: `FallbacksChain` = chain primary; any real model = fallback-only.
120
+
121
+ Notes:
122
+
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
+ - **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 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
+ - **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
+ - **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
+
129
+ ## Time-slot presets
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.
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 Default fallback chain entries are walked first).
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
+ - **Custom rows**: `start` / `end` (`HH:mm`, may wrap midnight) + optional `days` (0=Sunday…6=Saturday; omitted/empty = every day) + models.
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 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
+
81
139
  ## Preset roles
82
140
 
83
141
  The plugin ships **7 bundled generic subagent roles** out of the box — `designer` / `librarian` / `reviewer` / `scout` / `security-reviewer` / `sonic` / `task` — declared automatically on `apply` as seeded `roles.list` rows (`{ id, persona }`): idempotent, and never overwriting an operator persona. They appear in the Settings card (seed badge, id immutable) and in the `/fallbacks config` role summary, ready for `roles.rules` to reference.
package/README.zh-CN.md CHANGED
@@ -5,14 +5,33 @@
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 在目标模型上继续完成——任务不因模型问题中断。
13
13
 
14
14
  两个 dsh 前端均可用:**web** profile(设置 → 插件配置 → Fallbacks 卡片)与 **dsh-tui** 终端 profile(`/fallbacks` + `/fallbacks config`)。
15
15
 
16
+ ## 峰谷无忧
17
+
18
+ 峰谷无忧(分时切换)按墙钟窗口轮换**生效 root 链**:每个时段槽行拥有自己的 fallback 链,第一个窗口包含当前时刻的行将在下一个 root 请求取代全时段链——无行命中时,全时段链作为兜底保持在最后。峰谷窗口因此可以使用不同的模型链,而失败降级路径(降级切换)保持不变。
19
+
20
+ ![峰谷无忧](docs/assets/screenshot-1-zh.png)
21
+
22
+ 四个冻结的 UTC+8 预设(窗口为代码常量;存在预设行时 `tz` 锁定 Asia/Shanghai):
23
+
24
+ | 预设 | 窗口 |
25
+ |---|---|
26
+ | `liang-peak` | 每天 09:00–12:00 与 14:00–18:00 |
27
+ | `liang-valley` | 其它所有 UTC+8 时间(Liang Peak 的补集) |
28
+ | `glm-peak` | 周一至周五 14:00–18:00 |
29
+ | `glm-valley` | 其余时间(GLM Peak 的补集) |
30
+
31
+ GLM 峰与 GLM 谷仅在已配置 `zai-coding-cn` 时出现在设置卡选择器中。
32
+
33
+ 每个 root 请求时刻,第一条窗口包含当前时刻(按 `fallbacks.tz`,默认 Asia/Shanghai)的额外行生效;无行命中 → 全时段 `rootChain`——其链尾(默认模型)必须是恰好一个官方 V4 模型:`deepseek-official/deepseek-v4-flash` 或 `deepseek-official/deepseek-v4-pro`(二选一)。分时切换是路由种子而非失败决策:在下一个 root 请求生效、不消耗冷却、不计入 `maxSwitchesPerStep`,日志记为**分时切换**;失败降级保持**降级切换**。完整语义 → [分时槽预设(分时切换)](#分时槽预设分时切换) 与 [docs/configuration.md](docs/configuration.md)。
34
+
16
35
  ## 快速开始
17
36
 
18
37
  ### 安装
@@ -45,9 +64,21 @@ pnpm repair:fallbacks-switch-logs -- --apply --backup # 给旧事件打 igno
45
64
  ```yaml
46
65
  fallbacks:
47
66
  enabled: true # 功能级开关;默认关闭(false),需显式打开后生效
48
- rootChain: # 块 1:root 主代理降级链;主模型失败后按顺序尝试
49
- - anthropic/claude-3-5-sonnet
50
- - openai/*
67
+ rootChain: # 全时段链:最后一项是默认模型(官方 V4)
68
+ - anthropic/claude-3-5-sonnet # 前面是默认降级链(先走)
69
+ - deepseek-official/deepseek-v4-flash # 最后一档:Flash 或 Pro
70
+ timeSlots: # 可选:按墙钟时段轮换 root 生效链
71
+ - kind: preset # 冻结的 UTC+8 窗口;仅模型链可编辑(锁定 tz 为 Asia/Shanghai)
72
+ preset: liang-peak # 09:00–12:00 与 14:00–18:00,每天
73
+ chain:
74
+ - anthropic/claude-3-5-sonnet
75
+ - kind: custom # 自定义窗口(可跨午夜)
76
+ name: evening # 可选显示名称
77
+ start: '22:00'
78
+ end: '02:00'
79
+ days: [1, 5] # 可选;缺省/空 = 每天(0=周日…6=周六)
80
+ chain:
81
+ - openai/gpt-4o
51
82
  roles: # 块 2:先声明角色,再让规则引用
52
83
  list:
53
84
  - id: reviewer # 角色实体:id 唯一(/^[a-z0-9-]{1,32}$/);inherit 为保留字
@@ -55,12 +86,11 @@ fallbacks:
55
86
  chain:
56
87
  - openai/gpt-4o-mini
57
88
  fallback: inherit-root # 默认:角色链后追加 rootChain
58
- rules:
59
- - origin: subagent # 所有 subagent → reviewer 角色(自身链 + 继承 root)
60
- role: reviewer
89
+ rules: # 仅对子代理生效:规则不匹配 root 请求
90
+ - role: reviewer # 所有 subagent → reviewer 角色(自身链 + 继承 root)
61
91
  ```
62
92
 
63
- 未命中规则 → 内置 `inherit` → `rootChain`。`enabled` **默认关闭(`false`)**——未配置任何链时插件完全 no-op。完整参考(角色实体、fallback 策略、规则、selector、预设角色)→ [docs/configuration.md](docs/configuration.md)。
93
+ 未命中规则(或 root 请求)→ 内置 `inherit` → `rootChain`。`enabled` **默认关闭(`false`)**——未配置任何链时插件完全 no-op。全时段 `rootChain` 的**最后一项**必须恰好是一个官方 V4 模型(`deepseek-official/deepseek-v4-flash` 或 `deepseek-official/deepseek-v4-pro`)——设置卡与 gateway 在保存时拒绝其它尾巴(遗留非合规尾巴启动时告警并继续按 fallback-only 走原链,但无法原样保存)。完整参考(角色实体、fallback 策略、规则、selector、预设角色、分时槽预设)→ [docs/configuration.md](docs/configuration.md)。
64
94
 
65
95
  > **升级提示(行为变更)**:已有 `fallbacks:` 配置若**未显式写 `enabled` 键**,升级后解析为 `false`——请补上 `enabled: true` 以保持插件继续生效。
66
96
 
@@ -72,12 +102,40 @@ fallbacks:
72
102
 
73
103
  - **root / subagent 自动降级**:任意 agent 在模型故障下按链切换到下一个可用 provider/model,无需手动换模型。
74
104
  - **两块制配置**:`rootChain` 管 root 代理;声明式角色实体(`roles.list`)供 `roles.rules` 引用(或内置 `inherit`)。
105
+ - **选择器里把链当主模型**:`enabled` 开启时,宿主模型选择器(web 与 TUI 一致)出现虚拟 `FallbacksChain` / `Auto` 行——选中它即以配置的链作为 root 主模型(需要 all-day 链头合规才能成功覆盖);选真实模型则保持 fallback-only(见 [模型选择器中的 FallbacksChain](#模型选择器中的-fallbackschain))。
106
+ - **峰谷无忧(分时切换)**:可选的 `fallbacks.timeSlots` 行按墙钟窗口(配置级 `tz` 时区,默认 `Asia/Shanghai`)轮换 root 生效链——四个冻结的 UTC+8 预设(`liang-peak` / `liang-valley` / `glm-peak` / `glm-valley`,窗口为代码常量、仅模型链可编辑),或自定义 `start`/`end`/`days` 窗口。第一条命中的行生效;全时段行固定最后。时段切换在**下一个** root 请求生效,日志记为**分时切换**——路由种子而非失败决策:不消耗冷却、不计入 `maxSwitchesPerStep`。失败降级保留**降级切换**文案(见 [分时槽预设(分时切换)](#分时槽预设分时切换))。
75
107
  - **派发时角色解析**:在 subagent 的首次请求上,其角色按三个阶段解析——显式(`agentPreset` 匹配已声明角色 id)→ 确定性规则(不变)→ LLM 自动匹配(从已声明角色体系中选择,`fallbacks.roleAutoMatch` 默认 `true`)。解析出的角色的链头模型注入首次请求,并以显式 `role → model` 日志行记录(不写 durable `fallbacks/switch` 事件——issue #52 停写);设 `roleAutoMatch: false` 仅关闭 LLM 自动匹配阶段(显式 `agentPreset` 阶段仍生效——无显式角色时即复现原有仅规则行为)。设置卡总是渲染「启用角色自动匹配」开关(默认 `true`)以切换之——即使是从未声明过该键的旧配置,schema 默认值同样生效。
76
108
  - **冷却与回主**:被切离/失败的模型在冷却期内不再入选;`revertPolicy: cooldown-expiry` 冷却到期后自动回主模型。
77
109
  - **行为可见**:每次切换以 info 级日志行(from/to/role/reason)记录——无静默换模型。插件**刻意不写** durable `fallbacks/switch` 会话事件(issue #52——apply() 时的事件类型注册被证伪无效,含该事件的会话在 dsh 重启后拒绝加载)。由旧版插件写入、含此类事件的会话由 `scripts/repair-fallbacks-switch-logs.ts` 修复——旧事件被标记 ignorable 后,受影响会话可重新加载。
78
110
  - **安全阀**:`maxSwitchesPerStep` 限制每 step 切换次数、`alwaysModeRetryCap` 限制 always 模式重试——链循环不会放大延迟。
79
111
  - **无配置回归(no-op)**:`enabled` 默认关闭;未配置任何链时行为与未安装插件完全一致。
80
112
 
113
+ ## 模型选择器中的 FallbacksChain
114
+
115
+ 当 `enabled: true` 时,插件注册一个虚拟 provider **FallbacksChain**,目录中只有一行:**Auto**。web profile 与 dsh-tui 都能看到这一行:两者共享同一个 adapter catalog,无需 TUI 设置页或宿主补丁。该行只要插件启用就可见——遗留多模型或空的 all-day 链**不会**隐藏它(只是覆盖不会生效)。
116
+
117
+ 选择 **FallbacksChain / Auto** = 把配置的链作为 root **主模型**:root 请求路由到请求时刻生效链的第一个精确 `provider/model`,失败后由降级引擎从该链头照常沿链切换。选择任何真实目录模型则保持 v0.2.2 的 fallback-only 行为——会话模型为主,链只在它失败后介入。
118
+
119
+ **没有 `rootMode` 开关**——没有配置键、YAML 字段、设置开关或 gateway 标志。模式就是会话的 `{provider, model}` 选择本身:`FallbacksChain` = 链为主模型;任意真实模型 = fallback-only。
120
+
121
+ 注意:
122
+
123
+ - **选择器文案**:目录行的 `name`(composer 触发器显示)是动态的——`Auto: DeepSeek V4 Flash[Liang Peak]` / `Auto: DeepSeek V4 Flash[all-day]`(用 catalog 显示名,不是 model id);id 仍是 `Auto`。all-day 尾巴不合规则只显示 `Auto`。重新打开选择器即可刷新。
124
+ - **仅 root**:这一行只关乎 root 代理。subagent 的角色解析与注入不变;继承了该选择的 subagent 会话仍经链头路由——虚拟行只是薄委托,绝不是第二个路由引擎。
125
+ - **链尾合规门槛**:覆盖/委托成功要求 all-day 链**尾巴合规**——最后一项必须是恰好一个官方 V4 模型(`deepseek-official/deepseek-v4-flash` 或 `deepseek-official/deepseek-v4-pro`,即设置卡的「默认模型」面板);前面的默认降级链先走。禁用插件后该行隐藏(slot/链编辑不会触发注册抖动)。
126
+ - **过期选择**:行消失(插件禁用)而会话仍选中 `FallbacksChain / Auto` 时,会话继续把它显示为当前模型,但 `routable: false`——从目录选一个真实模型即可继续(宿主原生目录语义)。
127
+ - **能力跟随链头**:该行的模型元数据(上下文窗口、模态、推理)镜像当前生效链头;重试归属保持宽松默认——重试/失败记到被委托的真实链头,而非 `FallbacksChain` provider。完整语义 → [docs/configuration.md](docs/configuration.md)。
128
+
129
+ ## 分时槽预设(分时切换)
130
+
131
+ 峰谷无忧在[首页专题](#峰谷无忧)中介绍,本节是完整参考。分时槽行按墙钟窗口轮换**生效 root 链**——适合按峰谷切换模型,且不会把墙钟轮换误认为故障降级。文案严格区分:时段轮换的日志与 UI 用**分时切换**;失败降级保持**降级切换**;会话内「模型已降级」提示只出现在失败路径。
132
+
133
+ - **匹配顺序**:每个 root 请求时刻,第一条窗口包含当前时刻(按 `fallbacks.tz`,默认 `Asia/Shanghai` / UTC+8)的额外行生效——该行的模型链**取代**全时段链;无行命中则用全时段 `rootChain`。全时段行固定最后且**必选**:最后一项必须是恰好一个官方 V4 模型(Flash 或 Pro;前面的降级条目先走)。
134
+ - **预设**(冻结,不可编辑窗口):`liang-peak` = 每天 09:00–12:00 **与** 14:00–18:00;`liang-valley` = 其它所有 UTC+8 时间;`glm-peak` = 周一至周五 14:00–18:00;`glm-valley` = 其余时间。一个预设 id 对应一行;设置卡的选择器不会重复提供已添加的预设。
135
+ - **自定义行**:`start` / `end`(`HH:mm`,可跨午夜)+ 可选 `days`(0=周日…6=周六;缺省/空 = 每天)+ 模型。
136
+ - **下一请求生效**:时段边界跨越绝不打断进行中的 step——新行在下一个 root 请求生效。轮换仅挂载生效:info 日志 + 设置卡/`/fallbacks` 状态行,无 durable 切换事件。
137
+ - **设置卡**:主代理区块下分三块——**分时槽设置**(额外行:添加预设 / 添加自定义 / 删除 / 按钮或**拖拽**排序;预设行只读展示窗口摘要、仅可编辑模型链;自定义行带可编辑名称;**时区选择器**在此区块内,只要存在预设行就**锁定 Asia/Shanghai**——预设窗口是冻结的 UTC+8 常量)、**默认降级链**(all-day 链,可配置的 provider/model 选择器列表)与**默认模型**(官方 V4 Flash | Pro 二选一链头)。行可折叠为「名称 + 首个模型」。没有 `timeSlots.enabled` 总开关(添加行即开启),也没有 `rootMode` 控件。
138
+
81
139
  ## 预设角色(Preset roles)
82
140
 
83
141
  插件内置 **7 个通用子代理角色**,开箱即用——`designer` / `librarian` / `reviewer` / `scout` / `security-reviewer` / `sonic` / `task`——`apply` 时自动以 seeded `roles.list` 行(`{ id, persona }`)声明:幂等,且绝不覆盖 operator 同名 persona。它们出现在设置卡的 seed 徽标(id 不可改)与 `/fallbacks config` 的角色摘要中,可直接被 `roles.rules` 引用。
@@ -12,12 +12,22 @@
12
12
  * description, with a dirty "unsaved" pill and a rotating chevron
13
13
  * (`IconChevronDownOutline14` from ui-primitives — a CLIENT_EXTERNALS value
14
14
  * import), `aria-expanded`/`aria-label` like the upstream header; a divider
15
- * under the header; then the form content; then a footer with
16
- * Discard / Reset / Save carrying the upstream disabled semantics — save =
17
- * `!dirty || saving || !writable`, discard = `!dirty || saving` (KD-U1).
18
- * Disclosure is card-local state: which card a user has open is a reading
19
- * gesture, and staged edits outlive collapsing — the pill rides the header
20
- * (upstream rationale).
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).
21
31
  *
22
32
  * The form body is the two-block editing surface (spec §8): the `enabled`
23
33
  * checkbox row, the 6 top-level scalar fields (trigger codes / revert
@@ -33,13 +43,14 @@
33
43
  * non-empty `state.legacyKeys` renders the migration banner at the top of
34
44
  * the card body. The row editors keep their filled editorCard surface
35
45
  * inside the card, with `--dsw-alias-*` tokens throughout. The reset-
36
- * to-defaults confirmation stays a `Modal` (the delete-confirm pattern of
37
- * the Models page) — no `window.confirm`.
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.
38
49
  *
39
50
  * The page-only chrome is gone (720px column wrapper, title/intro banners,
40
51
  * page-bottom status block): the AC-7 read-only status (derived effective
41
- * model + recent-switch summary) is folded into the card body above the
42
- * footer, and the plugin-config section owns the column width.
52
+ * model + recent-switch summary) is folded into the card body, and the
53
+ * plugin-config section owns the column width.
43
54
  *
44
55
  * Degraded/error/loading states keep the same card chrome (KD-U3): the
45
56
  * header always renders title+description+chevron, and the body carries the
@@ -53,8 +64,11 @@
53
64
  * 'error'`) also forces the body open with an error notice and — when the
54
65
  * form is inert (`!writable`, i.e. the load never landed) — a Retry button;
55
66
  * a save failure keeps the editable form so the Save action itself is the
56
- * retry (the single `state.error` surface covers both, unlike the advisor's
57
- * separate apply-failure hints).
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.
58
72
  *
59
73
  * The degraded derivation is latched in the card (the store stays untouched):
60
74
  * `present` only ever changes inside the store's `accept()`, so the settled
@@ -66,7 +80,7 @@
66
80
  */
67
81
  import type { ReactNode } from 'react';
68
82
  import type { PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
69
- import type { SnapshotSelectorHook } from '@deepseek-ai/dsh-client-web-react';
83
+ import type { SnapshotSelectorHook } from '@deepseek-ai/dsh-client-ui-slots';
70
84
  import { FallbacksSettingsController, type FallbacksSettingsState } from './fallbacks-store.ts';
71
85
  /** Injected dependencies of {@link FallbacksCard} (slot `inject`). */
72
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 {
@@ -27,6 +27,7 @@
27
27
  import type { ClientConnectionRpc, ConfigurableProviderView, HistoryEntry, IApiClient, ModelProviderGroup, SessionId, SettingsNamespaceView } from '@deepseek-ai/dsh-client-connection/client';
28
28
  import { type SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client';
29
29
  import { type FallbackStrategy, type FallbacksConfig, type FallbacksRole, type FallbacksRoleRule, type FallbacksRoles } from '../config.ts';
30
+ import type { SlotRowConfig } from '../time-slots.ts';
30
31
  import type { FallbacksSwitchEventData } from '../events.ts';
31
32
  import type { SeedsWireStatus } from '../seeds.ts';
32
33
  /** The plugin's settings namespace on the host wire (settings/document-updated ns filter). */
@@ -216,6 +217,40 @@ export interface RootChainRow {
216
217
  export declare function rootChainToRows(rootChain: readonly string[], catalog?: CatalogLookup): RootChainRow[];
217
218
  /** Rebuild the rootChain from edited rows; rows with no usable selector drop out. */
218
219
  export declare function rowsToRootChain(rows: readonly RootChainRow[]): string[];
220
+ /**
221
+ * One extra time-slot row in the editor (plan fallbacks-timeslots Task 3):
222
+ * preset rows freeze their windows (read-only summary, models-only edits);
223
+ * custom rows edit start/end/days + chain. `kind` rides the wire VERBATIM —
224
+ * a hand-written YAML row with an unknown kind reads back as a custom-shaped
225
+ * row and serializes back unchanged, so the dirty check stays quiet (save
226
+ * validation rejects it).
227
+ */
228
+ export interface SlotEditorRow {
229
+ kind: string;
230
+ /** Frozen preset id — preset rows only (windows are code constants). */
231
+ preset?: string;
232
+ /** Custom rows: window start `HH:mm` text. */
233
+ start: string;
234
+ /** Custom rows: window end `HH:mm` text. */
235
+ end: string;
236
+ /** Custom rows: day mask 0=Sunday…6=Saturday; [] = every day. */
237
+ days: number[];
238
+ /** Custom rows: display name (PR #62 feedback round — collapsed rows). */
239
+ name: string;
240
+ /** UI-only collapse state — never serialized (dropped by rowsToTimeSlots). */
241
+ collapsed: boolean;
242
+ selectors: ChainSelectorRow[];
243
+ }
244
+ /** Project the time-slot rows into editable rows (chain selectors classified). */
245
+ export declare function timeSlotsToRows(timeSlots: readonly SlotRowConfig[], catalog?: CatalogLookup): SlotEditorRow[];
246
+ /** Rebuild the time-slot rows from edited rows; blank selectors drop out.
247
+ * `kind` rides verbatim (a hand-written unknown kind reads back unchanged;
248
+ * save validation rejects it) — the cast asserts the trusted editor shape.
249
+ * `days` is ALWAYS serialized ([] included): schemastery composes absent
250
+ * array fields as `[]`, so the composed config every card load accepts
251
+ * carries `days` on every row — the draft must too, or a clean card would
252
+ * read back dirty. */
253
+ export declare function rowsToTimeSlots(rows: readonly SlotEditorRow[]): SlotRowConfig[];
219
254
  /**
220
255
  * One declared-role row in the editor (block 2 `roles.list`): identity
221
256
  * fields + the role's own chain selector list + its append strategy.
@@ -227,6 +262,8 @@ export interface RoleRow {
227
262
  persona: string;
228
263
  selectors: ChainSelectorRow[];
229
264
  fallback: FallbackStrategy;
265
+ /** UI-only collapse state — never serialized (dropped by rowsToRoles). */
266
+ collapsed: boolean;
230
267
  }
231
268
  /** Project the declared roles into editable rows (chain selectors classified). */
232
269
  export declare function rolesToRows(roles: readonly FallbacksRole[], catalog?: CatalogLookup): RoleRow[];
@@ -259,16 +296,18 @@ export declare function ruleRoleOptions(roles: Pick<FallbacksRoles, 'list'>): st
259
296
  * `chains`/`roles.default` keys never ride it).
260
297
  */
261
298
  export declare function detectLegacyClientKeys(config: FallbacksConfig): string[];
262
- /** One role-rule row in the editor; empty origin means "any". */
299
+ /**
300
+ * One role-rule row in the editor (PR #62 feedback: no origin control —
301
+ * rules are subagent-only; a persisted wire `origin` is ignored).
302
+ */
263
303
  export interface RoleRuleRow {
264
- origin: string;
265
304
  provider: CatalogSelection;
266
305
  model: CatalogSelection;
267
306
  role: string;
268
307
  }
269
308
  /** Project the role rules into editable rows (provider/model classified). */
270
309
  export declare function rulesToRows(rules: readonly FallbacksRoleRule[], catalog?: CatalogLookup): RoleRuleRow[];
271
- /** Rebuild the role rules from edited rows; empty origin/provider/model drop out. */
310
+ /** Rebuild the role rules from edited rows; empty provider/model drop out. */
272
311
  export declare function rowsToRules(rows: readonly RoleRuleRow[]): FallbacksRoleRule[];
273
312
  /** Controller joining Settings reads, writes, and pushed invalidations. */
274
313
  export declare class FallbacksSettingsController {
@@ -369,10 +408,15 @@ export declare class FallbacksSettingsController {
369
408
  * reason }` outcome is still a successful RPC — the post-write read
370
409
  * result (config / legacyKeys / seeds) lands either way, and the revert
371
410
  * button stays disabled while the write is in flight.
411
+ *
412
+ * Returns the seed-default persona when the outcome is `{ reverted:
413
+ * true, persona }` — including the persist no-op (persisted already
414
+ * equals the seed). The card applies that string to the row's **draft**
415
+ * so an unsaved persona edit still snaps back (issue #59).
372
416
  * @param id - the seeded role id; the host matches it by trimmed id
373
417
  * against the seed registry (spec §9.3).
374
418
  */
375
- revertSeed(id: string): Promise<void>;
419
+ revertSeed(id: string): Promise<string | undefined>;
376
420
  /** Stop in-flight responses from publishing after plugin disposal. */
377
421
  dispose(): void;
378
422
  /**