dsh-llm-fallbacks 0.1.3 → 0.1.5

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.
@@ -35,6 +35,8 @@ export declare const zh: {
35
35
  'alwaysModeRetryCap.hint': string;
36
36
  'alwaysModeRetryCap.tooltip': string;
37
37
  'advanced.label': string;
38
+ 'advanced.expand': string;
39
+ 'advanced.collapse': string;
38
40
  'rootChain.label': string;
39
41
  'rootChain.hint': string;
40
42
  'rootChain.tooltip': string;
@@ -42,8 +44,7 @@ export declare const zh: {
42
44
  'chains.selector.remove': string;
43
45
  'chains.selector.providerPlaceholder': string;
44
46
  'chains.selector.modelPlaceholder': string;
45
- 'chains.selector.wildcard': string;
46
- 'chains.selector.noModels': string;
47
+ 'chains.selector.wildcardLegacy': string;
47
48
  'chains.selector.noModelsStrict': string;
48
49
  'roles.list.label': string;
49
50
  'roles.list.hint': string;
@@ -53,6 +54,10 @@ export declare const zh: {
53
54
  'roles.idPlaceholder': string;
54
55
  'roles.persona': string;
55
56
  'roles.personaPlaceholder': string;
57
+ 'roles.seedDefault': string;
58
+ 'roles.seedOverride': string;
59
+ 'roles.revertPersona': string;
60
+ 'roles.seedChainOptional': string;
56
61
  'roles.fallback': string;
57
62
  'roles.fallback.inherit-root': string;
58
63
  'roles.fallback.none': string;
@@ -166,6 +171,8 @@ export declare const en: {
166
171
  'alwaysModeRetryCap.hint': string;
167
172
  'alwaysModeRetryCap.tooltip': string;
168
173
  'advanced.label': string;
174
+ 'advanced.expand': string;
175
+ 'advanced.collapse': string;
169
176
  'rootChain.label': string;
170
177
  'rootChain.hint': string;
171
178
  'rootChain.tooltip': string;
@@ -173,8 +180,7 @@ export declare const en: {
173
180
  'chains.selector.remove': string;
174
181
  'chains.selector.providerPlaceholder': string;
175
182
  'chains.selector.modelPlaceholder': string;
176
- 'chains.selector.wildcard': string;
177
- 'chains.selector.noModels': string;
183
+ 'chains.selector.wildcardLegacy': string;
178
184
  'chains.selector.noModelsStrict': string;
179
185
  'roles.list.label': string;
180
186
  'roles.list.hint': string;
@@ -184,6 +190,10 @@ export declare const en: {
184
190
  'roles.idPlaceholder': string;
185
191
  'roles.persona': string;
186
192
  'roles.personaPlaceholder': string;
193
+ 'roles.seedDefault': string;
194
+ 'roles.seedOverride': string;
195
+ 'roles.revertPersona': string;
196
+ 'roles.seedChainOptional': string;
187
197
  'roles.fallback': string;
188
198
  'roles.fallback.inherit-root': string;
189
199
  'roles.fallback.none': string;
package/dist/gateway.d.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  /**
2
- * T1 (plan llm-fallbacks-settings-gateway) — host-side `fallbacks` config
3
- * gateway: the `/api/fallbacks/get` + `/api/fallbacks/set` +
4
- * `/api/fallbacks/reset` endpoints.
2
+ * T1 (plan llm-fallbacks-settings-gateway) + T3 (plan fallbacks-role-seeds) —
3
+ * host-side `fallbacks` config gateway: the `/api/fallbacks/get` +
4
+ * `/api/fallbacks/set` + `/api/fallbacks/reset` + `/api/fallbacks/revert-seed`
5
+ * endpoints.
5
6
  *
6
7
  * Transport: the typertGateway `/api` interceptor is the single host-wide RPC
7
8
  * slot (a plugin must NOT `connection.rpc.intercept('/api')` again — it would
@@ -37,7 +38,11 @@
37
38
  * method — advisor has only get/set) clears the user layer via
38
39
  * `ctx.settings.replace(ns, {})`: `set` is merge-only and cannot express
39
40
  * "reset to composition defaults" (sending default VALUES as a patch would
40
- * pin stale defaults into the user layer).
41
+ * pin stale defaults into the user layer). Every read response (get/set/reset)
42
+ * carries the additive `seeds: SeedsWireStatus[]` badge state (spec §9.4) and
43
+ * `revert-seed` exposes revert-to-current-seed-default for one id — both
44
+ * delegate to the per-apply `FallbacksSeedManager` passed into the
45
+ * constructor (single point of truth, no copied manager logic).
41
46
  *
42
47
  * The settings service is OPTIONAL (no settings service → the bridge source
43
48
  * stays the entry, `get` still works; `set`/`reset` fail with a clear
@@ -57,6 +62,7 @@ import type { Context } from '@deepseek-ai/cordis';
57
62
  import { TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol';
58
63
  import type { TypertContribution } from '@deepseek-ai/dsh-typert-registry';
59
64
  import type { FallbacksConfig } from './config';
65
+ import type { FallbacksSeedManager, SeedRevertOutcome, SeedsWireStatus } from './seeds';
60
66
  /** The `fallbacks` settings namespace (registered when a settings service exists). */
61
67
  export declare const FALLBACKS_SETTINGS_NAMESPACE: import("@deepseek-ai/dsh-settings").SettingsNamespace;
62
68
  /**
@@ -72,6 +78,20 @@ export interface FallbacksSettingsBridge {
72
78
  }
73
79
  /** Patch shape accepted by `fallbacks.set` — any subset of the config keys. */
74
80
  export type FallbacksConfigPatch = Partial<FallbacksConfig>;
81
+ /**
82
+ * The wire response of every read (get/set/reset — W-1/F-1): the normalized
83
+ * config plus `legacyKeys` plus the additive `seeds` badge state (spec §9.4,
84
+ * legacyKeys precedent — old clients ignore it).
85
+ */
86
+ export interface FallbacksReadResult {
87
+ config: FallbacksConfig;
88
+ legacyKeys: string[];
89
+ seeds: SeedsWireStatus[];
90
+ }
91
+ /** The `fallbacks/revert-seed` response — a read result plus the revert outcome. */
92
+ export interface FallbacksRevertResult extends FallbacksReadResult {
93
+ outcome: SeedRevertOutcome;
94
+ }
75
95
  /**
76
96
  * The host-side `fallbacks` config gateway (`/api/fallbacks/get` +
77
97
  * `/api/fallbacks/set` + `/api/fallbacks/reset`). Registered as the cordis
@@ -86,14 +106,19 @@ export type FallbacksConfigPatch = Partial<FallbacksConfig>;
86
106
  */
87
107
  export declare class FallbacksConfigGateway extends TypertRemoteService {
88
108
  private readonly bridge;
109
+ /** The per-apply seed manager — single point of truth for seed state (spec §9.4). */
110
+ private readonly seeds;
89
111
  /** The live settings service once the optional inject child activates. */
90
112
  private settings;
91
113
  /**
92
114
  * @param ctx - owning context (the plugin fiber's ctx inside `apply`).
93
115
  * @param bridge - the same `FallbacksSettingsBridge` the runtime reads, so
94
116
  * get/set/reset always operate on the live composed config.
117
+ * @param seeds - the per-apply `FallbacksSeedManager` constructed in
118
+ * `apply()` — the gateway delegates badge state and revert to it (no
119
+ * copied manager logic), through the io seam built over this bridge.
95
120
  */
96
- constructor(ctx: Context, bridge: FallbacksSettingsBridge);
121
+ constructor(ctx: Context, bridge: FallbacksSettingsBridge, seeds: FallbacksSeedManager);
97
122
  /**
98
123
  * Read the current composed config (schema defaults → entry base → settings
99
124
  * user layer). No hard-gate resolver (ADR-2): the raw composed config is
@@ -102,12 +127,11 @@ export declare class FallbacksConfigGateway extends TypertRemoteService {
102
127
  * two-block-era fields (`chains` / `roles.default` / undeclared rule
103
128
  * role refs) detected on the composed source (schemastery retains them,
104
129
  * plan Task 1 Step 1), so the client can show a migration banner (spec
105
- * §9, incremental field — old clients ignore it).
130
+ * §9, incremental field — old clients ignore it) — plus the additive
131
+ * `seeds` badge state (spec §9.4, legacyKeys precedent — old clients
132
+ * ignore it).
106
133
  */
107
- get(): {
108
- config: FallbacksConfig;
109
- legacyKeys: string[];
110
- };
134
+ get(): FallbacksReadResult;
111
135
  /**
112
136
  * Validate a config patch and write it to the settings USER layer (live —
113
137
  * the runtime re-reads the same bridge source; no restart needed).
@@ -117,28 +141,42 @@ export declare class FallbacksConfigGateway extends TypertRemoteService {
117
141
  * POST-WRITE composed source (W-1/F-1): `set` is a settings MERGE, so a
118
142
  * legacy user layer (`chains` / `roles.default`) survives a new-shape
119
143
  * save — the response must keep reporting it, or the client banner
120
- * would clear against server truth. Same shape as `get`.
144
+ * would clear against server truth — plus the post-write `seeds` badge
145
+ * state (same W-1/F-1 rule as `legacyKeys`). Same shape as `get`.
121
146
  * @throws when the patch fails `Config` validation, or when no settings
122
147
  * service is composed (KD-G5: the write channel is unavailable).
123
148
  */
124
- set(patch: FallbacksConfigPatch): Promise<{
125
- config: FallbacksConfig;
126
- legacyKeys: string[];
127
- }>;
149
+ set(patch: FallbacksConfigPatch): Promise<FallbacksReadResult>;
128
150
  /**
129
151
  * Clear the fallbacks settings USER layer so the composition defaults
130
152
  * reapply (`settings.replace(ns, {})` — the in-process removal path a
131
153
  * merge-only `set` cannot express).
132
154
  * @returns the new composed config plus `legacyKeys` on the post-write
133
155
  * source — `replace` drops the user layer, but legacy keys carried by
134
- * the entry base survive and are correctly re-reported (W-1/F-1).
156
+ * the entry base survive and are correctly re-reported (W-1/F-1) — plus
157
+ * the post-write `seeds` badge state (clearing the user layer also
158
+ * clears the materialized seed rows, so the honest response reports the
159
+ * emptied state; the registry survives and the next declare re-materializes).
135
160
  * @throws when no settings service is composed (KD-G5: the write channel
136
161
  * is unavailable).
137
162
  */
138
- reset(): Promise<{
139
- config: FallbacksConfig;
140
- legacyKeys: string[];
141
- }>;
163
+ reset(): Promise<FallbacksReadResult>;
164
+ /**
165
+ * Revert one seeded role to its CURRENT declared seed default (AC-3, spec
166
+ * §9.4) — the gateway half of surface (c), delegating to the same manager
167
+ * the service method uses (single point of truth). Business failures are
168
+ * values, never throws: a non-seeded id or a deleted row returns
169
+ * `{ reverted: false, reason }` without writing. The only throw is the
170
+ * KD-G5 settings-unavailable path, and only when a write is actually
171
+ * needed (an idempotent revert at the default needs no channel).
172
+ * @param id - the seeded role id; matched by trimmed id against the
173
+ * registry (row matching, spec §9.3).
174
+ * @returns the post-write read result (config / legacyKeys / seeds — the
175
+ * write happened before this read, W-1/F-1) plus the revert `outcome`.
176
+ * @throws TypeError when `id` is not a string; Error when the settings
177
+ * write channel is unavailable (KD-G5).
178
+ */
179
+ revertSeed(id: string): Promise<FallbacksRevertResult>;
142
180
  /**
143
181
  * Read the live composed config and normalize it to the typertGateway JSON
144
182
  * wire boundary. Containment (guide §10): a malformed stored user layer
@@ -156,11 +194,25 @@ export declare class FallbacksConfigGateway extends TypertRemoteService {
156
194
  /**
157
195
  * The wire response of every read (get/set/reset — W-1/F-1): the
158
196
  * normalized config plus `legacyKeys` detected on the live composed
159
- * source. set/reset must report the POST-WRITE source: the settings
160
- * merge retains legacy user-layer keys, so a save cannot clear them —
161
- * the honest response keeps the migration banner until a get agrees.
197
+ * source plus the additive `seeds` badge state. set/reset must report the
198
+ * POST-WRITE source: the settings merge retains legacy user-layer keys,
199
+ * so a save cannot clear them — the honest response keeps the migration
200
+ * banner until a get agrees; `seeds` follows the same rule (post-write
201
+ * badge state, W-1/F-1).
162
202
  */
163
203
  private readResult;
204
+ /**
205
+ * The io seam the seed manager writes through (spec §9.1): `read` walks
206
+ * the same live bridge source the gateway reads; `writeRoles` persists a
207
+ * full `{ list, rules }` to the settings user layer — both arrays always
208
+ * computed from a fresh composed read, so the write stays correct under
209
+ * dsh-settings `mergeLayers` array-replace semantics and never touches
210
+ * operator rules. The write channel fails with the same KD-G5 message as
211
+ * set/reset when no settings service is composed. Built fresh per call so
212
+ * the mutable settings capture is read at call time (the inject child
213
+ * swaps it when the settings service appears/disappears).
214
+ */
215
+ private seedsIo;
164
216
  }
165
217
  /**
166
218
  * The explicit typert contribution for the `fallbacks` gateway endpoints —
package/dist/index.d.ts CHANGED
@@ -34,6 +34,7 @@ import { Config } from './schema.ts';
34
34
  import { resolveChain } from './chains.ts';
35
35
  import { resolveRole } from './roles.ts';
36
36
  import { FallbackStateStore } from './state.ts';
37
+ import { type EffectiveRolesReadback, type SeedDeclaration, type SeedDeclareOutcome, type SeedRevertOutcome } from './seeds.ts';
37
38
  /** The plugin row id mounted by the profile bundle patch. */
38
39
  export declare const name = "llm-fallbacks";
39
40
  /**
@@ -46,9 +47,11 @@ export declare const provide: readonly ["llm-fallbacks"];
46
47
  /**
47
48
  * The named cordis service `ctx.get('llm-fallbacks')` exposes while the
48
49
  * plugin is applied: the pure-function library surface + `name`/`version`
49
- * metadata. Deliberately NO runtime state (no stateStore, no event
50
- * emitters, no filter helpers) — state stays observable via `fallbacks/switch`
51
- * events and the library API.
50
+ * metadata, plus the three ADDITIVE role-seed methods (spec §9.1, plan
51
+ * fallbacks-role-seeds T2). Deliberately no state BEARING FIELDS (no
52
+ * stateStore, no event emitters, no filter helpers) — the seed methods are
53
+ * closures over the per-apply `FallbacksSeedManager`, so state stays behind
54
+ * the closure and dies with the fiber (spec §9.5).
52
55
  */
53
56
  export interface FallbacksService {
54
57
  /** Matches the plugin `name`. */
@@ -59,6 +62,12 @@ export interface FallbacksService {
59
62
  resolveChain: typeof resolveChain;
60
63
  validateFallbacksConfig: typeof validateFallbacksConfig;
61
64
  detectLegacyKeys: typeof detectLegacyKeys;
65
+ /** (a) Declare the companion's FULL current seed set (replacement semantics, spec §9.1). */
66
+ declareSeeds(seeds: readonly SeedDeclaration[]): Promise<SeedDeclareOutcome>;
67
+ /** (b) Sync readback — effective taxonomy with seed annotations. */
68
+ getEffectiveRoles(): EffectiveRolesReadback;
69
+ /** (c) Revert one id to the CURRENT declared seed default. */
70
+ revertSeededPersona(id: string): Promise<SeedRevertOutcome>;
62
71
  }
63
72
  declare module '@deepseek-ai/cordis' {
64
73
  interface Context {
@@ -75,6 +84,7 @@ export { resolveRole, type AgentLike, type Origin, } from './roles.ts';
75
84
  export { annotateCandidates, createCandidateFilter, hasWildcardEntry, resolveCandidate, resolveChain, resolveChainViews, selectCandidates, type AnnotatedCandidate, type CandidateFilterOptions, type CandidateSkipReason, type FailingModel, } from './chains.ts';
76
85
  export { defaultFallbacksConfig, detectLegacyKeys, INHERIT_ROLE_ID, ROLE_ID_PATTERN, validateFallbacksConfig, type FallbacksConfig, type FallbacksConfigLogger, type FallbacksRole, type FallbacksRoles, type FallbacksRoleRule, type FallbackStrategy, type RevertPolicy, } from './config.ts';
77
86
  export { parseSelector, SelectorError, type Selector, } from './selectors.ts';
87
+ export { FallbacksSeedManager, type EffectiveRole, type EffectiveRolesReadback, type SeedConflict, type SeedDeclaration, type SeedDeclareOutcome, type SeedRevertFailReason, type SeedRevertOutcome, type SeedSkipReason, type SeedsIo, type SeedsWireStatus, } from './seeds.ts';
78
88
  /**
79
89
  * @internal Test seam (T3 review Minor 3): the per-agent fallback state store
80
90
  * of the plugin applied to `ctx` — last apply wins. Not part of the plugin's
package/dist/index.js CHANGED
@@ -403,10 +403,12 @@ const ROLES_KEYS = {
403
403
  };
404
404
  var FallbacksConfigGateway = class extends TypertRemoteService {
405
405
  bridge;
406
+ seeds;
406
407
  settings;
407
- constructor(ctx, bridge) {
408
+ constructor(ctx, bridge, seeds) {
408
409
  super(ctx, "fallbacks");
409
410
  this.bridge = bridge;
411
+ this.seeds = seeds;
410
412
  ctx.inject(["settings"], (sctx) => {
411
413
  this.settings = sctx.settings;
412
414
  return () => {
@@ -433,6 +435,14 @@ var FallbacksConfigGateway = class extends TypertRemoteService {
433
435
  await settings.replace(FALLBACKS_SETTINGS_NAMESPACE, {});
434
436
  return this.readResult();
435
437
  }
438
+ async revertSeed(id) {
439
+ if (typeof id !== "string") throw new TypeError("dsh-llm-fallbacks: seed revert id must be a string");
440
+ const outcome = await this.seeds.revert(id, this.seedsIo());
441
+ return {
442
+ ...this.readResult(),
443
+ outcome
444
+ };
445
+ }
436
446
  readConfig(source = this.bridge.source()) {
437
447
  const wire = {};
438
448
  for (const key of Object.keys(CONFIG_KEYS)) {
@@ -446,7 +456,18 @@ var FallbacksConfigGateway = class extends TypertRemoteService {
446
456
  const source = this.bridge.source();
447
457
  return {
448
458
  config: this.readConfig(source),
449
- legacyKeys: detectLegacyKeys(source)
459
+ legacyKeys: detectLegacyKeys(source),
460
+ seeds: this.seeds.wireStatus(this.seedsIo())
461
+ };
462
+ }
463
+ seedsIo() {
464
+ const settings = this.settings;
465
+ return {
466
+ read: () => this.bridge.source(),
467
+ writeRoles: (roles) => {
468
+ if (settings === void 0) throw new Error("fallbacks: settings service is unavailable — configuration cannot be written");
469
+ return settings.update(FALLBACKS_SETTINGS_NAMESPACE, { roles });
470
+ }
450
471
  };
451
472
  }
452
473
  };
@@ -514,6 +535,21 @@ function fallbacksTypertContribution() {
514
535
  invocation: { kind: "direct" },
515
536
  parameters: [],
516
537
  result: { mode: "src-json" }
538
+ },
539
+ {
540
+ id: "dsh-llm-fallbacks#fallbacks/revert-seed",
541
+ service: "fallbacks",
542
+ namespace: "fallbacks",
543
+ method: "revert-seed",
544
+ implementation: "revertSeed",
545
+ invocation: { kind: "direct" },
546
+ parameters: [{
547
+ name: "id",
548
+ wire: "id",
549
+ source: "json",
550
+ codec: { mode: "src-json" }
551
+ }],
552
+ result: { mode: "src-json" }
517
553
  }
518
554
  ]
519
555
  };
@@ -644,6 +680,188 @@ function registerFallbacksCommands(registry, controller, locale = "zh") {
644
680
  });
645
681
  }
646
682
  //#endregion
683
+ //#region .build/host/seeds.js
684
+ var FallbacksSeedManager = class {
685
+ logger;
686
+ registry = new Map();
687
+ constructor(logger) {
688
+ this.logger = logger;
689
+ }
690
+ async declare(seeds, io) {
691
+ const outcome = {
692
+ applied: [],
693
+ skipped: [],
694
+ conflicts: []
695
+ };
696
+ const registry = new Map();
697
+ for (const seed of seeds) {
698
+ if (typeof seed.id !== "string" || !ROLE_ID_PATTERN.test(seed.id)) {
699
+ outcome.skipped.push({
700
+ id: String(seed.id),
701
+ reason: "invalid-id"
702
+ });
703
+ this.warnSkip(seed.id, "invalid-id");
704
+ continue;
705
+ }
706
+ if (seed.id === "inherit") {
707
+ outcome.skipped.push({
708
+ id: seed.id,
709
+ reason: "reserved-id"
710
+ });
711
+ this.warnSkip(seed.id, "reserved-id");
712
+ continue;
713
+ }
714
+ if (registry.has(seed.id)) {
715
+ outcome.skipped.push({
716
+ id: seed.id,
717
+ reason: "duplicate-in-batch"
718
+ });
719
+ this.warnSkip(seed.id, "duplicate-in-batch");
720
+ continue;
721
+ }
722
+ registry.set(seed.id, seed.persona);
723
+ outcome.applied.push(seed.id);
724
+ }
725
+ const config = io.read();
726
+ const currentList = roleRows(config);
727
+ const currentRules = roleRules(config);
728
+ const newList = materialize(currentList, registry, this.registry, outcome.conflicts);
729
+ 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)`);
730
+ const computed = {
731
+ list: newList,
732
+ rules: currentRules
733
+ };
734
+ if (!deepEqual(newList, currentList)) await io.writeRoles(computed);
735
+ this.registry = registry;
736
+ return outcome;
737
+ }
738
+ effectiveRoles(io) {
739
+ return { roles: roleRows(io.read()).map((row) => {
740
+ const seedPersona = this.registry.get(row.id.trim());
741
+ const seeded = seedPersona !== void 0;
742
+ const effective = {
743
+ id: row.id,
744
+ persona: row.persona,
745
+ seeded,
746
+ personaOverridden: seeded && row.persona !== seedPersona
747
+ };
748
+ if (seeded) effective.seedPersona = seedPersona;
749
+ if (row.chain !== void 0) effective.chain = row.chain;
750
+ if (row.fallback !== void 0) effective.fallback = row.fallback;
751
+ return effective;
752
+ }) };
753
+ }
754
+ wireStatus(io) {
755
+ const status = [];
756
+ for (const row of roleRows(io.read())) {
757
+ const seedPersona = this.registry.get(row.id.trim());
758
+ if (seedPersona === void 0) continue;
759
+ status.push({
760
+ id: row.id,
761
+ overridden: row.persona !== seedPersona
762
+ });
763
+ }
764
+ return status;
765
+ }
766
+ async revert(id, io) {
767
+ const seedId = id.trim();
768
+ const seedPersona = this.registry.get(seedId);
769
+ if (seedPersona === void 0) return {
770
+ reverted: false,
771
+ reason: "not-seeded"
772
+ };
773
+ const config = io.read();
774
+ const rows = roleRows(config);
775
+ const rules = roleRules(config);
776
+ const index = rows.findIndex((row) => row.id.trim() === seedId);
777
+ if (index === -1) return {
778
+ reverted: false,
779
+ reason: "row-absent"
780
+ };
781
+ if (rows[index].persona === seedPersona) return {
782
+ reverted: true,
783
+ persona: seedPersona
784
+ };
785
+ const nextList = rows.map((row, i) => i === index ? {
786
+ ...row,
787
+ persona: seedPersona
788
+ } : row);
789
+ await io.writeRoles({
790
+ list: nextList,
791
+ rules
792
+ });
793
+ return {
794
+ reverted: true,
795
+ persona: seedPersona
796
+ };
797
+ }
798
+ warnSkip(id, reason) {
799
+ const shown = typeof id === "string" ? JSON.stringify(id) : String(id);
800
+ if (reason === "invalid-id") this.logger.warn(`llm-fallbacks: seeds: skipping seed id ${shown} — invalid-id (must match ${String(ROLE_ID_PATTERN)} as declared)`);
801
+ else if (reason === "reserved-id") this.logger.warn(`llm-fallbacks: seeds: skipping seed id ${shown} — reserved-id ("${INHERIT_ROLE_ID}" is not a legal seed target)`);
802
+ else this.logger.warn(`llm-fallbacks: seeds: skipping seed id ${shown} — duplicate-in-batch (first wins)`);
803
+ }
804
+ };
805
+ function materialize(rows, registry, previous, conflicts) {
806
+ const next = [];
807
+ for (const row of rows) {
808
+ const seedId = row.id.trim();
809
+ const incoming = registry.get(seedId);
810
+ if (incoming === void 0) {
811
+ next.push(row);
812
+ continue;
813
+ }
814
+ const prior = previous.get(seedId);
815
+ if (prior === void 0) {
816
+ if (row.persona !== incoming) conflicts.push({
817
+ id: seedId,
818
+ kind: "persona-source"
819
+ });
820
+ next.push(row);
821
+ continue;
822
+ }
823
+ if (row.persona === prior) {
824
+ next.push({
825
+ ...row,
826
+ persona: incoming
827
+ });
828
+ continue;
829
+ }
830
+ if (row.persona !== incoming) conflicts.push({
831
+ id: seedId,
832
+ kind: "persona-source"
833
+ });
834
+ next.push(row);
835
+ }
836
+ for (const [id, persona] of registry) if (!rows.some((row) => row.id.trim() === id)) next.push({
837
+ id,
838
+ persona
839
+ });
840
+ return next;
841
+ }
842
+ function roleRows(config) {
843
+ const list = config.roles?.list;
844
+ return Array.isArray(list) ? list : [];
845
+ }
846
+ function roleRules(config) {
847
+ const rules = config.roles?.rules;
848
+ return Array.isArray(rules) ? rules : [];
849
+ }
850
+ function deepEqual(a, b) {
851
+ if (a === b) return true;
852
+ if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false;
853
+ if (Array.isArray(a) !== Array.isArray(b)) return false;
854
+ if (Array.isArray(a)) {
855
+ const aa = a;
856
+ const bb = b;
857
+ return aa.length === bb.length && aa.every((item, index) => deepEqual(item, bb[index]));
858
+ }
859
+ const aKeys = Object.keys(a);
860
+ const bKeys = Object.keys(b);
861
+ if (aKeys.length !== bKeys.length) return false;
862
+ return aKeys.every((key) => deepEqual(a[key], b[key]));
863
+ }
864
+ //#endregion
647
865
  //#region .build/host/index.js
648
866
  const name = "llm-fallbacks";
649
867
  const provide = ["llm-fallbacks"];
@@ -695,6 +913,23 @@ function overrideConfig(seed, to) {
695
913
  function apply(ctx, config = defaultFallbacksConfig) {
696
914
  const logger = ctx.logger("llm-fallbacks");
697
915
  const entry = Config(config);
916
+ const seeds = new FallbacksSeedManager(logger);
917
+ const seedsSettingsUnavailable = "llm-fallbacks: seeds: settings service is unavailable — seed roles cannot be written";
918
+ let writeRoles = () => {
919
+ throw new Error(seedsSettingsUnavailable);
920
+ };
921
+ ctx.inject(["settings"], (sctx) => {
922
+ writeRoles = (roles) => sctx.settings.update(FALLBACKS_SETTINGS_NAMESPACE, { roles });
923
+ return () => {
924
+ writeRoles = () => {
925
+ throw new Error(seedsSettingsUnavailable);
926
+ };
927
+ };
928
+ });
929
+ const seedsIo = {
930
+ read: () => source(),
931
+ writeRoles: (roles) => writeRoles(roles)
932
+ };
698
933
  try {
699
934
  ctx.provide("llm-fallbacks", {
700
935
  name: "llm-fallbacks",
@@ -702,7 +937,10 @@ function apply(ctx, config = defaultFallbacksConfig) {
702
937
  resolveRole,
703
938
  resolveChain,
704
939
  validateFallbacksConfig,
705
- detectLegacyKeys
940
+ detectLegacyKeys,
941
+ declareSeeds: (declarations) => seeds.declare(declarations, seedsIo),
942
+ getEffectiveRoles: () => seeds.effectiveRoles(seedsIo),
943
+ revertSeededPersona: (id) => seeds.revert(id, seedsIo)
706
944
  });
707
945
  } catch (error) {
708
946
  if (!(error instanceof Error) || !error.message.includes("has been registered")) throw error;
@@ -726,7 +964,7 @@ function apply(ctx, config = defaultFallbacksConfig) {
726
964
  });
727
965
  const bridge = { source: () => source() };
728
966
  try {
729
- new FallbacksConfigGateway(ctx, bridge);
967
+ new FallbacksConfigGateway(ctx, bridge, seeds);
730
968
  } catch (error) {
731
969
  if (!(error instanceof Error) || !error.message.includes("has been registered")) throw error;
732
970
  ctx.logger("llm-fallbacks").debug("fallbacks gateway already registered — no gateway on this fiber (multi-fiber dedupe)");
@@ -861,4 +1099,4 @@ function apply(ctx, config = defaultFallbacksConfig) {
861
1099
  });
862
1100
  }
863
1101
  //#endregion
864
- export { Config, INHERIT_ROLE_ID, ROLE_ID_PATTERN, SelectorError, annotateCandidates, apply, countRetryEvents, createCandidateFilter, defaultFallbacksConfig, detectLegacyKeys, hasWildcardEntry, name, parseSelector, provide, resolveCandidate, resolveChain, resolveChainViews, resolveRole, selectCandidates, stateStore, validateFallbacksConfig };
1102
+ export { Config, FallbacksSeedManager, INHERIT_ROLE_ID, ROLE_ID_PATTERN, SelectorError, annotateCandidates, apply, countRetryEvents, createCandidateFilter, defaultFallbacksConfig, detectLegacyKeys, hasWildcardEntry, name, parseSelector, provide, resolveCandidate, resolveChain, resolveChainViews, resolveRole, selectCandidates, stateStore, validateFallbacksConfig };