dsh-llm-fallbacks 0.6.5 → 0.6.7

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
@@ -1,22 +1,35 @@
1
1
  # dsh-llm-fallbacks
2
2
 
3
- [English](README.md) | [中文](README.zh-CN.md)
3
+ [English](README.md) | [Chinese](README.zh-CN.md)
4
4
 
5
5
  [![npm](https://img.shields.io/npm/dt/dsh-llm-fallbacks)](https://www.npmjs.com/package/dsh-llm-fallbacks)
6
6
  [![license](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
7
7
  ![node](https://img.shields.io/badge/node-%3E%3D22-339933.svg)
8
8
  ![pnpm](https://img.shields.io/badge/pnpm-%3E%3D10-f69220.svg)
9
9
  ![dsh tui](https://img.shields.io/badge/dsh%20tui-compatible-4B32C3.svg)
10
- ![dsh](https://img.shields.io/badge/DSH-0.2.0--rc.2-4B32C3.svg)
10
+ ![dsh](https://img.shields.io/badge/DSH-0.2.1--rc.2-4B32C3.svg)
11
11
  [![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)
12
12
 
13
13
  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.
14
14
 
15
15
  Works in both dsh front ends: the **web** profile (Settings → Plugins → Fallbacks card) and the **dsh-tui** terminal profile (`/fallbacks` session diagnostics, `/fallbacks config` readback, and the `/settings` fallbacks section for editing).
16
16
 
17
- ## Time slots
17
+ ## Three core capabilities
18
18
 
19
- 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
+ ### Automatic model failover
20
+
21
+ Any agent — root or subagent — that keeps failing on a listed failure code walks its chain to the next available provider/model and keeps the step alive. No manual model switching, and nothing happens until you configure content.
22
+
23
+ - **Triggers**: `triggerCodes` (default `AUTH` / `QUOTA` / `RATE_LIMIT`) — **any** dsh failure code is accepted, including `CONTEXT_WINDOW_EXCEEDED`, which fails over to a larger-context candidate. Retryable failures (5xx, `TRANSPORT`, `TIMEOUT`, …) stay with llm-retry first.
24
+ - **Recovery**: a failed / switched-away model is held out of selection for `cooldownMs`, and `revertPolicy: cooldown-expiry` returns to the primary model on expiry. The opt-in `recovery: half-open` mode (YAML-only) makes that return evidence-driven instead of timer-based.
25
+ - **Safety valves**: `maxSwitchesPerStep` caps switches per step and `alwaysModeRetryCap` caps always-mode retries — chain loops cannot amplify latency.
26
+ - **Visibility**: every switch is recorded in an info-level log line (from/to/role/reason) — no silent model switching.
27
+
28
+ → [Automatic model failover](docs/features.md#automatic-model-failover) · [Context-window-aware fallback](docs/features.md#context-window-aware-fallback)
29
+
30
+ ### Peak/valley time slots
31
+
32
+ Time slots rotate the **effective root chain** by wall-clock windows: each 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 the last resort when no row matches. Rotation is a routing seed, not a failure decision: it consumes no cooldown and leaves the failure walk (fallback switch) untouched.
20
33
 
21
34
  ![Time slots](docs/assets/screenshot-1-en.png)
22
35
 
@@ -29,9 +42,19 @@ Four frozen UTC+8 presets (windows are code constants; preset rows lock `tz` to
29
42
  | `glm-peak` | Monday–Friday 14:00–18:00 |
30
43
  | `glm-valley` | every other time (complement of GLM Peak) |
31
44
 
32
- GLM Peak and GLM Valley are offered in the card picker only when `zai-coding-cn` is configured.
45
+ GLM Peak and GLM Valley are offered in the card picker only when `zai-coding-cn` is configured. Custom rows take `start` / `end` (`HH:mm`, may wrap midnight) and an optional `days` list.
46
+
47
+ → [Time slots](docs/features.md#time-slots)
48
+
49
+ ### Role-based subagents
50
+
51
+ A subagent's role is declared in its dispatch prompt — the Assignment header `**Execute as**: <id>` — and it is resolved against the role ids declared in `roles.list`. Only that **declared header** delivers the child's identity: the role's `persona` is installed on the child as a system-prompt section, the child's own session gets exactly one `[role: <id>]` notice row, and its session header shows a role badge.
52
+
53
+ Role **routing** is resolved separately, in four ordered stages, first hit winning — explicit `agentPreset` → **dispatch-declared** `**Execute as**: <id>` header → rules → LLM auto-match. If the child's dispatch prompt declares `**Execute as**: <id>`, that role's persona, `[role: <id>]` notice and badge are delivered **regardless of which routing stage wins** — an earlier explicit `agentPreset` can route the child as another role while the identity carriers stay the declared header's. A child with **no declared dispatch header** gets no persona, notice row or badge, even when rules or auto-match resolve a role for routing. Making the resolved role's chain head the child's model is plugin-originated and conditional: with the host `subagent-model-selection` policy enabled, an explicit authorized route is preserved (inject skipped) and an injected head must be inside the effective allowlist — an empty intersection injects nothing ([host policy](docs/features.md#host-subagent-model-selection-policy-dsh-012)).
54
+
55
+ This covers an **ordinary subagent dispatch and an Agent Team teammate** (`spawn_teammate`) alike: both are children dispatched with the same header, which the model writes — the same dispatch seam, no mechanism-specific configuration.
33
56
 
34
- 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 model — `deepseek-official/deepseek-flash` or `deepseek-official/deepseek-v4-pro` (XOR). 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).
57
+ → [Role-based subagents](docs/features.md#role-based-subagents)
35
58
 
36
59
  ## Quick start
37
60
 
@@ -42,9 +65,9 @@ dsh plugin --profile web add dsh-llm-fallbacks # web profile (Settings →
42
65
  dsh plugin --profile dsh-tui add dsh-llm-fallbacks # dsh-tui terminal profile
43
66
  ```
44
67
 
45
- Same plugin, either front end — the only difference is the `--profile` flag. Pin a version with `@<version>`. A registry install fetches the **built package** (`dist/`), nothing builds on the target machine. Registry / git / local-directory variants, uninstall, and `--dump-config` verification → [docs/install.md](docs/install.md). The plugin ships **localized metadata** (title/description for the Plugins-page detail view, English + 简体中文) and an **icon** — both declared in the package (`locale/*.json` + `icon.svg`) and picked up by the host automatically.
68
+ Same plugin, either front end — the only difference is the `--profile` flag. Pin a version with `@<version>`. A registry install fetches the **built package** (`dist/`), nothing builds on the target machine. Registry / git / local-directory variants, uninstall, and `--dump-config` verification → [docs/install.md](docs/install.md). The plugin ships **localized metadata** (title/description for the Plugins-page detail view, English + Simplified Chinese) and an **icon** — both declared in the package (`locale/*.json` + `icon.svg`) and picked up by the host automatically.
46
69
 
47
- ### Configuration surfaces
70
+ ### Configure
48
71
 
49
72
  The plugin's settings live in a shared `fallbacks:` namespace, editable from three surfaces:
50
73
 
@@ -52,254 +75,57 @@ The plugin's settings live in a shared `fallbacks:` namespace, editable from thr
52
75
  |---|---|---|
53
76
  | **Web settings card** | Settings → Plugins → Fallbacks | Full GUI editor for the `fallbacks:` namespace; writes the shared settings document |
54
77
  | **`$DSH_HOME/settings.yaml`** | `fallbacks:` section in the dsh settings document | The shared source of truth — the same file the web card writes; readable and editable everywhere, including scripted setups |
55
- | **TUI `/settings`** | fallbacks section in the dsh-tui settings screen | dsh-tui ≥ v0.8.5; native fields for simple keys, JSON text fields for complex structures (see [dsh-tui profile (terminal)](#dsh-tui-profile-terminal)) |
78
+ | **TUI `/settings`** | fallbacks section in the dsh-tui settings screen | dsh-tui ≥ v0.8.5; native fields for simple keys, JSON text fields for complex structures |
56
79
 
57
- Pick the surface that matches your front end: web users get the card, terminal users get `/settings`, and the YAML file works everywhere. (`/fallbacks` and `/fallbacks config` are diagnostics — read-only views, not edit surfaces.)
58
-
59
- ### Minimal configuration
60
-
61
- Add a `fallbacks:` section to the shared settings document (`$DSH_HOME/settings.yaml` — see [Configuration surfaces](#configuration-surfaces)):
80
+ (`/fallbacks` and `/fallbacks config` are diagnostics — read-only views, not edit surfaces.) A minimal section:
62
81
 
63
82
  ```yaml
64
83
  fallbacks:
65
84
  rootChain: # all-day chain: leading entries = fallback walk, last = Default model (official)
66
- - anthropic/claude-3-5-sonnet # walked first
67
- - deepseek-official/deepseek-flash # last resort (Flash or Pro)
85
+ - anthropic/claude-3-5-sonnet
86
+ - deepseek-official/deepseek-flash
68
87
  timeSlots: # optional: rotate the effective root chain by wall-clock windows
69
- - kind: preset # frozen UTC+8 window; only the chain is editable
70
- preset: liang-peak # Monday–Friday 09:00–12:00 and 14:00–18:00
88
+ - kind: preset
89
+ preset: liang-peak
71
90
  chain:
72
91
  - anthropic/claude-3-5-sonnet
73
- - kind: custom # custom window (may wrap midnight)
74
- name: evening # optional display name
75
- start: '22:00'
76
- end: '02:00'
77
- days: [1, 5] # optional; omitted/empty = every day (0=Sunday…6=Saturday)
78
- chain:
79
- - openai/gpt-4o
80
92
  roles: # optional: declare role entities, then reference them from rules
81
93
  list:
82
- - id: reviewer # unique id; "inherit" is reserved
94
+ - id: reviewer
83
95
  persona: Code-review subagents
84
96
  chain:
85
97
  - openai/gpt-4o-mini
86
- fallback: inherit-root # role chain first, then the inherited rootChain
98
+ fallback: inherit-root
87
99
  rules: # subagent-only: rules never match root requests
88
- - role: reviewer # all subagents → the reviewer role
100
+ - role: reviewer
89
101
  ```
90
102
 
91
- Build the section up in four steps:
92
-
93
- **1. Configure content.** There is **no feature switch** in the section — the plugin intervenes exactly when something is configured: a non-empty `rootChain`, at least one `timeSlots` row, or one declared role. An empty section is a complete no-op, and the Plugins-page row toggle is the master switch (disabling the plugin row stops it entirely).
94
-
95
- **2. Set the all-day `rootChain`.** Leading entries are the fallback chain, walked first when a request fails; the **last** entry is the Default model.
96
-
97
- > **Conformance**: the last entry must be exactly one official model — `deepseek-official/deepseek-flash` or `deepseek-official/deepseek-v4-pro` (XOR). The settings card and gateway reject any other tail on save; a legacy non-official tail warns at startup and keeps working as a fallback-only walk, but cannot be saved as-is. The retired `deepseek-v4-flash` id is no longer a legal tail — a saved tail with it now warns, goes inert (slot rows + virtual picker), and blocks save until a legal tail is picked. The official Pro tail is catalog-served as `deepseek-official/deepseek-v4-pro` (dsh 0.2.0-rc.2) and selectable in the card; a hand-written legacy `deepseek-official/deepseek-pro` tail — never catalog-served — reads back as non-conforming (no migration; re-pick the tail). The plugin does not probe catalog availability — a chain containing `deepseek-official/deepseek-v4-pro` dispatches to it like any other exact entry, and on the virtual route the `stream()` delegate serves the first dispatchable exact head of the effective chain, so a chain with a working entry before Pro still routes to that earlier entry.
103
+ Three rules of thumb:
98
104
 
99
- **3. Add `timeSlots` (optional).** Rows rotate the effective root chain by wall-clock windows. Preset rows use frozen UTC+8 windows (only their chain is editable; while a preset row exists, `tz` locks to `Asia/Shanghai`); custom rows take `start`/`end` (may wrap midnight) and an optional `days` list. The first row whose window contains the current moment wins; no match → the all-day `rootChain`. Rotation is a routing seed — it applies on the next root request and consumes no cooldown (see [Time slots](#time-slots)).
105
+ - **There is no feature switch**: the plugin intervenes exactly when content is configured — a non-empty `rootChain`, at least one `timeSlots` row, or one declared role. An empty section is a complete no-op, and the Plugins-page row toggle is the master switch.
106
+ - **The all-day `rootChain` tail must be exactly one official model** — `deepseek-official/deepseek-flash` **or** `deepseek-official/deepseek-v4-pro` (XOR); the leading entries are the fallback walk, walked first.
107
+ - **Slots and roles are optional**: the first time-slot row whose window matches replaces the all-day chain for the next root request; rules map subagents to declared roles and never match root requests.
100
108
 
101
- **4. Add `roles` (optional).** Declare role entities in `roles.list` (id, persona, chain, optional `fallback` policy), then map subagents to them with `roles.rules`. Rules never match root requests — with no rule match (or on a root request) the built-in `inherit` role applies and appends the `rootChain`.
109
+ Full reference (keys, entry syntax, selector rules, migration) → [docs/configuration.md](docs/configuration.md).
102
110
 
103
- Full reference (role entities, fallback strategies, rules, selectors, preset roles, time-slot presets) → [docs/configuration.md](docs/configuration.md).
104
-
105
- > **Upgrade note (breaking, behavior change)**: the config-level `enabled` switch is **removed** — a stored section still carrying `enabled: true/false` loads cleanly (the key is ignored and stripped on the next save), but the switch no longer gates anything. If you relied on `enabled: false` to keep the plugin inert, empty the section (or disable the plugin row on the Plugins page) instead.
111
+ > **Upgrade note (breaking)**: the config-level `enabled` switch is **removed** — a stored section still carrying `enabled: true/false` loads cleanly (the key is ignored and stripped on the next save), but the switch no longer gates anything. If you relied on `enabled: false` to keep the plugin inert, empty the section (or disable the plugin row on the Plugins page) instead.
106
112
 
107
113
  ### Verify
108
114
 
109
- Save the config and restart the session, then type `/fallbacks` — the read-only in-session diagnostics (origin, resolved role, chain, recent fallback switches, cooldown status). In a dsh-tui profile, `/fallbacks config` reads back the composed configuration; see [dsh-tui profile (terminal)](#dsh-tui-profile-terminal).
110
-
111
- ## Repair existing sessions
112
-
113
- A session log reaches the GUI only through the **frozen released migration chain**. That chain throws at the first row it cannot classify, so a pre-V3 log written by an older release — or by any plugin that merged a custom message-source kind — makes the session fail to load, and the bytes never change on their own. Two classes cover nearly every case, and both are the same kind of root cause: content outside what a released format edge admits.
114
-
115
- | Class | What the log carries | Root cause |
116
- |---|---|---|
117
- | `source-kind` | a message `source.kind` outside the released vocabulary | the vocabulary is frozen **per format edge**, so a plugin that merged a custom kind into `MessageSourceMap` leaves the V2→V3 edge refusing the log (`cannot safely transform unclassified message source`). The released kinds are `user`, `plugin`, `model`, `tool`, `agent-instructions`, `session-reference`, `team-message`, `goal`, `skill-invocation`, `skill-catalog`, `coordinator`, `subagent-report`, `subagent-settled`, `webhook`, `agent-message` |
118
- | `subagent-descriptor-version` | a `subagent/descriptor` row with `version: 2` | the V0→V1 edge admits only descriptor `version: 3`, and dsh releases `v0.1.0-rc.7` … `v0.1.1-rc.2` wrote version 2 |
119
-
120
- A third class — the legacy `fallbacks/switch` event type — is **not** repairable by a rewrite: see [Lossy recovery for `fallbacks/switch`](#lossy-recovery-for-fallbacksswitch-opt-in).
121
-
122
- **If you author a plugin:** through dsh 0.1.6 the persisted source vocabulary was frozen per format edge, and the sanctioned arm was `{ kind: 'plugin', plugin: '<stable-id>', form: … }`. Since dsh 0.1.7-rc.1 that vocabulary is producer-declared instead ("each producer declares its own `kind` in its own module" — dsh's own `model-selection` notice writes `{ kind: 'model-selection', form: 'notice', … }`), and the one rule a released edge enforces on a direct kind is that it is **non-empty**. Old logs written against the frozen vocabulary are exactly what this tool's `source-kind` class repairs.
123
-
124
- ### Usage
125
-
126
- The tool is run from this repository (clone + `pnpm install`). Its sources do ship in the npm tarball, but there is no runnable entry there — no `bin` is registered and `tsx` is a dev dependency — so a registry install cannot run it. It is a **read-only report by default**: it walks a session root, classifies every pre-V3 log into exactly one refusal class and prints per-class counts. Nothing is written unless `--apply` is given.
127
-
128
- ```sh
129
- git clone https://github.com/omdsh-dev/dsh-llm-fallbacks.git
130
- cd dsh-llm-fallbacks
131
- pnpm install
132
- pnpm repair:session-logs # read-only report (the default)
133
- pnpm repair:session-logs -- --root ~/.dsh/sessions # explicit session root
134
- pnpm repair:session-logs -- --apply # publish a repaired successor generation
135
- ```
136
-
137
- `--dry-run` no longer exists: the report **is** the default mode, and `--dry-run` is rejected as an unknown argument (exit 2).
138
-
139
- | Flag | Meaning |
140
- |---|---|
141
- | `--root DIR` | session root to walk (default: `$DSH_HOME` or `~/.dsh`, then `/sessions`) |
142
- | `--apply` | run the rules' proofs and publish a current-generation successor beside each repaired original (requires a resolved released catalog) |
143
- | `--class NAME` | restrict what `--apply` repairs to one refusal class; the listing, the class table and the exit code still cover every log, and the repairable verdict is always computed over the **full** policy, so it can never hide a refusal or promise a repair this invocation cannot perform |
144
- | `--catalog PATH` | explicit released catalog path (package directory, a directory holding it, or its module entry file). The resolved module is **executed**, not parsed — the same privilege as running this tool — and must declare a current format version ≥ 3; a below-version catalog, or a file candidate owned by a different package, is refused (exit 2) rather than trusted to verify its own output |
145
- | `--backup` | copy the original generation to `<name>.bak` before publishing; a run that publishes nothing removes the copy it created again (a pre-existing one is never touched, and one holding different bytes blocks the repair until you inspect and delete it) |
146
- | `--drop-legacy-events` | opt-in **lossy** recovery for legacy `fallbacks/switch` rows (see below) |
147
- | `--json` | machine-readable report instead of the text report |
148
- | `--quiet` | suppress the per-log lines and the by-class table (warnings and errors are never suppressed) |
149
- | `--help`, `-h` | print the usage text (including the `--apply` precondition) and exit 0 |
150
-
151
- The original generation is never modified and never truncated; rollback is deleting the published successor, which makes the original the generation dsh opens again.
152
-
153
- **`--apply` precondition: run it only while NO dsh instance is writing the sessions under `--root`.** The publisher links the successor into the session directory **without observing the host's flock lease** (that lease is host-internal and cannot be taken from this repo), so a dsh that is still appending to the old generation would be orphaned once the host prefers the successor. Stop dsh first.
154
-
155
- **Runtime floor: Node ≥ 22.15.** Reading and writing need `node:zlib` zstd, which that release added (`engines.node` allows `>= 22`); an older runtime fails closed with an actionable message instead of a module-link stack trace.
156
-
157
- **Two policies, and `ok-truncated`.** A log's class and its `ok` come from the **host loader's** policy — the one that decides whether the GUI opens the session — but that policy can swallow a refusal and drop every row after it. Every `ok` log is therefore cross-checked with the **same loader policy under strict recovery** (one axis apart, so a refusal there means rows were dropped rather than "not current-shaped"): when it refuses, the log is reported **`ok-truncated`** (a `strictRefusal` reason in `--json`, an `ok-truncated` count in the summary) because the session opens **without** the rows that refusal swallowed. Such sessions still exit `0` — they do load.
158
-
159
- **Discovery never fails open.** A namespace/session directory that cannot be read, a canonical generation **below the format floor** that is a symlink or not a regular file, and a leftover `session.repair.*.jsonl.zstd.tmp` are **reported** (a `skipped`/`staleStagingFiles` entry, always printed and never hidden by `--quiet`), suppress the "no session log …" line, and make the run exit `1`. Symlinks are reported, never followed: a repair writes beside the generation it repairs, which must stay inside `--root`. An unreadable `--root` is fatal (exit `2`) — never an empty report.
160
-
161
- **`--apply` refuses a revision it did not decode.** The digest of the bytes this run read is compared with the file before anything is staged, so a concurrent append is refused before the first write. If the source moves *after* a publication this run created, that successor is removed again; if it moves after accepting a pre-existing identical successor, the failure names that file and says to delete it — never "nothing was published".
162
-
163
- **A closed report stream is not a store event.** Piping the report into a consumer that stops reading (`pnpm repair:session-logs -- | head -n 5`) closes stdout, and the next write would otherwise raise an unhandled `EPIPE`. Instead the tool stops writing to that stream (with no stack trace), **still finishes the run** — an `--apply` pass publishes every remaining successor — and exits with the exit code below, which always describes what happened to the **store**, never whether the reader stayed. A stream error that is not a closed pipe (a full disk, an I/O failure) is **not** swallowed: it still surfaces as an uncaught error.
164
-
165
- **Exit codes**: `0` = every log loads (a session that opens with rows dropped under the strict policy is `ok-truncated` and still exits `0`); `1` = at least one log is still refused/unrepairable, a repair failed, a log was left unpublished (including one `--class` excluded), an input could not be inspected (a `skipped` path), or a stale `session.repair.*.jsonl.zstd.tmp` was found under the root; `2` = fatal (bad arguments, a missing or unreadable `--root`, `--apply` without a resolved catalog, a catalog below format v3, `--apply --drop-legacy-events` without `--backup`, or a runtime without `node:zlib` zstd).
166
-
167
- ### Lossy recovery for `fallbacks/switch` (opt-in)
168
-
169
- Versions before 0.2.2 wrote durable `fallbacks/switch` session events (issue #52: the apply()-time event-type registration is ineffective because plugin and host resolve different module instances). The frozen V0→V1 edge refuses that event type **even when the row carries `ignorable: true`**, so no rewrite can make such a row load — removal is the only in-repo recovery. Opting in means accepting two consequences:
170
-
171
- 1. **The session's recorded provider/model switch audit rows are deleted.** They survive only in the original generation (and in its `.bak` when `--backup` is used): the published successor is the only readable generation that lacks them.
172
- 2. **The surviving events are renumbered.** The same edge requires every event's `seq` to equal its running event count, so each surviving event receives the `seq` of its position in the surviving event stream and every Session-seq reference it carries is shifted with it. Their content is otherwise unchanged, byte for byte.
173
-
174
- ```sh
175
- pnpm repair:session-logs -- --drop-legacy-events # report: count what would be dropped
176
- pnpm repair:session-logs -- --drop-legacy-events --apply --backup # --backup is required here
177
- ```
178
-
179
- The mode is **off by default**, report mode writes nothing, and `--apply --drop-legacy-events` **requires `--backup`** (exit 2 without it). It fails closed, writing nothing, when a surviving row references a seq that would be dropped, when the source rows are not densely numbered, or when the renumber would cross the header's seed cut. `--json` reports `legacyEventCount` (legacy rows in the source log), `droppedEventCount`, `renumberedEventCount` and a machine-readable `lossyRefusal` reason per log; text runs print the dropped/renumbered counts as a loud warning. A row of any *other* unknown event type is never dropped — such a log stays unrepairable. Without the flag those sessions stay unreadable by design, with their bytes preserved.
180
-
181
- The durable fix belongs upstream at the migration edges (the frozen V0→V1 edge admitting the descriptor versions it shipped, and custom message-source kinds moving to the `plugin` arm).
182
-
183
- ## Features
184
-
185
- - **Automatic fallback for root and subagents**: any agent switches down the chain to the next available provider/model on model failure — no manual model switching.
186
- - **Two-block config**: `rootChain` for the root agent; declared role entities (`roles.list`) referenced by `roles.rules` (or the built-in `inherit`).
187
- - **Chain as root primary from the picker**: whenever the plugin has content configured, the host model picker (web and TUI alike) shows a virtual `FallbacksChain` / `Auto` row — selecting it uses the configured chain as the root primary (a conforming all-day head is required for the delegation to succeed); selecting a real model keeps fallback-only (see [FallbacksChain in the model picker](#fallbackschain-in-the-model-picker)).
188
- - **Time slots**: optional `fallbacks.timeSlots` rows rotate the effective root chain by wall-clock windows in the config-level `tz` timezone (default `Asia/Shanghai`) — four frozen UTC+8 presets (`liang-peak` / `liang-valley` / `glm-peak` / `glm-valley`, windows are code constants, models-only edits) or custom `start`/`end`/`days` windows. The first matching row wins; the all-day row is always last. A slot change applies on the **next** root request and is logged as a **time-slot switch** — a routing seed, never a failure decision: it consumes no cooldown and does not count against `maxSwitchesPerStep`. Failure walks keep the **fallback switch** copy (see [Time-slot presets](#time-slot-presets)).
189
- - **Dispatch-time role resolution**: on a subagent's first request its role is resolved in three stages — explicit (`agentPreset` matches a declared role id) → deterministic rules (matching either the recorded pair or the served head — see [FallbacksChain in the model picker](#fallbackschain-in-the-model-picker)) → LLM auto-match from the declared role taxonomy (`fallbacks.roleAutoMatch`, default `true`). The resolved role's chain-head model is injected into the first request and recorded via an explicit `role → model` log line (no durable `fallbacks/switch` event is written — issue #52 stop-write); set `roleAutoMatch: false` to disable the LLM auto-match stage (the explicit `agentPreset` stage still applies — with no explicit role this reproduces the previous rules-only behavior). The settings card always renders an **Enable role auto-match** switch (default `true`) to toggle it — the schema default applies even to legacy configs that never declared the key.
190
- - **Role persona on the child (chain-independent)**: when a subagent's dispatch declares a role this plugin knows (the `**Execute as**: <id>` field of its Assignment), that role's `persona` is installed on the child as its own persona — the role is the child's identity, not only a routing decision, and delivery does not depend on routing: a role with an empty `chain` gets its persona exactly like a chained one (the chain only picks the model). A persona the caller already set is never overwritten; a provider that cannot carry a persona skips it and the dispatch runs natively.
191
- - **Role announced inside the subagent's own session**: a child dispatched with a declared role gets exactly one plugin notice row in its own session, naming the role it was dispatched as — `[role: <id>]`, with ` (persona not applied)` when the role declares a persona that was skipped — written once at the child's first non-empty pre-step, never for a root session and never for `inherit`/unresolved dispatches. The row is a durable transcript row, so the role stays readable in the child's session after it settles.
192
- - **Subagent role badge**: when a subagent is **dispatched with a declared role** — the `**Execute as**: <id>` field of its Assignment, the same declaration that carries the persona, and the badge's ONLY source (policy on or off) — its session shows a compact role badge next to the session's agent-preset (mode) chip in the web session header; hover shows `role → latest request route` (the route of the newest recorded request, not the dispatch-time route). Only the DECLARED header is read, so a child whose role came from another stage (`agentPreset` match, a role rule, or the LLM auto-match) but which declares no Assignment header is still routed as that role yet shows **no** badge — the badge never reports a role inferred after the fact. `inherit`/unresolved sessions render no badge. The badge is durable: it is read from the child's own session record, so it is still shown for a **settled** subagent session and **after a host restart**.
193
- - **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)).
194
- - **Cooldown and revert**: failed / switched-away models are not re-selected during cooldown; `revertPolicy: cooldown-expiry` returns to the primary model automatically.
195
- - **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).
196
- - **Half-open recovery (opt-in)**: `recovery: half-open` makes recovery evidence-driven — an expired cooldown leaves the route half-open for one logged probe instead of restoring the preference; consecutive failures escalate the suppression duration (×2 per failure, capped at 1 h); an observed completion closes the circuit and fully restores the preference. `revertPolicy: 'never'` keeps the mechanism inert; state is session-scoped in-memory (a restart resets). YAML-only — the default `timer` keeps every existing behavior byte-identical (see [docs/configuration.md](docs/configuration.md#recovery-mode-recovery-key)).
197
- - **Visible behavior**: every switch is recorded in an info-level log line (from/to/role/reason) — no silent model switching. The plugin deliberately writes **no** durable `fallbacks/switch` session events (issue #52: the apply()-time event-type registration was proven ineffective, and a session containing the event refused to load after a dsh restart). Sessions written by older plugin versions that contain such events **cannot** be repaired by an `ignorable` flag — the released session-format migration chain (v0→v1) refuses unknown event types even when marked ignorable — so `pnpm repair:session-logs` reports them (and recovers them only with the opt-in lossy `--drop-legacy-events` — see [Repair existing sessions](#repair-existing-sessions)).
198
- - **Safety valves**: `maxSwitchesPerStep` caps switches per step and `alwaysModeRetryCap` caps always-mode retries — chain loops cannot amplify latency.
199
- - **No-config no-op**: with nothing configured the plugin behaves exactly like not being installed — content presence is the gate (see [Minimal configuration](#minimal-configuration)).
200
-
201
- ## dsh-tui profile (terminal)
202
-
203
- In a dsh-tui profile the plugin has three operator surfaces, with a strict duty split:
204
-
205
- - **`/fallbacks`** — what happened this session: origin, resolved role, effective chain, recent fallback switches, cooldown status (half-open marker rows when `recovery: half-open` is active). Read-only.
206
- - **`/fallbacks config`** — what is configured: composed-config readback (trigger codes, root chain, time slots, timezone, roles, role rules, cooldown, revert policy, safety valves, presets, role auto-match). Read-only apart from the one action command **`/fallbacks config revert-seed <role-id>`**, which restores a seeded role's persona to its declared seed default — the web settings card presents seeded personas read-only (no revert affordance), so the command is that action's only surface.
207
- - **`/settings`** — the edit surface. The plugin registers a **fallbacks** section with full parity to the web settings card: booleans (`roleAutoMatch`) render as toggles, selects (`presets`, `revertPolicy`) as pickers, and numbers (`cooldownMs`, `maxSwitchesPerStep`, `alwaysModeRetryCap`) as numeric inputs; complex structures (`rootChain`, `timeSlots`, `roles.list`, `roles.rules`) are JSON text fields and `triggerCodes` a comma-separated text field. Invalid drafts (bad JSON, non-conforming chains, malformed time-slot rows) block the save — the section never corrupts the config.
208
-
209
- **Requirements**: the `/settings` fallbacks section needs **dsh-tui ≥ v0.8.5** (commit `c51661f` or later on `main`; the settings seam shipped in v0.8.0, the groups shape + validation in v0.8.5). On an older dsh-tui the section is absent, and file editing remains the only TUI edit surface.
210
-
211
- File editing still works everywhere: the shared `$DSH_HOME/settings.yaml` (`fallbacks:` section — the same file the web card writes) for global settings, or the profile patch `~/.dsh/profiles/dsh-tui/cordis.patch.yml` (`config:` overrides on the plugin row) for dsh-tui-specific values. A patch row **replaces** the targeted row's whole `config` — restate every field you want to keep (schema defaults fill the rest).
212
-
213
- ## FallbacksChain in the model picker
214
-
215
- Whenever the plugin has content configured (a non-empty `rootChain`, at least one `timeSlots` row, or one declared role), it registers a virtual provider, **FallbacksChain**, with a single catalog row: **Auto**. The web profile and dsh-tui both see the row: they share the same adapter catalog, so the row needs no settings-page wiring or host patch (it is independent of the `/settings` fallbacks section, which edits configuration rather than the picker catalog). A legacy or empty all-day chain does NOT hide the row as long as other content is configured (the delegate just refuses to serve it); with nothing configured there is no row.
216
-
217
- Selecting **FallbacksChain / Auto** uses the configured chain as the root **primary**: the request stays on the virtual pair and the adapter's thin delegate dispatches the effective chain's first exact `provider/model` at request time, so the fallback engine degrades from that head as usual. Serving the selection unchanged (instead of rewriting the route to the head) is also what keeps the host's model-change notice quiet: the recorded route equals the session selection, so the notice appears once on a genuine selection change rather than being re-armed on every step. Selecting any real catalog model keeps the v0.2.2 fallback-only behavior — the session model is primary and the chain engages only after it fails.
218
-
219
- 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.
220
-
221
- Notes:
222
-
223
- - **Picker label**: the row's catalog `name` (what the composer trigger shows) is live — `Auto: DeepSeek Flash[Liang Peak]` / `Auto: DeepSeek Flash[all-day]` (catalog display name, not the model id); the id stays `Auto`. Bare `Auto` if the all-day tail is not conforming. Refresh by reopening the picker.
224
- - **Thin delegate on every origin**: the row is served by the same `stream()` delegate for the root agent and for a subagent session that inherits the selection — subagent role resolution and injection keep their semantics apart from one deliberate widening: dispatch-time rule matching accepts **either** the recorded pair or the served head (a superset of the previous matching, so a rule keyed on the real head keeps matching), and the virtual row is never a second routing engine. A subagent that inherits the selection still routes through the chain head.
225
- - **Conformance gate on the tail**: a successful delegation requires the all-day chain to be **tail-conforming** — its last entry must be exactly one official model (`deepseek-official/deepseek-flash` or `deepseek-official/deepseek-v4-pro`, the card's Default model panel); leading entries (Default fallback chain) are walked first. Deactivating the plugin (the Plugins-page row switch, or clearing all configured content) hides the row again (slot-row/chain edits never churn registration).
226
- - **Stale selection**: if the row disappears (plugin deactivated) while `FallbacksChain / Auto` is selected, the session keeps showing it as the current model with `routable: false` — pick a real model from the catalog to continue (host-native catalog semantics).
227
- - **Capabilities and retry policy follow the head**: the row's model metadata (context window, modalities, reasoning) mirrors the current effective head, and `providerRetryPolicy` returns **that head's** policy — so a user's `llm-deepseek.retryPolicy` applies on this route too, not the permissive default. The host captures the policy once at registration, so a later policy edit or a slot-driven head-provider rotation shows up only after the plugin re-registers. Retry events are keyed by the virtual provider — the route the runtime actually sees. Full semantics → [docs/configuration.md](docs/configuration.md).
228
-
229
- ## Time-slot presets
230
-
231
- 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.
232
-
233
- - **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 model (Flash / Pro XOR; leading Default fallback chain entries are walked first).
234
- - **Presets** (frozen, not user-editable): `liang-peak` = Monday–Friday 09:00–12:00 **and** 14:00–18:00; `liang-valley` = every other UTC+8 time; `glm-peak` = Monday–Friday 14:00–18:00; `glm-valley` = every other time. One preset id = one row; the card picker never offers a duplicate.
235
- - **Custom rows**: `start` / `end` (`HH:mm`, may wrap midnight) + optional `days` (0=Sunday…6=Saturday; omitted/empty = every day) + models.
236
- - **Next-request apply**: a slot boundary crossing never preempts an in-flight step — the new row takes effect on the next root request. Rotation is mount-only: info log + card/`/fallbacks` status line, no durable switch event.
237
- - **Flat settings card** (the official Plugins-page form language): the card is always open — no collapsible chrome and no feature switch — and one footer **Save/Discard** pair writes the whole validated draft. The Main agent section groups Time slots (extra rows — add preset / add custom / remove / reorder by buttons or **drag**; preset rows show a read-only window summary and edit models only; custom rows carry an editable name; the **timezone picker** lives here and **locks to Asia/Shanghai while any preset row exists**, since preset windows are frozen UTC+8 constants), Default fallback chain (walked first when no slot matches) and Default model (the official Flash | Pro last-resort fallback). Rows are collapsible to name + first model (content disclosure). There is no `timeSlots.enabled` master switch (adding a row is the opt-in) and no `rootMode` control.
238
-
239
- ## Preset roles
240
-
241
- 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 and now show source `user` as ordinary editable rows only when no live producer still declares the id, even though the operator never wrote them; a companion's live declaration keeps them seeded and read-only with source `external` when unnamed or its registered set name otherwise.)
242
-
243
- - **Switch**: `fallbacks.presets` — `'bundled'` (default) declares the preset roles on apply; `'none'` disables the automatic declaration (already-materialized rows stay).
244
- - Full semantics (upgrade behavior, conflict handling, library reuse of `presetRoles`) → [docs/configuration.md](docs/configuration.md).
245
-
246
- ## Agent presets (plugin composition)
247
-
248
- Agent **presets** are a dsh feature for composing plugins — they are declarative child-plugin lists a user can apply as a set, and they carry **no** model fields of their own. The fallbacks plugin needs no preset-specific configuration: because a preset composes plugins, the fallback applies **wherever the plugin's row is mounted** — a preset that includes the `dsh-llm-fallbacks` row gives that preset's sessions the same chains and roles configured for the plugin.
249
-
250
- Two composition caveats:
251
-
252
- - **A user preset override replaces the entire child list** (no merge): a preset that omits the fallbacks row silently unmounts the plugin for sessions using that preset. Restate every plugin row you want to keep.
253
- - **Preset-scoped chain configs are possible by mounting the row inside a preset**: the plugin row's `config:` overrides in a preset composition produce a chain configuration that applies to that preset's sessions only (patch rows replace the whole `config`, so restate every field).
254
-
255
- ## Fallback triggers (`triggerCodes`)
256
-
257
- `fallbacks.triggerCodes` is a plain list of dsh failure codes, and **any** code the harness can report is accepted — not only the three defaults (`AUTH` / `QUOTA` / `RATE_LIMIT`). A failure whose code is not listed passes straight through to llm-retry or to the original error, exactly as if the plugin were not installed. Retryable failures (5xx / `TRANSPORT` / `TIMEOUT` / `EMPTY_RESPONSE`) need no extra entries: llm-retry backs them off first and the failure enters the chain decision the same way once that budget is exhausted.
258
-
259
- ### Context-window rejections
260
-
261
- 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:
262
-
263
- ```yaml
264
- fallbacks:
265
- triggerCodes:
266
- - AUTH
267
- - QUOTA
268
- - RATE_LIMIT
269
- - CONTEXT_WINDOW_EXCEEDED # fail over when the request does not fit
270
- rootChain:
271
- - anthropic/claude-3-5-sonnet # walked first
272
- - deepseek-official/deepseek-flash # last resort (Flash or Pro)
273
- ```
274
-
275
- **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.
276
-
277
- **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.
278
-
279
- **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`.
280
-
281
- ## Host subagent model policy (dsh 0.1.2)
282
-
283
- 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.
284
-
285
- **Policy read (per session)**: the session `subagent/model-selection-policy` event wins; otherwise the `subagent-model-selection` settings service when `enabled`. Missing service / `enabled: false` / no event → policy off.
286
-
287
- When the policy is **enabled**:
288
-
289
- - **Authorized head preserved (skip inject)**: a subagent spawned with an explicit `provider`+`model` (spawn options, durable `request/header`, or a `model/selection` selection) keeps that route as its chain head — role-inject is skipped and the authorized route is never overwritten at the first request; the plugin chain applies only from failure time onward. Pure inheritance (no explicit selection fields) is **not** an authorized route.
290
- - **Allowlist-constrained inject**: pure inheritance still resolves a role in the three stages above, but the injected chain head is plugin-originated and must be on the effective allowlist — the first resolved in-allowlist candidate wins. Empty intersection → inject is skipped and the host seed stands (warn log).
291
- - **Allowlist-constrained failure switching**: after a triggering failure, the resolved candidates (wildcards already expanded; cooldown / step-failed / same-as-current filters already applied) are intersected with the allowlist in walk order. Empty intersection → **no switch**, no out-of-allowlist request, a warn log plus an in-memory blocked-attempt record (no durable session write — issue #52 stands).
292
- - **Fail-closed**: a present-but-malformed policy event, or enabled settings with an unreadable route list, disables plugin-originated injects and switches for that session (warn log); the host seed and non-switch behavior are untouched.
293
- - **Card status area (read-only)**: the Fallbacks card Subagents section shows the effective allowlist, the effective chain head with its source (`authorized` / `injected`), and an empty-intersection warning when a switch was blocked — the same numbers the runtime uses, never a second write-face.
294
- - **Virtual-route carve-out**: a subagent explicitly spawned on the plugin's virtual provider (`FallbacksChain` / `Auto`) is user-authorized for that route (spec D2 — explicit selection). The virtual adapter's internal delegation of its requests to the configured effective chain head is that route's documented purpose and is outside D1's selection semantics — D1 governs provider/model selection the plugin makes, not delegation inside an explicitly chosen virtual route — so the delegated head is not allowlist-filtered.
295
-
296
- When the policy is **disabled or absent**, inject and failure-switch **selection** are unchanged from 0.3.5 (no allowlist filter, no authorized-route skip).
115
+ Save the config and restart the session, then type `/fallbacks` — the read-only in-session diagnostics (origin, resolved role, chain, recent fallback switches, cooldown status). In a dsh-tui profile, `/fallbacks config` reads back the composed configuration.
297
116
 
298
- **Effort rule (policy-independent)**: `reasoningEffort` on every override path (role-inject, failure switch, always-cap switch) follows the upstream 0.1.2 `routeChanged` rule — an unchanged provider+model route keeps the seed effort; a route change drops it unless an effort is explicitly named for the override (explicit always survives). A stale effort is never carried into a different provider.
117
+ ## Feature highlights
299
118
 
300
- Full semantics → [docs/configuration.md](docs/configuration.md#host-subagent-model-selection-dsh-012).
119
+ - **Automatic failover** for root and subagents — any agent walks its chain on a listed failure code and the step continues. → [details](docs/features.md#automatic-model-failover)
120
+ - **Context-window-aware fallback** — `CONTEXT_WINDOW_EXCEEDED` fails over to a larger-context candidate; the switch is request-scoped (no cooldown). → [details](docs/features.md#context-window-aware-fallback)
121
+ - **Peak/valley time slots** — four frozen UTC+8 presets or custom windows rotate the effective root chain. → [details](docs/features.md#time-slots)
122
+ - **Role-based subagents** — a dispatch-declared role brings persona, notice row and badge to the child; its chain head is injected subject to host policy. → [details](docs/features.md#role-based-subagents)
123
+ - **FallbacksChain in the picker** — select `FallbacksChain / Auto` to make the chain the root primary; any real model stays fallback-only. → [details](docs/features.md#fallbackschain-in-the-model-picker)
124
+ - **Host subagent model policy** — the dsh 0.1.2 allowlist is a hard constraint on every plugin-originated subagent route. → [details](docs/features.md#host-subagent-model-selection-policy-dsh-012)
125
+ - **Five bundled preset roles** — `reviewer` / `scout` / `security-reviewer` / `sonic` / `task`, declared on apply. → [details](docs/features.md#preset-roles)
126
+ - **dsh-tui surfaces** — `/fallbacks`, `/fallbacks config`, and a `/settings` section with web-card parity. → [details](docs/features.md#dsh-tui-surfaces)
301
127
 
302
- ## Mount-only (no dsh modification)
128
+ ## Mount-only
303
129
 
304
130
  The plugin installs as a **pure mount**: bundle insert + client inject + its own gateway channel (`/api/fallbacks/get|set|reset`) — no dsh patches, no postinstall step, and dsh upgrades never require re-patching. Stale leftover patches from an older patched install are harmless.
305
131
 
@@ -309,9 +135,12 @@ The plugin installs as a **pure mount**: bundle insert + client inject + its own
309
135
  |---|---|
310
136
  | [docs/install.md](docs/install.md) | profile install (web + dsh-tui) / registry / git / local variants / uninstall / `--dump-config` verification |
311
137
  | [docs/configuration.md](docs/configuration.md) | full `fallbacks` namespace reference, selector syntax, example YAML, plugin-config card usage, TUI readback, behavior notes, preset roles |
138
+ | [docs/features.md](docs/features.md) | capability reference: failover, context-window fallback, time slots, role-based subagents, picker, host policy, preset roles, agent presets, dsh-tui |
139
+ | [docs/repair.md](docs/repair.md) | session-log repair: refusal classes, `--apply` publishing, flags, exit codes, lossy `--drop-legacy-events` |
312
140
  | [docs/consumer-api.md](docs/consumer-api.md) | developer consumption contract: library API + named `llm-fallbacks` service + role seeds, export inventory, lifecycle, typing |
313
141
  | [docs/release.md](docs/release.md) | release process: Trusted Publishing setup, Release prep SOP, fragment format, rollback |
314
142
  | [docs/verification.md](docs/verification.md) | verification records (test matrix, bundle layer order, runtime contracts, QA gate script) |
143
+ | [CHANGELOG.md](CHANGELOG.md) | released changes, assembled from `.changes/` fragments |
315
144
 
316
145
  ## License
317
146