dsh-llm-fallbacks 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/config.d.ts CHANGED
@@ -4,32 +4,27 @@
4
4
  * module must stay free of `@deepseek-ai/*` value imports so the client
5
5
  * bundle never reaches schemastery).
6
6
  *
7
- * Two-block config model (plan fallbacks-role-config-model): block 1
8
- * `rootChain` — the root agent's single fallback chain (empty = no
9
- * degradation) — plus block 2 declared role entities: `roles.list`
10
- * (id/persona/prompt?/permissions?/chain?/fallback) and
11
- * `roles.rules` enum references into the declared ids (or the built-in
12
- * `'inherit'` role). The legacy `chains` / `roles.default` keys are gone
13
- * from the schema and type (zero residual, migration table excepted); the
14
- * runtime consumes the new shape directly and flags surviving legacy keys
15
- * at startup via `detectLegacyKeys` (see `src/index.ts` apply()).
7
+ * Named virtual chains (config v2, plan named-virtual-chains): N named
8
+ * chains each selectable as `FallbacksChain/<chain id>`, carrying their own
9
+ * `entries` (默认降级链 + 链尾) and `timeSlots`. Declared roles keep identity
10
+ * (`id`/`persona`/`prompt?`/`permissions?`) plus an OPTIONAL `chain`
11
+ * POINTER — a manually specified chain id (「指定降级链」), never an auto
12
+ * mode: absent/empty = NOT SPECIFIED, the plugin does not intervene and the
13
+ * child inherits the delegating agent's route. The v1 two-block model
14
+ * (`rootChain` + per-role inline chains + `roles.rules`) is deleted; the
15
+ * runtime consumes the v2 shape directly and flags surviving v1 keys at
16
+ * startup via `detectLegacyKeys` (warn-only migration — no auto-write).
16
17
  *
17
- * Spec §4 is authoritative for field names and default values — notably
18
+ * Spec §1 is authoritative for field names and default values — notably
18
19
  * `triggerCodes` defaults to dsh's stable failure codes `['AUTH', 'QUOTA',
19
- * 'RATE_LIMIT']` (there is no `QUOTA_EXCEEDED` code in dsh), and an
20
- * unconfigured install (empty `rootChain`, no time slots, empty roles) is a
21
- * no-op pass-through exactly like an uninstalled plugin (AC-8 —
22
- * {@link isFallbackActive}). There is NO config-level `enabled` switch
23
- * (plan fallbacks-web-ux-alignment T2): the Plugins-page row toggle is the
24
- * master switch (row disable = fiber destroy), and the runtime gates re-key
25
- * to content presence. A stored profile still carrying the removed
26
- * `enabled:` key loads cleanly — the gateway tolerates and strips it (the
27
- * advisor LEGACY_KEYS mechanism), it is never read and never re-persisted.
20
+ * 'RATE_LIMIT']`. An unconfigured install (empty `chains`) is a no-op
21
+ * pass-through exactly like an uninstalled plugin — there is NO config-level
22
+ * `enabled` switch: the Plugins-page row toggle is the master switch and the
23
+ * runtime gates re-key to `isFallbackActive` (`chains.length > 0`). Nothing
24
+ * pre-creates a default chain: empty `chains` is the ONLY first-run state.
28
25
  *
29
26
  * This module is pure logic: it must not import any `@deepseek-ai/*` package
30
- * (types included) — `FallbacksConfig` is the plugin's own type. Task 3
31
- * registers the schemastery schema (see `./schema.ts`) with
32
- * `installSettingsSection` under the `fallbacks` settings namespace.
27
+ * (types included) — `FallbacksConfig` is the plugin's own type.
33
28
  *
34
29
  * @module dsh-llm-fallbacks/config
35
30
  */
@@ -38,31 +33,31 @@ import type { SlotRowConfig } from './time-slots.ts';
38
33
  export type RecoveryPolicy = 'timer' | 'half-open';
39
34
  /** How a cooled-down model comes back (spec §4). */
40
35
  export type RevertPolicy = 'cooldown-expiry' | 'never';
41
- /** A single role rule: match on provider/model patterns (spec §3). */
42
- export interface FallbacksRoleRule {
43
- /**
44
- * Legacy persisted wire field (PR #62 feedback): rules are subagent-only,
45
- * so this constraint is IGNORED at match time — root requests never
46
- * match rules. Kept in the type/schema so pre-feedback `settings.yaml`
47
- * files that carry `origin` still parse, validate, and save unchanged.
48
- */
49
- origin?: 'root' | 'subagent';
50
- provider?: string;
51
- model?: string;
52
- role: string;
53
- }
36
+ /** Chain id format (v2): case-sensitive, uppercase allowed so the migrated
37
+ * default id is exactly `Auto`. Deliberately NOT {@link ROLE_ID_PATTERN},
38
+ * which keeps governing role ids. */
39
+ export declare const CHAIN_ID_PATTERN: RegExp;
54
40
  /**
55
- * Chain-append strategy of a declared role entity: `inherit-root` (the
56
- * default) runs the role's own chain and then appends `rootChain`;
57
- * `none` uses only the role's own chain.
41
+ * One named virtual chain (v2). `id` doubles as the picker model id —
42
+ * renaming it would orphan every saved selection, so only `label` is
43
+ * editable in the UI. The LAST `entries` element is the 链尾: exactly one
44
+ * official model. The FIRST matching `timeSlots` row REPLACES `entries`.
58
45
  */
59
- export type FallbackStrategy = 'inherit-root' | 'none';
46
+ export interface NamedChain {
47
+ id: string;
48
+ /** Settings tab title + picker display prefix (display-only). */
49
+ label: string;
50
+ /** 默认降级链 + 链尾. */
51
+ entries: string[];
52
+ /** 分时 rows scoped to this chain; first match replaces {@link entries}. */
53
+ timeSlots?: SlotRowConfig[];
54
+ }
60
55
  /**
61
- * A declared role entity (plan fallbacks-role-config-model Task 1).
56
+ * A declared role entity (v2): identity + an optional chain POINTER.
62
57
  *
63
58
  * `prompt` / `permissions` are schema-reserved for the next iteration
64
- * (fallbacks-explicit-role-tool) — no consumer this round, and writing them
65
- * does NOT change this round's degradation behavior.
59
+ * — no consumer this round, and writing them does NOT change this
60
+ * round's degradation behavior.
66
61
  */
67
62
  export interface FallbacksRole {
68
63
  id: string;
@@ -75,21 +70,45 @@ export interface FallbacksRole {
75
70
  allow?: string[];
76
71
  deny?: string[];
77
72
  };
78
- chain?: string[];
79
- fallback?: FallbackStrategy;
73
+ /**
74
+ * Manually specified named chain id for children dispatched under this
75
+ * role (「指定降级链」) — a POINTER into `chains[]`, never a chain
76
+ * definition. Absent or empty ⇒ NOT SPECIFIED: the plugin does not
77
+ * intervene and the child inherits the delegating agent's route. The
78
+ * plugin never picks a chain on its own.
79
+ */
80
+ chain?: string;
80
81
  }
81
- /** Role grouping for fallback chains: declared entities + enum references. */
82
+ /** Role grouping (v2): declared entities only — `rules` is deleted. */
82
83
  export interface FallbacksRoles {
83
84
  list: FallbacksRole[];
84
- rules: FallbacksRoleRule[];
85
85
  }
86
86
  /**
87
- * The full `fallbacks` settings shape (two-block config model, verbatim
87
+ * One declared role as the SCHEMA may compose it: identical to
88
+ * {@link FallbacksRole} except that a v1 profile still carries the ARRAY form
89
+ * of `chain`. The schema must accept it (composition happens in cordis BEFORE
90
+ * `apply()`, so a rejection there kills the fiber and no migration can run);
91
+ * {@link migrateLegacyConfig} narrows it to the string pointer.
92
+ */
93
+ export interface FallbacksRoleSource extends Omit<FallbacksRole, 'chain'> {
94
+ chain?: string | string[];
95
+ }
96
+ /**
97
+ * The composed settings shape produced by the schemastery `Config` — the input
98
+ * of {@link migrateLegacyConfig}, not the shape the runtime consumes.
99
+ */
100
+ export interface FallbacksConfigSource extends Omit<FallbacksConfig, 'roles'> {
101
+ roles: {
102
+ list: FallbacksRoleSource[];
103
+ };
104
+ }
105
+ /**
106
+ * The full `fallbacks` settings shape (named virtual chains, verbatim
88
107
  * field names).
89
108
  */
90
109
  export interface FallbacksConfig {
91
110
  triggerCodes: string[];
92
- rootChain: string[];
111
+ chains: NamedChain[];
93
112
  roles: FallbacksRoles;
94
113
  cooldownMs: number;
95
114
  revertPolicy: RevertPolicy;
@@ -98,21 +117,12 @@ export interface FallbacksConfig {
98
117
  * Preset-role injection switch: `'bundled'` declares the 5 preset roles
99
118
  * (spec §9.2) as seed rows on apply; `'none'` disables declaration.
100
119
  * Optional on purpose — a required field would break library consumers
101
- * that construct `FallbacksConfig` literals with the existing 8 keys
102
- * (additive, non-breaking). The value domain is guarded by the schema
103
- * (`Config` in `src/schema.ts`), NOT by `validateFallbacksConfig`, and
104
- * every resolved config carries a value via the schema default.
120
+ * that construct `FallbacksConfig` literals. The value domain is guarded
121
+ * by the schema (`Config` in `src/schema.ts`), NOT by
122
+ * `validateFallbacksConfig`, and every resolved config carries a value
123
+ * via the schema default.
105
124
  */
106
125
  presets?: 'bundled' | 'none';
107
- /**
108
- * Extra time-slot rows (plan fallbacks-timeslots Task 1, P5): the FIRST
109
- * matching row's chain becomes the effective root chain at request time;
110
- * `rootChain` (the all-day chain) is always the last row.
111
- * Optional on purpose, mirroring `presets` — additive, non-breaking for
112
- * library consumers. Malformed rows warn once and are skipped by the
113
- * resolver; the gateway rejects them on save (Task 3).
114
- */
115
- timeSlots?: SlotRowConfig[];
116
126
  /**
117
127
  * Config-level timezone for slot matching (default `Asia/Shanghai`,
118
128
  * UTC+8). Not per-slot.
@@ -123,41 +133,40 @@ export interface FallbacksConfig {
123
133
  * 1): `'timer'` restores the preferred candidate when the cooldown
124
134
  * expires (today's behavior); `'half-open'` leaves the route half-open
125
135
  * for one logged probe instead. Optional on purpose, mirroring
126
- * `presets` — a required field would break library consumers that
127
- * construct `FallbacksConfig` literals with the existing keys
128
- * (additive, non-breaking). The value domain is guarded by the schema
129
- * (`Config` in `src/schema.ts`), NOT by `validateFallbacksConfig`, and
130
- * every resolved config carries a value via the schema default.
136
+ * `presets`. The value domain is guarded by the schema, NOT by
137
+ * `validateFallbacksConfig`, and every resolved config carries a value
138
+ * via the schema default.
131
139
  */
132
140
  recovery?: RecoveryPolicy;
133
141
  }
134
142
  /**
135
- * Spec §4 defaults — `Config({})` must equal this (no-op install).
136
- * There is no `enabled` default (the key is removed — the Plugins-page row
137
- * toggle is the master switch): an unconfigured install (empty rootChain,
138
- * no time slots, empty roles) behaves exactly like an uninstalled plugin
139
- * (AC-3 / no-op invariant, {@link isFallbackActive}).
143
+ * Spec §1 defaults — `Config({})` must equal this (no-op install).
144
+ * Empty `chains` is the ONLY first-run state: nothing pre-creates an
145
+ * `Auto` chain and nothing seeds `entries` — the picker row appears only
146
+ * after the user creates a chain in the card. The migrated/seeded default
147
+ * chain (`{ id: 'Auto', label: '默认链', entries, timeSlots }`) exists only
148
+ * in the migration table (docs) applied by hand — warn-only migration.
140
149
  */
141
150
  export declare const defaultFallbacksConfig: FallbacksConfig;
151
+ /** Role id format (aligned with yet-another-subagent `isValidProfileId`). */
152
+ export declare const ROLE_ID_PATTERN: RegExp;
142
153
  /**
143
- * Reserved role id: legal as a rule target (`roles.rules[].role`) and as
144
- * the no-rule-match fallback, but FORBIDDEN in `roles.list[].id`.
154
+ * Reserved role id: FORBIDDEN in `roles.list[].id` (the validator's reserved
155
+ * warn) and in seed declarations (`seeds.ts` reserved-id skip). `rules` that
156
+ * could target it are deleted (v2), but the id itself stays reserved so a
157
+ * declared role can never shadow the "no specific role" meaning.
145
158
  */
146
159
  export declare const INHERIT_ROLE_ID = "inherit";
147
160
  /**
148
- * The no-op gate (AC-8), re-keyed from the removed `enabled` switch to
149
- * CONTENT PRESENCE (plan fallbacks-web-ux-alignment T2): the plugin
150
- * intervenes exactly when something is configured — a non-empty `rootChain`
151
- * (the all-day chain), at least one time-slot row, or at least one declared
152
- * role. An inactive config is a pass-through exactly like an uninstalled
153
- * plugin; the Plugins-page row toggle remains the master switch (row
154
- * disable = fiber destroy). ONE helper for every runtime gate — the former
155
- * `config.enabled` sites (request waterfalls, picker-row registration, TUI
156
- * summary, General-row badge) all read through this.
161
+ * The no-op gate (AC-8): the plugin intervenes exactly when at least one
162
+ * named chain is configured. Roles alone no longer activate (a role
163
+ * without a chain is pure identity). An inactive config is a pass-through
164
+ * exactly like an uninstalled plugin; the Plugins-page row toggle remains
165
+ * the master switch (row disable = fiber destroy). A non-empty chain with
166
+ * empty `entries` still registers the picker row (content presence) but
167
+ * refuses to delegate (conformance gate in the walk).
157
168
  */
158
169
  export declare function isFallbackActive(config: FallbacksConfig): boolean;
159
- /** Role id format (aligned with yet-another-subagent `isValidProfileId`). */
160
- export declare const ROLE_ID_PATTERN: RegExp;
161
170
  /**
162
171
  * Minimal logger surface {@link validateFallbacksConfig} warns through —
163
172
  * keeps this module free of `@deepseek-ai/*` imports (a cordis Logger is
@@ -168,29 +177,67 @@ export interface FallbacksConfigLogger {
168
177
  }
169
178
  /**
170
179
  * Validate a fallbacks config (pure, warn-only — never throws, never
171
- * mutates): role id format/uniqueness/reserved word, rule role references
172
- * (declared ids + the built-in `'inherit'`), the `fallback` enum,
173
- * `rootChain`/role-chain selector legality, and the role model-config
174
- * requirement (a declared role with a missing/empty chain warns — a role
175
- * without a model config is meaningless, plan fallbacks-feedback-round
176
- * T2). `persona` is free text and is deliberately not
177
- * validated. Each violation emits one `llm-fallbacks: ...` warn and "does
178
- * not take effect" — the config stays usable (spec §4 / AC-4
179
- * warn-not-crash semantics).
180
+ * mutates). Per chain: id format (case-sensitive, {@link CHAIN_ID_PATTERN})
181
+ * + cross-chain uniqueness, tail conformance (the LAST entry must be
182
+ * exactly one official model — a non-conforming chain's slot rows and
183
+ * delegate stay inert), selector legality per entry, and the slot-row
184
+ * guards (unknown preset, duplicate preset per chain, preset rows carrying
185
+ * their own windows). Per role (after the chain loop, so the declared id
186
+ * set is complete): the existing id format/reserved/duplicate warns, and a
187
+ * non-empty `chain` pointer naming no declared chain id earns ONE warn and
188
+ * behaves as NOT SPECIFIED. Each violation emits one `llm-fallbacks: ...`
189
+ * warn — the config stays usable (warn-not-crash semantics). `persona` and
190
+ * `label` are free text and deliberately not validated.
180
191
  */
181
192
  export declare function validateFallbacksConfig(config: FallbacksConfig, logger: FallbacksConfigLogger): void;
182
193
  /**
183
- * Detect legacy (two-block-era) leftovers in a config SOURCE — the composed
184
- * object `source()` returns, or a raw settings document. Recognizes the
185
- * removed `chains` key, the removed always-mode retry cap and dispatch
186
- * role-auto-match keys (`alwaysModeRetryCap` / `roleAutoMatch` — direction
187
- * C, virtual-chain-only fallback), the removed `roles.default` field, the
188
- * removed role-entity fields `roles.list[].label` / `roles.list[].description`
189
- * (renamed to `persona`), and `roles.rules[].role` values that reference no
190
- * declared `roles.list` id and are not the built-in `'inherit'`. Returns
191
- * descriptive key/role names; the gateway attaches them as `get().legacyKeys`
192
- * so the client can show a migration banner (spec §9 — the source is read
193
- * directly because schemastery retains unknown keys, verified plan Task 1
194
- * Step 1).
194
+ * Detect legacy config leftovers in a config SOURCE — the composed object
195
+ * `source()` returns, or a raw settings document. Two generations:
196
+ *
197
+ * - Older removals: `alwaysModeRetryCap`, `roleAutoMatch` (direction C),
198
+ * `roles.default`, and the renamed role-entity fields
199
+ * `roles.list[].label` / `roles.list[].description`.
200
+ * - v1 two-block keys (named-virtual-chains): `rootChain`, top-level
201
+ * `timeSlots`, `roles.rules`, `roles.list[].fallback`, and the v1 ARRAY
202
+ * form of `roles.list[].chain` (an array where v2 declares a string —
203
+ * stripped rather than coerced).
204
+ *
205
+ * Returns descriptive key names; the gateway attaches them as
206
+ * `get().legacyKeys` so the client can show a migration banner. Warn-only:
207
+ * no legacy value is ever read or re-persisted.
195
208
  */
196
209
  export declare function detectLegacyKeys(source: Record<string, unknown>): string[];
210
+ /**
211
+ * The chain a v1 `rootChain` migrates into (plan fallbacks-v1-auto-migration):
212
+ * the id is stable so an already-selected `FallbacksChain/Auto` route keeps
213
+ * resolving, and the label is display-only.
214
+ */
215
+ export declare const MIGRATED_CHAIN_ID = "Auto";
216
+ /** Display label of {@link MIGRATED_CHAIN_ID}. */
217
+ export declare const MIGRATED_CHAIN_LABEL = "\u9ED8\u8BA4\u94FE";
218
+ /**
219
+ * Upgrade a config SOURCE to the v2 named-chain shape — in memory, at every
220
+ * read. Pure, total, idempotent: a v2 config comes back unchanged, a v1 one is
221
+ * converted, and any malformed shape degrades to the defaults instead of
222
+ * throwing (the banner keeps reporting the raw leftovers, so honesty survives
223
+ * the coercion).
224
+ *
225
+ * Conversion rules (plan §Goal):
226
+ * 1. v1 `rootChain` + top-level `timeSlots` become chain #0
227
+ * (`{ id: 'Auto', label: '默认链', entries, timeSlots }`), so the picker
228
+ * route the user already selected keeps working.
229
+ * 2. `roles.list[]` survive as identity rows (`id` / `persona` / reserved
230
+ * `prompt` / `permissions`); a v1 ARRAY `chain` is dropped (NOT SPECIFIED —
231
+ * the card's 指定降级链 select takes over) and `fallback` is dropped;
232
+ * `roles.rules` / `roles.default` / `enabled` never reach the output.
233
+ * 3. A config that already carries a non-empty `chains` array keeps it and only
234
+ * loses the v1 leftovers. An EMPTY `chains` alongside a v1 `rootChain` is a
235
+ * profile that never had v2 content, so it migrates (otherwise the user's
236
+ * chain would silently vanish).
237
+ *
238
+ * No file is rewritten here — the next explicit save persists the v2 shape.
239
+ * @param source - the composed config (schema defaults → plugin row → user
240
+ * layer) or any raw document.
241
+ * @returns a complete {@link FallbacksConfig}; never the input object itself.
242
+ */
243
+ export declare function migrateLegacyConfig(source: unknown): FallbacksConfig;
package/dist/gateway.d.ts CHANGED
@@ -66,7 +66,7 @@
66
66
  * @module dsh-llm-fallbacks/gateway
67
67
  */
68
68
  import type { Context } from '@deepseek-ai/cordis';
69
- import { type SettingsNamespace } from '@deepseek-ai/dsh-settings';
69
+ import SettingsForms, { type SettingsNamespace } from '@deepseek-ai/dsh-settings';
70
70
  import { TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol';
71
71
  import type { TypertContribution } from '@deepseek-ai/dsh-typert-registry';
72
72
  import type { FallbacksConfig } from './config';
@@ -91,63 +91,6 @@ export declare const FALLBACKS_SETTINGS_NAMESPACE: SettingsNamespace;
91
91
  * reference `apply()` reads — no separate registration step.
92
92
  */
93
93
  export declare const FALLBACKS_PROFILE_ENTRY = "llm-fallbacks";
94
- /** One exact child LLM route on the additive `subagentPolicy` wire field (T5). */
95
- export type SubagentPolicyRoute = {
96
- provider: string;
97
- model: string;
98
- };
99
- /** Settings namespace of the host's subagent-model-selection policy. */
100
- export declare const SUBAGENT_POLICY_SETTINGS_NS = "subagent-model-selection-settings";
101
- /**
102
- * Additive `/api/fallbacks/get` host-policy snapshot (spec D4 / T5).
103
- * Omitted only when the snapshot is unwired (3-arg constructor) so older
104
- * clients and gateway unit tests keep a byte-identical `{ config,
105
- * legacyKeys, seeds }` payload. A wired snapshot always emits the field —
106
- * `{ state: 'disabled' }` included — so a write after an enabled get cannot
107
- * keep-last an allowlist. Never written through `set`/`reset`.
108
- */
109
- export type SubagentPolicyWire = {
110
- state: 'enabled';
111
- allowedModels: SubagentPolicyRoute[];
112
- head?: {
113
- route: SubagentPolicyRoute;
114
- source: 'authorized' | 'injected';
115
- };
116
- blockedAttempt?: {
117
- at: number;
118
- route: SubagentPolicyRoute;
119
- reason: string;
120
- };
121
- } | {
122
- state: 'disabled';
123
- } | {
124
- state: 'unprovable';
125
- };
126
- /**
127
- * Live host-policy snapshot the gateway projects onto the wire. Built in
128
- * `apply()` from the SAME T1 reader the override paths use — no second
129
- * policy source. Optional: the 3-arg constructor (gateway unit tests)
130
- * omits the field entirely.
131
- */
132
- export type SubagentPolicySnapshotFn = () => {
133
- policy: {
134
- state: 'disabled';
135
- } | {
136
- state: 'enabled';
137
- allowedModels: readonly SubagentPolicyRoute[];
138
- } | {
139
- state: 'unprovable';
140
- };
141
- head?: {
142
- route: SubagentPolicyRoute;
143
- source: 'authorized' | 'injected';
144
- };
145
- blockedAttempt?: {
146
- at: number;
147
- route: SubagentPolicyRoute;
148
- reason: string;
149
- };
150
- };
151
94
  /**
152
95
  * The live configuration source for the gateway (guide §7 — the same bridge
153
96
  * shape the runtime reads through). `source()` returns the live composed
@@ -157,7 +100,15 @@ export type SubagentPolicySnapshotFn = () => {
157
100
  * `onChange` fan-out was removed in the QC fix wave — nothing subscribed).
158
101
  */
159
102
  export interface FallbacksSettingsBridge {
103
+ /** The EFFECTIVE config the runtime reads (v1 already upgraded in memory). */
160
104
  source(): FallbacksConfig;
105
+ /**
106
+ * The STORED view, before the in-memory upgrade. Legacy detection reads it,
107
+ * so the migration banner keeps reporting keys the effective config no
108
+ * longer carries (plan fallbacks-v1-auto-migration). Optional: a caller
109
+ * without one is assumed to hold the raw shape itself.
110
+ */
111
+ rawSource?(): FallbacksConfig;
161
112
  }
162
113
  /** Patch shape accepted by `fallbacks.set` — any subset of the config keys. */
163
114
  export type FallbacksConfigPatch = Partial<FallbacksConfig>;
@@ -170,13 +121,6 @@ export interface FallbacksReadResult {
170
121
  config: FallbacksConfig;
171
122
  legacyKeys: string[];
172
123
  seeds: SeedsWireStatus[];
173
- /**
174
- * Host subagent-model-selection policy (spec D4). Additive — old readers
175
- * ignore it; omitted only when no snapshot is wired. A wired snapshot
176
- * always carries the field (`disabled` included) so keep-last cannot
177
- * retain an allowlist after policy-off.
178
- */
179
- subagentPolicy?: SubagentPolicyWire;
180
124
  }
181
125
  /** The `fallbacks/revert-seed` response — a read result plus the revert outcome. */
182
126
  export interface FallbacksRevertResult extends FallbacksReadResult {
@@ -200,12 +144,6 @@ export declare class FallbacksConfigGateway extends TypertRemoteService {
200
144
  private readonly seeds;
201
145
  /** The live settings service once the optional inject child activates. */
202
146
  private settings;
203
- /**
204
- * Optional host-policy snapshot (T5). When omitted the additive
205
- * `subagentPolicy` field is absent on every read — the 3-arg constructor
206
- * used by gateway unit tests stays byte-identical to the pre-T5 payload.
207
- */
208
- private readonly subagentPolicy;
209
147
  /**
210
148
  * @param ctx - owning context (the plugin fiber's ctx inside `apply`).
211
149
  * @param bridge - the same `FallbacksSettingsBridge` the runtime reads, so
@@ -213,22 +151,21 @@ export declare class FallbacksConfigGateway extends TypertRemoteService {
213
151
  * @param seeds - the per-apply `FallbacksSeedManager` constructed in
214
152
  * `apply()` — the gateway delegates badge state and revert to it (no
215
153
  * copied manager logic), through the io seam built over this bridge.
216
- * @param subagentPolicy - live host-policy snapshot from `apply()`; omit
217
- * to leave the additive field off the wire.
218
154
  */
219
- constructor(ctx: Context, bridge: FallbacksSettingsBridge, seeds: FallbacksSeedManager, subagentPolicy?: SubagentPolicySnapshotFn);
155
+ constructor(ctx: Context, bridge: FallbacksSettingsBridge, seeds: FallbacksSeedManager);
220
156
  /**
221
157
  * Read the current composed config (schema defaults → entry base → profile
222
158
  * patch). No hard-gate resolver (ADR-2): the raw composed config is
223
159
  * the wire value. The removed `enabled` key never crosses the wire (the
224
160
  * read whitelist drops it — the snapshot-strip half of LEGACY_KEYS).
225
- * @returns the wire-normalized composed config plus `legacyKeys` — legacy
226
- * two-block-era fields (`chains` / `roles.default` / undeclared rule
227
- * role refs) detected on the composed source (schemastery retains them,
228
- * plan Task 1 Step 1), so the client can show a migration banner (spec
229
- * §9, incremental field — old clients ignore it) — plus the additive
230
- * `seeds` badge state (spec §9.4, legacyKeys precedent — old clients
231
- * ignore it).
161
+ * @returns the wire-normalized composed config plus `legacyKeys` — every
162
+ * v1 field detected on the composed source (`rootChain`, top-level
163
+ * `timeSlots`, `roles.rules`, `roles.default`, `roles.list[].fallback`,
164
+ * the v1 ARRAY form of `roles.list[].chain`, plus the older
165
+ * `alwaysModeRetryCap` / `roleAutoMatch` / renamed role fields —
166
+ * schemastery retains them), so the client can show a migration banner
167
+ * (incremental field — old clients ignore it) — plus the additive `seeds`
168
+ * badge state (legacyKeys precedent — old clients ignore it).
232
169
  */
233
170
  get(): FallbacksReadResult;
234
171
  /**
@@ -277,22 +214,6 @@ export declare class FallbacksConfigGateway extends TypertRemoteService {
277
214
  * write channel is unavailable (KD-G5).
278
215
  */
279
216
  revertSeed(id: string): Promise<FallbacksRevertResult>;
280
- /**
281
- * Merge the CURRENT host allowlist with the exact routes configured in this
282
- * plugin's chains (every `roles.list[].chain` entry plus `rootChain`), plus
283
- * the virtual `FallbacksChain/Auto` route. De-duplicated by exact
284
- * provider+model with first-seen order kept (the current allowlist first).
285
- * Wildcard (`provider/*`) and unparseable entries are skipped — never
286
- * expanded into routes. This is the only allowlist write face; it runs
287
- * exclusively from the explicit card button (never silently) and writes
288
- * ONLY `allowedModels` — never `enabled`.
289
- * @returns the post-write read result (the fresh `subagentPolicy` snapshot
290
- * reflects the new allowlist).
291
- * @throws when no live policy snapshot is wired, the policy is not proven
292
- * enabled (`disabled` / `unprovable`), or no settings service is composed
293
- * (KD-G5: no allowlist write without a proven-enabled policy).
294
- */
295
- syncAllowlist(): Promise<FallbacksReadResult>;
296
217
  /**
297
218
  * Read the live composed config and normalize it to the typertGateway JSON
298
219
  * wire boundary. Containment (guide §10): a malformed stored patch section
@@ -318,26 +239,59 @@ export declare class FallbacksConfigGateway extends TypertRemoteService {
318
239
  */
319
240
  private readResult;
320
241
  /**
321
- * The io seam the seed manager writes through (spec §9.1): `read` walks
322
- * the same live bridge source the gateway reads; `writeRoles` persists a
323
- * full `{ list, rules }` to the plugin's profile-entry section — both
324
- * arrays always computed from a fresh composed read, so the write stays
325
- * correct under the settings merge's array-replace semantics and never
326
- * touches operator rules. The write channel fails with the same KD-G5
327
- * message as set/reset when no settings service is composed. Built fresh
328
- * per call so the mutable settings capture is read at call time (the
329
- * inject child swaps it when the settings service appears/disappears).
242
+ * The io seam the seed manager writes through (spec §9.1): `read` walks the
243
+ * same live bridge source the gateway reads; `writeRoles` persists the
244
+ * declared roles as `{ list }` — the v2 shape, whose rows are `{ id,
245
+ * persona, chain? }` — computed from a fresh composed read, so the write
246
+ * stays correct under the settings merge's replace-whole-array semantics
247
+ * and every operator-specified `chain` pointer survives verbatim. It goes
248
+ * through {@link writeSection}, so a seed write also drops any stored v1
249
+ * field. The write channel fails with the same KD-G5 message as set/reset
250
+ * when no settings service is composed. Built fresh per call so the mutable
251
+ * settings capture is read at call time (the inject child swaps it when the
252
+ * settings service appears/disappears).
330
253
  */
331
254
  private seedsIo;
255
+ /**
256
+ * Write a CLEANED patch and, in the same save, remove v1 fields the
257
+ * persisted section may still carry. `settings.update` is merge-only, so a
258
+ * stored `rootChain` / top-level `timeSlots` / `roles.rules` /
259
+ * `roles.list[].fallback` would otherwise survive every v2 save; the
260
+ * removals ride `settings.mutate`'s `unset` path ops, which touch only the
261
+ * named fields (unrelated configuration, foreign keys and redacted secrets
262
+ * are preserved — a wholesale `replace` rebuilt from a descriptor would not
263
+ * be).
264
+ * @param patch - already stripped of v1 fields and null values.
265
+ */
266
+ private writeSection;
332
267
  }
268
+ /**
269
+ * Persist a CLEANED patch and, in the same operation, remove the v1 fields the
270
+ * ACTIVE PROFILE's persisted section still holds. `settings.update` is
271
+ * merge-only, so a stored `rootChain` / top-level `timeSlots` / `roles.rules` /
272
+ * `roles.list[].fallback` would otherwise survive every v2 save; the removals
273
+ * ride `settings.mutate`'s path ops, which touch only the named fields
274
+ * (unrelated configuration, foreign keys and redacted secrets are preserved —
275
+ * a wholesale `replace` rebuilt from a descriptor would delete them).
276
+ *
277
+ * Exported because the plugin has TWO write entry points — the gateway
278
+ * (`set` / `writeRoles`) and the apply-level `SeedsIo` (`declareSeeds`,
279
+ * `revertSeededPersona`, the bundled preset self-declaration) — and the same
280
+ * logical operation must clean the same way through both.
281
+ * @param settings - the live settings service.
282
+ * @param patch - a patch; v1 fields are stripped here, callers need not.
283
+ * @param ns - profile entry id (defaults to the plugin's own).
284
+ */
285
+ export declare function writeFallbacksSection(settings: SettingsForms, patch: Record<string, unknown>, ns?: string): Promise<void>;
333
286
  /**
334
287
  * Reject a patch the `Config` schema cannot express: non-object input, unknown
335
288
  * top-level keys (schemastery merges them silently — the settings service
336
289
  * would accept them), and schema type violations — plus the Task 3 semantic
337
290
  * guards the permissive schema deliberately does not carry: nested `roles`
338
- * keys, `timeSlots` row shapes (unknown preset ids, duplicate presets, preset
339
- * rows carrying windows, non-`HH:mm` custom bounds, out-of-range days, empty
340
- * chains), and the all-day `rootChain` head conformance.
291
+ * keys, `chains[]` row shape (id pattern, case-sensitive uniqueness, label),
292
+ * per-chain `timeSlots` row shapes (unknown preset ids, duplicate presets,
293
+ * preset rows carrying windows, non-`HH:mm` custom bounds, out-of-range days,
294
+ * empty chains), and each chain's non-empty `entries` tail conformance.
341
295
  *
342
296
  * Exported (plan fallbacks-tui-settings Task 1): the TUI settings section's
343
297
  * JSON-field `parse` mirrors the gateway save rules through this single