dsh-model-switch 0.4.25 → 0.4.27

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,4 +1,4 @@
1
- import { n as sessionProviderOf, t as resolveCapabilityRoute } from "./session-route-BCQFzqRP.js";
1
+ import { i as sessionProviderOf, n as groupRoute, t as deploymentRoute } from "./session-route-BjNPlliu.js";
2
2
  //#region lib/types/session-title.js
3
3
  /**
4
4
  * Model Switch-owned session-naming row.
@@ -10,16 +10,32 @@ import { n as sessionProviderOf, t as resolveCapabilityRoute } from "./session-r
10
10
  * saved group is named through that group's pair while every other session keeps
11
11
  * following its own model.
12
12
  *
13
- * The call itself stays inside the official implementation: the exported
14
- * `generateSessionTitleWithLlm` receives the session-resolved config and records
15
- * exactly that route in `session/title-llm-request`, so the durable record and
16
- * the actual request can never disagree. Nothing is remounted when settings
17
- * change — the route is read through on every naming request — so a settings
18
- * save can neither interrupt an in-flight naming request nor leave the profile
19
- * without a registered provider.
13
+ * The call itself stays inside the official implementation: the auxiliary-LLM
14
+ * package records exactly the route that was called in `session/title-llm-request`,
15
+ * so the durable record and the actual request can never disagree. Nothing is
16
+ * remounted when settings change — the route is read through on every naming
17
+ * request — so a settings save can neither interrupt an in-flight naming request
18
+ * nor leave the profile without a registered provider.
20
19
  *
21
- * `cordis.patch.yml` disables the official row (the seam allows one registered
22
- * provider per context), and this row mirrors the cadence the disabled row
20
+ * Two official auxiliary-LLM lines are supported, selected at runtime from the
21
+ * module surface, because the 0.2.1 release replaced the shared title call:
22
+ * - Up to DSH 0.2.0 (`generateSessionTitleWithLlm`): the module owns the system
23
+ * prompt, input framing, and output validation; the shared policy carries the
24
+ * title-length targets.
25
+ * - DSH 0.2.1+ (`executeSessionTitleLlm`): the provider owns the system prompt,
26
+ * the input, the reasoning-effort selection, and the output interpretation;
27
+ * the shared policy carries execution controls only. This row mirrors the
28
+ * official provider's wording and interpretation so a configured route keeps
29
+ * the official behavior.
30
+ *
31
+ * A naming request that fails is retried once with a widened output budget
32
+ * (max-token failures only) and then falls through the remaining route
33
+ * candidates — the session group's pair, the deployment-wide pair, the row's own
34
+ * pair, then the session's logged route — so one unavailable model or provider
35
+ * cannot leave the session on its deterministic fallback title forever.
36
+ *
37
+ * `cordis.patch.yml` disables the official rows (the seam allows one registered
38
+ * provider per context), and this row mirrors the cadence a disabled row
23
39
  * declared, so a profile that restated the row as all-prompts keeps its cadence.
24
40
  *
25
41
  * @module dsh-model-switch/session-title
@@ -54,6 +70,43 @@ const TITLE_CADENCES = Object.freeze([Object.freeze({
54
70
  automatic: "all-prompts",
55
71
  firstPromptOnly: false
56
72
  })]);
73
+ /** Execution controls both official auxiliary-LLM lines validate; defaults mirror the shipped official rows. */
74
+ const EXECUTION_DEFAULTS = Object.freeze({
75
+ maxInputBytes: 4096,
76
+ maxOutputTokens: 64,
77
+ timeoutMs: 6e4
78
+ });
79
+ /** Provider-owned title-length targets: required by the pre-0.2.1 line and moved into providers by 0.2.1. */
80
+ const TARGET_DEFAULTS = Object.freeze({
81
+ targetWords: 5,
82
+ targetCjkCharacters: 10
83
+ });
84
+ /** Execution keys accepted by both lines; provider-owned keys are excluded on purpose. */
85
+ const EXECUTION_KEYS = Object.freeze([
86
+ "maxInputBytes",
87
+ "maxOutputTokens",
88
+ "timeoutMs",
89
+ "provider",
90
+ "model"
91
+ ]);
92
+ /**
93
+ * Read one call entry point without assuming it exists.
94
+ *
95
+ * The module namespace of a host line that does not export the name can throw on
96
+ * access (mocked namespaces do), so a missing entry point reads as undefined
97
+ * instead of failing the row.
98
+ * @param module - the loaded auxiliary-LLM package.
99
+ * @param key - the call entry point to read.
100
+ * @returns The callable, or undefined when this line does not expose it.
101
+ */
102
+ function titleCallOf(module, key) {
103
+ try {
104
+ const value = module[key];
105
+ return typeof value === "function" ? value : void 0;
106
+ } catch {
107
+ return;
108
+ }
109
+ }
57
110
  /**
58
111
  * The cadence one official package name declares.
59
112
  * @param pkg - the disabled row's package name.
@@ -128,42 +181,341 @@ function forwardedConfig(entry) {
128
181
  */
129
182
  function officialTitlePackage(entry) {
130
183
  const pkg = entry.options?.name;
131
- return typeof pkg === "string" && pkg.trim() !== "" ? pkg : void 0;
184
+ if (typeof pkg !== "string" || pkg.trim() === "") return void 0;
185
+ return pkg;
186
+ }
187
+ /** A positive integer, or undefined for anything else (blank, fractional, negative, non-number). */
188
+ function positiveInteger(value) {
189
+ if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) return void 0;
190
+ return value;
132
191
  }
133
192
  /**
134
- * Read live Model Switch settings without requiring the host row.
193
+ * Split one untrusted stored policy into the halves both official lines accept.
135
194
  *
136
- * The host is read structurally instead of injected so this row keeps mirroring
137
- * the official naming behavior when the `model-switch` row is disabled: an
138
- * unreachable host resolves to the row's own policy rather than to a pinned
139
- * route.
140
- * @param ctx - this row's context.
141
- * @returns The settings, or undefined when the host is unavailable.
195
+ * Execution controls pass through untouched (an invalid value still fails the
196
+ * official validator). Absent execution fields fall back to the shipped
197
+ * official defaults, because a profile that mounts no official row has no
198
+ * policy to forward and this row must still run. Title-length targets are read
199
+ * here for the system instruction: the pre-0.2.1 line wants them in its shared
200
+ * config, while 0.2.1 moved them into each provider.
201
+ * @param forwarded - the disabled official row's stored config.
202
+ * @returns The execution subset and the title-length targets.
142
203
  */
143
- function currentSettings(ctx) {
144
- try {
145
- return ctx.get("modelSwitch", false)?.currentSettings();
146
- } catch {
147
- return;
204
+ function splitTitlePolicy(forwarded) {
205
+ const execution = {};
206
+ for (const key of EXECUTION_KEYS) {
207
+ const value = forwarded[key];
208
+ if (value !== void 0) execution[key] = value;
148
209
  }
210
+ for (const [key, fallback] of Object.entries(EXECUTION_DEFAULTS)) if (execution[key] === void 0) execution[key] = fallback;
211
+ return {
212
+ execution,
213
+ targets: {
214
+ targetWords: positiveInteger(forwarded.targetWords) ?? TARGET_DEFAULTS.targetWords,
215
+ targetCjkCharacters: positiveInteger(forwarded.targetCjkCharacters) ?? TARGET_DEFAULTS.targetCjkCharacters
216
+ }
217
+ };
218
+ }
219
+ /**
220
+ * Detect the 0.2.1+ auxiliary-LLM line.
221
+ * @param module - the loaded auxiliary-LLM package.
222
+ * @returns Whether the module exposes the provider-prepared call.
223
+ */
224
+ function isModernTitleModule(module) {
225
+ return titleCallOf(module, "executeSessionTitleLlm") !== void 0;
226
+ }
227
+ /**
228
+ * Validate the stored policy through the official validator of the installed line.
229
+ * @param module - the loaded auxiliary-LLM package.
230
+ * @param forwarded - the disabled official row's stored config.
231
+ * @returns The validated execution config and title-length targets.
232
+ * @throws when the official validator rejects the stored policy.
233
+ */
234
+ function resolveNamingPolicy(module, forwarded) {
235
+ const split = splitTitlePolicy(forwarded);
236
+ const resolve = module.resolveSessionTitleLlmConfig;
237
+ if (isModernTitleModule(module)) return {
238
+ config: resolve(split.execution),
239
+ targets: split.targets
240
+ };
241
+ const config = resolve({
242
+ ...split.execution,
243
+ ...split.targets
244
+ });
245
+ return {
246
+ config,
247
+ targets: {
248
+ targetWords: config.targetWords ?? split.targets.targetWords,
249
+ targetCjkCharacters: config.targetCjkCharacters ?? split.targets.targetCjkCharacters
250
+ }
251
+ };
252
+ }
253
+ /**
254
+ * Candidate routes for one naming request, most specific first and deduplicated.
255
+ *
256
+ * The stored session group's pair wins, then the deployment-wide pair, then the
257
+ * row's own configured pair, then the session's logged route. The list is also
258
+ * the fallback order: a failing candidate falls through to the next one.
259
+ * @param settings - live Model Switch settings; undefined skips the stored pairs.
260
+ * @param provider - the delegating session's provider, when one is observable.
261
+ * @param policy - the validated row policy (its own pair is the third candidate).
262
+ * @param requestRoute - the session's logged main-request route, when one exists.
263
+ * @returns The ordered, deduplicated candidates; empty when only the row policy applies.
264
+ */
265
+ function titleRouteCandidates(settings, provider, policy, requestRoute) {
266
+ const candidates = [];
267
+ const push = (route) => {
268
+ if (route === void 0) return;
269
+ if (candidates.some((candidate) => candidate.provider === route.provider && candidate.model === route.model)) return;
270
+ candidates.push(route);
271
+ };
272
+ push(groupRoute(settings, provider, "title"));
273
+ push(deploymentRoute(settings, "title"));
274
+ if (policy.provider !== void 0 && policy.model !== void 0) push({
275
+ provider: policy.provider,
276
+ model: policy.model
277
+ });
278
+ push(requestRoute);
279
+ return candidates;
280
+ }
281
+ /**
282
+ * Whether a failure may be retried on another candidate.
283
+ * A superseded or disposed request keeps no budget, and a deadline that fired is
284
+ * the caller's bound, not the route's fault: both end the attempt loop.
285
+ * @param error - the failure thrown by the auxiliary call.
286
+ * @param request - the service-owned request whose signal bounds the work.
287
+ * @returns Whether another candidate may be tried.
288
+ */
289
+ function shouldTryNextCandidate(error, request) {
290
+ if (request.signal.aborted) return false;
291
+ return error?.code !== "SESSION_TITLE_TIMEOUT";
292
+ }
293
+ /**
294
+ * Whether the failure is the model filling its output budget before finishing.
295
+ * Official lines report this as `title output reached maxOutputTokens`; a widened
296
+ * budget is the only retry that can succeed for it.
297
+ * @param error - the failure thrown by the auxiliary call.
298
+ * @returns Whether one widened-budget retry is worth trying.
299
+ */
300
+ function isMaxTokensFailure(error) {
301
+ if (!(error instanceof Error)) return false;
302
+ return /maxoutputtokens|max[-_ ]tokens/iu.test(error.message);
303
+ }
304
+ /**
305
+ * A widened output budget for the same route.
306
+ * @param maxOutputTokens - the policy's configured budget.
307
+ * @returns Four times the budget, capped at 4096 tokens.
308
+ */
309
+ function widerTitleBudget(maxOutputTokens) {
310
+ return Math.min(maxOutputTokens * 4, 4096);
311
+ }
312
+ /** Asterisk emphasis wrapping a whole line, such as `**Title**`. */
313
+ const EMPHASIS_WRAPPER = /^(?<marker>\*{1,3})(?<inner>\S(?:.*\S)?)\k<marker>$/u;
314
+ /** OSC control sequences, such as a title hyperlink. */
315
+ const OSC_SEQUENCE = /(?:\u001B\]|\u009D)(?:(?!\u0007|\u001B\\)[\s\S])*(?:\u0007|\u001B\\|$)/gu;
316
+ /** Control-sequence-introducer escapes such as SGR color codes. */
317
+ const CSI_SEQUENCE = /(?:\u001B\[|\u009B)[0-?]*[ -/]*[@-~]/gu;
318
+ /** Remaining two-byte ESC control sequences. */
319
+ const ESC_SEQUENCE = /\u001B[@-_]/gu;
320
+ /** Non-whitespace C0/C1 control characters. */
321
+ const CONTROL_CHARACTER = /[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u009F]/gu;
322
+ /** Directional and invisible controls that can make a displayed title deceptive. */
323
+ const DIRECTIONAL_CONTROL = /[\u200B\u200E\u200F\u202A-\u202E\u2060-\u2064\u2066-\u206F\uFEFF]/gu;
324
+ /**
325
+ * Normalize one accepted title exactly like the official service.
326
+ * @param input - untrusted title text.
327
+ * @returns A terminal-safe, whitespace-normalized one-line title, possibly empty.
328
+ */
329
+ function normalizeTitleText(input) {
330
+ return input.replace(OSC_SEQUENCE, "").replace(CSI_SEQUENCE, "").replace(ESC_SEQUENCE, "").replace(CONTROL_CHARACTER, "").replace(DIRECTIONAL_CONTROL, "").replace(/\s+/gu, " ").trim();
331
+ }
332
+ /**
333
+ * Take the title from model output: the first non-empty line, without asterisk
334
+ * emphasis wrapping that whole line. Models that disregard the one-line
335
+ * instruction put the title first and commentary after it.
336
+ * @param text - joined text blocks of the auxiliary response.
337
+ * @returns The candidate title line.
338
+ */
339
+ function titleFromOutput(text) {
340
+ const line = text.split(/\r?\n/u).map((item) => item.trim()).find((item) => item.length > 0) ?? "";
341
+ const groups = EMPHASIS_WRAPPER.exec(line)?.groups;
342
+ const marker = groups?.["marker"];
343
+ const inner = groups?.["inner"];
344
+ return marker === void 0 || inner === void 0 || inner.includes(marker) ? line : inner;
345
+ }
346
+ /**
347
+ * Interpret one assembled 0.2.1+ response as a title.
348
+ * Rejects terminal tool-call and max-token outcomes exactly like the official
349
+ * provider, so the row keeps the official acceptance rules.
350
+ * @param response - the assembled auxiliary response.
351
+ * @returns A non-empty normalized title.
352
+ * @throws when the finish reason, a tool-call block, or empty text makes the response unusable.
353
+ */
354
+ function titleFromResponse(response) {
355
+ switch (response.finish.kind) {
356
+ case "stop": break;
357
+ case "tool-calls": throw new Error("session-title-llm: title output must contain text only");
358
+ case "max-tokens": throw new Error("session-title-llm: title output reached maxOutputTokens");
359
+ default: throw new Error(`session-title-llm: unsupported finish reason "${String(response.finish.kind)}"`);
360
+ }
361
+ if (response.blocks.some((block) => block.type === "tool-call")) throw new Error("session-title-llm: title output must contain text only");
362
+ const title = normalizeTitleText(titleFromOutput(response.blocks.filter((block) => block.type === "text").map((block) => block.text ?? "").join(" ")));
363
+ if (title.length === 0) throw new Error("session-title-llm: title model produced no text");
364
+ return title;
365
+ }
366
+ /**
367
+ * Select the route's least supported reasoning effort for a title call, as the
368
+ * official 0.2.1 provider does: naming is a short auxiliary task, and a full
369
+ * reasoning budget can consume the whole output allowance.
370
+ * @param model - the prepared model metadata.
371
+ * @returns The first declared effort id, or undefined when the model has none.
372
+ */
373
+ function selectTitleReasoningEffort(model) {
374
+ return model.reasoning?.efforts?.[0]?.id;
375
+ }
376
+ /**
377
+ * The official 0.2.1+ system instruction, including the optional current-title anchor.
378
+ * @param targets - title-length targets.
379
+ * @param hasCurrentTitle - whether the request is anchored to an existing provider title.
380
+ * @returns The system prompt text.
381
+ */
382
+ function titleSystemPrompt(targets, hasCurrentTitle) {
383
+ return [
384
+ "Create a concise title for an AI coding-assistant session from the supplied human messages.",
385
+ "Return only the title on one line, **in plain text of natural language**, with no quotes, prefix, explanation, Markdown, XML, or terminal control codes. No code is allowed.",
386
+ "Use the language of the messages.",
387
+ `Aim for about ${targets.targetWords} words in non-CJK languages or ${targets.targetCjkCharacters} CJK characters.`,
388
+ "If the messages give little to name, still return a short best-effort title, such as Greeting, instead of explaining.",
389
+ ...hasCurrentTitle ? [
390
+ "An existing title is supplied as currentTitle. If it still accurately describes the main topic or task, return it exactly unchanged.",
391
+ "Follow-up questions, additional details within the same topic, acknowledgements such as \"thanks\", and requests to continue do not by themselves justify a title change.",
392
+ "Do not reword, polish, shorten, or replace synonyms in an adequate title. Keeping its exact wording takes priority over the target length.",
393
+ "Change the title only when the messages materially change or expand the main topic or task so that the existing title is no longer accurate."
394
+ ] : []
395
+ ].join("\n");
396
+ }
397
+ /**
398
+ * Frame the exact messages as JSON so user text cannot break structural delimiters.
399
+ * @param messages - the selected human messages.
400
+ * @param currentTitle - the accepted provider title to anchor on, when there is one.
401
+ * @returns The framed user input.
402
+ */
403
+ function frameTitleInput(messages, currentTitle) {
404
+ if (currentTitle === void 0) return `Generate the session title from this JSON array of human messages:\n${JSON.stringify(messages)}`;
405
+ return `Update the session title from this JSON object:\n${JSON.stringify({
406
+ currentTitle,
407
+ messages
408
+ })}`;
409
+ }
410
+ /**
411
+ * The accepted provider title of one request, when the service supplied one.
412
+ * Only a provider-sourced title anchors an update; a fallback title does not.
413
+ * @param request - the service-owned naming request.
414
+ * @returns The current title, or undefined when there is nothing to anchor on.
415
+ */
416
+ function currentProviderTitle(request) {
417
+ const current = request.currentTitle;
418
+ if (current === void 0 || current.source?.kind !== "provider" || typeof current.title !== "string") return void 0;
419
+ return current.title;
420
+ }
421
+ /**
422
+ * Run one candidate through the auxiliary-LLM line the profile installed.
423
+ * @param ctx - this row's context, exposing `llm`.
424
+ * @param module - the loaded auxiliary-LLM package.
425
+ * @param plan - the profile's naming policy and cadence.
426
+ * @param request - service-owned session, message revision, and cancellation.
427
+ * @param attempt - one candidate route with its composed config.
428
+ * @returns The official provider result.
429
+ */
430
+ async function runTitleCall(ctx, module, plan, request, attempt) {
431
+ const messages = selectTitleMessages(request.messages, plan.cadence.firstPromptOnly);
432
+ const execute = titleCallOf(module, "executeSessionTitleLlm");
433
+ if (execute !== void 0) {
434
+ const anchor = plan.cadence.firstPromptOnly ? void 0 : currentProviderTitle(request);
435
+ const prepared = {
436
+ system: titleSystemPrompt(plan.targets, anchor !== void 0),
437
+ input: frameTitleInput(messages, anchor),
438
+ messageSeqs: messages.map((message) => message.seq),
439
+ selectReasoningEffort: selectTitleReasoningEffort
440
+ };
441
+ const response = await execute(ctx, attempt.config, request, plan.cadence.id, prepared);
442
+ return {
443
+ title: titleFromResponse(response),
444
+ messageSeqs: prepared.messageSeqs,
445
+ model: response.model
446
+ };
447
+ }
448
+ const legacy = titleCallOf(module, "generateSessionTitleWithLlm");
449
+ if (legacy === void 0) throw new Error(`model-switch: ${TITLE_LLM_PACKAGE} exposes no title call this row understands`);
450
+ return legacy(ctx, {
451
+ ...plan.targets,
452
+ ...attempt.config
453
+ }, request, messages, plan.cadence.id);
149
454
  }
150
455
  /**
151
456
  * Execute one naming request through the official implementation.
152
457
  *
153
458
  * The route is resolved from the session being titled — its provider selects the
154
- * saved group, and the flat settings pair is the deployment default — and is
155
- * handed to `generateSessionTitleWithLlm`, which frames the request, records the
156
- * exact route in `session/title-llm-request`, and validates the result.
459
+ * saved group, the flat settings pair is the deployment default — and every
460
+ * candidate is tried in order until one succeeds. A max-token failure first gets
461
+ * one widened-budget retry on the same route; every other failure moves on.
157
462
  * @param ctx - this row's context, exposing `llm`.
158
463
  * @param module - the loaded auxiliary-LLM module.
159
464
  * @param plan - the profile's fixed naming policy.
160
465
  * @param request - service-owned session, message revision, and cancellation.
161
466
  * @returns The official provider result.
467
+ * @throws The last candidate failure when every candidate fails.
162
468
  */
163
469
  async function generateTitle(ctx, module, plan, request) {
164
- const provider = sessionProviderOf(ctx, request.session);
165
- const route = resolveCapabilityRoute(currentSettings(ctx), provider, "title");
166
- return module.generateSessionTitleWithLlm(ctx, composeTitleRoute(plan.config, route), request, selectTitleMessages(request.messages, plan.cadence.firstPromptOnly), plan.cadence.id);
470
+ const candidates = titleRouteCandidates(currentSettings(ctx), sessionProviderOf(ctx, request.session), plan.config, request.route);
471
+ const attempts = candidates.length === 0 ? [{
472
+ route: void 0,
473
+ config: plan.config
474
+ }] : candidates.map((route) => ({
475
+ route,
476
+ config: composeTitleRoute(plan.config, route)
477
+ }));
478
+ let lastError;
479
+ for (const attempt of attempts) try {
480
+ return await runTitleCall(ctx, module, plan, request, attempt);
481
+ } catch (error) {
482
+ lastError = error;
483
+ if (!shouldTryNextCandidate(error, request)) break;
484
+ if (!isMaxTokensFailure(error)) continue;
485
+ const widened = {
486
+ route: attempt.route,
487
+ config: {
488
+ ...attempt.config,
489
+ maxOutputTokens: widerTitleBudget(attempt.config.maxOutputTokens)
490
+ }
491
+ };
492
+ try {
493
+ return await runTitleCall(ctx, module, plan, request, widened);
494
+ } catch (retryError) {
495
+ lastError = retryError;
496
+ if (!shouldTryNextCandidate(retryError, request)) break;
497
+ }
498
+ }
499
+ const tried = attempts.map((attempt) => attempt.route === void 0 ? "the row policy" : `${attempt.route.provider}/${attempt.route.model}`).join(" → ");
500
+ if (!request.signal.aborted) ctx.logger.warn(`model-switch: session title generation failed on every candidate (${tried}): ${String(lastError)}`);
501
+ throw lastError;
502
+ }
503
+ /**
504
+ * Read live Model Switch settings without requiring the host row.
505
+ *
506
+ * The host is read structurally instead of injected so this row keeps mirroring
507
+ * the official naming behavior when the `model-switch` row is disabled: an
508
+ * unreachable host resolves to the row's own policy rather than to a pinned
509
+ * route.
510
+ * @param ctx - this row's context.
511
+ * @returns The settings, or undefined when the host is unavailable.
512
+ */
513
+ function currentSettings(ctx) {
514
+ try {
515
+ return ctx.get("modelSwitch", false)?.currentSettings();
516
+ } catch {
517
+ return;
518
+ }
167
519
  }
168
520
  /**
169
521
  * Mount the Model Switch session-naming row for this profile.
@@ -180,7 +532,7 @@ async function apply(ctx) {
180
532
  const cadence = titleCadence(entry === void 0 ? void 0 : officialTitlePackage(entry)) ?? TITLE_CADENCES[0];
181
533
  if (cadence === void 0) return;
182
534
  if (entry === void 0) ctx.logger.info("model-switch: no instantiated official session-title row to mirror; naming uses the first-prompt cadence");
183
- /** The displaced row's naming policy, or the official defaults when it is absent. */
535
+ /** The displaced row's naming policy, or the shipped defaults when it is absent. */
184
536
  const forwarded = entry === void 0 ? {} : forwardedConfig(entry);
185
537
  let module;
186
538
  try {
@@ -190,9 +542,9 @@ async function apply(ctx) {
190
542
  ctx.logger.error(cause);
191
543
  return;
192
544
  }
193
- let config;
545
+ let policy;
194
546
  try {
195
- config = module.resolveSessionTitleLlmConfig(forwarded);
547
+ policy = resolveNamingPolicy(module, forwarded);
196
548
  } catch (cause) {
197
549
  ctx.logger.error("model-switch: the official session-title row has no valid naming policy; naming keeps the deterministic fallback");
198
550
  ctx.logger.error(cause);
@@ -200,7 +552,8 @@ async function apply(ctx) {
200
552
  }
201
553
  const plan = {
202
554
  cadence,
203
- config
555
+ config: policy.config,
556
+ targets: policy.targets
204
557
  };
205
558
  try {
206
559
  ctx.sessionTitle.register({
@@ -214,4 +567,4 @@ async function apply(ctx) {
214
567
  }
215
568
  }
216
569
  //#endregion
217
- export { TITLE_CADENCES, apply, composeTitleRoute, forwardedConfig, inject, name, officialTitleEntry, officialTitlePackage, selectTitleMessages, titleCadence };
570
+ export { TITLE_CADENCES, apply, composeTitleRoute, forwardedConfig, frameTitleInput, inject, isMaxTokensFailure, isModernTitleModule, name, normalizeTitleText, officialTitleEntry, officialTitlePackage, resolveNamingPolicy, selectTitleMessages, selectTitleReasoningEffort, shouldTryNextCandidate, splitTitlePolicy, titleCadence, titleFromOutput, titleFromResponse, titleRouteCandidates, titleSystemPrompt, widerTitleBudget };
@@ -1,4 +1,4 @@
1
- import { r as sessionSubagentRouteOf } from "./session-route-BCQFzqRP.js";
1
+ import { a as sessionSubagentRouteOf } from "./session-route-BjNPlliu.js";
2
2
  import { ReasoningEffortId } from "@deepseek-ai/dsh-llm";
3
3
  import OfficialSubagentRuntime from "@deepseek-ai/dsh-subagent";
4
4
  //#region lib/types/subagent-runtime.js
@@ -144,9 +144,32 @@ async function mountWithStartupFallback(candidate, fallback) {
144
144
  startupFailure(error, await ledger.disposeAll());
145
145
  }
146
146
  }
147
+ /**
148
+ * Operator-facing notice for a Host whose Subagent runtime cannot be routed.
149
+ *
150
+ * DSH 0.2.1 replaced `start()`/`startContinuable()` with the activation API, so
151
+ * this adapter's override point is gone; the row then mounts the untouched
152
+ * official runtime instead of guessing a new seam.
153
+ * @param error - the typed startup incompatibility that rejected the candidate.
154
+ * @returns The notice logged once at startup.
155
+ */
156
+ function routedRuntimeUnavailableNotice(error) {
157
+ return `model-switch: the routed Subagent runtime is unavailable on this DSH Host (${error.message}); delegation follows the official route instead`;
158
+ }
159
+ /** Log one notice without letting a missing logger service fail the startup. */
160
+ function reportNotice(ctx, message) {
161
+ try {
162
+ ctx.logger.warn(message);
163
+ } catch {}
164
+ }
147
165
  async function mountProfileRuntime(ctx) {
148
166
  return (await mountWithStartupFallback(async (track) => {
149
- assertOfficialRuntimeSurface();
167
+ try {
168
+ assertOfficialRuntimeSurface();
169
+ } catch (error) {
170
+ if (error instanceof StartupIncompatibilityError) reportNotice(ctx, routedRuntimeUnavailableNotice(error));
171
+ throw error;
172
+ }
150
173
  assertRoutingSurface(ctx);
151
174
  const candidate = ctx.plugin(ModelSwitchSubagentRuntime);
152
175
  track(() => candidate.dispose());
@@ -186,4 +209,4 @@ var ModelSwitchSubagentRuntime = class extends OfficialSubagentRuntime {
186
209
  }
187
210
  };
188
211
  //#endregion
189
- export { ModelSwitchSubagentRuntime, StartupIncompatibilityError, profileSubagentRuntime as default, profileSubagentRuntime, mountWithStartupFallback, routeSubagentRequest };
212
+ export { ModelSwitchSubagentRuntime, StartupIncompatibilityError, profileSubagentRuntime as default, profileSubagentRuntime, mountWithStartupFallback, routeSubagentRequest, routedRuntimeUnavailableNotice };
@@ -0,0 +1,64 @@
1
+ import type { Context } from '@deepseek-ai/cordis';
2
+ import type { Entry, Loader } from '@deepseek-ai/cordis-plugin-loader';
3
+ /**
4
+ * Agent presets that ship their own isolated `compaction` group.
5
+ *
6
+ * The Web surface mounts preset-nested rows below a `cordis:group`, and the
7
+ * Loader's patch index cannot address rows inside a preset's `config.plugins`
8
+ * list, so this plugin bridges those rows at runtime instead of shipping copies
9
+ * of the shipped preset definitions: the official (or deployment-edited) preset
10
+ * stays authoritative, and only the nested compaction engine is swapped.
11
+ */
12
+ export declare const PRESET_ROW_IDS: readonly ["preset-standard", "preset-ptc", "preset-cordis"];
13
+ interface PresetRow {
14
+ readonly id?: unknown;
15
+ readonly name?: unknown;
16
+ readonly group?: unknown;
17
+ readonly disabled?: unknown;
18
+ readonly config?: unknown;
19
+ }
20
+ /**
21
+ * Swap the nested official compaction engine for this plugin's routed engine.
22
+ *
23
+ * Recurses through every nested entry list, so a preset that wraps its
24
+ * compaction group differently is still bridged. A list that carries no
25
+ * official engine is returned untouched. The transform is idempotent: a list
26
+ * whose official row is already disabled and already followed by the routed row
27
+ * reports `changed: false`.
28
+ * @param rows - one plugin list (a preset's `plugins` or a nested group's `config`).
29
+ * @returns The new list and whether anything changed.
30
+ */
31
+ export declare function bridgePresetPlugins(rows: readonly PresetRow[]): {
32
+ rows: PresetRow[];
33
+ changed: boolean;
34
+ };
35
+ /**
36
+ * Bridge one preset definition's plugin list.
37
+ * @param config - the preset row's stored config (`{ id, order, plugins }`).
38
+ * @returns The bridged config and whether anything changed.
39
+ */
40
+ export declare function bridgePresetConfig(config: unknown): {
41
+ config: unknown;
42
+ changed: boolean;
43
+ };
44
+ /**
45
+ * Locate one preset row in the loader tree.
46
+ * @param loader - the host loader.
47
+ * @param id - the preset id (`standard`, `ptc`, `cordis`).
48
+ * @returns The declaring entry, or undefined when this profile has no such preset.
49
+ */
50
+ export declare function presetEntry(loader: Loader | undefined, id: string): Entry | undefined;
51
+ /**
52
+ * Bridge every shipped preset in the running composition.
53
+ *
54
+ * Runs at Host startup, before any session can use a preset, so the wrapped
55
+ * engine is in place for the first session. An entry that has not started yet
56
+ * has its raw config replaced (so the first activation already carries the
57
+ * bridge); a started entry is reloaded through the public loader update. A
58
+ * preset without a compaction group is skipped, and a bridge failure is logged
59
+ * without failing the row: the official presets keep working.
60
+ * @param ctx - the Host plugin context.
61
+ * @returns The preset ids that were bridged.
62
+ */
63
+ export declare function bridgeAgentPresets(ctx: Context): Promise<readonly string[]>;
64
+ export {};