dsh-llm-fallbacks 0.2.1 → 0.3.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/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { createRequire } from "node:module";
2
2
  import { installSettingsSection, settingsNamespace } from "@deepseek-ai/dsh-settings";
3
- import * as dshSession from "@deepseek-ai/dsh-session";
4
3
  import z from "@deepseek-ai/schemastery";
4
+ import { LlmAdapter, LlmError, createUserMessage } from "@deepseek-ai/dsh-llm";
5
5
  import { TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
6
6
  //#region .build/host/selectors.js
7
7
  var SelectorError = class extends Error {
@@ -35,6 +35,202 @@ function resolveWildcardEntry(failingModel, provider) {
35
35
  raw: `${provider}/${failingModel}`
36
36
  };
37
37
  }
38
+ const PRESETS = {
39
+ "liang-peak": {
40
+ windows: [{
41
+ start: "09:00",
42
+ end: "12:00"
43
+ }, {
44
+ start: "14:00",
45
+ end: "18:00"
46
+ }],
47
+ complement: false,
48
+ label: "Liang Peak"
49
+ },
50
+ "liang-valley": {
51
+ windows: [{
52
+ start: "09:00",
53
+ end: "12:00"
54
+ }, {
55
+ start: "14:00",
56
+ end: "18:00"
57
+ }],
58
+ complement: true,
59
+ label: "Liang Valley"
60
+ },
61
+ "glm-peak": {
62
+ windows: [{
63
+ start: "14:00",
64
+ end: "18:00",
65
+ days: [
66
+ 1,
67
+ 2,
68
+ 3,
69
+ 4,
70
+ 5
71
+ ]
72
+ }],
73
+ complement: false,
74
+ label: "GLM Peak"
75
+ },
76
+ "glm-valley": {
77
+ windows: [{
78
+ start: "14:00",
79
+ end: "18:00",
80
+ days: [
81
+ 1,
82
+ 2,
83
+ 3,
84
+ 4,
85
+ 5
86
+ ]
87
+ }],
88
+ complement: true,
89
+ label: "GLM Valley"
90
+ }
91
+ };
92
+ const HHMM_RE = /^([01]\d|2[0-3]):[0-5]\d$/;
93
+ const WEEKDAY_INDEX = {
94
+ Sun: 0,
95
+ Mon: 1,
96
+ Tue: 2,
97
+ Wed: 3,
98
+ Thu: 4,
99
+ Fri: 5,
100
+ Sat: 6
101
+ };
102
+ const warnedMalformedRows = new WeakSet();
103
+ const warnedTimeZones = new Set();
104
+ function warnMalformed(row, reason) {
105
+ if (typeof row !== "object" || row === null) {
106
+ console.warn(`llm-fallbacks: skipping malformed time-slot row (${reason})`);
107
+ return;
108
+ }
109
+ if (warnedMalformedRows.has(row)) return;
110
+ warnedMalformedRows.add(row);
111
+ console.warn(`llm-fallbacks: skipping malformed time-slot row (${reason}): ${JSON.stringify(row)}`);
112
+ }
113
+ function wallClock(now, tz) {
114
+ try {
115
+ const parts = new Intl.DateTimeFormat("en-US", {
116
+ timeZone: tz,
117
+ weekday: "short",
118
+ hour: "2-digit",
119
+ minute: "2-digit",
120
+ hourCycle: "h23"
121
+ }).formatToParts(now);
122
+ let weekday = 0;
123
+ let minutes = 0;
124
+ for (const part of parts) if (part.type === "weekday") weekday = WEEKDAY_INDEX[part.value] ?? 0;
125
+ else if (part.type === "hour") minutes += Number(part.value) * 60;
126
+ else if (part.type === "minute") minutes += Number(part.value);
127
+ return {
128
+ weekday,
129
+ minutes
130
+ };
131
+ } catch (error) {
132
+ if (!warnedTimeZones.has(tz)) {
133
+ warnedTimeZones.add(tz);
134
+ console.warn(`llm-fallbacks: invalid timezone "${tz}" (${error.message}) — slot matching falls back to UTC`);
135
+ }
136
+ return {
137
+ weekday: now.getUTCDay(),
138
+ minutes: now.getUTCHours() * 60 + now.getUTCMinutes()
139
+ };
140
+ }
141
+ }
142
+ function minutesOf(hhmm) {
143
+ const [hour, minute] = hhmm.split(":").map(Number);
144
+ return hour * 60 + minute;
145
+ }
146
+ function containsWindow(window, clock) {
147
+ if (!(window.days === void 0 || window.days.length === 0 || window.days.includes(clock.weekday))) return false;
148
+ const start = minutesOf(window.start);
149
+ const end = minutesOf(window.end);
150
+ return start <= end ? start <= clock.minutes && clock.minutes < end : start <= clock.minutes || clock.minutes < end;
151
+ }
152
+ function matchesAnyWindow(windows, clock) {
153
+ return windows.some((window) => containsWindow(window, clock));
154
+ }
155
+ function describeRow(row) {
156
+ if (!Array.isArray(row.chain) || row.chain.length === 0) {
157
+ warnMalformed(row, "empty chain");
158
+ return;
159
+ }
160
+ if (row.kind === "preset") {
161
+ const preset = row.preset;
162
+ if (typeof preset !== "string" || !Object.hasOwn(PRESETS, preset)) {
163
+ warnMalformed(row, `unknown preset ${JSON.stringify(preset)}`);
164
+ return;
165
+ }
166
+ if (row.start !== void 0 || row.end !== void 0 || row.days !== void 0 && row.days.length > 0) {
167
+ warnMalformed(row, `preset windows are fixed — row "${preset}" cannot carry start/end/days`);
168
+ return;
169
+ }
170
+ return PRESETS[preset];
171
+ }
172
+ if (row.kind === "custom") {
173
+ const { start, end } = row;
174
+ if (typeof start !== "string" || typeof end !== "string" || !HHMM_RE.test(start) || !HHMM_RE.test(end)) {
175
+ warnMalformed(row, `invalid custom window ${JSON.stringify(start)}-${JSON.stringify(end)} (expected HH:mm)`);
176
+ return;
177
+ }
178
+ return {
179
+ windows: [{
180
+ start,
181
+ end,
182
+ days: row.days
183
+ }],
184
+ complement: false
185
+ };
186
+ }
187
+ warnMalformed(row, `unknown kind ${JSON.stringify(row.kind)}`);
188
+ }
189
+ function labelOf(row) {
190
+ if (row.kind === "preset" && typeof row.preset === "string" && Object.hasOwn(PRESETS, row.preset)) return PRESETS[row.preset].label;
191
+ return row.name !== void 0 && row.name.trim() !== "" ? row.name : `custom ${row.start}-${row.end}`;
192
+ }
193
+ function isAllDayConforming(chain) {
194
+ if (chain.length < 1) return false;
195
+ const tail = chain[chain.length - 1];
196
+ return tail === "deepseek-official/deepseek-v4-flash" || tail === "deepseek-official/deepseek-v4-pro";
197
+ }
198
+ function resolveEffectiveChain(config, now, tz) {
199
+ const state = resolveSlotState(config, now, tz);
200
+ return state.winner === "all-day" ? config.rootChain : state.winner.chain;
201
+ }
202
+ function resolveSlotState(config, now, tz) {
203
+ if (!isAllDayConforming(config.rootChain)) return {
204
+ winner: "all-day",
205
+ label: "all-day"
206
+ };
207
+ const clock = wallClock(now, tz);
208
+ const seenPresets = new Set();
209
+ const rows = Array.isArray(config.timeSlots) ? config.timeSlots : [];
210
+ for (const row of rows) {
211
+ if (typeof row !== "object" || row === null) {
212
+ warnMalformed(row, "row is not an object");
213
+ continue;
214
+ }
215
+ if (row.kind === "preset" && typeof row.preset === "string") {
216
+ if (seenPresets.has(row.preset)) {
217
+ warnMalformed(row, `duplicate preset "${row.preset}" — only the first row takes effect`);
218
+ continue;
219
+ }
220
+ seenPresets.add(row.preset);
221
+ }
222
+ const descriptor = describeRow(row);
223
+ if (descriptor === void 0) continue;
224
+ if (descriptor.complement ? !matchesAnyWindow(descriptor.windows, clock) : matchesAnyWindow(descriptor.windows, clock)) return {
225
+ winner: row,
226
+ label: labelOf(row)
227
+ };
228
+ }
229
+ return {
230
+ winner: "all-day",
231
+ label: "all-day"
232
+ };
233
+ }
38
234
  //#endregion
39
235
  //#region .build/host/config.js
40
236
  const defaultFallbacksConfig = {
@@ -53,7 +249,10 @@ const defaultFallbacksConfig = {
53
249
  revertPolicy: "cooldown-expiry",
54
250
  maxSwitchesPerStep: 8,
55
251
  alwaysModeRetryCap: 5,
56
- presets: "bundled"
252
+ presets: "bundled",
253
+ roleAutoMatch: true,
254
+ timeSlots: [],
255
+ tz: "Asia/Shanghai"
57
256
  };
58
257
  const INHERIT_ROLE_ID = "inherit";
59
258
  const ROLE_ID_PATTERN = /^[a-z0-9-]{1,32}$/;
@@ -73,6 +272,7 @@ function validateFallbacksConfig(config, logger) {
73
272
  if ((role.chain ?? []).length === 0) logger.warn(`llm-fallbacks: role "${role.id}" has no model config — declare at least one chain entry, or use the built-in "inherit" rule target instead`);
74
273
  if (role.fallback !== void 0 && role.fallback !== "inherit-root" && role.fallback !== "none") logger.warn(`llm-fallbacks: role "${role.id}" has invalid fallback "${String(role.fallback)}" — expected "inherit-root" or "none"`);
75
274
  }
275
+ if (config.rootChain.length > 0 && !isAllDayConforming(config.rootChain)) logger.warn("llm-fallbacks: rootChain must end with exactly one official V4 model (deepseek-official/deepseek-v4-flash or deepseek-official/deepseek-v4-pro) — time-slot rows and the virtual picker row stay inert until the all-day chain tail conforms");
76
276
  for (const entry of config.rootChain) try {
77
277
  parseSelector(entry);
78
278
  } catch (error) {
@@ -80,6 +280,23 @@ function validateFallbacksConfig(config, logger) {
80
280
  }
81
281
  const validTargets = new Set([...declaredIds, INHERIT_ROLE_ID]);
82
282
  for (const rule of config.roles.rules) if (!validTargets.has(rule.role.trim())) logger.warn(`llm-fallbacks: rule references undeclared role "${rule.role}" — expected one of roles.list ids or "inherit"`);
283
+ if (Array.isArray(config.timeSlots)) {
284
+ const seenSlotPresets = new Set();
285
+ for (const row of config.timeSlots) {
286
+ if (row.kind !== "preset") continue;
287
+ const preset = row.preset;
288
+ if (typeof preset !== "string" || !Object.hasOwn(PRESETS, preset)) {
289
+ logger.warn(`llm-fallbacks: unknown time-slot preset ${JSON.stringify(preset)} — row ignored`);
290
+ continue;
291
+ }
292
+ if (seenSlotPresets.has(preset)) {
293
+ logger.warn(`llm-fallbacks: duplicate time-slot preset "${preset}" — only the first row takes effect`);
294
+ continue;
295
+ }
296
+ seenSlotPresets.add(preset);
297
+ if (row.start !== void 0 || row.end !== void 0 || row.days !== void 0 && row.days.length > 0) logger.warn(`llm-fallbacks: preset "${preset}" carries its own window/day fields — preset windows are fixed code constants; the row is ignored`);
298
+ }
299
+ }
83
300
  }
84
301
  function detectLegacyKeys(source) {
85
302
  const keys = [];
@@ -139,8 +356,110 @@ const Config = z.object({
139
356
  revertPolicy: z.union([z.const("cooldown-expiry"), z.const("never")]).default("cooldown-expiry"),
140
357
  maxSwitchesPerStep: z.number().default(8),
141
358
  alwaysModeRetryCap: z.number().default(5),
142
- presets: z.union([z.const("bundled"), z.const("none")]).default("bundled")
359
+ presets: z.union([z.const("bundled"), z.const("none")]).default("bundled"),
360
+ roleAutoMatch: z.boolean().default(true),
361
+ timeSlots: z.array(z.object({
362
+ kind: z.string(),
363
+ preset: z.string(),
364
+ start: z.string(),
365
+ end: z.string(),
366
+ days: z.array(z.number()),
367
+ chain: z.array(z.string())
368
+ })).default([]),
369
+ tz: z.string().default("Asia/Shanghai")
143
370
  });
371
+ function buildAutomatchPrompt(roles, agent) {
372
+ const lines = [
373
+ "You are assigning a subagent to its best-fit role from a declared taxonomy.",
374
+ "",
375
+ "Declared roles:",
376
+ ...roles.map((role) => `- ${role.id.trim()}: ${role.persona}`),
377
+ "",
378
+ `Agent context — origin: ${agent.session?.header?.origin ?? "root"}`
379
+ ];
380
+ const preset = agent.session?.header?.agentPreset?.trim();
381
+ if (preset !== void 0 && preset !== "") lines.push(`agentPreset: ${preset}`);
382
+ lines.push("", "Reply with EXACTLY ONE of the declared role ids above, or the literal \"none\" when no role fits.", "Do not add any explanation, punctuation, or other text.");
383
+ return lines.join("\n");
384
+ }
385
+ function parseAutomatchAnswer(answer, roleIds) {
386
+ const cleaned = answer.replace(/^[^a-zA-Z0-9-]+/, "").replace(/[^a-zA-Z0-9-]+$/, "");
387
+ if (cleaned === "") return null;
388
+ const key = cleaned.toLowerCase();
389
+ if (key === "none") return null;
390
+ return roleIds.get(key) ?? null;
391
+ }
392
+ async function pickRoleByLlm(ctx, roles, agent, opts) {
393
+ try {
394
+ const list = Array.isArray(roles.list) ? roles.list : [];
395
+ if (list.length === 0) return null;
396
+ const llm = ctx.get("llm");
397
+ if (llm === void 0) return null;
398
+ const route = pickJudgmentRoute(list, agent);
399
+ if (route === null) return null;
400
+ const roleIds = new Map(list.map((role) => [role.id.trim(), role.id]));
401
+ const answer = await streamRoleAnswer(llm, route, buildAutomatchPrompt(list, agent), opts);
402
+ if (answer === null) return null;
403
+ return parseAutomatchAnswer(answer, roleIds);
404
+ } catch (error) {
405
+ opts.warn(`llm-fallbacks: auto-match failed — falling back to "inherit": ${describeError(error)}`);
406
+ return null;
407
+ }
408
+ }
409
+ function pickJudgmentRoute(roles, agent) {
410
+ for (const role of roles) for (const entry of role.chain ?? []) try {
411
+ const selector = parseSelector(entry);
412
+ if (selector.model !== void 0) return {
413
+ provider: selector.provider,
414
+ model: selector.model
415
+ };
416
+ } catch {}
417
+ const provider = agent.options?.provider;
418
+ const model = agent.options?.model;
419
+ if (provider !== void 0 && model !== void 0) return {
420
+ provider,
421
+ model
422
+ };
423
+ return null;
424
+ }
425
+ async function streamRoleAnswer(llm, route, prompt, opts) {
426
+ const controller = new AbortController();
427
+ const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? 5e3);
428
+ let text = "";
429
+ let finishKind;
430
+ try {
431
+ for await (const chunk of llm.stream({
432
+ provider: route.provider,
433
+ model: route.model,
434
+ system: prompt,
435
+ messages: [createUserMessage({
436
+ content: [{
437
+ type: "text",
438
+ text: "Pick the best-fit role for this agent."
439
+ }],
440
+ source: { kind: "user" }
441
+ })],
442
+ maxTokens: 32,
443
+ signal: controller.signal
444
+ })) if (chunk.type === "text-delta") text += chunk.text;
445
+ else if (chunk.type === "finish") {
446
+ finishKind = chunk.reason.kind;
447
+ break;
448
+ }
449
+ if (controller.signal.aborted) return null;
450
+ if (finishKind === "error" || finishKind === "aborted") return null;
451
+ return text;
452
+ } catch (error) {
453
+ if (controller.signal.aborted) opts.warn("llm-fallbacks: auto-match timed out — falling back to \"inherit\"");
454
+ else opts.warn(`llm-fallbacks: auto-match call failed — falling back to "inherit": ${describeError(error)}`);
455
+ return null;
456
+ } finally {
457
+ clearTimeout(timer);
458
+ }
459
+ }
460
+ function describeError(error) {
461
+ return error instanceof Error ? error.message : String(error);
462
+ }
144
463
  //#endregion
145
464
  //#region .build/host/chains.js
146
465
  function resolveCandidate(entry, failing, modelExists) {
@@ -247,9 +566,8 @@ function annotateCandidates(candidates, surviving, options) {
247
566
  //#endregion
248
567
  //#region .build/host/roles.js
249
568
  function resolveRole(agent, rules, roleIds, warn = console.warn) {
250
- const origin = agent.session?.header?.origin ?? "root";
569
+ if ((agent.session?.header?.origin ?? "root") !== "subagent") return INHERIT_ROLE_ID;
251
570
  for (const rule of rules) {
252
- if (rule.origin && rule.origin !== origin) continue;
253
571
  if (rule.provider && rule.provider !== agent.options?.provider) continue;
254
572
  if (rule.model && rule.model !== agent.options?.model) continue;
255
573
  const target = rule.role.trim();
@@ -264,6 +582,29 @@ function resolveRole(agent, rules, roleIds, warn = console.warn) {
264
582
  return INHERIT_ROLE_ID;
265
583
  }
266
584
  //#endregion
585
+ //#region .build/host/role-resolution.js
586
+ async function resolveRoleAtDispatch(agent, rules, roleIds, opts) {
587
+ const preset = agent.session?.header?.agentPreset?.trim();
588
+ if (preset !== void 0 && preset !== "" && preset !== "inherit") {
589
+ const declared = roleIds.get(preset);
590
+ if (declared !== void 0) return declared;
591
+ }
592
+ const role = resolveRole(agent, rules, roleIds, opts.warn);
593
+ if (role !== "inherit") return role;
594
+ if (opts.automatchEnabled && opts.automatch !== void 0) {
595
+ const picked = await opts.automatch(agent);
596
+ if (picked !== null && picked !== void 0) {
597
+ const declared = roleIds.get(picked.trim());
598
+ if (declared !== void 0) return declared;
599
+ opts.warn(`llm-fallbacks: auto-match returned undeclared role "${picked}" — falling back to "inherit"`);
600
+ }
601
+ }
602
+ return INHERIT_ROLE_ID;
603
+ }
604
+ function firstExactCandidate(all, wildcard) {
605
+ for (let index = 0; index < all.length; index += 1) if (!wildcard[index]) return all[index];
606
+ }
607
+ //#endregion
267
608
  //#region .build/host/cooldown.js
268
609
  var CooldownStore = class {
269
610
  entries = new Map();
@@ -399,12 +740,25 @@ const CONFIG_KEYS = {
399
740
  revertPolicy: true,
400
741
  maxSwitchesPerStep: true,
401
742
  alwaysModeRetryCap: true,
402
- presets: true
743
+ presets: true,
744
+ roleAutoMatch: true,
745
+ timeSlots: true,
746
+ tz: true
403
747
  };
404
748
  const ROLES_KEYS = {
405
749
  list: true,
406
750
  rules: true
407
751
  };
752
+ const SLOT_ROW_KEYS = {
753
+ kind: true,
754
+ preset: true,
755
+ start: true,
756
+ end: true,
757
+ days: true,
758
+ name: true,
759
+ chain: true
760
+ };
761
+ const SLOT_HHMM_RE = /^([01]\d|2[0-3]):[0-5]\d$/;
408
762
  var FallbacksConfigGateway = class extends TypertRemoteService {
409
763
  bridge;
410
764
  seeds;
@@ -494,9 +848,44 @@ function validateConfigPatch(patch) {
494
848
  for (const nestedKey of Object.keys(roles)) if (!Object.hasOwn(ROLES_KEYS, nestedKey)) throw new Error(`dsh-llm-fallbacks: unknown config key "roles.${nestedKey}"`);
495
849
  }
496
850
  }
851
+ if (key === "timeSlots") {
852
+ const rows = patch[key];
853
+ if (rows !== null && rows !== void 0) validateTimeSlotsPatch(rows);
854
+ }
855
+ if (key === "rootChain") {
856
+ const chain = patch[key];
857
+ if (chain !== null && chain !== void 0 && (!Array.isArray(chain) || !isAllDayConforming(chain))) throw new Error("dsh-llm-fallbacks: rootChain must end with exactly one official V4 model (deepseek-official/deepseek-v4-flash or deepseek-official/deepseek-v4-pro)");
858
+ }
497
859
  }
498
860
  Config(patch);
499
861
  }
862
+ function validateTimeSlotsPatch(rows) {
863
+ if (!Array.isArray(rows)) throw new Error("dsh-llm-fallbacks: timeSlots must be an array of slot rows");
864
+ const seenPresets = new Set();
865
+ rows.forEach((row, index) => {
866
+ const at = `timeSlots[${index}]`;
867
+ if (row === null || typeof row !== "object" || Array.isArray(row)) throw new Error(`dsh-llm-fallbacks: ${at} must be a plain object`);
868
+ const record = row;
869
+ for (const nestedKey of Object.keys(record)) if (!Object.hasOwn(SLOT_ROW_KEYS, nestedKey)) throw new Error(`dsh-llm-fallbacks: unknown config key "${at}.${nestedKey}"`);
870
+ if (record.kind === "preset") {
871
+ const preset = record.preset;
872
+ if (typeof preset !== "string" || !Object.hasOwn(PRESETS, preset)) throw new Error(`dsh-llm-fallbacks: ${at}.preset must be one of the four frozen preset ids (got ${JSON.stringify(preset)})`);
873
+ if (seenPresets.has(preset)) throw new Error(`dsh-llm-fallbacks: ${at} duplicates preset "${preset}" — at most one row per preset`);
874
+ seenPresets.add(preset);
875
+ if (record.start !== void 0 || record.end !== void 0 || Array.isArray(record.days) && record.days.length > 0) throw new Error(`dsh-llm-fallbacks: ${at} preset "${preset}" cannot carry start/end/days — preset windows are frozen code constants`);
876
+ if (record.name !== void 0) throw new Error(`dsh-llm-fallbacks: ${at} preset "${preset}" cannot carry a name — preset rows are named by the frozen label`);
877
+ } else if (record.kind === "custom") {
878
+ const { start, end } = record;
879
+ if (typeof start !== "string" || typeof end !== "string" || !SLOT_HHMM_RE.test(start) || !SLOT_HHMM_RE.test(end)) throw new Error(`dsh-llm-fallbacks: ${at} custom row requires HH:mm start and end (got ${JSON.stringify(start)}-${JSON.stringify(end)})`);
880
+ if (record.days !== void 0) {
881
+ if (!Array.isArray(record.days) || record.days.some((day) => !Number.isInteger(day) || day < 0 || day > 6)) throw new Error(`dsh-llm-fallbacks: ${at}.days must be an array of integers 0–6`);
882
+ }
883
+ if (record.name !== void 0 && typeof record.name !== "string") throw new Error(`dsh-llm-fallbacks: ${at}.name must be a string`);
884
+ } else throw new Error(`dsh-llm-fallbacks: ${at}.kind must be "preset" or "custom" (got ${JSON.stringify(record.kind)})`);
885
+ const chain = record.chain;
886
+ if (!Array.isArray(chain) || chain.length === 0 || chain.some((entry) => typeof entry !== "string")) throw new Error(`dsh-llm-fallbacks: ${at}.chain must be a non-empty string array`);
887
+ });
888
+ }
500
889
  function fallbacksTypertContribution() {
501
890
  return {
502
891
  package: "dsh-llm-fallbacks",
@@ -567,7 +956,7 @@ const CONFIG_SUBCOMMAND_DESCRIPTION = {
567
956
  const FALLBACKS_COMMAND_LOCALES = {
568
957
  zh: {
569
958
  title: "当前会话 fallback 诊断(只读)",
570
- description: "查看当前会话的降级链、最近切换与冷却状态(只读)",
959
+ description: "查看当前会话的降级链、最近降级切换与冷却状态(只读)",
571
960
  usageConfig: CONFIG_SUBCOMMAND_DESCRIPTION.zh,
572
961
  usage: ` /fallbacks config ${CONFIG_SUBCOMMAND_DESCRIPTION.zh}`,
573
962
  origin: "会话来源",
@@ -575,7 +964,8 @@ const FALLBACKS_COMMAND_LOCALES = {
575
964
  chain: "链",
576
965
  inheritRoot: "(inherit-root)",
577
966
  chainNone: "未配置",
578
- switches: "最近切换",
967
+ slot: "分时",
968
+ switches: "最近降级切换",
579
969
  switchesNone: "本会话暂无 fallback 切换",
580
970
  switchLine: "{from} → {to}(role={role},reason={reason})",
581
971
  cooldown: "冷却",
@@ -599,12 +989,13 @@ const FALLBACKS_COMMAND_LOCALES = {
599
989
  configMaxSwitches: "单步最大切换",
600
990
  configAlwaysCap: "always 上限",
601
991
  configPresets: "预置",
992
+ configRoleAutoMatch: "角色自动匹配",
602
993
  configEdit: "编辑:~/.dsh/profiles/<profile>/cordis.patch.yml(插件行)或 $DSH_HOME/settings.yaml(fallbacks: 分节)",
603
994
  configEditHint: "TUI 无法修改配置——只能编辑文件"
604
995
  },
605
996
  en: {
606
997
  title: "Session fallback diagnostics (read-only)",
607
- description: "Inspect fallback chain, recent switches, and cooldown for this session (read-only)",
998
+ description: "Inspect fallback chain, recent fallback switches, and cooldown for this session (read-only)",
608
999
  usageConfig: CONFIG_SUBCOMMAND_DESCRIPTION.en,
609
1000
  usage: ` /fallbacks config ${CONFIG_SUBCOMMAND_DESCRIPTION.en}`,
610
1001
  origin: "Session origin",
@@ -612,7 +1003,8 @@ const FALLBACKS_COMMAND_LOCALES = {
612
1003
  chain: "Chain",
613
1004
  inheritRoot: " (inherit-root)",
614
1005
  chainNone: "not configured",
615
- switches: "Recent switches",
1006
+ slot: "Time slot",
1007
+ switches: "Recent fallback switches",
616
1008
  switchesNone: "No fallback switches in this session",
617
1009
  switchLine: "{from} → {to} (role={role}, reason={reason})",
618
1010
  cooldown: "Cooldown",
@@ -636,6 +1028,7 @@ const FALLBACKS_COMMAND_LOCALES = {
636
1028
  configMaxSwitches: "Max switches/step",
637
1029
  configAlwaysCap: "Always-mode cap",
638
1030
  configPresets: "Presets",
1031
+ configRoleAutoMatch: "Auto-match",
639
1032
  configEdit: "Edit: ~/.dsh/profiles/<profile>/cordis.patch.yml (plugin row) or $DSH_HOME/settings.yaml (fallbacks: section)",
640
1033
  configEditHint: "TUI cannot change config — edit files only"
641
1034
  }
@@ -709,6 +1102,7 @@ function fallbacksConfigText(summary, locale = "zh") {
709
1102
  `${t.configMaxSwitches}: ${summary.maxSwitchesPerStep}`,
710
1103
  `${t.configAlwaysCap}: ${summary.alwaysModeRetryCap}`,
711
1104
  `${t.configPresets}: ${summary.presets}`,
1105
+ `${t.configRoleAutoMatch}: ${summary.roleAutoMatch ? t.configEnabled : t.configDisabled}`,
712
1106
  "",
713
1107
  t.configEdit,
714
1108
  t.configEditHint
@@ -724,6 +1118,7 @@ function fallbacksCommandText(snapshot, locale = "zh") {
724
1118
  const suffix = snapshot.inherit ? t.inheritRoot : "";
725
1119
  lines.push(`${t.chain}: ${snapshot.chain.join(" → ")}${suffix}`);
726
1120
  }
1121
+ lines.push(`${t.slot}: ${snapshot.slot.label}`);
727
1122
  if (snapshot.switches.length === 0) lines.push(`${t.switches}: ${t.switchesNone}`);
728
1123
  else {
729
1124
  lines.push(`${t.switches} (${snapshot.switches.length}):`);
@@ -979,7 +1374,7 @@ function fallbacksChildren(canonicalPath) {
979
1374
  if (canonicalPath.length === 1) return [FALLBACKS_CONFIG_NODE];
980
1375
  return [];
981
1376
  }
982
- const FALLBACKS_PROVIDER = {
1377
+ const FALLBACKS_PROVIDER$1 = {
983
1378
  root: FALLBACKS_TUI_ROOT,
984
1379
  descriptions: {
985
1380
  zh: FALLBACKS_COMMAND_LOCALES.zh.description,
@@ -993,7 +1388,7 @@ function installTuiClient(ctx, opts) {
993
1388
  const trees = tctx.tuiCommandTrees;
994
1389
  if (trees === void 0) return;
995
1390
  try {
996
- return trees.register(FALLBACKS_PROVIDER);
1391
+ return trees.register(FALLBACKS_PROVIDER$1);
997
1392
  } catch (error) {
998
1393
  if (!(error instanceof Error) || !error.message.includes("already registered")) throw error;
999
1394
  tctx.logger("llm-fallbacks").debug("llm-fallbacks: tui command tree already registered — no provider on this fiber");
@@ -1002,6 +1397,151 @@ function installTuiClient(ctx, opts) {
1002
1397
  });
1003
1398
  }
1004
1399
  //#endregion
1400
+ //#region .build/host/virtual-adapter.js
1401
+ const FALLBACKS_PROVIDER = "FallbacksChain";
1402
+ const FALLBACKS_CHAIN_MODEL = "Auto";
1403
+ const EMPTY_EFFECTIVE_CHAIN_CODE = "EMPTY_EFFECTIVE_CHAIN";
1404
+ const UNDISPATCHABLE_HEAD_CODE = "UNDISPATCHABLE_EFFECTIVE_HEAD";
1405
+ const LLM_UNAVAILABLE_CODE = "LLM_UNAVAILABLE";
1406
+ function firstDispatchableExactHead(chain) {
1407
+ for (const entry of chain) {
1408
+ let selector;
1409
+ try {
1410
+ selector = parseSelector(entry);
1411
+ } catch {
1412
+ continue;
1413
+ }
1414
+ if (selector.model === void 0) continue;
1415
+ if (selector.provider === "FallbacksChain") continue;
1416
+ return {
1417
+ provider: selector.provider,
1418
+ model: selector.model
1419
+ };
1420
+ }
1421
+ }
1422
+ function effectiveHeadOf(config, now) {
1423
+ if (!isAllDayConforming(config.rootChain)) return void 0;
1424
+ return firstDispatchableExactHead(resolveEffectiveChain(config, now, config.tz ?? "Asia/Shanghai"));
1425
+ }
1426
+ function pickerDisplayName(config, now = new Date(), modelDisplayName) {
1427
+ const head = effectiveHeadOf(config, now);
1428
+ if (head === void 0) return FALLBACKS_CHAIN_MODEL;
1429
+ const slot = resolveSlotState(config, now, config.tz ?? "Asia/Shanghai");
1430
+ const model = modelDisplayName !== void 0 && modelDisplayName !== "" ? modelDisplayName : head.model;
1431
+ return `${FALLBACKS_CHAIN_MODEL}: ${model}[${slot.label}]`;
1432
+ }
1433
+ async function resolveHeadDisplayName(llm, head) {
1434
+ if (llm === void 0) return head.model;
1435
+ try {
1436
+ const row = (await llm.listModels(head.provider)).find((model) => model.id === head.model);
1437
+ if (row !== void 0 && row.name !== "") return row.name;
1438
+ } catch {}
1439
+ try {
1440
+ const info = await llm.resolveModelInfo(head.provider, head.model);
1441
+ if (info.name !== "") return info.name;
1442
+ } catch {}
1443
+ return head.model;
1444
+ }
1445
+ var FallbacksChainAdapter = class extends LlmAdapter {
1446
+ readConfig;
1447
+ getLlm;
1448
+ constructor(readConfig, getLlm) {
1449
+ super();
1450
+ this.readConfig = readConfig;
1451
+ this.getLlm = getLlm;
1452
+ }
1453
+ providerInfo(provider) {
1454
+ return {
1455
+ id: provider,
1456
+ name: FALLBACKS_PROVIDER
1457
+ };
1458
+ }
1459
+ async listModels(provider) {
1460
+ const config = this.readConfig();
1461
+ const now = new Date();
1462
+ const head = effectiveHeadOf(config, now);
1463
+ const display = head === void 0 ? void 0 : await resolveHeadDisplayName(this.getLlm(), head);
1464
+ return [{
1465
+ provider,
1466
+ id: FALLBACKS_CHAIN_MODEL,
1467
+ name: pickerDisplayName(config, now, display)
1468
+ }];
1469
+ }
1470
+ async resolveModel(provider, model, signal) {
1471
+ const head = effectiveHeadOf(this.readConfig(), new Date());
1472
+ const llm = this.getLlm();
1473
+ if (head !== void 0 && llm !== void 0) try {
1474
+ const info = await llm.resolveModelInfo(head.provider, head.model, signal);
1475
+ return {
1476
+ provider,
1477
+ id: model,
1478
+ name: info.name,
1479
+ ...info.description === void 0 ? {} : { description: info.description },
1480
+ ...info.inputModalities === void 0 ? {} : { inputModalities: info.inputModalities },
1481
+ ...info.context === void 0 ? {} : { context: info.context },
1482
+ ...info.defaultMaxTokens === void 0 ? {} : { defaultMaxTokens: info.defaultMaxTokens },
1483
+ ...info.reasoning === void 0 ? {} : { reasoning: info.reasoning }
1484
+ };
1485
+ } catch {}
1486
+ return {
1487
+ provider,
1488
+ id: model,
1489
+ name: model
1490
+ };
1491
+ }
1492
+ stream(options) {
1493
+ const config = this.readConfig();
1494
+ const now = new Date();
1495
+ if (resolveEffectiveChain(config, now, config.tz ?? "Asia/Shanghai").length === 0) throw new LlmError("llm-fallbacks: the effective chain is empty — the virtual FallbacksChain route has no head to delegate to", EMPTY_EFFECTIVE_CHAIN_CODE);
1496
+ const head = effectiveHeadOf(config, now);
1497
+ if (head === void 0) throw new LlmError("llm-fallbacks: the effective chain head cannot be dispatched (non-conforming all-day, wildcard, malformed, or self-route) — refusing to delegate", UNDISPATCHABLE_HEAD_CODE);
1498
+ const llm = this.getLlm();
1499
+ if (llm === void 0) throw new LlmError("llm-fallbacks: the llm runtime is unavailable — cannot delegate the FallbacksChain call", LLM_UNAVAILABLE_CODE);
1500
+ return llm.stream({
1501
+ ...options,
1502
+ provider: head.provider,
1503
+ model: head.model
1504
+ });
1505
+ }
1506
+ };
1507
+ function installFallbacksAdapter(ctx, readConfig) {
1508
+ let llm;
1509
+ let disposeAdapter;
1510
+ let registered = false;
1511
+ const logger = ctx.logger("llm-fallbacks");
1512
+ const adapter = new FallbacksChainAdapter(readConfig, () => llm);
1513
+ const reconcile = () => {
1514
+ const shouldRegister = readConfig().enabled;
1515
+ if (shouldRegister && !registered) {
1516
+ if (llm === void 0) return;
1517
+ try {
1518
+ disposeAdapter = llm.registerAdapter([FALLBACKS_PROVIDER], adapter);
1519
+ registered = true;
1520
+ logger.info("llm-fallbacks: virtual adapter registered — FallbacksChain/Auto is selectable in the model picker");
1521
+ } catch (error) {
1522
+ if (!(error instanceof Error) || !("code" in error) || error.code !== "DUPLICATE_ADAPTER") throw error;
1523
+ logger.debug("llm-fallbacks: virtual adapter already registered — no route on this fiber (multi-fiber dedupe)");
1524
+ }
1525
+ } else if (!shouldRegister && registered) {
1526
+ disposeAdapter?.();
1527
+ disposeAdapter = void 0;
1528
+ registered = false;
1529
+ logger.info("llm-fallbacks: virtual adapter unregistered — FallbacksChain/Auto hidden from the model picker");
1530
+ }
1531
+ };
1532
+ ctx.inject(["llm"], (llmCtx) => {
1533
+ llm = llmCtx.llm;
1534
+ reconcile();
1535
+ return () => {
1536
+ disposeAdapter?.();
1537
+ disposeAdapter = void 0;
1538
+ registered = false;
1539
+ llm = void 0;
1540
+ };
1541
+ });
1542
+ return reconcile;
1543
+ }
1544
+ //#endregion
1005
1545
  //#region .build/host/index.js
1006
1546
  const name = "llm-fallbacks";
1007
1547
  const provide = ["llm-fallbacks"];
@@ -1042,6 +1582,10 @@ function currentModel(agent, provider) {
1042
1582
  model: agent.session.requestHeader()?.config.model ?? agent.options.model ?? ""
1043
1583
  };
1044
1584
  }
1585
+ function slotWinnerKey(winner) {
1586
+ if (winner === "all-day") return "all-day";
1587
+ return winner.kind === "preset" ? `preset:${winner.preset}` : `custom:${winner.start}-${winner.end}`;
1588
+ }
1045
1589
  function overrideConfig(seed, to) {
1046
1590
  const { reasoningEffort: _inherited, ...withoutInheritedEffort } = seed;
1047
1591
  return {
@@ -1052,15 +1596,6 @@ function overrideConfig(seed, to) {
1052
1596
  }
1053
1597
  function apply(ctx, config = defaultFallbacksConfig) {
1054
1598
  const logger = ctx.logger("llm-fallbacks");
1055
- const known = dshSession.KNOWN_SESSION_EVENT_TYPES;
1056
- let sessionEventRegistered = false;
1057
- if (known instanceof Set) try {
1058
- known.add("fallbacks/switch");
1059
- sessionEventRegistered = known.has("fallbacks/switch");
1060
- } catch {
1061
- sessionEventRegistered = false;
1062
- }
1063
- let sessionEventWarned = false;
1064
1599
  const entry = Config(config);
1065
1600
  const seeds = new FallbacksSeedManager(logger);
1066
1601
  const seedsSettingsUnavailable = "llm-fallbacks: seeds: settings service is unavailable — seed roles cannot be written";
@@ -1099,6 +1634,7 @@ function apply(ctx, config = defaultFallbacksConfig) {
1099
1634
  ctx.logger("llm-fallbacks").debug("fallbacks service already registered — no service on this fiber (multi-fiber dedupe)");
1100
1635
  }
1101
1636
  let source = () => entry;
1637
+ const reconcileFallbacksAdapter = installFallbacksAdapter(ctx, () => source());
1102
1638
  validateFallbacksConfig(entry, logger);
1103
1639
  const legacyKeys = detectLegacyKeys(source());
1104
1640
  if (legacyKeys.length > 0) logger.warn("llm-fallbacks: legacy config keys detected (chains/roles.default/undeclared role refs); see docs/configuration.md migration table — %o", legacyKeys);
@@ -1112,6 +1648,8 @@ function apply(ctx, config = defaultFallbacksConfig) {
1112
1648
  const current = source();
1113
1649
  roleIds = new Map(current.roles.list.map((role) => [role.id.trim(), role.id]));
1114
1650
  hasChains = current.rootChain.length > 0 || current.roles.list.some((role) => (role.chain?.length ?? 0) > 0);
1651
+ reconcileFallbacksAdapter();
1652
+ slotWinners.clear();
1115
1653
  }
1116
1654
  });
1117
1655
  const bridge = { source: () => source() };
@@ -1132,6 +1670,8 @@ function apply(ctx, config = defaultFallbacksConfig) {
1132
1670
  });
1133
1671
  const states = new FallbackStateStore();
1134
1672
  stateStores.set(ctx, states);
1673
+ const dispatchInjected = new Set();
1674
+ const slotWinners = new Map();
1135
1675
  async function decide(agent, turn, step, current, reason, state) {
1136
1676
  const config = source();
1137
1677
  if (state !== void 0) {
@@ -1139,9 +1679,10 @@ function apply(ctx, config = defaultFallbacksConfig) {
1139
1679
  if (state.stepFailures.switchCount >= config.maxSwitchesPerStep) return null;
1140
1680
  }
1141
1681
  const role = resolveRole(agent, config.roles.rules, roleIds, logger.warn);
1142
- const { all, wildcard } = resolveChainViews(config.roles.list, config.rootChain, role, current.provider, current.model, logger.warn);
1682
+ const rootTail = agent.session?.header?.origin === "subagent" ? config.rootChain : resolveEffectiveChain(config, new Date(), config.tz ?? "Asia/Shanghai");
1683
+ const { all, wildcard } = resolveChainViews(config.roles.list, rootTail, role, current.provider, current.model, logger.warn);
1143
1684
  if (all.length === 0) return null;
1144
- const modelExists = hasWildcardEntry(config.roles.list, config.rootChain, role) ? await makeModelExists(ctx, [...new Set(all.map((candidate) => candidate.provider))]) : void 0;
1685
+ const modelExists = hasWildcardEntry(config.roles.list, rootTail, role) ? await makeModelExists(ctx, [...new Set(all.map((candidate) => candidate.provider))]) : void 0;
1145
1686
  const cooldown = { isSuppressed: (key) => state !== void 0 && states.isSuppressed(state, key) };
1146
1687
  const failed = { has: (key) => state !== void 0 && state.stepFailures.failed.has(key) };
1147
1688
  const surviving = selectCandidates(all, wildcard, createCandidateFilter({
@@ -1151,7 +1692,7 @@ function apply(ctx, config = defaultFallbacksConfig) {
1151
1692
  }), modelExists);
1152
1693
  const target = surviving[0];
1153
1694
  if (target === void 0 || target.model === void 0) return null;
1154
- logger.info("llm-fallbacks: agent \"%s\" switch %s/%s -> %s/%s (role=%s, reason=%s, candidates=%o)", agent.id, current.provider, current.model, target.provider, target.model, role, reason, annotateCandidates(all, surviving, {
1695
+ logger.info("llm-fallbacks: agent \"%s\" fallback switch (降级切换) %s/%s -> %s/%s (role=%s, reason=%s, candidates=%o)", agent.id, current.provider, current.model, target.provider, target.model, role, reason, annotateCandidates(all, surviving, {
1155
1696
  current,
1156
1697
  cooldown,
1157
1698
  failed
@@ -1169,7 +1710,7 @@ function apply(ctx, config = defaultFallbacksConfig) {
1169
1710
  reason
1170
1711
  };
1171
1712
  }
1172
- function commit(agent, state, pending, turn, step) {
1713
+ function commit(state, pending, turn, step) {
1173
1714
  const config = source();
1174
1715
  const fromKey = selectorKey(pending.from.provider, pending.from.model);
1175
1716
  const until = config.revertPolicy === "never" ? Number.POSITIVE_INFINITY : Date.now() + config.cooldownMs;
@@ -1178,21 +1719,6 @@ function apply(ctx, config = defaultFallbacksConfig) {
1178
1719
  states.suppress(state, fromKey, until);
1179
1720
  states.recordFailure(state, fromKey);
1180
1721
  states.recordSwitch(state);
1181
- if (!sessionEventRegistered) {
1182
- if (!sessionEventWarned) {
1183
- sessionEventWarned = true;
1184
- logger.warn("llm-fallbacks: session event type \"fallbacks/switch\" is not registered with this harness — skipping the durable event (the switch itself is applied)");
1185
- }
1186
- return;
1187
- }
1188
- agent.session.append("fallbacks/switch", {
1189
- turn,
1190
- step,
1191
- from: pending.from,
1192
- to: pending.to,
1193
- role: pending.role,
1194
- reason: pending.reason
1195
- });
1196
1722
  }
1197
1723
  ctx.on("agent/request-error", async ({ agent, turn, step, provider, failure }, next) => {
1198
1724
  const config = source();
@@ -1202,7 +1728,7 @@ function apply(ctx, config = defaultFallbacksConfig) {
1202
1728
  try {
1203
1729
  const pending = await decide(agent, turn, step, current, "trigger-code", states.peek(agent.id));
1204
1730
  if (pending === null) return next();
1205
- commit(agent, states.get(agent.id), pending, turn, step);
1731
+ commit(states.get(agent.id), pending, turn, step);
1206
1732
  return { kind: "retry" };
1207
1733
  } catch (error) {
1208
1734
  logger.warn("llm-fallbacks: decision path failed, passing the original failure through: %s", error?.message ?? String(error));
@@ -1215,6 +1741,51 @@ function apply(ctx, config = defaultFallbacksConfig) {
1215
1741
  const applied = state === void 0 ? void 0 : states.applyPending(state, turn, step);
1216
1742
  if (applied !== void 0) return overrideConfig(seed, applied.to);
1217
1743
  const config = source();
1744
+ if (config.enabled && isAllDayConforming(config.rootChain) && (config.timeSlots?.length ?? 0) > 0 && agent.session?.header?.origin !== "subagent") {
1745
+ const slot = resolveSlotState(config, new Date(), config.tz ?? "Asia/Shanghai");
1746
+ const key = slotWinnerKey(slot.winner);
1747
+ const previous = slotWinners.get(agent.id);
1748
+ if (previous !== void 0 && previous.key !== key) logger.info("llm-fallbacks: agent \"%s\" time-slot switch (分时切换): %s -> %s", agent.id, previous.label, slot.label);
1749
+ slotWinners.set(agent.id, {
1750
+ key,
1751
+ label: slot.label
1752
+ });
1753
+ }
1754
+ if (config.enabled && seed.provider === "FallbacksChain" && seed.model === "Auto" && agent.session?.header?.origin !== "subagent") {
1755
+ if (!isAllDayConforming(config.rootChain)) logger.warn("llm-fallbacks: FallbacksChain/Auto selected but the all-day rootChain is not conforming (exactly one official V4 model) — no primary override");
1756
+ else {
1757
+ const head = firstDispatchableExactHead(resolveEffectiveChain(config, new Date(), config.tz ?? "Asia/Shanghai"));
1758
+ if (head === void 0) logger.warn("llm-fallbacks: FallbacksChain/Auto selected but the effective chain has no exact head (empty, wildcard-only, or self-route) — no primary override");
1759
+ else {
1760
+ logger.info("llm-fallbacks: FallbacksChain/Auto selection overrides to the effective head %s/%s", head.provider, head.model);
1761
+ return overrideConfig(seed, head);
1762
+ }
1763
+ }
1764
+ }
1765
+ if (config.enabled && hasChains && agent.session?.header?.origin === "subagent" && !dispatchInjected.has(agent.id)) {
1766
+ dispatchInjected.add(agent.id);
1767
+ try {
1768
+ const role = await resolveRoleAtDispatch(agent, config.roles.rules, roleIds, {
1769
+ automatchEnabled: config.roleAutoMatch ?? true,
1770
+ automatch: (agent) => pickRoleByLlm(ctx, config.roles, agent, { warn: logger.warn }),
1771
+ warn: logger.warn
1772
+ });
1773
+ if (role !== "inherit") {
1774
+ const { all, wildcard } = resolveChainViews(config.roles.list, config.rootChain, role, seed.provider, seed.model, logger.warn);
1775
+ const head = firstExactCandidate(all, wildcard);
1776
+ if (head !== void 0 && head.model !== void 0 && !(head.provider === seed.provider && head.model === seed.model)) {
1777
+ const to = {
1778
+ provider: head.provider,
1779
+ model: head.model
1780
+ };
1781
+ logger.info("llm-fallbacks: agent \"%s\" role-inject role=%s model=%s/%s", agent.id, role, head.provider, head.model);
1782
+ return overrideConfig(seed, to);
1783
+ }
1784
+ }
1785
+ } catch (error) {
1786
+ logger.warn("llm-fallbacks: dispatch role-injection failed, proceeding with the request unchanged: %s", error?.message ?? String(error));
1787
+ }
1788
+ }
1218
1789
  if (hasChains && config.enabled && config.alwaysModeRetryCap > 0 && countRetryEvents(agent.session, turn, step, seed.provider) >= config.alwaysModeRetryCap) {
1219
1790
  const decisionState = states.peek(agent.id);
1220
1791
  const pending = await decide(agent, turn, step, {
@@ -1223,7 +1794,7 @@ function apply(ctx, config = defaultFallbacksConfig) {
1223
1794
  }, "always-cap", decisionState);
1224
1795
  if (pending !== null) {
1225
1796
  const commitState = states.get(agent.id);
1226
- commit(agent, commitState, pending, turn, step);
1797
+ commit(commitState, pending, turn, step);
1227
1798
  const appliedCap = states.applyPending(commitState, turn, step);
1228
1799
  if (appliedCap !== void 0) return overrideConfig(seed, appliedCap.to);
1229
1800
  }
@@ -1237,9 +1808,13 @@ function apply(ctx, config = defaultFallbacksConfig) {
1237
1808
  });
1238
1809
  ctx.on("agent/disposed", ({ agent }) => {
1239
1810
  states.delete(agent.id);
1811
+ dispatchInjected.delete(agent.id);
1812
+ slotWinners.delete(agent.id);
1240
1813
  });
1241
1814
  ctx.effect(() => () => {
1242
1815
  states.clear();
1816
+ dispatchInjected.clear();
1817
+ slotWinners.clear();
1243
1818
  }, "llm-fallbacks: clear per-agent state");
1244
1819
  const fallbacksCommandController = {
1245
1820
  getSnapshot(agent) {
@@ -1250,6 +1825,7 @@ function apply(ctx, config = defaultFallbacksConfig) {
1250
1825
  origin: agent.session.header?.origin ?? "root",
1251
1826
  role,
1252
1827
  ...resolveChainForDiagnostic(config.roles.list, config.rootChain, role, logger.warn),
1828
+ slot: resolveSlotState(config, new Date(), config.tz ?? "Asia/Shanghai"),
1253
1829
  switches: recentFallbacksSwitches(agent.session.events, 5),
1254
1830
  cooldown: state === void 0 ? [] : state.cooldown.snapshot()
1255
1831
  };
@@ -1268,7 +1844,8 @@ function apply(ctx, config = defaultFallbacksConfig) {
1268
1844
  revertPolicy: config.revertPolicy,
1269
1845
  maxSwitchesPerStep: config.maxSwitchesPerStep,
1270
1846
  alwaysModeRetryCap: config.alwaysModeRetryCap,
1271
- presets: config.presets ?? "bundled"
1847
+ presets: config.presets ?? "bundled",
1848
+ roleAutoMatch: config.roleAutoMatch ?? true
1272
1849
  };
1273
1850
  }
1274
1851
  };