dsh-llm-fallbacks 0.2.1 → 0.3.0

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
@@ -13,6 +13,25 @@ Automatic provider/model fallback chains for dsh (DeepSeek Harness): when an age
13
13
 
14
14
  Works in both dsh front ends: the **web** profile (Settings → 插件配置 → Fallbacks card) and the **dsh-tui** terminal profile (`/fallbacks` + `/fallbacks config`).
15
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 (降级切换) 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 (默认模型) must be exactly one official V4 model — `deepseek-official/deepseek-v4-flash` XOR `deepseek-official/deepseek-v4-pro`. Slot rotation is a routing seed, not a failure decision: it applies on the next root request, consumes no cooldown, and is logged as a time-slot switch (分时切换) — failure walks keep 降级切换. Full semantics → [Time-slot presets (分时切换)](#time-slot-presets-分时切换) and [docs/configuration.md](docs/configuration.md).
34
+
16
35
  ## Quick start
17
36
 
18
37
  ### Install
@@ -24,6 +43,20 @@ dsh plugin --profile dsh-tui add dsh-llm-fallbacks # dsh-tui terminal profile
24
43
 
25
44
  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).
26
45
 
46
+ ### Repair existing sessions (versions before 0.2.2)
47
+
48
+ Versions before 0.2.2 wrote durable `fallbacks/switch` session events that newer dsh releases refuse to load (issue #52 — the apply()-time event-type registration is ineffective because plugin and host resolve different module instances). If existing sessions fail to open after an upgrade, clone this repository and repair the logs (stop dsh first):
49
+
50
+ ```sh
51
+ git clone https://github.com/omdsh-dev/dsh-llm-fallbacks.git
52
+ cd dsh-llm-fallbacks
53
+ pnpm install
54
+ pnpm repair:fallbacks-switch-logs -- --dry-run # preview which sessions would change
55
+ pnpm repair:fallbacks-switch-logs -- --apply --backup # mark legacy events ignorable
56
+ ```
57
+
58
+ The script scans `~/.dsh/sessions` by default (override with `--root <dir>`), marks legacy `fallbacks/switch` events `ignorable: true` so the host read path accepts the session again, and keeps a `<file>.bak` per repaired log. `--apply` requires `--backup` and must run with dsh stopped. From 0.2.2 on, the plugin stops writing durable switch events, so no new sessions need repair.
59
+
27
60
  ### Minimal configuration
28
61
 
29
62
  Add a `fallbacks:` section to the dsh settings document (default `$DSH_HOME/settings.yaml`):
@@ -31,9 +64,21 @@ Add a `fallbacks:` section to the dsh settings document (default `$DSH_HOME/sett
31
64
  ```yaml
32
65
  fallbacks:
33
66
  enabled: true # feature switch; defaults to false — set explicitly to enable
34
- rootChain: # block 1: root agent's chain, tried in order after the primary model fails
35
- - anthropic/claude-3-5-sonnet
36
- - openai/*
67
+ rootChain: # all-day chain: LAST entry is 默认模型 (official V4);
68
+ - anthropic/claude-3-5-sonnet # leading entries = 默认降级链 (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
37
82
  roles: # block 2: declare role entities first, then let rules reference them
38
83
  list:
39
84
  - id: reviewer # unique id (/^[a-z0-9-]{1,32}$/); "inherit" is reserved
@@ -41,28 +86,56 @@ fallbacks:
41
86
  chain:
42
87
  - openai/gpt-4o-mini
43
88
  fallback: inherit-root # default: role chain, then append rootChain
44
- rules:
45
- - origin: subagent # all subagents → reviewer role (own chain + inherited root)
46
- role: reviewer
89
+ rules: # subagent-only: rules never match root requests
90
+ - role: reviewer # all subagents → reviewer role (own chain + inherited root)
47
91
  ```
48
92
 
49
- 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).
50
94
 
51
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.
52
96
 
53
97
  ### Verify
54
98
 
55
- Save and restart the session, then type `/fallbacks` — the read-only in-session diagnostics (origin, resolved role, chain, recent `fallbacks/switch` events, cooldown status). Sessions containing `fallbacks/switch` events load after a restart because the plugin registers the event type at startup (rc.6 runtime registration; an upstream registration surface is pending) — without the plugin installed, such sessions refuse to load again until the upstream surface lands (see the Features note below). In a dsh-tui profile, `/fallbacks config` additionally reads back the composed configuration (the TUI has no settings page — config is file-only; see [docs/configuration.md](docs/configuration.md)).
99
+ Save and restart the session, then type `/fallbacks` — the read-only in-session diagnostics (origin, resolved role, chain, recent `fallbacks/switch` events, cooldown status). The plugin no longer writes durable `fallbacks/switch` session events (issue #52 — the apply()-time registration was proven ineffective), so new switches show up in the info logs, not in the recent-switch surfaces; sessions written by older plugin versions that contain `fallbacks/switch` events are repaired with `scripts/repair-fallbacks-switch-logs.ts`, which marks legacy events ignorable so those sessions load again (see the Features note below). In a dsh-tui profile, `/fallbacks config` additionally reads back the composed configuration (the TUI has no settings page — config is file-only; see [docs/configuration.md](docs/configuration.md)).
56
100
 
57
101
  ## Features
58
102
 
59
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.
60
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-分时切换)).
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.
61
108
  - **Cooldown and revert**: failed / switched-away models are not re-selected during cooldown; `revertPolicy: cooldown-expiry` returns to the primary model automatically.
62
- - **Visible behavior**: every switch appends a persisted `fallbacks/switch` session event (from/to/role/reason) with info-level logs — no silent model switching. The event type is registered with the harness at plugin startup (rc.6 runtime registration; an upstream registration surface is pending), so persisted events stay loadable across restarts while the plugin is installed.
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.
63
110
  - **Safety valves**: `maxSwitchesPerStep` caps switches per step and `alwaysModeRetryCap` caps always-mode retries — chain loops cannot amplify latency.
64
111
  - **No-config no-op**: `enabled` defaults to off; with no chains configured the plugin behaves exactly like not being installed.
65
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 默认模型 panel); leading entries (默认降级链) 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 默认降级链 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 主代理 section groups 分时槽设置 (extra rows — add preset / add custom / remove / reorder by buttons or **drag**; preset rows show a read-only window summary and edit models only; custom rows carry an editable name; the **timezone picker** lives here and **locks to Asia/Shanghai while any preset row exists**, since preset windows are frozen UTC+8 constants), 默认降级链 (walked first when no slot matches) and 默认模型 (the official V4 Flash | Pro last-resort fallback). Rows are collapsible to name + first model. There is no `timeSlots.enabled` master switch (adding a row is the opt-in) and no `rootMode` control.
138
+
66
139
  ## Preset roles
67
140
 
68
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
@@ -13,6 +13,25 @@ dsh(DeepSeek Harness)的自动模型降级插件:当 root agent 或 subage
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
  ### 安装
@@ -24,6 +43,20 @@ dsh plugin --profile dsh-tui add dsh-llm-fallbacks # dsh-tui 终端 profile
24
43
 
25
44
  同一个插件、两个前端——区别只在 `--profile` 参数。钉版本:加 `@<version>`。registry 安装拉取的是**已构建产物**(`dist/`),目标机无需构建。registry / git / 本地目录变体、卸载与 `--dump-config` 验证 → [docs/install.md](docs/install.md)。
26
45
 
46
+ ### 修复旧会话(0.2.2 之前的版本)
47
+
48
+ 0.2.2 之前的版本会把 `fallbacks/switch` 事件写入会话持久化日志,而新版 dsh 拒绝加载这类会话(issue #52——apply() 时的注册因插件与宿主解析到不同模块实例而无效)。如果升级后已有会话打不开,clone 本仓库并修复日志(先停 dsh):
49
+
50
+ ```sh
51
+ git clone https://github.com/omdsh-dev/dsh-llm-fallbacks.git
52
+ cd dsh-llm-fallbacks
53
+ pnpm install
54
+ pnpm repair:fallbacks-switch-logs -- --dry-run # 预览哪些会话会被改动
55
+ pnpm repair:fallbacks-switch-logs -- --apply --backup # 给旧事件打 ignorable 标记
56
+ ```
57
+
58
+ 脚本默认扫描 `~/.dsh/sessions`(可用 `--root <dir>` 覆盖),把遗留 `fallbacks/switch` 事件标记为 `ignorable: true`,宿主读路径即可重新接受该会话;每个被修复的日志保留一份 `<file>.bak`。`--apply` 必须搭配 `--backup`,且须在 dsh 停止时运行。从 0.2.2 起插件不再写 durable 切换事件,新会话无需修复。
59
+
27
60
  ### 最小配置
28
61
 
29
62
  在 dsh 的设置文档(默认 `$DSH_HOME/settings.yaml`)中添加 `fallbacks:` 分节:
@@ -31,9 +64,21 @@ dsh plugin --profile dsh-tui add dsh-llm-fallbacks # dsh-tui 终端 profile
31
64
  ```yaml
32
65
  fallbacks:
33
66
  enabled: true # 功能级开关;默认关闭(false),需显式打开后生效
34
- rootChain: # 块 1:root 主代理降级链;主模型失败后按顺序尝试
35
- - anthropic/claude-3-5-sonnet
36
- - 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
37
82
  roles: # 块 2:先声明角色,再让规则引用
38
83
  list:
39
84
  - id: reviewer # 角色实体:id 唯一(/^[a-z0-9-]{1,32}$/);inherit 为保留字
@@ -41,28 +86,56 @@ fallbacks:
41
86
  chain:
42
87
  - openai/gpt-4o-mini
43
88
  fallback: inherit-root # 默认:角色链后追加 rootChain
44
- rules:
45
- - origin: subagent # 所有 subagent → reviewer 角色(自身链 + 继承 root)
46
- role: reviewer
89
+ rules: # 仅对子代理生效:规则不匹配 root 请求
90
+ - role: reviewer # 所有 subagent → reviewer 角色(自身链 + 继承 root)
47
91
  ```
48
92
 
49
- 未命中规则 → 内置 `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)。
50
94
 
51
95
  > **升级提示(行为变更)**:已有 `fallbacks:` 配置若**未显式写 `enabled` 键**,升级后解析为 `false`——请补上 `enabled: true` 以保持插件继续生效。
52
96
 
53
97
  ### 验证
54
98
 
55
- 保存并重启会话后,键入 `/fallbacks`——只读的会话内诊断(来源、解析角色、链、最近的 `fallbacks/switch` 事件、冷却状态)。含 `fallbacks/switch` 事件的会话能在重启后正常加载,是因为插件在启动时注册了该事件类型(rc.6 运行时注册;上游注册面待落地)——卸载插件后,含此类事件的会话将再次拒绝加载,直到上游注册面落地(见下方「能力一览」说明)。在 dsh-tui profile 中,`/fallbacks config` 额外回读组合配置(TUI 无设置页——配置仅文件,见 [docs/configuration.md](docs/configuration.md))。
99
+ 保存并重启会话后,键入 `/fallbacks`——只读的会话内诊断(来源、解析角色、链、最近的 `fallbacks/switch` 事件、冷却状态)。插件**不再写入** durable `fallbacks/switch` 会话事件(issue #52——apply() 时的注册被证伪无效),因此新切换只出现在 info 日志中,不再出现在 recent-switch 展示面;由旧版插件写入、含 `fallbacks/switch` 事件的会话,可用 `scripts/repair-fallbacks-switch-logs.ts` 修复——脚本把旧事件标记为 ignorable,会话即可重新加载(见下方「能力一览」说明)。在 dsh-tui profile 中,`/fallbacks config` 额外回读组合配置(TUI 无设置页——配置仅文件,见 [docs/configuration.md](docs/configuration.md))。
56
100
 
57
101
  ## 能力一览
58
102
 
59
103
  - **root / subagent 自动降级**:任意 agent 在模型故障下按链切换到下一个可用 provider/model,无需手动换模型。
60
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`。失败降级保留**降级切换**文案(见 [分时槽预设(分时切换)](#分时槽预设分时切换))。
107
+ - **派发时角色解析**:在 subagent 的首次请求上,其角色按三个阶段解析——显式(`agentPreset` 匹配已声明角色 id)→ 确定性规则(不变)→ LLM 自动匹配(从已声明角色体系中选择,`fallbacks.roleAutoMatch` 默认 `true`)。解析出的角色的链头模型注入首次请求,并以显式 `role → model` 日志行记录(不写 durable `fallbacks/switch` 事件——issue #52 停写);设 `roleAutoMatch: false` 仅关闭 LLM 自动匹配阶段(显式 `agentPreset` 阶段仍生效——无显式角色时即复现原有仅规则行为)。设置卡总是渲染「启用角色自动匹配」开关(默认 `true`)以切换之——即使是从未声明过该键的旧配置,schema 默认值同样生效。
61
108
  - **冷却与回主**:被切离/失败的模型在冷却期内不再入选;`revertPolicy: cooldown-expiry` 冷却到期后自动回主模型。
62
- - **行为可见**:每次切换追加持久化会话事件 `fallbacks/switch`(from/to/role/reason),配合 info 级日志——无静默换模型。插件在启动时把该事件类型注册进宿主(rc.6 运行时注册;上游注册面待落地),持久化事件在插件安装期间可跨重启加载。
109
+ - **行为可见**:每次切换以 info 级日志行(from/to/role/reason)记录——无静默换模型。插件**刻意不写** durable `fallbacks/switch` 会话事件(issue #52——apply() 时的事件类型注册被证伪无效,含该事件的会话在 dsh 重启后拒绝加载)。由旧版插件写入、含此类事件的会话由 `scripts/repair-fallbacks-switch-logs.ts` 修复——旧事件被标记 ignorable 后,受影响会话可重新加载。
63
110
  - **安全阀**:`maxSwitchesPerStep` 限制每 step 切换次数、`alwaysModeRetryCap` 限制 always 模式重试——链循环不会放大延迟。
64
111
  - **无配置回归(no-op)**:`enabled` 默认关闭;未配置任何链时行为与未安装插件完全一致。
65
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
+
66
139
  ## 预设角色(Preset roles)
67
140
 
68
141
  插件内置 **7 个通用子代理角色**,开箱即用——`designer` / `librarian` / `reviewer` / `scout` / `security-reviewer` / `sonic` / `task`——`apply` 时自动以 seeded `roles.list` 行(`{ id, persona }`)声明:幂等,且绝不覆盖 operator 同名 persona。它们出现在设置卡的 seed 徽标(id 不可改)与 `/fallbacks config` 的角色摘要中,可直接被 `roles.rules` 引用。
@@ -0,0 +1,98 @@
1
+ /**
2
+ * LLM auto-match role selection (plan fallbacks-role-automatch Task 3).
3
+ *
4
+ * This module is the bounded LLM caller behind the resolver's stage-3
5
+ * auto-match hook (`resolveRoleAtDispatch`, `role-resolution.ts`). When the
6
+ * explicit and rules stages resolve to `'inherit'` and `roleAutoMatch` is
7
+ * enabled, `pickRoleByLlm` asks the model to pick the best-fit declared role
8
+ * from the taxonomy and returns its declared RAW id, or `null` (= `'inherit'`).
9
+ *
10
+ * Contract (architect-locked, plan Global Constraints · Auto-match bounded):
11
+ * - ONE bounded completion per decision: small `maxTokens`
12
+ * ({@link AUTOMATCH_MAX_TOKENS}) and a timeout
13
+ * ({@link AUTOMATCH_TIMEOUT_MS}, default 5s) that abandons the stream via
14
+ * the streaming contract's cancellation (`options.signal`) — no dangling
15
+ * iterator, no unhandled rejection.
16
+ * - Never throws. Every failure mode — throw, timeout, `none`, no valid id,
17
+ * garbage answer, malformed inputs — resolves to `null`, so the "never
18
+ * throws out of `agent/request`" invariant holds end-to-end (the resolver
19
+ * deliberately does NOT catch the hook).
20
+ * - Fast paths that avoid an LLM call entirely: empty taxonomy, absent
21
+ * `ctx.llm` service, and no concrete provider/model route for the judgment
22
+ * call. The plugin does NOT listen on `llm/stream`, so this call cannot
23
+ * recurse into the plugin.
24
+ *
25
+ * Judgment-call route choice (documented): the concrete provider/model for the
26
+ * judgment call is taken from the roles' DECLARED context first — the first
27
+ * exact (non-wildcard) `provider/model` selector across the declared chains,
28
+ * in declaration order (the operator's own taxonomy is the most stable route
29
+ * set). If no declared chain yields an exact selector, the agent's current
30
+ * `options.provider`/`options.model` is used when both halves are present;
31
+ * otherwise the call is skipped (`null`).
32
+ *
33
+ * Pure helpers exported for unit testing:
34
+ * - {@link buildAutomatchPrompt} — the taxonomy prompt (declared ids +
35
+ * personas + agent origin/agentPreset; instructs exactly-one-declared-id-or-
36
+ * `none`).
37
+ * - {@link parseAutomatchAnswer} — parses a declared id out of the model
38
+ * answer with surrounding whitespace/punctuation/quote/backtick tolerance;
39
+ * `none` (case-insensitive) and unknown ids → `null`. `roleIds` is the
40
+ * trimmed-id → declared-raw-id map, the same canonicalization the resolver
41
+ * uses.
42
+ *
43
+ * @module dsh-llm-fallbacks/automatch
44
+ */
45
+ import { type GenerateOptions, type StreamChunk } from '@deepseek-ai/dsh-llm';
46
+ import type { FallbacksRole, FallbacksRoles } from './config.ts';
47
+ import type { AgentLike } from './roles.ts';
48
+ /** Default auto-match completion timeout in ms — abandons the stream. */
49
+ export declare const AUTOMATCH_TIMEOUT_MS = 5000;
50
+ /** Output bound (tokens) for the judgment call: one id or the literal `none`. */
51
+ export declare const AUTOMATCH_MAX_TOKENS = 32;
52
+ /**
53
+ * Minimal `llm` service surface {@link pickRoleByLlm} reads. The real
54
+ * `LlmRuntime.stream(options)` matches structurally; this face keeps the
55
+ * module free of the full cordis Context merge.
56
+ */
57
+ export interface AutomatchLlm {
58
+ stream(options: GenerateOptions): AsyncIterable<StreamChunk>;
59
+ }
60
+ /**
61
+ * Minimal context surface: `ctx.get('llm')` (optional service — absent →
62
+ * `null` fast-path). The broad `get(name: string)` face is satisfied by the
63
+ * real cordis `Context` (`ReflectService.get` overloads return `any`/the
64
+ * merged service), so the Task-4 wiring can pass `ctx` directly.
65
+ */
66
+ export interface AutomatchContext {
67
+ get(name: string): AutomatchLlm | undefined;
68
+ }
69
+ /** Options for {@link pickRoleByLlm}. */
70
+ export interface PickRoleByLlmOptions {
71
+ /** Completion timeout in ms; defaults to {@link AUTOMATCH_TIMEOUT_MS}. */
72
+ timeoutMs?: number;
73
+ /** Warning sink — the decision path injects the plugin logger. */
74
+ warn: (message: string) => void;
75
+ }
76
+ /**
77
+ * Build the auto-match taxonomy prompt: every declared role's trimmed id and
78
+ * persona, the agent's origin (and non-empty `agentPreset` when present), and
79
+ * the exactly-one-declared-id-or-`none` instruction. Pure — no I/O.
80
+ */
81
+ export declare function buildAutomatchPrompt(roles: readonly FallbacksRole[], agent: AgentLike): string;
82
+ /**
83
+ * Parse the model's answer into a declared role id.
84
+ *
85
+ * The answer is expected to BE an id (possibly wrapped in whitespace, quotes,
86
+ * backticks, or trailing punctuation); surrounding characters outside the id
87
+ * grammar (`[a-zA-Z0-9-]`) are stripped, then the remaining string must match
88
+ * a declared id exactly (case-insensitive) via `roleIds` (trimmed id →
89
+ * declared RAW id, the resolver's canonicalization). `none` (case-insensitive)
90
+ * and any unknown/malformed answer → `null`.
91
+ */
92
+ export declare function parseAutomatchAnswer(answer: string, roleIds: ReadonlyMap<string, string>): string | null;
93
+ /**
94
+ * Bounded LLM role-selection call (see module header). Resolves the declared
95
+ * RAW id of the best-fit role, or `null` on any fast path or failure. NEVER
96
+ * throws.
97
+ */
98
+ export declare function pickRoleByLlm(ctx: AutomatchContext, roles: FallbacksRoles, agent: AgentLike, opts: PickRoleByLlmOptions): Promise<string | null>;
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * Fallbacks settings card — the `fallbacks` plugin card on the web settings
3
- * "插件配置" page (spec §4). Registered into the `settings.plugin.item` slot
4
- * (id `fallbacks`, order 30, alongside the upstream bash/agent-loop/web-search
5
- * cards and the advisor card); owner props are empty and all data flows
3
+ * "插件配置" page (spec §4). Registered into the `settings.plugin.item` keyed
4
+ * slot (key `fallbacks`, the settings namespace the card edits, alongside
5
+ * the upstream bash/agent-loop/web-search cards and the advisor card, in
6
+ * registration order); owner props are empty and all data flows
6
7
  * through {@link FallbacksSettingsController}.
7
8
  *
8
9
  * The card chrome replicates the upstream `PluginCard` contract (self-drawn:
@@ -11,12 +12,22 @@
11
12
  * description, with a dirty "unsaved" pill and a rotating chevron
12
13
  * (`IconChevronDownOutline14` from ui-primitives — a CLIENT_EXTERNALS value
13
14
  * import), `aria-expanded`/`aria-label` like the upstream header; a divider
14
- * under the header; then the form content; then a footer with
15
- * Discard / Reset / Save carrying the upstream disabled semantics — save =
16
- * `!dirty || saving || !writable`, discard = `!dirty || saving` (KD-U1).
17
- * Disclosure is card-local state: which card a user has open is a reading
18
- * gesture, and staged edits outlive collapsing — the pill rides the header
19
- * (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).
20
31
  *
21
32
  * The form body is the two-block editing surface (spec §8): the `enabled`
22
33
  * checkbox row, the 6 top-level scalar fields (trigger codes / revert
@@ -32,13 +43,14 @@
32
43
  * non-empty `state.legacyKeys` renders the migration banner at the top of
33
44
  * the card body. The row editors keep their filled editorCard surface
34
45
  * inside the card, with `--dsw-alias-*` tokens throughout. The reset-
35
- * to-defaults confirmation stays a `Modal` (the delete-confirm pattern of
36
- * 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.
37
49
  *
38
50
  * The page-only chrome is gone (720px column wrapper, title/intro banners,
39
51
  * page-bottom status block): the AC-7 read-only status (derived effective
40
- * model + recent-switch summary) is folded into the card body above the
41
- * 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.
42
54
  *
43
55
  * Degraded/error/loading states keep the same card chrome (KD-U3): the
44
56
  * header always renders title+description+chevron, and the body carries the
@@ -52,8 +64,11 @@
52
64
  * 'error'`) also forces the body open with an error notice and — when the
53
65
  * form is inert (`!writable`, i.e. the load never landed) — a Retry button;
54
66
  * a save failure keeps the editable form so the Save action itself is the
55
- * retry (the single `state.error` surface covers both, unlike the advisor's
56
- * 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.
57
72
  *
58
73
  * The degraded derivation is latched in the card (the store stays untouched):
59
74
  * `present` only ever changes inside the store's `accept()`, so the settled
@@ -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). */
@@ -180,12 +181,18 @@ export declare function classifyModel(provider: string, raw: string, catalog: Ca
180
181
  */
181
182
  export declare function extractRecentSwitches(entries: readonly HistoryEntry[], limit?: number): FallbacksSwitchSnapshot[];
182
183
  /**
183
- * Derive the status block's "current effective model" (spec §2.5 D-6): ①
184
- * disabled / empty rootChain → unavailable; ② a recent switch exists → the
185
- * latest one's `to`; ③ otherwise → the config's primary target. A **display
186
- * value** — never a live route probe (the section appends the non-probing
187
- * note inline right after the derived value, available case only; the
188
- * unavailable 空态 renders its own copy without the note).
184
+ * Derive the "current effective model" (spec §2.5 D-6): ① disabled / empty
185
+ * rootChain → unavailable; ② a recent switch exists → the latest one's `to`;
186
+ * ③ otherwise → the config's primary target. A **display value** — never a
187
+ * live route probe.
188
+ *
189
+ * INTENTIONAL D-6 CONTRACT RETENTION: after the AC-2 trim (plan
190
+ * fallbacks-settings-visibility Task 2) the settings card's status block no
191
+ * longer consumes this derivation, and no other production code imports it —
192
+ * it is retained (NOT dead code to delete) as the spec §2.5 D-6 derived-value
193
+ * surface, pinned by `tests/fallbacks-store.spec.ts` (D-6 display-value
194
+ * contract). Keep both exports until the spec derivation is removed or gains
195
+ * a real consumer.
189
196
  */
190
197
  export declare function deriveEffectiveModel(config: FallbacksConfig, switches: readonly FallbacksSwitchSnapshot[]): EffectiveModelView;
191
198
  /** One chain selector row: provider + model (or wildcard). */
@@ -210,6 +217,40 @@ export interface RootChainRow {
210
217
  export declare function rootChainToRows(rootChain: readonly string[], catalog?: CatalogLookup): RootChainRow[];
211
218
  /** Rebuild the rootChain from edited rows; rows with no usable selector drop out. */
212
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[];
213
254
  /**
214
255
  * One declared-role row in the editor (block 2 `roles.list`): identity
215
256
  * fields + the role's own chain selector list + its append strategy.
@@ -221,6 +262,8 @@ export interface RoleRow {
221
262
  persona: string;
222
263
  selectors: ChainSelectorRow[];
223
264
  fallback: FallbackStrategy;
265
+ /** UI-only collapse state — never serialized (dropped by rowsToRoles). */
266
+ collapsed: boolean;
224
267
  }
225
268
  /** Project the declared roles into editable rows (chain selectors classified). */
226
269
  export declare function rolesToRows(roles: readonly FallbacksRole[], catalog?: CatalogLookup): RoleRow[];
@@ -253,16 +296,18 @@ export declare function ruleRoleOptions(roles: Pick<FallbacksRoles, 'list'>): st
253
296
  * `chains`/`roles.default` keys never ride it).
254
297
  */
255
298
  export declare function detectLegacyClientKeys(config: FallbacksConfig): string[];
256
- /** 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
+ */
257
303
  export interface RoleRuleRow {
258
- origin: string;
259
304
  provider: CatalogSelection;
260
305
  model: CatalogSelection;
261
306
  role: string;
262
307
  }
263
308
  /** Project the role rules into editable rows (provider/model classified). */
264
309
  export declare function rulesToRows(rules: readonly FallbacksRoleRule[], catalog?: CatalogLookup): RoleRuleRow[];
265
- /** 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. */
266
311
  export declare function rowsToRules(rows: readonly RoleRuleRow[]): FallbacksRoleRule[];
267
312
  /** Controller joining Settings reads, writes, and pushed invalidations. */
268
313
  export declare class FallbacksSettingsController {
@@ -363,10 +408,15 @@ export declare class FallbacksSettingsController {
363
408
  * reason }` outcome is still a successful RPC — the post-write read
364
409
  * result (config / legacyKeys / seeds) lands either way, and the revert
365
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).
366
416
  * @param id - the seeded role id; the host matches it by trimmed id
367
417
  * against the seed registry (spec §9.3).
368
418
  */
369
- revertSeed(id: string): Promise<void>;
419
+ revertSeed(id: string): Promise<string | undefined>;
370
420
  /** Stop in-flight responses from publishing after plugin disposal. */
371
421
  dispose(): void;
372
422
  /**
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * dsh-llm-fallbacks client half: registers the Fallbacks card into the
3
- * plugin-config page's `settings.plugin.item` slot (the official "插件配置"
4
- * settings page — id `fallbacks`, order 30, alongside the upstream bash /
5
- * agent-loop / web-search cards and the advisor card).
3
+ * plugin-config page's `settings.plugin.item` keyed slot (the official
4
+ * "插件配置" settings page — key `fallbacks`, the settings namespace the card
5
+ * edits, appearing after the upstream bash / agent-loop / web-search cards
6
+ * and the advisor card in registration order).
6
7
  *
7
8
  * Wiring (mirrors dsh-advisor):
8
9
  * - Registers the `fallbacks` locale dictionaries (zh/en).
@@ -11,10 +12,11 @@
11
12
  * `/api/fallbacks/get|set|reset`), while `settings.describe` (writable +
12
13
  * namespace directory) and the provider/model catalog stay on
13
14
  * `connection.api` (see `fallbacks-store.ts`).
14
- * - Registers the `settings.plugin.item` card `id: 'fallbacks'` (order 30)
15
- * with a business-only inject face ({@link FallbacksSettingsController} +
16
- * the snapshot-selector hook); the old Settings-nav section registration
17
- * is removed — deleting the section registration deletes the nav entry.
15
+ * - Registers the `settings.plugin.item` card `key: 'fallbacks'` (the rc.7
16
+ * keyed slot — no `id`/`order`) with a business-only inject face
17
+ * ({@link FallbacksSettingsController} + the snapshot-selector hook); the
18
+ * old Settings-nav section registration is removed — deleting the section
19
+ * registration deletes the nav entry.
18
20
  * - Refreshes the store on pushed invalidations — the forwarded remote
19
21
  * events `settings/document-updated` (ns-filtered to the fallbacks
20
22
  * namespace; refetches the descriptor + recent-switch summary) and