dsh-llm-fallbacks 0.2.1 → 0.2.2

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/events.d.ts CHANGED
@@ -1,15 +1,29 @@
1
1
  /**
2
2
  * `fallbacks/switch` session event vocabulary (spec §5 table; plan Task 3).
3
3
  *
4
- * The event is appended (never rewrites history — AC-7) at every switch,
5
- * including the always-mode cap path: "无事件即无切换" (Global Constraint).
4
+ * Writing is stopped (issue #52): the plugin no longer appends this event —
5
+ * the apply()-time event-type registration was proven ineffective (module
6
+ * instance mismatch), and a session containing the event refused to load
7
+ * after a dsh restart. The event now exists only in history: old logs are
8
+ * repaired by `scripts/repair-fallbacks-switch-logs.ts`, which marks legacy
9
+ * events ignorable so affected sessions load again.
6
10
  * The module is type-only — the augmentation is erased at runtime; the
7
11
  * plugin's runtime behavior lives in `src/index.ts`.
8
12
  *
9
13
  * @module dsh-llm-fallbacks/events
10
14
  */
11
- /** Why a switch was decided (spec §5.1 `PendingSwitch.reason`). */
12
- export type FallbackSwitchReason = 'trigger-code' | 'always-cap';
15
+ /**
16
+ * Why a switch was decided (spec §5.1 `PendingSwitch.reason`).
17
+ *
18
+ * `role-inject` is the dispatch-time reason (plan fallbacks-role-automatch
19
+ * Task 4): a subagent-origin agent's first request was re-routed to its
20
+ * resolved role's chain head. It is ADDITIVE to the failure-time reasons —
21
+ * the event shape stays a superset of the pre-feature payload, and existing
22
+ * renderers show unknown reasons raw (AC-5). Unlike `trigger-code` /
23
+ * `always-cap`, a `role-inject` event is NOT a failure decision: it carries
24
+ * no pending switch / cooldown / failure bookkeeping (no `commit()`).
25
+ */
26
+ export type FallbackSwitchReason = 'trigger-code' | 'always-cap' | 'role-inject';
13
27
  /** Durable payload of one provider/model switch (spec §5 table). */
14
28
  export interface FallbacksSwitchEventData {
15
29
  turn: number;
@@ -30,7 +44,7 @@ export interface FallbacksSwitchEventData {
30
44
  }
31
45
  declare module '@deepseek-ai/dsh-session/types' {
32
46
  interface SessionEventMap {
33
- /** Durable, append-only record of one provider/model switch decided by dsh-llm-fallbacks. */
47
+ /** Historical, append-only record of one provider/model switch decided by dsh-llm-fallbacks (no longer written — issue #52 stop-write). */
34
48
  'fallbacks/switch': FallbacksSwitchEventData;
35
49
  }
36
50
  }
package/dist/index.d.ts CHANGED
@@ -13,7 +13,7 @@
13
13
  * (**always mode included**) → `next()`; otherwise resolve role + chain,
14
14
  * and when a candidate survives the filter (current / cooldown /
15
15
  * step-failed / `provider/*`-missing-id) write the pending switch +
16
- * cooldown + failure bookkeeping + append `fallbacks/switch`, then return
16
+ * cooldown + failure bookkeeping, then return
17
17
  * `{ kind: 'retry' }` (own recovery, no `next()`).
18
18
  * - `agent/request` waterfall: apply a pending switch after `await next()`
19
19
  * (provider/model override, inherited `reasoningEffort` dropped — the
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { createRequire } from "node:module";
2
2
  import { installSettingsSection, settingsNamespace } from "@deepseek-ai/dsh-settings";
3
- import * as dshSession from "@deepseek-ai/dsh-session";
4
3
  import z from "@deepseek-ai/schemastery";
4
+ import { createUserMessage } from "@deepseek-ai/dsh-llm";
5
5
  import { TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
6
6
  //#region .build/host/selectors.js
7
7
  var SelectorError = class extends Error {
@@ -53,7 +53,8 @@ const defaultFallbacksConfig = {
53
53
  revertPolicy: "cooldown-expiry",
54
54
  maxSwitchesPerStep: 8,
55
55
  alwaysModeRetryCap: 5,
56
- presets: "bundled"
56
+ presets: "bundled",
57
+ roleAutoMatch: true
57
58
  };
58
59
  const INHERIT_ROLE_ID = "inherit";
59
60
  const ROLE_ID_PATTERN = /^[a-z0-9-]{1,32}$/;
@@ -139,8 +140,101 @@ const Config = z.object({
139
140
  revertPolicy: z.union([z.const("cooldown-expiry"), z.const("never")]).default("cooldown-expiry"),
140
141
  maxSwitchesPerStep: z.number().default(8),
141
142
  alwaysModeRetryCap: z.number().default(5),
142
- presets: z.union([z.const("bundled"), z.const("none")]).default("bundled")
143
+ presets: z.union([z.const("bundled"), z.const("none")]).default("bundled"),
144
+ roleAutoMatch: z.boolean().default(true)
143
145
  });
146
+ function buildAutomatchPrompt(roles, agent) {
147
+ const lines = [
148
+ "You are assigning a subagent to its best-fit role from a declared taxonomy.",
149
+ "",
150
+ "Declared roles:",
151
+ ...roles.map((role) => `- ${role.id.trim()}: ${role.persona}`),
152
+ "",
153
+ `Agent context — origin: ${agent.session?.header?.origin ?? "root"}`
154
+ ];
155
+ const preset = agent.session?.header?.agentPreset?.trim();
156
+ if (preset !== void 0 && preset !== "") lines.push(`agentPreset: ${preset}`);
157
+ lines.push("", "Reply with EXACTLY ONE of the declared role ids above, or the literal \"none\" when no role fits.", "Do not add any explanation, punctuation, or other text.");
158
+ return lines.join("\n");
159
+ }
160
+ function parseAutomatchAnswer(answer, roleIds) {
161
+ const cleaned = answer.replace(/^[^a-zA-Z0-9-]+/, "").replace(/[^a-zA-Z0-9-]+$/, "");
162
+ if (cleaned === "") return null;
163
+ const key = cleaned.toLowerCase();
164
+ if (key === "none") return null;
165
+ return roleIds.get(key) ?? null;
166
+ }
167
+ async function pickRoleByLlm(ctx, roles, agent, opts) {
168
+ try {
169
+ const list = Array.isArray(roles.list) ? roles.list : [];
170
+ if (list.length === 0) return null;
171
+ const llm = ctx.get("llm");
172
+ if (llm === void 0) return null;
173
+ const route = pickJudgmentRoute(list, agent);
174
+ if (route === null) return null;
175
+ const roleIds = new Map(list.map((role) => [role.id.trim(), role.id]));
176
+ const answer = await streamRoleAnswer(llm, route, buildAutomatchPrompt(list, agent), opts);
177
+ if (answer === null) return null;
178
+ return parseAutomatchAnswer(answer, roleIds);
179
+ } catch (error) {
180
+ opts.warn(`llm-fallbacks: auto-match failed — falling back to "inherit": ${describeError(error)}`);
181
+ return null;
182
+ }
183
+ }
184
+ function pickJudgmentRoute(roles, agent) {
185
+ for (const role of roles) for (const entry of role.chain ?? []) try {
186
+ const selector = parseSelector(entry);
187
+ if (selector.model !== void 0) return {
188
+ provider: selector.provider,
189
+ model: selector.model
190
+ };
191
+ } catch {}
192
+ const provider = agent.options?.provider;
193
+ const model = agent.options?.model;
194
+ if (provider !== void 0 && model !== void 0) return {
195
+ provider,
196
+ model
197
+ };
198
+ return null;
199
+ }
200
+ async function streamRoleAnswer(llm, route, prompt, opts) {
201
+ const controller = new AbortController();
202
+ const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? 5e3);
203
+ let text = "";
204
+ let finishKind;
205
+ try {
206
+ for await (const chunk of llm.stream({
207
+ provider: route.provider,
208
+ model: route.model,
209
+ system: prompt,
210
+ messages: [createUserMessage({
211
+ content: [{
212
+ type: "text",
213
+ text: "Pick the best-fit role for this agent."
214
+ }],
215
+ source: { kind: "user" }
216
+ })],
217
+ maxTokens: 32,
218
+ signal: controller.signal
219
+ })) if (chunk.type === "text-delta") text += chunk.text;
220
+ else if (chunk.type === "finish") {
221
+ finishKind = chunk.reason.kind;
222
+ break;
223
+ }
224
+ if (controller.signal.aborted) return null;
225
+ if (finishKind === "error" || finishKind === "aborted") return null;
226
+ return text;
227
+ } catch (error) {
228
+ if (controller.signal.aborted) opts.warn("llm-fallbacks: auto-match timed out — falling back to \"inherit\"");
229
+ else opts.warn(`llm-fallbacks: auto-match call failed — falling back to "inherit": ${describeError(error)}`);
230
+ return null;
231
+ } finally {
232
+ clearTimeout(timer);
233
+ }
234
+ }
235
+ function describeError(error) {
236
+ return error instanceof Error ? error.message : String(error);
237
+ }
144
238
  //#endregion
145
239
  //#region .build/host/chains.js
146
240
  function resolveCandidate(entry, failing, modelExists) {
@@ -264,6 +358,29 @@ function resolveRole(agent, rules, roleIds, warn = console.warn) {
264
358
  return INHERIT_ROLE_ID;
265
359
  }
266
360
  //#endregion
361
+ //#region .build/host/role-resolution.js
362
+ async function resolveRoleAtDispatch(agent, rules, roleIds, opts) {
363
+ const preset = agent.session?.header?.agentPreset?.trim();
364
+ if (preset !== void 0 && preset !== "" && preset !== "inherit") {
365
+ const declared = roleIds.get(preset);
366
+ if (declared !== void 0) return declared;
367
+ }
368
+ const role = resolveRole(agent, rules, roleIds, opts.warn);
369
+ if (role !== "inherit") return role;
370
+ if (opts.automatchEnabled && opts.automatch !== void 0) {
371
+ const picked = await opts.automatch(agent);
372
+ if (picked !== null && picked !== void 0) {
373
+ const declared = roleIds.get(picked.trim());
374
+ if (declared !== void 0) return declared;
375
+ opts.warn(`llm-fallbacks: auto-match returned undeclared role "${picked}" — falling back to "inherit"`);
376
+ }
377
+ }
378
+ return INHERIT_ROLE_ID;
379
+ }
380
+ function firstExactCandidate(all, wildcard) {
381
+ for (let index = 0; index < all.length; index += 1) if (!wildcard[index]) return all[index];
382
+ }
383
+ //#endregion
267
384
  //#region .build/host/cooldown.js
268
385
  var CooldownStore = class {
269
386
  entries = new Map();
@@ -399,7 +516,8 @@ const CONFIG_KEYS = {
399
516
  revertPolicy: true,
400
517
  maxSwitchesPerStep: true,
401
518
  alwaysModeRetryCap: true,
402
- presets: true
519
+ presets: true,
520
+ roleAutoMatch: true
403
521
  };
404
522
  const ROLES_KEYS = {
405
523
  list: true,
@@ -599,6 +717,7 @@ const FALLBACKS_COMMAND_LOCALES = {
599
717
  configMaxSwitches: "单步最大切换",
600
718
  configAlwaysCap: "always 上限",
601
719
  configPresets: "预置",
720
+ configRoleAutoMatch: "角色自动匹配",
602
721
  configEdit: "编辑:~/.dsh/profiles/<profile>/cordis.patch.yml(插件行)或 $DSH_HOME/settings.yaml(fallbacks: 分节)",
603
722
  configEditHint: "TUI 无法修改配置——只能编辑文件"
604
723
  },
@@ -636,6 +755,7 @@ const FALLBACKS_COMMAND_LOCALES = {
636
755
  configMaxSwitches: "Max switches/step",
637
756
  configAlwaysCap: "Always-mode cap",
638
757
  configPresets: "Presets",
758
+ configRoleAutoMatch: "Auto-match",
639
759
  configEdit: "Edit: ~/.dsh/profiles/<profile>/cordis.patch.yml (plugin row) or $DSH_HOME/settings.yaml (fallbacks: section)",
640
760
  configEditHint: "TUI cannot change config — edit files only"
641
761
  }
@@ -709,6 +829,7 @@ function fallbacksConfigText(summary, locale = "zh") {
709
829
  `${t.configMaxSwitches}: ${summary.maxSwitchesPerStep}`,
710
830
  `${t.configAlwaysCap}: ${summary.alwaysModeRetryCap}`,
711
831
  `${t.configPresets}: ${summary.presets}`,
832
+ `${t.configRoleAutoMatch}: ${summary.roleAutoMatch ? t.configEnabled : t.configDisabled}`,
712
833
  "",
713
834
  t.configEdit,
714
835
  t.configEditHint
@@ -1052,15 +1173,6 @@ function overrideConfig(seed, to) {
1052
1173
  }
1053
1174
  function apply(ctx, config = defaultFallbacksConfig) {
1054
1175
  const logger = ctx.logger("llm-fallbacks");
1055
- const known = dshSession.KNOWN_SESSION_EVENT_TYPES;
1056
- let sessionEventRegistered = false;
1057
- if (known instanceof Set) try {
1058
- known.add("fallbacks/switch");
1059
- sessionEventRegistered = known.has("fallbacks/switch");
1060
- } catch {
1061
- sessionEventRegistered = false;
1062
- }
1063
- let sessionEventWarned = false;
1064
1176
  const entry = Config(config);
1065
1177
  const seeds = new FallbacksSeedManager(logger);
1066
1178
  const seedsSettingsUnavailable = "llm-fallbacks: seeds: settings service is unavailable — seed roles cannot be written";
@@ -1132,6 +1244,7 @@ function apply(ctx, config = defaultFallbacksConfig) {
1132
1244
  });
1133
1245
  const states = new FallbackStateStore();
1134
1246
  stateStores.set(ctx, states);
1247
+ const dispatchInjected = new Set();
1135
1248
  async function decide(agent, turn, step, current, reason, state) {
1136
1249
  const config = source();
1137
1250
  if (state !== void 0) {
@@ -1169,7 +1282,7 @@ function apply(ctx, config = defaultFallbacksConfig) {
1169
1282
  reason
1170
1283
  };
1171
1284
  }
1172
- function commit(agent, state, pending, turn, step) {
1285
+ function commit(state, pending, turn, step) {
1173
1286
  const config = source();
1174
1287
  const fromKey = selectorKey(pending.from.provider, pending.from.model);
1175
1288
  const until = config.revertPolicy === "never" ? Number.POSITIVE_INFINITY : Date.now() + config.cooldownMs;
@@ -1178,21 +1291,6 @@ function apply(ctx, config = defaultFallbacksConfig) {
1178
1291
  states.suppress(state, fromKey, until);
1179
1292
  states.recordFailure(state, fromKey);
1180
1293
  states.recordSwitch(state);
1181
- if (!sessionEventRegistered) {
1182
- if (!sessionEventWarned) {
1183
- sessionEventWarned = true;
1184
- logger.warn("llm-fallbacks: session event type \"fallbacks/switch\" is not registered with this harness — skipping the durable event (the switch itself is applied)");
1185
- }
1186
- return;
1187
- }
1188
- agent.session.append("fallbacks/switch", {
1189
- turn,
1190
- step,
1191
- from: pending.from,
1192
- to: pending.to,
1193
- role: pending.role,
1194
- reason: pending.reason
1195
- });
1196
1294
  }
1197
1295
  ctx.on("agent/request-error", async ({ agent, turn, step, provider, failure }, next) => {
1198
1296
  const config = source();
@@ -1202,7 +1300,7 @@ function apply(ctx, config = defaultFallbacksConfig) {
1202
1300
  try {
1203
1301
  const pending = await decide(agent, turn, step, current, "trigger-code", states.peek(agent.id));
1204
1302
  if (pending === null) return next();
1205
- commit(agent, states.get(agent.id), pending, turn, step);
1303
+ commit(states.get(agent.id), pending, turn, step);
1206
1304
  return { kind: "retry" };
1207
1305
  } catch (error) {
1208
1306
  logger.warn("llm-fallbacks: decision path failed, passing the original failure through: %s", error?.message ?? String(error));
@@ -1215,6 +1313,30 @@ function apply(ctx, config = defaultFallbacksConfig) {
1215
1313
  const applied = state === void 0 ? void 0 : states.applyPending(state, turn, step);
1216
1314
  if (applied !== void 0) return overrideConfig(seed, applied.to);
1217
1315
  const config = source();
1316
+ if (config.enabled && hasChains && agent.session?.header?.origin === "subagent" && !dispatchInjected.has(agent.id)) {
1317
+ dispatchInjected.add(agent.id);
1318
+ try {
1319
+ const role = await resolveRoleAtDispatch(agent, config.roles.rules, roleIds, {
1320
+ automatchEnabled: config.roleAutoMatch ?? true,
1321
+ automatch: (agent) => pickRoleByLlm(ctx, config.roles, agent, { warn: logger.warn }),
1322
+ warn: logger.warn
1323
+ });
1324
+ if (role !== "inherit") {
1325
+ const { all, wildcard } = resolveChainViews(config.roles.list, config.rootChain, role, seed.provider, seed.model, logger.warn);
1326
+ const head = firstExactCandidate(all, wildcard);
1327
+ if (head !== void 0 && head.model !== void 0 && !(head.provider === seed.provider && head.model === seed.model)) {
1328
+ const to = {
1329
+ provider: head.provider,
1330
+ model: head.model
1331
+ };
1332
+ logger.info("llm-fallbacks: agent \"%s\" role-inject role=%s model=%s/%s", agent.id, role, head.provider, head.model);
1333
+ return overrideConfig(seed, to);
1334
+ }
1335
+ }
1336
+ } catch (error) {
1337
+ logger.warn("llm-fallbacks: dispatch role-injection failed, proceeding with the request unchanged: %s", error?.message ?? String(error));
1338
+ }
1339
+ }
1218
1340
  if (hasChains && config.enabled && config.alwaysModeRetryCap > 0 && countRetryEvents(agent.session, turn, step, seed.provider) >= config.alwaysModeRetryCap) {
1219
1341
  const decisionState = states.peek(agent.id);
1220
1342
  const pending = await decide(agent, turn, step, {
@@ -1223,7 +1345,7 @@ function apply(ctx, config = defaultFallbacksConfig) {
1223
1345
  }, "always-cap", decisionState);
1224
1346
  if (pending !== null) {
1225
1347
  const commitState = states.get(agent.id);
1226
- commit(agent, commitState, pending, turn, step);
1348
+ commit(commitState, pending, turn, step);
1227
1349
  const appliedCap = states.applyPending(commitState, turn, step);
1228
1350
  if (appliedCap !== void 0) return overrideConfig(seed, appliedCap.to);
1229
1351
  }
@@ -1237,9 +1359,11 @@ function apply(ctx, config = defaultFallbacksConfig) {
1237
1359
  });
1238
1360
  ctx.on("agent/disposed", ({ agent }) => {
1239
1361
  states.delete(agent.id);
1362
+ dispatchInjected.delete(agent.id);
1240
1363
  });
1241
1364
  ctx.effect(() => () => {
1242
1365
  states.clear();
1366
+ dispatchInjected.clear();
1243
1367
  }, "llm-fallbacks: clear per-agent state");
1244
1368
  const fallbacksCommandController = {
1245
1369
  getSnapshot(agent) {
@@ -1268,7 +1392,8 @@ function apply(ctx, config = defaultFallbacksConfig) {
1268
1392
  revertPolicy: config.revertPolicy,
1269
1393
  maxSwitchesPerStep: config.maxSwitchesPerStep,
1270
1394
  alwaysModeRetryCap: config.alwaysModeRetryCap,
1271
- presets: config.presets ?? "bundled"
1395
+ presets: config.presets ?? "bundled",
1396
+ roleAutoMatch: config.roleAutoMatch ?? true
1272
1397
  };
1273
1398
  }
1274
1399
  };
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Dispatch-time three-stage role resolution (plan fallbacks-role-automatch
3
+ * Task 2).
4
+ *
5
+ * Precedence (first hit wins):
6
+ * 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 rules;
11
+ * 2. rules — the existing `resolveRole` passthrough, unchanged (a declared
12
+ * role wins; no-rule-match / an explicit rule targeting `'inherit'`
13
+ * resolve to `'inherit'`);
14
+ * 3. auto-match — only when stage 2 resolved to `'inherit'` AND
15
+ * `opts.automatchEnabled`: call `opts.automatch` with the agent. The hook
16
+ * (Task 3 supplies the bounded LLM caller) MUST return a declared raw id
17
+ * or `null`; the resolver defensively validates the return against
18
+ * `roleIds` — an unknown id warns and resolves to `'inherit'`, never an
19
+ * undeclared role. When `automatch` is undefined or `automatchEnabled`
20
+ * is false the hook is skipped.
21
+ *
22
+ * `'inherit'` is the single "no specific role" outcome — reached by
23
+ * no-rule-match OR an explicit rule targeting `'inherit'` — and both are
24
+ * auto-match eligible when enabled. An operator who wants a class to never
25
+ * auto-match sets `roleAutoMatch: false` (→ `automatchEnabled: false`).
26
+ *
27
+ * Pure ordering logic: no config object and no LLM service inside this
28
+ * module — the LLM is an injectable hook owned by Task 3/4.
29
+ *
30
+ * @module dsh-llm-fallbacks/role-resolution
31
+ */
32
+ import { type FallbacksRoleRule } from './config.ts';
33
+ import { type AgentLike } from './roles.ts';
34
+ import type { Selector } from './selectors.ts';
35
+ export type { FallbacksRoleRule } from './config.ts';
36
+ /** Options for {@link resolveRoleAtDispatch}. */
37
+ export interface ResolveRoleAtDispatchOptions {
38
+ /**
39
+ * Whether the auto-match stage may run (the `roleAutoMatch` config switch).
40
+ * When `false`, stage 3 never runs and the outcome is identical to the
41
+ * rules stage alone.
42
+ */
43
+ automatchEnabled: boolean;
44
+ /**
45
+ * Auto-match hook — Task 3 supplies the bounded LLM caller; the resolver
46
+ * itself contains no LLM. Contract: given the agent being resolved, return
47
+ * a declared raw role id or `null`. The resolver defensively validates the
48
+ * return against `roleIds` — an unknown id warns and resolves to
49
+ * `'inherit'`, never an undeclared role. Skipped when undefined.
50
+ */
51
+ automatch?: (agent: AgentLike) => Promise<string | null>;
52
+ /** Warning sink — the decision path injects the plugin logger. */
53
+ warn: (message: string) => void;
54
+ }
55
+ /**
56
+ * Resolve the dispatch-time role for an agent across three ordered stages
57
+ * (explicit → rules → auto-match hook). Never throws on its own inputs; the
58
+ * hook's own rejection is deliberately NOT caught here (the Task-3/4 wiring
59
+ * bounds the LLM call so it never throws out of the request path).
60
+ *
61
+ * `roleIds` is the canonical trimmed-id map (`trimmed id → declared raw id`,
62
+ * built in `src/index.ts` apply()): every stage canonicalizes its reference
63
+ * by trim and returns the DECLARED RAW id, so roleDef lookups in
64
+ * `chains.ts` / `commands.ts` find the stored `roles.list[]` entry exactly
65
+ * (qc2 F-001 — no silent role inertness on padded YAML ids).
66
+ */
67
+ export declare function resolveRoleAtDispatch(agent: AgentLike, rules: FallbacksRoleRule[], roleIds: ReadonlyMap<string, string>, opts: ResolveRoleAtDispatchOptions): Promise<string>;
68
+ /**
69
+ * The dispatch-time chain head (plan fallbacks-role-automatch Task 4): the
70
+ * FIRST candidate of a `resolveChainViews` pass whose `wildcard` flag is
71
+ * false — an exact `provider/model` entry. `provider/*` entries are NOT
72
+ * dispatch injection targets (no failing model to anchor them; a guessed
73
+ * injection risks a hard failure whose code may sit outside `triggerCodes`),
74
+ * so a wildcard-only chain yields `undefined` → no injection (today's
75
+ * behavior). No cooldown/failed filtering and no existence probe run here —
76
+ * a fresh subagent's first request has no failure-scoped state (see the Task
77
+ * 4 decision) and exact heads are never probed. Pairs with
78
+ * {@link resolveChainViews}'s parallel `wildcard` provenance (`src/chains.ts`);
79
+ * the caller resolves the role's concatenated chain first.
80
+ */
81
+ export declare function firstExactCandidate(all: readonly Selector[], wildcard: readonly boolean[]): Selector | undefined;
package/dist/roles.d.ts CHANGED
@@ -2,6 +2,12 @@
2
2
  * Role resolution for fallback chains (spec §7.1; plan fallbacks-role-runtime
3
3
  * Task 1).
4
4
  *
5
+ * This module is the RULES stage. Dispatch-time role resolution
6
+ * (plan fallbacks-role-automatch) is the three-stage resolver in
7
+ * `role-resolution.ts` — explicit (`session.header.agentPreset`) → this
8
+ * rules stage → an optional auto-match hook; `AgentLike` carries the additive
9
+ * `session.header.agentPreset?` carrier for that explicit stage.
10
+ *
5
11
  * Precedence (first hit wins):
6
12
  * 1. the first `rules` entry whose specified origin/provider/model patterns
7
13
  * all match the agent;
@@ -47,6 +53,8 @@ export interface AgentLike {
47
53
  session?: {
48
54
  header?: {
49
55
  origin?: Origin;
56
+ /** Dispatch-time explicit role carrier (plan fallbacks-role-automatch Task 2). */
57
+ agentPreset?: string;
50
58
  };
51
59
  };
52
60
  }
package/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "dsh",
6
6
  "dsh-plugin"
7
7
  ],
8
- "version": "0.2.1",
8
+ "version": "0.2.2",
9
9
  "type": "module",
10
10
  "main": "./dist/index.js",
11
11
  "types": "./dist/index.d.ts",
@@ -48,6 +48,7 @@
48
48
  "build-client": "tsx scripts/build-client.ts",
49
49
  "release:prepare": "tsx scripts/prepare-release.ts",
50
50
  "release:validate": "tsx scripts/validate-release-version.ts",
51
+ "repair:fallbacks-switch-logs": "tsx scripts/repair-fallbacks-switch-logs.ts",
51
52
  "test": "vitest run",
52
53
  "typecheck": "tsc -p tsconfig.build.json --noEmit && tsc --noEmit",
53
54
  "prepare": "pnpm run build"
@@ -57,24 +58,24 @@
57
58
  },
58
59
  "peerDependencies": {
59
60
  "@deepseek-ai/cordis": "^4.0.1",
60
- "@deepseek-ai/dsh-agent": "^0.1.0-rc.6",
61
- "@deepseek-ai/dsh-api-gateway": "^0.1.0-rc.6",
62
- "@deepseek-ai/dsh-api-remotes": "^0.1.0-rc.6",
63
- "@deepseek-ai/dsh-client-connection": "^0.1.0-rc.6",
64
- "@deepseek-ai/dsh-client-locale": "^0.1.0-rc.6",
65
- "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.6",
66
- "@deepseek-ai/dsh-client-ui-settings-plugins": "^0.1.0-rc.6",
67
- "@deepseek-ai/dsh-client-ui-settings": "^0.1.0-rc.6",
68
- "@deepseek-ai/dsh-client-ui-conversation": "^0.1.0-rc.6",
69
- "@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.6",
70
- "@deepseek-ai/dsh-client-web-react": "^0.1.0-rc.6",
71
- "@deepseek-ai/dsh-client-ui-primitives": "^0.1.0-rc.6",
72
- "@deepseek-ai/dsh-commands": "^0.1.0-rc.6",
73
- "@deepseek-ai/dsh-llm": "^0.1.0-rc.6",
74
- "@deepseek-ai/dsh-session": "^0.1.0-rc.6",
75
- "@deepseek-ai/dsh-settings": "^0.1.0-rc.6",
76
- "@deepseek-ai/dsh-typert-protocol": "^0.1.0-rc.6",
77
- "@deepseek-ai/dsh-typert-registry": "^0.1.0-rc.6",
61
+ "@deepseek-ai/dsh-agent": "^0.1.0-rc.7",
62
+ "@deepseek-ai/dsh-api-gateway": "^0.1.0-rc.7",
63
+ "@deepseek-ai/dsh-api-remotes": "^0.1.0-rc.7",
64
+ "@deepseek-ai/dsh-client-connection": "^0.1.0-rc.7",
65
+ "@deepseek-ai/dsh-client-locale": "^0.1.0-rc.7",
66
+ "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.7",
67
+ "@deepseek-ai/dsh-client-ui-settings-plugins": "^0.1.0-rc.7",
68
+ "@deepseek-ai/dsh-client-ui-settings": "^0.1.0-rc.7",
69
+ "@deepseek-ai/dsh-client-ui-conversation": "^0.1.0-rc.7",
70
+ "@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.7",
71
+ "@deepseek-ai/dsh-client-web-react": "^0.1.0-rc.7",
72
+ "@deepseek-ai/dsh-client-ui-primitives": "^0.1.0-rc.7",
73
+ "@deepseek-ai/dsh-commands": "^0.1.0-rc.7",
74
+ "@deepseek-ai/dsh-llm": "^0.1.0-rc.7",
75
+ "@deepseek-ai/dsh-session": "^0.1.0-rc.7",
76
+ "@deepseek-ai/dsh-settings": "^0.1.0-rc.7",
77
+ "@deepseek-ai/dsh-typert-protocol": "^0.1.0-rc.7",
78
+ "@deepseek-ai/dsh-typert-registry": "^0.1.0-rc.7",
78
79
  "react": "^18.2.0",
79
80
  "@deepseek-ai/schemastery": "^3.18.1"
80
81
  },