dsh-llm-fallbacks 0.4.2 → 0.4.4

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
@@ -34,6 +34,9 @@ function resolveWildcardEntry(failingModel, provider) {
34
34
  raw: `${provider}/${failingModel}`
35
35
  };
36
36
  }
37
+ function isOfficialAllDayId(id) {
38
+ return id === "deepseek-official/deepseek-flash" || id === "deepseek-official/deepseek-pro";
39
+ }
37
40
  const PRESETS = {
38
41
  "liang-peak": {
39
42
  windows: [{
@@ -220,7 +223,7 @@ function labelOf(row) {
220
223
  function isAllDayConforming(chain) {
221
224
  if (chain.length < 1) return false;
222
225
  const tail = chain[chain.length - 1];
223
- return tail === "deepseek-official/deepseek-v4-flash" || tail === "deepseek-official/deepseek-v4-pro";
226
+ return isOfficialAllDayId(tail);
224
227
  }
225
228
  function resolveEffectiveChain(config, now, tz) {
226
229
  const state = resolveSlotState(config, now, tz);
@@ -359,7 +362,7 @@ function validateFallbacksConfig(config, logger) {
359
362
  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`);
360
363
  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"`);
361
364
  }
362
- 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");
365
+ if (config.rootChain.length > 0 && !isAllDayConforming(config.rootChain)) logger.warn("llm-fallbacks: rootChain must end with exactly one official model (deepseek-official/deepseek-flash or deepseek-official/deepseek-pro) — time-slot rows and the virtual picker row stay inert until the all-day chain tail conforms");
363
366
  for (const entry of config.rootChain) try {
364
367
  parseSelector(entry);
365
368
  } catch (error) {
@@ -618,18 +621,25 @@ function hasWildcardEntry(roles, rootChain, role) {
618
621
  } catch {}
619
622
  return false;
620
623
  }
624
+ function fitsContextWindow(candidate, minContextWindow, contextWindowOf) {
625
+ if (minContextWindow === void 0 || contextWindowOf === void 0) return true;
626
+ if (candidate.model === void 0) return true;
627
+ const candidateWindow = contextWindowOf(candidate.provider, candidate.model);
628
+ return candidateWindow === void 0 || candidateWindow > minContextWindow;
629
+ }
621
630
  function createCandidateFilter(options) {
622
- const { current, cooldown, failed, modelExists } = options;
631
+ const { current, cooldown, failed, modelExists, minContextWindow, contextWindowOf } = options;
623
632
  return (candidate) => {
624
633
  if (candidate.provider === current.provider && candidate.model === current.model) return false;
625
634
  if (cooldown.isSuppressed(selectorKey(candidate.provider, candidate.model))) return false;
626
635
  if (failed.has(selectorKey(candidate.provider, candidate.model))) return false;
636
+ if (!fitsContextWindow(candidate, minContextWindow, contextWindowOf)) return false;
627
637
  if (modelExists && candidate.model !== void 0 && !modelExists(candidate.provider, candidate.model)) return false;
628
638
  return true;
629
639
  };
630
640
  }
631
641
  function annotateCandidates(candidates, surviving, options) {
632
- const { current, cooldown, failed } = options;
642
+ const { current, cooldown, failed, minContextWindow, contextWindowOf } = options;
633
643
  const usable = new Set(surviving.map((candidate) => selectorKey(candidate.provider, candidate.model)));
634
644
  return candidates.map((candidate) => {
635
645
  if (candidate.provider === current.provider && candidate.model === current.model) return {
@@ -646,6 +656,10 @@ function annotateCandidates(candidates, surviving, options) {
646
656
  candidate,
647
657
  skip: "step-failed"
648
658
  };
659
+ if (!fitsContextWindow(candidate, minContextWindow, contextWindowOf)) return {
660
+ candidate,
661
+ skip: "context-window"
662
+ };
649
663
  return {
650
664
  candidate,
651
665
  skip: "missing-id"
@@ -954,6 +968,7 @@ function overrideConfigWithRouteRule(seed, to, explicitEffort) {
954
968
  //#endregion
955
969
  //#region .build/host/gateway.js
956
970
  const FALLBACKS_SETTINGS_NAMESPACE = "fallbacks";
971
+ const SUBAGENT_ROLES_MAX_IDS = 256;
957
972
  const CONFIG_KEYS = {
958
973
  enabled: true,
959
974
  triggerCodes: true,
@@ -988,11 +1003,13 @@ var FallbacksConfigGateway = class extends TypertRemoteService {
988
1003
  seeds;
989
1004
  settings;
990
1005
  subagentPolicy;
991
- constructor(ctx, bridge, seeds, subagentPolicy) {
1006
+ subagentRolesSnapshot;
1007
+ constructor(ctx, bridge, seeds, subagentPolicy, subagentRoles) {
992
1008
  super(ctx, "fallbacks");
993
1009
  this.bridge = bridge;
994
1010
  this.seeds = seeds;
995
1011
  this.subagentPolicy = subagentPolicy;
1012
+ this.subagentRolesSnapshot = subagentRoles;
996
1013
  ctx.inject(["settings"], (sctx) => {
997
1014
  this.settings = sctx.settings;
998
1015
  return () => {
@@ -1027,6 +1044,23 @@ var FallbacksConfigGateway = class extends TypertRemoteService {
1027
1044
  outcome
1028
1045
  };
1029
1046
  }
1047
+ subagentRoles(ids) {
1048
+ if (!Array.isArray(ids) || ids.some((id) => typeof id !== "string")) throw new TypeError("dsh-llm-fallbacks: subagent-roles ids must be an array of strings");
1049
+ if (ids.length > SUBAGENT_ROLES_MAX_IDS) throw new TypeError(`dsh-llm-fallbacks: subagent-roles ids batch exceeds ${SUBAGENT_ROLES_MAX_IDS} entries`);
1050
+ if (this.subagentRolesSnapshot === void 0) return {};
1051
+ let records;
1052
+ try {
1053
+ records = this.subagentRolesSnapshot();
1054
+ } catch {
1055
+ return {};
1056
+ }
1057
+ const projected = new Map();
1058
+ for (const id of ids) {
1059
+ const record = records.get(id);
1060
+ if (record !== void 0) projected.set(id, projectSubagentRoleRecord(record));
1061
+ }
1062
+ return Object.fromEntries(projected);
1063
+ }
1030
1064
  readConfig(source = this.bridge.source()) {
1031
1065
  const wire = {};
1032
1066
  for (const key of Object.keys(CONFIG_KEYS)) {
@@ -1093,6 +1127,17 @@ function projectSubagentPolicy(snapshot) {
1093
1127
  };
1094
1128
  return wire;
1095
1129
  }
1130
+ function projectSubagentRoleRecord(record) {
1131
+ const wire = {
1132
+ role: record.role,
1133
+ at: record.at
1134
+ };
1135
+ if (record.model !== void 0) wire.model = {
1136
+ provider: record.model.provider,
1137
+ model: record.model.model
1138
+ };
1139
+ return wire;
1140
+ }
1096
1141
  function normalizeRoles(value) {
1097
1142
  if (typeof value !== "object" || value === null || Array.isArray(value)) return value;
1098
1143
  const roles = {};
@@ -1118,7 +1163,7 @@ function validateConfigPatch(patch) {
1118
1163
  }
1119
1164
  if (key === "rootChain") {
1120
1165
  const chain = patch[key];
1121
- 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)");
1166
+ if (chain !== null && chain !== void 0 && (!Array.isArray(chain) || !isAllDayConforming(chain))) throw new Error("dsh-llm-fallbacks: rootChain must end with exactly one official model (deepseek-official/deepseek-flash or deepseek-official/deepseek-pro)");
1122
1167
  }
1123
1168
  }
1124
1169
  Config(patch);
@@ -1207,6 +1252,21 @@ function fallbacksTypertContribution() {
1207
1252
  codec: { mode: "src-json" }
1208
1253
  }],
1209
1254
  result: { mode: "src-json" }
1255
+ },
1256
+ {
1257
+ id: "dsh-llm-fallbacks#fallbacks/subagent-roles",
1258
+ service: "fallbacks",
1259
+ namespace: "fallbacks",
1260
+ method: "subagent-roles",
1261
+ implementation: "subagentRoles",
1262
+ invocation: { kind: "direct" },
1263
+ parameters: [{
1264
+ name: "ids",
1265
+ wire: "ids",
1266
+ source: "json",
1267
+ codec: { mode: "src-json" }
1268
+ }],
1269
+ result: { mode: "src-json" }
1210
1270
  }
1211
1271
  ]
1212
1272
  };
@@ -1544,13 +1604,14 @@ var FallbacksSeedManager = class {
1544
1604
  constructor(logger) {
1545
1605
  this.logger = logger;
1546
1606
  }
1547
- async declare(seeds, io) {
1607
+ async declare(seeds, io, options) {
1548
1608
  const outcome = {
1549
1609
  applied: [],
1550
1610
  skipped: [],
1551
1611
  conflicts: []
1552
1612
  };
1553
- const registry = new Map();
1613
+ const source = resolveSource(options, this.logger);
1614
+ const entries = new Map();
1554
1615
  for (const seed of seeds) {
1555
1616
  if (typeof seed.id !== "string" || !ROLE_ID_PATTERN.test(seed.id)) {
1556
1617
  outcome.skipped.push({
@@ -1568,7 +1629,7 @@ var FallbacksSeedManager = class {
1568
1629
  this.warnSkip(seed.id, "reserved-id");
1569
1630
  continue;
1570
1631
  }
1571
- if (registry.has(seed.id)) {
1632
+ if (entries.has(seed.id)) {
1572
1633
  outcome.skipped.push({
1573
1634
  id: seed.id,
1574
1635
  reason: "duplicate-in-batch"
@@ -1576,33 +1637,37 @@ var FallbacksSeedManager = class {
1576
1637
  this.warnSkip(seed.id, "duplicate-in-batch");
1577
1638
  continue;
1578
1639
  }
1579
- registry.set(seed.id, seed.persona);
1640
+ entries.set(seed.id, {
1641
+ persona: seed.persona,
1642
+ set: source
1643
+ });
1580
1644
  outcome.applied.push(seed.id);
1581
1645
  }
1582
1646
  const config = io.read();
1583
1647
  const currentList = roleRows(config);
1584
1648
  const currentRules = roleRules(config);
1585
- const newList = materialize(currentList, registry, this.registry, outcome.conflicts);
1649
+ const newList = materialize(currentList, personaView(entries), personaView(this.registry), outcome.conflicts);
1586
1650
  for (const conflict of outcome.conflicts) this.logger.warn(`llm-fallbacks: seeds: persona-source conflict for seed id ${JSON.stringify(conflict.id)} — operator row persona kept (never overwritten)`);
1587
1651
  const computed = {
1588
1652
  list: newList,
1589
1653
  rules: currentRules
1590
1654
  };
1591
1655
  if (!deepEqual(newList, currentList)) await io.writeRoles(computed);
1592
- this.registry = registry;
1656
+ this.registry = entries;
1593
1657
  return outcome;
1594
1658
  }
1595
1659
  effectiveRoles(io) {
1596
1660
  return { roles: roleRows(io.read()).map((row) => {
1597
- const seedPersona = this.registry.get(row.id.trim());
1598
- const seeded = seedPersona !== void 0;
1661
+ const entry = this.registry.get(row.id.trim());
1662
+ const seeded = entry !== void 0;
1599
1663
  const effective = {
1600
1664
  id: row.id,
1601
1665
  persona: row.persona,
1602
1666
  seeded,
1603
- personaOverridden: seeded && row.persona !== seedPersona
1667
+ personaOverridden: seeded && row.persona !== entry.persona,
1668
+ source: seeded ? entry.set : "user"
1604
1669
  };
1605
- if (seeded) effective.seedPersona = seedPersona;
1670
+ if (seeded) effective.seedPersona = entry.persona;
1606
1671
  if (row.chain !== void 0) effective.chain = row.chain;
1607
1672
  if (row.fallback !== void 0) effective.fallback = row.fallback;
1608
1673
  return effective;
@@ -1611,19 +1676,20 @@ var FallbacksSeedManager = class {
1611
1676
  wireStatus(io) {
1612
1677
  const status = [];
1613
1678
  for (const row of roleRows(io.read())) {
1614
- const seedPersona = this.registry.get(row.id.trim());
1615
- if (seedPersona === void 0) continue;
1679
+ const entry = this.registry.get(row.id.trim());
1680
+ if (entry === void 0) continue;
1616
1681
  status.push({
1617
1682
  id: row.id,
1618
- overridden: row.persona !== seedPersona
1683
+ overridden: row.persona !== entry.persona,
1684
+ source: entry.set
1619
1685
  });
1620
1686
  }
1621
1687
  return status;
1622
1688
  }
1623
1689
  async revert(id, io) {
1624
1690
  const seedId = id.trim();
1625
- const seedPersona = this.registry.get(seedId);
1626
- if (seedPersona === void 0) return {
1691
+ const entry = this.registry.get(seedId);
1692
+ if (entry === void 0) return {
1627
1693
  reverted: false,
1628
1694
  reason: "not-seeded"
1629
1695
  };
@@ -1635,13 +1701,13 @@ var FallbacksSeedManager = class {
1635
1701
  reverted: false,
1636
1702
  reason: "row-absent"
1637
1703
  };
1638
- if (rows[index].persona === seedPersona) return {
1704
+ if (rows[index].persona === entry.persona) return {
1639
1705
  reverted: true,
1640
- persona: seedPersona
1706
+ persona: entry.persona
1641
1707
  };
1642
1708
  const nextList = rows.map((row, i) => i === index ? {
1643
1709
  ...row,
1644
- persona: seedPersona
1710
+ persona: entry.persona
1645
1711
  } : row);
1646
1712
  await io.writeRoles({
1647
1713
  list: nextList,
@@ -1649,7 +1715,7 @@ var FallbacksSeedManager = class {
1649
1715
  });
1650
1716
  return {
1651
1717
  reverted: true,
1652
- persona: seedPersona
1718
+ persona: entry.persona
1653
1719
  };
1654
1720
  }
1655
1721
  warnSkip(id, reason) {
@@ -1659,6 +1725,26 @@ var FallbacksSeedManager = class {
1659
1725
  else this.logger.warn(`llm-fallbacks: seeds: skipping seed id ${shown} — duplicate-in-batch (first wins)`);
1660
1726
  }
1661
1727
  };
1728
+ const RESERVED_SET_NAMES = [
1729
+ "bundled",
1730
+ "user",
1731
+ "external"
1732
+ ];
1733
+ function resolveSource(options, logger) {
1734
+ if (options?.bundled === true) return "bundled";
1735
+ const raw = options?.set;
1736
+ if (raw === void 0) return "external";
1737
+ if (typeof raw === "string" && raw.trim() !== "" && !RESERVED_SET_NAMES.includes(raw.trim())) return raw.trim();
1738
+ const shown = typeof raw === "string" ? JSON.stringify(raw) : String(raw);
1739
+ const reserved = RESERVED_SET_NAMES.map((name) => JSON.stringify(name)).join(" / ");
1740
+ logger.warn(`llm-fallbacks: seeds: ignoring declare set name ${shown} — invalid (empty after trim, non-string, or a reserved label ${reserved}); provenance degrades to "external", the seeds still apply`);
1741
+ return "external";
1742
+ }
1743
+ function personaView(entries) {
1744
+ const personas = new Map();
1745
+ for (const [id, entry] of entries) personas.set(id, entry.persona);
1746
+ return personas;
1747
+ }
1662
1748
  function materialize(rows, registry, previous, conflicts) {
1663
1749
  const next = [];
1664
1750
  for (const row of rows) {
@@ -1733,14 +1819,6 @@ const presetRoles = [
1733
1819
  id: "scout",
1734
1820
  persona: "Read-only scout for exploratory codebase research, rapid analysis, and broad pattern search. Return compressed, structured findings another agent can reuse without re-reading the tree. Run searches in parallel; if a search is empty, try at least one alternate strategy before concluding the target is absent. Infer thoroughness from the task (quick, medium, or thorough; default medium); never write, edit, or run state-changing commands."
1735
1821
  },
1736
- {
1737
- id: "designer",
1738
- persona: "UI/UX specialist for design implementation, review, and visual refinement. Analyze the existing design system first (tokens, theme, and primitives) and compose with it; if none exists, define a minimal system before implementing. Cover loading, empty, error, disabled, hover, and focus states; verify accessibility (contrast, focus rings, semantic HTML) and responsive layout. Avoid generic AI-slop patterns; in review, cite file and line with a concrete issue and a specific fix."
1739
- },
1740
- {
1741
- id: "librarian",
1742
- persona: "Research specialist for external libraries and APIs who returns definitive, source-verified answers. Treat source as truth, documentation as aspiration, and training data as history; prefer locally installed packages, then official docs. Cross-check at least two locations; copy API signatures verbatim and report the investigated version. Stay read-only on the user's project; if a lookup is empty, try at least two fallback strategies before concluding nothing exists."
1743
- },
1744
1822
  {
1745
1823
  id: "reviewer",
1746
1824
  persona: "Code-review specialist for quality and security analysis of a patch before merge. Anchor every finding to the assigned diff; report only issues that are provable, actionable, unintentional, and introduced by the patch. For any new type, variant, or value that crosses a module boundary, inspect the consuming-side dispatch point. Rank findings P0 (blocks release) through P3 (nice to have); do not edit files or trigger builds."
@@ -1983,10 +2061,10 @@ function buildFallbacksTuiSection() {
1983
2061
  zh: "根降级链(JSON)",
1984
2062
  en: "Root chain (JSON)"
1985
2063
  },
1986
- hint: "JSON array of model selectors; the last entry must be an official V4 model.",
2064
+ hint: "JSON array of model selectors; the last entry must be an official model (deepseek-official/deepseek-flash or deepseek-official/deepseek-pro). Pro is a legal tail but not yet available in the catalog — the settings card shows it disabled.",
1987
2065
  hintDescriptions: {
1988
- zh: "模型选择器 JSON 数组;末项必须是官方 V4 模型。",
1989
- en: "JSON array of model selectors; the last entry must be an official V4 model."
2066
+ zh: "模型选择器 JSON 数组;末项必须是官方模型(deepseek-official/deepseek-flash 或 deepseek-official/deepseek-pro)。Pro 是合法链尾但目录暂未提供——设置卡中显示为禁用。",
2067
+ en: "JSON array of model selectors; the last entry must be an official model (deepseek-official/deepseek-flash or deepseek-official/deepseek-pro). Pro is a legal tail but not yet available in the catalog — the settings card shows it disabled."
1990
2068
  },
1991
2069
  group: "chain",
1992
2070
  kind: "text",
@@ -2097,7 +2175,13 @@ function installTuiSettingsSection(ctx, opts) {
2097
2175
  ctx.inject(["tuiSettingsSections"], (tctx) => {
2098
2176
  const registry = tctx.tuiSettingsSections;
2099
2177
  if (registry === void 0) return;
2100
- return registry.register(buildFallbacksTuiSection());
2178
+ try {
2179
+ return registry.register(buildFallbacksTuiSection());
2180
+ } catch (error) {
2181
+ if (!(error instanceof Error) || !error.message.includes("already registered")) throw error;
2182
+ tctx.logger("llm-fallbacks").debug("llm-fallbacks: tui settings section already registered — no section on this fiber");
2183
+ return () => {};
2184
+ }
2101
2185
  });
2102
2186
  }
2103
2187
  //#endregion
@@ -2259,6 +2343,8 @@ function installFallbacksAdapter(ctx, readConfig) {
2259
2343
  const name = "llm-fallbacks";
2260
2344
  const provide = ["llm-fallbacks"];
2261
2345
  const { version } = createRequire(import.meta.url)("../package.json");
2346
+ const CONTEXT_WINDOW_EXCEEDED_CODE = "CONTEXT_WINDOW_EXCEEDED";
2347
+ const REQUEST_SCOPED_CODES = new Set([CONTEXT_WINDOW_EXCEEDED_CODE]);
2262
2348
  const stateStores = new WeakMap();
2263
2349
  function stateStore(ctx) {
2264
2350
  return stateStores.get(ctx);
@@ -2271,6 +2357,10 @@ const chainHeadStores = new WeakMap();
2271
2357
  function chainHeads(ctx) {
2272
2358
  return chainHeadStores.get(ctx);
2273
2359
  }
2360
+ const subagentRoleRecordStores = new WeakMap();
2361
+ function subagentRoleRecords(ctx) {
2362
+ return subagentRoleRecordStores.get(ctx);
2363
+ }
2274
2364
  function latestByAt(map) {
2275
2365
  let latest;
2276
2366
  for (const value of map.values()) if (latest === void 0 || value.at >= latest.at) latest = value;
@@ -2290,6 +2380,26 @@ async function makeModelExists(ctx, providers) {
2290
2380
  }));
2291
2381
  return (provider, model) => catalog.get(provider)?.has(model) ?? false;
2292
2382
  }
2383
+ async function makeContextWindowOf(ctx, routes) {
2384
+ const llm = ctx.get("llm");
2385
+ if (llm === void 0) return () => void 0;
2386
+ const windows = new Map();
2387
+ if (typeof llm.listModels === "function") await Promise.all([...new Set(routes.map((route) => route.provider))].map(async (provider) => {
2388
+ try {
2389
+ for (const model of await llm.listModels(provider)) if (typeof model.contextWindow === "number") windows.set(selectorKey(provider, model.id), model.contextWindow);
2390
+ } catch {}
2391
+ }));
2392
+ const resolveModelInfo = llm.resolveModelInfo?.bind(llm);
2393
+ if (resolveModelInfo !== void 0) await Promise.all(routes.map(async (route) => {
2394
+ const key = selectorKey(route.provider, route.model);
2395
+ if (windows.has(key)) return;
2396
+ try {
2397
+ const window = (await resolveModelInfo(route.provider, route.model))?.context?.contextWindow;
2398
+ if (typeof window === "number") windows.set(key, window);
2399
+ } catch {}
2400
+ }));
2401
+ return (provider, model) => windows.get(selectorKey(provider, model));
2402
+ }
2293
2403
  function countRetryEvents(session, turn, step, provider) {
2294
2404
  let count = 0;
2295
2405
  const events = session.snapshotEvents();
@@ -2367,6 +2477,24 @@ function apply(ctx, config = defaultFallbacksConfig) {
2367
2477
  };
2368
2478
  ctx.inject(["settings"], (sctx) => {
2369
2479
  writeRoles = (roles) => sctx.settings.update(FALLBACKS_SETTINGS_NAMESPACE, { roles });
2480
+ try {
2481
+ sctx.provide("llm-fallbacks", {
2482
+ name: "llm-fallbacks",
2483
+ version,
2484
+ resolveRole,
2485
+ resolveChain,
2486
+ validateFallbacksConfig,
2487
+ detectLegacyKeys,
2488
+ declareSeeds: (declarations, options) => seeds.declare(declarations, seedsIo, options?.set === void 0 ? void 0 : { set: options.set }),
2489
+ getEffectiveRoles: () => seeds.effectiveRoles(seedsIo),
2490
+ revertSeededPersona: (id) => seeds.revert(id, seedsIo)
2491
+ });
2492
+ serviceOwned = true;
2493
+ } catch (error) {
2494
+ serviceOwned = false;
2495
+ if (!(error instanceof Error) || !error.message.includes("has been registered")) throw error;
2496
+ ctx.logger("llm-fallbacks").debug("fallbacks service already registered — no service on this fiber (multi-fiber dedupe)");
2497
+ }
2370
2498
  return () => {
2371
2499
  writeRoles = () => {
2372
2500
  throw new Error(seedsSettingsUnavailable);
@@ -2377,25 +2505,7 @@ function apply(ctx, config = defaultFallbacksConfig) {
2377
2505
  read: () => source(),
2378
2506
  writeRoles: (roles) => writeRoles(roles)
2379
2507
  };
2380
- let serviceOwned = false;
2381
- try {
2382
- ctx.provide("llm-fallbacks", {
2383
- name: "llm-fallbacks",
2384
- version,
2385
- resolveRole,
2386
- resolveChain,
2387
- validateFallbacksConfig,
2388
- detectLegacyKeys,
2389
- declareSeeds: (declarations) => seeds.declare(declarations, seedsIo),
2390
- getEffectiveRoles: () => seeds.effectiveRoles(seedsIo),
2391
- revertSeededPersona: (id) => seeds.revert(id, seedsIo)
2392
- });
2393
- serviceOwned = true;
2394
- } catch (error) {
2395
- if (!(error instanceof Error) || !error.message.includes("has been registered")) throw error;
2396
- serviceOwned = false;
2397
- ctx.logger("llm-fallbacks").debug("fallbacks service already registered — no service on this fiber (multi-fiber dedupe)");
2398
- }
2508
+ let serviceOwned = ctx.get("llm-fallbacks") === void 0;
2399
2509
  let source = () => entry;
2400
2510
  const reconcileFallbacksAdapter = installFallbacksAdapter(ctx, () => source());
2401
2511
  validateFallbacksConfig(entry, logger);
@@ -2422,6 +2532,8 @@ function apply(ctx, config = defaultFallbacksConfig) {
2422
2532
  chainHeadStores.set(ctx, chainHeadMap);
2423
2533
  const blockedAttemptMap = new Map();
2424
2534
  blockedAttemptStores.set(ctx, blockedAttemptMap);
2535
+ const subagentRoleRecordMap = new Map();
2536
+ subagentRoleRecordStores.set(ctx, subagentRoleRecordMap);
2425
2537
  try {
2426
2538
  new FallbacksConfigGateway(ctx, bridge, seeds, () => {
2427
2539
  try {
@@ -2436,7 +2548,7 @@ function apply(ctx, config = defaultFallbacksConfig) {
2436
2548
  } catch {
2437
2549
  return { policy: { state: "unprovable" } };
2438
2550
  }
2439
- });
2551
+ }, () => subagentRoleRecordMap);
2440
2552
  } catch (error) {
2441
2553
  if (!(error instanceof Error) || !error.message.includes("has been registered")) throw error;
2442
2554
  ctx.logger("llm-fallbacks").debug("fallbacks gateway already registered — no gateway on this fiber (multi-fiber dedupe)");
@@ -2455,7 +2567,7 @@ function apply(ctx, config = defaultFallbacksConfig) {
2455
2567
  const dispatchInjected = new Set();
2456
2568
  const slotWinners = new Map();
2457
2569
  const lastKnownPolicySettings = new Map();
2458
- async function decide(agent, turn, step, current, reason, state) {
2570
+ async function decide(agent, turn, step, current, reason, state, failureCode) {
2459
2571
  const config = source();
2460
2572
  if (state !== void 0) {
2461
2573
  states.syncStep(state, turn, step);
@@ -2468,10 +2580,17 @@ function apply(ctx, config = defaultFallbacksConfig) {
2468
2580
  const modelExists = hasWildcardEntry(config.roles.list, rootTail, role) ? await makeModelExists(ctx, [...new Set(all.map((candidate) => candidate.provider))]) : void 0;
2469
2581
  const cooldown = { isSuppressed: (key) => state !== void 0 && states.isSuppressed(state, key, Date.now(), source().recovery ?? "timer") };
2470
2582
  const failed = { has: (key) => state !== void 0 && state.stepFailures.failed.has(key) };
2583
+ const contextWindowOf = failureCode === CONTEXT_WINDOW_EXCEEDED_CODE ? await makeContextWindowOf(ctx, [current, ...all.flatMap((candidate) => candidate.model === void 0 ? [] : [{
2584
+ provider: candidate.provider,
2585
+ model: candidate.model
2586
+ }])]) : void 0;
2587
+ const minContextWindow = contextWindowOf?.(current.provider, current.model);
2471
2588
  const surviving = selectCandidates(all, wildcard, createCandidateFilter({
2472
2589
  current,
2473
2590
  cooldown,
2474
- failed
2591
+ failed,
2592
+ minContextWindow,
2593
+ contextWindowOf
2475
2594
  }), modelExists);
2476
2595
  const first = surviving[0];
2477
2596
  let target;
@@ -2511,7 +2630,9 @@ function apply(ctx, config = defaultFallbacksConfig) {
2511
2630
  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, {
2512
2631
  current,
2513
2632
  cooldown,
2514
- failed
2633
+ failed,
2634
+ minContextWindow,
2635
+ contextWindowOf
2515
2636
  }).map(({ candidate, skip }) => skip === void 0 ? `${candidate.provider}/${candidate.model}` : `${candidate.provider}/${candidate.model} (skipped: ${skip})`));
2516
2637
  return {
2517
2638
  from: {
@@ -2529,10 +2650,12 @@ function apply(ctx, config = defaultFallbacksConfig) {
2529
2650
  function commit(state, pending, turn, step) {
2530
2651
  const config = source();
2531
2652
  const fromKey = selectorKey(pending.from.provider, pending.from.model);
2532
- const until = config.revertPolicy === "never" ? Number.POSITIVE_INFINITY : config.recovery === "half-open" ? Date.now() + escalatedCooldownMs(config.cooldownMs, state.recovery.recordFailure(fromKey)) : Date.now() + config.cooldownMs;
2533
2653
  states.syncStep(state, turn, step);
2534
2654
  states.writePending(state, pending);
2535
- states.suppress(state, fromKey, until);
2655
+ if ((pending.scope ?? "route") === "route") {
2656
+ const until = config.revertPolicy === "never" ? Number.POSITIVE_INFINITY : config.recovery === "half-open" ? Date.now() + escalatedCooldownMs(config.cooldownMs, state.recovery.recordFailure(fromKey)) : Date.now() + config.cooldownMs;
2657
+ states.suppress(state, fromKey, until);
2658
+ }
2536
2659
  states.recordFailure(state, fromKey);
2537
2660
  states.recordSwitch(state);
2538
2661
  }
@@ -2551,12 +2674,16 @@ function apply(ctx, config = defaultFallbacksConfig) {
2551
2674
  const current = currentModel(agent, provider);
2552
2675
  if (!current.model) return next();
2553
2676
  try {
2554
- const pending = await decide(agent, turn, step, current, "trigger-code", states.peek(agent.id));
2677
+ const pending = await decide(agent, turn, step, current, "trigger-code", states.peek(agent.id), failure.code);
2555
2678
  if (pending === null) {
2556
2679
  failHalfOpenProbe(agent.id, current);
2557
2680
  return next();
2558
2681
  }
2559
- commit(states.get(agent.id), pending, turn, step);
2682
+ const scope = REQUEST_SCOPED_CODES.has(failure.code) ? "request" : void 0;
2683
+ commit(states.get(agent.id), scope === void 0 ? pending : {
2684
+ ...pending,
2685
+ scope
2686
+ }, turn, step);
2560
2687
  return { kind: "retry" };
2561
2688
  } catch (error) {
2562
2689
  logger.warn("llm-fallbacks: decision path failed, passing the original failure through: %s", error?.message ?? String(error));
@@ -2580,7 +2707,7 @@ function apply(ctx, config = defaultFallbacksConfig) {
2580
2707
  });
2581
2708
  }
2582
2709
  if (config.enabled && seed.provider === "FallbacksChain" && seed.model === "Auto" && agent.session?.header?.origin !== "subagent") {
2583
- 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");
2710
+ if (!isAllDayConforming(config.rootChain)) logger.warn("llm-fallbacks: FallbacksChain/Auto selected but the all-day rootChain is not conforming (exactly one official model: deepseek-official/deepseek-flash or deepseek-official/deepseek-pro) — no primary override");
2584
2711
  else {
2585
2712
  const head = firstDispatchableExactHead(resolveEffectiveChain(config, new Date(), config.tz ?? "Asia/Shanghai"));
2586
2713
  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");
@@ -2628,6 +2755,14 @@ function apply(ctx, config = defaultFallbacksConfig) {
2628
2755
  model: head.model
2629
2756
  };
2630
2757
  }
2758
+ subagentRoleRecordMap.set(agent.id, {
2759
+ role,
2760
+ model: to ?? {
2761
+ provider: seed.provider,
2762
+ model: seed.model
2763
+ },
2764
+ at: Date.now()
2765
+ });
2631
2766
  if (to !== void 0 && !(to.provider === seed.provider && to.model === seed.model)) {
2632
2767
  if (policy.state === "enabled") chainHeadMap.set(agent.id, {
2633
2768
  at: Date.now(),
@@ -2676,6 +2811,7 @@ function apply(ctx, config = defaultFallbacksConfig) {
2676
2811
  slotWinners.delete(agent.id);
2677
2812
  blockedAttemptMap.delete(agent.id);
2678
2813
  chainHeadMap.delete(agent.id);
2814
+ subagentRoleRecordMap.delete(agent.id);
2679
2815
  lastKnownPolicySettings.delete(agent.id);
2680
2816
  });
2681
2817
  ctx.on("session/event", (session, event) => {
@@ -2694,6 +2830,7 @@ function apply(ctx, config = defaultFallbacksConfig) {
2694
2830
  slotWinners.clear();
2695
2831
  blockedAttemptMap.clear();
2696
2832
  chainHeadMap.clear();
2833
+ subagentRoleRecordMap.clear();
2697
2834
  lastKnownPolicySettings.clear();
2698
2835
  }, "llm-fallbacks: clear per-agent state");
2699
2836
  const fallbacksCommandController = {
@@ -2766,10 +2903,10 @@ function apply(ctx, config = defaultFallbacksConfig) {
2766
2903
  ctx.inject(["settings"], () => {
2767
2904
  if (!serviceOwned) return;
2768
2905
  if (seedsIo.read().presets === "none") return;
2769
- seeds.declare(presetRoles, seedsIo).catch((error) => {
2906
+ seeds.declare(presetRoles, seedsIo, { bundled: true }).catch((error) => {
2770
2907
  logger.error("llm-fallbacks: seeds: preset role declaration failed — %s", error?.message ?? String(error));
2771
2908
  });
2772
2909
  });
2773
2910
  }
2774
2911
  //#endregion
2775
- export { Config, FallbacksSeedManager, INHERIT_ROLE_ID, ROLE_ID_PATTERN, SelectorError, annotateCandidates, apply, blockedAttempts, chainHeads, countRetryEvents, createCandidateFilter, defaultFallbacksConfig, detectLegacyKeys, hasWildcardEntry, name, parseSelector, presetRoles, provide, resolveCandidate, resolveChain, resolveChainViews, resolveRole, selectCandidates, stateStore, validateFallbacksConfig };
2912
+ export { Config, FallbacksSeedManager, INHERIT_ROLE_ID, ROLE_ID_PATTERN, SelectorError, annotateCandidates, apply, blockedAttempts, chainHeads, countRetryEvents, createCandidateFilter, defaultFallbacksConfig, detectLegacyKeys, hasWildcardEntry, name, parseSelector, presetRoles, provide, resolveCandidate, resolveChain, resolveChainViews, resolveRole, selectCandidates, stateStore, subagentRoleRecords, validateFallbacksConfig };
package/dist/presets.d.ts CHANGED
@@ -1,17 +1,20 @@
1
1
  /**
2
- * Bundled preset role declarations (plan fallbacks-preset-roles Task 1).
2
+ * Bundled preset role declarations (plan fallbacks-preset-roles Task 1;
3
+ * trimmed 7 → 5 by the seed-source-provenance plan, D4).
3
4
  *
4
5
  * Derivation: distilled from the omp bundled agent prompts —
5
- * `packages/coding-agent/src/prompts/agents/{scout,designer,librarian,reviewer,security-reviewer}.md`
6
+ * `packages/coding-agent/src/prompts/agents/{scout,reviewer,security-reviewer}.md`
6
7
  * and `task.md` (task/sonic share the body; frontmatter injected in
7
8
  * `src/task/agents.ts`) — snapshot date 2026-08-16. Each persona is a
8
9
  * concise distillation (frontmatter description + core directives), NOT a
9
10
  * verbatim copy of the full prompt; the frozen text lives in spec
10
- * `fallbacks-preset-roles-spec.md` §9.2 (implementer SSOT).
11
+ * `fallbacks-preset-roles-spec.md` §9.2 (implementer SSOT). `designer` and
12
+ * `librarian` were removed from the bundled set; rows persisted by earlier
13
+ * versions survive untouched (they read back with source `user`).
11
14
  *
12
15
  * Pure data module: no io, no side effects, no classes. Types import only
13
16
  * `./seeds.ts` — no `@deepseek-ai/*` imports (bundle purity gate).
14
17
  */
15
18
  import type { SeedDeclaration } from './seeds.ts';
16
- /** The 7 bundled omp-style preset roles (spec §9.1 shape, §9.2 personas). */
19
+ /** The 5 bundled omp-style preset roles (spec §9.1 shape, §9.2 personas). */
17
20
  export declare const presetRoles: readonly SeedDeclaration[];