dsh-llm-fallbacks 0.4.1 → 0.4.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
@@ -130,6 +130,8 @@ Save the config and restart the session, then type `/fallbacks` — the read-onl
130
130
  - **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)).
131
131
  - **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)).
132
132
  - **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.
133
+ - **Subagent role badge**: when a subagent's dispatch resolves a non-`inherit` role (policy on or off), its session shows a compact role badge next to the session title in the web session header — hover shows `role → provider/model`, the effective route after any override/inject. `inherit`/unresolved sessions render no badge; records are in-process only (a host restart clears them — the `role → model` info log stays the durable record).
134
+ - **Context-window-aware fallback**: `triggerCodes` accepts any dsh failure code, including `CONTEXT_WINDOW_EXCEEDED` — a request too big for the current model fails over to a candidate with a **larger** context window (candidates that cannot fit either are skipped), and because the route itself is healthy the switch is request-scoped: no cooldown, no half-open probe burned (see [Fallback triggers](#fallback-triggers-triggercodes)).
133
135
  - **Cooldown and revert**: failed / switched-away models are not re-selected during cooldown; `revertPolicy: cooldown-expiry` returns to the primary model automatically.
134
136
  - **Host subagent model policy (dsh 0.1.2)**: when the host `subagent-model-selection` policy is enabled, its allowlist is a hard constraint on every plugin-originated subagent route — an explicit authorized spawn route stays the chain head (role-inject skipped), inheritance inject heads and failure-switch targets are intersected with the effective allowlist, and an empty intersection skips the inject/switch (warn log + read-only card warning; no out-of-allowlist request is ever sent). A present-but-unreadable policy fails closed. Policy off/absent → inject and failure-switch selection exactly as 0.3.5. Override `reasoningEffort` follows the upstream routeChanged rule on every path (same route → keep; route change → drop unless explicit). See [Host subagent model policy](#host-subagent-model-policy-dsh-012).
135
137
  - **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)).
@@ -142,7 +144,7 @@ Save the config and restart the session, then type `/fallbacks` — the read-onl
142
144
  In a dsh-tui profile the plugin has three operator surfaces, with a strict duty split:
143
145
 
144
146
  - **`/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.
145
- - **`/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 (a web-card action the settings seam cannot express).
147
+ - **`/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.
146
148
  - **`/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.
147
149
 
148
150
  **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.
@@ -177,11 +179,38 @@ Time slots are introduced in the [featured overview](#time-slots) above; this se
177
179
 
178
180
  ## Preset roles
179
181
 
180
- 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.
182
+ The plugin ships **5 bundled generic subagent roles** out of the box — `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 as read-only rows (a source badge on every row; the id and persona cannot be edited there) and in the `/fallbacks config` role summary, ready for `roles.rules` to reference. (`designer` and `librarian` are no longer bundled: rows saved by earlier versions keep their persona but now show source `user`, even though the operator never wrote them.)
181
183
 
182
184
  - **Switch**: `fallbacks.presets` — `'bundled'` (default) declares the preset roles on apply; `'none'` disables the automatic declaration (already-materialized rows stay).
183
185
  - Full semantics (upgrade behavior, conflict handling, library reuse of `presetRoles`) → [docs/configuration.md](docs/configuration.md).
184
186
 
187
+ ## Fallback triggers (`triggerCodes`)
188
+
189
+ `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.
190
+
191
+ ### Context-window rejections
192
+
193
+ A request larger than the model's context window is reported as `CONTEXT_WINDOW_EXCEEDED` — dsh core's canonical code for a provider 400 "maximum context length". It is not a retryable code, so list it explicitly to fail over to a larger-context model:
194
+
195
+ ```yaml
196
+ fallbacks:
197
+ enabled: true
198
+ triggerCodes:
199
+ - AUTH
200
+ - QUOTA
201
+ - RATE_LIMIT
202
+ - CONTEXT_WINDOW_EXCEEDED # fail over when the request does not fit
203
+ rootChain:
204
+ - anthropic/claude-3-5-sonnet # walked first
205
+ - deepseek-official/deepseek-v4-flash # last resort (Flash or Pro)
206
+ ```
207
+
208
+ **Ordering — the fallback runs before compaction.** With `CONTEXT_WINDOW_EXCEEDED` in `triggerCodes`, this plugin's `agent/request-error` listener handles the rejection **before** the harness's compaction plugin gets to compact: the conversation moves to the fallback model on the first overflow, and the context is never compacted. Leave the code out of `triggerCodes` if you would rather have compaction tried first.
209
+
210
+ **Request-scoped switching.** A context-window rejection says the *request* was too big, not that the route is unhealthy. Such a switch is **request-scoped**: the from-route is recorded as failed for the current step (so the step does not bounce straight back to it) and still counts against `maxSwitchesPerStep`, but it is **not** put on cooldown for `cooldownMs` and does not feed the half-open recovery counter — the next, shorter prompt goes to the primary model as usual, and no half-open probe is spent on a route that never failed. Every other trigger code stays **route-scoped** (cooldown + recovery bookkeeping) exactly as before.
211
+
212
+ **Candidates that cannot fit are skipped.** On a context-window walk the chain skips any candidate whose advertised context window is not larger than the failing model's — falling over from a 128k model to an 8k one would only fail again. Windows come from the host model catalog: the catalog row's `contextWindow` when it carries one, otherwise `llm.resolveModelInfo(provider, model)`'s `context.contextWindow`. A model that discloses no window either way is kept as a candidate, so a provider without capacity metadata never empties a chain. Skipped candidates are named in the switch log line as `skipped: context-window`.
213
+
185
214
  ## Host subagent model policy (dsh 0.1.2)
186
215
 
187
216
  dsh 0.1.2 adds host-side child-model selection for subagents: a `subagent-model-selection` settings allowlist, a per-session `subagent/model-selection-policy` event, and spawn-time `provider/model/reasoning_effort` routing. The plugin reconciles with it under one runtime arbiter — plugin roles/chains remain the failure-recovery layer the host does not provide.
package/README.zh-CN.md CHANGED
@@ -130,6 +130,8 @@ fallbacks:
130
130
  - **选择器里把链当主模型**:`enabled` 开启时,宿主模型选择器(web 与 TUI 一致)出现虚拟 `FallbacksChain` / `Auto` 行——选中它即以配置的链作为 root 主模型(需要 all-day 链头合规才能成功覆盖);选真实模型则保持 fallback-only(见 [模型选择器中的 FallbacksChain](#模型选择器中的-fallbackschain))。
131
131
  - **峰谷无忧(分时切换)**:可选的 `fallbacks.timeSlots` 行按墙钟窗口(配置级 `tz` 时区,默认 `Asia/Shanghai`)轮换 root 生效链——四个冻结的 UTC+8 预设(`liang-peak` / `liang-valley` / `glm-peak` / `glm-valley`,窗口为代码常量、仅模型链可编辑),或自定义 `start`/`end`/`days` 窗口。第一条命中的行生效;全时段行固定最后。时段切换在**下一个** root 请求生效,日志记为**分时切换**——路由种子而非失败决策:不消耗冷却、不计入 `maxSwitchesPerStep`。失败降级保留**降级切换**文案(见 [分时槽预设(分时切换)](#分时槽预设分时切换))。
132
132
  - **派发时角色解析**:在 subagent 的首次请求上,其角色按三个阶段解析——显式(`agentPreset` 匹配已声明角色 id)→ 确定性规则(不变)→ LLM 自动匹配(从已声明角色体系中选择,`fallbacks.roleAutoMatch` 默认 `true`)。解析出的角色的链头模型注入首次请求,并以显式 `role → model` 日志行记录(不写 durable `fallbacks/switch` 事件——issue #52 停写);设 `roleAutoMatch: false` 仅关闭 LLM 自动匹配阶段(显式 `agentPreset` 阶段仍生效——无显式角色时即复现原有仅规则行为)。设置卡总是渲染「启用角色自动匹配」开关(默认 `true`)以切换之——即使是从未声明过该键的旧配置,schema 默认值同样生效。
133
+ - **子代理角色徽标**:当 subagent 的派发解析出非 `inherit` 角色(策略开或关)时,其会话在 Web 会话头部的标题旁显示一个紧凑的角色徽标——悬停显示 `role → provider/model`(覆盖/注入后的实际生效路由)。`inherit`/未解析角色的会话不显示徽标;记录仅存于进程内(宿主重启即清空——`role → model` info 日志仍是持久记录)。
134
+ - **上下文窗口感知降级**:`triggerCodes` 接受任意 dsh 失败码,包括 `CONTEXT_WINDOW_EXCEEDED`——请求超出当前模型上下文时,降级到上下文窗口**更大**的候选(装不下的候选被跳过);由于路由本身健康,该切换是请求级的:不冷却、不浪费半开探针(见 [降级触发码](#降级触发码triggercodes))。
133
135
  - **冷却与回主**:被切离/失败的模型在冷却期内不再入选;`revertPolicy: cooldown-expiry` 冷却到期后自动回主模型。
134
136
  - **宿主子代理模型策略(dsh 0.1.2)**:当宿主 `subagent-model-selection` 策略启用时,其允许列表对每个插件发起的 subagent 路由都是硬约束——显式授权的派发路由保持为链头(跳过角色注入),继承注入的链头与失败切换目标都与生效允许列表求交集,交集为空则跳过注入/切换(warn 日志 + 只读卡片警告;绝不发送允许列表之外的请求)。策略存在但不可读时 fail-closed。策略关闭/缺省时,注入与失败切换的选择与 0.3.5 完全一致。覆盖路径上的 `reasoningEffort` 遵循上游 routeChanged 规则(同路由 → 保留;跨路由 → 除非显式指定否则丢弃)。见 [宿主子代理模型策略](#宿主子代理模型策略dsh-012)。
135
137
  - **半开恢复(可选)**:`recovery: half-open` 让恢复以证据驱动——冷却到期后路由进入 **half-open**,以一次记录探针(logged probe)放行,而不是直接恢复首选;连续失败使抑制时长按 **×2** 逐次升级、**1 小时**封顶;观察到完成即闭合回路、完全恢复首选。`revertPolicy: 'never'` 使该机制完全失效;状态为会话级内存态(重启即重置)。仅 YAML 配置——默认 `timer` 保持所有既有行为逐字节一致(见 [docs/configuration.md](docs/configuration.md#recovery-mode-recovery-key))。
@@ -142,7 +144,7 @@ fallbacks:
142
144
  在 dsh-tui profile 中,插件有三个操作面——职责严格区分:
143
145
 
144
146
  - **`/fallbacks`** —— 本次会话发生了什么:来源、解析角色、生效链、最近降级切换、冷却状态(`recovery: half-open` 生效时显示 half-open 标记行)。只读。
145
- - **`/fallbacks config`** —— 配置了什么:组合配置回读(触发码、根链、分时槽、时区、角色、角色规则、冷却、回主策略、安全阀、预置、角色自动匹配)。除唯一的动作命令 **`/fallbacks config revert-seed <role-id>`** 外只读——该命令把某个 seed 角色的 persona 还原为已声明的默认(设置 seam 无法表达 Web 卡的这类动作能力)。
147
+ - **`/fallbacks config`** —— 配置了什么:组合配置回读(触发码、根链、分时槽、时区、角色、角色规则、冷却、回主策略、安全阀、预置、角色自动匹配)。除唯一的动作命令 **`/fallbacks config revert-seed <role-id>`** 外只读——该命令把某个 seed 角色的 persona 还原为已声明的默认(Web 设置卡将 seed 角色的 persona 呈现为只读、不提供还原入口,此命令是该动作的唯一入口)。
146
148
  - **`/settings`** —— 编辑界面。插件注册 **fallbacks** 区块,与 **Web 设置卡完全一致**:布尔(`enabled`、`roleAutoMatch`)渲染为开关、下拉(`presets`、`revertPolicy`)为选择器、数值(`cooldownMs`、`maxSwitchesPerStep`、`alwaysModeRetryCap`)为数字输入;复杂结构(`rootChain`、`timeSlots`、`roles.list`、`roles.rules`)为 JSON 文本字段,`triggerCodes` 为逗号分隔文本字段。非法草稿(JSON 解析失败、链尾不合规、分时行畸形)会阻止保存——区块绝不写入损坏配置。
147
149
 
148
150
  **版本要求**:`/settings` 的 fallbacks 区块需要 **dsh-tui ≥ v0.8.5**(`main` 上 commit `c51661f` 及以后;settings seam 于 v0.8.0 引入,groups 结构与校验于 v0.8.5 引入)。更旧的 dsh-tui 没有该区块,文件编辑仍是 TUI 唯一编辑面。
@@ -177,11 +179,38 @@ fallbacks:
177
179
 
178
180
  ## 预设角色(Preset roles)
179
181
 
180
- 插件内置 **7 个通用子代理角色**,开箱即用——`designer` / `librarian` / `reviewer` / `scout` / `security-reviewer` / `sonic` / `task`——`apply` 时自动以 seeded `roles.list` 行(`{ id, persona }`)声明:幂等,且绝不覆盖 operator 同名 persona。它们出现在设置卡的 seed 徽标(id 不可改)与 `/fallbacks config` 的角色摘要中,可直接被 `roles.rules` 引用。
182
+ 插件内置 **5 个通用子代理角色**,开箱即用——`reviewer` / `scout` / `security-reviewer` / `sonic` / `task`——`apply` 时自动以 seeded `roles.list` 行(`{ id, persona }`)声明:幂等,且绝不覆盖 operator 同名 persona。它们以只读行的形式出现在设置卡中(每行带来源徽标,id 与 persona 均不可在卡内编辑),并出现在 `/fallbacks config` 的角色摘要中,可直接被 `roles.rules` 引用。(`designer` 与 `librarian` 不再内置:早期版本保存的行保留其 persona,但现在显示 source `user`——尽管 operator 并未写过这些行。)
181
183
 
182
184
  - **开关**:`fallbacks.presets`——`'bundled'`(默认)在 apply 时声明预设角色;`'none'` 关闭自动声明(已物化行保留)。
183
185
  - 完整语义(升级行为、冲突处理、`presetRoles` 库复用)→ [docs/configuration.md](docs/configuration.md)。
184
186
 
187
+ ## 降级触发码(`triggerCodes`)
188
+
189
+ `fallbacks.triggerCodes` 是一份 dsh 失败码列表,**任何**宿主可上报的失败码都被接受——不限于三个默认值(`AUTH` / `QUOTA` / `RATE_LIMIT`)。未列出的失败码原样透传给 llm-retry 或原始错误,与未安装插件完全一致。可重试型失败(5xx / `TRANSPORT` / `TIMEOUT` / `EMPTY_RESPONSE`)无需额外条目:llm-retry 先行退避,预算耗尽后同样进入链决策。
190
+
191
+ ### 上下文窗口超限(context-window)
192
+
193
+ 请求超出模型上下文窗口时,上报的失败码是 `CONTEXT_WINDOW_EXCEEDED`——dsh 内核对 provider 400 "maximum context length" 的规范码。它不是可重试码,需显式列出,才能降级到上下文更大的模型:
194
+
195
+ ```yaml
196
+ fallbacks:
197
+ enabled: true
198
+ triggerCodes:
199
+ - AUTH
200
+ - QUOTA
201
+ - RATE_LIMIT
202
+ - CONTEXT_WINDOW_EXCEEDED # 请求装不下时降级
203
+ rootChain:
204
+ - anthropic/claude-3-5-sonnet # 先走
205
+ - deepseek-official/deepseek-v4-flash # 兜底(Flash 或 Pro)
206
+ ```
207
+
208
+ **顺序——降级先于压缩。** 只要 `triggerCodes` 含 `CONTEXT_WINDOW_EXCEEDED`,本插件的 `agent/request-error` 监听就会**先于**宿主压缩插件处理该拒绝:首次超限即把会话切到 fallback 模型,上下文完全不会被压缩。若更希望先尝试压缩,就不要把该码列入 `triggerCodes`。
209
+
210
+ **请求级切换。** 上下文超限说明**这一次请求**太大,而不是路由不健康。此类切换是**请求级(request-scoped)**:from 路由仍记入本 step 的失败集合(本 step 不会立刻弹回该模型)、仍计入 `maxSwitchesPerStep`,但**不**进入 `cooldownMs` 冷却、也不累加半开恢复计数器——下一条更短的提示词照常走主模型,不会在从未故障的路由上浪费一次半开探针。其余触发码一律保持**路由级(route-scoped)**(冷却 + 恢复记账),与既有行为一致。
211
+
212
+ **装不下的候选被跳过。** 上下文超限走链时,凡是已知上下文窗口不大于失败模型的候选一律跳过——从 128k 模型降到 8k 只会再失败一次。窗口取自宿主模型目录:目录行自带 `contextWindow` 时取之,否则取 `llm.resolveModelInfo(provider, model)` 的 `context.contextWindow`。两处都未公开窗口的模型保留为候选,因此缺少容量元数据的 provider 绝不会把链清空。被跳过的候选在切换日志行中标注 `skipped: context-window`。
213
+
185
214
  ## 宿主子代理模型策略(dsh 0.1.2)
186
215
 
187
216
  dsh 0.1.2 为 subagent 增加了宿主侧的子模型选择:`subagent-model-selection` 设置允许列表、每会话的 `subagent/model-selection-policy` 事件、派发时的 `provider/model/reasoning_effort` 路由。插件在单一运行时仲裁点下与之协调——插件的角色/链仍是宿主不提供的失败恢复层。
package/dist/chains.d.ts CHANGED
@@ -27,7 +27,11 @@
27
27
  * direct-call default.
28
28
  *
29
29
  * Cooldown / failed-set / same-as-current filtering is caller-side;
30
- * {@link createCandidateFilter} provides the ready-made predicate.
30
+ * {@link createCandidateFilter} provides the ready-made predicate. A
31
+ * `CONTEXT_WINDOW_EXCEEDED` walk adds one more caller-side dimension to the
32
+ * same predicate — candidates whose disclosed context window is not larger
33
+ * than the failing model's cannot fit the request either
34
+ * ({@link fitsContextWindow}; unknown windows keep the candidate).
31
35
  *
32
36
  * @module dsh-llm-fallbacks/chains
33
37
  */
@@ -134,15 +138,30 @@ export interface CandidateFilterOptions {
134
138
  failed: Pick<StepFailureSet, 'has'>;
135
139
  /** Optional existence probe: candidates the target provider lacks are skipped. */
136
140
  modelExists?: (provider: string, model: string) => boolean;
141
+ /**
142
+ * The failing model's context window, when known — set only for a
143
+ * `CONTEXT_WINDOW_EXCEEDED` walk. Candidates whose own known window is not
144
+ * LARGER than this cannot fit the request either and are skipped.
145
+ */
146
+ minContextWindow?: number;
147
+ /** Context-window lookup for a candidate route; `undefined` = not disclosed. */
148
+ contextWindowOf?: (provider: string, model: string) => number | undefined;
137
149
  }
138
150
  /**
139
151
  * The caller-side candidate filter (Task 3): a candidate is usable when it
140
152
  * differs from the current model, is not cooldown-suppressed, has not failed
141
- * in this step, and (when `modelExists` is given) exists on its provider.
153
+ * in this step, can fit the request that overflowed (context-window walks
154
+ * only — see {@link fitsContextWindow}), and (when `modelExists` is given)
155
+ * exists on its provider.
142
156
  */
143
157
  export declare function createCandidateFilter(options: CandidateFilterOptions): (candidate: Selector) => boolean;
144
- /** Why one considered candidate was excluded from the selection (spec §2 行为可见性). */
145
- export type CandidateSkipReason = 'same-as-current' | 'cooldown' | 'step-failed' | 'missing-id';
158
+ /**
159
+ * Why one considered candidate was excluded from the selection (spec §2
160
+ * 行为可见性). `context-window` is the `CONTEXT_WINDOW_EXCEEDED` walk's
161
+ * exclusion: the candidate's disclosed window is not larger than the failing
162
+ * model's, so the request that just overflowed cannot fit there either.
163
+ */
164
+ export type CandidateSkipReason = 'same-as-current' | 'cooldown' | 'step-failed' | 'context-window' | 'missing-id';
146
165
  /** One entry of the ordered, per-candidate annotation (T3 review Minor 1). */
147
166
  export interface AnnotatedCandidate {
148
167
  candidate: Selector;
@@ -164,4 +183,4 @@ export interface AnnotatedCandidate {
164
183
  * This is the diagnostic counterpart of {@link createCandidateFilter} — pure,
165
184
  * order-preserving, and duplicate-preserving.
166
185
  */
167
- export declare function annotateCandidates(candidates: readonly Selector[], surviving: readonly Selector[], options: Pick<CandidateFilterOptions, 'current' | 'cooldown' | 'failed'>): AnnotatedCandidate[];
186
+ export declare function annotateCandidates(candidates: readonly Selector[], surviving: readonly Selector[], options: Pick<CandidateFilterOptions, 'current' | 'cooldown' | 'failed' | 'minContextWindow' | 'contextWindowOf'>): AnnotatedCandidate[];
@@ -0,0 +1,23 @@
1
+ import type { ReactNode } from 'react';
2
+ import type { PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
3
+ import type { FallbacksSettingsController } from './fallbacks-store.ts';
4
+ /**
5
+ * Injected dependencies of {@link SubagentRoleBadge} (slot `inject`): the
6
+ * shared controller, the same seam the settings card and the General row use.
7
+ * The badge reads through {@link FallbacksSettingsController.fetchSubagentRole}
8
+ * — the controller's rpc face (the same `/api` caller the store rides); no
9
+ * second channel, no extra inject member.
10
+ */
11
+ export interface SubagentRoleBadgeInjected {
12
+ /** The shared controller (its `fetchSubagentRole` rides the gateway channel). */
13
+ controller: FallbacksSettingsController;
14
+ }
15
+ /** Props delivered by the utilities slot outlet: runtime share + locale seat + inject face. */
16
+ export type SubagentRoleBadgeProps = PropsRuntime<'conversation.session.header.utilities'> & PropsLocale<'fallbacks'> & SubagentRoleBadgeInjected;
17
+ /**
18
+ * Render the viewed session's dispatch-resolved role badge.
19
+ * @param props - composed slot props (the `sessionId` session-kit seat is
20
+ * read structurally — see the module docblock).
21
+ * @returns the badge element, or `null` when there is nothing to show.
22
+ */
23
+ export declare function SubagentRoleBadge(props: SubagentRoleBadgeProps): ReactNode;
@@ -169,6 +169,47 @@ export type SubagentPolicyView = {
169
169
  * throws on an old or misshapen field (old-payload tolerance).
170
170
  */
171
171
  export declare function parseSubagentPolicyWire(value: unknown): SubagentPolicyView | undefined;
172
+ /**
173
+ * One dispatch-resolved subagent role record as the session-header badge
174
+ * reads it (plan subagent-role-badge T3) — a structural twin of the gateway
175
+ * wire type, parsed from unknown, never imported from host modules
176
+ * (`src/client/**` consumes the gateway channel only — the
177
+ * `SubagentPolicyView` precedent). `model` is optional on the wire (gateway
178
+ * version skew); the writer always sets it.
179
+ */
180
+ export interface SubagentRoleView {
181
+ /** The dispatch-resolved role id (never `inherit` — the writer skips it). */
182
+ role: string;
183
+ /** The route the subagent actually runs (override target, else the seed). */
184
+ model?: {
185
+ provider: string;
186
+ model: string;
187
+ };
188
+ /** Record time, Unix epoch milliseconds. */
189
+ at: number;
190
+ }
191
+ /**
192
+ * Shape-guard one wire role record (`fallbacks/subagent-roles` readback).
193
+ * A non-object, a blank/non-string `role`, or a non-number `at` fails the
194
+ * WHOLE record (`undefined` → the badge renders nothing). A present-but-
195
+ * malformed `model` drops the FIELD while the record still parses (the
196
+ * `parseSeedsWire` `source` rule) — the badge then hovers the role alone
197
+ * instead of disappearing. Records are NORMALIZED to the known keys, so a
198
+ * wire record can never smuggle a non-string `model` past the guard's type.
199
+ */
200
+ export declare function parseSubagentRoleRecord(value: unknown): SubagentRoleView | undefined;
201
+ /**
202
+ * Read this session's dispatch-resolved role record through the plugin
203
+ * gateway channel (plan subagent-role-badge T3): the batch-of-ids endpoint
204
+ * called with the one id the badge cares about. NEVER throws — a channel
205
+ * down, an old gateway without the endpoint (rpc failure), or a malformed
206
+ * payload all resolve `undefined` (the badge renders nothing —
207
+ * degrade-never-crash, the fail-closed shape of `parseSubagentPolicyWire`).
208
+ * @param rpc - the connection's generic RPC caller (`/api` channel).
209
+ * @param sessionId - the session whose record to read.
210
+ * @returns the parsed record, or `undefined` when absent/failed.
211
+ */
212
+ export declare function fetchSubagentRoleRecord(rpc: ClientConnectionRpc, sessionId: string): Promise<SubagentRoleView | undefined>;
172
213
  /**
173
214
  * The provider dropdown's offer set (spec §2.5 D-4): catalog providers whose
174
215
  * settings profile resolves in the describe namespaces — the Models page's
@@ -308,6 +349,13 @@ export interface RoleRow {
308
349
  fallback: FallbackStrategy;
309
350
  /** UI-only collapse state — never serialized (dropped by rowsToRoles). */
310
351
  collapsed: boolean;
352
+ /**
353
+ * UI-only seeded-persona brief disclosure (plan role-card-seeded-ux) —
354
+ * never serialized (dropped by rowsToRoles). Only seeded rows render the
355
+ * affordance (a read-only single-line brief + the expandable full text);
356
+ * ordinary rows ignore the flag.
357
+ */
358
+ personaOpen: boolean;
311
359
  }
312
360
  /** Project the declared roles into editable rows (chain selectors classified). */
313
361
  export declare function rolesToRows(roles: readonly FallbacksRole[], catalog?: CatalogLookup): RoleRow[];
@@ -501,24 +549,17 @@ export declare class FallbacksSettingsController {
501
549
  */
502
550
  resetToDefaults(): Promise<void>;
503
551
  /**
504
- * Revert one seeded role to its CURRENT declared seed default (spec §9.4,
505
- * AC-3) through the gateway channel (`/api/fallbacks/revert-seed`). Same
506
- * write guards as {@link save} — writable / saving / write-generation —
507
- * and the same KD-G3 error handling: any business rejection or transport
508
- * failure surfaces its message in `state.error` for the error banner and
509
- * the form stays editable for retry. A business `{ reverted: false,
510
- * reason }` outcome is still a successful RPC — the post-write read
511
- * result (config / legacyKeys / seeds) lands either way, and the revert
512
- * button stays disabled while the write is in flight.
513
- *
514
- * Returns the seed-default persona when the outcome is `{ reverted:
515
- * true, persona }` — including the persist no-op (persisted already
516
- * equals the seed). The card applies that string to the row's **draft**
517
- * so an unsaved persona edit still snaps back (issue #59).
518
- * @param id - the seeded role id; the host matches it by trimmed id
519
- * against the seed registry (spec §9.3).
552
+ * Read one session's dispatch-resolved subagent role record through the
553
+ * gateway channel (plan subagent-role-badge T3 — the session-header badge's
554
+ * data face). Thin delegate over {@link fetchSubagentRoleRecord} on the
555
+ * SAME rpc caller the store rides: the batch-of-ids readback called with
556
+ * the one id the badge cares about. Never throws — a channel down, an old
557
+ * gateway without the endpoint, or a malformed payload all resolve
558
+ * `undefined` (the badge renders nothing).
559
+ * @param sessionId - the session whose record to read.
560
+ * @returns the parsed record, or `undefined` when absent/failed.
520
561
  */
521
- revertSeed(id: string): Promise<string | undefined>;
562
+ fetchSubagentRole(sessionId: string): Promise<SubagentRoleView | undefined>;
522
563
  /** Stop in-flight responses from publishing after plugin disposal. */
523
564
  dispose(): void;
524
565
  /**
@@ -36,6 +36,7 @@ import type { Context as ClientContext } from '@deepseek-ai/cordis';
36
36
  export type { FallbacksCardInjected, FallbacksCardProps } from './FallbacksCard.tsx';
37
37
  export type { GeneralFallbacksRowInjected, GeneralFallbacksRowProps } from './GeneralFallbacksRow.tsx';
38
38
  export type { ConversationFallbackSwitchProps, FallbacksSwitchChatData, } from './ConversationFallbackSwitch.tsx';
39
+ export type { SubagentRoleBadgeInjected, SubagentRoleBadgeProps } from './SubagentRoleBadge.tsx';
39
40
  export type { FallbacksSettingsState } from './fallbacks-store.ts';
40
41
  export { FallbacksSettingsController, FALLBACKS_SETTINGS_NS } from './fallbacks-store.ts';
41
42
  /**