dsh-llm-fallbacks 0.4.4 → 0.5.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
@@ -668,11 +668,15 @@ function annotateCandidates(candidates, surviving, options) {
668
668
  }
669
669
  //#endregion
670
670
  //#region .build/host/roles.js
671
- function resolveRole(agent, rules, roleIds, warn = console.warn) {
671
+ function resolveRole(agent, rules, roleIds, warn = console.warn, servedRoute) {
672
672
  if ((agent.session?.header?.origin ?? "root") !== "subagent") return INHERIT_ROLE_ID;
673
+ const recordedRoute = {
674
+ provider: agent.options?.provider,
675
+ model: agent.options?.model
676
+ };
673
677
  for (const rule of rules) {
674
- if (rule.provider && rule.provider !== agent.options?.provider) continue;
675
- if (rule.model && rule.model !== agent.options?.model) continue;
678
+ const matchesPair = (pair) => (!rule.provider || rule.provider === pair.provider) && (!rule.model || rule.model === pair.model);
679
+ if (!matchesPair(recordedRoute) && !(servedRoute !== void 0 && matchesPair(servedRoute))) continue;
676
680
  const target = rule.role.trim();
677
681
  if (target === "inherit") return INHERIT_ROLE_ID;
678
682
  const declared = roleIds.get(target);
@@ -692,7 +696,7 @@ async function resolveRoleAtDispatch(agent, rules, roleIds, opts) {
692
696
  const declared = roleIds.get(preset);
693
697
  if (declared !== void 0) return declared;
694
698
  }
695
- const role = resolveRole(agent, rules, roleIds, opts.warn);
699
+ const role = resolveRole(agent, rules, roleIds, opts.warn, opts.servedRoute);
696
700
  if (role !== "inherit") return role;
697
701
  if (opts.automatchEnabled && opts.automatch !== void 0) {
698
702
  const picked = await opts.automatch(agent);
@@ -1601,6 +1605,7 @@ function registerFallbacksCommands(registry, controller, locale = "zh") {
1601
1605
  var FallbacksSeedManager = class {
1602
1606
  logger;
1603
1607
  registry = new Map();
1608
+ declareSeq = 0;
1604
1609
  constructor(logger) {
1605
1610
  this.logger = logger;
1606
1611
  }
@@ -1646,28 +1651,69 @@ var FallbacksSeedManager = class {
1646
1651
  const config = io.read();
1647
1652
  const currentList = roleRows(config);
1648
1653
  const currentRules = roleRules(config);
1649
- const newList = materialize(currentList, personaView(entries), personaView(this.registry), outcome.conflicts);
1654
+ const candidate = new Map(this.registry);
1655
+ if (entries.size === 0) candidate.delete(source);
1656
+ else candidate.set(source, {
1657
+ seq: this.declareSeq + 1,
1658
+ entries
1659
+ });
1660
+ const prior = new Map();
1661
+ const incoming = new Map();
1662
+ for (const id of entries.keys()) {
1663
+ const before = this.resolveDeclared(id)?.persona;
1664
+ const after = this.resolveDeclared(id, candidate)?.persona;
1665
+ if (before !== void 0) prior.set(id, before);
1666
+ if (after !== void 0) incoming.set(id, after);
1667
+ }
1668
+ const newList = materialize(currentList, incoming, prior, outcome.conflicts);
1650
1669
  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)`);
1651
1670
  const computed = {
1652
1671
  list: newList,
1653
1672
  rules: currentRules
1654
1673
  };
1655
1674
  if (!deepEqual(newList, currentList)) await io.writeRoles(computed);
1656
- this.registry = entries;
1675
+ if (entries.size === 0) this.registry.delete(source);
1676
+ else this.registry.set(source, {
1677
+ seq: ++this.declareSeq,
1678
+ entries
1679
+ });
1657
1680
  return outcome;
1658
1681
  }
1682
+ resolveDeclared(id, registry = this.registry) {
1683
+ const bundled = registry.get("bundled")?.entries.get(id);
1684
+ if (bundled !== void 0) return {
1685
+ persona: bundled.persona,
1686
+ source: "bundled"
1687
+ };
1688
+ let winner;
1689
+ for (const [label, slice] of registry) {
1690
+ if (label === "bundled") continue;
1691
+ const entry = slice.entries.get(id);
1692
+ if (entry === void 0) continue;
1693
+ if (winner === void 0 || slice.seq > winner.seq) winner = {
1694
+ persona: entry.persona,
1695
+ source: label,
1696
+ seq: slice.seq
1697
+ };
1698
+ }
1699
+ if (winner === void 0) return void 0;
1700
+ return {
1701
+ persona: winner.persona,
1702
+ source: winner.source
1703
+ };
1704
+ }
1659
1705
  effectiveRoles(io) {
1660
1706
  return { roles: roleRows(io.read()).map((row) => {
1661
- const entry = this.registry.get(row.id.trim());
1662
- const seeded = entry !== void 0;
1707
+ const declared = this.resolveDeclared(row.id.trim());
1708
+ const seeded = declared !== void 0;
1663
1709
  const effective = {
1664
1710
  id: row.id,
1665
1711
  persona: row.persona,
1666
1712
  seeded,
1667
- personaOverridden: seeded && row.persona !== entry.persona,
1668
- source: seeded ? entry.set : "user"
1713
+ personaOverridden: seeded && row.persona !== declared.persona,
1714
+ source: seeded ? declared.source : "user"
1669
1715
  };
1670
- if (seeded) effective.seedPersona = entry.persona;
1716
+ if (seeded) effective.seedPersona = declared.persona;
1671
1717
  if (row.chain !== void 0) effective.chain = row.chain;
1672
1718
  if (row.fallback !== void 0) effective.fallback = row.fallback;
1673
1719
  return effective;
@@ -1676,20 +1722,20 @@ var FallbacksSeedManager = class {
1676
1722
  wireStatus(io) {
1677
1723
  const status = [];
1678
1724
  for (const row of roleRows(io.read())) {
1679
- const entry = this.registry.get(row.id.trim());
1680
- if (entry === void 0) continue;
1725
+ const declared = this.resolveDeclared(row.id.trim());
1726
+ if (declared === void 0) continue;
1681
1727
  status.push({
1682
1728
  id: row.id,
1683
- overridden: row.persona !== entry.persona,
1684
- source: entry.set
1729
+ overridden: row.persona !== declared.persona,
1730
+ source: declared.source
1685
1731
  });
1686
1732
  }
1687
1733
  return status;
1688
1734
  }
1689
1735
  async revert(id, io) {
1690
1736
  const seedId = id.trim();
1691
- const entry = this.registry.get(seedId);
1692
- if (entry === void 0) return {
1737
+ const declared = this.resolveDeclared(seedId);
1738
+ if (declared === void 0) return {
1693
1739
  reverted: false,
1694
1740
  reason: "not-seeded"
1695
1741
  };
@@ -1701,13 +1747,13 @@ var FallbacksSeedManager = class {
1701
1747
  reverted: false,
1702
1748
  reason: "row-absent"
1703
1749
  };
1704
- if (rows[index].persona === entry.persona) return {
1750
+ if (rows[index].persona === declared.persona) return {
1705
1751
  reverted: true,
1706
- persona: entry.persona
1752
+ persona: declared.persona
1707
1753
  };
1708
1754
  const nextList = rows.map((row, i) => i === index ? {
1709
1755
  ...row,
1710
- persona: entry.persona
1756
+ persona: declared.persona
1711
1757
  } : row);
1712
1758
  await io.writeRoles({
1713
1759
  list: nextList,
@@ -1715,7 +1761,7 @@ var FallbacksSeedManager = class {
1715
1761
  });
1716
1762
  return {
1717
1763
  reverted: true,
1718
- persona: entry.persona
1764
+ persona: declared.persona
1719
1765
  };
1720
1766
  }
1721
1767
  warnSkip(id, reason) {
@@ -1740,43 +1786,38 @@ function resolveSource(options, logger) {
1740
1786
  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
1787
  return "external";
1742
1788
  }
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
- }
1748
- function materialize(rows, registry, previous, conflicts) {
1789
+ function materialize(rows, incoming, prior, conflicts) {
1749
1790
  const next = [];
1750
1791
  for (const row of rows) {
1751
1792
  const seedId = row.id.trim();
1752
- const incoming = registry.get(seedId);
1753
- if (incoming === void 0) {
1793
+ const inc = incoming.get(seedId);
1794
+ if (inc === void 0) {
1754
1795
  next.push(row);
1755
1796
  continue;
1756
1797
  }
1757
- const prior = previous.get(seedId);
1758
- if (prior === void 0) {
1759
- if (row.persona !== incoming) conflicts.push({
1798
+ const before = prior.get(seedId);
1799
+ if (before === void 0) {
1800
+ if (row.persona !== inc) conflicts.push({
1760
1801
  id: seedId,
1761
1802
  kind: "persona-source"
1762
1803
  });
1763
1804
  next.push(row);
1764
1805
  continue;
1765
1806
  }
1766
- if (row.persona === prior) {
1807
+ if (row.persona === before && inc !== before) {
1767
1808
  next.push({
1768
1809
  ...row,
1769
- persona: incoming
1810
+ persona: inc
1770
1811
  });
1771
1812
  continue;
1772
1813
  }
1773
- if (row.persona !== incoming) conflicts.push({
1814
+ if (row.persona !== inc) conflicts.push({
1774
1815
  id: seedId,
1775
1816
  kind: "persona-source"
1776
1817
  });
1777
1818
  next.push(row);
1778
1819
  }
1779
- for (const [id, persona] of registry) if (!rows.some((row) => row.id.trim() === id)) next.push({
1820
+ for (const [id, persona] of incoming) if (!rows.some((row) => row.id.trim() === id)) next.push({
1780
1821
  id,
1781
1822
  persona
1782
1823
  });
@@ -2230,6 +2271,37 @@ async function resolveHeadDisplayName(llm, head) {
2230
2271
  } catch {}
2231
2272
  return head.model;
2232
2273
  }
2274
+ function envelopeProvenance(replayState) {
2275
+ if (typeof replayState !== "object" || replayState === null) return void 0;
2276
+ const response = replayState.response;
2277
+ if (typeof response !== "object" || response === null) return void 0;
2278
+ const { provider, model } = response;
2279
+ if (typeof provider !== "string" || provider === "" || provider === "FallbacksChain") return void 0;
2280
+ if (typeof model !== "string" || model === "") return void 0;
2281
+ return {
2282
+ provider,
2283
+ model
2284
+ };
2285
+ }
2286
+ function restoreEnvelopeProvenance(messages) {
2287
+ let changed = false;
2288
+ const restored = messages.map((message) => {
2289
+ const source = message.source;
2290
+ if (message.role !== "assistant" || source.kind !== "model") return message;
2291
+ if (source.provider !== "FallbacksChain" || source.model !== "Auto") return message;
2292
+ const provenance = envelopeProvenance(source.replayState);
2293
+ if (provenance === void 0) return message;
2294
+ changed = true;
2295
+ return {
2296
+ ...message,
2297
+ source: {
2298
+ ...source,
2299
+ ...provenance
2300
+ }
2301
+ };
2302
+ });
2303
+ return changed ? restored : messages;
2304
+ }
2233
2305
  var FallbacksChainAdapter = class extends LlmAdapter {
2234
2306
  readConfig;
2235
2307
  getLlm;
@@ -2277,6 +2349,15 @@ var FallbacksChainAdapter = class extends LlmAdapter {
2277
2349
  name: model
2278
2350
  };
2279
2351
  }
2352
+ providerRetryPolicy(_provider) {
2353
+ try {
2354
+ const head = effectiveHeadOf(this.readConfig(), new Date());
2355
+ if (head === void 0 || head.provider === "FallbacksChain") return void 0;
2356
+ return this.getLlm()?.providerRetryPolicy(head.provider);
2357
+ } catch {
2358
+ return;
2359
+ }
2360
+ }
2280
2361
  imageRequestPricing(_provider, _model) {
2281
2362
  try {
2282
2363
  const head = effectiveHeadOf(this.readConfig(), new Date());
@@ -2297,7 +2378,8 @@ var FallbacksChainAdapter = class extends LlmAdapter {
2297
2378
  return llm.stream({
2298
2379
  ...options,
2299
2380
  provider: head.provider,
2300
- model: head.model
2381
+ model: head.model,
2382
+ messages: restoreEnvelopeProvenance(options.messages)
2301
2383
  });
2302
2384
  }
2303
2385
  };
@@ -2412,11 +2494,29 @@ function countRetryEvents(session, turn, step, provider) {
2412
2494
  }
2413
2495
  return count;
2414
2496
  }
2415
- function currentModel(agent, provider) {
2416
- return {
2497
+ function anchorServedRoute(config, route, now) {
2498
+ if (route.provider !== "FallbacksChain" || route.model !== "Auto") return route;
2499
+ const head = effectiveHeadOf(config, now);
2500
+ return head === void 0 ? route : {
2501
+ provider: head.provider,
2502
+ model: head.model
2503
+ };
2504
+ }
2505
+ function servedRouteFor(agent, config) {
2506
+ const provider = agent.options?.provider;
2507
+ const model = agent.options?.model;
2508
+ if (provider === void 0 || model === void 0) return void 0;
2509
+ const anchored = anchorServedRoute(config, {
2510
+ provider,
2511
+ model
2512
+ }, new Date());
2513
+ return anchored.provider === provider && anchored.model === model ? void 0 : anchored;
2514
+ }
2515
+ function currentModel(agent, provider, config, now) {
2516
+ return anchorServedRoute(config, {
2417
2517
  provider,
2418
2518
  model: agent.session.requestHeader()?.config.model ?? agent.options.model ?? ""
2419
- };
2519
+ }, now);
2420
2520
  }
2421
2521
  function slotWinnerKey(winner) {
2422
2522
  if (winner === "all-day") return "all-day";
@@ -2573,7 +2673,7 @@ function apply(ctx, config = defaultFallbacksConfig) {
2573
2673
  states.syncStep(state, turn, step);
2574
2674
  if (state.stepFailures.switchCount >= config.maxSwitchesPerStep) return null;
2575
2675
  }
2576
- const role = resolveRole(agent, config.roles.rules, roleIds, logger.warn);
2676
+ const role = resolveRole(agent, config.roles.rules, roleIds, logger.warn, servedRouteFor(agent, config));
2577
2677
  const rootTail = agent.session?.header?.origin === "subagent" ? config.rootChain : resolveEffectiveChain(config, new Date(), config.tz ?? "Asia/Shanghai");
2578
2678
  const { all, wildcard } = resolveChainViews(config.roles.list, rootTail, role, current.provider, current.model, logger.warn);
2579
2679
  if (all.length === 0) return null;
@@ -2671,7 +2771,7 @@ function apply(ctx, config = defaultFallbacksConfig) {
2671
2771
  ctx.on("agent/request-error", async ({ agent, turn, step, provider, failure }, next) => {
2672
2772
  const config = source();
2673
2773
  if (!config.enabled || !config.triggerCodes.includes(failure.code)) return next();
2674
- const current = currentModel(agent, provider);
2774
+ const current = currentModel(agent, provider, config, new Date());
2675
2775
  if (!current.model) return next();
2676
2776
  try {
2677
2777
  const pending = await decide(agent, turn, step, current, "trigger-code", states.peek(agent.id), failure.code);
@@ -2706,17 +2806,6 @@ function apply(ctx, config = defaultFallbacksConfig) {
2706
2806
  label: slot.label
2707
2807
  });
2708
2808
  }
2709
- if (config.enabled && seed.provider === "FallbacksChain" && seed.model === "Auto" && agent.session?.header?.origin !== "subagent") {
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");
2711
- else {
2712
- const head = firstDispatchableExactHead(resolveEffectiveChain(config, new Date(), config.tz ?? "Asia/Shanghai"));
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");
2714
- else {
2715
- logger.info("llm-fallbacks: FallbacksChain/Auto selection overrides to the effective head %s/%s", head.provider, head.model);
2716
- return overrideConfig(seed, head);
2717
- }
2718
- }
2719
- }
2720
2809
  if (config.enabled && hasChains && agent.session?.header?.origin === "subagent" && !dispatchInjected.has(agent.id)) {
2721
2810
  dispatchInjected.add(agent.id);
2722
2811
  try {
@@ -2739,7 +2828,8 @@ function apply(ctx, config = defaultFallbacksConfig) {
2739
2828
  const role = await resolveRoleAtDispatch(agent, config.roles.rules, roleIds, {
2740
2829
  automatchEnabled: config.roleAutoMatch ?? true,
2741
2830
  automatch: (agent) => pickRoleByLlm(ctx, config.roles, agent, { warn: logger.warn }),
2742
- warn: logger.warn
2831
+ warn: logger.warn,
2832
+ servedRoute: servedRouteFor(agent, config)
2743
2833
  });
2744
2834
  if (role !== "inherit") {
2745
2835
  const { all, wildcard } = resolveChainViews(config.roles.list, config.rootChain, role, seed.provider, seed.model, logger.warn);
@@ -2784,19 +2874,17 @@ function apply(ctx, config = defaultFallbacksConfig) {
2784
2874
  }
2785
2875
  if (hasChains && config.enabled && config.alwaysModeRetryCap > 0 && countRetryEvents(agent.session, turn, step, seed.provider) >= config.alwaysModeRetryCap) {
2786
2876
  const decisionState = states.peek(agent.id);
2787
- const pending = await decide(agent, turn, step, {
2877
+ const capRoute = anchorServedRoute(config, {
2788
2878
  provider: seed.provider,
2789
2879
  model: seed.model
2790
- }, "always-cap", decisionState);
2880
+ }, new Date());
2881
+ const pending = await decide(agent, turn, step, capRoute, "always-cap", decisionState);
2791
2882
  if (pending !== null) {
2792
2883
  const commitState = states.get(agent.id);
2793
2884
  commit(commitState, pending, turn, step);
2794
2885
  const appliedCap = states.applyPending(commitState, turn, step);
2795
2886
  if (appliedCap !== void 0) return overrideConfig(seed, appliedCap.to);
2796
- } else failHalfOpenProbe(agent.id, {
2797
- provider: seed.provider,
2798
- model: seed.model
2799
- });
2887
+ } else failHalfOpenProbe(agent.id, capRoute);
2800
2888
  }
2801
2889
  return seed;
2802
2890
  });
@@ -2816,13 +2904,18 @@ function apply(ctx, config = defaultFallbacksConfig) {
2816
2904
  });
2817
2905
  ctx.on("session/event", (session, event) => {
2818
2906
  if (event.type !== "assistant/message") return;
2819
- if ((source().recovery ?? "timer") !== "half-open") return;
2907
+ const config = source();
2908
+ if ((config.recovery ?? "timer") !== "half-open") return;
2820
2909
  if (event.data.interrupted === true) return;
2821
2910
  const message = event.data.message;
2822
2911
  if (message.source.kind !== "model") return;
2823
2912
  const state = states.peek(session.id);
2824
2913
  if (state === void 0) return;
2825
- states.observeSuccess(state, selectorKey(message.source.provider, message.source.model));
2914
+ const observed = anchorServedRoute(config, {
2915
+ provider: message.source.provider,
2916
+ model: message.source.model
2917
+ }, new Date());
2918
+ states.observeSuccess(state, selectorKey(observed.provider, observed.model));
2826
2919
  });
2827
2920
  ctx.effect(() => () => {
2828
2921
  states.clear();
@@ -2836,7 +2929,7 @@ function apply(ctx, config = defaultFallbacksConfig) {
2836
2929
  const fallbacksCommandController = {
2837
2930
  getSnapshot(agent) {
2838
2931
  const config = source();
2839
- const role = resolveRole(agent, config.roles.rules, roleIds, logger.warn);
2932
+ const role = resolveRole(agent, config.roles.rules, roleIds, logger.warn, servedRouteFor(agent, config));
2840
2933
  const state = states.peek(agent.id);
2841
2934
  const recovery = config.recovery ?? "timer";
2842
2935
  if (state !== void 0 && recovery === "half-open") states.syncRecovery(state, Date.now(), recovery);
@@ -51,6 +51,17 @@ export interface ResolveRoleAtDispatchOptions {
51
51
  automatch?: (agent: AgentLike) => Promise<string | null>;
52
52
  /** Warning sink — the decision path injects the plugin logger. */
53
53
  warn: (message: string) => void;
54
+ /**
55
+ * The route the agent's request was actually SERVED by, when that differs
56
+ * from the pair recorded on the agent (the virtual `FallbacksChain/Auto`
57
+ * row). Rule matching accepts either, so a rule keyed on the real chain head
58
+ * keeps matching a child whose recorded route is the virtual pair (QC1 I-1).
59
+ * `undefined` for every real route.
60
+ */
61
+ servedRoute?: {
62
+ provider: string;
63
+ model: string;
64
+ };
54
65
  }
55
66
  /**
56
67
  * Resolve the dispatch-time role for an agent across three ordered stages
package/dist/roles.d.ts CHANGED
@@ -82,4 +82,7 @@ export interface AgentLike {
82
82
  * decision path injects the cordis `logger.warn` so every plugin warning
83
83
  * flows through the plugin log namespace (qc2 F-002 / qc3 S-3).
84
84
  */
85
- export declare function resolveRole(agent: AgentLike, rules: FallbacksRoleRule[], roleIds: ReadonlyMap<string, string>, warn?: (message: string) => void): string;
85
+ export declare function resolveRole(agent: AgentLike, rules: FallbacksRoleRule[], roleIds: ReadonlyMap<string, string>, warn?: (message: string) => void, servedRoute?: {
86
+ provider: string;
87
+ model: string;
88
+ }): string;
package/dist/seeds.d.ts CHANGED
@@ -11,20 +11,34 @@
11
11
  * State model (spec §9.2) — two stores, strictly separated:
12
12
  * 1. operator config rows (persisted; a seeded role is a plain
13
13
  * `roles.list` row `{ id, persona }`), and
14
- * 2. an in-memory per-apply seed registry (`Map<id, { persona, set }>` —
15
- * the declared default persona plus its provenance label), declare =
16
- * replacement (the batch is the companion's full current set).
14
+ * 2. an in-memory per-apply seed registry keyed by PRODUCER LABEL
15
+ * (`bundled` | set name | `external`), each slice holding that
16
+ * producer's current id → `{ persona, set }` declarations. Declare =
17
+ * replacement PER PRODUCER: a declare replaces only its own slice, so
18
+ * the plugin's bundled presets stay `bundled` even when a companion
19
+ * merge-preserves them into its own batch (plan
20
+ * seeds-source-and-persona-width, Decisions #1).
17
21
  * `seeded` / `personaOverridden` / `source` are DERIVED at read time, never
18
22
  * stored — a config round-trip cannot orphan an override (AC-3), and
19
23
  * provenance rides the in-memory registry only, so it can never churn a
20
24
  * settings write.
21
25
  *
26
+ * Resolution precedence (Decisions #2): `bundled` wins for any id the
27
+ * plugin's own preset self-declare currently declares; otherwise the most
28
+ * recent non-bundled producer declaring the id wins (its set name, or
29
+ * `external` when unnamed); no live declaration ⇒ `user`. Unnamed producers
30
+ * share the `external` slice (Decisions #3); `{ set }` is the remedy.
31
+ *
22
32
  * Materialization (spec §9.2): append `{ id, persona }` (two keys only,
23
33
  * R4) / attach row untouched / at-default tracking / override preserved +
24
- * `'persona-source'` conflict / omitted id drops from the registry while
25
- * the row stays (R2). No delta → no settings write (idempotent, AC-1).
26
- * Compute → write → commit registry: a failed write throws and leaves the
27
- * registry unchanged (retry-safe).
34
+ * `'persona-source'` conflict / omitted id drops from the declaring
35
+ * producer's slice while the row stays (R2). Tracking is driven by the
36
+ * id's RESOLVED effective default — `prior` = resolved before the
37
+ * declare, `incoming` = resolved after the candidate slice commit — so a
38
+ * producer that is not the resolution winner for an id it declares never
39
+ * rewrites the row (F-001). No delta → no settings write (idempotent,
40
+ * AC-1). Compute → write → commit registry: a failed write throws and
41
+ * leaves the registry unchanged (retry-safe).
28
42
  *
29
43
  * @module dsh-llm-fallbacks/seeds
30
44
  */
@@ -151,13 +165,22 @@ export interface SeedsIo {
151
165
  */
152
166
  export declare class FallbacksSeedManager {
153
167
  private readonly logger;
154
- /** Per-apply declaration registry: declared default persona + provenance label. */
168
+ /**
169
+ * Per-producer declaration registry: producer label (`bundled` | set
170
+ * name | `external`) → that producer's current slice. A declare replaces
171
+ * only its own slice; an empty batch drops the slice.
172
+ */
155
173
  private registry;
174
+ /** Monotonic declare counter — "most recent producer" for resolution precedence. */
175
+ private declareSeq;
156
176
  constructor(logger: FallbacksConfigLogger);
157
177
  /**
158
- * Declare seeds with replacement semantics — the batch is the
159
- * companion's FULL current declaration set; ids omitted from the batch
160
- * drop out of the registry while their rows remain (R2).
178
+ * Declare seeds with replacement semantics PER PRODUCER — the batch is
179
+ * the producer's FULL current declaration set; ids omitted from the
180
+ * batch drop out of that producer's slice while their rows remain (R2).
181
+ * Other producers' slices are untouched, so a companion merge-preserving
182
+ * the bundled presets into its own batch never re-labels them (Decisions
183
+ * #1).
161
184
  *
162
185
  * `options` labels the whole batch's provenance (spec §2), resolved once
163
186
  * per call: an invalid set name warns once and degrades to the unnamed
@@ -165,27 +188,46 @@ export declare class FallbacksSeedManager {
165
188
  *
166
189
  * Per-id validation AS DECLARED (spec §9.3): non-string / pattern miss /
167
190
  * reserved `inherit` / duplicate-in-batch → skip + warn; valid siblings
168
- * still apply (AC-5). Materializes per spec §9.2, writes only when the
169
- * computed `{ list, rules }` differs from the current composed roles
170
- * (idempotent, AC-1), and commits the registry only after a successful
171
- * write (compute → write → commit; retry-safe).
191
+ * still apply (AC-5). Materializes per spec §9.2 against the id's
192
+ * RESOLVED effective default before/after the candidate slice commit
193
+ * (at-default tracking follows the resolution winner — a producer that
194
+ * is not the winner for an id it declares never rewrites the row),
195
+ * writes only when the computed `{ list, rules }` differs from the
196
+ * current composed roles (idempotent, AC-1), and commits the slice only
197
+ * after a successful write (compute → write → commit; retry-safe). An
198
+ * empty batch drops the producer's slice.
172
199
  */
173
200
  declare(seeds: readonly SeedDeclaration[], io: SeedsIo, options?: SeedsInternalDeclareOptions): Promise<SeedDeclareOutcome>;
201
+ /**
202
+ * Resolve one id's live declaration across producer slices (spec §2,
203
+ * Decisions #2): the bundled preset self-declare wins for any id it
204
+ * currently declares; otherwise the most recent non-bundled producer
205
+ * declaring the id wins (its set name, or `external` when unnamed); no
206
+ * live declaration ⇒ `undefined` (the row reads `user`). Single place —
207
+ * `effectiveRoles`, `wireStatus`, and `revert` can never disagree. The
208
+ * optional `registry` argument resolves against a CANDIDATE registry
209
+ * (the declaring slice already replaced) — the same precedence, used by
210
+ * `declare` to compute the post-commit effective default for tracking.
211
+ */
212
+ private resolveDeclared;
174
213
  /**
175
214
  * Readback (b) — sync, derived: every config row annotated with
176
215
  * `seeded` / `personaOverridden` / `seedPersona` (trimmed row-id
177
216
  * membership in the live declaration set; persona inequality) plus the
178
- * row's provenance `source` (spec §2): the declaring set's label, or
179
- * `user` when no live declaration covers the row. Nothing
180
- * override-shaped is stored, so a config round-trip cannot orphan state.
217
+ * row's provenance `source` (spec §2): the winning producer's label per
218
+ * `resolveDeclared`, or `user` when no live declaration covers the row.
219
+ * Nothing override-shaped is stored, so a config round-trip cannot
220
+ * orphan state.
181
221
  */
182
222
  effectiveRoles(io: SeedsIo): EffectiveRolesReadback;
183
223
  /** Card badge state (spec §9.4): seeded rows, with the override flag and the provenance label. */
184
224
  wireStatus(io: SeedsIo): SeedsWireStatus[];
185
225
  /**
186
- * Revert one id to the CURRENT declared seed default (AC-3). Writes
187
- * persona only — the row is otherwise copied verbatim (R4). Ids absent
188
- * from the registry (`not-seeded`) or with a deleted row (`row-absent`)
226
+ * Revert one id to the CURRENT declared seed default (AC-3) — resolved
227
+ * through the same precedence as the readbacks, so a preserved bundled id
228
+ * reverts to the bundled persona, never a companion's copy. Writes
229
+ * persona only — the row is otherwise copied verbatim (R4). Ids with no
230
+ * live declaration (`not-seeded`) or with a deleted row (`row-absent`)
189
231
  * return a non-reverted outcome without throwing; a failed settings
190
232
  * write propagates loudly (spec §9.1).
191
233
  */
@@ -78,7 +78,7 @@ export declare const PRESETS: Record<PresetId, PresetDefinition>;
78
78
  * are the ordered walk before that last-resort fallback. An empty chain or
79
79
  * a chain whose tail is not an official model (including the retired V4
80
80
  * ids) keeps slot rows inert and refuses the virtual-row
81
- * override/delegate; the v0.2.2 failure walk over the raw chain stays
81
+ * delegate; the v0.2.2 failure walk over the raw chain stays
82
82
  * verbatim.
83
83
  */
84
84
  export declare function isAllDayConforming(chain: readonly string[]): boolean;
@@ -92,7 +92,8 @@ export declare function isAllDayConforming(chain: readonly string[]): boolean;
92
92
  * P6 (qc1 F-001): without a conforming all-day the slot rows are inert, so
93
93
  * this IS the raw `rootChain` — the gate lives in {@link resolveSlotState},
94
94
  * the single source every slot surface (this, the 分时切换 log, the
95
- * `/fallbacks` strip, select-is-primary, the virtual adapter delegate) reads.
95
+ * `/fallbacks` strip, the virtual adapter delegate, its served-route anchor)
96
+ * reads.
96
97
  */
97
98
  export declare function resolveEffectiveChain(config: FallbacksConfig, now: Date, tz: string): string[];
98
99
  /**