dsh-llm-fallbacks 0.4.4 → 0.5.1

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.
@@ -0,0 +1,156 @@
1
+ /**
2
+ * In-session role notice row (plan role-based-subagent-adoption Task 3).
3
+ *
4
+ * The child of a role-carrying dispatch gets ONE durable transcript row naming
5
+ * the role it was dispatched as, so the role is visible where the work is read
6
+ * — inside the child's own session — without reaching for live plugin state.
7
+ *
8
+ * Channel: an `agent/pre-step` listener that appends EXACTLY ONE message to the
9
+ * decision the loop is about to enter (`decision.messages`), which the loop
10
+ * then commits durably on the step's first attempt
11
+ * (`agent-loop/src/agent.ts:373-378` — `if (firstAttempt) for (const message of
12
+ * decision.messages)` the loop commits each message to the session log as a
13
+ * `'user/message'`). The listener is registered with `{ prepend: true }` (the
14
+ * `modelSwitchNotice` precedent in
15
+ * `core/agent/src/model-selection.ts:108-125`), so it is the OUTERMOST layer:
16
+ * `await next()` yields the FINAL decision every other listener shaped. The
17
+ * contract is EXACTLY ONE row, not its position: a plugin that registers its
18
+ * own `{ prepend: true }` `agent/pre-step` listener AFTER this one sits outside
19
+ * it and may append after the notice, and the loop commits the decision's
20
+ * messages in order — so the row is appended to whatever the inner listeners
21
+ * shaped, not guaranteed to be the last one.
22
+ *
23
+ * NOT `agent.inject()`: that queue is documented as lossy — "It may miss a
24
+ * request whose pre-step already claimed its batch. Cancellation or disposal
25
+ * may discard pending context." (`core/agent/src/runtime-types.ts:231-241`), and
26
+ * with `wakeup: false` an idle child is never woken to receive it. The pre-step
27
+ * decision has neither failure mode.
28
+ *
29
+ * Session-write contract (plan Global Constraints, hard): the row is a
30
+ * `user/message` built with `createUserMessage` and the sanctioned plugin source
31
+ * `{ kind: 'plugin', plugin: 'dsh-llm-fallbacks', form: 'notice', summary: … }`.
32
+ * `plugin` is a member of the FROZEN released source-kind set the V2→V3 edge
33
+ * classifies (`dsh-session-format-v2-to-v3/lib/index.js:14-30`: 15 kinds,
34
+ * includes `plugin`); a bespoke kind would write fine and then refuse at the
35
+ * next frozen edge with `cannot safely transform unclassified message source`
36
+ * (`:125`). NO custom session event type, NO `ignorable` marker — a mount-only
37
+ * plugin cannot publish either (`.mstar/projects/_default/references/
38
+ * subagent-role-badge-visible/explore-role-seam-report.md` §Q3). The guard test
39
+ * in `tests/role-notice.spec.ts` pins that membership against a checked-in
40
+ * mirror of the set.
41
+ *
42
+ * Once per child SESSION: the emitter consumes the seam's per-child record
43
+ * marker (`firstNoticePending`) AND a per-agent `emitted` set (mirroring the
44
+ * existing `dispatchInjected` pattern), both written only AFTER the row is built
45
+ * — so a throwing producer degrades without consuming the child's one notice.
46
+ * Never for a root agent, never for `inherit`/unresolved (those never get a
47
+ * record), never twice across steps or within-step retries.
48
+ *
49
+ * CF-6 — the emitted marker is SESSION-stable and bounded. `Agent.id` IS the
50
+ * child session id, and a continuable child's durable session outlives its
51
+ * activation, so the caller (`src/index.ts`) keeps this marker across
52
+ * `agent/disposed` and clears it only when the plugin itself is disposed; that
53
+ * is what makes the documented "exactly one notice row per child session" hold
54
+ * across a dispose + re-activate resume. Because it is never per-activation
55
+ * cleared it must be bounded: {@link markNoticeEmitted} evicts the oldest entry
56
+ * once {@link NOTICE_EMITTED_LIMIT} sessions are remembered (a child evicted
57
+ * that way can at worst be announced a second time, and the projection fold is
58
+ * last-wins, so no wrong role is ever produced).
59
+ *
60
+ * Degrade-never-crash: any throw in the path is contained with ONE debug line
61
+ * and the decision is returned UNCHANGED — a step is never broken, and no
62
+ * partial row is ever published.
63
+ *
64
+ * @module dsh-llm-fallbacks/role-notice
65
+ */
66
+ import type { Context } from '@deepseek-ai/cordis';
67
+ import { type UserMessage } from '@deepseek-ai/dsh-llm';
68
+ import type { SubagentSeamRecord } from './subagents-seam.ts';
69
+ /**
70
+ * The `plugin` field of every notice this plugin writes. Stable: the durable
71
+ * source's provenance label is read from it (`ui-conversation`
72
+ * `context-provenance.ts` — "the plugin id, or the bare source kind").
73
+ */
74
+ export declare const ROLE_NOTICE_PLUGIN = "dsh-llm-fallbacks";
75
+ /**
76
+ * The message source KIND the notice rides. `plugin` is the sanctioned kind for
77
+ * a plugin-authored notice and a member of the frozen released set (see the
78
+ * module header); the value is exported so the guard test asserts on the exact
79
+ * literal the emitter uses.
80
+ */
81
+ export declare const ROLE_NOTICE_SOURCE_KIND = "plugin";
82
+ /** Suffix appended when the role declares a persona that was NOT delivered. */
83
+ export declare const ROLE_NOTICE_PERSONA_SKIPPED_SUFFIX = " (persona not applied)";
84
+ /**
85
+ * How many child sessions whose notice was already emitted stay remembered
86
+ * (CF-6). The marker is session-stable — kept across `agent/disposed` so a
87
+ * re-activated continuable child is not announced twice — so it needs a bound:
88
+ * past this many sessions the oldest entry is evicted (worst case: a later
89
+ * re-activation of that evicted child writes a second identical row; the
90
+ * projection fold is last-wins, so the pill never names a wrong role).
91
+ */
92
+ export declare const NOTICE_EMITTED_LIMIT = 256;
93
+ /**
94
+ * Mark one child session as announced, evicting the oldest remembered session
95
+ * once {@link NOTICE_EMITTED_LIMIT} is reached (insertion-ordered `Set`
96
+ * semantics: the oldest entry is the first value). A session already marked
97
+ * keeps its position — its marker is only ever written once.
98
+ */
99
+ export declare function markNoticeEmitted(emitted: Set<string>, childSessionId: string): void;
100
+ /** One notice-message producer: role id + whether the declared persona was skipped. */
101
+ export type RoleNoticeBuilder = (role: string, personaNotApplied: boolean) => UserMessage;
102
+ /**
103
+ * Build the ONE role notice row (pure): `[role: <id>]`, with
104
+ * ` (persona not applied)` appended when the role declares a persona that Task
105
+ * 2's capability gate declined to deliver. The summary keeps `role: <id>` either
106
+ * way — it is the collapsed row's one-line account, and the durable role string
107
+ * must live there as well as in the content.
108
+ *
109
+ * @param role - the DECLARED RAW role id resolved at the dispatch seam.
110
+ * @param personaNotApplied - `true` only when a declared persona was skipped.
111
+ */
112
+ export declare function buildRoleNotice(role: string, personaNotApplied: boolean): UserMessage;
113
+ /** Options for {@link installRoleNotice}. */
114
+ export interface RoleNoticeOptions {
115
+ /**
116
+ * The dispatch seam's per-child records, keyed by child SESSION id (which IS
117
+ * `Agent.id`). Read at each pre-step — never captured — so a record written by
118
+ * a dispatch that races the child's first step is still seen.
119
+ */
120
+ records: Map<string, SubagentSeamRecord>;
121
+ /**
122
+ * Per-agent marker of children whose notice was already emitted (mirrors the
123
+ * runtime's `dispatchInjected`): the in-memory half of the once-per-child
124
+ * guarantee, independent of the record's `firstNoticePending` (which a record
125
+ * rewrite preserves deliberately) and of the loop's retry bookkeeping.
126
+ *
127
+ * SESSION-stable and bounded (CF-6): the caller keeps it across
128
+ * `agent/disposed` so a re-activated child is never announced twice, and
129
+ * {@link markNoticeEmitted} bounds it. ROOT-shared across the fibers applied
130
+ * over one root (CF-5), so a second fiber's emitter cannot announce a child
131
+ * the first already announced.
132
+ */
133
+ emitted: Set<string>;
134
+ /**
135
+ * Message producer. Defaults to {@link buildRoleNotice}; the only reason to
136
+ * replace it is to pin the contained-degrade path (a throwing producer must
137
+ * publish no row and must NOT consume the child's one notice).
138
+ */
139
+ buildNotice?: RoleNoticeBuilder;
140
+ /** Contained debug sink (at most ONE line per contained degrade). */
141
+ debug?: (message: string) => void;
142
+ }
143
+ /**
144
+ * Register the `agent/pre-step` notice emitter on `ctx`; returns its disposer
145
+ * (the owning fiber's teardown registers it, exactly like the seam's other
146
+ * listeners).
147
+ *
148
+ * Emission order per pre-step (cheapest first, all read-only until the emit):
149
+ * `next()` → reject/abort → empty decision → `origin === 'subagent'` → already
150
+ * emitted → pending record. The empty-decision guard is NOT cosmetic: the loop
151
+ * skips a step whose admitted messages are empty (`phase.step === 0 &&
152
+ * decision.messages.length === 0` → turn completed, no model call), so a notice
153
+ * pushed into an empty decision would turn a no-op step into a model call whose
154
+ * only input is our row.
155
+ */
156
+ export declare function installRoleNotice(ctx: Context, options: RoleNoticeOptions): () => void;
@@ -0,0 +1,41 @@
1
+ /**
2
+ * The session-projection contract of the subagent role pill (plan
3
+ * role-based-subagent-adoption Task 3b): the ONE key the host fold
4
+ * (`./role-projection.ts`) publishes and the session-header badge reads.
5
+ *
6
+ * PURE module — no `@deepseek-ai/*` import, no host-only dependency — because
7
+ * BOTH halves import it: the host through the seam's install point, the client
8
+ * through `SubagentRoleBadge`. Splitting the key (and the value guard) out of
9
+ * the host module is what keeps the client half free of the fold's host
10
+ * imports, and a single shared literal is what keeps the two halves from
11
+ * drifting into a silent "no pill".
12
+ *
13
+ * @module dsh-llm-fallbacks/role-projection-key
14
+ */
15
+ /**
16
+ * The projection key the role pill reads (`useProjection(ROLE_PROJECTION_KEY)`).
17
+ * Lower-camel like the host's own session-scoped keys (`modelSelection`,
18
+ * `sessionListMetadata`); the `fallbacks` prefix keeps a third-party key
19
+ * unambiguous inside the shared `SessionProjectionMap`.
20
+ */
21
+ export declare const ROLE_PROJECTION_KEY = "fallbacksSubagentRole";
22
+ /**
23
+ * The client-visible value of the role projection: the DECLARED RAW role id the
24
+ * child was dispatched as (padding included — it is the id the settings
25
+ * document declares), or `null` while the child's log carries no role notice
26
+ * row (`./role-notice.ts`).
27
+ *
28
+ * A role id, NOT a record: the route is a SEPARATE host projection
29
+ * (`modelSelection`), because the notice row is written once at the child's
30
+ * first step while the route it ends up on can change afterwards.
31
+ */
32
+ export type RoleProjectionValue = string | null;
33
+ /**
34
+ * Read a projection value defensively (degrade-never-crash): only a non-blank
35
+ * string is a role. `undefined` — the key is not registered on this host, or no
36
+ * baseline/frame has carried it yet — and every foreign value read as "no
37
+ * role", so the pill collapses to nothing instead of naming something wrong.
38
+ *
39
+ * @param value - the raw `useProjection('<key>')` value.
40
+ */
41
+ export declare function readRoleProjectionValue(value: unknown): string | undefined;
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Durable role visibility for the session-header badge (plan
3
+ * role-based-subagent-adoption Task 3b) — ONE host-side session projection unit
4
+ * folding the role out of the child's OWN durable log.
5
+ *
6
+ * Data source = Task 3's notice row, the plugin's single write primitive: the
7
+ * fold reads the child's committed `user/message` events, keeps the one whose
8
+ * provenance is ours (`source.kind === 'plugin'` + `source.plugin ===
9
+ * ROLE_NOTICE_PLUGIN`), and parses the role out of its **content text**
10
+ * (`[role: <id>]`). No second write path, no new event type, no plugin-owned
11
+ * store: the projection is a pure READ of the log, and the host's projection
12
+ * cache checkpoints every registered unit at session creation, `turn/end` and
13
+ * disposal — so a settled child (served as an unpublished observation whose
14
+ * `projections` ride the follow opening snapshot) and a post-restart view both
15
+ * work with nothing of ours on disk.
16
+ *
17
+ * NEVER `source.summary`: summaries are truncated at
18
+ * `CONTEXT_SUMMARY_MAX_CHARS = 120` (`@deepseek-ai/dsh-llm` `message.ts`,
19
+ * applied by `boundContextSummary`) while a role id has no length bound in the
20
+ * settings schema — a long id would be CUT there and the pill would name a role
21
+ * that does not exist. The content blocks are unbounded, so the content text is
22
+ * the only correct source; the row's provenance is what makes the pre-filter
23
+ * cheap.
24
+ *
25
+ * Mount-only + STRUCTURAL: the registry and the unit contract are consumed as
26
+ * the local structural views below and this module value-imports no host seam,
27
+ * so the host calls INTO us. The declared
28
+ * `@deepseek-ai/dsh-session-projection` peerDependency (Task 3b L2 review M-2)
29
+ * serves the in-process Step-0 spec, which builds the REAL registry — being the
30
+ * real registry is the whole point of that proof — not a runtime import here
31
+ * (the badge reads its seat structurally for the same reason).
32
+ * The runtime shape relied on — `register({ key, stateSchema, init, apply,
33
+ * wire: { viewSchema, view }, stateVersion })` → unregister disposer — is the
34
+ * documented `ProjectionDefinition` contract of
35
+ * `@deepseek-ai/dsh-session-projection@0.1.5-rc.1`
36
+ * (`lib/types/index.d.ts:38-80,150-152`); only `.parse` is ever called on the
37
+ * schemas (`lib/index.js:255,305,433`).
38
+ *
39
+ * @module dsh-llm-fallbacks/role-projection
40
+ */
41
+ import type { Context } from '@deepseek-ai/cordis';
42
+ import type { SessionEvent } from '@deepseek-ai/dsh-session';
43
+ import { type RoleProjectionValue } from './role-projection-key.ts';
44
+ /**
45
+ * The `.parse` face the registry uses on a definition's `stateSchema` /
46
+ * `wire.viewSchema` (a structural stand-in for the peer's `ZodType`, so this
47
+ * module needs no zod import and no new peer).
48
+ */
49
+ interface ProjectionValueSchema<T> {
50
+ parse(value: unknown): T;
51
+ }
52
+ /**
53
+ * One registered projection unit, as this plugin declares it (the consumed
54
+ * subset of the host's `ProjectionDefinition`): `init` and `apply` are PURE and
55
+ * SYNCHRONOUS, `state` is plain JSON (the persisted-checkpoint precondition),
56
+ * and `wire` publishes the client-visible value.
57
+ */
58
+ export interface RoleProjectionUnit {
59
+ readonly key: string;
60
+ readonly stateSchema: ProjectionValueSchema<RoleProjectionState | null>;
61
+ init(header: unknown, inheritedEventCount: number): RoleProjectionState | null;
62
+ apply(state: RoleProjectionState | null, event: SessionEvent): RoleProjectionState | null;
63
+ readonly stateVersion: number;
64
+ readonly wire: {
65
+ readonly viewSchema: ProjectionValueSchema<RoleProjectionValue>;
66
+ view(state: RoleProjectionState | null): RoleProjectionValue;
67
+ };
68
+ }
69
+ /**
70
+ * The host fold state: the fork boundary the fold was initialized with, plus the
71
+ * resolved role once an OWN notice row has landed (`role` absent until then).
72
+ * An OBJECT, not a bare string, because the registry's publication rule is
73
+ * reference identity (`Object.is`) — the fold must be able to prove it left the
74
+ * state untouched, and that proof is only meaningful for a value the fold could
75
+ * otherwise re-allocate.
76
+ *
77
+ * `inheritedEventCount` is part of the state, not a closure constant: the
78
+ * registry hands `init` the exact fork-inherited prefix length and then folds
79
+ * the WHOLE log into `apply`, so the boundary has to survive into every `apply`
80
+ * call (and into a persisted checkpoint row) for the fold to skip an ancestor's
81
+ * events.
82
+ */
83
+ export interface RoleProjectionState {
84
+ /**
85
+ * The exact fork-inherited prefix length `init` was handed: events with
86
+ * `seq < inheritedEventCount` belong to the ANCESTOR the child was seeded
87
+ * from, never to the child.
88
+ */
89
+ readonly inheritedEventCount: number;
90
+ /** The DECLARED RAW role id, verbatim (padding included); absent until then. */
91
+ readonly role?: string;
92
+ }
93
+ /**
94
+ * The registry face this plugin uses (`ctx.sessionProjections`): the
95
+ * client-visible `register` overload plus the read faces the in-process proof
96
+ * reads. Structural on purpose — see the module header.
97
+ */
98
+ export interface RoleProjectionRegistryView {
99
+ register(unit: RoleProjectionUnit): () => void;
100
+ }
101
+ /**
102
+ * The projection state/version. Bump `stateVersion` whenever the fold changes —
103
+ * v2 added the fork boundary (`inheritedEventCount`), so a v1 checkpoint row is
104
+ * NOT re-usable and must be refolded from the log.
105
+ */
106
+ export declare const ROLE_PROJECTION_STATE_VERSION = 2;
107
+ /**
108
+ * The ONE role projection unit (plan Task 3b).
109
+ *
110
+ * `init` records the fork boundary and nothing else (a child's own log carries
111
+ * no role yet), `apply` skips every event BELOW that boundary — the seeded
112
+ * prefix is the ANCESTOR's log, and the plan pins "child whose notice never
113
+ * landed: no pill, never a wrong pill" — and otherwise folds last-wins over our
114
+ * own notice rows, returning the SAME state reference for every event it does
115
+ * not use (the registry's `Object.is` rule: an unchanged reference produces zero
116
+ * downstream work and no client publication) — including a repeated notice with
117
+ * the same role, which is what makes the fold idempotent under a replay.
118
+ *
119
+ * The boundary carries into the state because the registry calls `apply` with
120
+ * the WHOLE log (including the fork prefix) on every rung: `buildCell`,
121
+ * `drive`'s late-registration fold and `restore` all fold from `init` forward.
122
+ * The host's own sibling unit in the same tree does exactly this
123
+ * (`@deepseek-ai/dsh-subagent` `subagentCatalogProjectionDefinition`:
124
+ * `init: (_header, inheritedEventCount) => ({ inheritedEventCount })` and
125
+ * `apply: … || event.seq < state.inheritedEventCount → return state`).
126
+ */
127
+ export declare const roleProjectionUnit: RoleProjectionUnit;
128
+ /** Options for {@link installRoleProjection}. */
129
+ export interface RoleProjectionOptions {
130
+ /** Contained debug sink (at most ONE line per contained degrade). */
131
+ debug?: (message: string) => void;
132
+ }
133
+ /**
134
+ * Register {@link roleProjectionUnit} on `ctx.sessionProjections` through the
135
+ * same conditional-inject idiom the plugin already uses for `settings` / `llm` /
136
+ * `typert`: the child activates only when the registry is composed, so a
137
+ * composition without it keeps the key absent and the badge renders nothing
138
+ * (capability absence — never a dispatch failure, never a crash).
139
+ *
140
+ * Registration is an effect on the child fiber: a second apply over the same
141
+ * root registers the SAME key at the SAME `stateVersion`, which the registry
142
+ * counts (one unit, the key survives until the last holder unloads) — so this
143
+ * needs no dedupe of its own, and a reshaped registry that refuses the
144
+ * registration degrades with one contained debug line.
145
+ *
146
+ * @param ctx - the plugin fiber's context.
147
+ * @param options - contained debug sink.
148
+ * @returns the disposer that withdraws the child (and with it the key).
149
+ */
150
+ export declare function installRoleProjection(ctx: Context, options?: RoleProjectionOptions): () => void;
151
+ export {};
@@ -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
  */