@gajae-code/coding-agent 0.12.21 → 0.13.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.
Files changed (222) hide show
  1. package/CHANGELOG.md +67 -2
  2. package/dist/types/cli/args.d.ts +5 -2
  3. package/dist/types/cli/root-flags.d.ts +246 -0
  4. package/dist/types/cli.d.ts +11 -19
  5. package/dist/types/commands/daemon.d.ts +0 -30
  6. package/dist/types/commands/launch.d.ts +11 -19
  7. package/dist/types/commands/quick-lane.d.ts +32 -0
  8. package/dist/types/commands/sdk.d.ts +21 -15
  9. package/dist/types/config/file-lock-gc.d.ts +2 -0
  10. package/dist/types/config/model-equivalence.d.ts +20 -0
  11. package/dist/types/config/model-profile-activation.d.ts +62 -17
  12. package/dist/types/config/model-profiles.d.ts +10 -0
  13. package/dist/types/config/model-registry.d.ts +68 -1
  14. package/dist/types/config/model-resolver.d.ts +15 -4
  15. package/dist/types/config/models-config-schema.d.ts +7 -2
  16. package/dist/types/config/provider-selection-policy.d.ts +62 -0
  17. package/dist/types/config/settings-schema.d.ts +36 -78
  18. package/dist/types/config/settings.d.ts +2 -0
  19. package/dist/types/edit/renderer.d.ts +5 -2
  20. package/dist/types/edit/streaming.d.ts +13 -0
  21. package/dist/types/extensibility/custom-tools/types.d.ts +2 -0
  22. package/dist/types/extensibility/extensions/compact-handler.d.ts +2 -1
  23. package/dist/types/extensibility/extensions/types.d.ts +3 -0
  24. package/dist/types/gjc-runtime/gc-runtime.d.ts +19 -5
  25. package/dist/types/internal-urls/local-protocol.d.ts +1 -0
  26. package/dist/types/modes/components/model-selector.d.ts +0 -2
  27. package/dist/types/modes/components/provider-order-context.d.ts +45 -0
  28. package/dist/types/modes/components/provider-order-editor.d.ts +24 -0
  29. package/dist/types/modes/components/settings-defs.d.ts +5 -1
  30. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  31. package/dist/types/quick-lane/classify.d.ts +40 -0
  32. package/dist/types/runtime/stt-settings-setup.d.ts +18 -0
  33. package/dist/types/sdk/broker/broker.d.ts +56 -2
  34. package/dist/types/sdk/broker/operation-receipt-ledger.d.ts +25 -0
  35. package/dist/types/sdk/broker/session-index.d.ts +83 -2
  36. package/dist/types/sdk/broker/state-version.d.ts +1 -1
  37. package/dist/types/sdk/bus/notification-service.d.ts +6 -0
  38. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +8 -1
  39. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +4 -1
  40. package/dist/types/sdk/bus/telegram-daemon.d.ts +26 -0
  41. package/dist/types/sdk/cli/rows.d.ts +98 -0
  42. package/dist/types/sdk/cli/session-cli.d.ts +35 -3
  43. package/dist/types/sdk/elevation/allowlist.d.ts +20 -0
  44. package/dist/types/sdk/elevation/audit.d.ts +93 -0
  45. package/dist/types/sdk/elevation/capability.d.ts +3 -0
  46. package/dist/types/sdk/elevation/digest.d.ts +72 -0
  47. package/dist/types/sdk/elevation/dispatch-receipt.d.ts +85 -0
  48. package/dist/types/sdk/elevation/grant-ledger.d.ts +163 -0
  49. package/dist/types/sdk/elevation/owner.d.ts +48 -0
  50. package/dist/types/sdk/guides/cache.d.ts +98 -0
  51. package/dist/types/sdk/guides/catalog.d.ts +156 -0
  52. package/dist/types/sdk/guides/cli.d.ts +39 -0
  53. package/dist/types/sdk/guides/manifest.d.ts +66 -0
  54. package/dist/types/sdk/guides/verify.d.ts +66 -0
  55. package/dist/types/sdk/host/control/dispatch.d.ts +8 -0
  56. package/dist/types/sdk/host/control/index.d.ts +1 -1
  57. package/dist/types/sdk/host/control/operations.d.ts +3 -2
  58. package/dist/types/sdk/host/query/cursor.d.ts +10 -0
  59. package/dist/types/sdk/host/query/handlers.d.ts +21 -0
  60. package/dist/types/sdk/host/session-runtime.d.ts +9 -0
  61. package/dist/types/sdk/mcp/server.d.ts +2 -2
  62. package/dist/types/sdk/prompt-status.d.ts +11 -0
  63. package/dist/types/sdk/session.d.ts +4 -0
  64. package/dist/types/sdk/transport/index.d.ts +1 -0
  65. package/dist/types/sdk/transport/relay.d.ts +1 -0
  66. package/dist/types/sdk/transport/serve-cli.d.ts +20 -1
  67. package/dist/types/session/agent-session.d.ts +32 -18
  68. package/dist/types/session/blob-store.d.ts +13 -7
  69. package/dist/types/session/fallback-chain-controller.d.ts +15 -0
  70. package/dist/types/session/internal/managed-session-storage.d.ts +25 -1
  71. package/dist/types/session/internal/session-memory-sidecar.d.ts +5 -5
  72. package/dist/types/session/session-manager.d.ts +139 -10
  73. package/dist/types/session/session-storage.d.ts +48 -0
  74. package/dist/types/setup/provider-onboarding.d.ts +7 -1
  75. package/dist/types/slash-commands/builtin-registry.d.ts +14 -0
  76. package/dist/types/stt/setup.d.ts +2 -0
  77. package/dist/types/task/executor.d.ts +10 -2
  78. package/dist/types/tools/browser/runtime-diagnostics.d.ts +65 -0
  79. package/dist/types/tools/browser/tab-protocol.d.ts +4 -0
  80. package/dist/types/tools/browser/tab-supervisor.d.ts +2 -0
  81. package/dist/types/tools/browser/tab-worker.d.ts +2 -0
  82. package/dist/types/tools/browser.d.ts +2 -0
  83. package/dist/types/tools/image-gen.d.ts +14 -18
  84. package/dist/types/tools/index.d.ts +2 -0
  85. package/dist/types/tools/resource-gc.d.ts +2 -0
  86. package/dist/types/utils/pasted-image-path.d.ts +6 -0
  87. package/package.json +9 -9
  88. package/scripts/generate-sdk-operation-inventory.ts +8 -0
  89. package/scripts/run-test-manifest.ts +61 -21
  90. package/scripts/verify-gjc-sdk-canonicalization.ts +9 -4
  91. package/src/cli/args.ts +62 -9
  92. package/src/cli/completion-cli.ts +1 -0
  93. package/src/cli/fast-help.ts +1 -0
  94. package/src/cli/root-flags.ts +100 -0
  95. package/src/cli/setup-cli.ts +6 -2
  96. package/src/cli.ts +42 -65
  97. package/src/commands/acp.ts +1 -1
  98. package/src/commands/daemon.ts +2 -31
  99. package/src/commands/gc.ts +1 -1
  100. package/src/commands/launch.ts +8 -129
  101. package/src/commands/quick-lane.ts +51 -0
  102. package/src/commands/sdk.ts +200 -26
  103. package/src/commands/setup.ts +4 -2
  104. package/src/config/file-lock-gc.ts +4 -1
  105. package/src/config/model-equivalence.ts +76 -7
  106. package/src/config/model-profile-activation.ts +1115 -265
  107. package/src/config/model-profiles.ts +134 -2
  108. package/src/config/model-registry.ts +352 -50
  109. package/src/config/model-resolver.ts +289 -38
  110. package/src/config/models-config-schema.ts +11 -4
  111. package/src/config/provider-selection-policy.ts +146 -0
  112. package/src/config/settings-schema.ts +50 -77
  113. package/src/config/settings.ts +40 -0
  114. package/src/coordinator-mcp/server.ts +32 -2
  115. package/src/edit/renderer.ts +175 -223
  116. package/src/edit/streaming.ts +108 -1
  117. package/src/export/html/index.ts +57 -7
  118. package/src/extensibility/custom-tools/types.ts +2 -0
  119. package/src/extensibility/extensions/compact-handler.ts +3 -2
  120. package/src/extensibility/extensions/runner.ts +6 -0
  121. package/src/extensibility/extensions/types.ts +3 -0
  122. package/src/gjc-runtime/gc-runtime.ts +379 -20
  123. package/src/gjc-runtime/launch-worktree.ts +5 -1
  124. package/src/gjc-runtime/team-runtime.ts +59 -2
  125. package/src/gjc-runtime/tmux-owner-isolation.ts +19 -0
  126. package/src/gjc-runtime/ultragoal-receipt-freshness.ts +16 -1
  127. package/src/internal-urls/docs-index.generated.ts +17 -13
  128. package/src/internal-urls/local-protocol.ts +1 -0
  129. package/src/main.ts +40 -3
  130. package/src/memories/index.ts +4 -2
  131. package/src/modes/acp/acp-agent.ts +235 -91
  132. package/src/modes/components/model-selector.ts +206 -42
  133. package/src/modes/components/provider-order-context.ts +197 -0
  134. package/src/modes/components/provider-order-editor.ts +441 -0
  135. package/src/modes/components/settings-defs.ts +21 -1
  136. package/src/modes/components/settings-selector.ts +45 -2
  137. package/src/modes/components/tool-execution.ts +47 -15
  138. package/src/modes/controllers/command-controller.ts +6 -3
  139. package/src/modes/controllers/extension-ui-controller.ts +2 -0
  140. package/src/modes/controllers/input-controller.ts +73 -2
  141. package/src/modes/controllers/selector-controller.ts +260 -153
  142. package/src/modes/prompt-suggestion-controller.ts +1 -1
  143. package/src/modes/runtime-init.ts +1 -0
  144. package/src/modes/utils/ui-helpers.ts +18 -0
  145. package/src/prompts/tools/browser.md +1 -0
  146. package/src/quick-lane/classify.ts +257 -0
  147. package/src/rlm/index.ts +1 -1
  148. package/src/runtime/stt-settings-setup.ts +57 -0
  149. package/src/sdk/acp/adapter.ts +2 -1
  150. package/src/sdk/broker/broker.ts +780 -12
  151. package/src/sdk/broker/lifecycle.ts +70 -29
  152. package/src/sdk/broker/operation-receipt-ledger.ts +144 -0
  153. package/src/sdk/broker/session-index.ts +463 -48
  154. package/src/sdk/broker/state-version.ts +13 -9
  155. package/src/sdk/broker/transport.ts +14 -1
  156. package/src/sdk/bus/index.ts +34 -13
  157. package/src/sdk/bus/kind-aware-reconciliation.ts +21 -2
  158. package/src/sdk/bus/notification-service.ts +47 -1
  159. package/src/sdk/bus/prompt-reconciliation.ts +22 -3
  160. package/src/sdk/bus/telegram-daemon-contract.ts +4 -1
  161. package/src/sdk/bus/telegram-daemon.ts +54 -3
  162. package/src/sdk/cli/rows.ts +247 -0
  163. package/src/sdk/cli/session-cli.ts +930 -79
  164. package/src/sdk/elevation/allowlist.ts +54 -0
  165. package/src/sdk/elevation/audit.ts +208 -0
  166. package/src/sdk/elevation/capability.ts +41 -0
  167. package/src/sdk/elevation/digest.ts +129 -0
  168. package/src/sdk/elevation/dispatch-receipt.ts +184 -0
  169. package/src/sdk/elevation/grant-ledger.ts +1142 -0
  170. package/src/sdk/elevation/owner.ts +109 -0
  171. package/src/sdk/guides/cache.ts +363 -0
  172. package/src/sdk/guides/catalog.ts +554 -0
  173. package/src/sdk/guides/cli.ts +235 -0
  174. package/src/sdk/guides/manifest.ts +152 -0
  175. package/src/sdk/guides/verify.ts +178 -0
  176. package/src/sdk/host/control/dispatch.ts +49 -1
  177. package/src/sdk/host/control/index.ts +1 -0
  178. package/src/sdk/host/control/operations.ts +8 -1
  179. package/src/sdk/host/query/cursor.ts +47 -3
  180. package/src/sdk/host/query/handlers.ts +155 -2
  181. package/src/sdk/host/sdk-surface-parity.test.ts +1 -0
  182. package/src/sdk/host/session-runtime.test.ts +391 -1
  183. package/src/sdk/host/session-runtime.ts +308 -73
  184. package/src/sdk/host/surface-policy.ts +3 -0
  185. package/src/sdk/host/websocket-transport.ts +7 -1
  186. package/src/sdk/mcp/server.ts +88 -15
  187. package/src/sdk/prompt-status.ts +8 -0
  188. package/src/sdk/protocol/operation-inventory.generated.json +62 -0
  189. package/src/sdk/protocol/operation-registry.ts +3 -2
  190. package/src/sdk/session.ts +87 -39
  191. package/src/sdk/transport/index.ts +1 -0
  192. package/src/sdk/transport/relay.ts +3 -0
  193. package/src/sdk/transport/serve-cli.ts +111 -24
  194. package/src/secrets/obfuscator.ts +92 -12
  195. package/src/session/agent-session.ts +322 -93
  196. package/src/session/blob-store.ts +60 -8
  197. package/src/session/fallback-chain-controller.ts +35 -0
  198. package/src/session/internal/managed-session-scope.ts +25 -3
  199. package/src/session/internal/managed-session-storage.ts +574 -73
  200. package/src/session/internal/session-memory-sidecar.ts +19 -29
  201. package/src/session/session-manager.ts +2166 -743
  202. package/src/session/session-storage.ts +715 -48
  203. package/src/setup/provider-onboarding.ts +19 -3
  204. package/src/setup/provider-presets.json +3 -1
  205. package/src/slash-commands/builtin-registry.ts +85 -3
  206. package/src/stt/downloader.ts +2 -3
  207. package/src/stt/setup.ts +29 -4
  208. package/src/task/executor.ts +22 -18
  209. package/src/task/index.ts +24 -1
  210. package/src/tools/archive-reader.ts +11 -2
  211. package/src/tools/browser/runtime-diagnostics.ts +249 -0
  212. package/src/tools/browser/tab-protocol.ts +4 -0
  213. package/src/tools/browser/tab-supervisor.ts +5 -0
  214. package/src/tools/browser/tab-worker.ts +36 -1
  215. package/src/tools/browser.ts +7 -0
  216. package/src/tools/image-gen.ts +110 -166
  217. package/src/tools/index.ts +2 -0
  218. package/src/tools/read.ts +1 -1
  219. package/src/tools/resource-gc.ts +6 -1
  220. package/src/tools/tool-catalog.generated.ts +5 -1
  221. package/src/utils/pasted-image-path.ts +93 -0
  222. package/src/web/search/index.ts +3 -3
@@ -1,11 +1,12 @@
1
1
  import { ThinkingLevel } from "@gajae-code/agent-core";
2
2
  import type { Api, Model } from "@gajae-code/ai/core";
3
- import type { AgentSession } from "../session/agent-session";
4
- import { formatClampedModelSelector } from "../thinking";
3
+ import type { AgentSession, DefaultFallbackRuntimeState } from "../session/agent-session";
4
+ import { clampExplicitThinkingLevelForModel, formatClampedModelSelector } from "../thinking";
5
5
  import { validateModelProfileName } from "./model-profile-contract";
6
6
  import {
7
7
  aggregateModelProfileRequiredProviders,
8
8
  formatModelProfileDisplayLabel,
9
+ PROXY_ROUTABLE_PROVIDER_IDS,
9
10
  resolveProfileBindings,
10
11
  } from "./model-profiles";
11
12
 
@@ -20,9 +21,11 @@ import {
20
21
  } from "./model-registry";
21
22
  import {
22
23
  formatModelSelectorValue,
24
+ formatModelString,
23
25
  parseModelString,
24
26
  resolveModelChainWithAuth,
25
27
  resolveModelRoleValue,
28
+ splitSelectorThinkingSuffix,
26
29
  } from "./model-resolver";
27
30
  import { type ModelSelectorValue, normalizeModelSelectorValue } from "./model-selector-value";
28
31
  import type { Settings } from "./settings";
@@ -31,6 +34,7 @@ type ModelProfileActivationSession = Pick<
31
34
  AgentSession,
32
35
  "model" | "thinkingLevel" | "sessionId" | "getConfiguredModelChain" | "setConfiguredModelChain"
33
36
  > & {
37
+ credentialSessionId?: string;
34
38
  setModelTemporary?: AgentSession["setModelTemporary"];
35
39
  setActiveModelProfile?: (name: string | undefined) => void;
36
40
  getActiveModelProfile?: () => string | undefined;
@@ -45,8 +49,30 @@ type ModelProfileActivationSession = Pick<
45
49
  /** Current profile-installed override keys, for deriving the activation base. */
46
50
  getProfileInstalledOverrideKeys?: () => { modelRoles: readonly string[]; agentModelOverrides: readonly string[] };
47
51
  getSessionDefaultModelSelector?: () => string | undefined;
48
- recordResumeDefaultModel?: (selector: string) => void;
52
+ recordResumeDefaultModel?: (selector: string | undefined) => void;
49
53
  seedDefaultFallbackResolution?: (activeIndex: number, skips: Array<{ selector: string; reason: string }>) => void;
54
+ getDefaultFallbackRuntimeState?: () => DefaultFallbackRuntimeState;
55
+ restoreDefaultFallbackRuntimeState?: (state: DefaultFallbackRuntimeState) => void;
56
+ restoreModelSelectionForRollback?: AgentSession["restoreModelSelectionForRollback"];
57
+ modelRegistry?: Pick<
58
+ ModelRegistry,
59
+ | "getSessionCanonicalVariant"
60
+ | "restoreSessionCanonicalVariant"
61
+ | "clearCanonicalVariant"
62
+ | "getAvailable"
63
+ | "lookupAliasExists"
64
+ | "resolveModelByLookupAlias"
65
+ | "authStorage"
66
+ | "isCredentiallessProvider"
67
+ >;
68
+ getConfiguredModelChainState?: (role: string) => ConfiguredModelChainState | undefined;
69
+ };
70
+
71
+ type ConfiguredModelChainState = {
72
+ entries: readonly string[];
73
+ origin: string;
74
+ identity?: string;
75
+ explicitHead: boolean;
50
76
  };
51
77
 
52
78
  export interface PrepareModelProfileActivationOptions {
@@ -61,8 +87,23 @@ export interface PrepareModelProfileActivationOptions {
61
87
  | "resolveCanonicalModel"
62
88
  | "getCanonicalVariants"
63
89
  | "getCanonicalId"
64
- > & { getError?: ModelRegistry["getError"] };
65
- settings: Pick<Settings, "get" | "getGlobal">;
90
+ > &
91
+ Partial<
92
+ Pick<
93
+ ModelRegistry,
94
+ | "getAvailable"
95
+ | "resolveModelByLookupAlias"
96
+ | "lookupAliasExists"
97
+ | "clearCanonicalVariant"
98
+ | "seedCanonicalVariant"
99
+ | "getSessionCanonicalVariant"
100
+ | "restoreSessionCanonicalVariant"
101
+ | "getConfiguredProviderIds"
102
+ >
103
+ > & {
104
+ getError?: ModelRegistry["getError"];
105
+ };
106
+ settings: Pick<Settings, "get" | "getGlobal" | "getOverride">;
66
107
  profileName: string;
67
108
  }
68
109
  export interface ApplyModelProfileActivationOptions {
@@ -72,14 +113,27 @@ export interface ApplyModelProfileActivationOptions {
72
113
  export interface PreparedModelProfileActivation {
73
114
  profileName: string;
74
115
  session: ModelProfileActivationSession & { setModelTemporary: AgentSession["setModelTemporary"] };
75
- settings: Pick<Settings, "clearOverride" | "get" | "getGlobal" | "override" | "set" | "unset" | "flush">;
116
+ settings: Pick<
117
+ Settings,
118
+ "clearOverride" | "get" | "getGlobal" | "getOverride" | "override" | "set" | "unset" | "flush" | "flushOrThrow"
119
+ >;
76
120
  previousModel: Model<Api> | undefined;
77
121
  previousThinkingLevel: ThinkingLevel | undefined;
78
122
  previousAgentModelOverrides: Record<string, ModelSelectorValue>;
79
123
  previousModelRoles: Record<string, ModelSelectorValue>;
124
+ previousPersistedModelRoles: Record<string, ModelSelectorValue> | undefined;
125
+ previousPersistedAgentModelOverrides: Record<string, ModelSelectorValue> | undefined;
126
+ previousModelRolesOverride: Record<string, ModelSelectorValue> | undefined;
127
+ previousAgentModelOverridesOverride: Record<string, ModelSelectorValue> | undefined;
128
+ previousDefaultProfileOverride: string | undefined;
129
+ previousPersistedDefaultProfile: string | undefined;
130
+ previousPersistedDefaultThinkingLevel: Exclude<ThinkingLevel, "inherit"> | undefined;
131
+ previousDefaultThinkingLevelOverride: ThinkingLevel | undefined;
80
132
  baseAgentModelOverrides: Record<string, ModelSelectorValue>;
81
133
  baseModelRoles: Record<string, ModelSelectorValue>;
82
134
  previousDefaultChain: readonly string[] | undefined;
135
+ previousDefaultChainState: ConfiguredModelChainState | undefined;
136
+ previousDefaultFallbackRuntimeState: DefaultFallbackRuntimeState | undefined;
83
137
  defaultModel: Model<Api> | undefined;
84
138
  defaultThinkingLevel: ThinkingLevel | undefined;
85
139
  /** Full configured default fallback chain with resolvable entries clamped. */
@@ -99,13 +153,36 @@ export interface PreparedModelProfileActivation {
99
153
  * resume default without promoting a transient model to it.
100
154
  */
101
155
  previousSessionDefaultModel: string | undefined;
156
+ /**
157
+ * Exact concrete sticky selector ("provider/id") snapshotted from the
158
+ * registry BEFORE the session sticky canonical variant is invalidated during
159
+ * prepare. Captured verbatim — never re-derived from the live model — so a
160
+ * transient live-model switch cannot corrupt the restored sticky. Rollback
161
+ * restores this exact selector when present; otherwise the sticky is cleared
162
+ * so a stale provider cannot silently resurrect.
163
+ */
164
+ previousCanonicalVariant: string | undefined;
165
+ /** Registry used to resolve and restore the session sticky canonical variant. */
166
+ modelRegistry: PrepareModelProfileActivationOptions["modelRegistry"];
102
167
  }
103
168
  export interface MaterializeModelProfileAssignmentOptions {
104
169
  session: Pick<
105
170
  ModelProfileActivationSession,
106
- "model" | "thinkingLevel" | "getConfiguredModelChain" | "setActiveModelProfile" | "getActiveModelProfile"
107
- > & { clearProfileInstalledOverrides?: () => void };
108
- settings: Pick<Settings, "clearOverride" | "get" | "override" | "set" | "unset">;
171
+ | "model"
172
+ | "thinkingLevel"
173
+ | "sessionId"
174
+ | "credentialSessionId"
175
+ | "getConfiguredModelChain"
176
+ | "getConfiguredModelChainState"
177
+ | "setConfiguredModelChain"
178
+ | "setActiveModelProfile"
179
+ | "getActiveModelProfile"
180
+ | "getDefaultFallbackRuntimeState"
181
+ | "restoreDefaultFallbackRuntimeState"
182
+ | "modelRegistry"
183
+ | "clearProfileInstalledOverrides"
184
+ >;
185
+ settings: Pick<Settings, "clearOverride" | "get" | "getGlobal" | "getOverride" | "override" | "set" | "unset">;
109
186
  role: GjcModelAssignmentTargetId;
110
187
  selector: string;
111
188
  }
@@ -113,9 +190,21 @@ export interface MaterializeModelProfileAssignmentOptions {
113
190
  export interface MaterializeModelProfileAssignmentsOptions {
114
191
  session: Pick<
115
192
  ModelProfileActivationSession,
116
- "model" | "thinkingLevel" | "getConfiguredModelChain" | "setActiveModelProfile" | "getActiveModelProfile"
117
- > & { clearProfileInstalledOverrides?: () => void };
118
- settings: Pick<Settings, "clearOverride" | "get" | "override" | "set" | "unset">;
193
+ | "model"
194
+ | "thinkingLevel"
195
+ | "sessionId"
196
+ | "credentialSessionId"
197
+ | "getConfiguredModelChain"
198
+ | "getConfiguredModelChainState"
199
+ | "setConfiguredModelChain"
200
+ | "setActiveModelProfile"
201
+ | "getActiveModelProfile"
202
+ | "getDefaultFallbackRuntimeState"
203
+ | "restoreDefaultFallbackRuntimeState"
204
+ | "modelRegistry"
205
+ | "clearProfileInstalledOverrides"
206
+ >;
207
+ settings: Pick<Settings, "clearOverride" | "get" | "getGlobal" | "getOverride" | "override" | "set" | "unset">;
119
208
  assignments: ReadonlyMap<GjcModelAssignmentTargetId, string> | Partial<Record<GjcModelAssignmentTargetId, string>>;
120
209
  }
121
210
 
@@ -141,30 +230,160 @@ function getMaterializedAssignments(
141
230
  function materializeConfiguredDefaultChain(
142
231
  session: Pick<ModelProfileActivationSession, "model" | "thinkingLevel" | "getConfiguredModelChain">,
143
232
  ): ModelSelectorValue | undefined {
144
- const configuredChain = session.getConfiguredModelChain("default") ?? [];
145
- if (!session.model) {
146
- return configuredChain.length === 0
147
- ? undefined
148
- : configuredChain.length === 1
149
- ? configuredChain[0]
150
- : [...configuredChain];
151
- }
233
+ if (!session.model) return undefined;
234
+ return formatModelSelectorValue(`${session.model.provider}/${session.model.id}`, session.thinkingLevel);
235
+ }
152
236
 
153
- const activeSelector = formatModelSelectorValue(
154
- `${session.model.provider}/${session.model.id}`,
155
- session.thinkingLevel,
237
+ function concretizeMaterializedAssignmentValues(
238
+ options: MaterializeModelProfileAssignmentOptions | MaterializeModelProfileAssignmentsOptions,
239
+ assignments: Record<string, ModelSelectorValue>,
240
+ ): Record<string, ModelSelectorValue> {
241
+ const modelRegistry = options.session.modelRegistry;
242
+ const sessionId = (options.session as { sessionId?: string }).sessionId;
243
+ const credentialSessionId = options.session.credentialSessionId ?? sessionId;
244
+ if (!modelRegistry || !sessionId) return assignments;
245
+ const availableModels = modelRegistry.getAvailable();
246
+ const authenticatedModels = availableModels.filter(model => {
247
+ const isCredentiallessProvider = modelRegistry.isCredentiallessProvider?.bind(modelRegistry);
248
+ const hasUsableAuth = modelRegistry.authStorage?.hasUsableAuth?.bind(modelRegistry.authStorage);
249
+ if (!isCredentiallessProvider || !hasUsableAuth) return true;
250
+ return isCredentiallessProvider(model.provider) || hasUsableAuth(model.provider);
251
+ });
252
+ return Object.fromEntries(
253
+ Object.entries(assignments).map(([role, selectorValue]) => {
254
+ const concrete = normalizeModelSelectorValue(selectorValue)
255
+ .map(selector => {
256
+ if (splitSelectorThinkingSuffix(selector).selector.includes("/")) return selector;
257
+ const resolved = resolveModelRoleValue(selector, authenticatedModels, {
258
+ settings: options.settings as Settings,
259
+ modelRegistry,
260
+ sessionId,
261
+ credentialSessionId,
262
+ aliasIntent: "preset-equivalent",
263
+ });
264
+ if (!resolved.model) return undefined;
265
+ const concreteSelector = `${resolved.model.provider}/${resolved.model.id}`;
266
+ return resolved.explicitThinkingLevel && resolved.thinkingLevel
267
+ ? formatModelSelectorValue(concreteSelector, resolved.thinkingLevel)
268
+ : concreteSelector;
269
+ })
270
+ .filter((selector): selector is string => selector !== undefined);
271
+ if (concrete.length === 0) {
272
+ throw new Error(
273
+ `Active model profile assignment ${role} could not be concretized: ${normalizeModelSelectorValue(selectorValue).join(", ")}`,
274
+ );
275
+ }
276
+ return [role, concrete.length === 1 && typeof selectorValue === "string" ? concrete[0] : concrete];
277
+ }),
278
+ );
279
+ }
280
+
281
+ function commitMaterializedProfileAssignments(
282
+ options: MaterializeModelProfileAssignmentOptions | MaterializeModelProfileAssignmentsOptions,
283
+ modelRoles: Record<string, ModelSelectorValue>,
284
+ agentModelOverrides: Record<string, ModelSelectorValue>,
285
+ ): boolean {
286
+ const previousPersistedModelRoles = options.settings.getGlobal("modelRoles");
287
+ const previousPersistedAgentModelOverrides = options.settings.getGlobal("task.agentModelOverrides");
288
+ const previousPersistedDefaultProfile = options.settings.getGlobal("modelProfile.default");
289
+ const previousModelRolesOverride = options.settings.getOverride("modelRoles");
290
+ const previousAgentModelOverridesOverride = options.settings.getOverride("task.agentModelOverrides");
291
+ const previousDefaultProfileOverride = options.settings.getOverride("modelProfile.default");
292
+ const previousActiveProfile = options.session.getActiveModelProfile?.();
293
+ const previousChain = options.session.getConfiguredModelChainState?.("default");
294
+ const previousFallbackRuntimeState = options.session.getDefaultFallbackRuntimeState?.();
295
+ const previousCanonicalVariant = options.session.modelRegistry?.getSessionCanonicalVariant?.(
296
+ (options.session as { sessionId?: string }).sessionId ?? "",
156
297
  );
157
- const exactIndex = configuredChain.indexOf(activeSelector);
158
- const activeIndex =
159
- exactIndex !== -1
160
- ? exactIndex
161
- : configuredChain.findIndex(entry => {
162
- const parsed = parseModelString(entry);
163
- return parsed?.provider === session.model?.provider && parsed?.id === session.model?.id;
164
- });
165
- const effectiveChain =
166
- activeIndex === -1 ? [activeSelector] : [activeSelector, ...configuredChain.slice(activeIndex + 1)];
167
- return effectiveChain.length === 1 ? effectiveChain[0] : effectiveChain;
298
+ try {
299
+ const nextModelRoles = concretizeMaterializedAssignmentValues(options, modelRoles);
300
+ const nextAgentModelOverrides = concretizeMaterializedAssignmentValues(options, agentModelOverrides);
301
+ options.settings.set("modelRoles", nextModelRoles);
302
+ options.settings.set("task.agentModelOverrides", nextAgentModelOverrides);
303
+ options.settings.unset("modelProfile.default");
304
+ options.settings.clearOverride("modelProfile.default");
305
+ options.settings.override("modelRoles", nextModelRoles);
306
+ options.settings.override("task.agentModelOverrides", nextAgentModelOverrides);
307
+ options.session.setConfiguredModelChain(
308
+ "default",
309
+ normalizeModelSelectorValue(nextModelRoles.default),
310
+ "modelRoles",
311
+ undefined,
312
+ true,
313
+ );
314
+ options.session.setActiveModelProfile?.(undefined);
315
+ return true;
316
+ } catch (error) {
317
+ const rollbackErrors: unknown[] = [];
318
+ const restore = (action: () => void): void => {
319
+ try {
320
+ action();
321
+ } catch (rollbackError) {
322
+ rollbackErrors.push(rollbackError);
323
+ }
324
+ };
325
+ restore(() =>
326
+ options.session.setConfiguredModelChain(
327
+ "default",
328
+ previousChain?.entries ?? [],
329
+ previousChain?.origin ?? "rollback",
330
+ previousChain?.identity,
331
+ previousChain?.explicitHead ?? true,
332
+ ),
333
+ );
334
+ if (previousFallbackRuntimeState) {
335
+ restore(() => options.session.restoreDefaultFallbackRuntimeState?.(previousFallbackRuntimeState));
336
+ }
337
+ if (options.session.modelRegistry) {
338
+ restore(() => {
339
+ const sessionId = (options.session as { sessionId?: string }).sessionId;
340
+ if (!sessionId) return;
341
+ if (previousCanonicalVariant) {
342
+ options.session.modelRegistry?.restoreSessionCanonicalVariant?.(sessionId, previousCanonicalVariant);
343
+ } else {
344
+ options.session.modelRegistry?.clearCanonicalVariant?.(sessionId);
345
+ }
346
+ });
347
+ }
348
+ restore(() =>
349
+ previousPersistedModelRoles === undefined
350
+ ? options.settings.unset("modelRoles")
351
+ : options.settings.set("modelRoles", previousPersistedModelRoles),
352
+ );
353
+ restore(() =>
354
+ previousPersistedAgentModelOverrides === undefined
355
+ ? options.settings.unset("task.agentModelOverrides")
356
+ : options.settings.set("task.agentModelOverrides", previousPersistedAgentModelOverrides),
357
+ );
358
+ restore(() =>
359
+ previousPersistedDefaultProfile === undefined
360
+ ? options.settings.unset("modelProfile.default")
361
+ : options.settings.set("modelProfile.default", previousPersistedDefaultProfile),
362
+ );
363
+ restore(() =>
364
+ previousModelRolesOverride === undefined
365
+ ? options.settings.clearOverride("modelRoles")
366
+ : options.settings.override("modelRoles", previousModelRolesOverride),
367
+ );
368
+ restore(() =>
369
+ previousAgentModelOverridesOverride === undefined
370
+ ? options.settings.clearOverride("task.agentModelOverrides")
371
+ : options.settings.override("task.agentModelOverrides", previousAgentModelOverridesOverride),
372
+ );
373
+ restore(() =>
374
+ previousDefaultProfileOverride === undefined
375
+ ? options.settings.clearOverride("modelProfile.default")
376
+ : options.settings.override("modelProfile.default", previousDefaultProfileOverride),
377
+ );
378
+ restore(() => options.session.setActiveModelProfile?.(previousActiveProfile));
379
+ if (rollbackErrors.length > 0) {
380
+ throw new AggregateError(
381
+ [error, ...rollbackErrors],
382
+ "Failed to materialize model profile and fully restore prior state",
383
+ );
384
+ }
385
+ throw error;
386
+ }
168
387
  }
169
388
 
170
389
  export function materializeActiveModelProfileAssignment(options: MaterializeModelProfileAssignmentOptions): boolean {
@@ -188,15 +407,9 @@ export function materializeActiveModelProfileAssignment(options: MaterializeMode
188
407
  nextAgentModelOverrides[options.role] = options.selector;
189
408
  }
190
409
 
191
- options.settings.set("modelRoles", nextModelRoles);
192
- options.settings.set("task.agentModelOverrides", nextAgentModelOverrides);
193
- options.settings.unset("modelProfile.default");
194
- options.settings.clearOverride("modelProfile.default");
195
- options.settings.override("modelRoles", nextModelRoles);
196
- options.settings.override("task.agentModelOverrides", nextAgentModelOverrides);
197
- options.session.setActiveModelProfile?.(undefined);
198
- options.session.clearProfileInstalledOverrides?.();
199
- return true;
410
+ const committed = commitMaterializedProfileAssignments(options, nextModelRoles, nextAgentModelOverrides);
411
+ if (committed) options.session.clearProfileInstalledOverrides?.();
412
+ return committed;
200
413
  }
201
414
 
202
415
  export function materializeActiveModelProfileAssignments(options: MaterializeModelProfileAssignmentsOptions): boolean {
@@ -224,27 +437,23 @@ export function materializeActiveModelProfileAssignments(options: MaterializeMod
224
437
  }
225
438
  }
226
439
 
227
- options.settings.set("modelRoles", nextModelRoles);
228
- options.settings.set("task.agentModelOverrides", nextAgentModelOverrides);
229
- options.settings.unset("modelProfile.default");
230
- options.settings.clearOverride("modelProfile.default");
231
- options.settings.override("modelRoles", nextModelRoles);
232
- options.settings.override("task.agentModelOverrides", nextAgentModelOverrides);
233
- options.session.setActiveModelProfile?.(undefined);
234
- options.session.clearProfileInstalledOverrides?.();
235
- return true;
440
+ const committed = commitMaterializedProfileAssignments(options, nextModelRoles, nextAgentModelOverrides);
441
+ if (committed) options.session.clearProfileInstalledOverrides?.();
442
+ return committed;
236
443
  }
237
444
 
238
445
  export class ModelProfileCredentialError extends Error {
239
446
  readonly code = "authentication_failed";
240
447
  readonly profileLabel: string;
241
448
  readonly providers: readonly string[];
449
+ readonly role: string | undefined;
242
450
 
243
- constructor(profileLabel: string, providers: readonly string[]) {
451
+ constructor(profileLabel: string, providers: readonly string[], role?: string) {
244
452
  super(formatModelProfileCredentialError(profileLabel, providers));
245
453
  this.name = "ModelProfileCredentialError";
246
454
  this.profileLabel = profileLabel;
247
455
  this.providers = [...providers];
456
+ this.role = role;
248
457
  }
249
458
  }
250
459
 
@@ -319,39 +528,336 @@ function rewriteBindingsProviders(
319
528
  ),
320
529
  };
321
530
  }
531
+ /**
532
+ * Resolve the explicitly configured OpenAI-compatible proxy provider id for a
533
+ * preset. Returns undefined when unset or empty. Passwords/labels are never
534
+ * treated as proxy ids here; only lowercase provider ids from settings.
535
+ */
536
+ function resolveProxyProviderId(settings: Pick<Settings, "get" | "getGlobal" | "getOverride">): string | undefined {
537
+ const value = settings.get("modelProfile.proxyProvider");
538
+ if (typeof value !== "string" || value.trim() === "") return undefined;
539
+ const id = value.trim().toLowerCase();
540
+ if (!/^[a-z0-9][a-z0-9._-]*$/.test(id)) {
541
+ throw new Error(
542
+ `modelProfile.proxyProvider must be a lowercase provider id (got "${value.trim()}"). Configure an OpenAI-compatible proxy with \`gjc setup provider\`, then set its id here.`,
543
+ );
544
+ }
545
+ return id;
546
+ }
547
+
548
+ function resolveProxyMode(settings: Pick<Settings, "get" | "getGlobal" | "getOverride">): "fallback" | "always" {
549
+ const value = settings.get("modelProfile.proxyMode");
550
+ if (value === undefined || value === "fallback" || value === "always") return value ?? "fallback";
551
+ throw new Error(`modelProfile.proxyMode must be "fallback" or "always" (got "${String(value)}")`);
552
+ }
553
+
554
+ /**
555
+ * Rewrite a qualified provider selector so its model is served through the
556
+ * configured proxy. The proxy catalog uses sub-provider-prefixed model ids
557
+ * (e.g. `xiaomi/mimo-v2.5-pro` under `litellm`). Exact provider-prefixed
558
+ * matches win; a unique final-segment match is accepted for gateways that do
559
+ * not retain the upstream provider prefix. Missing or ambiguous matches fail
560
+ * closed rather than leaving an unauthenticated role selector behind.
561
+ */
562
+ function rewriteSelectorForProxy(
563
+ selector: string,
564
+ proxyProvider: string,
565
+ proxyMode: "fallback" | "always",
566
+ allModels: Model<Api>[],
567
+ directlyAuthenticated: ReadonlySet<string>,
568
+ ): string {
569
+ const suffix = splitSelectorThinkingSuffix(selector);
570
+ const baseSelector = suffix.selector;
571
+ const slash = baseSelector.indexOf("/");
572
+ const proxyModels = allModels.filter(model => model.provider === proxyProvider);
573
+ if (slash < 0) {
574
+ if (proxyMode === "fallback") return selector;
575
+ const matches = proxyModels.filter(model => model.id === baseSelector);
576
+ if (matches.length !== 1) {
577
+ throw new Error(
578
+ `Configured proxy "${proxyProvider}" does not expose an unambiguous model for "${baseSelector}"`,
579
+ );
580
+ }
581
+ const rewritten = `${proxyProvider}/${matches[0]!.id}`;
582
+ return suffix.thinkingLevel ? formatModelSelectorValue(rewritten, suffix.thinkingLevel) : rewritten;
583
+ }
584
+ const directProvider = baseSelector.substring(0, slash);
585
+ if (proxyMode === "fallback" && directlyAuthenticated.has(directProvider)) return selector;
586
+ if (!PROXY_ROUTABLE_PROVIDER_IDS.has(directProvider)) return selector;
587
+ if (proxyProvider === directProvider) {
588
+ throw new Error(`Configured proxy "${proxyProvider}" cannot route its own direct selector "${baseSelector}"`);
589
+ }
590
+ const directModelId = baseSelector.substring(slash + 1);
591
+ const exactMatches = proxyModels.filter(model => model.id === `${directProvider}/${directModelId}`);
592
+ const flatMatches = proxyModels.filter(model => model.id === directModelId);
593
+ const matches = exactMatches.length > 0 ? exactMatches : flatMatches;
594
+ if (matches.length === 0) {
595
+ throw new Error(`Configured proxy "${proxyProvider}" does not expose a model for "${baseSelector}"`);
596
+ }
597
+ if (matches.length > 1) {
598
+ throw new Error(`Configured proxy "${proxyProvider}" has ambiguous models for "${baseSelector}"`);
599
+ }
600
+ const rewritten = `${proxyProvider}/${matches[0]!.id}`;
601
+ return suffix.thinkingLevel ? formatModelSelectorValue(rewritten, suffix.thinkingLevel) : rewritten;
602
+ }
603
+
604
+ function rewriteSelectorValueForProxy(
605
+ selectorValue: ModelSelectorValue,
606
+ proxyProvider: string,
607
+ proxyMode: "fallback" | "always",
608
+ allModels: Model<Api>[],
609
+ directlyAuthenticated: ReadonlySet<string>,
610
+ ): ModelSelectorValue {
611
+ const selectors = normalizeModelSelectorValue(selectorValue).map(selector =>
612
+ rewriteSelectorForProxy(selector, proxyProvider, proxyMode, allModels, directlyAuthenticated),
613
+ );
614
+ return selectors.length === 1 && typeof selectorValue === "string" ? selectors[0] : selectors;
615
+ }
616
+
617
+ function rewriteBindingsForProxy(
618
+ bindings: {
619
+ defaultSelector?: ModelSelectorValue;
620
+ modelRoles: Record<string, ModelSelectorValue>;
621
+ agentModelOverrides: Record<string, ModelSelectorValue>;
622
+ },
623
+ proxyProvider: string,
624
+ proxyMode: "fallback" | "always",
625
+ allModels: Model<Api>[],
626
+ directlyAuthenticated: ReadonlySet<string>,
627
+ ): {
628
+ defaultSelector?: ModelSelectorValue;
629
+ modelRoles: Record<string, ModelSelectorValue>;
630
+ agentModelOverrides: Record<string, ModelSelectorValue>;
631
+ } {
632
+ return {
633
+ defaultSelector: bindings.defaultSelector
634
+ ? rewriteSelectorValueForProxy(
635
+ bindings.defaultSelector,
636
+ proxyProvider,
637
+ proxyMode,
638
+ allModels,
639
+ directlyAuthenticated,
640
+ )
641
+ : undefined,
642
+ modelRoles: Object.fromEntries(
643
+ Object.entries(bindings.modelRoles).map(([role, selector]) => [
644
+ role,
645
+ rewriteSelectorValueForProxy(selector, proxyProvider, proxyMode, allModels, directlyAuthenticated),
646
+ ]),
647
+ ),
648
+ agentModelOverrides: Object.fromEntries(
649
+ Object.entries(bindings.agentModelOverrides).map(([role, selector]) => [
650
+ role,
651
+ rewriteSelectorValueForProxy(selector, proxyProvider, proxyMode, allModels, directlyAuthenticated),
652
+ ]),
653
+ ),
654
+ };
655
+ }
322
656
 
323
657
  function formatMaterializedSelector(selector: string, model: Model<Api>): string {
658
+ const suffix = splitSelectorThinkingSuffix(selector);
659
+ if (!suffix.selector.includes("/") && suffix.thinkingLevel && model.thinking) {
660
+ const clamped = clampExplicitThinkingLevelForModel(model, suffix.thinkingLevel);
661
+ return clamped && clamped !== ThinkingLevel.Inherit
662
+ ? formatModelSelectorValue(suffix.selector, clamped)
663
+ : suffix.selector;
664
+ }
324
665
  const clampedSelector = formatClampedModelSelector(selector, model);
325
666
  const explicitThinkingLevel = parseModelString(selector)?.thinkingLevel;
326
667
  if (!explicitThinkingLevel || parseModelString(clampedSelector)?.thinkingLevel) return clampedSelector;
327
668
  return formatModelSelectorValue(clampedSelector, explicitThinkingLevel);
328
669
  }
670
+ function getBareSelectorCredentialProviders(selector: string, modelRegistry: ModelRegistry): string[] {
671
+ const suffix = splitSelectorThinkingSuffix(selector);
672
+ const alias = (suffix.thinkingLevel ? suffix.selector : selector).trim().toLowerCase();
673
+ const providers = modelRegistry
674
+ .getAll()
675
+ .filter(model => {
676
+ const modelId = model.id.trim().toLowerCase();
677
+ return modelId === alias || modelId.slice(modelId.lastIndexOf("/") + 1) === alias;
678
+ })
679
+ .map(model => model.provider);
680
+ return [...new Set(providers)];
681
+ }
329
682
 
330
- function resolveAndClampSelectorValue(
683
+ async function resolveAndClampSelectorValue(
331
684
  selectorValue: ModelSelectorValue,
332
685
  availableModels: Model<Api>[],
333
- options: { settings: Settings; modelRegistry: ModelRegistry },
686
+ options: {
687
+ settings: Settings;
688
+ modelRegistry: ModelRegistry;
689
+ sessionId: string;
690
+ credentialSessionId: string;
691
+ aliasIntent: "preset-equivalent";
692
+ requireQualifiedResolution?: boolean;
693
+ },
334
694
  profileLabel: string,
335
695
  role: string,
336
- ): ModelSelectorValue {
696
+ ): Promise<ModelSelectorValue> {
337
697
  const selectors = normalizeModelSelectorValue(selectorValue);
338
698
  const clamped: string[] = [];
699
+ const unresolvedKnownBareProviders = new Set<string>();
700
+ const unresolvedQualifiedSelectors: string[] = [];
701
+ let everySelectorIsKnownBare = selectors.length > 1;
339
702
  let resolvedAny = false;
340
703
  for (const selector of selectors) {
341
- const resolved = resolveModelRoleValue(selector, availableModels, options);
342
- if (resolved.model) {
343
- clamped.push(formatMaterializedSelector(selector, resolved.model));
344
- resolvedAny = true;
345
- } else {
704
+ const bareAlias = !splitSelectorThinkingSuffix(selector).selector.includes("/");
705
+ let resolved = resolveModelRoleValue(selector, availableModels, options);
706
+ if (bareAlias) {
707
+ const authenticated = await resolveModelChainWithAuth(
708
+ [selector],
709
+ {
710
+ getAvailable: () => availableModels,
711
+ getApiKey: model =>
712
+ options.modelRegistry.getApiKeyForProvider(
713
+ model.provider,
714
+ options.credentialSessionId,
715
+ model.baseUrl,
716
+ ),
717
+ resolveCanonicalModel: options.modelRegistry.resolveCanonicalModel.bind(options.modelRegistry),
718
+ resolveModelByLookupAlias: options.modelRegistry.resolveModelByLookupAlias?.bind(options.modelRegistry),
719
+ lookupAliasExists: options.modelRegistry.lookupAliasExists?.bind(options.modelRegistry),
720
+ clearCanonicalVariant: options.modelRegistry.clearCanonicalVariant?.bind(options.modelRegistry),
721
+ },
722
+ options.settings,
723
+ options.credentialSessionId,
724
+ {
725
+ managedFallback: true,
726
+ aliasIntent: options.aliasIntent,
727
+ canonicalSessionId: options.sessionId,
728
+ credentialSessionId: options.credentialSessionId,
729
+ },
730
+ );
731
+ resolved = {
732
+ model: authenticated.model,
733
+ thinkingLevel: authenticated.thinkingLevel,
734
+ explicitThinkingLevel: authenticated.explicitThinkingLevel,
735
+ warning: undefined,
736
+ };
737
+ }
738
+ if (!resolved.model) {
739
+ if (bareAlias) {
740
+ const providers = getBareSelectorCredentialProviders(selector, options.modelRegistry);
741
+ const selectorSuffix = splitSelectorThinkingSuffix(selector);
742
+ const bareSelector = selectorSuffix.thinkingLevel ? selectorSuffix.selector : selector;
743
+ const aliasKnown =
744
+ options.modelRegistry.lookupAliasExists?.(bareSelector.toLowerCase()) ?? providers.length > 0;
745
+ if (selectors.length === 1) {
746
+ if (!aliasKnown) {
747
+ throw new Error(
748
+ `Model profile "${profileLabel}" ${role} selector "${bareSelector}" does not match any catalog model`,
749
+ );
750
+ }
751
+ throw new ModelProfileCredentialError(
752
+ profileLabel,
753
+ providers.length > 0 ? providers : [bareSelector],
754
+ role,
755
+ );
756
+ }
757
+ if (aliasKnown) {
758
+ for (const provider of providers.length > 0 ? providers : [bareSelector]) {
759
+ unresolvedKnownBareProviders.add(provider);
760
+ }
761
+ } else {
762
+ everySelectorIsKnownBare = false;
763
+ }
764
+ } else {
765
+ unresolvedQualifiedSelectors.push(selector);
766
+ everySelectorIsKnownBare = false;
767
+ }
346
768
  clamped.push(selector);
769
+ continue;
347
770
  }
771
+ resolvedAny = true;
772
+ clamped.push(formatMaterializedSelector(selector, resolved.model));
348
773
  }
349
- if (!resolvedAny && role === "default") {
350
- throw new Error(`Model profile "${profileLabel}" ${role} selector did not resolve: ${selectors[0]}`);
774
+ if (!resolvedAny && everySelectorIsKnownBare && unresolvedKnownBareProviders.size > 0) {
775
+ throw new ModelProfileCredentialError(profileLabel, [...unresolvedKnownBareProviders].sort(), role);
776
+ }
777
+ if (options.requireQualifiedResolution && !resolvedAny && unresolvedQualifiedSelectors.length > 0) {
778
+ throw new Error(
779
+ `Model profile "${profileLabel}" ${role} selectors do not match any catalog model: ${unresolvedQualifiedSelectors.join(", ")}`,
780
+ );
351
781
  }
352
782
  return clamped.length === 1 && typeof selectorValue === "string" ? clamped[0] : clamped;
353
783
  }
354
784
 
785
+ async function concretizeProfileSelectorValue(
786
+ selectorValue: ModelSelectorValue,
787
+ prepared: PreparedModelProfileActivation,
788
+ ): Promise<ModelSelectorValue> {
789
+ const candidates = prepared.modelRegistry.getAvailable?.() ?? prepared.modelRegistry.getAll();
790
+ const credentialSessionId = prepared.session.credentialSessionId ?? prepared.session.sessionId;
791
+ const concrete = await Promise.all(
792
+ normalizeModelSelectorValue(selectorValue).map(async selector => {
793
+ const bareAlias = !splitSelectorThinkingSuffix(selector).selector.includes("/");
794
+ const resolved = bareAlias
795
+ ? await resolveModelChainWithAuth(
796
+ [selector],
797
+ {
798
+ getAvailable: () => candidates,
799
+ getApiKey: model =>
800
+ prepared.modelRegistry.getApiKeyForProvider(model.provider, credentialSessionId, model.baseUrl),
801
+ resolveCanonicalModel: prepared.modelRegistry.resolveCanonicalModel.bind(prepared.modelRegistry),
802
+ resolveModelByLookupAlias: prepared.modelRegistry.resolveModelByLookupAlias?.bind(
803
+ prepared.modelRegistry,
804
+ ),
805
+ lookupAliasExists: prepared.modelRegistry.lookupAliasExists?.bind(prepared.modelRegistry),
806
+ clearCanonicalVariant: prepared.modelRegistry.clearCanonicalVariant?.bind(prepared.modelRegistry),
807
+ },
808
+ prepared.settings as Settings,
809
+ credentialSessionId,
810
+ {
811
+ managedFallback: true,
812
+ aliasIntent: "preset-equivalent",
813
+ canonicalSessionId: prepared.session.sessionId,
814
+ credentialSessionId,
815
+ },
816
+ )
817
+ : resolveModelRoleValue(selector, candidates, {
818
+ settings: prepared.settings as Settings,
819
+ modelRegistry: prepared.modelRegistry,
820
+ sessionId: prepared.session.sessionId,
821
+ credentialSessionId,
822
+ aliasIntent: "preset-equivalent",
823
+ });
824
+ if (!resolved.model) {
825
+ if (bareAlias) return undefined;
826
+ return selector;
827
+ }
828
+ const concreteSelector = `${resolved.model.provider}/${resolved.model.id}`;
829
+ return resolved.explicitThinkingLevel && resolved.thinkingLevel
830
+ ? formatModelSelectorValue(concreteSelector, resolved.thinkingLevel)
831
+ : concreteSelector;
832
+ }),
833
+ );
834
+ const resolvedConcrete = concrete.filter((selector): selector is string => selector !== undefined);
835
+ if (resolvedConcrete.length === 0) {
836
+ throw new Error(
837
+ `Model profile deletion could not concretize authenticated selector: ${normalizeModelSelectorValue(selectorValue)[0]}`,
838
+ );
839
+ }
840
+ return resolvedConcrete.length === 1 && typeof selectorValue === "string" ? resolvedConcrete[0]! : resolvedConcrete;
841
+ }
842
+
843
+ /**
844
+ * Restore the session's canonical sticky variant after a failed activation.
845
+ * The exact pre-clear sticky selector (snapshotted verbatim, never re-derived
846
+ * from the live model) is restored when present; otherwise any stale sticky
847
+ * variant is cleared so a previous provider cannot silently resurrect.
848
+ */
849
+ function restoreCanonicalVariant(
850
+ modelRegistry: PrepareModelProfileActivationOptions["modelRegistry"],
851
+ sessionId: string,
852
+ previousCanonicalVariant: string | undefined,
853
+ ): void {
854
+ if (previousCanonicalVariant !== undefined) {
855
+ const restored = modelRegistry.restoreSessionCanonicalVariant?.(sessionId, previousCanonicalVariant) === true;
856
+ if (!restored) modelRegistry.clearCanonicalVariant?.(sessionId);
857
+ } else {
858
+ modelRegistry.clearCanonicalVariant?.(sessionId);
859
+ }
860
+ }
355
861
  export async function prepareModelProfileActivation(
356
862
  options: PrepareModelProfileActivationOptions,
357
863
  ): Promise<PreparedModelProfileActivation> {
@@ -360,160 +866,277 @@ export async function prepareModelProfileActivation(
360
866
  const profile = profiles.get(profileName) ?? options.modelRegistry.getModelProfile(profileName)!;
361
867
  const profileLabel = formatModelProfileDisplayLabel(profile);
362
868
 
363
- const requiredProviders = aggregateModelProfileRequiredProviders(profile.requiredProviders, profile);
364
- const alternativeGroups = profile.alternativeProviderGroups ?? [];
365
- const alternativeSet = new Set(alternativeGroups.flat());
869
+ const previousModel = options.session.model;
870
+ // Snapshot the exact pre-clear sticky selector (verbatim, not re-derived from
871
+ // the live model) so a failed prepare/apply/materialize rollback restores the
872
+ // genuinely-sticky provider even when the live model is a transient switch.
873
+ const credentialSessionId = options.session.credentialSessionId ?? options.session.sessionId;
874
+ const previousCanonicalVariant = options.modelRegistry.getSessionCanonicalVariant?.(options.session.sessionId);
366
875
 
367
- const missingProviders: string[] = [];
368
- const authenticatedProviders: string[] = [];
369
- for (const provider of requiredProviders) {
370
- const apiKey = await options.modelRegistry.getApiKeyForProvider(provider, options.session.sessionId);
371
- if (apiKey !== kNoAuth && !isAuthenticated(apiKey)) {
372
- missingProviders.push(provider);
373
- } else {
374
- authenticatedProviders.push(provider);
375
- }
376
- }
876
+ // Explicit profile activation/reselection invalidates the session's sticky
877
+ // canonical variant BEFORE the new profile's aliases resolve, so the old
878
+ // provider's sticky variant cannot win the new profile's resolution.
879
+ options.modelRegistry.clearCanonicalVariant?.(options.session.sessionId);
377
880
 
378
- // Required providers are the only activation prerequisites. Mapped fallback
379
- // providers are resolution-time candidates and intentionally do not gate here.
380
- const strictMissing = missingProviders.filter(provider => !alternativeSet.has(provider));
381
- if (strictMissing.length > 0) {
382
- throw new ModelProfileCredentialError(profileLabel, strictMissing);
383
- }
384
- for (const group of alternativeGroups) {
385
- const groupAuthenticated = group.some(provider => authenticatedProviders.includes(provider));
386
- if (!groupAuthenticated) {
387
- throw new ModelProfileCredentialError(profileLabel, [...group]);
881
+ try {
882
+ const requiredProviders = aggregateModelProfileRequiredProviders(profile.requiredProviders, profile);
883
+ const alternativeGroups = profile.alternativeProviderGroups ?? [];
884
+ const alternativeSet = new Set(alternativeGroups.flat());
885
+
886
+ const missingProviders: string[] = [];
887
+ const authenticatedProviders: string[] = [];
888
+ for (const provider of requiredProviders) {
889
+ const apiKey = await options.modelRegistry.getApiKeyForProvider(provider, credentialSessionId);
890
+ if (apiKey !== kNoAuth && !isAuthenticated(apiKey)) {
891
+ missingProviders.push(provider);
892
+ } else {
893
+ authenticatedProviders.push(provider);
894
+ }
388
895
  }
389
- }
390
896
 
391
- const availableModels = options.modelRegistry.getAll();
392
- let bindings = resolveProfileBindings(profile);
393
- if (missingProviders.length > 0 && alternativeGroups.length > 0) {
394
- bindings = rewriteBindingsProviders(bindings, new Set(authenticatedProviders), alternativeGroups);
395
- }
396
- const defaultSelectors = bindings.defaultSelector ? normalizeModelSelectorValue(bindings.defaultSelector) : [];
397
- const defaultChain =
398
- defaultSelectors.length > 0
399
- ? normalizeModelSelectorValue(
400
- resolveAndClampSelectorValue(
401
- bindings.defaultSelector!,
402
- availableModels,
403
- { settings: options.settings as Settings, modelRegistry: options.modelRegistry as ModelRegistry },
404
- profileLabel,
405
- "default",
406
- ),
407
- )
408
- : [];
409
- const defaultResolution = await resolveModelChainWithAuth(
410
- defaultChain,
411
- {
412
- ...options.modelRegistry,
413
- getAvailable: () => availableModels,
414
- getApiKey: (model, sessionId) =>
415
- options.modelRegistry.getApiKeyForProvider(model.provider, sessionId, model.baseUrl),
416
- } as ModelRegistry,
417
- options.settings as Settings,
418
- options.session.sessionId,
419
- { managedFallback: true },
420
- );
421
- const defaultModel = defaultResolution.model;
422
- const defaultThinkingLevel = defaultResolution.thinkingLevel;
423
- const defaultActiveIndex = defaultModel ? defaultResolution.activeIndex : undefined;
424
- const defaultResolutionSkips = defaultResolution.skips;
425
- if (bindings.defaultSelector && !defaultModel) {
426
- throw new Error(`Model profile "${profileLabel}" default selectors did not resolve to an authenticated model`);
427
- }
897
+ // Required providers are the only activation prerequisites. Mapped fallback
898
+ // providers are resolution-time candidates and intentionally do not gate here.
899
+ // A proxy-routable strict provider is satisfied through the configured
900
+ // OpenAI-compatible proxy when that proxy is itself authenticated; otherwise
901
+ // we fail closed pointing at the proxy (or the provider when none is set).
902
+ const proxyProvider = profile.source === "builtin" ? resolveProxyProviderId(options.settings) : undefined;
903
+ const proxyMode = profile.source === "builtin" ? resolveProxyMode(options.settings) : "fallback";
904
+ if (proxyMode === "always" && proxyProvider === undefined) {
905
+ throw new Error('modelProfile.proxyMode "always" requires modelProfile.proxyProvider');
906
+ }
907
+ if (proxyProvider !== undefined) {
908
+ const configuredProxyProviders = options.modelRegistry.getConfiguredProviderIds?.();
909
+ if (!configuredProxyProviders?.includes(proxyProvider)) {
910
+ throw new Error(
911
+ `modelProfile.proxyProvider "${proxyProvider}" is not configured. Configure it with \`gjc setup provider\` before activating a preset.`,
912
+ );
913
+ }
914
+ }
915
+ const proxyApiKey =
916
+ proxyProvider === undefined
917
+ ? undefined
918
+ : await options.modelRegistry.getApiKeyForProvider(proxyProvider, credentialSessionId);
919
+ const proxyAuthenticated =
920
+ proxyProvider !== undefined &&
921
+ proxyApiKey !== undefined &&
922
+ (proxyApiKey === kNoAuth || isAuthenticated(proxyApiKey));
923
+ if (proxyMode === "always" && !proxyAuthenticated) {
924
+ throw new ModelProfileCredentialError(profileLabel, [proxyProvider!]);
925
+ }
428
926
 
429
- const modelRoles: Record<string, ModelSelectorValue> = {};
430
- for (const [role, selectorValue] of Object.entries(bindings.modelRoles) as [
431
- GjcModelAssignmentTargetId,
432
- ModelSelectorValue,
433
- ][]) {
434
- modelRoles[role] = resolveAndClampSelectorValue(
435
- selectorValue,
436
- availableModels,
437
- { settings: options.settings as Settings, modelRegistry: options.modelRegistry as ModelRegistry },
438
- profileLabel,
439
- role,
927
+ const strictMissing = missingProviders.filter(
928
+ provider => !PROXY_ROUTABLE_PROVIDER_IDS.has(provider) && !alternativeSet.has(provider),
440
929
  );
441
- }
930
+ if (strictMissing.length > 0) {
931
+ throw new ModelProfileCredentialError(profileLabel, strictMissing);
932
+ }
933
+ const strictRoutableMissing = missingProviders.filter(
934
+ provider => PROXY_ROUTABLE_PROVIDER_IDS.has(provider) && !alternativeSet.has(provider),
935
+ );
936
+ if (strictRoutableMissing.length > 0 && (proxyProvider === undefined || !proxyAuthenticated)) {
937
+ throw new ModelProfileCredentialError(
938
+ profileLabel,
939
+ proxyProvider === undefined ? strictRoutableMissing : [proxyProvider],
940
+ );
941
+ }
942
+ for (const group of alternativeGroups) {
943
+ const groupAuthenticated = group.some(provider => authenticatedProviders.includes(provider));
944
+ if (groupAuthenticated) continue;
945
+ const allRoutable = group.every(provider => PROXY_ROUTABLE_PROVIDER_IDS.has(provider));
946
+ if (allRoutable && proxyAuthenticated) continue;
947
+ throw new ModelProfileCredentialError(
948
+ profileLabel,
949
+ allRoutable && proxyProvider !== undefined ? [proxyProvider] : [...group],
950
+ );
951
+ }
442
952
 
443
- const agentModelOverrides: Record<string, ModelSelectorValue> = {};
444
- for (const [role, selectorValue] of Object.entries(bindings.agentModelOverrides) as [
445
- GjcModelAssignmentTargetId,
446
- ModelSelectorValue,
447
- ][]) {
448
- agentModelOverrides[role] = resolveAndClampSelectorValue(
449
- selectorValue,
450
- availableModels,
451
- { settings: options.settings as Settings, modelRegistry: options.modelRegistry as ModelRegistry },
452
- profileLabel,
453
- role,
953
+ const availableModels = options.modelRegistry.getAvailable?.() ?? options.modelRegistry.getAll();
954
+ let bindings = resolveProfileBindings(profile);
955
+ if (missingProviders.length > 0 && alternativeGroups.length > 0) {
956
+ bindings = rewriteBindingsProviders(bindings, new Set(authenticatedProviders), alternativeGroups);
957
+ }
958
+ // Built-in preset selectors are routed through a configured authenticated
959
+ // proxy according to the selected mode. This session-scoped rewrite is never
960
+ // persisted to models.yml.
961
+ if (proxyProvider !== undefined && proxyAuthenticated && profile.source === "builtin") {
962
+ bindings = rewriteBindingsForProxy(
963
+ bindings,
964
+ proxyProvider,
965
+ proxyMode,
966
+ availableModels,
967
+ new Set(authenticatedProviders),
968
+ );
969
+ }
970
+ const defaultSelectors = bindings.defaultSelector ? normalizeModelSelectorValue(bindings.defaultSelector) : [];
971
+ const defaultChain =
972
+ defaultSelectors.length > 0
973
+ ? normalizeModelSelectorValue(
974
+ await resolveAndClampSelectorValue(
975
+ bindings.defaultSelector!,
976
+ availableModels,
977
+ {
978
+ settings: options.settings as Settings,
979
+ modelRegistry: options.modelRegistry as ModelRegistry,
980
+ sessionId: options.session.sessionId,
981
+ credentialSessionId,
982
+ aliasIntent: "preset-equivalent",
983
+ },
984
+ profileLabel,
985
+ "default",
986
+ ),
987
+ )
988
+ : [];
989
+ const defaultResolution = await resolveModelChainWithAuth(
990
+ defaultChain,
991
+ {
992
+ getAvailable: () => availableModels,
993
+ getApiKey: (model, sessionId) =>
994
+ options.modelRegistry.getApiKeyForProvider(model.provider, sessionId, model.baseUrl),
995
+ resolveCanonicalModel: options.modelRegistry.resolveCanonicalModel?.bind(options.modelRegistry),
996
+ getCanonicalVariants: options.modelRegistry.getCanonicalVariants?.bind(options.modelRegistry),
997
+ getCanonicalId: options.modelRegistry.getCanonicalId?.bind(options.modelRegistry),
998
+ resolveModelByLookupAlias: options.modelRegistry.resolveModelByLookupAlias?.bind(options.modelRegistry),
999
+ lookupAliasExists: options.modelRegistry.lookupAliasExists?.bind(options.modelRegistry),
1000
+ clearCanonicalVariant: options.modelRegistry.clearCanonicalVariant?.bind(options.modelRegistry),
1001
+ } as ModelRegistry,
1002
+ options.settings as Settings,
1003
+ credentialSessionId,
1004
+ {
1005
+ managedFallback: true,
1006
+ aliasIntent: "preset-equivalent",
1007
+ canonicalSessionId: options.session.sessionId,
1008
+ credentialSessionId,
1009
+ },
454
1010
  );
455
- }
1011
+ const defaultModel = defaultResolution.model;
1012
+ const defaultThinkingLevel = defaultResolution.thinkingLevel;
1013
+ const defaultActiveIndex = defaultModel ? defaultResolution.activeIndex : undefined;
1014
+ const defaultResolutionSkips = defaultResolution.skips;
1015
+ if (bindings.defaultSelector && !defaultModel) {
1016
+ const configuredDefaults = normalizeModelSelectorValue(bindings.defaultSelector);
1017
+ if (configuredDefaults.length === 1) {
1018
+ throw new Error(
1019
+ `Model profile "${profileLabel}" default selector did not resolve: ${configuredDefaults[0]}`,
1020
+ );
1021
+ }
1022
+ throw new Error(`Model profile "${profileLabel}" default selectors did not resolve to an authenticated model`);
1023
+ }
456
1024
 
457
- return {
458
- profileName,
459
- session: options.session as PreparedModelProfileActivation["session"],
460
- settings: options.settings as PreparedModelProfileActivation["settings"],
461
- previousModel: options.session.model,
462
- previousThinkingLevel: options.session.thinkingLevel,
463
- previousAgentModelOverrides: { ...options.settings.get("task.agentModelOverrides") },
464
- previousModelRoles: { ...options.settings.get("modelRoles") },
465
- // The activation replaces only the previously installed profile-owned
466
- // keys: derive the base from the effective map (durable + project +
467
- // bindings + non-profile overrides) minus those keys, so project- or
468
- // globally-scoped role overrides the new profile omits are preserved.
469
- baseAgentModelOverrides: Object.fromEntries(
470
- Object.entries(options.settings.get("task.agentModelOverrides") ?? {}).filter(
471
- ([key]) => !(options.session.getProfileInstalledOverrideKeys?.().agentModelOverrides ?? []).includes(key),
1025
+ const modelRoles: Record<string, ModelSelectorValue> = {};
1026
+ for (const [role, selectorValue] of Object.entries(bindings.modelRoles) as [
1027
+ GjcModelAssignmentTargetId,
1028
+ ModelSelectorValue,
1029
+ ][]) {
1030
+ modelRoles[role] = await resolveAndClampSelectorValue(
1031
+ selectorValue,
1032
+ availableModels,
1033
+ {
1034
+ settings: options.settings as Settings,
1035
+ modelRegistry: options.modelRegistry as ModelRegistry,
1036
+ sessionId: options.session.sessionId,
1037
+ credentialSessionId,
1038
+ aliasIntent: "preset-equivalent",
1039
+ requireQualifiedResolution: profile.source !== "builtin",
1040
+ },
1041
+ profileLabel,
1042
+ role,
1043
+ );
1044
+ }
1045
+
1046
+ const agentModelOverrides: Record<string, ModelSelectorValue> = {};
1047
+ for (const [role, selectorValue] of Object.entries(bindings.agentModelOverrides) as [
1048
+ GjcModelAssignmentTargetId,
1049
+ ModelSelectorValue,
1050
+ ][]) {
1051
+ agentModelOverrides[role] = await resolveAndClampSelectorValue(
1052
+ selectorValue,
1053
+ availableModels,
1054
+ {
1055
+ settings: options.settings as Settings,
1056
+ modelRegistry: options.modelRegistry as ModelRegistry,
1057
+ sessionId: options.session.sessionId,
1058
+ credentialSessionId,
1059
+ aliasIntent: "preset-equivalent",
1060
+ requireQualifiedResolution: profile.source !== "builtin",
1061
+ },
1062
+ profileLabel,
1063
+ role,
1064
+ );
1065
+ }
1066
+
1067
+ return {
1068
+ profileName,
1069
+ session: options.session as PreparedModelProfileActivation["session"],
1070
+ settings: options.settings as PreparedModelProfileActivation["settings"],
1071
+ modelRegistry: options.modelRegistry,
1072
+ previousModel,
1073
+ previousCanonicalVariant,
1074
+ previousThinkingLevel: options.session.thinkingLevel,
1075
+ previousAgentModelOverrides: { ...options.settings.get("task.agentModelOverrides") },
1076
+ previousModelRoles: { ...options.settings.get("modelRoles") },
1077
+ baseAgentModelOverrides: Object.fromEntries(
1078
+ Object.entries(options.settings.get("task.agentModelOverrides") ?? {}).filter(
1079
+ ([key]) =>
1080
+ !(options.session.getProfileInstalledOverrideKeys?.().agentModelOverrides ?? []).includes(key),
1081
+ ),
472
1082
  ),
473
- ),
474
- baseModelRoles: Object.fromEntries(
475
- Object.entries(options.settings.get("modelRoles") ?? {}).filter(
476
- ([key]) => !(options.session.getProfileInstalledOverrideKeys?.().modelRoles ?? []).includes(key),
1083
+ baseModelRoles: Object.fromEntries(
1084
+ Object.entries(options.settings.get("modelRoles") ?? {}).filter(
1085
+ ([key]) => !(options.session.getProfileInstalledOverrideKeys?.().modelRoles ?? []).includes(key),
1086
+ ),
477
1087
  ),
478
- ),
479
- previousDefaultChain: options.session.getConfiguredModelChain("default"),
480
-
481
- defaultModel,
482
- defaultThinkingLevel,
483
- defaultActiveIndex,
484
- defaultResolutionSkips,
485
- defaultChain,
486
- modelRoles,
487
- agentModelOverrides,
488
- previousActiveModelProfile: options.session.getActiveModelProfile?.(),
489
- previousSessionDefaultModel: options.session.getSessionDefaultModelSelector?.(),
490
- };
1088
+ previousPersistedModelRoles: options.settings.getGlobal("modelRoles"),
1089
+ previousPersistedAgentModelOverrides: options.settings.getGlobal("task.agentModelOverrides"),
1090
+ previousModelRolesOverride: options.settings.getOverride("modelRoles"),
1091
+ previousAgentModelOverridesOverride: options.settings.getOverride("task.agentModelOverrides"),
1092
+ previousDefaultProfileOverride: options.settings.getOverride("modelProfile.default"),
1093
+ previousPersistedDefaultProfile: options.settings.getGlobal("modelProfile.default"),
1094
+ previousPersistedDefaultThinkingLevel: options.settings.getGlobal("defaultThinkingLevel") as
1095
+ | Exclude<ThinkingLevel, "inherit">
1096
+ | undefined,
1097
+ previousDefaultThinkingLevelOverride: options.settings.getOverride("defaultThinkingLevel"),
1098
+ previousDefaultChain: options.session.getConfiguredModelChain("default"),
1099
+ previousDefaultChainState: options.session.getConfiguredModelChainState?.("default"),
1100
+
1101
+ defaultModel,
1102
+ defaultThinkingLevel,
1103
+ defaultActiveIndex,
1104
+ defaultResolutionSkips,
1105
+ defaultChain,
1106
+ modelRoles,
1107
+ agentModelOverrides,
1108
+ previousActiveModelProfile: options.session.getActiveModelProfile?.(),
1109
+ previousSessionDefaultModel: options.session.getSessionDefaultModelSelector?.(),
1110
+ previousDefaultFallbackRuntimeState: options.session.getDefaultFallbackRuntimeState?.(),
1111
+ };
1112
+ } catch (error) {
1113
+ restoreCanonicalVariant(options.modelRegistry, options.session.sessionId, previousCanonicalVariant);
1114
+ throw error;
1115
+ }
491
1116
  }
492
1117
 
493
1118
  export async function applyPreparedModelProfileActivation(
494
1119
  prepared: PreparedModelProfileActivation,
495
1120
  options: ApplyModelProfileActivationOptions = {},
496
1121
  ): Promise<void> {
497
- const previousModel = prepared.previousModel;
498
- const previousThinkingLevel = prepared.previousThinkingLevel;
499
- const previousAgentModelOverrides = prepared.previousAgentModelOverrides;
500
- const previousModelRoles = prepared.previousModelRoles;
501
- const previousPersistedDefault = prepared.settings.getGlobal("modelProfile.default");
502
- const previousDefaultThinkingLevel = prepared.settings.get("defaultThinkingLevel");
503
- const previousActiveModelProfile = prepared.previousActiveModelProfile;
504
- const previousSessionDefaultModel = prepared.previousSessionDefaultModel;
505
- let modelChanged = false;
1122
+ let modelMutationStarted = false;
506
1123
  let overridesChanged = false;
507
- let defaultChanged = false;
508
1124
  let modelRolesChanged = false;
509
- let defaultThinkingChanged = false;
1125
+ let persistentMutationStarted = false;
510
1126
  let defaultChainChanged = false;
1127
+ let resumeDefaultChanged = false;
511
1128
 
512
1129
  try {
513
- if (prepared.defaultChain.length > 0) {
1130
+ const ownedDefaultChain =
1131
+ prepared.defaultChain.length > 0
1132
+ ? prepared.defaultChain
1133
+ : (prepared.previousDefaultChain ??
1134
+ (prepared.previousModel ? [formatModelString(prepared.previousModel)] : []));
1135
+ if (ownedDefaultChain.length > 0) {
1136
+ defaultChainChanged = true;
514
1137
  prepared.session.setConfiguredModelChain(
515
1138
  "default",
516
- prepared.defaultChain,
1139
+ ownedDefaultChain,
517
1140
  "profile-activation",
518
1141
  prepared.profileName,
519
1142
  true,
@@ -524,24 +1147,22 @@ export async function applyPreparedModelProfileActivation(
524
1147
  prepared.defaultResolutionSkips,
525
1148
  );
526
1149
  }
527
-
528
- defaultChainChanged = true;
529
1150
  }
530
1151
  if (prepared.defaultModel) {
1152
+ modelMutationStarted = true;
531
1153
  await prepared.session.setModelTemporary(
532
1154
  prepared.defaultModel,
533
1155
  options.thinkingLevelOverride ?? prepared.defaultThinkingLevel,
534
- {
535
- persistAsSessionDefault: true,
536
- cause: "profile-activation",
537
- },
1156
+ { cause: "profile-activation" },
538
1157
  );
539
- modelChanged = true;
540
- }
541
- if (Object.keys(prepared.modelRoles).length > 0) {
542
- prepared.settings.override("modelRoles", { ...prepared.baseModelRoles, ...prepared.modelRoles });
543
- modelRolesChanged = true;
544
1158
  }
1159
+ // Always reinstall the model role layer from the durable base plus the
1160
+ // new profile's roles so omitted roles from the previous profile are dropped.
1161
+ prepared.settings.override("modelRoles", {
1162
+ ...prepared.baseModelRoles,
1163
+ ...prepared.modelRoles,
1164
+ });
1165
+ modelRolesChanged = true;
545
1166
  // Always reinstall the agent role layer from the durable base plus the
546
1167
  // new profile's roles: a default-only or role-free successor must drop
547
1168
  // the previous profile's role-agent mappings rather than inheriting them.
@@ -551,68 +1172,128 @@ export async function applyPreparedModelProfileActivation(
551
1172
  });
552
1173
  overridesChanged = true;
553
1174
  if (options.persistDefault) {
1175
+ persistentMutationStarted = true;
554
1176
  prepared.settings.set("modelRoles", {});
555
1177
  prepared.settings.set("task.agentModelOverrides", {});
556
1178
  if (prepared.defaultThinkingLevel !== undefined && prepared.defaultThinkingLevel !== ThinkingLevel.Inherit) {
557
1179
  prepared.settings.set("defaultThinkingLevel", prepared.defaultThinkingLevel);
558
- defaultThinkingChanged = true;
559
1180
  }
560
1181
  prepared.settings.set("modelProfile.default", prepared.profileName);
561
- defaultChanged = true;
562
- await prepared.settings.flush();
1182
+ await prepared.settings.flushOrThrow();
563
1183
  }
564
1184
  prepared.session.setActiveModelProfile?.(prepared.profileName);
1185
+ if (prepared.defaultModel) {
1186
+ prepared.modelRegistry.seedCanonicalVariant?.(prepared.session.sessionId, prepared.defaultModel);
1187
+ resumeDefaultChanged = true;
1188
+ prepared.session.recordResumeDefaultModel?.(`${prepared.defaultModel.provider}/${prepared.defaultModel.id}`);
1189
+ }
565
1190
  prepared.session.noteProfileInstalledOverrides?.(
566
1191
  Object.keys(prepared.modelRoles),
567
1192
  Object.keys(prepared.agentModelOverrides),
568
- // Snapshotted before this activation replaced the runtime model.
569
- previousModel,
1193
+ prepared.previousModel,
570
1194
  );
571
1195
  } catch (error) {
572
- if (defaultChanged) {
573
- prepared.settings.set("modelProfile.default", previousPersistedDefault);
574
- prepared.settings.set("modelRoles", previousModelRoles);
575
- prepared.settings.set("task.agentModelOverrides", previousAgentModelOverrides);
576
- if (defaultThinkingChanged) {
577
- prepared.settings.set("defaultThinkingLevel", previousDefaultThinkingLevel);
1196
+ const rollbackErrors: unknown[] = [];
1197
+ const restore = (action: () => void): void => {
1198
+ try {
1199
+ action();
1200
+ } catch (rollbackError) {
1201
+ rollbackErrors.push(rollbackError);
578
1202
  }
1203
+ };
1204
+ if (persistentMutationStarted) {
1205
+ restore(() =>
1206
+ prepared.previousPersistedDefaultProfile === undefined
1207
+ ? prepared.settings.unset("modelProfile.default")
1208
+ : prepared.settings.set("modelProfile.default", prepared.previousPersistedDefaultProfile),
1209
+ );
1210
+ restore(() =>
1211
+ prepared.previousPersistedModelRoles === undefined
1212
+ ? prepared.settings.unset("modelRoles")
1213
+ : prepared.settings.set("modelRoles", prepared.previousPersistedModelRoles),
1214
+ );
1215
+ restore(() =>
1216
+ prepared.previousPersistedAgentModelOverrides === undefined
1217
+ ? prepared.settings.unset("task.agentModelOverrides")
1218
+ : prepared.settings.set("task.agentModelOverrides", prepared.previousPersistedAgentModelOverrides),
1219
+ );
1220
+ restore(() =>
1221
+ prepared.previousPersistedDefaultThinkingLevel === undefined
1222
+ ? prepared.settings.unset("defaultThinkingLevel")
1223
+ : prepared.settings.set("defaultThinkingLevel", prepared.previousPersistedDefaultThinkingLevel),
1224
+ );
579
1225
  }
580
1226
  if (modelRolesChanged) {
581
- prepared.settings.override("modelRoles", previousModelRoles);
1227
+ restore(() =>
1228
+ prepared.previousModelRolesOverride === undefined
1229
+ ? prepared.settings.clearOverride("modelRoles")
1230
+ : prepared.settings.override("modelRoles", prepared.previousModelRolesOverride),
1231
+ );
582
1232
  }
583
1233
  if (overridesChanged) {
584
- prepared.settings.override("task.agentModelOverrides", previousAgentModelOverrides);
1234
+ restore(() =>
1235
+ prepared.previousAgentModelOverridesOverride === undefined
1236
+ ? prepared.settings.clearOverride("task.agentModelOverrides")
1237
+ : prepared.settings.override("task.agentModelOverrides", prepared.previousAgentModelOverridesOverride),
1238
+ );
1239
+ }
1240
+ if (modelMutationStarted) {
1241
+ try {
1242
+ if (prepared.session.restoreModelSelectionForRollback) {
1243
+ await prepared.session.restoreModelSelectionForRollback(
1244
+ prepared.previousModel,
1245
+ prepared.previousThinkingLevel,
1246
+ );
1247
+ } else if (prepared.previousModel) {
1248
+ await prepared.session.setModelTemporary(prepared.previousModel, prepared.previousThinkingLevel, {
1249
+ cause: "rollback",
1250
+ });
1251
+ } else {
1252
+ throw new Error("Model-less profile activation rollback is unavailable");
1253
+ }
1254
+ } catch (rollbackError) {
1255
+ rollbackErrors.push(rollbackError);
1256
+ }
1257
+ }
1258
+ if (resumeDefaultChanged) {
1259
+ restore(() => prepared.session.recordResumeDefaultModel?.(prepared.previousSessionDefaultModel));
585
1260
  }
586
1261
  if (defaultChainChanged) {
587
- prepared.session.setConfiguredModelChain(
588
- "default",
589
- prepared.previousDefaultChain ?? [],
590
- "rollback",
591
- prepared.previousActiveModelProfile,
592
- true,
1262
+ const previousChain = prepared.previousDefaultChainState;
1263
+ restore(() =>
1264
+ prepared.session.setConfiguredModelChain(
1265
+ "default",
1266
+ previousChain?.entries ??
1267
+ prepared.previousDefaultChain ??
1268
+ (prepared.previousModel ? [`${prepared.previousModel.provider}/${prepared.previousModel.id}`] : []),
1269
+ previousChain?.origin ?? "rollback",
1270
+ previousChain?.identity,
1271
+ previousChain?.explicitHead ?? true,
1272
+ ),
593
1273
  );
594
1274
  }
595
- prepared.session.setActiveModelProfile?.(previousActiveModelProfile);
596
- if (modelChanged) {
597
- // Runtime rolls back to the pre-activation live model. That model may
598
- // itself be a transient retry/fallback/context-promotion/plan switch,
599
- // so it is recorded as role:"temporary" (NOT the resume default) to
600
- // preserve the issue #849 protection.
601
- if (previousModel) {
602
- await prepared.session.setModelTemporary(previousModel, previousThinkingLevel, { cause: "rollback" });
603
- }
604
- // The happy path already appended the profile main model as the resume
605
- // default (role:"default"). Re-assert the pre-activation resume default
606
- // so a failed activation does not poison future resume. Fall back to the
607
- // live model only when there was no explicit pre-activation default
608
- // (nothing to protect). Append-only — never touches the runtime model.
609
- const restoreDefaultSelector =
610
- previousSessionDefaultModel ??
611
- (previousModel ? `${previousModel.provider}/${previousModel.id}` : undefined);
612
- if (restoreDefaultSelector) {
613
- prepared.session.recordResumeDefaultModel?.(restoreDefaultSelector);
1275
+ if (prepared.previousDefaultFallbackRuntimeState) {
1276
+ restore(() =>
1277
+ prepared.session.restoreDefaultFallbackRuntimeState?.(prepared.previousDefaultFallbackRuntimeState!),
1278
+ );
1279
+ }
1280
+ restore(() => prepared.session.setActiveModelProfile?.(prepared.previousActiveModelProfile));
1281
+ restore(() =>
1282
+ restoreCanonicalVariant(prepared.modelRegistry, prepared.session.sessionId, prepared.previousCanonicalVariant),
1283
+ );
1284
+ if (persistentMutationStarted) {
1285
+ try {
1286
+ await prepared.settings.flushOrThrow();
1287
+ } catch (rollbackError) {
1288
+ rollbackErrors.push(rollbackError);
614
1289
  }
615
1290
  }
1291
+ if (rollbackErrors.length > 0) {
1292
+ throw new AggregateError(
1293
+ [error, ...rollbackErrors],
1294
+ "Model profile activation failed and rollback was incomplete",
1295
+ );
1296
+ }
616
1297
  throw error;
617
1298
  }
618
1299
  }
@@ -622,32 +1303,68 @@ export interface MaterializeModelProfileForDeletionResult {
622
1303
  agentModelOverrides: Record<string, ModelSelectorValue>;
623
1304
  previousModelRoles: Record<string, ModelSelectorValue>;
624
1305
  previousAgentModelOverrides: Record<string, ModelSelectorValue>;
1306
+ previousPersistedModelRoles: Record<string, ModelSelectorValue> | undefined;
1307
+ previousPersistedAgentModelOverrides: Record<string, ModelSelectorValue> | undefined;
1308
+ previousModelRolesOverride: Record<string, ModelSelectorValue> | undefined;
1309
+ previousAgentModelOverridesOverride: Record<string, ModelSelectorValue> | undefined;
1310
+ previousDefaultProfileOverride: string | undefined;
625
1311
  previousDefaultProfile: string | undefined;
626
1312
  previousPersistedDefaultProfile: string | undefined;
627
1313
  previousActiveModelProfile: string | undefined;
1314
+ previousDefaultChainState: ConfiguredModelChainState | undefined;
1315
+ previousDefaultFallbackRuntimeState: DefaultFallbackRuntimeState | undefined;
1316
+ /**
1317
+ * Restores the session sticky canonical variant that was snapshotted before
1318
+ * materialization cleared it. Internal closure capturing the registry,
1319
+ * session, and exact pre-clear selector — avoids threading a registry
1320
+ * through the restore options. Optional so callers that never materialized
1321
+ * (or use a registry without sticky support) can still restore settings.
1322
+ */
1323
+ restoreSessionCanonicalVariant?: () => void;
628
1324
  }
629
1325
 
630
1326
  export async function materializeModelProfileForDeletion(
631
1327
  options: PrepareModelProfileActivationOptions & {
632
- settings: Pick<Settings, "clearOverride" | "flush" | "get" | "getGlobal" | "override" | "set" | "unset">;
1328
+ settings: Pick<Settings, "clearOverride" | "flushOrThrow" | "get" | "getGlobal" | "override" | "set" | "unset">;
633
1329
  },
634
1330
  ): Promise<MaterializeModelProfileForDeletionResult> {
635
1331
  const prepared = await prepareModelProfileActivation(options);
636
1332
  const previousDefaultProfile = prepared.settings.get("modelProfile.default");
637
1333
  const previousPersistedDefaultProfile = prepared.settings.getGlobal("modelProfile.default");
1334
+ const concretizeForDeletion = async (selector: ModelSelectorValue): Promise<ModelSelectorValue> => {
1335
+ try {
1336
+ return await concretizeProfileSelectorValue(selector, prepared);
1337
+ } catch (error) {
1338
+ restoreCanonicalVariant(prepared.modelRegistry, prepared.session.sessionId, prepared.previousCanonicalVariant);
1339
+ throw error;
1340
+ }
1341
+ };
1342
+ const concreteDefaultChain =
1343
+ prepared.defaultChain.length > 0
1344
+ ? normalizeModelSelectorValue(await concretizeForDeletion(prepared.defaultChain))
1345
+ : [];
1346
+ const concreteModelRoles: Record<string, ModelSelectorValue> = {};
1347
+ for (const [role, selector] of Object.entries(prepared.modelRoles)) {
1348
+ concreteModelRoles[role] = await concretizeForDeletion(selector);
1349
+ }
1350
+ const concreteAgentModelOverrides: Record<string, ModelSelectorValue> = {};
1351
+ for (const [role, selector] of Object.entries(prepared.agentModelOverrides)) {
1352
+ concreteAgentModelOverrides[role] = await concretizeForDeletion(selector);
1353
+ }
638
1354
  const nextModelRoles = {
639
1355
  ...prepared.previousModelRoles,
640
- ...(prepared.defaultChain.length > 0
1356
+ ...(concreteDefaultChain.length > 0
641
1357
  ? {
642
- default: prepared.defaultChain.length === 1 ? prepared.defaultChain[0] : [...prepared.defaultChain],
1358
+ default: concreteDefaultChain.length === 1 ? concreteDefaultChain[0] : [...concreteDefaultChain],
643
1359
  }
644
1360
  : {}),
645
- ...prepared.modelRoles,
1361
+ ...concreteModelRoles,
646
1362
  };
647
1363
  const nextAgentModelOverrides = {
648
1364
  ...prepared.previousAgentModelOverrides,
649
- ...prepared.agentModelOverrides,
1365
+ ...concreteAgentModelOverrides,
650
1366
  };
1367
+ let defaultChainChanged = false;
651
1368
 
652
1369
  try {
653
1370
  prepared.settings.set("modelRoles", nextModelRoles);
@@ -657,15 +1374,85 @@ export async function materializeModelProfileForDeletion(
657
1374
  prepared.settings.override("modelRoles", nextModelRoles);
658
1375
  prepared.settings.override("task.agentModelOverrides", nextAgentModelOverrides);
659
1376
  prepared.session.setActiveModelProfile?.(undefined);
660
- await prepared.settings.flush();
1377
+ if (prepared.defaultChain.length > 0) {
1378
+ defaultChainChanged = true;
1379
+ prepared.session.setConfiguredModelChain(
1380
+ "default",
1381
+ concreteDefaultChain,
1382
+ "profile-deletion-materialized",
1383
+ undefined,
1384
+ true,
1385
+ );
1386
+ }
1387
+ await prepared.settings.flushOrThrow();
1388
+ prepared.session.clearProfileInstalledOverrides?.();
661
1389
  } catch (error) {
662
- prepared.settings.set("modelRoles", prepared.previousModelRoles);
663
- prepared.settings.set("task.agentModelOverrides", prepared.previousAgentModelOverrides);
664
- prepared.settings.set("modelProfile.default", previousPersistedDefaultProfile);
665
- prepared.settings.override("modelRoles", prepared.previousModelRoles);
666
- prepared.settings.override("task.agentModelOverrides", prepared.previousAgentModelOverrides);
667
- prepared.settings.override("modelProfile.default", previousDefaultProfile);
668
- prepared.session.setActiveModelProfile?.(prepared.previousActiveModelProfile);
1390
+ const previousChain = prepared.previousDefaultChainState;
1391
+ const rollbackErrors: unknown[] = [];
1392
+ const restore = (action: () => void): void => {
1393
+ try {
1394
+ action();
1395
+ } catch (rollbackError) {
1396
+ rollbackErrors.push(rollbackError);
1397
+ }
1398
+ };
1399
+ restore(() =>
1400
+ restoreCanonicalVariant(prepared.modelRegistry, prepared.session.sessionId, prepared.previousCanonicalVariant),
1401
+ );
1402
+ if (defaultChainChanged) {
1403
+ restore(() =>
1404
+ prepared.session.setConfiguredModelChain(
1405
+ "default",
1406
+ previousChain?.entries ?? prepared.previousDefaultChain ?? [],
1407
+ previousChain?.origin ?? "rollback",
1408
+ previousChain?.identity,
1409
+ previousChain?.explicitHead ?? true,
1410
+ ),
1411
+ );
1412
+ }
1413
+ if (prepared.previousDefaultFallbackRuntimeState) {
1414
+ restore(() =>
1415
+ prepared.session.restoreDefaultFallbackRuntimeState?.(prepared.previousDefaultFallbackRuntimeState!),
1416
+ );
1417
+ }
1418
+ restore(() =>
1419
+ prepared.previousPersistedModelRoles === undefined
1420
+ ? prepared.settings.unset("modelRoles")
1421
+ : prepared.settings.set("modelRoles", prepared.previousPersistedModelRoles),
1422
+ );
1423
+ restore(() =>
1424
+ prepared.previousPersistedAgentModelOverrides === undefined
1425
+ ? prepared.settings.unset("task.agentModelOverrides")
1426
+ : prepared.settings.set("task.agentModelOverrides", prepared.previousPersistedAgentModelOverrides),
1427
+ );
1428
+ restore(() => prepared.settings.set("modelProfile.default", previousPersistedDefaultProfile));
1429
+ restore(() =>
1430
+ prepared.previousModelRolesOverride === undefined
1431
+ ? prepared.settings.clearOverride("modelRoles")
1432
+ : prepared.settings.override("modelRoles", prepared.previousModelRolesOverride),
1433
+ );
1434
+ restore(() =>
1435
+ prepared.previousAgentModelOverridesOverride === undefined
1436
+ ? prepared.settings.clearOverride("task.agentModelOverrides")
1437
+ : prepared.settings.override("task.agentModelOverrides", prepared.previousAgentModelOverridesOverride),
1438
+ );
1439
+ restore(() =>
1440
+ prepared.previousDefaultProfileOverride === undefined
1441
+ ? prepared.settings.clearOverride("modelProfile.default")
1442
+ : prepared.settings.override("modelProfile.default", prepared.previousDefaultProfileOverride),
1443
+ );
1444
+ restore(() => prepared.session.setActiveModelProfile?.(prepared.previousActiveModelProfile));
1445
+ try {
1446
+ await prepared.settings.flushOrThrow();
1447
+ } catch (rollbackError) {
1448
+ rollbackErrors.push(rollbackError);
1449
+ }
1450
+ if (rollbackErrors.length > 0) {
1451
+ throw new AggregateError(
1452
+ [error, ...rollbackErrors],
1453
+ "Profile deletion materialization failed and rollback was incomplete",
1454
+ );
1455
+ }
669
1456
  throw error;
670
1457
  }
671
1458
 
@@ -674,25 +1461,88 @@ export async function materializeModelProfileForDeletion(
674
1461
  agentModelOverrides: nextAgentModelOverrides,
675
1462
  previousModelRoles: prepared.previousModelRoles,
676
1463
  previousAgentModelOverrides: prepared.previousAgentModelOverrides,
1464
+ previousPersistedModelRoles: prepared.previousPersistedModelRoles,
1465
+ previousPersistedAgentModelOverrides: prepared.previousPersistedAgentModelOverrides,
1466
+ previousModelRolesOverride: prepared.previousModelRolesOverride,
1467
+ previousAgentModelOverridesOverride: prepared.previousAgentModelOverridesOverride,
1468
+ previousDefaultProfileOverride: prepared.previousDefaultProfileOverride,
677
1469
  previousDefaultProfile,
678
1470
  previousPersistedDefaultProfile,
679
1471
  previousActiveModelProfile: prepared.previousActiveModelProfile,
1472
+ previousDefaultChainState: prepared.previousDefaultChainState,
1473
+ previousDefaultFallbackRuntimeState: prepared.previousDefaultFallbackRuntimeState,
1474
+ restoreSessionCanonicalVariant: () =>
1475
+ restoreCanonicalVariant(prepared.modelRegistry, prepared.session.sessionId, prepared.previousCanonicalVariant),
680
1476
  };
681
1477
  }
682
1478
 
683
1479
  export async function restoreMaterializedModelProfileForDeletion(options: {
684
- settings: Pick<Settings, "flush" | "override" | "set">;
685
- session: Pick<ModelProfileActivationSession, "setActiveModelProfile">;
1480
+ settings: Pick<Settings, "clearOverride" | "flushOrThrow" | "override" | "set" | "unset">;
1481
+ session: Pick<
1482
+ ModelProfileActivationSession,
1483
+ "setActiveModelProfile" | "setConfiguredModelChain" | "restoreDefaultFallbackRuntimeState"
1484
+ >;
686
1485
  snapshot: MaterializeModelProfileForDeletionResult;
687
1486
  }): Promise<void> {
688
- options.settings.set("modelRoles", options.snapshot.previousModelRoles);
689
- options.settings.set("task.agentModelOverrides", options.snapshot.previousAgentModelOverrides);
690
- options.settings.set("modelProfile.default", options.snapshot.previousPersistedDefaultProfile);
691
- options.settings.override("modelRoles", options.snapshot.previousModelRoles);
692
- options.settings.override("task.agentModelOverrides", options.snapshot.previousAgentModelOverrides);
693
- options.settings.override("modelProfile.default", options.snapshot.previousDefaultProfile);
694
- options.session.setActiveModelProfile?.(options.snapshot.previousActiveModelProfile);
695
- await options.settings.flush();
1487
+ const restoreErrors: unknown[] = [];
1488
+ const restore = (action: () => void): void => {
1489
+ try {
1490
+ action();
1491
+ } catch (error) {
1492
+ restoreErrors.push(error);
1493
+ }
1494
+ };
1495
+ restore(() => options.snapshot.restoreSessionCanonicalVariant?.());
1496
+ const previousChain = options.snapshot.previousDefaultChainState;
1497
+ restore(() =>
1498
+ options.session.setConfiguredModelChain(
1499
+ "default",
1500
+ previousChain?.entries ?? [],
1501
+ previousChain?.origin ?? "rollback",
1502
+ previousChain?.identity,
1503
+ previousChain?.explicitHead ?? true,
1504
+ ),
1505
+ );
1506
+ if (options.snapshot.previousDefaultFallbackRuntimeState) {
1507
+ restore(() =>
1508
+ options.session.restoreDefaultFallbackRuntimeState?.(options.snapshot.previousDefaultFallbackRuntimeState!),
1509
+ );
1510
+ }
1511
+ restore(() =>
1512
+ options.snapshot.previousPersistedModelRoles === undefined
1513
+ ? options.settings.unset("modelRoles")
1514
+ : options.settings.set("modelRoles", options.snapshot.previousPersistedModelRoles),
1515
+ );
1516
+ restore(() =>
1517
+ options.snapshot.previousPersistedAgentModelOverrides === undefined
1518
+ ? options.settings.unset("task.agentModelOverrides")
1519
+ : options.settings.set("task.agentModelOverrides", options.snapshot.previousPersistedAgentModelOverrides),
1520
+ );
1521
+ restore(() => options.settings.set("modelProfile.default", options.snapshot.previousPersistedDefaultProfile));
1522
+ restore(() =>
1523
+ options.snapshot.previousModelRolesOverride === undefined
1524
+ ? options.settings.clearOverride("modelRoles")
1525
+ : options.settings.override("modelRoles", options.snapshot.previousModelRolesOverride),
1526
+ );
1527
+ restore(() =>
1528
+ options.snapshot.previousAgentModelOverridesOverride === undefined
1529
+ ? options.settings.clearOverride("task.agentModelOverrides")
1530
+ : options.settings.override("task.agentModelOverrides", options.snapshot.previousAgentModelOverridesOverride),
1531
+ );
1532
+ restore(() =>
1533
+ options.snapshot.previousDefaultProfileOverride === undefined
1534
+ ? options.settings.clearOverride("modelProfile.default")
1535
+ : options.settings.override("modelProfile.default", options.snapshot.previousDefaultProfileOverride),
1536
+ );
1537
+ restore(() => options.session.setActiveModelProfile?.(options.snapshot.previousActiveModelProfile));
1538
+ try {
1539
+ await options.settings.flushOrThrow();
1540
+ } catch (error) {
1541
+ restoreErrors.push(error);
1542
+ }
1543
+ if (restoreErrors.length > 0) {
1544
+ throw new AggregateError(restoreErrors, "Failed to fully restore materialized model profile deletion state");
1545
+ }
696
1546
  }
697
1547
 
698
1548
  export async function activateModelProfile(