ytdb-slate 0.4.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Slate is a thread-weaving orchestration extension for the [pi coding agent](https://pi.dev).
4
4
 
5
- The orchestrator (your main pi session) dispatches **bounded actions** to persistent **worker threads**. Each completed action is compressed by an LLM into an **episode** — a durable, structured record (intent, actions, findings, artifacts, open issues, handoff notes) that the orchestrator composes into further dispatches instead of re-reading raw transcripts. On top of that, Slate injects a mandatory workflow doctrine: research before design, design review, adversarial review, and track review — with optional umbrella **draft-PR publishing** for tracks. An opt-in **model-failover** map adds high availability: when a model API fails, the orchestrator, worker threads, and episode compression each retry once on a configured equal-quality alternative.
5
+ The orchestrator (your main pi session) dispatches **bounded actions** to persistent **worker threads**. Each completed action is compressed by an LLM into an **episode** — a durable, structured record (intent, actions, findings, artifacts, open issues, handoff notes) that the orchestrator composes into further dispatches instead of re-reading raw transcripts. On top of that, Slate injects a mandatory workflow doctrine: research before design, design review, adversarial review, and track review — with optional umbrella **draft-PR publishing** for tracks. An opt-in **model-failover** map adds high availability: when a model API fails, the orchestrator, worker threads, and episode compression each retry once on a configured equal-quality alternative. A second opt-in, **action-level model routing**, gives each dispatched action a model and an effort level chosen to be up to the task and no more, so cost is bounded per action instead of per session.
6
6
 
7
7
  ## Why Slate?
8
8
 
@@ -51,6 +51,12 @@ This summary is orientation only; the shipped docs listed below are normative.
51
51
 
52
52
  Slate can ride through model API outages. The opt-in `modelFailover` map in `.pi/slate.json` (trusted projects only) maps a model (`provider/id`) to an equal-quality alternative; on an eligible model API failure — not an abort or a context overflow, and for the orchestrator and worker sites only after pi's own retries are exhausted — the affected site (orchestrator, worker thread, or episode compression) retries once on the mapped model (single hop, never chained). An orchestrator failover — like the handoff adoption that re-applies a parent session's model — would otherwise leave pi's global model defaults changed, so Slate restores them on a best-effort basis unless `preserveGlobalModelDefault` is `false`. The map is empty by default (feature off) and read once at session start. Full semantics: the `modelFailover` row in [Configuration](#configuration) and the shipped [`docs/model-failover.md`](docs/model-failover.md) — if they disagree, that document wins.
53
53
 
54
+ ## Action-level model routing
55
+
56
+ Slate can pick the model and the effort level per dispatched action rather than running every action on whatever model the orchestrator happens to be on. `router.models` in `.pi/slate.json` (trusted projects only) is a closed list of `provider/id` specs. Each action uses its `model` and `effort` arguments when present. Otherwise, it uses the thread's base model and a level DERIVED for the model it routes to. That level is the lowest one with a capability measurement, never a fixed default. The list is empty by default. An empty list disables candidate-list enforcement, base seeding, effort derivation, and the router's window, billing, and substitution paths. Per-action `model` and `effort` arguments stay active on both router states. Slate always checks an explicit effort against pi's vocabulary, and against the ladder of the model it routes to whenever it has that model's profile data. An action that omits `model` plans for the thread's pre-router pin, and a thread with no pin gives it no plan target. An action that omits `effort` resolves no level. In both cases the worker session returns to what it opened on. [`docs/model-routing.md`](docs/model-routing.md) holds the full contract, including the known cases where the model or level that runs differs. Only models Slate ships a benchmark profile for can be routed to; entries pi cannot serve, or has no profile for, are dropped with a warning naming them.
57
+
58
+ What the code enforces is deliberately narrower than what the routing data advises. The dispatch guards refuse an unlisted model, a level off the target model's ladder and a level the provider rejects outright; they warn about — and mark — an unmeasured level, refusing it only when `router.allowUnmeasuredEffort` is `false`; and the context-window guard substitutes a wider listed model instead of ever blocking an action. The obligations that come from the profile data itself — keeping review and gate actions on measured levels, and honoring a REFUSE such as `anthropic/claude-fable-5`'s for zero-data-retention work — are stated in the doctrine for the orchestrator to honor; they are **not** code-enforced guards. Full semantics: the `router.*` rows in [Configuration](#configuration) and the shipped [`docs/model-routing.md`](docs/model-routing.md) — if they disagree, that document wins.
59
+
54
60
  ## Install
55
61
 
56
62
  Install into a project (pinned, project-scoped — recorded in `.pi/settings.json`):
@@ -68,7 +74,7 @@ Optional config file: `slate.json` in the project's pi config dir (`.pi/slate.js
68
74
  | Key | Type | Default | Semantics |
69
75
  | --- | --- | --- | --- |
70
76
  | `orchestratorModeDefault` | boolean | `false` | Start fresh interactive sessions with orchestrator mode ON. |
71
- | `episodeModel` | string | newest available Anthropic Sonnet, else the worker's own model | Model (`provider/id`) used to compress a finished action into an episode. |
77
+ | `episodeModel` | string | newest available Anthropic Sonnet, else the orchestrator's own base model | Model (`provider/id`) used to compress a finished action into an episode. Never the model an action was routed to: an episode is read by every later consumer of the thread, so one cheap route must not degrade the record. An unusable value is reported and the default is used. |
72
78
  | `workerTools` | string[] | `["read", "bash", "edit", "write", "grep", "find", "ls"]` | Tools available to worker threads (an empty list also falls back to the default). |
73
79
  | `workerExtensions` | string[] | `[]` | Regex patterns (matched **unanchored**) selecting which of the host session's already-loaded extensions also load into every worker thread; each matched extension's tools are added **on top of** `workerTools`. Empty (default) means workers load no extensions. The orchestrator keeps its restricted tool set but its doctrine is told what was whitelisted. Invalid patterns are dropped with a warning at session start. |
74
80
  | `maxConcurrent` | number | `4` | Maximum number of worker actions running concurrently (must be ≥ 1 — unenforced: a value of 0 or less silently hangs all dispatches). Excess dispatches wait in a queue; actions on the same thread always run in dispatch order. Default rationale: shipped `docs/design-principles.md` §5 (repo-local note). |
@@ -80,6 +86,8 @@ Optional config file: `slate.json` in the project's pi config dir (`.pi/slate.js
80
86
  | `reviewPerspectivesPath` | string | — | Project review charters, each declaring its own finding-ID prefix. The doctrine references this **path**; the orchestrator reads the file alongside the shipped review rules. |
81
87
  | `modelFailover` | object (string → string) | — (empty, failover off) | Map of `provider/id` → equal-quality alternative model; on a model API failure the affected site retries once on the mapped model (worker/orchestrator sites only after pi's own retries are exhausted; episode compression has no pi retry loop). Read once at session start — see [`docs/model-failover.md`](docs/model-failover.md). |
82
88
  | `preserveGlobalModelDefault` | boolean | `true` | Put pi's global model defaults (`defaultProvider`, `defaultModel`, `defaultThinkingLevel`) back after a Slate-initiated model switch — orchestrator failover and handoff adoption — so the switch stays session-scoped. `false` turns that restore off at both sites (the switches themselves are unaffected), leaving each one persisted in pi's global settings. Best-effort: Slate reverts only a value its own switch produced, and can stand down or give up — limits in [`docs/model-failover.md`](docs/model-failover.md). |
89
+ | `router.models` | string[] | `[]` (empty — routing off) | Closed list of models (`provider/id`) an action may use. Empty or absent means the router is OFF. Slate enforces no candidate list, seeds no thread base, derives no effort level, and runs no router-only window, billing, or substitution path. Per-action `model` and `effort` arguments stay active on both router states. Slate still checks explicit effort against pi's vocabulary and the routed model's ladder when it has that model's profile data. An action that omits `model` plans for the thread's pre-router pin, and a thread with no pin gives it no plan target. An action that omits `effort` resolves no level. In both cases the worker session returns to what it opened on. The known cases where the model or level that runs differs are listed in [`docs/model-routing.md`](docs/model-routing.md). A listed model becomes routable only if Slate has its benchmark profile, pi registers it, and credentials are configured. Slate drops other entries with a warning. If all entries drop, routing turns off with one summary warning. Slate reads and resolves the list once per session. See [`docs/model-routing.md`](docs/model-routing.md). |
90
+ | `router.allowUnmeasuredEffort` | boolean | `true` | What happens when an explicitly requested effort level is on the target model's ladder but has no capability measurement in Slate's profiles: `true` dispatches it with a ⚠ notice and marks the episode `(unmeasured level)`; `false` refuses the dispatch. Either way a level that is off the model's ladder, or one the provider rejects outright, is refused, and a level Slate DERIVES for an action is always a measured one. |
83
91
 
84
92
  Example `.pi/slate.json` (the `docs/agents/...` paths are placeholders — point them at markdown files that actually exist in **your** project):
85
93
 
@@ -93,7 +101,8 @@ Example `.pi/slate.json` (the `docs/agents/...` paths are placeholders — point
93
101
  "workflow": { "draftPRs": true },
94
102
  "doctrineExtraPath": "docs/agents/workflow-additions.md",
95
103
  "reviewPerspectivesPath": "docs/agents/review-perspectives.md",
96
- "modelFailover": { "anthropic/claude-sonnet-5": "openai/gpt-5.2" }
104
+ "modelFailover": { "anthropic/claude-sonnet-5": "openai/gpt-5.2" },
105
+ "router": { "models": ["openai/gpt-5.6-luna", "anthropic/claude-opus-5"] }
97
106
  }
98
107
  ```
99
108
 
@@ -126,7 +135,7 @@ Slate reads project configuration (`.pi/slate.json`) and injects project files (
126
135
 
127
136
  ## Shipped docs
128
137
 
129
- In orchestrator mode, Slate appends a short **doctrine** (a block of numbered rules) to the orchestrator's system prompt each turn. The doctrine does not embed the workflow docs — it cites them by **absolute path**, resolved inside the installed package (not your project), and the orchestrator reads them on demand:
138
+ In orchestrator mode, Slate appends a short **doctrine** (a block of numbered rules) to the orchestrator's system prompt each turn. The doctrine does not embed the workflow docs — it cites them by **absolute path**, resolved inside the installed package (not your project), and the orchestrator reads them on demand. Those embedded paths are also why the block's character count depends on your install location; [`docs/context-budget.md`](docs/context-budget.md) has the measured sizes, with and without the optional rules, and the arithmetic for your own install:
130
139
 
131
140
  - `docs/track-workflow.md` — the track-based workflow (research → design review → adversarial review → track review)
132
141
  - `docs/pr-publishing.md` — umbrella draft-PR publishing (cited only when `workflow.draftPRs` is `true`)
@@ -134,6 +143,7 @@ In orchestrator mode, Slate appends a short **doctrine** (a block of numbered ru
134
143
  - `docs/design-principles.md` — Slate's own design rationale
135
144
  - `docs/model-failover.md` — the opt-in `modelFailover` map (**reference documentation** — unlike the entries above it is not workflow doctrine and is not cited by the doctrine)
136
145
  - `docs/context-budget.md` — the orchestrator `contextBudget`: defaults, per-model overrides, the window clamp, and the pricing rationale (also **reference documentation**, not cited by the doctrine)
146
+ - `docs/model-routing.md` — action-level model routing: the two `router` keys, how a model becomes routable, how an omitted effort level resolves, the dispatch guards, and the warnings a first routed session emits. Unlike the two entries above it **is** cited by the doctrine, by absolute path, but only while the routing rule renders — it is the extra embedded path that appears with the rule, the fourth normally and the fifth when `workflow.draftPRs` is also on, which the size arithmetic in `docs/context-budget.md` accounts for. The routable model list itself is never read from this file: the rule renders it live from the session's own resolution, because that set depends on your registry and credentials.
137
147
 
138
148
  Project-specific additions layer on top — they extend, not replace, the shipped doctrine — via two distinct mechanisms:
139
149
 
@@ -118,6 +118,114 @@ paused, threshold compactions pass through as an escape valve.
118
118
  Overflow-recovery compaction and manual `/compact` are never
119
119
  touched.
120
120
 
121
+ ## What the always-loaded doctrine costs the budget
122
+
123
+ The budget's denominator includes Slate's own always-loaded block.
124
+ While orchestrator mode is on, the doctrine sits in the
125
+ orchestrator's system prompt for the whole session — and, like the
126
+ rest of the conversation (see the pricing cliff above), it is sent
127
+ again with every request. It occupies its space in the window once;
128
+ you pay its input tokens every turn.
129
+
130
+ **Its size depends on where the package is installed.** That is not
131
+ obvious, and it catches anyone who measures: the doctrine cites the
132
+ shipped docs by ABSOLUTE path, so three to five full filesystem
133
+ paths are embedded in the block — three at minimum
134
+ (`track-workflow.md`, `review-rules.md`, `design-principles.md`),
135
+ plus `pr-publishing.md` when `workflow.draftPRs` is on, plus
136
+ `model-routing.md` when the routing rule renders. Every additional
137
+ character in the installed `docs/` directory therefore costs 3–5
138
+ characters of doctrine.
139
+
140
+ So the figures below are **portable characters**, defined exactly as
141
+ Slate's own automated size check defines them: the block with each
142
+ occurrence of the installed `docs/` DIRECTORY removed, keeping the
143
+ filename. That makes them install-invariant by construction —
144
+ verified by measuring at a 12-character docs path and at this repo's
145
+ 58-character one, which give identical portable counts — and it
146
+ makes the arithmetic one multiplication:
147
+
148
+ > rendered characters = portable + (embedded paths × length of your
149
+ > installed `docs/` directory)
150
+
151
+ Every row below states its full basis, because router state, model
152
+ count and `draftPRs` each move the number:
153
+
154
+ | router | models | `draftPRs` | paths | portable | lines | rendered at a 58-char docs dir |
155
+ | --- | --- | --- | --- | --- | --- | --- |
156
+ | off | — | off | 3 | 1,929 | 38 | 2,103 |
157
+ | off | — | on | 4 | 1,948 | 38 | 2,180 |
158
+ | on | 6 configured | off | 4 | 3,870 | 58 | 4,102 |
159
+ | on | 6 configured | on | 5 | 3,889 | 58 | 4,179 |
160
+ | on | all 9 shipped | off | 4 | 4,448 | 61 | 4,680 |
161
+ | on | all 9 shipped | on | 5 | 4,467 | 61 | 4,757 |
162
+
163
+ An untrusted project reads the first row whatever its `slate.json`
164
+ says: no project config is loaded, so neither optional rule renders.
165
+ Line counts do not vary with the install path. Enabling `draftPRs`
166
+ costs 19 portable characters plus one more embedded path.
167
+
168
+ Two parts of the block grow with configuration rather than with the
169
+ path:
170
+
171
+ - The routing rule is a live table with ONE ROW PER ROUTABLE MODEL,
172
+ so what renders is the models you CONFIGURE — not the nine Slate
173
+ ships profiles for. It is 1,941 portable characters / 20 lines for
174
+ six configured models and 2,519 / 23 for all nine. A model ROW
175
+ costs 154–186 characters; the legend adds a one-off clause per
176
+ marker it has to explain, so listing the first cheap-tier model
177
+ costs ~50 characters more than its row (two new clauses). That is
178
+ why stepping from six to nine averages 193 per model where the
179
+ first six average 171.
180
+ - The worker-extension rule grows per whitelisted extension and per
181
+ tool that extension contributes, so it has no fixed size; one
182
+ extension contributing two tools measured 373 portable characters
183
+ / 7 lines.
184
+
185
+ Against a 256,000-token budget none of this is material — as a rough
186
+ estimate, at 4 characters per token (no tokenizer was run, and the
187
+ table is denser than prose, so treat it as a floor) the whole block
188
+ is ≈480–525 tokens with both optional rules off and ≈970–1,025 with
189
+ six models routed, each range spanning portable to as-rendered-here.
190
+ That is under half a percent of the default budget, though it is
191
+ re-sent on every request rather than paid once. It is listed here
192
+ because it is context Slate itself puts in front of the model on
193
+ every turn, and because it is the number to check before assuming
194
+ the budget's headroom is all conversation.
195
+
196
+ ## Worker threads: the routing window guard
197
+
198
+ The budget above is the ORCHESTRATOR's. Worker threads have no
199
+ budget and never pause; with action-level routing on, they get a
200
+ weaker but automatic counterpart. Before each dispatch — whenever
201
+ the thread's context size is knowable, so not for a thread whose
202
+ worker session has not been opened yet — the routed model's
203
+ REGISTRY context window is checked against that size using pi's OWN
204
+ compaction settings, the same settings whose `reserveTokens` clamps
205
+ the budget above. (The check runs even if you disabled
206
+ auto-compaction: turning compaction off does not make an
207
+ over-window dispatch safe, it only turns the compaction into an
208
+ overflow error.) If the thread no longer fits, the action is moved
209
+ to the widest listed model when that one is strictly wider, with a
210
+ warning; if nothing listed is wider it runs anyway and pi compacts.
211
+ The guard never blocks a dispatch and never pauses a thread.
212
+
213
+ Two consequences for this document's subject:
214
+
215
+ - The registry window is what decides the substitution, so the
216
+ overrides in the next section change worker routing too: raising
217
+ `gpt-5.6-*` to 1,050,000 makes those models genuinely wide
218
+ candidates rather than 272K ones.
219
+ - A narrow model in `router.models` is not a hazard the way it would
220
+ be for the orchestrator — it is a model long threads get routed
221
+ AWAY from, paying the wider model's rate instead. That is not
222
+ silent: the substituted action carries a ⚠ notice naming both
223
+ models.
224
+
225
+ Routing itself — the two `router` keys, what makes a model routable,
226
+ how effort levels resolve and the full guard list — is documented in
227
+ `model-routing.md` in this directory.
228
+
121
229
  ## Using GPT-5.6's full 1.05M window
122
230
 
123
231
  Two deliberate steps. First, raise the registry window in
@@ -155,6 +263,17 @@ dropped:
155
263
  WARNING: beyond 272K total input every request bills at the
156
264
  long-context rates listed above.
157
265
 
266
+ With action-level routing on, this override also settles a warning
267
+ you will otherwise see once per session for each configured
268
+ `gpt-5.6-*` model: Slate's own profile for those models records a
269
+ 1,050,000-token window, pi's stock registry reports 272,000, and the
270
+ router reports that divergence without adjudicating it (routing uses
271
+ the registry figure). Raising the registry window removes the
272
+ disagreement at its source. Leaving it alone is equally valid — the
273
+ registry figure is the conservative one — and `model-routing.md`
274
+ explains why Slate does not silence the warning by editing its own
275
+ number.
276
+
158
277
  ## Accepted limitations
159
278
 
160
279
  - On models with a 272K pricing tier and a larger registry window
@@ -174,3 +293,9 @@ long-context rates listed above.
174
293
  compaction can land in the same cycle and the handoff brief is
175
294
  written from compacted context — episodes and thread state survive
176
295
  on disk, so handoff still functions.
296
+ - The doctrine sizes above cover the shipped rules only. A project
297
+ that also injects `doctrineExtraPath` or `orchestratorPromptDocs`,
298
+ or whitelists worker extensions, pays for those on top in the same
299
+ always-loaded position, and Slate measures none of it against the
300
+ budget separately — it simply arrives as context the budget then
301
+ has less room for.
@@ -8,9 +8,22 @@ for its content. This document records the reasoning behind the
8
8
  extension's design so maintainers — and the orchestrator itself, on
9
9
  demand — can check changes and behavior against the original intent.
10
10
 
11
- Note: module headers cite ExecPlan decision ids (D3–D9, M1–M3) from the
12
- original implementation plan, which is not part of this repository. This
13
- document is the in-repo source for the architecture rationale.
11
+ Note: module headers cite ids from design records that are not part of this
12
+ repository — the original implementation plan (ExecPlan: D3–D9, M1–M3), later
13
+ design rounds (higher-numbered D ids, and W ids for named warnings), and
14
+ per-round review findings, whose prefix says which review raised them:
15
+ AD (adversarial), AF (agent-failure), BG (blocker/bug), CN (concurrency),
16
+ CQ (code quality), DF (data fidelity), N (numeric), RG (regression),
17
+ RI (research integrity), SE (security), WB (worker boundary) and WS (worker
18
+ safety). `extension/model-profiles.ts` additionally cites research TRACE keys
19
+ (`G1a`, `GM3`, `O4.2`, `A4b`, …). Those name rows in the research corpus that
20
+ produced the profile table; the corpus lives in the project repository and is
21
+ NOT part of the published package, so in an installed copy the keys are
22
+ provenance marks to quote when reporting a data problem, not links to follow.
23
+ What a consumer can act on without the corpus is in `model-routing.md`
24
+ (§ where the numbers come from): the table's provenance rules, its `asOf`
25
+ date, and which fields are marked untraced. This document is the in-repo
26
+ source for the architecture rationale.
14
27
 
15
28
  ## 1. The problems Slate is built to solve
16
29
 
@@ -167,6 +180,38 @@ while staying safe on common consumer API tiers; wider fan-outs only
167
180
  pay tail latency, and projects with higher-tier keys raise the cap in
168
181
  `slate.json`.
169
182
 
183
+ Repo-local note (not from the report): **action-level model routing**
184
+ applies P10's discipline to a second scarce resource. P10 treats
185
+ CONTEXT as RAM; the optional `router.models` list treats SPEND the same
186
+ way — each dispatched action gets a model and an effort level chosen to
187
+ be up to the task and no more, instead of every action inheriting
188
+ whatever model the orchestrator happens to be running. It is also a P1
189
+ corollary: an action is only a bounded unit if what it may cost is
190
+ bounded too, and the closed model list plus a derived (never inflated)
191
+ effort level is what bounds it. The two disciplines even share
192
+ machinery — pi's compaction settings, which clamp the orchestrator's
193
+ context budget, also decide per dispatch whether a worker thread still
194
+ fits the model it is routed to, in which case the action moves to the
195
+ widest listed model rather than being blocked.
196
+
197
+ Candidate routing is OFF until `router.models` lists something — the
198
+ per-action `model` and `effort` arguments, and a fallback a failover
199
+ is holding, stay active either way. The feature is
200
+ built on a deliberate split that is worth holding onto when reasoning
201
+ about the architecture: the code ENFORCES less than the routing data
202
+ ADVISES. Dispatch guards refuse what is mechanically wrong — a model
203
+ outside the configured list, a level a model does not offer — because
204
+ those are decidable from the data. The obligations that require
205
+ JUDGMENT about the work, such as which actions count as review or gate
206
+ actions and what a compliance constraint demands of a given task, are
207
+ stated in the doctrine for the orchestrator to honor and are not
208
+ guards. That is the same reasoning as P6: the harness makes the right
209
+ thing natural and blocks what it can decide, rather than pretending to
210
+ adjudicate what it cannot see. Which obligations fall on which side of
211
+ that line changes as the feature grows, so this document deliberately
212
+ does not restate the list — `model-routing.md` in this directory owns
213
+ it, along with the mechanics and the provenance of the routing data.
214
+
170
215
  Repo-local note (not from the report): the P7 guard was originally
171
216
  absolute — workers loaded no extensions, so Slate's `thread` tool simply
172
217
  never existed for them. The optional `workerExtensions` key (`slate.json`,
@@ -268,7 +313,19 @@ the load-on-demand discipline the extension itself prescribes:
268
313
  `review-rules.md`, and a pointer to the track-based workflow protocol
269
314
  that ships with this package (`track-workflow.md` in this directory),
270
315
  appended to the system prompt every turn while orchestrator mode is
271
- on. It costs a few hundred tokens and covers everything routine
316
+ on. Sizing it matters to the tiering argument, so it is measured
317
+ rather than asserted: with both conditional tail rules and
318
+ `workflow.draftPRs` off, the block is **1,929 portable characters /
319
+ 38 lines**, and configuring the router for six models roughly
320
+ DOUBLES it, to **3,870** on that same basis — the routing rule
321
+ renders one table row per routable model. (Portable characters, and
322
+ the rest of the configurations, are defined and tabulated in
323
+ `context-budget.md`; the raw count also depends on where the package
324
+ is installed, because the doctrine embeds absolute doc paths.) At a
325
+ rough 4 characters per token that is order-500 tokens, order-1,000
326
+ with the router on — so the original "a few hundred tokens" holds
327
+ only for the fixed rules, and a configured router buys its table with
328
+ a real slice of every request. It still covers everything routine
272
329
  dispatching needs.
273
330
  - **Tier 2 — on demand.** This document. The doctrine carries a short
274
331
  pointer to it (doctrine rule 10); the orchestrator reads it only when
@@ -89,11 +89,11 @@ Failover applies at three sites:
89
89
  model and the failed action is re-prompted once with a continuation
90
90
  nudge. The transcript is preserved; usage and cost accumulate across
91
91
  both attempts. The thread stays on the mapped model while its
92
- session is live and returns to its configured model when the session
93
- is reopened (e.g. after a pi restart). The `threads` listing marks a
94
- switched live session as `model=A ⇒B (live)` — the marker means a
95
- live failover switch happened (it is not a value comparison) and
96
- disappears when the session is reopened.
92
+ session is live, including across later model-less actions, and the
93
+ `threads` listing marks it `live=<model> (failover)`. The hold, the
94
+ marker and the retry inside the failing dispatch are cases M1, M6
95
+ and M2 of [Known cases where the model or level
96
+ differs](model-routing.md#known-cases-where-the-model-or-level-differs).
97
97
  - **Episode compression.** The compression call is retried once with
98
98
  the mapped compressor model — only if it is distinct from the
99
99
  original, resolvable, and authed. If the retry also fails, the