dsh-llm-fallbacks 0.6.13 → 0.8.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.
@@ -1,105 +1,37 @@
1
1
  /**
2
- * Dispatch-time role resolution (plan fallbacks-role-automatch Task 2; the
3
- * dispatch-role stage added by plan dsh-148-agent-team-role-delivery Task 1).
4
- *
5
- * Precedence (first hit wins) across four ordered stages:
2
+ * Dispatch-time role resolution (named virtual chains, plan
3
+ * named-virtual-chains T3): narrowed to TWO stages —
6
4
  * 1. explicit — `session.header.agentPreset` trimmed matches a declared role
7
- * id → that role's DECLARED RAW id (via the `roleIds` trimmed-id map, the
8
- * same canonicalization the rules path uses); the reserved `'inherit'`
9
- * and undeclared presets are NEVER dispatchable explicit roles and fall
10
- * through to the next stage;
5
+ * id → that role's DECLARED RAW id (via the `roleIds` trimmed-id map); the
6
+ * reserved `'inherit'` and undeclared presets are NEVER dispatchable
7
+ * explicit roles;
11
8
  * 2. dispatch — `opts.dispatchRole`: the role the dispatch seam resolved from
12
9
  * the Assignment/teammate prompt (`**Execute as**: <id>`, the single
13
10
  * parser in `src/subagents-seam.ts`), canonicalized by the same trim +
14
11
  * `roleIds` lookup. An absent, empty, `'inherit'` or no-longer-declared
15
- * reference falls through silently — a role removed after the dispatch is
16
- * not an error;
17
- * 3. rules — the existing `resolveRole` passthrough, unchanged (a declared
18
- * role wins; no-rule-match / an explicit rule targeting `'inherit'`
19
- * resolve to `'inherit'`);
20
- * 4. auto-match — only when stage 3 resolved to `'inherit'` AND
21
- * `opts.automatchEnabled`: call `opts.automatch` with the agent. The hook
22
- * (Task 3 supplies the bounded LLM caller) MUST return a declared raw id
23
- * or `null`; the resolver defensively validates the return against
24
- * `roleIds` — an unknown id warns and resolves to `'inherit'`, never an
25
- * undeclared role. When `automatch` is undefined or `automatchEnabled`
26
- * is false the hook is skipped.
27
- *
28
- * `'inherit'` is the single "no specific role" outcome — reached by
29
- * no-rule-match OR an explicit rule targeting `'inherit'` — and both are
30
- * auto-match eligible when enabled. An operator who wants a class to never
31
- * auto-match sets `roleAutoMatch: false` (→ `automatchEnabled: false`).
12
+ * reference resolves to `undefined` — no role, no error.
32
13
  *
33
- * Pure ordering logic: no config object and no LLM service inside this
34
- * module — the LLM is an injectable hook owned by Task 3/4.
14
+ * The rules stage is DELETED with `roles.rules` (v2). `undefined` is the
15
+ * single "no specific role" outcome — the plugin does not intervene and the
16
+ * child inherits the delegating agent's route.
35
17
  *
36
18
  * @module dsh-llm-fallbacks/role-resolution
37
19
  */
38
- import { type FallbacksRoleRule } from './config.ts';
39
- import { type AgentLike } from './roles.ts';
40
- import type { Selector } from './selectors.ts';
41
- export type { FallbacksRoleRule } from './config.ts';
42
- /** Options for {@link resolveRoleAtDispatch}. */
43
- export interface ResolveRoleAtDispatchOptions {
20
+ export interface DeclaredRoleOptions {
44
21
  /**
45
22
  * Dispatch-declared role: the DECLARED RAW id the seam resolved from the
46
23
  * Assignment/teammate prompt (`**Execute as**: <id>`). Validated against the
47
24
  * live `roleIds` map; absent, `inherit`, or no-longer-declared → skipped.
48
25
  */
49
26
  dispatchRole?: string;
50
- /**
51
- * Whether the auto-match stage may run (the `roleAutoMatch` config switch).
52
- * When `false`, stage 4 never runs and the outcome is identical to the
53
- * explicit/dispatch/rules stages alone.
54
- */
55
- automatchEnabled: boolean;
56
- /**
57
- * Auto-match hook — Task 3 supplies the bounded LLM caller; the resolver
58
- * itself contains no LLM. Contract: given the agent being resolved, return
59
- * a declared raw role id or `null`. The resolver defensively validates the
60
- * return against `roleIds` — an unknown id warns and resolves to
61
- * `'inherit'`, never an undeclared role. Skipped when undefined.
62
- */
63
- automatch?: (agent: AgentLike) => Promise<string | null>;
64
- /** Warning sink — the decision path injects the plugin logger. */
65
- warn: (message: string) => void;
66
- /**
67
- * The route the agent's request was actually SERVED by, when that differs
68
- * from the pair recorded on the agent (the virtual `FallbacksChain/Auto`
69
- * row). Rule matching accepts either, so a rule keyed on the real chain head
70
- * keeps matching a child whose recorded route is the virtual pair (QC1 I-1).
71
- * `undefined` for every real route.
72
- */
73
- servedRoute?: {
74
- provider: string;
75
- model: string;
76
- };
77
27
  }
78
28
  /**
79
- * Resolve the dispatch-time role for an agent across four ordered stages
80
- * (explicit → dispatch → rules → auto-match hook). Never throws on its own
81
- * inputs; the hook's own rejection is deliberately NOT caught here (the
82
- * Task-3/4 wiring bounds the LLM call so it never throws out of the request
83
- * path).
29
+ * The dispatch role: explicit preset → dispatch-declared. Returns
30
+ * `undefined` when neither stage resolves (NOT SPECIFIED — no plugin
31
+ * intervention).
84
32
  *
85
33
  * `roleIds` is the canonical trimmed-id map (`trimmed id → declared raw id`,
86
34
  * built in `src/index.ts` apply()): every stage canonicalizes its reference
87
- * by trim and returns the DECLARED RAW id, so roleDef lookups in
88
- * `chains.ts` / `commands.ts` find the stored `roles.list[]` entry exactly
89
- * (qc2 F-001 — no silent role inertness on padded YAML ids).
90
- */
91
- export declare function resolveRoleAtDispatch(agent: AgentLike, rules: FallbacksRoleRule[], roleIds: ReadonlyMap<string, string>, opts: ResolveRoleAtDispatchOptions): Promise<string>;
92
- /**
93
- * The dispatch-time chain head (plan fallbacks-role-automatch Task 4): the
94
- * FIRST candidate of a `resolveChainViews` pass whose `wildcard` flag is
95
- * false — an exact `provider/model` entry. `provider/*` entries are NOT
96
- * dispatch injection targets (no failing model to anchor them; a guessed
97
- * injection risks a hard failure whose code may sit outside `triggerCodes`),
98
- * so a wildcard-only chain yields `undefined` → no injection (today's
99
- * behavior). No cooldown/failed filtering and no existence probe run here —
100
- * a fresh subagent's first request has no failure-scoped state (see the Task
101
- * 4 decision) and exact heads are never probed. Pairs with
102
- * {@link resolveChainViews}'s parallel `wildcard` provenance (`src/chains.ts`);
103
- * the caller resolves the role's concatenated chain first.
35
+ * by trim and returns the DECLARED RAW id.
104
36
  */
105
- export declare function firstExactCandidate(all: readonly Selector[], wildcard: readonly boolean[]): Selector | undefined;
37
+ export declare function resolveDeclaredRole(agentPreset: string | undefined, roleIds: ReadonlyMap<string, string>, opts: DeclaredRoleOptions): string | undefined;
package/dist/schema.d.ts CHANGED
@@ -15,9 +15,10 @@
15
15
  * Object fields are optional by default in schemastery; `.default()` fills
16
16
  * the spec defaults, `.required()` keeps mandatory fields. Unknown keys are
17
17
  * RETAINED by the composition (verified plan Task 1 Step 1) — that is what
18
- * lets `detectLegacyKeys` flag two-block-era leftovers (`chains` /
19
- * `roles.default`) on the composed object at startup (warn + gateway
20
- * `legacyKeys`, see `src/index.ts` apply()).
18
+ * lets `detectLegacyKeys` flag v1 two-block leftovers (`rootChain` /
19
+ * `roles.rules` / …) on the composed object at startup (warn-only
20
+ * migration; the gateway strips them via LEGACY_KEYS, see
21
+ * `src/gateway.ts`).
21
22
  *
22
23
  * Current host contract: the whole schema is marked `volatile` (the host
23
24
  * retired `installSection` — the Loader entry Config IS the settings
@@ -34,7 +35,7 @@
34
35
  * @module dsh-llm-fallbacks/schema
35
36
  */
36
37
  import z from '@deepseek-ai/schemastery';
37
- import type { FallbacksConfig } from './config.ts';
38
+ import type { FallbacksConfigSource } from './config.ts';
38
39
  declare module '@deepseek-ai/schemastery' {
39
40
  namespace Schemastery {
40
41
  interface Meta {
@@ -43,4 +44,4 @@ declare module '@deepseek-ai/schemastery' {
43
44
  }
44
45
  }
45
46
  }
46
- export declare const Config: z<FallbacksConfig>;
47
+ export declare const Config: z<FallbacksConfigSource>;
package/dist/seeds.d.ts CHANGED
@@ -42,7 +42,7 @@
42
42
  *
43
43
  * @module dsh-llm-fallbacks/seeds
44
44
  */
45
- import { type FallbackStrategy, type FallbacksConfig, type FallbacksConfigLogger, type FallbacksRoles } from './config.ts';
45
+ import { type FallbacksConfig, type FallbacksConfigLogger, type FallbacksRoles } from './config.ts';
46
46
  /** A seed declaration from a companion plugin (spec §9.1). */
47
47
  export interface SeedDeclaration {
48
48
  id: string;
@@ -112,10 +112,8 @@ export interface EffectiveRole {
112
112
  id: string;
113
113
  /** Effective row persona. */
114
114
  persona: string;
115
- /** Passthrough — never touched by seeds (R4). */
116
- chain?: string[];
117
- /** Passthrough — never touched by seeds (R4). */
118
- fallback?: FallbackStrategy;
115
+ /** Passthrough — never touched by seeds (R4): the named-chain POINTER. */
116
+ chain?: string;
119
117
  /** Id is in the live declaration set (trimmed row-id match). */
120
118
  seeded: boolean;
121
119
  /** `seeded` && row persona !== current seed default. */
@@ -154,7 +152,7 @@ export interface SeedsWireStatus {
154
152
  export interface SeedsIo {
155
153
  /** Fresh composed config read (the same source the gateway reads). */
156
154
  read(): FallbacksConfig;
157
- /** Persist a full `{ list, rules }` to the settings user layer. */
155
+ /** Persist the `{ list }` to the settings user layer. */
158
156
  writeRoles(roles: FallbacksRoles): Promise<void>;
159
157
  }
160
158
  /**
@@ -192,7 +190,7 @@ export declare class FallbacksSeedManager {
192
190
  * RESOLVED effective default before/after the candidate slice commit
193
191
  * (at-default tracking follows the resolution winner — a producer that
194
192
  * is not the winner for an id it declares never rewrites the row),
195
- * writes only when the computed `{ list, rules }` differs from the
193
+ * writes only when the computed `{ list }` differs from the
196
194
  * current composed roles (idempotent, AC-1), and commits the slice only
197
195
  * after a successful write (compute → write → commit; retry-safe). An
198
196
  * empty batch drops the producer's slice.
package/dist/state.d.ts CHANGED
@@ -1,137 +1,34 @@
1
1
  /**
2
- * Per-agent fallback state machine (spec §5.1; plan Task 3).
2
+ * Per-agent fallback state (direction C, plan dsh-022-rc1-upgrade).
3
3
  *
4
4
  * One `AgentFallbackState` per `agent.id`, held in a `FallbackStateStore`
5
- * created at plugin apply, removed on `agent/disposed`, defensively pruned on
6
- * `agent/status` idle, and cleared wholesale on plugin dispose (spec §6:
7
- * 无残留状态).
5
+ * created at plugin apply, removed on `agent/disposed`, and cleared wholesale
6
+ * on plugin dispose (spec §6: 无残留状态).
8
7
  *
9
- * **pendingSwitch lifecycle** (spec §5.1):
10
- * 1. **Produce** (decision point — request-error trigger-code or
11
- * agent/request always-cap): `writePending` stores the decision and clears
12
- * `appliedTurnStep`, so a *fresh* decision is always applicable at the
13
- * current (turn, step) — required for chains (decision B→C must apply even
14
- * though A→B was already applied at the same (turn, step)).
15
- * 2. **Apply** (`agent/request`): `applyPending` returns the pending switch
16
- * when one exists and has not already been applied at the current
17
- * (turn, step), records `appliedTurnStep`, and clears `pendingSwitch`
18
- * (anti-replay guard).
19
- * 3. **Discard**: chain exhaustion / safety valve → the caller delegates
20
- * (`next()`) and the pending state dies with the agent.
21
- *
22
- * **stepFailures** resets whenever (turn, step) advances (`syncStep`);
23
- * **cooldown** (`CooldownStore`) is a `provider/model → expiry epoch ms` map
24
- * with lazy expiry (`revertPolicy: 'never'` = `Infinity` TTL). The time-boxed
25
- * cooldown deliberately survives idle cleanup so `cooldown-expiry` revert
26
- * (US-4 / T4 integration) works across turns.
8
+ * The store is the CROSS-CALL memory the chain walk reads: **cooldown**
9
+ * (`CooldownStore` — a `provider/model → expiry epoch ms` map with lazy expiry;
10
+ * `revertPolicy: 'never'` = `Infinity` TTL) plus the **recovery** circuit
11
+ * (`RecoveryStore`). The time-boxed cooldown deliberately survives idle
12
+ * cleanup so `cooldown-expiry` revert (US-4 / T4 integration) works across
13
+ * turns. Per-request bookkeeping (the failed-candidate set, the switch cap) is
14
+ * walk-local: a walk lives inside one `stream()` call, so nothing durable is
15
+ * kept for it here.
27
16
  *
28
17
  * @module dsh-llm-fallbacks/state
29
18
  */
30
- import type { FallbackSwitchReason } from './events.ts';
31
- import { CooldownStore, StepFailureSet } from './cooldown.ts';
19
+ import { CooldownStore } from './cooldown.ts';
32
20
  import { RecoveryStore } from './recovery.ts';
33
21
  import type { RecoveryPolicy } from './config.ts';
34
- /**
35
- * What a switch's failure was actually about.
36
- *
37
- * `'route'` (the default) is every failure the plugin has ever walked: the
38
- * provider/model itself is unhealthy (auth, quota, rate limit, ...), so the
39
- * from-route is cooled down and counted as a route failure.
40
- * `'request'` is a failure of the REQUEST on a healthy route — a
41
- * `CONTEXT_WINDOW_EXCEEDED` rejection is the request being too big, not the
42
- * model being down. `commit()` keeps the step-scoped bookkeeping for those
43
- * but writes no suppression and no recovery failure (see `src/index.ts`
44
- * `REQUEST_SCOPED_CODES`).
45
- */
46
- export type SwitchScope = 'route' | 'request';
47
- /** Decision awaiting application at the next `agent/request` boundary (spec §5.1). */
48
- export interface PendingSwitch {
49
- from: {
50
- provider: string;
51
- model: string;
52
- };
53
- to: {
54
- provider: string;
55
- model: string;
56
- };
57
- role: string;
58
- reason: FallbackSwitchReason;
59
- /**
60
- * What the failure was about — absent means `'route'`, the semantics every
61
- * switch had before request-scoped codes existed (same optional-with-a-
62
- * documented-default idiom as `FallbacksRole.fallback`). Only the
63
- * `agent/request-error` listener ever sets `'request'`.
64
- */
65
- scope?: SwitchScope;
66
- }
67
- /**
68
- * One plugin-originated subagent switch the host allowlist blocked (plan
69
- * dsh-012-subagent-routing T3; spec D1). The intersection of the resolved
70
- * candidates with the effective allowlist was empty while the unconstrained walk
71
- * would have switched — the attempt is recorded in-memory only (issue #52:
72
- * no durable session write) for the Subagents card warning (spec D4/T5).
73
- */
74
- export interface BlockedSwitchAttempt {
75
- /** Epoch ms when the switch was blocked. */
76
- at: number;
77
- /** The route the unconstrained walk would have switched to (exact provider+model). */
78
- route: {
79
- provider: string;
80
- model: string;
81
- };
82
- /** The failure reason that triggered the would-be switch. */
83
- reason: FallbackSwitchReason;
84
- }
85
- /**
86
- * How the effective subagent chain head was chosen (plan dsh-012-subagent-routing
87
- * T5 / T2 M3). Lives on a per-agent map (mirrors {@link BlockedSwitchAttempt}),
88
- * not on {@link AgentFallbackState} — recording a head is not a switch intent
89
- * (F-004: the fallback store grows only inside `commit()`).
90
- */
91
- export type ChainHeadSource = 'authorized' | 'injected';
92
- /**
93
- * The effective chain head for one subagent, plus the source label the
94
- * Subagents card renders (spec D4). In-memory only (issue #52).
95
- */
96
- export interface EffectiveChainHead {
97
- /** Epoch ms when this head was recorded (recency for the card snapshot). */
98
- at: number;
99
- /** Exact provider+model of the effective chain head. */
100
- route: {
101
- provider: string;
102
- model: string;
103
- };
104
- /** `authorized` = host spawn preserved; `injected` = plugin role-inject. */
105
- source: ChainHeadSource;
106
- }
107
- /** The current (turn, step)'s failed-model set and switch budget (spec §5.1). */
108
- export interface StepFailures {
109
- turn: number;
110
- step: number;
111
- /** `${provider}/${model}` keys failed in this step (double suppression with cooldown). */
112
- failed: StepFailureSet;
113
- /** Switches committed in this step; the caller judges it against `maxSwitchesPerStep`. */
114
- switchCount: number;
115
- }
116
- /** One agent's whole fallback runtime state (spec §5.1). */
22
+ /** One agent's fallback runtime state. */
117
23
  export interface AgentFallbackState {
118
- /** Decision produced but not yet applied at an `agent/request` boundary. */
119
- pendingSwitch?: PendingSwitch;
120
- /** The (turn, step) the last pending switch was applied to — anti-replay. */
121
- appliedTurnStep?: {
122
- turn: number;
123
- step: number;
124
- };
125
- /** Current (turn, step)'s failure bookkeeping. */
126
- stepFailures: StepFailures;
127
24
  /** `provider/model → expiry epoch ms`; lazily expired on read. */
128
25
  cooldown: CooldownStore;
129
26
  /** Half-open recovery bookkeeping (plan fallbacks-half-open-recovery P2). */
130
27
  recovery: RecoveryStore;
131
28
  }
132
29
  /**
133
- * The `Map<agent.id, AgentFallbackState>` store plus the state-machine
134
- * operations (spec §5.1).
30
+ * The `Map<agent.id, AgentFallbackState>` store plus the state operations the
31
+ * walk and the diagnostics surfaces read.
135
32
  */
136
33
  export declare class FallbackStateStore {
137
34
  private readonly states;
@@ -146,29 +43,6 @@ export declare class FallbackStateStore {
146
43
  delete(agentId: string): void;
147
44
  /** Remove every agent's state (plugin dispose — no residual state). */
148
45
  clear(): void;
149
- /** Reset step-scoped bookkeeping when (turn, step) advances (spec §5.1). */
150
- syncStep(state: AgentFallbackState, turn: number, step: number): void;
151
- /** Record the current model as failed in this step (spec §5.1). */
152
- recordFailure(state: AgentFallbackState, key: string): void;
153
- /** Bump the step's switch budget (spec §5.1). */
154
- recordSwitch(state: AgentFallbackState): void;
155
- /**
156
- * Produce a pending switch. A fresh decision always supersedes a previous
157
- * one and must be applicable at the current (turn, step), so the applied
158
- * marker is cleared here (see module doc — chains).
159
- */
160
- writePending(state: AgentFallbackState, pending: PendingSwitch): void;
161
- /**
162
- * Apply the pending switch at (turn, step) when one exists and has not
163
- * already been applied there; records the applied marker and clears the
164
- * pending switch (spec §5.1 lifecycle step 2).
165
- */
166
- applyPending(state: AgentFallbackState, turn: number, step: number): PendingSwitch | undefined;
167
- /**
168
- * Defensive `agent/status` idle cleanup: drop per-step state. The time-boxed
169
- * cooldown survives so `cooldown-expiry` revert works across turns (US-4).
170
- */
171
- clearStepState(state: AgentFallbackState): void;
172
46
  /** Suppress `key` until `untilEpochMs` (`Infinity` for `revertPolicy: 'never'`). */
173
47
  suppress(state: AgentFallbackState, key: string, untilEpochMs: number): void;
174
48
  /**
@@ -58,8 +58,28 @@
58
58
  * (the runtime mints its own child id and `agent/created` has already run), in
59
59
  * which case this module resolves the same question against the live roles
60
60
  * source and carries the verdict onto the record keyed off the RESOLVED id,
61
- * before the child's first pre-step. This module writes no request field,
62
- * reads no provider persona capability, and injects no agent preset.
61
+ * before the child's first pre-step. This module reads no provider persona
62
+ * capability and injects no agent preset.
63
+ *
64
+ * Role chain stamp (named virtual chains §3, `injectRoleChain`): the SAME
65
+ * resolution point is where a dispatch child is pointed at a named chain —
66
+ * `request.agentOptions` is stamped with `{ provider: 'FallbacksChain',
67
+ * model: <role.chain> }` (rc.1 merges it over the parent's route when
68
+ * `manager.startLocal` creates the child) when all FOUR conditions hold: a
69
+ * declared role resolved, its `chain` is non-empty AND declared, the request
70
+ * carries no explicit `agentOptions` (explicit = authorized, untouched), and
71
+ * the backend is LOCAL. No host-policy consultation: the mapping is the
72
+ * user's STATIC config, never a model-authored route — no allowlist, no
73
+ * `unprovable` branch, no `virtualRouteInjected` flag. The seam does NOT
74
+ * check conformance: a declared-but-non-conforming chain is still stamped and
75
+ * the walk's own tail gate refuses it — one gate, in the walk. Injection is
76
+ * creation-time only BY CONSTRUCTION: rc.1's `startActivation` rejects any
77
+ * child id that already exists (`DUPLICATE_CHILD`): a caller-reserved
78
+ * `childId` the seam already recorded is a RE-ACTIVATION of that child (its
79
+ * durable selection is already fixed), so it stamps nothing — that activation
80
+ * writes the role record only (I-000081). A non-stamped dispatch forwards the
81
+ * caller's spec untouched (external backends keep pure inheritance; not
82
+ * specified ⇒ the child inherits the delegating agent's route).
63
83
  *
64
84
  * In-session notice row (plan Task 3): the SAME install point registers the
65
85
  * `agent/pre-step` emitter (`./role-notice.ts`) over the record map it owns, so
@@ -89,7 +109,7 @@
89
109
  * @module dsh-llm-fallbacks/subagents-seam
90
110
  */
91
111
  import type { Context } from '@deepseek-ai/cordis';
92
- import { type FallbacksRole } from './config.ts';
112
+ import { type FallbacksConfig, type FallbacksRole } from './config.ts';
93
113
  /**
94
114
  * The cordis service-read waterfall event the seam registers on. Referenced
95
115
  * ONCE (registration) so a future cordis rename is a one-line change here.
@@ -169,14 +189,25 @@ export interface SubagentStartRequestView {
169
189
  readonly id?: string;
170
190
  };
171
191
  };
192
+ /**
193
+ * rc.1 `SubagentStartRequest.agentOptions` (design §1.1) — the route the
194
+ * caller (or the model, on the tool path) asked the child to run on. Read
195
+ * only: an explicit pair here is the AUTHORIZED route and is never touched.
196
+ */
197
+ readonly agentOptions?: {
198
+ readonly provider?: string;
199
+ readonly model?: string;
200
+ };
172
201
  }
173
202
  /**
174
203
  * Structural view of the activation spec (rc.1 `SubagentActivationSpec`,
175
- * consumed fields only): `childId` is the caller-reserved identity a resume
176
- * already carries — also the record fallback when the request carries no
177
- * Assignment header — and `label` is the catalog join's other half (the
178
- * cataloged child membership carries this exact string). `signal`, `delivery`
179
- * and every future field travel with the caller's spec object.
204
+ * consumed fields only): `childId` is the caller-RESERVED identity of a NEW
205
+ * local child (rc.1 `startLocal` rejects an id that already exists with
206
+ * `DUPLICATE_CHILD`, so `startActivation` never attaches to a child that
207
+ * outlived an earlier activation) — also the record fallback when the request
208
+ * carries no Assignment header — and `label` is the catalog join's other half
209
+ * (the cataloged child membership carries this exact string). `signal`,
210
+ * `delivery` and every future field travel with the caller's spec object.
180
211
  */
181
212
  export interface SubagentActivationSpecView {
182
213
  readonly provider: string;
@@ -199,6 +230,12 @@ export interface SubagentSeamOptions {
199
230
  * wired) ⇒ no verdict, exactly like a listener that never saw the child.
200
231
  */
201
232
  roles?: () => readonly FallbacksRole[];
233
+ /**
234
+ * Live config reader — the SAME `source()` the runtime and the adapter walk
235
+ * read. Absent ⇒ the seam never stamps a role chain (a partial wiring keeps
236
+ * the native dispatch unchanged).
237
+ */
238
+ source?: () => FallbacksConfig;
202
239
  /** Contained debug sink (at most one line per contained degrade). */
203
240
  debug?: (message: string) => void;
204
241
  }
@@ -220,8 +257,8 @@ export interface SubagentSeam {
220
257
  * deliberately NOT cleared on `agent/disposed` — a continuable child's durable
221
258
  * session survives the activation, so clearing the marker would let a
222
259
  * re-activation resume append a second notice row to the same log. It is
223
- * bounded (`NOTICE_EMITTED_LIMIT`) and cleared only in the plugin dispose
224
- * effect.
260
+ * bounded (`NOTICE_EMITTED_LIMIT`) and cleared only when the LAST applied
261
+ * fiber of its root disposes (a surviving fiber's children still need it).
225
262
  */
226
263
  readonly noticeEmitted: Set<string>;
227
264
  /**
@@ -233,7 +270,7 @@ export interface SubagentSeam {
233
270
  * message.
234
271
  */
235
272
  readonly ownsWrapper: boolean;
236
- /** Stop intercepting service reads (the owning fiber's teardown also does). */
273
+ /** Withdraw this fiber's registrations (its own lifecycle effect does this on teardown). */
237
274
  dispose(): void;
238
275
  }
239
276
  /**
@@ -272,15 +309,15 @@ export declare function resolveDeclaredRoleFromAssignment(prompt: string, roleId
272
309
  * DECLARED RAW id the seam already resolved, so this is an exact id match, not
273
310
  * a second canonicalization pass.
274
311
  *
275
- * Only `id` and `persona` are read: no `chain`, no `rootChain`, no routing
276
- * state — a `chain: []` role's persona resolves exactly like a chained one's.
312
+ * Only `id` and `persona` are read: no `chain` pointer, no routing state — a
313
+ * role that specifies a chain names its persona exactly like one that does not.
277
314
  *
278
315
  * @param roles - the live declared roles (`roles.list`).
279
316
  * @param roleId - the DECLARED RAW role id resolved at the seam.
280
317
  */
281
318
  export declare function personaForRole(roles: readonly FallbacksRole[], roleId: string): string | undefined;
282
319
  /**
283
- * @internal Test seam (mirrors `chainHeads`): the seam installed on
320
+ * @internal Test seam: the seam installed on
284
321
  * `ctx`, if any. Not part of the plugin's public surface — lets tests read and
285
322
  * seed the per-child record map without reaching into the installation
286
323
  * closure. `undefined` when no seam is installed.
@@ -9,12 +9,12 @@
9
9
  * wires these helpers at request time (Task 2). Malformed rows NEVER
10
10
  * throw: they warn ONCE per row instance (through `console.warn` — the
11
11
  * resolver's 3-argument contract has no logger parameter) and are
12
- * skipped; a legacy non-empty non-conforming `rootChain` keeps the
13
- * legacy failure walk verbatim (P6).
12
+ * skipped; a non-empty non-conforming `entries` tail keeps the failure walk
13
+ * verbatim (the chain's delegate stays inert).
14
14
  *
15
15
  * @module dsh-llm-fallbacks/time-slots
16
16
  */
17
- import type { FallbacksConfig } from './config.ts';
17
+ import type { NamedChain } from './config.ts';
18
18
  /** Official all-day selectors — the ONLY legal tails (length 1, XOR):
19
19
  * Flash (the current official default) and Pro, both selectable. The Pro
20
20
  * tail id is the catalog id verbatim: the official default catalog
@@ -87,32 +87,29 @@ export declare const PRESETS: Record<PresetId, PresetDefinition>;
87
87
  */
88
88
  export declare function isAllDayConforming(chain: readonly string[]): boolean;
89
89
  /**
90
- * Effective chain for a root request at `now` (P5): the FIRST extra row
91
- * whose descriptor contains `now` (stored order) — that row's `chain`
92
- * REPLACES the all-day chain (never concatenated); no match ⇒ `rootChain`
93
- * (the all-day row, always last and required). Malformed rows warn once
94
- * and are skipped; never throws.
90
+ * Effective chain for a request on ONE named chain at `now`: the FIRST
91
+ * matching row of {@link NamedChain.timeSlots} (stored order) — that row's
92
+ * `chain` REPLACES {@link NamedChain.entries} (never concatenated); no match
93
+ * ⇒ the chain's own `entries`. Malformed rows warn once and are skipped;
94
+ * never throws.
95
95
  *
96
- * P6 (qc1 F-001): without a conforming all-day the slot rows are inert, so
97
- * this IS the raw `rootChain` — the gate lives in {@link resolveSlotState},
98
- * the single source every slot surface (this, the 分时切换 log, the
99
- * `/fallbacks` strip, the virtual adapter delegate, its served-route anchor)
100
- * reads.
96
+ * Without a conforming entries tail the slot rows are inert, so this IS the
97
+ * raw `entries` — the gate lives in {@link resolveSlotState}, the single
98
+ * source every slot surface reads.
101
99
  */
102
- export declare function resolveEffectiveChain(config: FallbacksConfig, now: Date, tz: string): string[];
100
+ export declare function resolveEffectiveChain(chain: NamedChain, now: Date, tz: string): string[];
103
101
  /**
104
- * Slot winner + display label (P5): drives 分时切换 detection (per-root-agent
105
- * last-winner marker, in-process only) and the card / `/fallbacks` status
106
- * strip. `winner` is the matching row or `'all-day'`; `label` names the
107
- * slot (frozen preset label or `custom HH:mm-HH:mm`).
102
+ * Slot winner + display label for one named chain: drives 分时切换 detection
103
+ * (per-(agent, chain) last-winner marker, in-process only) and the card /
104
+ * `/fallbacks` status strip. `winner` is the matching row or `'all-day'`;
105
+ * `label` names the slot (frozen preset label or `custom HH:mm-HH:mm`).
108
106
  *
109
- * P6 gate (qc1 F-001): without a conforming all-day
110
- * (`isAllDayConforming(config.rootChain)`) the winner is ALWAYS `'all-day'`
111
- * — a legacy multi-model (or empty) chain earns no slot rows, so every
112
- * surface fed by this resolver reports the inert state and routing stays on
113
- * the raw `rootChain` (the legacy walk verbatim).
107
+ * Gate: without a conforming entries tail (`isAllDayConforming(chain.entries)`)
108
+ * the winner is ALWAYS `'all-day'` — a multi-model (or empty) chain earns no
109
+ * slot rows, so every surface fed by this resolver reports the inert state and
110
+ * routing stays on the raw `entries`.
114
111
  */
115
- export declare function resolveSlotState(config: FallbacksConfig, now: Date, tz: string): {
112
+ export declare function resolveSlotState(chain: NamedChain, now: Date, tz: string): {
116
113
  winner: SlotRowConfig | 'all-day';
117
114
  label: string;
118
115
  };
@@ -114,12 +114,11 @@ export declare const FALLBACKS_TUI_SECTION_NS = "fallbacks";
114
114
  */
115
115
  export declare const JSON_FIELD_MAX_DRAFT_BYTES: number;
116
116
  /**
117
- * The `fallbacks` /settings section: 12 fields covering all 14 web-card
118
- * capabilities (the default-model choice rides `rootChain`'s last entry and
119
- * the per-role fallback strategy rides `roles.list` JSON; the config-level
120
- * `enabled` switch is REMOVED — plan fallbacks-web-ux-alignment T2, the
121
- * Plugins-page row toggle is the master switch and the runtime gates read
122
- * content presence). Scalar
117
+ * The `fallbacks` /settings section covering the web-card capabilities
118
+ * (named virtual chains v2: ONE `Chains (JSON)` field over `chains` on the
119
+ * same bounded-parse machinery; `Roles (JSON)` keeps the chain POINTER; the
120
+ * v1 `rootChain` / top-level `timeSlots` / `roles.rules` fields are REMOVED
121
+ * with their mechanisms). Scalar
123
122
  * capabilities use native kinds (boolean/number/select); complex structures
124
123
  * use `text` fields with custom `format`/`parse` that mirror the gateway's
125
124
  * validation. Built fresh per call — the host deep-freezes whatever it