@gajae-code/coding-agent 0.18.11 → 0.18.12

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 (270) hide show
  1. package/CHANGELOG.md +293 -0
  2. package/dist/types/async/job-manager.d.ts +7 -3
  3. package/dist/types/async/result-formatting.d.ts +2 -0
  4. package/dist/types/autoresearch/git.d.ts +1 -1
  5. package/dist/types/capability/fs.d.ts +2 -2
  6. package/dist/types/cli/accounts-cli.d.ts +2 -2
  7. package/dist/types/cli/doctor/install-repairs.d.ts +4 -1
  8. package/dist/types/cli/install-activation.d.ts +10 -4
  9. package/dist/types/cli/public-command-errors.d.ts +1 -0
  10. package/dist/types/cli/update-cli.d.ts +14 -1
  11. package/dist/types/cli-ordinary.d.ts +2 -2
  12. package/dist/types/commands/accounts.d.ts +2 -1
  13. package/dist/types/commands/migrate.d.ts +1 -1
  14. package/dist/types/commands/sdk.d.ts +5 -0
  15. package/dist/types/commit/changelog/index.d.ts +1 -0
  16. package/dist/types/config/autorouting-tier-map.d.ts +30 -0
  17. package/dist/types/config/config-hot-reload-trust.d.ts +12 -0
  18. package/dist/types/config/config-hot-reload.d.ts +39 -0
  19. package/dist/types/config/file-lock.d.ts +8 -1
  20. package/dist/types/config/model-discovery-manager.d.ts +2 -0
  21. package/dist/types/config/model-profile-activation.d.ts +62 -6
  22. package/dist/types/config/model-registry.d.ts +56 -5
  23. package/dist/types/config/model-resolver.d.ts +2 -0
  24. package/dist/types/config/models-config-schema.d.ts +10 -0
  25. package/dist/types/config/settings-schema.d.ts +46 -3
  26. package/dist/types/config/settings.d.ts +33 -1
  27. package/dist/types/dap/config.d.ts +8 -4
  28. package/dist/types/debug/system-info.d.ts +6 -1
  29. package/dist/types/discovery/mcp-compat.d.ts +3 -1
  30. package/dist/types/edit/modes/patch.d.ts +5 -0
  31. package/dist/types/edit/modes/replace.d.ts +3 -0
  32. package/dist/types/exec/bash-executor.d.ts +12 -0
  33. package/dist/types/gjc-runtime/managed-owner-admission.d.ts +3 -0
  34. package/dist/types/gjc-runtime/session-state-lock.d.ts +2 -0
  35. package/dist/types/gjc-runtime/state-runtime.d.ts +0 -2
  36. package/dist/types/migrate/adapters/codex.d.ts +2 -1
  37. package/dist/types/migrate/adapters/index.d.ts +10 -1
  38. package/dist/types/migrate/executor.d.ts +3 -1
  39. package/dist/types/migrate/types.d.ts +15 -3
  40. package/dist/types/modes/components/settings-selector.d.ts +6 -0
  41. package/dist/types/modes/controllers/selector-controller.d.ts +2 -0
  42. package/dist/types/modes/theme/theme.d.ts +1 -0
  43. package/dist/types/runtime-mcp/content-limits.d.ts +4 -0
  44. package/dist/types/runtime-mcp/manager.d.ts +7 -0
  45. package/dist/types/runtime-mcp/plugin-mcp-name-filter.d.ts +22 -0
  46. package/dist/types/runtime-mcp/tool-bridge.d.ts +4 -0
  47. package/dist/types/sdk/broker/broker-exit.d.ts +2 -2
  48. package/dist/types/sdk/broker/broker.d.ts +7 -1
  49. package/dist/types/sdk/broker/daemon-entry.d.ts +11 -1
  50. package/dist/types/sdk/broker/doctor-restart.d.ts +1 -1
  51. package/dist/types/sdk/broker/ensure.d.ts +136 -7
  52. package/dist/types/sdk/broker/hop.d.ts +36 -0
  53. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +18 -10
  54. package/dist/types/sdk/broker/lock-host-identity.d.ts +5 -0
  55. package/dist/types/sdk/broker/session-index.d.ts +4 -1
  56. package/dist/types/sdk/bus/chat-command-policy.d.ts +2 -1
  57. package/dist/types/sdk/bus/index.d.ts +23 -0
  58. package/dist/types/sdk/bus/reconciliation-store.d.ts +10 -0
  59. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +3 -1
  60. package/dist/types/sdk/cli/session-cli.d.ts +0 -1
  61. package/dist/types/sdk/host/session-runtime.d.ts +32 -0
  62. package/dist/types/sdk/prompt-deadline-flush.d.ts +42 -4
  63. package/dist/types/sdk/prompt-deadline-manager.d.ts +2 -0
  64. package/dist/types/sdk/protocol/adapter-validation.d.ts +10 -4
  65. package/dist/types/sdk/reconciliation-extensions.d.ts +1 -1
  66. package/dist/types/sdk/session-list.d.ts +1 -1
  67. package/dist/types/session/agent-session.d.ts +58 -2
  68. package/dist/types/session/internal/managed-session-scope.d.ts +2 -2
  69. package/dist/types/session/internal/managed-session-storage.d.ts +1 -1
  70. package/dist/types/session/internal/native-publish-outcome.d.ts +1 -1
  71. package/dist/types/session/messages.d.ts +5 -1
  72. package/dist/types/session/session-manager.d.ts +13 -3
  73. package/dist/types/session/startup-auth-config.d.ts +24 -2
  74. package/dist/types/session/streaming-output.d.ts +11 -0
  75. package/dist/types/session/terminal-abort.d.ts +3 -1
  76. package/dist/types/session-import/service.d.ts +1 -1
  77. package/dist/types/skill-state/workflow-mutation-guard.d.ts +7 -3
  78. package/dist/types/slash-commands/helpers/provider-auth-status.d.ts +8 -0
  79. package/dist/types/task/executor.d.ts +19 -2
  80. package/dist/types/tools/ast-edit-write-path.d.ts +10 -0
  81. package/dist/types/tools/atomic-file-write.d.ts +9 -7
  82. package/dist/types/tools/bash-allowed-prefixes.d.ts +11 -0
  83. package/dist/types/tools/descriptors.d.ts +2 -0
  84. package/dist/types/tools/fetch.d.ts +1 -1
  85. package/dist/types/tools/index.d.ts +2 -0
  86. package/dist/types/tools/python.d.ts +1 -1
  87. package/dist/types/tools/report-issue.d.ts +41 -0
  88. package/dist/types/tools/subagent.d.ts +23 -0
  89. package/dist/types/web/scrapers/types.d.ts +8 -1
  90. package/dist/types/web/search/providers/insane.d.ts +7 -1
  91. package/dist/types/web/search/providers/searxng.d.ts +5 -0
  92. package/package.json +8 -8
  93. package/scripts/build-binary.ts +2 -1
  94. package/scripts/capture-subagent-compact-panel-showcase.ts +105 -0
  95. package/scripts/generate-docs-index.ts +8 -1
  96. package/scripts/generate-sdk-operation-inventory.ts +22 -4
  97. package/src/async/job-manager.ts +356 -56
  98. package/src/async/result-formatting.ts +40 -0
  99. package/src/autoresearch/git.ts +9 -2
  100. package/src/autoresearch/prompts/prompt-setup.md +1 -1
  101. package/src/capability/fs.ts +27 -19
  102. package/src/capability/index.ts +2 -2
  103. package/src/cli/accounts-cli.ts +176 -8
  104. package/src/cli/auth-broker-cli.ts +21 -11
  105. package/src/cli/doctor/install-repairs.ts +35 -5
  106. package/src/cli/fast-help.ts +1 -1
  107. package/src/cli/install-activation.ts +47 -4
  108. package/src/cli/migrate-cli.ts +7 -5
  109. package/src/cli/public-command-errors.ts +4 -0
  110. package/src/cli/root-flags.ts +2 -2
  111. package/src/cli/stats-cli.ts +9 -5
  112. package/src/cli/update-cli.ts +224 -7
  113. package/src/cli-ordinary.ts +7 -2
  114. package/src/commands/accounts.ts +10 -5
  115. package/src/commands/migrate.ts +3 -3
  116. package/src/commands/sdk.ts +199 -42
  117. package/src/commands/ultragoal.ts +16 -8
  118. package/src/commit/changelog/index.ts +39 -1
  119. package/src/commit/message.ts +10 -3
  120. package/src/config/autorouting-tier-map.ts +30 -0
  121. package/src/config/config-hot-reload-trust.ts +148 -0
  122. package/src/config/config-hot-reload.ts +734 -0
  123. package/src/config/file-lock.ts +216 -56
  124. package/src/config/model-bindings-applier.ts +85 -47
  125. package/src/config/model-discovery-manager.ts +10 -0
  126. package/src/config/model-profile-activation.ts +716 -113
  127. package/src/config/model-registry.ts +1334 -158
  128. package/src/config/model-resolver.ts +3 -1
  129. package/src/config/models-config-schema.ts +6 -0
  130. package/src/config/settings-schema.ts +60 -3
  131. package/src/config/settings.ts +458 -24
  132. package/src/coordinator-mcp/policy.ts +26 -1
  133. package/src/crash/sanitize.ts +4 -0
  134. package/src/customization/import.ts +3 -1
  135. package/src/dap/config.ts +31 -11
  136. package/src/dap/defaults.json +1 -1
  137. package/src/debug/crash-diagnostics.ts +2 -1
  138. package/src/debug/report-bundle.ts +19 -0
  139. package/src/debug/system-info.ts +8 -3
  140. package/src/defaults/gjc/extensions/grok-cli-vendor/src/shared/base-url.ts +21 -7
  141. package/src/discovery/agents-md.ts +1 -1
  142. package/src/discovery/helpers.ts +1 -2
  143. package/src/discovery/mcp-compat.ts +120 -17
  144. package/src/edit/apply-patch/index.ts +12 -1
  145. package/src/edit/index.ts +13 -17
  146. package/src/edit/modes/patch.ts +187 -8
  147. package/src/edit/modes/replace.ts +136 -0
  148. package/src/eval/js/context-manager.ts +29 -3
  149. package/src/eval/js/worker-entry.ts +51 -18
  150. package/src/eval/py/executor.ts +14 -4
  151. package/src/eval/py/runner.py +144 -5
  152. package/src/exec/bash-executor.ts +81 -4
  153. package/src/exec/bash-shell-guardian.ts +4 -1
  154. package/src/exec/bash-shell-supervisor.ts +8 -4
  155. package/src/exec/isolated-shell.ts +5 -1
  156. package/src/export/html/index.ts +46 -6
  157. package/src/extensibility/custom-tools/wrapper.ts +3 -1
  158. package/src/extensibility/extensions/loader.ts +8 -1
  159. package/src/extensibility/plugins/marketplace/fetcher.ts +2 -2
  160. package/src/gjc-runtime/gc-runtime.ts +33 -6
  161. package/src/gjc-runtime/launch-tmux.ts +7 -2
  162. package/src/gjc-runtime/launch-worktree.test.ts +148 -0
  163. package/src/gjc-runtime/launch-worktree.ts +101 -14
  164. package/src/gjc-runtime/managed-owner-admission.ts +46 -2
  165. package/src/gjc-runtime/session-resolution.ts +38 -6
  166. package/src/gjc-runtime/session-state-lock.ts +59 -14
  167. package/src/gjc-runtime/state-runtime.ts +23 -19
  168. package/src/hooks/native-skill-hook.ts +14 -11
  169. package/src/internal-urls/docs-index.generated.ts +14 -13
  170. package/src/internal-urls/local-protocol.ts +142 -8
  171. package/src/lsp/index.ts +29 -0
  172. package/src/lsp/workspace-path.ts +23 -0
  173. package/src/migrate/action-planner.ts +57 -0
  174. package/src/migrate/adapters/claude-code.ts +23 -4
  175. package/src/migrate/adapters/codex.ts +22 -4
  176. package/src/migrate/adapters/index.ts +22 -4
  177. package/src/migrate/adapters/opencode.ts +6 -1
  178. package/src/migrate/executor.ts +84 -7
  179. package/src/migrate/report.ts +19 -2
  180. package/src/migrate/types.ts +14 -4
  181. package/src/modes/DESIGN.md +51 -0
  182. package/src/modes/acp/permission-mode.ts +18 -1
  183. package/src/modes/components/assistant-message.ts +55 -1
  184. package/src/modes/components/settings-selector.ts +103 -37
  185. package/src/modes/controllers/command-controller.ts +38 -2
  186. package/src/modes/controllers/plan-mode-controller.ts +17 -7
  187. package/src/modes/controllers/selector-controller.ts +62 -6
  188. package/src/modes/interactive-mode.ts +68 -0
  189. package/src/modes/theme/theme.ts +15 -0
  190. package/src/prompts/tools/debug.md +1 -1
  191. package/src/prompts/tools/read.md +1 -1
  192. package/src/prompts/tools/report-issue.md +13 -0
  193. package/src/prompts/tools/task.md +1 -1
  194. package/src/runtime-credential-selector.ts +2 -2
  195. package/src/runtime-mcp/content-limits.ts +7 -0
  196. package/src/runtime-mcp/manager-pool.test.ts +5 -4
  197. package/src/runtime-mcp/manager.ts +32 -0
  198. package/src/runtime-mcp/oauth-flow.ts +32 -6
  199. package/src/runtime-mcp/plugin-mcp-name-filter.ts +62 -0
  200. package/src/runtime-mcp/render.ts +16 -1
  201. package/src/runtime-mcp/tool-bridge.ts +126 -17
  202. package/src/sdk/broker/broker-exit.ts +8 -5
  203. package/src/sdk/broker/broker.ts +342 -82
  204. package/src/sdk/broker/daemon-entry.ts +338 -116
  205. package/src/sdk/broker/doctor-restart.ts +2 -1
  206. package/src/sdk/broker/ensure.ts +1143 -265
  207. package/src/sdk/broker/hop.ts +274 -0
  208. package/src/sdk/broker/lifecycle-ledger.ts +35 -11
  209. package/src/sdk/broker/lock-host-identity.ts +29 -0
  210. package/src/sdk/broker/session-index.ts +62 -12
  211. package/src/sdk/bus/chat-command-policy.ts +16 -3
  212. package/src/sdk/bus/index.ts +52 -21
  213. package/src/sdk/bus/reconciliation-store.ts +201 -8
  214. package/src/sdk/bus/telegram-daemon-contract.ts +3 -1
  215. package/src/sdk/cli/session-cli.ts +21 -6
  216. package/src/sdk/host/session-runtime.test.ts +118 -23
  217. package/src/sdk/host/session-runtime.ts +352 -47
  218. package/src/sdk/lifecycle/service.ts +1 -0
  219. package/src/sdk/prompt-deadline-flush.ts +78 -5
  220. package/src/sdk/prompt-deadline-manager.ts +68 -14
  221. package/src/sdk/protocol/adapter-validation.ts +53 -11
  222. package/src/sdk/protocol/operation-inventory.generated.json +101 -2
  223. package/src/sdk/reconciliation-extensions.ts +1 -0
  224. package/src/sdk/session-list.ts +1 -0
  225. package/src/sdk/session.ts +114 -61
  226. package/src/session/agent-session.ts +1043 -81
  227. package/src/session/internal/managed-session-scope.ts +126 -39
  228. package/src/session/internal/managed-session-storage.ts +14 -11
  229. package/src/session/internal/native-publish-outcome.ts +5 -1
  230. package/src/session/messages.ts +47 -6
  231. package/src/session/session-manager.ts +236 -14
  232. package/src/session/startup-auth-config.ts +116 -8
  233. package/src/session/streaming-output.ts +262 -4
  234. package/src/session/terminal-abort.ts +14 -2
  235. package/src/session-import/service.ts +1 -0
  236. package/src/skill-state/workflow-mutation-guard.ts +491 -32
  237. package/src/slash-commands/builtin-registry.ts +15 -3
  238. package/src/slash-commands/helpers/credential-switch.ts +10 -5
  239. package/src/slash-commands/helpers/provider-auth-status.ts +456 -0
  240. package/src/task/executor.ts +103 -12
  241. package/src/task/index.ts +389 -175
  242. package/src/tools/ast-edit-write-path.ts +64 -0
  243. package/src/tools/ast-edit.ts +18 -1
  244. package/src/tools/atomic-file-write.ts +46 -15
  245. package/src/tools/auto-generated-guard.ts +3 -0
  246. package/src/tools/bash-allowed-prefixes.ts +153 -5
  247. package/src/tools/bash-interactive.ts +14 -2
  248. package/src/tools/bash.ts +14 -0
  249. package/src/tools/debug.ts +7 -6
  250. package/src/tools/descriptors.test.ts +44 -0
  251. package/src/tools/descriptors.ts +34 -2
  252. package/src/tools/fetch.ts +3 -2
  253. package/src/tools/index.ts +2 -0
  254. package/src/tools/output-meta.ts +23 -2
  255. package/src/tools/python.ts +1 -1
  256. package/src/tools/read.ts +17 -35
  257. package/src/tools/report-issue.ts +184 -0
  258. package/src/tools/subagent-render.ts +114 -12
  259. package/src/tools/subagent.ts +108 -1
  260. package/src/tools/todo-write.ts +41 -3
  261. package/src/tools/tool-catalog.generated.ts +31 -3
  262. package/src/tools/write.ts +35 -3
  263. package/src/utils/commit-message-generator.ts +9 -2
  264. package/src/utils/file-mentions.ts +29 -1
  265. package/src/utils/title-generator.ts +7 -2
  266. package/src/web/scrapers/index.ts +3 -1
  267. package/src/web/scrapers/types.ts +8 -0
  268. package/src/web/scrapers/youtube.ts +3 -2
  269. package/src/web/search/providers/insane.ts +91 -28
  270. package/src/web/search/providers/searxng.ts +62 -10
@@ -1,7 +1,12 @@
1
+ import * as util from "node:util";
1
2
  import { ThinkingLevel } from "@gajae-code/agent-core";
2
3
  import { type Api, isKnownProvider, type Model } from "@gajae-code/ai/core";
3
4
  import { logger } from "@gajae-code/utils";
4
- import type { AgentSession, DefaultFallbackRuntimeState } from "../session/agent-session";
5
+ import type {
6
+ AgentSession,
7
+ AgentSessionProfileInstalledOverrideState,
8
+ DefaultFallbackRuntimeState,
9
+ } from "../session/agent-session";
5
10
  import { clampExplicitThinkingLevelForModel, formatClampedModelSelector } from "../thinking";
6
11
  import { validateModelProfileName } from "./model-profile-contract";
7
12
  import {
@@ -55,20 +60,36 @@ type ModelProfileActivationSession = Pick<
55
60
  modelRoles: readonly string[],
56
61
  agentModelOverrides: readonly string[],
57
62
  preProfileModel: Model<Api> | undefined,
63
+ baseline?: {
64
+ modelRoles: Readonly<Record<string, ModelSelectorValue>>;
65
+ agentModelOverrides: Readonly<Record<string, ModelSelectorValue>>;
66
+ },
67
+ installed?: {
68
+ modelRoles: Readonly<Record<string, ModelSelectorValue>>;
69
+ agentModelOverrides: Readonly<Record<string, ModelSelectorValue>>;
70
+ },
71
+ manual?: { modelRoles: readonly string[]; agentModelOverrides: readonly string[] },
58
72
  ) => void;
59
73
  /** Drop the recorded profile-installed override keys (e.g. after materialization). */
60
74
  clearProfileInstalledOverrides?: () => void;
61
75
  /** Current profile-installed override keys, for deriving the activation base. */
62
76
  getProfileInstalledOverrideKeys?: () => { modelRoles: readonly string[]; agentModelOverrides: readonly string[] };
77
+ /** Profile-installed baselines, preserved when a refreshed profile drops an assignment. */
78
+ getProfileInstalledOverrideState?: () => AgentSessionProfileInstalledOverrideState;
79
+ restoreProfileInstalledOverrideState?: (state: AgentSessionProfileInstalledOverrideState) => void;
63
80
  /** Re-apply vendor-separated delegation (task tool + prompt) after the role layer changed. */
64
81
  syncEagerDelegation?: () => Promise<void>;
65
82
  getSessionDefaultModelSelector?: () => string | undefined;
66
83
  recordResumeDefaultModel?: (selector: string | undefined) => void;
67
84
  getUserCanonicalVariantSelection?: () => UserCanonicalVariantSelection;
85
+ getUserModelSelectionRevision?: () => number;
68
86
  seedDefaultFallbackResolution?: (activeIndex: number, skips: Array<{ selector: string; reason: string }>) => void;
69
87
  getDefaultFallbackRuntimeState?: () => DefaultFallbackRuntimeState;
70
88
  restoreDefaultFallbackRuntimeState?: (state: DefaultFallbackRuntimeState) => void;
71
89
  restoreModelSelectionForRollback?: AgentSession["restoreModelSelectionForRollback"];
90
+ prepareModelSelectionForProfileActivation?: AgentSession["prepareModelSelectionForProfileActivation"];
91
+ commitPreparedProfileModelSelection?: AgentSession["commitPreparedProfileModelSelection"];
92
+ finishPreparedProfileModelSelection?: AgentSession["finishPreparedProfileModelSelection"];
72
93
  modelRegistry?: Pick<
73
94
  ModelRegistry,
74
95
  | "getSessionCanonicalVariant"
@@ -79,7 +100,8 @@ type ModelProfileActivationSession = Pick<
79
100
  | "resolveModelByLookupAlias"
80
101
  | "authStorage"
81
102
  | "isCredentiallessProvider"
82
- >;
103
+ > &
104
+ Partial<Pick<ModelRegistry, "seedCanonicalVariant">>;
83
105
  getConfiguredModelChainState?: (role: string) => ConfiguredModelChainState | undefined;
84
106
  };
85
107
 
@@ -90,6 +112,24 @@ type ConfiguredModelChainState = {
90
112
  explicitHead: boolean;
91
113
  };
92
114
 
115
+ type PublishedModelProfileActivationState = {
116
+ model: Model<Api> | undefined;
117
+ thinkingLevel: ThinkingLevel | undefined;
118
+ modelRolesOverride: Readonly<Record<string, ModelSelectorValue>> | undefined;
119
+ agentModelOverridesOverride: Readonly<Record<string, ModelSelectorValue>> | undefined;
120
+ defaultChainState: ConfiguredModelChainState | undefined;
121
+ defaultFallbackRuntimeState: DefaultFallbackRuntimeState | undefined;
122
+ activeModelProfile: string | undefined;
123
+ profileInstalledOverrideState: AgentSessionProfileInstalledOverrideState | undefined;
124
+ sessionDefaultModel: string | undefined;
125
+ persistedModelRoles: Readonly<Record<string, ModelSelectorValue>> | undefined;
126
+ persistedAgentModelOverrides: Readonly<Record<string, ModelSelectorValue>> | undefined;
127
+ persistedDefaultProfile: string | undefined;
128
+ persistedDefaultThinkingLevel: Exclude<ThinkingLevel, "inherit"> | undefined;
129
+ defaultThinkingLevelOverride: Exclude<ThinkingLevel, "inherit"> | undefined;
130
+ defaultProfileOverride: string | undefined;
131
+ };
132
+
93
133
  export interface PrepareModelProfileActivationOptions {
94
134
  session: ModelProfileActivationSession;
95
135
  modelRegistry: Pick<
@@ -108,6 +148,8 @@ export interface PrepareModelProfileActivationOptions {
108
148
  Pick<
109
149
  ModelRegistry,
110
150
  | "getAvailable"
151
+ | "hasUsableAuthForProvider"
152
+ | "isStagedReloadCandidate"
111
153
  | "resolveModelByLookupAlias"
112
154
  | "lookupAliasExists"
113
155
  | "clearCanonicalVariant"
@@ -115,6 +157,7 @@ export interface PrepareModelProfileActivationOptions {
115
157
  | "getSessionCanonicalVariant"
116
158
  | "restoreSessionCanonicalVariant"
117
159
  | "getConfiguredProviderIds"
160
+ | "getConfiguredModelBindings"
118
161
  | "isKnownProvider"
119
162
  >
120
163
  > & {
@@ -122,11 +165,21 @@ export interface PrepareModelProfileActivationOptions {
122
165
  authStorage?: ModelRegistry["authStorage"];
123
166
  };
124
167
  settings: Pick<Settings, "get" | "getGlobal" | "getOverride">;
168
+ /** Live mutation target when `settings` is a validated staged view. */
169
+ settingsForApply?: Settings;
170
+ prepareSessionModelSelection?: boolean;
171
+ preserveManualOverrides?: boolean;
172
+ preserveCanonicalVariant?: boolean;
173
+ signal?: AbortSignal;
125
174
  profileName: string;
126
175
  }
127
176
  export interface ApplyModelProfileActivationOptions {
128
177
  persistDefault?: boolean;
129
178
  thinkingLevelOverride?: ThinkingLevel;
179
+ preserveDefaultModelSelection?: boolean;
180
+ preserveLiveModelSelection?: boolean;
181
+ deferRuntimePublication?: boolean;
182
+ signal?: AbortSignal;
130
183
  /** Cancels deferred activation if a newer user model selection exists. */
131
184
  isCurrent?: () => boolean;
132
185
  }
@@ -148,11 +201,18 @@ export interface PreparedModelProfileActivation {
148
201
  previousDefaultProfileOverride: string | undefined;
149
202
  previousPersistedDefaultProfile: string | undefined;
150
203
  previousPersistedDefaultThinkingLevel: Exclude<ThinkingLevel, "inherit"> | undefined;
151
- previousDefaultThinkingLevelOverride: ThinkingLevel | undefined;
204
+ previousDefaultThinkingLevelOverride: Exclude<ThinkingLevel, "inherit"> | undefined;
152
205
  baseAgentModelOverrides: Record<string, ModelSelectorValue>;
153
206
  baseModelRoles: Record<string, ModelSelectorValue>;
154
207
  previousDefaultChain: readonly string[] | undefined;
155
208
  previousDefaultChainState: ConfiguredModelChainState | undefined;
209
+ previousProfileInstalledOverrideState: AgentSessionProfileInstalledOverrideState | undefined;
210
+ preparedDefaultModelSelection:
211
+ | Awaited<ReturnType<NonNullable<ModelProfileActivationSession["prepareModelSelectionForProfileActivation"]>>>
212
+ | undefined;
213
+ defaultChainPublished: boolean;
214
+ defaultModelPublished: boolean;
215
+ publishedState: PublishedModelProfileActivationState | undefined;
156
216
  previousDefaultFallbackRuntimeState: DefaultFallbackRuntimeState | undefined;
157
217
  defaultModel: Model<Api> | undefined;
158
218
  defaultThinkingLevel: ThinkingLevel | undefined;
@@ -164,6 +224,8 @@ export interface PreparedModelProfileActivation {
164
224
  defaultResolutionSkips: Array<{ selector: string; reason: string }>;
165
225
  modelRoles: Record<string, ModelSelectorValue>;
166
226
  agentModelOverrides: Record<string, ModelSelectorValue>;
227
+ manualModelRoleOverrides: ReadonlySet<string>;
228
+ manualAgentModelOverrides: ReadonlySet<string>;
167
229
  previousActiveModelProfile: string | undefined;
168
230
  /**
169
231
  * The session resume default ("provider/id") captured BEFORE activation —
@@ -528,6 +590,17 @@ async function getProfileProviderApiKey(
528
590
  }
529
591
  }
530
592
 
593
+ function isStagedProviderAuthAvailable(
594
+ registry: PrepareModelProfileActivationOptions["modelRegistry"],
595
+ provider: string,
596
+ sessionId: string,
597
+ ): boolean {
598
+ return (
599
+ registry.isStagedReloadCandidate?.() === true &&
600
+ (registry.hasUsableAuthForProvider?.(provider, sessionId) ?? false)
601
+ );
602
+ }
603
+
531
604
  export function formatModelProfileCredentialError(profileLabel: string, providers: readonly string[]): string {
532
605
  return `Model profile "${profileLabel}" requires credentials for: ${providers.join(", ")}. Run /login and configure the missing provider(s), then retry.`;
533
606
  }
@@ -825,7 +898,8 @@ export async function resolveModelProfileDefaultChain(options: {
825
898
  if (requiredProviderSet.has(provider) && !alternativeSet.has(provider)) throw error;
826
899
  continue;
827
900
  }
828
- if (apiKey === kNoAuth || isAuthenticated(apiKey)) authenticatedProviders.add(provider);
901
+ const hasUsableAuth = isStagedProviderAuthAvailable(options.modelRegistry, provider, options.credentialSessionId);
902
+ if (apiKey === kNoAuth || isAuthenticated(apiKey) || hasUsableAuth) authenticatedProviders.add(provider);
829
903
  else if (requiredProviderSet.has(provider)) missingProviders.push(provider);
830
904
  }
831
905
  const proxyProvider = profile.source !== "user" ? resolveProxyProviderId(options.settings) : undefined;
@@ -851,6 +925,9 @@ export async function resolveModelProfileDefaultChain(options: {
851
925
  options.credentialSessionId,
852
926
  profileLabel,
853
927
  );
928
+ const proxyHasUsableAuth =
929
+ proxyProvider !== undefined &&
930
+ isStagedProviderAuthAvailable(options.modelRegistry, proxyProvider, options.credentialSessionId);
854
931
  if (
855
932
  proxyProvider !== undefined &&
856
933
  !isModelProfileProxyConfigured(proxyProvider, configuredProviderIds, proxyApiKey === kNoAuth)
@@ -859,7 +936,8 @@ export async function resolveModelProfileDefaultChain(options: {
859
936
  `modelProfile.proxyProvider "${proxyProvider}" is not configured. Configure it with \`gjc setup provider\` before activating a preset.`,
860
937
  );
861
938
  }
862
- const proxyAuthenticated = proxyApiKey !== undefined && (proxyApiKey === kNoAuth || isAuthenticated(proxyApiKey));
939
+ const proxyAuthenticated =
940
+ (proxyApiKey !== undefined && (proxyApiKey === kNoAuth || isAuthenticated(proxyApiKey))) || proxyHasUsableAuth;
863
941
  if (proxyMode === "always" && !proxyAuthenticated)
864
942
  throw new ModelProfileCredentialError(profileLabel, [proxyProvider!]);
865
943
  const strictMissing = missingProviders.filter(
@@ -948,6 +1026,8 @@ export async function resolveModelProfileDefaultChain(options: {
948
1026
  credentialSessionId: options.credentialSessionId,
949
1027
  isCredentialUnavailable: provider =>
950
1028
  isSessionCredentialPinBlocking(options.modelRegistry, provider, options.credentialSessionId),
1029
+ isProviderAuthAvailable: provider =>
1030
+ isStagedProviderAuthAvailable(options.modelRegistry, provider, options.credentialSessionId),
951
1031
  },
952
1032
  );
953
1033
  return { profileName, entries: defaultChain, ...resolution };
@@ -1147,6 +1227,8 @@ async function resolveAndClampSelectorValue(
1147
1227
  credentialSessionId: options.credentialSessionId,
1148
1228
  isCredentialUnavailable: provider =>
1149
1229
  isSessionCredentialPinBlocking(options.modelRegistry, provider, options.credentialSessionId),
1230
+ isProviderAuthAvailable: provider =>
1231
+ isStagedProviderAuthAvailable(options.modelRegistry, provider, options.credentialSessionId),
1150
1232
  },
1151
1233
  );
1152
1234
  resolved = {
@@ -1279,6 +1361,8 @@ async function concretizeProfileSelectorValue(
1279
1361
  credentialSessionId,
1280
1362
  isCredentialUnavailable: provider =>
1281
1363
  isSessionCredentialPinBlocking(prepared.modelRegistry, provider, credentialSessionId),
1364
+ isProviderAuthAvailable: provider =>
1365
+ isStagedProviderAuthAvailable(prepared.modelRegistry, provider, credentialSessionId),
1282
1366
  },
1283
1367
  )
1284
1368
  : resolveModelRoleValue(selector, candidates, {
@@ -1314,7 +1398,7 @@ async function concretizeProfileSelectorValue(
1314
1398
  * variant is cleared so a previous provider cannot silently resurrect.
1315
1399
  */
1316
1400
  function restoreCanonicalVariant(
1317
- modelRegistry: PrepareModelProfileActivationOptions["modelRegistry"],
1401
+ modelRegistry: Partial<Pick<ModelRegistry, "restoreSessionCanonicalVariant" | "clearCanonicalVariant">>,
1318
1402
  sessionId: string,
1319
1403
  previousCanonicalVariant: string | undefined,
1320
1404
  ): void {
@@ -1326,8 +1410,18 @@ function restoreCanonicalVariant(
1326
1410
  }
1327
1411
  }
1328
1412
 
1413
+ function selectorValuesEqual(left: ModelSelectorValue | undefined, right: ModelSelectorValue | undefined): boolean {
1414
+ if (left === undefined || right === undefined) return left === right;
1415
+ const leftSelectors = normalizeModelSelectorValue(left);
1416
+ const rightSelectors = normalizeModelSelectorValue(right);
1417
+ return (
1418
+ leftSelectors.length === rightSelectors.length &&
1419
+ leftSelectors.every((selector, index) => selector === rightSelectors[index])
1420
+ );
1421
+ }
1422
+
1329
1423
  function restoreCanonicalVariantAfterPreparation(
1330
- modelRegistry: PrepareModelProfileActivationOptions["modelRegistry"],
1424
+ modelRegistry: Partial<Pick<ModelRegistry, "restoreSessionCanonicalVariant" | "clearCanonicalVariant">>,
1331
1425
  session: ModelProfileActivationSession,
1332
1426
  previousCanonicalVariant: string | undefined,
1333
1427
  previousUserSelection: UserCanonicalVariantSelection | undefined,
@@ -1343,7 +1437,6 @@ function restoreCanonicalVariantAfterPreparation(
1343
1437
  userSelectionChanged ? currentUserSelection.canonicalVariant : previousCanonicalVariant,
1344
1438
  );
1345
1439
  }
1346
-
1347
1440
  export async function prepareModelProfileActivation(
1348
1441
  options: PrepareModelProfileActivationOptions,
1349
1442
  ): Promise<PreparedModelProfileActivation> {
@@ -1353,6 +1446,7 @@ export async function prepareModelProfileActivation(
1353
1446
  const profileLabel = formatModelProfileDisplayLabel(profile);
1354
1447
 
1355
1448
  const previousModel = options.session.model;
1449
+ const applySettings = options.settingsForApply ?? (options.settings as Settings);
1356
1450
  // Snapshot the exact pre-clear sticky selector (verbatim, not re-derived from
1357
1451
  // the live model) so a failed prepare/apply/materialize rollback restores the
1358
1452
  // genuinely-sticky provider even when the live model is a transient switch.
@@ -1363,7 +1457,7 @@ export async function prepareModelProfileActivation(
1363
1457
  // Explicit profile activation/reselection invalidates the session's sticky
1364
1458
  // canonical variant BEFORE the new profile's aliases resolve, so the old
1365
1459
  // provider's sticky variant cannot win the new profile's resolution.
1366
- options.modelRegistry.clearCanonicalVariant?.(options.session.sessionId);
1460
+ if (!options.preserveCanonicalVariant) options.modelRegistry.clearCanonicalVariant?.(options.session.sessionId);
1367
1461
 
1368
1462
  try {
1369
1463
  const requiredProviders = aggregateModelProfileRequiredProviders(profile.requiredProviders, profile);
@@ -1413,7 +1507,8 @@ export async function prepareModelProfileActivation(
1413
1507
  if (requiredProviderSet.has(provider) && !alternativeSet.has(provider)) throw error;
1414
1508
  continue;
1415
1509
  }
1416
- if (apiKey !== kNoAuth && !isAuthenticated(apiKey)) {
1510
+ const hasUsableAuth = isStagedProviderAuthAvailable(options.modelRegistry, provider, credentialSessionId);
1511
+ if (apiKey !== kNoAuth && !isAuthenticated(apiKey) && !hasUsableAuth) {
1417
1512
  if (requiredProviderSet.has(provider)) missingProviders.push(provider);
1418
1513
  } else {
1419
1514
  authenticatedProviders.push(provider);
@@ -1444,6 +1539,9 @@ export async function prepareModelProfileActivation(
1444
1539
  proxyProvider === undefined
1445
1540
  ? undefined
1446
1541
  : await getProfileProviderApiKey(options.modelRegistry, proxyProvider, credentialSessionId, profileLabel);
1542
+ const proxyHasUsableAuth =
1543
+ proxyProvider !== undefined &&
1544
+ isStagedProviderAuthAvailable(options.modelRegistry, proxyProvider, credentialSessionId);
1447
1545
  if (proxyProvider !== undefined) {
1448
1546
  const configuredProxyProviders = options.modelRegistry.getConfiguredProviderIds?.();
1449
1547
  if (!isModelProfileProxyConfigured(proxyProvider, configuredProxyProviders, proxyApiKey === kNoAuth)) {
@@ -1454,8 +1552,8 @@ export async function prepareModelProfileActivation(
1454
1552
  }
1455
1553
  const proxyAuthenticated =
1456
1554
  proxyProvider !== undefined &&
1457
- proxyApiKey !== undefined &&
1458
- (proxyApiKey === kNoAuth || isAuthenticated(proxyApiKey));
1555
+ ((proxyApiKey !== undefined && (proxyApiKey === kNoAuth || isAuthenticated(proxyApiKey))) ||
1556
+ proxyHasUsableAuth);
1459
1557
  if (proxyMode === "always" && !proxyAuthenticated) {
1460
1558
  throw new ModelProfileCredentialError(profileLabel, [proxyProvider!]);
1461
1559
  }
@@ -1547,6 +1645,8 @@ export async function prepareModelProfileActivation(
1547
1645
  credentialSessionId,
1548
1646
  isCredentialUnavailable: provider =>
1549
1647
  isSessionCredentialPinBlocking(options.modelRegistry, provider, credentialSessionId),
1648
+ isProviderAuthAvailable: provider =>
1649
+ isStagedProviderAuthAvailable(options.modelRegistry, provider, credentialSessionId),
1550
1650
  },
1551
1651
  );
1552
1652
  const defaultModel = defaultResolution.model;
@@ -1573,49 +1673,129 @@ export async function prepareModelProfileActivation(
1573
1673
  credentialSessionId,
1574
1674
  profileLabel,
1575
1675
  });
1676
+ const installedKeys = options.session.getProfileInstalledOverrideKeys?.();
1677
+ const installedState = options.session.getProfileInstalledOverrideState?.();
1678
+ const baseModelRoles = { ...(options.settings.getOverride("modelRoles") ?? {}) };
1679
+ const baseAgentModelOverrides = { ...(options.settings.getOverride("task.agentModelOverrides") ?? {}) };
1680
+ const manualModelRoleOverrides = new Set(
1681
+ options.preserveManualOverrides ? (installedState?.manualModelRoles ?? []) : [],
1682
+ );
1683
+ const manualAgentModelOverrides = new Set(
1684
+ options.preserveManualOverrides ? (installedState?.manualAgentModelOverrides ?? []) : [],
1685
+ );
1686
+ const configuredAgentModelOverrides = options.modelRegistry?.getConfiguredModelBindings?.()?.agentModelOverrides;
1687
+ for (const role of installedKeys?.modelRoles ?? []) {
1688
+ if (options.preserveManualOverrides && manualModelRoleOverrides.has(role)) continue;
1689
+ const installed = installedState?.installedModelRoles.get(role);
1690
+ if (
1691
+ options.preserveManualOverrides &&
1692
+ installed !== undefined &&
1693
+ !selectorValuesEqual(baseModelRoles[role], installed)
1694
+ ) {
1695
+ manualModelRoleOverrides.add(role);
1696
+ continue;
1697
+ }
1698
+ delete baseModelRoles[role];
1699
+ }
1700
+ for (const role of installedKeys?.agentModelOverrides ?? []) {
1701
+ if (options.preserveManualOverrides && manualAgentModelOverrides.has(role)) continue;
1702
+ const installed = installedState?.installedAgentModelOverrides.get(role);
1703
+ if (
1704
+ options.preserveManualOverrides &&
1705
+ installed !== undefined &&
1706
+ !selectorValuesEqual(baseAgentModelOverrides[role], installed)
1707
+ ) {
1708
+ manualAgentModelOverrides.add(role);
1709
+ continue;
1710
+ }
1711
+ delete baseAgentModelOverrides[role];
1712
+ }
1713
+ // Compare the current session override with the installed profile value
1714
+ // before excluding configured bindings from the successor baseline. The
1715
+ // current value may itself be the installed profile value; filtering first
1716
+ // would misclassify that role as a manual override during configuration reload.
1717
+ for (const role of Object.keys(configuredAgentModelOverrides ?? {})) {
1718
+ if (!manualAgentModelOverrides.has(role)) delete baseAgentModelOverrides[role];
1719
+ }
1720
+ for (const [role, value] of installedState?.modelRoles ?? []) {
1721
+ if (manualModelRoleOverrides.has(role)) continue;
1722
+ if (value === undefined) delete baseModelRoles[role];
1723
+ else baseModelRoles[role] = value;
1724
+ }
1725
+ for (const [role, value] of installedState?.agentModelOverrides ?? []) {
1726
+ if (manualAgentModelOverrides.has(role)) continue;
1727
+ const configuredBinding = configuredAgentModelOverrides?.[role];
1728
+ if (configuredBinding !== undefined) {
1729
+ // Configured bindings are reapplied when the active profile is cleared;
1730
+ // leaving them in this baseline makes omitted successor roles stay live.
1731
+ delete baseAgentModelOverrides[role];
1732
+ } else if (installedState?.configuredAgentModelOverrides.has(role)) {
1733
+ // A removed binding must not revive its former value from the profile
1734
+ // baseline after a configuration reload.
1735
+ delete baseAgentModelOverrides[role];
1736
+ } else if (value === undefined) {
1737
+ delete baseAgentModelOverrides[role];
1738
+ } else {
1739
+ baseAgentModelOverrides[role] = value;
1740
+ }
1741
+ }
1742
+ const preparedDefaultModelSelection =
1743
+ options.prepareSessionModelSelection && defaultModel
1744
+ ? await options.session.prepareModelSelectionForProfileActivation?.(
1745
+ defaultModel,
1746
+ defaultThinkingLevel,
1747
+ options.signal,
1748
+ )
1749
+ : undefined;
1750
+ if (options.prepareSessionModelSelection && defaultModel && !preparedDefaultModelSelection) {
1751
+ throw new Error("Session cannot prepare a synchronous profile model selection");
1752
+ }
1753
+ options.signal?.throwIfAborted();
1754
+ const nextModelRoles = { ...modelRoles };
1755
+ const nextAgentModelOverrides = { ...agentModelOverrides };
1756
+ for (const role of manualModelRoleOverrides) delete nextModelRoles[role];
1757
+ for (const role of manualAgentModelOverrides) delete nextAgentModelOverrides[role];
1576
1758
 
1577
1759
  return {
1578
1760
  profileName,
1579
1761
  session: options.session as PreparedModelProfileActivation["session"],
1580
- settings: options.settings as PreparedModelProfileActivation["settings"],
1762
+ settings: applySettings as PreparedModelProfileActivation["settings"],
1581
1763
  modelRegistry: options.modelRegistry,
1582
1764
  previousModel,
1583
1765
  previousCanonicalVariant,
1584
1766
  previousUserCanonicalVariantSelection,
1585
1767
  previousThinkingLevel: options.session.thinkingLevel,
1586
- previousAgentModelOverrides: { ...options.settings.get("task.agentModelOverrides") },
1587
- previousModelRoles: { ...options.settings.get("modelRoles") },
1588
- baseAgentModelOverrides: Object.fromEntries(
1589
- Object.entries(options.settings.get("task.agentModelOverrides") ?? {}).filter(
1590
- ([key]) =>
1591
- !(options.session.getProfileInstalledOverrideKeys?.().agentModelOverrides ?? []).includes(key),
1592
- ),
1593
- ),
1594
- baseModelRoles: Object.fromEntries(
1595
- Object.entries(options.settings.get("modelRoles") ?? {}).filter(
1596
- ([key]) => !(options.session.getProfileInstalledOverrideKeys?.().modelRoles ?? []).includes(key),
1597
- ),
1598
- ),
1599
- previousPersistedModelRoles: options.settings.getGlobal("modelRoles"),
1600
- previousPersistedAgentModelOverrides: options.settings.getGlobal("task.agentModelOverrides"),
1601
- previousModelRolesOverride: options.settings.getOverride("modelRoles"),
1602
- previousAgentModelOverridesOverride: options.settings.getOverride("task.agentModelOverrides"),
1603
- previousDefaultProfileOverride: options.settings.getOverride("modelProfile.default"),
1604
- previousPersistedDefaultProfile: options.settings.getGlobal("modelProfile.default"),
1605
- previousPersistedDefaultThinkingLevel: options.settings.getGlobal("defaultThinkingLevel") as
1768
+ previousAgentModelOverrides: { ...applySettings.get("task.agentModelOverrides") },
1769
+ previousModelRoles: { ...applySettings.get("modelRoles") },
1770
+ baseAgentModelOverrides,
1771
+ baseModelRoles,
1772
+ previousPersistedModelRoles: applySettings.getGlobal("modelRoles"),
1773
+ previousPersistedAgentModelOverrides: applySettings.getGlobal("task.agentModelOverrides"),
1774
+ previousModelRolesOverride: applySettings.getOverride("modelRoles"),
1775
+ previousAgentModelOverridesOverride: applySettings.getOverride("task.agentModelOverrides"),
1776
+ previousDefaultProfileOverride: applySettings.getOverride("modelProfile.default"),
1777
+ previousPersistedDefaultProfile: applySettings.getGlobal("modelProfile.default"),
1778
+ previousPersistedDefaultThinkingLevel: applySettings.getGlobal("defaultThinkingLevel") as
1606
1779
  | Exclude<ThinkingLevel, "inherit">
1607
1780
  | undefined,
1608
- previousDefaultThinkingLevelOverride: options.settings.getOverride("defaultThinkingLevel"),
1781
+ previousDefaultThinkingLevelOverride: applySettings.getOverride("defaultThinkingLevel"),
1609
1782
  previousDefaultChain: options.session.getConfiguredModelChain("default"),
1610
1783
  previousDefaultChainState: options.session.getConfiguredModelChainState?.("default"),
1784
+ previousProfileInstalledOverrideState: installedState,
1785
+ preparedDefaultModelSelection,
1786
+ defaultChainPublished: false,
1787
+ defaultModelPublished: false,
1788
+ publishedState: undefined,
1611
1789
 
1612
1790
  defaultModel,
1613
1791
  defaultThinkingLevel,
1614
1792
  defaultActiveIndex,
1615
1793
  defaultResolutionSkips,
1616
1794
  defaultChain,
1617
- modelRoles,
1618
- agentModelOverrides,
1795
+ modelRoles: nextModelRoles,
1796
+ agentModelOverrides: nextAgentModelOverrides,
1797
+ manualModelRoleOverrides,
1798
+ manualAgentModelOverrides,
1619
1799
  previousActiveModelProfile: options.session.getActiveModelProfile?.(),
1620
1800
  previousSessionDefaultModel: options.session.getSessionDefaultModelSelector?.(),
1621
1801
  previousDefaultFallbackRuntimeState: options.session.getDefaultFallbackRuntimeState?.(),
@@ -1707,34 +1887,44 @@ function restoreConcurrentProfileOverrides(
1707
1887
  return Object.keys(restored).length > 0 || previous !== undefined ? restored : undefined;
1708
1888
  }
1709
1889
 
1710
- export async function applyPreparedModelProfileActivation(
1890
+ function capturePublishedModelProfileActivationState(
1711
1891
  prepared: PreparedModelProfileActivation,
1712
- options: ApplyModelProfileActivationOptions = {},
1713
- ): Promise<void> {
1714
- if (options.isCurrent && !options.isCurrent()) return;
1715
- let activationStage = "default chain";
1716
- let modelMutationStarted = false;
1717
- let overridesChanged = false;
1718
- let modelRolesChanged = false;
1719
- let persistentMutationStarted = false;
1720
- let defaultChainChanged = false;
1721
- let resumeDefaultChanged = false;
1722
- let activatedDefaultChainState: ConfiguredModelChainState | undefined;
1723
- let activatedFallbackRuntimeState: DefaultFallbackRuntimeState | undefined;
1724
- let activatedModelRolesOverride: unknown;
1725
- let activatedAgentModelOverridesOverride: unknown;
1892
+ ): PublishedModelProfileActivationState {
1893
+ return {
1894
+ model: prepared.session.model,
1895
+ thinkingLevel: prepared.session.thinkingLevel,
1896
+ modelRolesOverride: prepared.settings.getOverride("modelRoles"),
1897
+ agentModelOverridesOverride: prepared.settings.getOverride("task.agentModelOverrides"),
1898
+ defaultChainState: prepared.session.getConfiguredModelChainState?.("default"),
1899
+ defaultFallbackRuntimeState: prepared.session.getDefaultFallbackRuntimeState?.(),
1900
+ activeModelProfile: prepared.session.getActiveModelProfile?.(),
1901
+ profileInstalledOverrideState: prepared.session.getProfileInstalledOverrideState?.(),
1902
+ sessionDefaultModel: prepared.session.getSessionDefaultModelSelector?.(),
1903
+ persistedModelRoles: prepared.settings.getGlobal("modelRoles"),
1904
+ persistedAgentModelOverrides: prepared.settings.getGlobal("task.agentModelOverrides"),
1905
+ persistedDefaultProfile: prepared.settings.getGlobal("modelProfile.default"),
1906
+ persistedDefaultThinkingLevel: prepared.settings.getGlobal("defaultThinkingLevel"),
1907
+ defaultThinkingLevelOverride: prepared.settings.getOverride("defaultThinkingLevel"),
1908
+ defaultProfileOverride: prepared.settings.getOverride("modelProfile.default"),
1909
+ };
1910
+ }
1726
1911
 
1727
- try {
1728
- // Set the fallback chain before yielding to setModelTemporary, so a user
1729
- // selection during async mutation uses the fallback chain as-is rather than
1730
- // having the profile's chain written over a concurrent user choice.
1912
+ /** Publish the prepared model selection and role layer without awaiting. */
1913
+ export function publishPreparedModelProfileActivation(
1914
+ prepared: PreparedModelProfileActivation,
1915
+ options: Pick<
1916
+ ApplyModelProfileActivationOptions,
1917
+ "preserveDefaultModelSelection" | "preserveLiveModelSelection"
1918
+ > = {},
1919
+ ): void {
1920
+ if (!options.preserveDefaultModelSelection && prepared.preparedDefaultModelSelection) {
1731
1921
  const ownedDefaultChain =
1732
1922
  prepared.defaultChain.length > 0
1733
1923
  ? prepared.defaultChain
1734
1924
  : (prepared.previousDefaultChain ??
1735
1925
  (prepared.previousModel ? [formatModelString(prepared.previousModel)] : []));
1736
1926
  if (ownedDefaultChain.length > 0) {
1737
- defaultChainChanged = true;
1927
+ prepared.defaultChainPublished = true;
1738
1928
  prepared.session.setConfiguredModelChain(
1739
1929
  "default",
1740
1930
  ownedDefaultChain,
@@ -1748,44 +1938,151 @@ export async function applyPreparedModelProfileActivation(
1748
1938
  prepared.defaultResolutionSkips,
1749
1939
  );
1750
1940
  }
1751
- activatedDefaultChainState = prepared.session.getConfiguredModelChainState?.("default");
1752
- activatedFallbackRuntimeState = prepared.session.getDefaultFallbackRuntimeState?.();
1753
1941
  }
1754
- if (prepared.defaultModel) {
1942
+ }
1943
+ if (
1944
+ !options.preserveDefaultModelSelection &&
1945
+ !options.preserveLiveModelSelection &&
1946
+ prepared.preparedDefaultModelSelection
1947
+ ) {
1948
+ prepared.defaultModelPublished = true;
1949
+ if (!prepared.session.commitPreparedProfileModelSelection) {
1950
+ throw new Error("Session cannot commit a prepared profile model selection");
1951
+ }
1952
+ prepared.session.commitPreparedProfileModelSelection(prepared.preparedDefaultModelSelection);
1953
+ (prepared.session.modelRegistry ?? prepared.modelRegistry).seedCanonicalVariant?.(
1954
+ prepared.session.sessionId,
1955
+ prepared.defaultModel!,
1956
+ );
1957
+ prepared.session.recordResumeDefaultModel?.(`${prepared.defaultModel!.provider}/${prepared.defaultModel!.id}`);
1958
+ }
1959
+ prepared.settings.override("modelRoles", {
1960
+ ...prepared.baseModelRoles,
1961
+ ...prepared.modelRoles,
1962
+ });
1963
+ prepared.settings.override("task.agentModelOverrides", {
1964
+ ...prepared.baseAgentModelOverrides,
1965
+ ...prepared.agentModelOverrides,
1966
+ });
1967
+ prepared.session.setActiveModelProfile?.(prepared.profileName);
1968
+ prepared.session.noteProfileInstalledOverrides?.(
1969
+ Object.keys(prepared.modelRoles),
1970
+ Object.keys(prepared.agentModelOverrides),
1971
+ prepared.previousModel,
1972
+ { modelRoles: prepared.baseModelRoles, agentModelOverrides: prepared.baseAgentModelOverrides },
1973
+ { modelRoles: prepared.modelRoles, agentModelOverrides: prepared.agentModelOverrides },
1974
+ {
1975
+ modelRoles: [...prepared.manualModelRoleOverrides],
1976
+ agentModelOverrides: [...prepared.manualAgentModelOverrides],
1977
+ },
1978
+ );
1979
+ prepared.publishedState = capturePublishedModelProfileActivationState(prepared);
1980
+ }
1981
+
1982
+ export async function finishPreparedModelProfileActivation(prepared: PreparedModelProfileActivation): Promise<void> {
1983
+ if (prepared.defaultModelPublished && prepared.preparedDefaultModelSelection) {
1984
+ try {
1985
+ await prepared.session.finishPreparedProfileModelSelection?.(prepared.preparedDefaultModelSelection);
1986
+ } catch (error) {
1987
+ logger.warn("Failed to refresh session tools after model profile activation", {
1988
+ profile: prepared.profileName,
1989
+ error: error instanceof Error ? error.message : String(error),
1990
+ });
1991
+ }
1992
+ }
1993
+ try {
1994
+ await prepared.session.syncEagerDelegation?.();
1995
+ } catch (error) {
1996
+ logger.warn("Failed to sync eager delegation after model profile activation", {
1997
+ profile: prepared.profileName,
1998
+ error: error instanceof Error ? error.message : String(error),
1999
+ });
2000
+ }
2001
+ }
2002
+
2003
+ export async function applyPreparedModelProfileActivation(
2004
+ prepared: PreparedModelProfileActivation,
2005
+ options: ApplyModelProfileActivationOptions = {},
2006
+ ): Promise<void> {
2007
+ let activationStage = "default chain";
2008
+ let modelMutationStarted = false;
2009
+ let overridesChanged = false;
2010
+ let modelRolesChanged = false;
2011
+ let persistentMutationStarted = false;
2012
+ let defaultChainChanged = false;
2013
+ let resumeDefaultChanged = false;
2014
+ let activatedDefaultChainState: ConfiguredModelChainState | undefined;
2015
+ let activatedFallbackRuntimeState: DefaultFallbackRuntimeState | undefined;
2016
+ let activatedModelRolesOverride: unknown;
2017
+ let activatedAgentModelOverridesOverride: unknown;
2018
+ let activatedProfileInstalledOverrideState: AgentSessionProfileInstalledOverrideState | undefined;
2019
+ let activatedResumeDefaultModel: string | undefined;
2020
+
2021
+ try {
2022
+ if (!options.deferRuntimePublication && !options.preserveDefaultModelSelection) {
2023
+ const ownedDefaultChain =
2024
+ prepared.defaultChain.length > 0
2025
+ ? prepared.defaultChain
2026
+ : (prepared.previousDefaultChain ??
2027
+ (prepared.previousModel ? [formatModelString(prepared.previousModel)] : []));
2028
+ if (ownedDefaultChain.length > 0) {
2029
+ defaultChainChanged = true;
2030
+ prepared.session.setConfiguredModelChain(
2031
+ "default",
2032
+ ownedDefaultChain,
2033
+ "profile-activation",
2034
+ prepared.profileName,
2035
+ true,
2036
+ );
2037
+ if (prepared.defaultActiveIndex !== undefined) {
2038
+ prepared.session.seedDefaultFallbackResolution?.(
2039
+ prepared.defaultActiveIndex,
2040
+ prepared.defaultResolutionSkips,
2041
+ );
2042
+ }
2043
+ activatedDefaultChainState = prepared.session.getConfiguredModelChainState?.("default");
2044
+ activatedFallbackRuntimeState = prepared.session.getDefaultFallbackRuntimeState?.();
2045
+ }
2046
+ }
2047
+ if (
2048
+ !options.deferRuntimePublication &&
2049
+ !options.preserveDefaultModelSelection &&
2050
+ !options.preserveLiveModelSelection &&
2051
+ prepared.defaultModel
2052
+ ) {
1755
2053
  activationStage = "model selection";
1756
2054
  await prepared.session.setModelTemporary(
1757
2055
  prepared.defaultModel,
1758
2056
  options.thinkingLevelOverride ?? prepared.defaultThinkingLevel,
1759
2057
  {
1760
2058
  cause: "profile-activation",
2059
+ signal: options.signal,
1761
2060
  shouldMutate: options.isCurrent,
1762
2061
  onMutationStarted: () => {
1763
2062
  modelMutationStarted = true;
1764
2063
  },
1765
2064
  },
1766
2065
  );
2066
+ options.signal?.throwIfAborted();
1767
2067
  if (options.isCurrent && !modelMutationStarted) throw new ModelProfileActivationSupersededError();
1768
2068
  if (options.isCurrent && !options.isCurrent()) throw new ModelProfileActivationSupersededError();
1769
2069
  }
1770
- // Always reinstall the model role layer from the durable base plus the
1771
- // new profile's roles so omitted roles from the previous profile are dropped.
1772
- activationStage = "model role overrides";
1773
- prepared.settings.override("modelRoles", {
1774
- ...prepared.baseModelRoles,
1775
- ...prepared.modelRoles,
1776
- });
1777
- activatedModelRolesOverride = prepared.settings.getOverride("modelRoles");
1778
- modelRolesChanged = true;
1779
- // Always reinstall the agent role layer from the durable base plus the
1780
- // new profile's roles: a default-only or role-free successor must drop
1781
- // the previous profile's role-agent mappings rather than inheriting them.
1782
- activationStage = "agent role overrides";
1783
- prepared.settings.override("task.agentModelOverrides", {
1784
- ...prepared.baseAgentModelOverrides,
1785
- ...prepared.agentModelOverrides,
1786
- });
1787
- activatedAgentModelOverridesOverride = prepared.settings.getOverride("task.agentModelOverrides");
1788
- overridesChanged = true;
2070
+ if (!options.deferRuntimePublication) {
2071
+ activationStage = "model role overrides";
2072
+ prepared.settings.override("modelRoles", {
2073
+ ...prepared.baseModelRoles,
2074
+ ...prepared.modelRoles,
2075
+ });
2076
+ activatedModelRolesOverride = prepared.settings.getOverride("modelRoles");
2077
+ modelRolesChanged = true;
2078
+ activationStage = "agent role overrides";
2079
+ prepared.settings.override("task.agentModelOverrides", {
2080
+ ...prepared.baseAgentModelOverrides,
2081
+ ...prepared.agentModelOverrides,
2082
+ });
2083
+ activatedAgentModelOverridesOverride = prepared.settings.getOverride("task.agentModelOverrides");
2084
+ overridesChanged = true;
2085
+ }
1789
2086
  if (options.persistDefault) {
1790
2087
  persistentMutationStarted = true;
1791
2088
  activationStage = "persist model roles";
@@ -1802,28 +2099,46 @@ export async function applyPreparedModelProfileActivation(
1802
2099
  await prepared.settings.flushOrThrow();
1803
2100
  }
1804
2101
  if (options.isCurrent && !options.isCurrent()) throw new ModelProfileActivationSupersededError();
1805
- activationStage = "active profile marker";
1806
- prepared.session.setActiveModelProfile?.(prepared.profileName);
1807
- if (prepared.defaultModel) {
2102
+ if (!options.deferRuntimePublication) {
2103
+ activationStage = "active profile marker";
2104
+ prepared.session.setActiveModelProfile?.(prepared.profileName);
2105
+ activationStage = "installed role tracking";
2106
+ prepared.session.noteProfileInstalledOverrides?.(
2107
+ Object.keys(prepared.modelRoles),
2108
+ Object.keys(prepared.agentModelOverrides),
2109
+ prepared.previousModel,
2110
+ { modelRoles: prepared.baseModelRoles, agentModelOverrides: prepared.baseAgentModelOverrides },
2111
+ { modelRoles: prepared.modelRoles, agentModelOverrides: prepared.agentModelOverrides },
2112
+ {
2113
+ modelRoles: [...prepared.manualModelRoleOverrides],
2114
+ agentModelOverrides: [...prepared.manualAgentModelOverrides],
2115
+ },
2116
+ );
2117
+ activatedProfileInstalledOverrideState = prepared.session.getProfileInstalledOverrideState?.();
2118
+ }
2119
+ if (
2120
+ !options.deferRuntimePublication &&
2121
+ !options.preserveDefaultModelSelection &&
2122
+ !options.preserveLiveModelSelection &&
2123
+ prepared.defaultModel
2124
+ ) {
1808
2125
  activationStage = "canonical model variant";
1809
2126
  prepared.modelRegistry.seedCanonicalVariant?.(prepared.session.sessionId, prepared.defaultModel);
1810
2127
  resumeDefaultChanged = true;
1811
2128
  activationStage = "resume default model";
1812
- prepared.session.recordResumeDefaultModel?.(`${prepared.defaultModel.provider}/${prepared.defaultModel.id}`);
2129
+ activatedResumeDefaultModel = `${prepared.defaultModel.provider}/${prepared.defaultModel.id}`;
2130
+ prepared.session.recordResumeDefaultModel?.(activatedResumeDefaultModel);
1813
2131
  }
1814
- activationStage = "installed role tracking";
1815
- prepared.session.noteProfileInstalledOverrides?.(
1816
- Object.keys(prepared.modelRoles),
1817
- Object.keys(prepared.agentModelOverrides),
1818
- prepared.previousModel,
1819
- );
2132
+ prepared.publishedState = capturePublishedModelProfileActivationState(prepared);
1820
2133
  } catch (error) {
1821
2134
  const activationWasCancelled = error instanceof ModelProfileActivationSupersededError;
1822
- const selectionSuperseded = options.isCurrent?.() === false;
2135
+ const selectionIsCurrent = () => options.isCurrent?.() !== false;
1823
2136
  const shouldRestoreFallbackRuntimeState =
1824
2137
  prepared.previousDefaultFallbackRuntimeState !== undefined &&
1825
- (!selectionSuperseded ||
2138
+ (selectionIsCurrent() ||
1826
2139
  sameSerializedValue(prepared.session.getDefaultFallbackRuntimeState?.(), activatedFallbackRuntimeState));
2140
+ let modelRollbackSelectionRevision: number | undefined;
2141
+ let modelRollbackSelectionWasCurrent = false;
1827
2142
  const rollbackErrors: Array<{ stage: string; error: unknown }> = [];
1828
2143
  const restore = (stage: string, action: () => void): void => {
1829
2144
  try {
@@ -1834,7 +2149,7 @@ export async function applyPreparedModelProfileActivation(
1834
2149
  };
1835
2150
  if (persistentMutationStarted) {
1836
2151
  if (
1837
- !selectionSuperseded ||
2152
+ selectionIsCurrent() ||
1838
2153
  sameSerializedValue(prepared.settings.getGlobal("modelProfile.default"), prepared.profileName)
1839
2154
  ) {
1840
2155
  restore("restore default profile setting", () =>
@@ -1845,7 +2160,7 @@ export async function applyPreparedModelProfileActivation(
1845
2160
  }
1846
2161
  const currentPersistedModelRoles = prepared.settings.getGlobal("modelRoles");
1847
2162
  const restoredModelRoles =
1848
- selectionSuperseded && currentPersistedModelRoles && !sameSerializedValue(currentPersistedModelRoles, {})
2163
+ !selectionIsCurrent() && currentPersistedModelRoles && !sameSerializedValue(currentPersistedModelRoles, {})
1849
2164
  ? { ...(prepared.previousPersistedModelRoles ?? {}), ...currentPersistedModelRoles }
1850
2165
  : prepared.previousPersistedModelRoles;
1851
2166
  restore("restore model role setting", () =>
@@ -1855,7 +2170,7 @@ export async function applyPreparedModelProfileActivation(
1855
2170
  );
1856
2171
  const currentPersistedAgentModelOverrides = prepared.settings.getGlobal("task.agentModelOverrides");
1857
2172
  const restoredPersistedAgentModelOverrides =
1858
- selectionSuperseded &&
2173
+ !selectionIsCurrent() &&
1859
2174
  currentPersistedAgentModelOverrides &&
1860
2175
  !sameSerializedValue(currentPersistedAgentModelOverrides, {})
1861
2176
  ? {
@@ -1873,7 +2188,7 @@ export async function applyPreparedModelProfileActivation(
1873
2188
  ? prepared.defaultThinkingLevel
1874
2189
  : prepared.previousPersistedDefaultThinkingLevel;
1875
2190
  if (
1876
- !selectionSuperseded ||
2191
+ selectionIsCurrent() ||
1877
2192
  sameSerializedValue(prepared.settings.getGlobal("defaultThinkingLevel"), activatedThinkingLevel)
1878
2193
  ) {
1879
2194
  restore("restore thinking level setting", () =>
@@ -1885,7 +2200,7 @@ export async function applyPreparedModelProfileActivation(
1885
2200
  }
1886
2201
  if (
1887
2202
  modelRolesChanged &&
1888
- (!selectionSuperseded ||
2203
+ (selectionIsCurrent() ||
1889
2204
  sameSerializedValue(prepared.settings.getOverride("modelRoles"), activatedModelRolesOverride))
1890
2205
  ) {
1891
2206
  restore("restore model role overrides", () =>
@@ -1896,7 +2211,7 @@ export async function applyPreparedModelProfileActivation(
1896
2211
  }
1897
2212
  if (
1898
2213
  overridesChanged &&
1899
- (!selectionSuperseded ||
2214
+ (selectionIsCurrent() ||
1900
2215
  sameSerializedValue(
1901
2216
  prepared.settings.getOverride("task.agentModelOverrides"),
1902
2217
  activatedAgentModelOverridesOverride,
@@ -1908,7 +2223,9 @@ export async function applyPreparedModelProfileActivation(
1908
2223
  : prepared.settings.override("task.agentModelOverrides", prepared.previousAgentModelOverridesOverride),
1909
2224
  );
1910
2225
  }
1911
- if (modelMutationStarted && !selectionSuperseded) {
2226
+ if (modelMutationStarted && selectionIsCurrent()) {
2227
+ modelRollbackSelectionWasCurrent = selectionIsCurrent();
2228
+ modelRollbackSelectionRevision = prepared.session.getUserModelSelectionRevision?.();
1912
2229
  try {
1913
2230
  if (prepared.session.restoreModelSelectionForRollback) {
1914
2231
  await prepared.session.restoreModelSelectionForRollback(
@@ -1918,6 +2235,7 @@ export async function applyPreparedModelProfileActivation(
1918
2235
  } else if (prepared.previousModel) {
1919
2236
  await prepared.session.setModelTemporary(prepared.previousModel, prepared.previousThinkingLevel, {
1920
2237
  cause: "rollback",
2238
+ shouldMutate: selectionIsCurrent,
1921
2239
  });
1922
2240
  } else {
1923
2241
  throw new Error("Model-less profile activation rollback is unavailable");
@@ -1926,14 +2244,18 @@ export async function applyPreparedModelProfileActivation(
1926
2244
  rollbackErrors.push({ stage: "restore live model", error: rollbackError });
1927
2245
  }
1928
2246
  }
1929
- if (resumeDefaultChanged) {
2247
+ if (
2248
+ resumeDefaultChanged &&
2249
+ selectionIsCurrent() &&
2250
+ sameSerializedValue(prepared.session.getSessionDefaultModelSelector?.(), activatedResumeDefaultModel)
2251
+ ) {
1930
2252
  restore("restore resume default", () =>
1931
2253
  prepared.session.recordResumeDefaultModel?.(prepared.previousSessionDefaultModel),
1932
2254
  );
1933
2255
  }
1934
2256
  if (
1935
2257
  defaultChainChanged &&
1936
- (!selectionSuperseded ||
2258
+ (selectionIsCurrent() ||
1937
2259
  sameSerializedValue(prepared.session.getConfiguredModelChainState?.("default"), activatedDefaultChainState))
1938
2260
  ) {
1939
2261
  const previousChain = prepared.previousDefaultChainState;
@@ -1949,16 +2271,33 @@ export async function applyPreparedModelProfileActivation(
1949
2271
  ),
1950
2272
  );
1951
2273
  }
1952
- if (shouldRestoreFallbackRuntimeState) {
2274
+ const userSelectionChangedDuringModelRollback =
2275
+ modelRollbackSelectionRevision !== undefined
2276
+ ? prepared.session.getUserModelSelectionRevision?.() !== modelRollbackSelectionRevision
2277
+ : modelRollbackSelectionWasCurrent && !selectionIsCurrent();
2278
+ if (shouldRestoreFallbackRuntimeState && !userSelectionChangedDuringModelRollback) {
1953
2279
  restore("restore fallback runtime", () =>
1954
2280
  prepared.session.restoreDefaultFallbackRuntimeState?.(prepared.previousDefaultFallbackRuntimeState!),
1955
2281
  );
1956
2282
  }
1957
- if (!selectionSuperseded) {
2283
+ if (selectionIsCurrent()) {
1958
2284
  restore("restore active profile", () =>
1959
2285
  prepared.session.setActiveModelProfile?.(prepared.previousActiveModelProfile),
1960
2286
  );
1961
2287
  }
2288
+ if (
2289
+ prepared.previousProfileInstalledOverrideState &&
2290
+ (selectionIsCurrent() ||
2291
+ (activatedProfileInstalledOverrideState !== undefined &&
2292
+ util.isDeepStrictEqual(
2293
+ prepared.session.getProfileInstalledOverrideState?.(),
2294
+ activatedProfileInstalledOverrideState,
2295
+ )))
2296
+ ) {
2297
+ restore("restore installed role tracking", () =>
2298
+ prepared.session.restoreProfileInstalledOverrideState?.(prepared.previousProfileInstalledOverrideState!),
2299
+ );
2300
+ }
1962
2301
  restore("restore canonical model variant", () =>
1963
2302
  restoreCanonicalVariantAfterPreparation(
1964
2303
  prepared.modelRegistry,
@@ -1980,17 +2319,254 @@ export async function applyPreparedModelProfileActivation(
1980
2319
  if (activationWasCancelled) return;
1981
2320
  throw error;
1982
2321
  }
1983
- // The installed role layer decides whether this profile is vendor-separated,
1984
- // so delegation must be re-applied to the live session rather than only to
1985
- // sessions started after activation. Activation itself already succeeded; a
1986
- // failed refresh must not roll it back.
1987
- try {
1988
- await prepared.session.syncEagerDelegation?.();
1989
- } catch (error) {
1990
- logger.warn("Failed to sync eager delegation after model profile activation", {
1991
- profile: prepared.profileName,
1992
- error: error instanceof Error ? error.message : String(error),
1993
- });
2322
+ if (!options.deferRuntimePublication) await finishPreparedModelProfileActivation(prepared);
2323
+ }
2324
+
2325
+ /** Restore an applied activation when its enclosing settings/registry commit fails. */
2326
+ export async function rollbackPreparedModelProfileActivation(
2327
+ prepared: PreparedModelProfileActivation,
2328
+ options: ApplyModelProfileActivationOptions = {},
2329
+ ): Promise<void> {
2330
+ const failures: Array<{ stage: string; error: unknown }> = [];
2331
+ const restore = (stage: string, action: () => void): void => {
2332
+ try {
2333
+ action();
2334
+ } catch (error) {
2335
+ failures.push({ stage, error });
2336
+ }
2337
+ };
2338
+ const restoreAsync = async (stage: string, action: () => Promise<void>): Promise<void> => {
2339
+ try {
2340
+ await action();
2341
+ } catch (error) {
2342
+ failures.push({ stage, error });
2343
+ }
2344
+ };
2345
+ const selectionIsCurrent = () => options.isCurrent?.() !== false;
2346
+ const publishedState = prepared.publishedState;
2347
+ const canRestorePublishedState = (current: unknown, activated: unknown): boolean =>
2348
+ publishedState === undefined ? selectionIsCurrent() : util.isDeepStrictEqual(current, activated);
2349
+ const canRollbackPublishedProfile = (): boolean =>
2350
+ publishedState === undefined
2351
+ ? selectionIsCurrent()
2352
+ : util.isDeepStrictEqual(prepared.session.getActiveModelProfile?.(), publishedState.activeModelProfile);
2353
+
2354
+ if (options.persistDefault) {
2355
+ if (
2356
+ canRestorePublishedState(
2357
+ prepared.settings.getGlobal("modelProfile.default"),
2358
+ publishedState?.persistedDefaultProfile,
2359
+ )
2360
+ ) {
2361
+ restore("restore persisted default profile", () =>
2362
+ prepared.previousPersistedDefaultProfile === undefined
2363
+ ? prepared.settings.unset("modelProfile.default")
2364
+ : prepared.settings.set("modelProfile.default", prepared.previousPersistedDefaultProfile),
2365
+ );
2366
+ }
2367
+ const currentPersistedModelRoles = prepared.settings.getGlobal("modelRoles");
2368
+ const restoredPersistedModelRoles =
2369
+ publishedState && canRollbackPublishedProfile()
2370
+ ? restoreConcurrentProfileOverrides(
2371
+ prepared.previousPersistedModelRoles,
2372
+ publishedState.persistedModelRoles,
2373
+ currentPersistedModelRoles,
2374
+ )
2375
+ : publishedState === undefined && selectionIsCurrent()
2376
+ ? prepared.previousPersistedModelRoles
2377
+ : currentPersistedModelRoles;
2378
+ if (!util.isDeepStrictEqual(restoredPersistedModelRoles, currentPersistedModelRoles)) {
2379
+ restore("restore persisted model roles", () =>
2380
+ restoredPersistedModelRoles === undefined
2381
+ ? prepared.settings.unset("modelRoles")
2382
+ : prepared.settings.set("modelRoles", restoredPersistedModelRoles),
2383
+ );
2384
+ }
2385
+ const currentPersistedAgentOverrides = prepared.settings.getGlobal("task.agentModelOverrides");
2386
+ const restoredPersistedAgentOverrides =
2387
+ publishedState && canRollbackPublishedProfile()
2388
+ ? restoreConcurrentProfileOverrides(
2389
+ prepared.previousPersistedAgentModelOverrides,
2390
+ publishedState.persistedAgentModelOverrides,
2391
+ currentPersistedAgentOverrides,
2392
+ )
2393
+ : publishedState === undefined && selectionIsCurrent()
2394
+ ? prepared.previousPersistedAgentModelOverrides
2395
+ : currentPersistedAgentOverrides;
2396
+ if (!util.isDeepStrictEqual(restoredPersistedAgentOverrides, currentPersistedAgentOverrides)) {
2397
+ restore("restore persisted agent overrides", () =>
2398
+ restoredPersistedAgentOverrides === undefined
2399
+ ? prepared.settings.unset("task.agentModelOverrides")
2400
+ : prepared.settings.set("task.agentModelOverrides", restoredPersistedAgentOverrides),
2401
+ );
2402
+ }
2403
+ if (
2404
+ canRestorePublishedState(
2405
+ prepared.settings.getGlobal("defaultThinkingLevel"),
2406
+ publishedState?.persistedDefaultThinkingLevel,
2407
+ )
2408
+ ) {
2409
+ restore("restore persisted thinking level", () =>
2410
+ prepared.previousPersistedDefaultThinkingLevel === undefined
2411
+ ? prepared.settings.unset("defaultThinkingLevel")
2412
+ : prepared.settings.set("defaultThinkingLevel", prepared.previousPersistedDefaultThinkingLevel),
2413
+ );
2414
+ }
2415
+ if (
2416
+ canRestorePublishedState(
2417
+ prepared.settings.getOverride("defaultThinkingLevel"),
2418
+ publishedState?.defaultThinkingLevelOverride,
2419
+ )
2420
+ ) {
2421
+ restore("restore thinking level override", () =>
2422
+ prepared.previousDefaultThinkingLevelOverride === undefined
2423
+ ? prepared.settings.clearOverride("defaultThinkingLevel")
2424
+ : prepared.settings.override("defaultThinkingLevel", prepared.previousDefaultThinkingLevelOverride),
2425
+ );
2426
+ }
2427
+ }
2428
+ const currentModelRolesOverride = prepared.settings.getOverride("modelRoles");
2429
+ const restoredModelRolesOverride =
2430
+ publishedState && canRollbackPublishedProfile()
2431
+ ? restoreConcurrentProfileOverrides(
2432
+ prepared.previousModelRolesOverride,
2433
+ publishedState.modelRolesOverride,
2434
+ currentModelRolesOverride,
2435
+ )
2436
+ : publishedState === undefined && selectionIsCurrent()
2437
+ ? prepared.previousModelRolesOverride
2438
+ : currentModelRolesOverride;
2439
+ if (!util.isDeepStrictEqual(restoredModelRolesOverride, currentModelRolesOverride)) {
2440
+ restore("restore model role overrides", () =>
2441
+ restoredModelRolesOverride === undefined
2442
+ ? prepared.settings.clearOverride("modelRoles")
2443
+ : prepared.settings.override("modelRoles", restoredModelRolesOverride),
2444
+ );
2445
+ }
2446
+ const currentAgentModelOverridesOverride = prepared.settings.getOverride("task.agentModelOverrides");
2447
+ const restoredAgentModelOverridesOverride =
2448
+ publishedState && canRollbackPublishedProfile()
2449
+ ? restoreConcurrentProfileOverrides(
2450
+ prepared.previousAgentModelOverridesOverride,
2451
+ publishedState.agentModelOverridesOverride,
2452
+ currentAgentModelOverridesOverride,
2453
+ )
2454
+ : publishedState === undefined && selectionIsCurrent()
2455
+ ? prepared.previousAgentModelOverridesOverride
2456
+ : currentAgentModelOverridesOverride;
2457
+ if (!util.isDeepStrictEqual(restoredAgentModelOverridesOverride, currentAgentModelOverridesOverride)) {
2458
+ restore("restore agent role overrides", () =>
2459
+ restoredAgentModelOverridesOverride === undefined
2460
+ ? prepared.settings.clearOverride("task.agentModelOverrides")
2461
+ : prepared.settings.override("task.agentModelOverrides", restoredAgentModelOverridesOverride),
2462
+ );
2463
+ }
2464
+ if (!options.preserveDefaultModelSelection) {
2465
+ const previousChain = prepared.previousDefaultChainState;
2466
+ if (
2467
+ prepared.defaultChainPublished &&
2468
+ canRollbackPublishedProfile() &&
2469
+ canRestorePublishedState(
2470
+ prepared.session.getConfiguredModelChainState?.("default"),
2471
+ publishedState?.defaultChainState,
2472
+ )
2473
+ ) {
2474
+ restore("restore default chain", () =>
2475
+ prepared.session.setConfiguredModelChain(
2476
+ "default",
2477
+ previousChain?.entries ??
2478
+ prepared.previousDefaultChain ??
2479
+ (prepared.previousModel ? [`${prepared.previousModel.provider}/${prepared.previousModel.id}`] : []),
2480
+ previousChain?.origin ?? "rollback",
2481
+ previousChain?.identity,
2482
+ previousChain?.explicitHead ?? true,
2483
+ ),
2484
+ );
2485
+ }
2486
+ if (
2487
+ !options.preserveLiveModelSelection &&
2488
+ selectionIsCurrent() &&
2489
+ prepared.defaultModelPublished &&
2490
+ prepared.defaultModel
2491
+ ) {
2492
+ await restoreAsync("restore live model", async () => {
2493
+ if (prepared.session.restoreModelSelectionForRollback) {
2494
+ await prepared.session.restoreModelSelectionForRollback(
2495
+ prepared.previousModel,
2496
+ prepared.previousThinkingLevel,
2497
+ );
2498
+ } else if (prepared.previousModel) {
2499
+ await prepared.session.setModelTemporary(prepared.previousModel, prepared.previousThinkingLevel, {
2500
+ cause: "rollback",
2501
+ shouldMutate: selectionIsCurrent,
2502
+ });
2503
+ } else {
2504
+ throw new Error("Model-less profile activation rollback is unavailable");
2505
+ }
2506
+ });
2507
+ if (
2508
+ selectionIsCurrent() &&
2509
+ canRestorePublishedState(
2510
+ prepared.session.getSessionDefaultModelSelector?.(),
2511
+ publishedState?.sessionDefaultModel,
2512
+ )
2513
+ ) {
2514
+ restore("restore session resume default", () =>
2515
+ prepared.session.recordResumeDefaultModel?.(prepared.previousSessionDefaultModel),
2516
+ );
2517
+ }
2518
+ }
2519
+ if (
2520
+ (prepared.defaultChainPublished || prepared.defaultModelPublished) &&
2521
+ prepared.previousDefaultFallbackRuntimeState &&
2522
+ canRollbackPublishedProfile() &&
2523
+ canRestorePublishedState(
2524
+ prepared.session.getDefaultFallbackRuntimeState?.(),
2525
+ publishedState?.defaultFallbackRuntimeState,
2526
+ )
2527
+ ) {
2528
+ restore("restore fallback runtime", () =>
2529
+ prepared.session.restoreDefaultFallbackRuntimeState?.(prepared.previousDefaultFallbackRuntimeState!),
2530
+ );
2531
+ }
2532
+ }
2533
+ if (
2534
+ selectionIsCurrent() &&
2535
+ canRollbackPublishedProfile() &&
2536
+ canRestorePublishedState(prepared.session.getActiveModelProfile?.(), publishedState?.activeModelProfile)
2537
+ ) {
2538
+ restore("restore active profile", () =>
2539
+ prepared.session.setActiveModelProfile?.(prepared.previousActiveModelProfile),
2540
+ );
2541
+ }
2542
+ if (
2543
+ prepared.previousProfileInstalledOverrideState &&
2544
+ canRollbackPublishedProfile() &&
2545
+ canRestorePublishedState(
2546
+ prepared.session.getProfileInstalledOverrideState?.(),
2547
+ publishedState?.profileInstalledOverrideState,
2548
+ )
2549
+ ) {
2550
+ restore("restore installed role tracking", () =>
2551
+ prepared.session.restoreProfileInstalledOverrideState?.(prepared.previousProfileInstalledOverrideState!),
2552
+ );
2553
+ }
2554
+ restore("restore canonical model variant", () =>
2555
+ restoreCanonicalVariantAfterPreparation(
2556
+ prepared.session.modelRegistry ?? prepared.modelRegistry,
2557
+ prepared.session,
2558
+ prepared.previousCanonicalVariant,
2559
+ prepared.previousUserCanonicalVariantSelection,
2560
+ ),
2561
+ );
2562
+ if (options.persistDefault) {
2563
+ await restoreAsync("restore persisted settings", () => prepared.settings.flushOrThrow());
2564
+ }
2565
+ if (failures.length > 0) {
2566
+ throw new AggregateError(
2567
+ failures.map(failure => failure.error),
2568
+ `Failed to fully roll back model profile activation: ${failures.map(failure => failure.stage).join(", ")}`,
2569
+ );
1994
2570
  }
1995
2571
  }
1996
2572
 
@@ -2009,6 +2585,7 @@ export interface MaterializeModelProfileForDeletionResult {
2009
2585
  previousActiveModelProfile: string | undefined;
2010
2586
  previousDefaultChainState: ConfiguredModelChainState | undefined;
2011
2587
  previousDefaultFallbackRuntimeState: DefaultFallbackRuntimeState | undefined;
2588
+ previousProfileInstalledOverrideState: AgentSessionProfileInstalledOverrideState | undefined;
2012
2589
  /**
2013
2590
  * Restores the session sticky canonical variant that was snapshotted before
2014
2591
  * materialization cleared it. Internal closure capturing the registry,
@@ -2138,6 +2715,11 @@ export async function materializeModelProfileForDeletion(
2138
2715
  : prepared.settings.override("modelProfile.default", prepared.previousDefaultProfileOverride),
2139
2716
  );
2140
2717
  restore(() => prepared.session.setActiveModelProfile?.(prepared.previousActiveModelProfile));
2718
+ if (prepared.previousProfileInstalledOverrideState) {
2719
+ restore(() =>
2720
+ prepared.session.restoreProfileInstalledOverrideState?.(prepared.previousProfileInstalledOverrideState!),
2721
+ );
2722
+ }
2141
2723
  try {
2142
2724
  await prepared.settings.flushOrThrow();
2143
2725
  } catch (rollbackError) {
@@ -2167,6 +2749,7 @@ export async function materializeModelProfileForDeletion(
2167
2749
  previousActiveModelProfile: prepared.previousActiveModelProfile,
2168
2750
  previousDefaultChainState: prepared.previousDefaultChainState,
2169
2751
  previousDefaultFallbackRuntimeState: prepared.previousDefaultFallbackRuntimeState,
2752
+ previousProfileInstalledOverrideState: prepared.previousProfileInstalledOverrideState,
2170
2753
  restoreSessionCanonicalVariant: () =>
2171
2754
  restoreCanonicalVariant(prepared.modelRegistry, prepared.session.sessionId, prepared.previousCanonicalVariant),
2172
2755
  };
@@ -2176,7 +2759,10 @@ export async function restoreMaterializedModelProfileForDeletion(options: {
2176
2759
  settings: Pick<Settings, "clearOverride" | "flushOrThrow" | "override" | "set" | "unset">;
2177
2760
  session: Pick<
2178
2761
  ModelProfileActivationSession,
2179
- "setActiveModelProfile" | "setConfiguredModelChain" | "restoreDefaultFallbackRuntimeState"
2762
+ | "setActiveModelProfile"
2763
+ | "setConfiguredModelChain"
2764
+ | "restoreDefaultFallbackRuntimeState"
2765
+ | "restoreProfileInstalledOverrideState"
2180
2766
  >;
2181
2767
  snapshot: MaterializeModelProfileForDeletionResult;
2182
2768
  }): Promise<void> {
@@ -2231,6 +2817,13 @@ export async function restoreMaterializedModelProfileForDeletion(options: {
2231
2817
  : options.settings.override("modelProfile.default", options.snapshot.previousDefaultProfileOverride),
2232
2818
  );
2233
2819
  restore(() => options.session.setActiveModelProfile?.(options.snapshot.previousActiveModelProfile));
2820
+ if (options.snapshot.previousProfileInstalledOverrideState) {
2821
+ restore(() =>
2822
+ options.session.restoreProfileInstalledOverrideState?.(
2823
+ options.snapshot.previousProfileInstalledOverrideState!,
2824
+ ),
2825
+ );
2826
+ }
2234
2827
  try {
2235
2828
  await options.settings.flushOrThrow();
2236
2829
  } catch (error) {
@@ -2270,6 +2863,7 @@ export async function applyModelProfileRuntimeBindings(
2270
2863
  const previousModelRolesOverride = prepared.settings.getOverride("modelRoles");
2271
2864
  const previousAgentModelOverridesOverride = prepared.settings.getOverride("task.agentModelOverrides");
2272
2865
  const previousActiveModelProfile = prepared.session.getActiveModelProfile?.();
2866
+ const previousProfileInstalledOverrideState = prepared.session.getProfileInstalledOverrideState?.();
2273
2867
  let activatedModelRolesOverride: Readonly<Record<string, ModelSelectorValue>> | undefined;
2274
2868
  let activatedAgentModelOverridesOverride: Readonly<Record<string, ModelSelectorValue>> | undefined;
2275
2869
  try {
@@ -2312,6 +2906,9 @@ export async function applyModelProfileRuntimeBindings(
2312
2906
  if (prepared.session.getActiveModelProfile?.() === prepared.profileName) {
2313
2907
  prepared.session.setActiveModelProfile?.(previousActiveModelProfile);
2314
2908
  }
2909
+ if (previousProfileInstalledOverrideState) {
2910
+ prepared.session.restoreProfileInstalledOverrideState?.(previousProfileInstalledOverrideState);
2911
+ }
2315
2912
  try {
2316
2913
  await prepared.session.syncEagerDelegation?.();
2317
2914
  } catch (error) {
@@ -2326,6 +2923,12 @@ export async function applyModelProfileRuntimeBindings(
2326
2923
  Object.keys(prepared.modelRoles),
2327
2924
  Object.keys(prepared.agentModelOverrides),
2328
2925
  prepared.previousModel,
2926
+ { modelRoles: prepared.baseModelRoles, agentModelOverrides: prepared.baseAgentModelOverrides },
2927
+ { modelRoles: prepared.modelRoles, agentModelOverrides: prepared.agentModelOverrides },
2928
+ {
2929
+ modelRoles: [...prepared.manualModelRoleOverrides],
2930
+ agentModelOverrides: [...prepared.manualAgentModelOverrides],
2931
+ },
2329
2932
  );
2330
2933
  } finally {
2331
2934
  if (isCurrent && !isCurrent()) {