dsh-llm-fallbacks 0.3.0 → 0.3.2

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.
@@ -11,12 +11,17 @@
11
11
  * the command is silently absent when no command registry is composed (no
12
12
  * top-level inject pollution — advisor T1 fix).
13
13
  *
14
- * The handler is **read-only**: it never mutates fallback state (no cooldown
15
- * reset, no pending-switch writes). zh/en copy lives in this file — the
16
- * client half's `src/client/locales.ts` is a separate client-side dictionary.
17
- * The host carries no per-session locale signal (the `locale` service is
18
- * client-side), so the wiring picks a deterministic default (`zh`, this
19
- * repo's primary language); both dictionaries are unit-tested.
14
+ * The bare diagnostic and the config readback are **read-only**: they never
15
+ * mutate fallback state (no cooldown reset, no pending-switch writes). The
16
+ * `config revert-seed <role-id>` subcommand is the one write action (plan
17
+ * fallbacks-tui-settings Task 2, AC-3): it delegates to the controller's
18
+ * `revertSeed` (wired to the seeds service) and prints the outcome — a
19
+ * web-card action capability the settings seam cannot express. zh/en copy
20
+ * lives in this file — the client half's `src/client/locales.ts` is a
21
+ * separate client-side dictionary. The host carries no per-session locale
22
+ * signal (the `locale` service is client-side), so the wiring picks a
23
+ * deterministic default (`zh`, this repo's primary language); both
24
+ * dictionaries are unit-tested.
20
25
  *
21
26
  * @module dsh-llm-fallbacks/commands
22
27
  */
@@ -24,7 +29,8 @@ import type { CommandDefinition } from '@deepseek-ai/dsh-commands';
24
29
  import { type FallbacksRole } from './config.ts';
25
30
  import type { Origin } from './roles.ts';
26
31
  import type { FallbacksSwitchEventData } from './events.ts';
27
- import type { SlotRowConfig } from './time-slots.ts';
32
+ import type { SeedRevertFailReason } from './seeds.ts';
33
+ import { type SlotRowConfig } from './time-slots.ts';
28
34
  /** How many recent `fallbacks/switch` events `/fallbacks` shows (newest first). */
29
35
  export declare const RECENT_SWITCHES_LIMIT = 5;
30
36
  /** Minimal command registry surface (satisfied by the dsh `CommandService`). */
@@ -90,15 +96,40 @@ export interface FallbacksCommandSnapshot {
90
96
  * entities carry id/persona/chain/fallback (NO per-role `model`;
91
97
  * `provider`/`model` live on `roles.rules`), so the readback line is id +
92
98
  * chain count, never a rules dump.
99
+ *
100
+ * Enriched readback (plan fallbacks-tui-settings Task 2, AC-4): timeSlots /
101
+ * tz / roles.rules join the summary so TUI operators can verify `/settings`
102
+ * edits. One summarized time-slot row: preset rows carry `{ preset,
103
+ * chainCount }` (windows are frozen code constants in {@link PRESETS} and
104
+ * never stored on the row — the render resolves the window text from
105
+ * PRESETS); custom rows carry `{ start, end, chainCount }`. Rules rows carry
106
+ * `{ provider, model, role }` — an omitted provider/model (wildcard match at
107
+ * runtime) is summarized as `''` and rendered as `*`.
93
108
  */
94
109
  export interface FallbacksConfigSummary {
95
110
  readonly enabled: boolean;
96
111
  readonly triggerCodes: readonly string[];
97
112
  readonly rootChain: readonly string[];
113
+ /** Summarized time-slot rows (preset rows carry `preset`, custom rows carry `start`/`end`; both may carry a day mask). */
114
+ readonly timeSlots: readonly {
115
+ preset?: string;
116
+ start?: string;
117
+ end?: string;
118
+ days?: readonly number[];
119
+ chainCount: number;
120
+ }[];
121
+ /** Config-level timezone for slot matching (default `Asia/Shanghai`). */
122
+ readonly tz: string;
98
123
  readonly roles: readonly {
99
124
  id: string;
100
125
  chainCount: number;
101
126
  }[];
127
+ /** Summarized role rules: provider/model patterns → declared role (or the built-in `'inherit'`). */
128
+ readonly rules: readonly {
129
+ provider: string;
130
+ model: string;
131
+ role: string;
132
+ }[];
102
133
  readonly cooldownMs: number;
103
134
  readonly revertPolicy: string;
104
135
  readonly maxSwitchesPerStep: number;
@@ -122,6 +153,23 @@ export interface FallbacksCommandController {
122
153
  * same live config source the runtime reads. Never mutates state.
123
154
  */
124
155
  getConfig(): FallbacksConfigSummary;
156
+ /**
157
+ * Revert one role's persona to its CURRENT declared seed default (AC-3).
158
+ * Surfaces the outcome as VALUES, not copy (qc1 F-003 / qc2 F-006 / qc3
159
+ * F-003): `ok: true` when reverted; `ok: false` with the
160
+ * {@link SeedRevertFailReason} code explaining why not (not a seeded role
161
+ * / role row absent). The command handler localizes the code per its
162
+ * registration locale — the controller never pre-localizes. A
163
+ * settings-write failure propagates loudly as a rejected promise (the
164
+ * same contract as the seeds service — never swallowed into an
165
+ * `ok: false`); the handler maps it to a structured error. Implemented by
166
+ * the wiring against the per-apply seed manager
167
+ * (`seeds.revert(roleId, seedsIo)`), not the typert gateway.
168
+ */
169
+ revertSeed(roleId: string): Promise<{
170
+ ok: boolean;
171
+ reason?: SeedRevertFailReason;
172
+ }>;
125
173
  }
126
174
  /** zh/en dictionaries for the `/fallbacks` output. */
127
175
  export declare const FALLBACKS_COMMAND_LOCALES: {
@@ -129,7 +177,8 @@ export declare const FALLBACKS_COMMAND_LOCALES: {
129
177
  readonly title: "当前会话 fallback 诊断(只读)";
130
178
  readonly description: "查看当前会话的降级链、最近降级切换与冷却状态(只读)";
131
179
  readonly usageConfig: "查看组合后的 fallbacks 配置(设置回读)";
132
- readonly usage: " /fallbacks config 查看组合后的 fallbacks 配置(设置回读)";
180
+ readonly usageRevertSeed: "将角色的 persona 还原为已声明的 Seed 默认";
181
+ readonly usage: " /fallbacks config 查看组合后的 fallbacks 配置(设置回读)\n /fallbacks config revert-seed <role-id> 将角色的 persona 还原为已声明的 Seed 默认";
133
182
  readonly origin: "会话来源";
134
183
  readonly role: "角色";
135
184
  readonly chain: "链";
@@ -153,22 +202,37 @@ export declare const FALLBACKS_COMMAND_LOCALES: {
153
202
  readonly configTriggerCodes: "触发码";
154
203
  readonly configRootChain: "根链";
155
204
  readonly configEmpty: "(空)";
205
+ readonly configTimeSlots: "分时槽";
206
+ readonly configSlotPresetItem: "{preset}(chain: {n}, window {window})";
207
+ readonly configSlotPresetBare: "{preset}(chain: {n})";
208
+ readonly configSlotCustomItem: "custom {start}-{end}(chain: {n})";
209
+ readonly configTz: "时区";
156
210
  readonly configRoles: "角色";
157
211
  readonly configRoleItem: "{id}(chain: {n})";
212
+ readonly configRules: "角色规则";
158
213
  readonly configCooldown: "冷却";
159
214
  readonly configRevert: "回主策略";
160
215
  readonly configMaxSwitches: "单步最大切换";
161
216
  readonly configAlwaysCap: "always 上限";
162
217
  readonly configPresets: "预置";
163
218
  readonly configRoleAutoMatch: "角色自动匹配";
164
- readonly configEdit: "编辑:~/.dsh/profiles/<profile>/cordis.patch.yml(插件行)或 $DSH_HOME/settings.yaml(fallbacks: 分节)";
165
- readonly configEditHint: "TUI 无法修改配置——只能编辑文件";
219
+ readonly configEdit: "编辑:/settings(TUI 设置界面)或 ~/.dsh/profiles/<profile>/cordis.patch.yml(插件行)/ $DSH_HOME/settings.yaml(fallbacks: 分节)";
220
+ readonly configEditHint: "TUI 通过 /settings 修改配置;文件编辑仍然可用";
221
+ readonly revertSeedOk: "角色 {id} 已还原为 Seed 默认";
222
+ readonly revertSeedFail: "角色 {id} 未还原({reason})";
223
+ readonly revertSeedError: "角色 {id} 还原失败(设置写入失败)";
224
+ readonly revertSeedReason: {
225
+ readonly 'not-seeded': "未声明种子";
226
+ readonly 'row-absent': "角色行不存在";
227
+ readonly 'settings-unavailable': "设置通道不可用";
228
+ };
166
229
  };
167
230
  readonly en: {
168
231
  readonly title: "Session fallback diagnostics (read-only)";
169
232
  readonly description: "Inspect fallback chain, recent fallback switches, and cooldown for this session (read-only)";
170
233
  readonly usageConfig: "show the composed fallbacks config (settings readback)";
171
- readonly usage: " /fallbacks config show the composed fallbacks config (settings readback)";
234
+ readonly usageRevertSeed: "revert a role's persona to its declared seed default";
235
+ readonly usage: " /fallbacks config show the composed fallbacks config (settings readback)\n /fallbacks config revert-seed <role-id> revert a role's persona to its declared seed default";
172
236
  readonly origin: "Session origin";
173
237
  readonly role: "Role";
174
238
  readonly chain: "Chain";
@@ -192,29 +256,53 @@ export declare const FALLBACKS_COMMAND_LOCALES: {
192
256
  readonly configTriggerCodes: "Trigger codes";
193
257
  readonly configRootChain: "Root chain";
194
258
  readonly configEmpty: "(empty)";
259
+ readonly configTimeSlots: "Time slots";
260
+ readonly configSlotPresetItem: "{preset} (chain: {n}, window {window})";
261
+ readonly configSlotPresetBare: "{preset} (chain: {n})";
262
+ readonly configSlotCustomItem: "custom {start}-{end} (chain: {n})";
263
+ readonly configTz: "TZ";
195
264
  readonly configRoles: "Roles";
196
265
  readonly configRoleItem: "{id} (chain: {n})";
266
+ readonly configRules: "Rules";
197
267
  readonly configCooldown: "Cooldown";
198
268
  readonly configRevert: "Revert";
199
269
  readonly configMaxSwitches: "Max switches/step";
200
270
  readonly configAlwaysCap: "Always-mode cap";
201
271
  readonly configPresets: "Presets";
202
272
  readonly configRoleAutoMatch: "Auto-match";
203
- readonly configEdit: "Edit: ~/.dsh/profiles/<profile>/cordis.patch.yml (plugin row) or $DSH_HOME/settings.yaml (fallbacks: section)";
204
- readonly configEditHint: "TUI cannot change config — edit files only";
273
+ readonly configEdit: "Edit: /settings (TUI settings screen) or ~/.dsh/profiles/<profile>/cordis.patch.yml (plugin row) / $DSH_HOME/settings.yaml (fallbacks: section)";
274
+ readonly configEditHint: "TUI edits config via /settings; file editing still works";
275
+ readonly revertSeedOk: "role {id} reverted to its seed default";
276
+ readonly revertSeedFail: "role {id} not reverted ({reason})";
277
+ readonly revertSeedError: "role {id} revert failed (settings write failed)";
278
+ readonly revertSeedReason: {
279
+ readonly 'not-seeded': "not a seeded role";
280
+ readonly 'row-absent': "role row absent";
281
+ readonly 'settings-unavailable': "settings channel unavailable";
282
+ };
205
283
  };
206
284
  };
207
285
  /** A locale id supported by {@link FALLBACKS_COMMAND_LOCALES}. */
208
286
  export type FallbacksCommandLocale = keyof typeof FALLBACKS_COMMAND_LOCALES;
209
- /** The `/fallbacks` subcommands: `'config'` (composed-config readback) or
210
- * `''` (the bare session snapshot). */
211
- export type FallbacksSubcommand = '' | 'config';
287
+ /** The `/fallbacks` subcommands: `'config'` (composed-config readback),
288
+ * `'revert-seed'` (persona revert action, requires a role id), or `''` (the
289
+ * bare session snapshot). */
290
+ export type FallbacksSubcommand = '' | 'config' | 'revert-seed';
291
+ /** One parsed subcommand invocation: the kind plus the optional role id
292
+ * carried by `revert-seed`. */
293
+ export interface FallbacksSubcommandParse {
294
+ readonly kind: FallbacksSubcommand;
295
+ /** The role id argument of `revert-seed` (present iff `kind === 'revert-seed'`). */
296
+ readonly arg?: string;
297
+ }
212
298
  /**
213
299
  * Map an invocation's rawInput to a subcommand: trimmed `'config'` →
214
- * `'config'`; everything else (incl. empty) → `''` (bare snapshot). Lenient
215
- * by design — unknown input keeps today's bare behavior, never errors.
300
+ * `{ kind: 'config' }`; `'config revert-seed <id>'` → `{ kind: 'revert-seed',
301
+ * arg: id }`; everything else (incl. empty, a missing id, or an unknown
302
+ * subcommand under `config`) → `{ kind: '' }` (bare snapshot). Lenient by
303
+ * design — unknown input keeps today's bare behavior, never errors.
216
304
  */
217
- export declare function parseFallbacksSubcommand(rawInput: string): FallbacksSubcommand;
305
+ export declare function parseFallbacksSubcommand(rawInput: string): FallbacksSubcommandParse;
218
306
  /**
219
307
  * The newest `limit` `fallbacks/switch` events from a session's raw event
220
308
  * log, newest first. Unknown event shapes and malformed `fallbacks/switch`
@@ -248,18 +336,23 @@ export declare function resolveChainForDiagnostic(roles: readonly FallbacksRole[
248
336
  readonly inherit: boolean;
249
337
  };
250
338
  /**
251
- * Cap for long list lines in the composed-config readback: beyond this many
252
- * items a line truncates with `…` (the `Roles:` count always stays the FULL
253
- * count). Same sanity scale as {@link RECENT_SWITCHES_LIMIT}.
339
+ * Cap for the composed-config readback's LIST lines — Trigger codes, Root
340
+ * chain, Time slots, Roles, and Rules (qc2 Task-2 Minor): beyond this many
341
+ * items a line truncates with `…` while its leading count always stays the
342
+ * FULL count. Same sanity scale as {@link RECENT_SWITCHES_LIMIT}.
254
343
  */
255
344
  export declare const FALLBACKS_CONFIG_LIST_CAP = 5;
256
345
  /**
257
346
  * Render the `/fallbacks config` surface (plan fallbacks-tui-client T2,
258
- * AC-2): the composed `fallbacks` namespace as the runtime reads it + file-only
259
- * edit hints (TUI has no write surface). The FIRST LINE marks the
260
- * composed-config readback — distinct from the diagnostic title and never
261
- * merged into {@link fallbacksCommandText} (two operator surfaces, product
262
- * lock). Locale defaults to `zh` (the command default); en dictionary tested.
347
+ * AC-2 + fallbacks-tui-settings Task 2 AC-4): the composed `fallbacks`
348
+ * namespace as the runtime reads it — enriched with the time-slot rows
349
+ * (preset rows resolve their frozen window from {@link PRESETS}), the
350
+ * config timezone, and the role rules — plus edit hints pointing at the
351
+ * `/settings` TUI edit surface while keeping the file-edit documentation.
352
+ * The FIRST LINE marks the composed-config readback — distinct from the
353
+ * diagnostic title and never merged into {@link fallbacksCommandText} (two
354
+ * operator surfaces, product lock). Locale defaults to `zh` (the command
355
+ * default); en dictionary tested.
263
356
  */
264
357
  export declare function fallbacksConfigText(summary: FallbacksConfigSummary, locale?: FallbacksCommandLocale): string;
265
358
  /**
package/dist/gateway.d.ts CHANGED
@@ -214,6 +214,23 @@ export declare class FallbacksConfigGateway extends TypertRemoteService {
214
214
  */
215
215
  private seedsIo;
216
216
  }
217
+ /**
218
+ * Reject a patch the `Config` schema cannot express: non-object input, unknown
219
+ * top-level keys (schemastery merges them silently — the settings service
220
+ * would accept them), and schema type violations — plus the Task 3 semantic
221
+ * guards the permissive schema deliberately does not carry: nested `roles`
222
+ * keys, `timeSlots` row shapes (unknown preset ids, duplicate presets, preset
223
+ * rows carrying windows, non-`HH:mm` custom bounds, out-of-range days, empty
224
+ * chains), and the all-day `rootChain` head conformance.
225
+ *
226
+ * Exported (plan fallbacks-tui-settings Task 1): the TUI settings section's
227
+ * JSON-field `parse` mirrors the gateway save rules through this single
228
+ * validator (it internally routes `timeSlots` → {@link validateTimeSlotsPatch}),
229
+ * so an invalid draft blocks the TUI save exactly like a rejected gateway
230
+ * patch. No shared-module extraction — the two-keyword diff is the minimal
231
+ * change.
232
+ */
233
+ export declare function validateConfigPatch(patch: unknown): void;
217
234
  /**
218
235
  * The explicit typert contribution for the `fallbacks` gateway endpoints —
219
236
  * registered via `ctx.typert.register(...)` (see `apply` in `src/index.ts`).