dsh-model-switch 0.4.23 → 0.4.24

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,217 @@
1
+ import { n as sessionProviderOf, t as resolveCapabilityRoute } from "./session-route-h9SAqajc.js";
2
+ //#region lib/types/session-title.js
3
+ /**
4
+ * Model Switch-owned session-naming row.
5
+ *
6
+ * The official naming rows hardwire the naming route at registration time: the
7
+ * configured provider/model pair wins over the session's own logged route for
8
+ * every session in the profile. This row registers ONE provider of its own and
9
+ * resolves the route PER REQUEST instead, so a session whose provider has a
10
+ * saved group is named through that group's pair while every other session keeps
11
+ * following its own model.
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.
20
+ *
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
23
+ * declared, so a profile that restated the row as all-prompts keeps its cadence.
24
+ *
25
+ * @module dsh-model-switch/session-title
26
+ */
27
+ var __rewriteRelativeImportExtension = function(path, preserveJsx) {
28
+ if (typeof path === "string" && /^\.\.?\//.test(path)) return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function(m, tsx, d, ext, cm) {
29
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : d + ext + "." + cm.toLowerCase() + "js";
30
+ });
31
+ return path;
32
+ };
33
+ /** Official session-title provider row this module supersedes. */
34
+ const OFFICIAL_TITLE_ROW = "session-title-llm";
35
+ /** The auxiliary-LLM package carrying the shared naming policy and call. */
36
+ const TITLE_LLM_PACKAGE = "@deepseek-ai/dsh-session-title-llm";
37
+ /** Cordis plugin identity; matches the cordis.patch.yml insert row id. */
38
+ const name = "model-switch-session-title";
39
+ /** The loader names the disabled row, and the seam services the call needs are awaited before the first naming request. */
40
+ const inject = [
41
+ "loader",
42
+ "sessionTitle",
43
+ "llm"
44
+ ];
45
+ /** Both official cadence packages, in the order a profile may declare them. */
46
+ const TITLE_CADENCES = Object.freeze([Object.freeze({
47
+ pkg: "@deepseek-ai/dsh-session-title-first-prompt-llm",
48
+ id: "session-title-first-prompt-llm",
49
+ automatic: "first-prompt",
50
+ firstPromptOnly: true
51
+ }), Object.freeze({
52
+ pkg: "@deepseek-ai/dsh-session-title-all-prompts-llm",
53
+ id: "session-title-all-prompts-llm",
54
+ automatic: "all-prompts",
55
+ firstPromptOnly: false
56
+ })]);
57
+ /**
58
+ * The cadence one official package name declares.
59
+ * @param pkg - the disabled row's package name.
60
+ * @returns The matching cadence, or undefined for a row this row cannot mirror.
61
+ */
62
+ function titleCadence(pkg) {
63
+ if (pkg === void 0) return void 0;
64
+ return TITLE_CADENCES.find((cadence) => cadence.pkg === pkg);
65
+ }
66
+ /**
67
+ * Overlay the session-resolved route over the row's own validated policy.
68
+ *
69
+ * A resolved route always names both halves. When nothing resolves, the row's
70
+ * own policy is returned unchanged: an explicit pair the profile configured on
71
+ * the official row stays the deployment default, and a row without one keeps
72
+ * following each session's logged route.
73
+ * @param config - policy validated from the disabled official row.
74
+ * @param route - session-resolved naming route, or undefined for the row's own policy.
75
+ * @returns A detached config for one naming request.
76
+ */
77
+ function composeTitleRoute(config, route) {
78
+ if (route === void 0) return config;
79
+ return {
80
+ ...config,
81
+ provider: route.provider,
82
+ model: route.model
83
+ };
84
+ }
85
+ /**
86
+ * Frame the exact human messages one naming revision represents.
87
+ * @param messages - every eligible human message of the revision.
88
+ * @param firstPromptOnly - cadence flag: true mirrors the first-prompt provider.
89
+ * @returns The selected messages, in order.
90
+ * @throws when a first-prompt cadence receives an empty revision — the official provider's own contract.
91
+ */
92
+ function selectTitleMessages(messages, firstPromptOnly) {
93
+ if (!firstPromptOnly) return messages;
94
+ const first = messages[0];
95
+ if (first === void 0) throw new Error("first-prompt title provider requires one human message");
96
+ return [first];
97
+ }
98
+ /**
99
+ * Locate the official session-title provider row in the loader tree.
100
+ * Disabled entries stay in the tree, so a name-matched disable still matches.
101
+ * @param loader - the host loader, when one is reachable.
102
+ * @returns The official entry, or undefined when this profile has no such row.
103
+ */
104
+ function officialTitleEntry(loader) {
105
+ if (!loader) return void 0;
106
+ for (const entry of loader.entries()) {
107
+ if (entry.id === OFFICIAL_TITLE_ROW) return entry;
108
+ const pkg = entry.options?.name;
109
+ if (typeof pkg === "string" && TITLE_CADENCES.some((cadence) => cadence.pkg === pkg)) return entry;
110
+ }
111
+ }
112
+ /**
113
+ * Copy the official row's stored config, ignoring non-object payloads.
114
+ * @param entry - the official session-title provider entry.
115
+ * @returns A plain object spread; missing or invalid configs become `{}`. The
116
+ * result is untrusted: the profile may be hand-edited, so the official policy
117
+ * validator runs over it before any naming request is served.
118
+ */
119
+ function forwardedConfig(entry) {
120
+ const raw = entry.options?.config;
121
+ if (raw && typeof raw === "object" && !Array.isArray(raw)) return raw;
122
+ return {};
123
+ }
124
+ /**
125
+ * The official package name this row mounts (either cadence).
126
+ * @param entry - the official session-title provider entry.
127
+ * @returns The package name, or undefined when the row declares none.
128
+ */
129
+ function officialTitlePackage(entry) {
130
+ const pkg = entry.options?.name;
131
+ return typeof pkg === "string" && pkg.trim() !== "" ? pkg : void 0;
132
+ }
133
+ /**
134
+ * Read live Model Switch settings without requiring the host row.
135
+ *
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.
142
+ */
143
+ function currentSettings(ctx) {
144
+ try {
145
+ return ctx.get("modelSwitch", false)?.currentSettings();
146
+ } catch {
147
+ return;
148
+ }
149
+ }
150
+ /**
151
+ * Execute one naming request through the official implementation.
152
+ *
153
+ * 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.
157
+ * @param ctx - this row's context, exposing `llm`.
158
+ * @param module - the loaded auxiliary-LLM module.
159
+ * @param plan - the profile's fixed naming policy.
160
+ * @param request - service-owned session, message revision, and cancellation.
161
+ * @returns The official provider result.
162
+ */
163
+ 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);
167
+ }
168
+ /**
169
+ * Mount the Model Switch session-naming row for this profile.
170
+ *
171
+ * A profile with no official naming row, an unrecognized row package, a missing
172
+ * auxiliary-LLM package, or an invalid stored policy keeps the session-title
173
+ * service's deterministic fallback titles and reports the lost capability
174
+ * explicitly instead of guessing a route. The registration is an effect of this
175
+ * row's fiber, so unloading the profile unregisters the provider.
176
+ * @param ctx - this row's context.
177
+ */
178
+ async function apply(ctx) {
179
+ const entry = officialTitleEntry(ctx.get("loader", false));
180
+ const cadence = titleCadence(entry === void 0 ? void 0 : officialTitlePackage(entry)) ?? TITLE_CADENCES[0];
181
+ if (cadence === void 0) return;
182
+ 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. */
184
+ const forwarded = entry === void 0 ? {} : forwardedConfig(entry);
185
+ let module;
186
+ try {
187
+ module = await import(__rewriteRelativeImportExtension(TITLE_LLM_PACKAGE));
188
+ } catch (cause) {
189
+ ctx.logger.error(`model-switch: ${TITLE_LLM_PACKAGE} is unavailable; model-backed session naming cannot run at all`);
190
+ ctx.logger.error(cause);
191
+ return;
192
+ }
193
+ let config;
194
+ try {
195
+ config = module.resolveSessionTitleLlmConfig(forwarded);
196
+ } catch (cause) {
197
+ ctx.logger.error("model-switch: the official session-title row has no valid naming policy; naming keeps the deterministic fallback");
198
+ ctx.logger.error(cause);
199
+ return;
200
+ }
201
+ const plan = {
202
+ cadence,
203
+ config
204
+ };
205
+ try {
206
+ ctx.sessionTitle.register({
207
+ id: cadence.id,
208
+ automatic: cadence.automatic,
209
+ generate: (request) => generateTitle(ctx, module, plan, request)
210
+ });
211
+ } catch (cause) {
212
+ ctx.logger.error("model-switch: another session-title provider is already registered; the configurable naming route is unavailable");
213
+ ctx.logger.error(cause);
214
+ }
215
+ }
216
+ //#endregion
217
+ export { TITLE_CADENCES, apply, composeTitleRoute, forwardedConfig, inject, name, officialTitleEntry, officialTitlePackage, selectTitleMessages, titleCadence };
@@ -1,3 +1,4 @@
1
+ import { r as sessionSubagentRouteOf } from "./session-route-h9SAqajc.js";
1
2
  import { ReasoningEffortId } from "@deepseek-ai/dsh-llm";
2
3
  import OfficialSubagentRuntime from "@deepseek-ai/dsh-subagent";
3
4
  //#region lib/types/subagent-runtime.js
@@ -20,27 +21,28 @@ function present(value) {
20
21
  function spawnHasAnyRouteField(options) {
21
22
  return present(options?.provider) || present(options?.model) || options?.reasoningEffort !== void 0;
22
23
  }
23
- function fixedRoute(settings) {
24
- if (!present(settings.subagentProvider) || !present(settings.subagentModel)) return void 0;
25
- return {
26
- provider: settings.subagentProvider,
27
- model: settings.subagentModel,
28
- ...present(settings.subagentReasoningEffort) ? { reasoningEffort: ReasoningEffortId(settings.subagentReasoningEffort) } : {}
29
- };
30
- }
31
- /** Inject a fixed Default Subagent route, or leave the request for Official inherit. */
32
- function routeSubagentRequest(request, settings) {
24
+ /**
25
+ * Inject one resolved route into a spawn that names none.
26
+ *
27
+ * A spawn that already names a provider, model, or effort keeps it: the caller's
28
+ * explicit selection outranks any profile policy. The route itself is derived
29
+ * from the DELEGATING session (its provider selects a saved group, else the flat
30
+ * Subagent card in `fixed` mode), so two sessions on different providers route
31
+ * their own spawns and neither can rewrite the other's policy.
32
+ * @param request - the caller's spawn request.
33
+ * @param route - the delegating session's resolved route, or undefined for official inherit.
34
+ * @returns The request to hand the official runtime.
35
+ */
36
+ function routeSubagentRequest(request, route) {
33
37
  if (spawnHasAnyRouteField(request.agentOptions)) return request;
34
- if (settings.subagentMode !== "fixed") return request;
35
- const selected = fixedRoute(settings);
36
- if (selected === void 0) return request;
38
+ if (route === void 0) return request;
37
39
  return {
38
40
  ...request,
39
41
  agentOptions: {
40
42
  ...request.agentOptions,
41
- provider: selected.provider,
42
- model: selected.model,
43
- ...selected.reasoningEffort === void 0 ? {} : { reasoningEffort: selected.reasoningEffort }
43
+ provider: route.provider,
44
+ model: route.model,
45
+ ...route.reasoningEffort === void 0 ? {} : { reasoningEffort: ReasoningEffortId(route.reasoningEffort) }
44
46
  }
45
47
  };
46
48
  }
@@ -170,7 +172,8 @@ var ModelSwitchSubagentRuntime = class extends OfficialSubagentRuntime {
170
172
  routed(name, request) {
171
173
  const provider = this.ctx.subagents.getProvider(name);
172
174
  if (provider !== void 0 && provider.capabilities.agentOptions !== true) return request;
173
- return routeSubagentRequest(request, routingSurface(this.ctx).currentSettings());
175
+ const modelSwitch = routingSurface(this.ctx);
176
+ return routeSubagentRequest(request, sessionSubagentRouteOf(this.ctx, request.parent.session, modelSwitch.currentSettings()));
174
177
  }
175
178
  start(name, request) {
176
179
  return super.start(name, this.routed(name, request));
@@ -9,6 +9,14 @@
9
9
  * route configured the wrapper passes options through by identity, so the
10
10
  * reviewer follows the session model exactly.
11
11
  *
12
+ * The reviewer route is deliberately DEPLOYMENT-WIDE. The official gate builds
13
+ * its request with no session id and no purpose and closes over the single
14
+ * context it was applied with, so this plugin has no public way to tell which
15
+ * session a review serves; guessing one would attribute a review to the wrong
16
+ * session and make the recorded reviewer route false. The unsupported
17
+ * per-session reviewer route is recorded in `docs/seam-gap.md` with the public
18
+ * seam it would need.
19
+ *
12
20
  * The official gate stays an opt-in capability: the row mounts it only when
13
21
  * the profile actually started with the experimental-auto-review bundle.
14
22
  * `cordis.patch.yml` disables the official row (one `registerAuto` per
@@ -7,7 +7,7 @@ import { type ProviderGroupConfig } from '../provider-group.js';
7
7
  import type { RuntimeCapabilities } from '../runtime-capabilities.js';
8
8
  import { type CapabilitiesSnapshot } from './search-capabilities.js';
9
9
  import type { ModelSwitchLocaleKey } from './locales.js';
10
- import type { ConfigViewForm } from './derived-config-form.js';
10
+ import type { ConfigViewForm, ConfigViewOp } from './derived-config-form.js';
11
11
  export interface ModelSwitchSettingsFace {
12
12
  t: (key: ModelSwitchLocaleKey) => string;
13
13
  hooks: {
@@ -16,11 +16,16 @@ export interface ModelSwitchSettingsFace {
16
16
  imageSettings: ConfigViewForm<CapabilityRouteView>;
17
17
  autoReviewSettings: ConfigViewForm<CapabilityRouteView>;
18
18
  compactionSettings: ConfigViewForm<CapabilityRouteView>;
19
+ titleSettings: ConfigViewForm<CapabilityRouteView>;
19
20
  switchSettings: Pick<ConfigForm<ModelSwitchSettingsView>, 'getSnapshot' | 'subscribe'>;
20
21
  };
21
22
  capabilities: RuntimeCapabilities;
22
- setSubagent: (field: 'mode' | 'provider' | 'model' | 'reasoningEffort', value: string | undefined) => Promise<void>;
23
- setCapability: (route: 'search' | 'image' | 'autoReview' | 'compaction', field: 'provider' | 'model', value: string | undefined) => Promise<void>;
23
+ /**
24
+ * Apply one row's changes as a SINGLE revision-fenced write, so a refused
25
+ * write can never leave a half-written provider/model pair behind.
26
+ */
27
+ saveSubagent: (ops: readonly ConfigViewOp<SubagentSettingsView>[]) => Promise<void>;
28
+ saveCapability: (route: 'search' | 'image' | 'autoReview' | 'compaction' | 'title', ops: readonly ConfigViewOp<CapabilityRouteView>[]) => Promise<void>;
24
29
  setCompactOnSwitch: (value: boolean) => Promise<void>;
25
30
  /** Replace or remove (undefined) one provider's saved capability group. */
26
31
  saveProviderGroup: (provider: string, group: ProviderGroupConfig | undefined) => Promise<void>;
@@ -1,9 +1,23 @@
1
1
  import type { ConfigForm, ConfigFormSnapshot } from '@deepseek-ai/dsh-client-ui-settings/client';
2
+ /** One field-level change expressed against a view model, before source-path mapping. */
3
+ export interface ConfigViewOp<T> {
4
+ field: keyof T & string;
5
+ value?: unknown;
6
+ }
2
7
  export interface ConfigViewForm<T> {
3
8
  getSnapshot(): ConfigFormSnapshot<T>;
4
9
  subscribe(listener: () => void): () => void;
5
10
  set(field: keyof T & string, value: unknown): Promise<void>;
6
11
  unset(field: keyof T & string): Promise<void>;
12
+ /**
13
+ * Apply several view-field changes as ONE revision-fenced source write, so a row save
14
+ * either lands completely or is rejected as a whole instead of leaving a half-written pair.
15
+ *
16
+ * @param ops view fields to change; `value === undefined` unsets the source field
17
+ * @param expectedRevision revision the draft was seeded from; forwarded to the source fence
18
+ * @returns a promise that rejects with `settings-rejected` when the host refuses the write
19
+ */
20
+ mutate(ops: readonly ConfigViewOp<T>[], expectedRevision?: number): Promise<void>;
7
21
  }
8
22
  export declare function deriveConfigForm<Source, View>(source: ConfigForm<Source>, project: (value: Source) => View, fields: {
9
23
  [K in keyof View & string]: keyof Source & string;
@@ -39,13 +39,18 @@ export declare const zh: {
39
39
  readonly compactionFollow: "跟随会话模型";
40
40
  readonly compactionHelp: "为官方压缩(/compact 与官方自动压缩)指定摘要模型;关闭(两个字段都留空)时使用官方默认,即当前会话的目标模型。";
41
41
  readonly compactionRouteHelp: "摘要请求固定使用这里的 Provider/模型,与会话目标模型无关;保存后对下一次压缩立即生效。保存时若恰有压缩进行中,可能被打断并在下次重试。";
42
+ readonly sessionTitle: "会话命名模型";
43
+ readonly titleFollow: "跟随会话模型";
44
+ readonly titleHelp: "为官方自动命名(新会话首条消息后生成标题)指定模型;关闭(两个字段都留空)时命名跟随当前会话模型。";
45
+ readonly titleRouteHelp: "命名请求固定使用这里的 Provider/模型(不带推理强度);关闭时跟随会话模型。保存后立即对下一次命名生效,不会中断正在进行的命名,会话日志记录的也是实际使用的模型。";
42
46
  readonly providerGroups: "供应商配置组";
43
47
  readonly providerGroupsCard: "按供应商预设路由";
44
- readonly providerGroupsSummary: "会话模型切换到该供应商时自动应用对应组";
45
- readonly providerGroupsHelp: "为压缩、子代理、审查按供应商分别保存路由。";
46
- readonly providerGroupsBodyHelp: "每个供应商保存一组压缩、子代理、审查路由;本会话的模型切换到该供应商时(无论通过插件的模型选择器还是 TUI 等其他界面),这三条路由自动写入上方对应卡片。切换到未配置的供应商时现有路由保持不变;应用会覆盖三张卡片的当前值(含手动修改)。";
48
+ readonly providerGroupsSummary: "使用该供应商的会话自动采用对应组";
49
+ readonly providerGroupsHelp: "为会话命名与子代理按供应商分别保存路由。";
50
+ readonly providerGroupsBodyHelp: "每个供应商保存一组会话命名与子代理路由。会话自身的供应商与该组一致时,该会话的命名与子代理按组解析,否则回落到上方卡片里的全局设置。切换某个会话的模型不会写入任何全局设置,因此不同供应商的多个会话互不影响。压缩与自动审查仍是全局卡片:压缩事件要如实记录实际写入摘要的模型,审查的拦截点没有会话身份。";
47
51
  readonly providerGroupPick: "选择供应商以编辑其配置组。";
48
52
  readonly providerGroupDelete: "删除该组";
53
+ readonly saveTimeout: "保存超时,请重试";
49
54
  readonly 'reason.central-subagent-routing': "Alpha.4 没有全局子代理启动路由接口。";
50
55
  readonly 'reason.packaged-preset-roots': "Alpha.4 不支持插件提供额外 preset root。";
51
56
  readonly 'reason.tool-owner-suppression': "Alpha.4 没有工具所有者或来源抑制接口。";
@@ -15,6 +15,8 @@ export interface ModelSwitchSettingsView {
15
15
  autoReviewModel?: string;
16
16
  compactProvider?: string;
17
17
  compactModel?: string;
18
+ titleProvider?: string;
19
+ titleModel?: string;
18
20
  /** Raw saved per-provider groups; edited whole through the provider-groups card. */
19
21
  providerGroups?: ProviderGroupsStorage;
20
22
  }
@@ -54,10 +56,16 @@ export declare const COMPACTION_SETTINGS_FIELDS: Readonly<{
54
56
  readonly provider: "compactProvider";
55
57
  readonly model: "compactModel";
56
58
  }>;
59
+ /** Automatic session-title naming route; both unset means the official default (the session's own logged route). */
60
+ export declare const TITLE_SETTINGS_FIELDS: Readonly<{
61
+ readonly provider: "titleProvider";
62
+ readonly model: "titleModel";
63
+ }>;
57
64
  export declare function deriveSubagentSettings(settings: ModelSwitchSettingsView): SubagentSettingsView;
58
65
  export declare function deriveSearchSettings(settings: ModelSwitchSettingsView): CapabilityRouteView;
59
66
  export declare function deriveImageSettings(settings: ModelSwitchSettingsView): CapabilityRouteView;
60
67
  export declare function deriveAutoReviewSettings(settings: ModelSwitchSettingsView): CapabilityRouteView;
61
68
  export declare function deriveCompactionSettings(settings: ModelSwitchSettingsView): CapabilityRouteView;
69
+ export declare function deriveTitleSettings(settings: ModelSwitchSettingsView): CapabilityRouteView;
62
70
  /** Whether both fields of a model route form a complete pair; an incomplete pair stays off. */
63
71
  export declare function routePairConfigured(route: CapabilityRouteView | undefined): boolean;
@@ -5,7 +5,16 @@ import type { BasicCompactionConfig } from '@deepseek-ai/dsh-compaction-basic';
5
5
  export declare const name = "model-switch-compaction";
6
6
  /** The loader is awaited at row activation; settings and the official row are still read structurally so stock compaction survives a disabled Model Switch row. */
7
7
  export declare const inject: string[];
8
- /** Both identifiers trimmed and non-empty; undefined means "official default". */
8
+ /**
9
+ * Both identifiers trimmed and non-empty; undefined means "official default".
10
+ *
11
+ * The summarization route stays deployment-wide on purpose. The official
12
+ * compaction transaction durably records the provider/model that wrote the
13
+ * summary, so a per-session rewrite of the summarization call would make that
14
+ * record false; a deployment that needs provider-scoped summarization uses the
15
+ * official exact-target `modelPolicies` table, which this row forwards
16
+ * verbatim from the profile's `compaction-basic` row config.
17
+ */
9
18
  export interface CompactionRoute {
10
19
  readonly provider: string;
11
20
  readonly model: string;
@@ -39,6 +48,17 @@ export declare function officialCompactionEntry(loader: Loader | undefined): Ent
39
48
  * @returns A plain object spread; missing or invalid configs become `{}`.
40
49
  */
41
50
  export declare function forwardedConfig(entry: Entry): BasicCompactionConfig;
51
+ /**
52
+ * Whether the compaction capability is actually served from this context.
53
+ *
54
+ * Awaiting a plugin fiber settles even when the plugin's injections were never
55
+ * satisfied, so the mount is confirmed through the public service instead of by
56
+ * the absence of an error: an unactivated engine must never be recorded as the
57
+ * live engine, or the row would stop retrying while `/compact` had no service.
58
+ * @param ctx - this row's context.
59
+ * @returns true when `ctx.compaction` exposes the engine's public entry points.
60
+ */
61
+ export declare function engineActive(ctx: Context): boolean;
42
62
  /**
43
63
  * Mount the Model Switch compaction engine for this profile.
44
64
  *
@@ -46,9 +66,10 @@ export declare function forwardedConfig(entry: Entry): BasicCompactionConfig;
46
66
  * with the same package under the configurable summarization route. Host
47
67
  * settings and the loader are read structurally so a disabled `model-switch`
48
68
  * row leaves official compaction untouched. Reconciles run on
49
- * `model-switch/settings-update` (emitted by the host on volatile commits and
50
- * ordinary saves); the listener is global so instances of this row inside
51
- * agent-preset realms still hear the host row's emission. The initial
69
+ * `model-switch/settings-update` (emitted by the host at construction, on
70
+ * volatile commits, and on ordinary saves) and read the settled settings, so one
71
+ * save costs at most one remount; the listener is global so instances of this
72
+ * row inside agent-preset realms still hear the host row's emission. The initial
52
73
  * reconcile is awaited so `/compact` resolves the service as soon as this row
53
74
  * completes.
54
75
  * @param ctx - this row's context.
@@ -21,6 +21,10 @@ export interface ModelSwitchSettings {
21
21
  compactProvider?: string;
22
22
  /** Summarization model for `/compact` and automatic compaction; set together with `compactProvider`. */
23
23
  compactModel?: string;
24
+ /** Naming provider for automatic session titles; both fields unset = official default (the session's own logged route). */
25
+ titleProvider?: string;
26
+ /** Naming model for automatic session titles; set together with `titleProvider`. */
27
+ titleModel?: string;
24
28
  /** Per-provider compaction/subagent/review presets, keyed by provider id; absent when no group is saved. */
25
29
  providerGroups?: ProviderGroups;
26
30
  }
@@ -39,6 +43,8 @@ export interface Config {
39
43
  autoReviewModel: Volatile<string | undefined>;
40
44
  compactProvider: Volatile<string | undefined>;
41
45
  compactModel: Volatile<string | undefined>;
46
+ titleProvider: Volatile<string | undefined>;
47
+ titleModel: Volatile<string | undefined>;
42
48
  /** Saved per-provider capability groups as one entry per provider; entry route fields are '' when the capability stays at its default. */
43
49
  providerGroups: Volatile<readonly ProviderGroupEntry[]>;
44
50
  }
@@ -1,15 +1,24 @@
1
1
  /**
2
- * Provider configuration groups: per-provider presets for the compaction,
3
- * subagent, and auto-review routes.
2
+ * Provider configuration groups: per-provider presets for the session-naming
3
+ * and subagent routes.
4
4
  *
5
- * A group stores the exact flat settings fields it will write, so applying a
6
- * group is a plain settings-layer write: every capability consumer keeps
7
- * reading the live values unchanged. All functions here are pure and never
8
- * throw on malformed storage.
5
+ * A group is a DERIVED policy, not a write: `src/session-route.ts` resolves the
6
+ * routes one session should use from its own provider plus this stored table, so
7
+ * switching a session's provider never rewrites deployment-wide settings and two
8
+ * sessions on different providers cannot overwrite each other. The field names
9
+ * below are the flat settings field names a group pins, which keeps the stored
10
+ * shape identical to the earlier write-through design.
11
+ *
12
+ * Legacy entries may still carry `compactProvider`/`compactModel`/
13
+ * `autoReviewProvider`/`autoReviewModel`: normalization preserves them so an
14
+ * older stored document is never silently rewritten into a different meaning,
15
+ * while the consumption side ignores them (a group save from the current UI
16
+ * writes '' for every unsupported field, which normalization drops).
17
+ *
18
+ * All functions here are pure and never throw on malformed storage.
9
19
  */
10
- import type { SubagentMode } from './host-settings.js';
11
- /** Flat settings fields a provider group can pin — exactly what an application writes. */
12
- export type ProviderGroupRouteField = 'compactProvider' | 'compactModel' | 'autoReviewProvider' | 'autoReviewModel' | 'subagentProvider' | 'subagentModel' | 'subagentReasoningEffort';
20
+ /** Flat settings fields a provider group carries; the first four are legacy-only. */
21
+ export type ProviderGroupRouteField = 'compactProvider' | 'compactModel' | 'autoReviewProvider' | 'autoReviewModel' | 'subagentProvider' | 'subagentModel' | 'subagentReasoningEffort' | 'titleProvider' | 'titleModel';
13
22
  /** Stored entry: the owning provider plus every route field; '' means the capability stays at its default. */
14
23
  export interface ProviderGroupEntry {
15
24
  provider: string;
@@ -20,6 +29,8 @@ export interface ProviderGroupEntry {
20
29
  subagentProvider: string;
21
30
  subagentModel: string;
22
31
  subagentReasoningEffort: string;
32
+ titleProvider: string;
33
+ titleModel: string;
23
34
  }
24
35
  /** Normalized view of one provider's group; absent fields follow their default state. */
25
36
  export type ProviderGroupConfig = Partial<Record<ProviderGroupRouteField, string>>;
@@ -40,22 +51,6 @@ export declare function normalizeProviderGroups(raw: unknown): ProviderGroups;
40
51
  * which is what leaves the live routes untouched during a switch.
41
52
  */
42
53
  export declare function groupOf(groups: ProviderGroups | undefined, provider: string): ProviderGroupConfig | undefined;
43
- /** One application write; `value` undefined means "unset this field". */
44
- export type ProviderGroupWrite = {
45
- readonly field: 'subagentMode';
46
- readonly mode: SubagentMode;
47
- } | {
48
- readonly field: ProviderGroupRouteField;
49
- readonly value: string | undefined;
50
- };
51
- /**
52
- * Ordered writes applying one group with full-replacement semantics: pinned
53
- * capabilities land as pairs, everything else reverts to its default state
54
- * (compaction/review follow the session model; Subagent follows official
55
- * inherit). The mode write comes last so a fixed route never publishes with
56
- * the previous provider's fields still half-written.
57
- */
58
- export declare function providerGroupWrites(group: ProviderGroupConfig): ProviderGroupWrite[];
59
54
  /** Storage entry for one provider's group; absent or blank route fields become ''. */
60
55
  export declare function entryOf(provider: string, group: ProviderGroupConfig): ProviderGroupEntry;
61
56
  /**
@@ -65,14 +60,3 @@ export declare function entryOf(provider: string, group: ProviderGroupConfig): P
65
60
  * provider only normalizes.
66
61
  */
67
62
  export declare function withProviderGroup(storage: unknown, provider: string, group: ProviderGroupConfig | undefined): ProviderGroupsStorage;
68
- /** Minimal writable settings surface an application needs; satisfied by the owned ConfigForm. */
69
- export interface ProviderGroupSink {
70
- set(field: 'subagentMode' | ProviderGroupRouteField, value: string): Promise<unknown>;
71
- unset(field: ProviderGroupRouteField): Promise<unknown>;
72
- }
73
- /**
74
- * Apply a saved group's routes to the live flat settings.
75
- * @returns true when a group existed and every write settled; false when the
76
- * provider has no group (nothing was written). A rejected write propagates.
77
- */
78
- export declare function applyProviderGroupTo(sink: ProviderGroupSink, groups: ProviderGroups | undefined, provider: string): Promise<boolean>;