@gajae-code/coding-agent 0.18.11 → 0.19.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 (462) hide show
  1. package/CHANGELOG.md +760 -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/capability/rule.d.ts +7 -2
  7. package/dist/types/cli/accounts-cli.d.ts +2 -2
  8. package/dist/types/cli/append-system-prompt.d.ts +6 -0
  9. package/dist/types/cli/args.d.ts +7 -2
  10. package/dist/types/cli/customize-doctor-precedence.d.ts +16 -0
  11. package/dist/types/cli/customize-doctor.d.ts +4 -3
  12. package/dist/types/cli/doctor/install-repairs.d.ts +4 -1
  13. package/dist/types/cli/install-activation.d.ts +10 -4
  14. package/dist/types/cli/public-command-errors.d.ts +2 -0
  15. package/dist/types/cli/root-flags.d.ts +0 -12
  16. package/dist/types/cli/update-cli.d.ts +21 -3
  17. package/dist/types/cli-main.d.ts +0 -6
  18. package/dist/types/cli-ordinary.d.ts +2 -2
  19. package/dist/types/commands/accounts.d.ts +2 -1
  20. package/dist/types/commands/launch.d.ts +0 -6
  21. package/dist/types/commands/migrate.d.ts +1 -1
  22. package/dist/types/commands/sdk.d.ts +13 -0
  23. package/dist/types/commit/changelog/index.d.ts +1 -0
  24. package/dist/types/commit/map-reduce/index.d.ts +4 -4
  25. package/dist/types/config/autorouting-tier-map.d.ts +53 -1
  26. package/dist/types/config/config-hot-reload-trust.d.ts +12 -0
  27. package/dist/types/config/config-hot-reload.d.ts +44 -0
  28. package/dist/types/config/file-lock.d.ts +52 -3
  29. package/dist/types/config/model-discovery-manager.d.ts +2 -0
  30. package/dist/types/config/model-profile-activation.d.ts +79 -9
  31. package/dist/types/config/model-registry.d.ts +72 -8
  32. package/dist/types/config/model-resolver.d.ts +7 -4
  33. package/dist/types/config/models-config-schema.d.ts +10 -0
  34. package/dist/types/config/settings-schema.d.ts +57 -4
  35. package/dist/types/config/settings.d.ts +33 -1
  36. package/dist/types/coordinator-mcp/server.d.ts +4 -0
  37. package/dist/types/customization/import.d.ts +2 -0
  38. package/dist/types/customization/types.d.ts +1 -1
  39. package/dist/types/dap/config.d.ts +8 -4
  40. package/dist/types/dap/session.d.ts +11 -2
  41. package/dist/types/debug/system-info.d.ts +6 -1
  42. package/dist/types/discovery/helpers.d.ts +7 -2
  43. package/dist/types/discovery/mcp-compat.d.ts +3 -1
  44. package/dist/types/edit/index.d.ts +5 -0
  45. package/dist/types/edit/modes/patch.d.ts +14 -0
  46. package/dist/types/edit/modes/replace.d.ts +3 -0
  47. package/dist/types/eval/py/runtime.d.ts +9 -0
  48. package/dist/types/exec/bash-executor.d.ts +12 -0
  49. package/dist/types/export/ttsr.d.ts +19 -0
  50. package/dist/types/extensibility/extensions/types.d.ts +5 -1
  51. package/dist/types/extensibility/slash-commands.d.ts +1 -0
  52. package/dist/types/gjc-runtime/managed-owner-admission.d.ts +3 -0
  53. package/dist/types/gjc-runtime/ralplan-current-final.d.ts +54 -0
  54. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +5 -0
  55. package/dist/types/gjc-runtime/session-state-lock.d.ts +2 -0
  56. package/dist/types/gjc-runtime/state-runtime.d.ts +0 -2
  57. package/dist/types/gjc-runtime/tmux-sessions.d.ts +24 -0
  58. package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +6 -0
  59. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +1 -0
  60. package/dist/types/harness-control-plane/receipt-spool.d.ts +4 -0
  61. package/dist/types/hindsight/content.d.ts +5 -6
  62. package/dist/types/internal-urls/types.d.ts +2 -0
  63. package/dist/types/lsp/clients/swiftlint-client.d.ts +0 -4
  64. package/dist/types/main.d.ts +23 -18
  65. package/dist/types/migrate/action-planner.d.ts +10 -1
  66. package/dist/types/migrate/adapters/codex.d.ts +2 -1
  67. package/dist/types/migrate/adapters/index.d.ts +42 -1
  68. package/dist/types/migrate/executor.d.ts +3 -1
  69. package/dist/types/migrate/hook-module.d.ts +23 -0
  70. package/dist/types/migrate/report.d.ts +2 -1
  71. package/dist/types/migrate/types.d.ts +82 -3
  72. package/dist/types/modes/components/agent-dashboard.d.ts +1 -1
  73. package/dist/types/modes/components/model-selector.d.ts +43 -0
  74. package/dist/types/modes/components/settings-selector.d.ts +6 -0
  75. package/dist/types/modes/controllers/selector-controller.d.ts +2 -0
  76. package/dist/types/modes/prompt-suggestion-controller.d.ts +1 -1
  77. package/dist/types/modes/theme/theme.d.ts +1 -0
  78. package/dist/types/modes/utils/host-status.d.ts +15 -11
  79. package/dist/types/runtime-mcp/content-limits.d.ts +4 -0
  80. package/dist/types/runtime-mcp/manager.d.ts +7 -0
  81. package/dist/types/runtime-mcp/plugin-mcp-name-filter.d.ts +22 -0
  82. package/dist/types/runtime-mcp/project-host-values.d.ts +11 -5
  83. package/dist/types/runtime-mcp/tool-bridge.d.ts +4 -0
  84. package/dist/types/sdk/broker/broker-exit.d.ts +3 -3
  85. package/dist/types/sdk/broker/broker.d.ts +29 -2
  86. package/dist/types/sdk/broker/daemon-entry.d.ts +11 -1
  87. package/dist/types/sdk/broker/doctor-restart.d.ts +1 -1
  88. package/dist/types/sdk/broker/ensure.d.ts +141 -9
  89. package/dist/types/sdk/broker/hop.d.ts +45 -0
  90. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +18 -10
  91. package/dist/types/sdk/broker/lifecycle.d.ts +22 -0
  92. package/dist/types/sdk/broker/lock-host-identity.d.ts +5 -0
  93. package/dist/types/sdk/broker/session-index.d.ts +6 -1
  94. package/dist/types/sdk/broker/transport.d.ts +2 -0
  95. package/dist/types/sdk/bus/chat-command-policy.d.ts +2 -1
  96. package/dist/types/sdk/bus/index.d.ts +44 -0
  97. package/dist/types/sdk/bus/reconciliation-store.d.ts +10 -0
  98. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +9 -1
  99. package/dist/types/sdk/bus/telegram-daemon.d.ts +2 -0
  100. package/dist/types/sdk/bus/threaded-render.d.ts +2 -1
  101. package/dist/types/sdk/cli/session-cli.d.ts +0 -1
  102. package/dist/types/sdk/host/control/operations.d.ts +5 -1
  103. package/dist/types/sdk/host/events.d.ts +4 -0
  104. package/dist/types/sdk/host/host.d.ts +2 -1
  105. package/dist/types/sdk/host/prompt-image-upload.d.ts +43 -0
  106. package/dist/types/sdk/host/session-runtime.d.ts +40 -8
  107. package/dist/types/sdk/host/surface-policy.d.ts +2 -1
  108. package/dist/types/sdk/lifecycle/service.d.ts +12 -0
  109. package/dist/types/sdk/prompt-deadline-flush.d.ts +42 -4
  110. package/dist/types/sdk/prompt-deadline-manager.d.ts +2 -0
  111. package/dist/types/sdk/prompt-status.d.ts +11 -4
  112. package/dist/types/sdk/protocol/adapter-validation.d.ts +10 -4
  113. package/dist/types/sdk/reconciliation-extensions.d.ts +1 -1
  114. package/dist/types/sdk/session-list.d.ts +1 -1
  115. package/dist/types/session/agent-session.d.ts +107 -9
  116. package/dist/types/session/internal/managed-session-scope.d.ts +2 -2
  117. package/dist/types/session/internal/managed-session-storage.d.ts +1 -1
  118. package/dist/types/session/internal/native-publish-outcome.d.ts +1 -1
  119. package/dist/types/session/messages.d.ts +5 -1
  120. package/dist/types/session/session-manager.d.ts +29 -3
  121. package/dist/types/session/startup-auth-config.d.ts +24 -2
  122. package/dist/types/session/streaming-output.d.ts +11 -0
  123. package/dist/types/session/terminal-abort.d.ts +3 -1
  124. package/dist/types/session-import/service.d.ts +1 -1
  125. package/dist/types/skill-state/workflow-mutation-guard.d.ts +20 -4
  126. package/dist/types/slash-commands/helpers/mcp.d.ts +5 -0
  127. package/dist/types/slash-commands/helpers/provider-auth-status.d.ts +8 -0
  128. package/dist/types/ssh/ssh-executor.d.ts +8 -1
  129. package/dist/types/system-prompt.d.ts +3 -2
  130. package/dist/types/task/executor.d.ts +19 -2
  131. package/dist/types/task/role-body.d.ts +11 -0
  132. package/dist/types/tools/ast-edit-write-path.d.ts +10 -0
  133. package/dist/types/tools/atomic-file-write.d.ts +9 -7
  134. package/dist/types/tools/awaiting-input.d.ts +28 -0
  135. package/dist/types/tools/bash-allowed-prefixes.d.ts +17 -0
  136. package/dist/types/tools/bash-skill-urls.d.ts +4 -0
  137. package/dist/types/tools/bash.d.ts +7 -0
  138. package/dist/types/tools/descriptors.d.ts +15 -2
  139. package/dist/types/tools/fetch.d.ts +7 -1
  140. package/dist/types/tools/index.d.ts +2 -0
  141. package/dist/types/tools/irc.d.ts +5 -3
  142. package/dist/types/tools/python.d.ts +1 -1
  143. package/dist/types/tools/report-issue.d.ts +41 -0
  144. package/dist/types/tools/subagent.d.ts +23 -0
  145. package/dist/types/tools/tmux-self-injection-guard.d.ts +2 -0
  146. package/dist/types/tools/ultragoal-ask-guard.d.ts +1 -1
  147. package/dist/types/tools/write.d.ts +2 -0
  148. package/dist/types/utils/file-mentions.d.ts +5 -0
  149. package/dist/types/utils/image-limits.d.ts +7 -0
  150. package/dist/types/utils/image-loading.d.ts +0 -1
  151. package/dist/types/utils/open.d.ts +0 -1
  152. package/dist/types/utils/pasted-image-loading.d.ts +1 -5
  153. package/dist/types/utils/prompt-suggestion.d.ts +2 -2
  154. package/dist/types/utils/shell-snapshot.d.ts +2 -0
  155. package/dist/types/utils/title-generator.d.ts +13 -3
  156. package/dist/types/web/scrapers/types.d.ts +8 -1
  157. package/dist/types/web/search/providers/codex.d.ts +6 -7
  158. package/dist/types/web/search/providers/insane.d.ts +7 -1
  159. package/dist/types/web/search/providers/searxng.d.ts +5 -0
  160. package/package.json +10 -9
  161. package/scripts/acp-conformance-agent.ts +12 -2
  162. package/scripts/build-binary.ts +2 -1
  163. package/scripts/capture-subagent-compact-panel-showcase.ts +105 -0
  164. package/scripts/generate-docs-index.ts +8 -1
  165. package/scripts/generate-sdk-operation-inventory.ts +38 -4
  166. package/src/async/job-manager.ts +356 -56
  167. package/src/async/result-formatting.ts +40 -0
  168. package/src/autoresearch/git.ts +9 -2
  169. package/src/autoresearch/prompts/prompt-setup.md +1 -1
  170. package/src/capability/fs.ts +58 -22
  171. package/src/capability/index.ts +2 -2
  172. package/src/capability/rule.ts +11 -2
  173. package/src/cli/accounts-cli.ts +176 -8
  174. package/src/cli/append-system-prompt.ts +12 -0
  175. package/src/cli/args.ts +11 -7
  176. package/src/cli/auth-broker-cli.ts +21 -11
  177. package/src/cli/customize-doctor-precedence.ts +31 -0
  178. package/src/cli/customize-doctor.ts +49 -39
  179. package/src/cli/doctor/install-repairs.ts +35 -5
  180. package/src/cli/fast-help.ts +1 -3
  181. package/src/cli/install-activation.ts +47 -4
  182. package/src/cli/migrate-cli.ts +19 -6
  183. package/src/cli/public-command-errors.ts +6 -0
  184. package/src/cli/root-flags.ts +2 -4
  185. package/src/cli/stats-cli.ts +9 -5
  186. package/src/cli/update-cli.ts +471 -13
  187. package/src/cli-ordinary.ts +7 -2
  188. package/src/commands/accounts.ts +10 -5
  189. package/src/commands/harness.ts +25 -15
  190. package/src/commands/migrate.ts +3 -3
  191. package/src/commands/sdk.ts +277 -50
  192. package/src/commands/ultragoal.ts +16 -8
  193. package/src/commit/agentic/agent.ts +6 -6
  194. package/src/commit/analysis/conventional.ts +14 -5
  195. package/src/commit/changelog/index.ts +39 -1
  196. package/src/commit/map-reduce/index.ts +7 -7
  197. package/src/commit/message.ts +10 -3
  198. package/src/commit/pipeline.ts +12 -12
  199. package/src/config/autorouting-tier-map.ts +50 -1
  200. package/src/config/config-file.ts +106 -3
  201. package/src/config/config-hot-reload-trust.ts +148 -0
  202. package/src/config/config-hot-reload.ts +742 -0
  203. package/src/config/file-lock.ts +1615 -328
  204. package/src/config/keybindings.ts +154 -7
  205. package/src/config/model-bindings-applier.ts +85 -47
  206. package/src/config/model-discovery-manager.ts +10 -0
  207. package/src/config/model-profile-activation.ts +889 -148
  208. package/src/config/model-registry.ts +1706 -205
  209. package/src/config/model-resolver.ts +52 -22
  210. package/src/config/models-config-schema.ts +6 -0
  211. package/src/config/settings-schema.ts +73 -4
  212. package/src/config/settings.ts +460 -24
  213. package/src/coordinator-mcp/policy.ts +26 -1
  214. package/src/coordinator-mcp/server.ts +37 -2
  215. package/src/crash/sanitize.ts +7 -1
  216. package/src/customization/import.ts +279 -8
  217. package/src/customization/inventory.ts +24 -8
  218. package/src/customization/types.ts +1 -0
  219. package/src/dap/client.ts +3 -2
  220. package/src/dap/config.ts +31 -11
  221. package/src/dap/defaults.json +1 -1
  222. package/src/dap/session.ts +161 -36
  223. package/src/debug/crash-diagnostics.ts +89 -6
  224. package/src/debug/report-bundle.ts +90 -2
  225. package/src/debug/system-info.ts +8 -3
  226. package/src/defaults/gjc/extensions/grok-cli-vendor/src/shared/base-url.ts +21 -7
  227. package/src/discovery/agents-md.ts +1 -1
  228. package/src/discovery/builtin.ts +4 -3
  229. package/src/discovery/helpers.ts +25 -16
  230. package/src/discovery/mcp-compat.ts +120 -17
  231. package/src/edit/apply-patch/index.ts +12 -1
  232. package/src/edit/index.ts +204 -22
  233. package/src/edit/modes/patch.ts +214 -8
  234. package/src/edit/modes/replace.ts +136 -0
  235. package/src/eval/js/context-manager.ts +29 -3
  236. package/src/eval/js/worker-entry.ts +51 -18
  237. package/src/eval/py/executor.ts +14 -4
  238. package/src/eval/py/runner.py +144 -5
  239. package/src/eval/py/runtime.ts +44 -4
  240. package/src/exec/bash-executor.ts +81 -4
  241. package/src/exec/bash-shell-guardian.ts +4 -1
  242. package/src/exec/bash-shell-supervisor.ts +8 -4
  243. package/src/exec/isolated-shell.ts +5 -1
  244. package/src/exec/non-interactive-env.ts +0 -1
  245. package/src/export/html/index.ts +86 -6
  246. package/src/export/ttsr.ts +176 -1
  247. package/src/extensibility/custom-tools/wrapper.ts +3 -1
  248. package/src/extensibility/extensions/loader.ts +10 -1
  249. package/src/extensibility/extensions/ouroboros-ooo-bridge.ts +63 -1
  250. package/src/extensibility/extensions/types.ts +5 -1
  251. package/src/extensibility/hooks/loader.ts +10 -2
  252. package/src/extensibility/plugins/marketplace/fetcher.ts +2 -2
  253. package/src/extensibility/plugins/marketplace/manager.ts +36 -10
  254. package/src/extensibility/slash-commands.ts +5 -0
  255. package/src/gjc-runtime/gc-runtime.ts +37 -7
  256. package/src/gjc-runtime/launch-tmux.ts +30 -15
  257. package/src/gjc-runtime/launch-worktree.test.ts +148 -0
  258. package/src/gjc-runtime/launch-worktree.ts +101 -14
  259. package/src/gjc-runtime/managed-owner-admission.ts +46 -2
  260. package/src/gjc-runtime/ralplan-current-final.ts +294 -0
  261. package/src/gjc-runtime/ralplan-runtime.ts +95 -1
  262. package/src/gjc-runtime/session-resolution.ts +38 -6
  263. package/src/gjc-runtime/session-state-lock.ts +59 -14
  264. package/src/gjc-runtime/state-runtime.ts +23 -19
  265. package/src/gjc-runtime/state-writer.ts +109 -5
  266. package/src/gjc-runtime/tmux-sessions.ts +93 -13
  267. package/src/gjc-runtime/ultragoal-change-set.ts +90 -11
  268. package/src/gjc-runtime/ultragoal-guard.ts +9 -1
  269. package/src/gjc-runtime/ultragoal-runtime.ts +15 -5
  270. package/src/harness-control-plane/finalize.ts +11 -0
  271. package/src/harness-control-plane/receipt-spool.ts +105 -28
  272. package/src/harness-control-plane/storage.ts +124 -6
  273. package/src/hindsight/content.ts +116 -16
  274. package/src/hindsight/mental-models.ts +22 -10
  275. package/src/hindsight/state.ts +7 -4
  276. package/src/hooks/native-skill-hook.ts +17 -12
  277. package/src/internal-urls/artifact-protocol.ts +3 -1
  278. package/src/internal-urls/docs-index.generated.ts +23 -22
  279. package/src/internal-urls/local-protocol.ts +167 -8
  280. package/src/internal-urls/types.ts +2 -0
  281. package/src/lsp/client.ts +2 -1
  282. package/src/lsp/clients/biome-client.ts +2 -0
  283. package/src/lsp/clients/swiftlint-client.ts +2 -0
  284. package/src/lsp/index.ts +29 -0
  285. package/src/lsp/workspace-path.ts +23 -0
  286. package/src/main.ts +330 -92
  287. package/src/memories/index.ts +25 -2
  288. package/src/migrate/action-planner.ts +574 -2
  289. package/src/migrate/adapters/claude-code.ts +162 -5
  290. package/src/migrate/adapters/codex.ts +77 -7
  291. package/src/migrate/adapters/index.ts +258 -5
  292. package/src/migrate/adapters/opencode.ts +10 -1
  293. package/src/migrate/executor.ts +222 -7
  294. package/src/migrate/hook-module.ts +193 -0
  295. package/src/migrate/report.ts +63 -2
  296. package/src/migrate/types.ts +84 -4
  297. package/src/modes/DESIGN.md +51 -0
  298. package/src/modes/acp/acp-agent.ts +10 -1
  299. package/src/modes/acp/permission-mode.ts +18 -1
  300. package/src/modes/components/agent-dashboard.ts +20 -4
  301. package/src/modes/components/assistant-message.ts +55 -1
  302. package/src/modes/components/customization/customization-dashboard.ts +7 -0
  303. package/src/modes/components/customization/import-wizard.ts +29 -3
  304. package/src/modes/components/model-selector.ts +157 -103
  305. package/src/modes/components/settings-selector.ts +103 -37
  306. package/src/modes/components/welcome.ts +3 -1
  307. package/src/modes/controllers/command-controller.ts +300 -9
  308. package/src/modes/controllers/event-controller.ts +1 -1
  309. package/src/modes/controllers/extension-ui-controller.ts +31 -1
  310. package/src/modes/controllers/goal-mode-controller.ts +79 -10
  311. package/src/modes/controllers/input-controller.ts +2 -0
  312. package/src/modes/controllers/plan-mode-controller.ts +66 -11
  313. package/src/modes/controllers/runtime-mcp-command-controller.ts +7 -6
  314. package/src/modes/controllers/selector-controller.ts +76 -7
  315. package/src/modes/interactive-mode.ts +123 -4
  316. package/src/modes/prompt-suggestion-controller.ts +1 -1
  317. package/src/modes/runtime-init.ts +5 -1
  318. package/src/modes/theme/theme.ts +15 -0
  319. package/src/modes/utils/host-status.ts +21 -10
  320. package/src/prompts/agent-fragments/browser-aside.md +2 -2
  321. package/src/prompts/system/irc-incoming.md +3 -1
  322. package/src/prompts/tools/debug.md +1 -1
  323. package/src/prompts/tools/irc.md +1 -1
  324. package/src/prompts/tools/read.md +1 -1
  325. package/src/prompts/tools/report-issue.md +13 -0
  326. package/src/prompts/tools/task.md +1 -1
  327. package/src/runtime-credential-selector.ts +2 -2
  328. package/src/runtime-mcp/content-limits.ts +7 -0
  329. package/src/runtime-mcp/manager-pool.test.ts +5 -4
  330. package/src/runtime-mcp/manager.ts +40 -3
  331. package/src/runtime-mcp/oauth-flow.ts +32 -6
  332. package/src/runtime-mcp/plugin-mcp-name-filter.ts +62 -0
  333. package/src/runtime-mcp/project-host-values.ts +12 -8
  334. package/src/runtime-mcp/render.ts +16 -1
  335. package/src/runtime-mcp/tool-bridge.ts +194 -21
  336. package/src/runtime-mcp/transports/stdio.ts +2 -1
  337. package/src/sdk/broker/broker-exit.ts +10 -5
  338. package/src/sdk/broker/broker.ts +484 -85
  339. package/src/sdk/broker/daemon-entry.ts +338 -116
  340. package/src/sdk/broker/doctor-restart.ts +2 -1
  341. package/src/sdk/broker/ensure.ts +1214 -268
  342. package/src/sdk/broker/hop.ts +286 -0
  343. package/src/sdk/broker/lifecycle-ledger.ts +38 -11
  344. package/src/sdk/broker/lifecycle.ts +102 -6
  345. package/src/sdk/broker/lock-host-identity.ts +29 -0
  346. package/src/sdk/broker/session-index.ts +91 -16
  347. package/src/sdk/broker/transport.ts +17 -1
  348. package/src/sdk/bus/chat-command-policy.ts +16 -3
  349. package/src/sdk/bus/chat-daemon-control.ts +2 -2
  350. package/src/sdk/bus/index.ts +460 -35
  351. package/src/sdk/bus/reconciliation-store.ts +201 -8
  352. package/src/sdk/bus/session-control.ts +2 -0
  353. package/src/sdk/bus/slack-daemon.ts +105 -9
  354. package/src/sdk/bus/telegram-daemon-contract.ts +9 -1
  355. package/src/sdk/bus/telegram-daemon.ts +10 -2
  356. package/src/sdk/bus/threaded-render.ts +20 -1
  357. package/src/sdk/cli/session-cli.ts +23 -6
  358. package/src/sdk/host/control/dispatch.ts +13 -1
  359. package/src/sdk/host/control/operations.ts +10 -1
  360. package/src/sdk/host/events.ts +67 -4
  361. package/src/sdk/host/host.ts +8 -2
  362. package/src/sdk/host/prompt-image-upload.ts +438 -0
  363. package/src/sdk/host/sdk-surface-parity.test.ts +15 -0
  364. package/src/sdk/host/session-runtime.test.ts +1737 -129
  365. package/src/sdk/host/session-runtime.ts +684 -83
  366. package/src/sdk/host/surface-policy.ts +4 -0
  367. package/src/sdk/host/websocket-transport.lifecycle.test.ts +90 -2
  368. package/src/sdk/host/websocket-transport.ts +19 -7
  369. package/src/sdk/lifecycle/service.ts +15 -1
  370. package/src/sdk/prompt-deadline-flush.ts +78 -5
  371. package/src/sdk/prompt-deadline-manager.ts +68 -14
  372. package/src/sdk/prompt-status.ts +6 -4
  373. package/src/sdk/protocol/adapter-validation.ts +81 -12
  374. package/src/sdk/protocol/operation-inventory.generated.json +261 -2
  375. package/src/sdk/protocol/operation-registry.ts +33 -3
  376. package/src/sdk/reconciliation-extensions.ts +1 -0
  377. package/src/sdk/session-list.ts +1 -0
  378. package/src/sdk/session.ts +237 -97
  379. package/src/session/agent-session.ts +2726 -479
  380. package/src/session/history-storage.ts +182 -12
  381. package/src/session/internal/managed-session-scope.ts +173 -57
  382. package/src/session/internal/managed-session-storage.ts +85 -12
  383. package/src/session/internal/native-publish-outcome.ts +5 -1
  384. package/src/session/messages.ts +47 -6
  385. package/src/session/session-manager.ts +326 -19
  386. package/src/session/startup-auth-config.ts +116 -8
  387. package/src/session/streaming-output.ts +262 -4
  388. package/src/session/terminal-abort.ts +14 -2
  389. package/src/session-import/redact.ts +53 -2
  390. package/src/session-import/service.ts +1 -0
  391. package/src/skill-state/workflow-mutation-guard.ts +2145 -206
  392. package/src/slash-commands/builtin-registry.ts +21 -6
  393. package/src/slash-commands/helpers/credential-switch.ts +10 -5
  394. package/src/slash-commands/helpers/mcp.ts +11 -3
  395. package/src/slash-commands/helpers/provider-auth-status.ts +456 -0
  396. package/src/ssh/ssh-executor.ts +9 -2
  397. package/src/stt/recorder.ts +26 -9
  398. package/src/system-prompt.ts +61 -16
  399. package/src/task/executor.ts +115 -20
  400. package/src/task/index.ts +389 -175
  401. package/src/task/role-body.ts +84 -0
  402. package/src/task/token-log-mode.test.ts +162 -0
  403. package/src/task/token-log.ts +91 -2
  404. package/src/tools/archive-reader.ts +650 -37
  405. package/src/tools/ask.ts +1 -0
  406. package/src/tools/ast-edit-write-path.ts +64 -0
  407. package/src/tools/ast-edit.ts +18 -1
  408. package/src/tools/ast-grep.ts +7 -2
  409. package/src/tools/atomic-file-write.ts +46 -15
  410. package/src/tools/auto-generated-guard.ts +15 -3
  411. package/src/tools/awaiting-input.ts +114 -0
  412. package/src/tools/bash-allowed-prefixes.ts +214 -6
  413. package/src/tools/bash-interactive.ts +14 -2
  414. package/src/tools/bash-skill-urls.ts +58 -5
  415. package/src/tools/bash.ts +113 -26
  416. package/src/tools/browser/tab-worker.ts +39 -13
  417. package/src/tools/browser.ts +17 -7
  418. package/src/tools/cron.ts +10 -1
  419. package/src/tools/debug.ts +16 -8
  420. package/src/tools/descriptors.test.ts +44 -0
  421. package/src/tools/descriptors.ts +50 -6
  422. package/src/tools/eval.ts +15 -0
  423. package/src/tools/fetch.ts +93 -4
  424. package/src/tools/gh.ts +27 -12
  425. package/src/tools/image-gen.ts +20 -6
  426. package/src/tools/index.ts +2 -0
  427. package/src/tools/irc.ts +5 -3
  428. package/src/tools/monitor.ts +9 -1
  429. package/src/tools/output-meta.ts +74 -5
  430. package/src/tools/python.ts +1 -1
  431. package/src/tools/read.ts +26 -37
  432. package/src/tools/recipe/runner.ts +23 -12
  433. package/src/tools/render-utils.ts +2 -2
  434. package/src/tools/report-issue.ts +184 -0
  435. package/src/tools/search.ts +3 -2
  436. package/src/tools/ssh.ts +274 -8
  437. package/src/tools/subagent-render.ts +114 -12
  438. package/src/tools/subagent.ts +118 -5
  439. package/src/tools/tmux-self-injection-guard.ts +3301 -122
  440. package/src/tools/todo-write.ts +41 -3
  441. package/src/tools/tool-catalog.generated.ts +32 -4
  442. package/src/tools/ultragoal-ask-guard.ts +13 -1
  443. package/src/tools/write.ts +42 -3
  444. package/src/tui/tree-list.ts +15 -1
  445. package/src/utils/commit-message-generator.ts +12 -4
  446. package/src/utils/external-editor.ts +21 -7
  447. package/src/utils/file-mentions.ts +75 -7
  448. package/src/utils/git.ts +38 -7
  449. package/src/utils/image-limits.ts +8 -0
  450. package/src/utils/image-loading.ts +1 -1
  451. package/src/utils/open.ts +73 -4
  452. package/src/utils/pasted-image-loading.ts +9 -8
  453. package/src/utils/prompt-suggestion.ts +5 -4
  454. package/src/utils/shell-snapshot.ts +9 -1
  455. package/src/utils/title-generator.ts +72 -22
  456. package/src/web/scrapers/index.ts +3 -1
  457. package/src/web/scrapers/types.ts +8 -0
  458. package/src/web/scrapers/youtube.ts +3 -2
  459. package/src/web/search/index.ts +2 -1
  460. package/src/web/search/providers/codex.ts +29 -32
  461. package/src/web/search/providers/insane.ts +91 -28
  462. 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 {
@@ -48,38 +53,58 @@ type ModelProfileActivationSession = Pick<
48
53
  > & {
49
54
  credentialSessionId?: string;
50
55
  setModelTemporary?: AgentSession["setModelTemporary"];
51
- setActiveModelProfile?: (name: string | undefined) => void;
56
+ setActiveModelProfile?: (name: string | undefined, activatedByThisProcess?: boolean) => void;
52
57
  getActiveModelProfile?: () => string | undefined;
58
+ getActivatedModelProfile?: () => string | undefined;
53
59
  /** Record which runtime override keys this activation installed (session-scoped). */
54
60
  noteProfileInstalledOverrides?: (
55
61
  modelRoles: readonly string[],
56
62
  agentModelOverrides: readonly string[],
57
63
  preProfileModel: Model<Api> | undefined,
64
+ baseline?: {
65
+ modelRoles: Readonly<Record<string, ModelSelectorValue>>;
66
+ agentModelOverrides: Readonly<Record<string, ModelSelectorValue>>;
67
+ },
68
+ installed?: {
69
+ modelRoles: Readonly<Record<string, ModelSelectorValue>>;
70
+ agentModelOverrides: Readonly<Record<string, ModelSelectorValue>>;
71
+ },
72
+ manual?: { modelRoles: readonly string[]; agentModelOverrides: readonly string[] },
73
+ configuredDefaultChain?: readonly string[],
58
74
  ) => void;
59
75
  /** Drop the recorded profile-installed override keys (e.g. after materialization). */
60
76
  clearProfileInstalledOverrides?: () => void;
61
77
  /** Current profile-installed override keys, for deriving the activation base. */
62
78
  getProfileInstalledOverrideKeys?: () => { modelRoles: readonly string[]; agentModelOverrides: readonly string[] };
79
+ /** Profile-installed baselines, preserved when a refreshed profile drops an assignment. */
80
+ getProfileInstalledOverrideState?: () => AgentSessionProfileInstalledOverrideState;
81
+ restoreProfileInstalledOverrideState?: (state: AgentSessionProfileInstalledOverrideState) => void;
63
82
  /** Re-apply vendor-separated delegation (task tool + prompt) after the role layer changed. */
64
83
  syncEagerDelegation?: () => Promise<void>;
65
84
  getSessionDefaultModelSelector?: () => string | undefined;
66
85
  recordResumeDefaultModel?: (selector: string | undefined) => void;
67
86
  getUserCanonicalVariantSelection?: () => UserCanonicalVariantSelection;
87
+ getUserModelSelectionRevision?: () => number;
68
88
  seedDefaultFallbackResolution?: (activeIndex: number, skips: Array<{ selector: string; reason: string }>) => void;
69
89
  getDefaultFallbackRuntimeState?: () => DefaultFallbackRuntimeState;
70
90
  restoreDefaultFallbackRuntimeState?: (state: DefaultFallbackRuntimeState) => void;
71
91
  restoreModelSelectionForRollback?: AgentSession["restoreModelSelectionForRollback"];
92
+ prepareModelSelectionForProfileActivation?: AgentSession["prepareModelSelectionForProfileActivation"];
93
+ commitPreparedProfileModelSelection?: AgentSession["commitPreparedProfileModelSelection"];
94
+ finishPreparedProfileModelSelection?: AgentSession["finishPreparedProfileModelSelection"];
72
95
  modelRegistry?: Pick<
73
96
  ModelRegistry,
74
97
  | "getSessionCanonicalVariant"
75
98
  | "restoreSessionCanonicalVariant"
76
99
  | "clearCanonicalVariant"
100
+ | "getModelProfile"
77
101
  | "getAvailable"
78
102
  | "lookupAliasExists"
79
103
  | "resolveModelByLookupAlias"
80
104
  | "authStorage"
81
105
  | "isCredentiallessProvider"
82
- >;
106
+ > &
107
+ Partial<Pick<ModelRegistry, "seedCanonicalVariant">>;
83
108
  getConfiguredModelChainState?: (role: string) => ConfiguredModelChainState | undefined;
84
109
  };
85
110
 
@@ -90,6 +115,25 @@ type ConfiguredModelChainState = {
90
115
  explicitHead: boolean;
91
116
  };
92
117
 
118
+ type PublishedModelProfileActivationState = {
119
+ model: Model<Api> | undefined;
120
+ thinkingLevel: ThinkingLevel | undefined;
121
+ modelRolesOverride: Readonly<Record<string, ModelSelectorValue>> | undefined;
122
+ agentModelOverridesOverride: Readonly<Record<string, ModelSelectorValue>> | undefined;
123
+ defaultChainState: ConfiguredModelChainState | undefined;
124
+ defaultFallbackRuntimeState: DefaultFallbackRuntimeState | undefined;
125
+ activeModelProfile: string | undefined;
126
+ activatedModelProfile: string | undefined;
127
+ profileInstalledOverrideState: AgentSessionProfileInstalledOverrideState | undefined;
128
+ sessionDefaultModel: string | undefined;
129
+ persistedModelRoles: Readonly<Record<string, ModelSelectorValue>> | undefined;
130
+ persistedAgentModelOverrides: Readonly<Record<string, ModelSelectorValue>> | undefined;
131
+ persistedDefaultProfile: string | undefined;
132
+ persistedDefaultThinkingLevel: Exclude<ThinkingLevel, "inherit"> | undefined;
133
+ defaultThinkingLevelOverride: Exclude<ThinkingLevel, "inherit"> | undefined;
134
+ defaultProfileOverride: string | undefined;
135
+ };
136
+
93
137
  export interface PrepareModelProfileActivationOptions {
94
138
  session: ModelProfileActivationSession;
95
139
  modelRegistry: Pick<
@@ -108,6 +152,8 @@ export interface PrepareModelProfileActivationOptions {
108
152
  Pick<
109
153
  ModelRegistry,
110
154
  | "getAvailable"
155
+ | "hasUsableAuthForProvider"
156
+ | "isStagedReloadCandidate"
111
157
  | "resolveModelByLookupAlias"
112
158
  | "lookupAliasExists"
113
159
  | "clearCanonicalVariant"
@@ -115,6 +161,7 @@ export interface PrepareModelProfileActivationOptions {
115
161
  | "getSessionCanonicalVariant"
116
162
  | "restoreSessionCanonicalVariant"
117
163
  | "getConfiguredProviderIds"
164
+ | "getConfiguredModelBindings"
118
165
  | "isKnownProvider"
119
166
  >
120
167
  > & {
@@ -122,11 +169,21 @@ export interface PrepareModelProfileActivationOptions {
122
169
  authStorage?: ModelRegistry["authStorage"];
123
170
  };
124
171
  settings: Pick<Settings, "get" | "getGlobal" | "getOverride">;
172
+ /** Live mutation target when `settings` is a validated staged view. */
173
+ settingsForApply?: Settings;
174
+ prepareSessionModelSelection?: boolean;
175
+ preserveManualOverrides?: boolean;
176
+ preserveCanonicalVariant?: boolean;
177
+ signal?: AbortSignal;
125
178
  profileName: string;
126
179
  }
127
180
  export interface ApplyModelProfileActivationOptions {
128
181
  persistDefault?: boolean;
129
182
  thinkingLevelOverride?: ThinkingLevel;
183
+ preserveDefaultModelSelection?: boolean;
184
+ preserveLiveModelSelection?: boolean;
185
+ deferRuntimePublication?: boolean;
186
+ signal?: AbortSignal;
130
187
  /** Cancels deferred activation if a newer user model selection exists. */
131
188
  isCurrent?: () => boolean;
132
189
  }
@@ -148,11 +205,18 @@ export interface PreparedModelProfileActivation {
148
205
  previousDefaultProfileOverride: string | undefined;
149
206
  previousPersistedDefaultProfile: string | undefined;
150
207
  previousPersistedDefaultThinkingLevel: Exclude<ThinkingLevel, "inherit"> | undefined;
151
- previousDefaultThinkingLevelOverride: ThinkingLevel | undefined;
208
+ previousDefaultThinkingLevelOverride: Exclude<ThinkingLevel, "inherit"> | undefined;
152
209
  baseAgentModelOverrides: Record<string, ModelSelectorValue>;
153
210
  baseModelRoles: Record<string, ModelSelectorValue>;
154
211
  previousDefaultChain: readonly string[] | undefined;
155
212
  previousDefaultChainState: ConfiguredModelChainState | undefined;
213
+ previousProfileInstalledOverrideState: AgentSessionProfileInstalledOverrideState | undefined;
214
+ preparedDefaultModelSelection:
215
+ | Awaited<ReturnType<NonNullable<ModelProfileActivationSession["prepareModelSelectionForProfileActivation"]>>>
216
+ | undefined;
217
+ defaultChainPublished: boolean;
218
+ defaultModelPublished: boolean;
219
+ publishedState: PublishedModelProfileActivationState | undefined;
156
220
  previousDefaultFallbackRuntimeState: DefaultFallbackRuntimeState | undefined;
157
221
  defaultModel: Model<Api> | undefined;
158
222
  defaultThinkingLevel: ThinkingLevel | undefined;
@@ -164,7 +228,10 @@ export interface PreparedModelProfileActivation {
164
228
  defaultResolutionSkips: Array<{ selector: string; reason: string }>;
165
229
  modelRoles: Record<string, ModelSelectorValue>;
166
230
  agentModelOverrides: Record<string, ModelSelectorValue>;
231
+ manualModelRoleOverrides: ReadonlySet<string>;
232
+ manualAgentModelOverrides: ReadonlySet<string>;
167
233
  previousActiveModelProfile: string | undefined;
234
+ previousActivatedModelProfile: string | undefined;
168
235
  /**
169
236
  * The session resume default ("provider/id") captured BEFORE activation —
170
237
  * the model resume would restore prior to this profile. Snapshotted
@@ -198,6 +265,8 @@ export interface MaterializeModelProfileAssignmentOptions {
198
265
  | "setConfiguredModelChain"
199
266
  | "setActiveModelProfile"
200
267
  | "getActiveModelProfile"
268
+ | "getActivatedModelProfile"
269
+ | "getProfileInstalledOverrideState"
201
270
  | "getDefaultFallbackRuntimeState"
202
271
  | "restoreDefaultFallbackRuntimeState"
203
272
  | "modelRegistry"
@@ -220,6 +289,8 @@ export interface MaterializeModelProfileAssignmentsOptions {
220
289
  | "setConfiguredModelChain"
221
290
  | "setActiveModelProfile"
222
291
  | "getActiveModelProfile"
292
+ | "getActivatedModelProfile"
293
+ | "getProfileInstalledOverrideState"
223
294
  | "getDefaultFallbackRuntimeState"
224
295
  | "restoreDefaultFallbackRuntimeState"
225
296
  | "modelRegistry"
@@ -248,9 +319,80 @@ function getMaterializedAssignments(
248
319
  return result;
249
320
  }
250
321
 
322
+ /**
323
+ * Resolve the profile a materialization may consume.
324
+ *
325
+ * Only a profile this process actually activated owns role-only materialization.
326
+ * A persisted profile whose activation failed or was only restored from history
327
+ * stays in place. An explicit default selection may supersede that durable profile.
328
+ */
329
+ export function resolveMaterializationProfileIdentity(
330
+ activatedProfile: string | undefined,
331
+ durableProfile: string | undefined,
332
+ includesDefault: boolean,
333
+ ): string | undefined {
334
+ if (activatedProfile) return activatedProfile;
335
+ return includesDefault ? durableProfile : undefined;
336
+ }
337
+
338
+ function resolveMaterializationProfile(
339
+ options: MaterializeModelProfileAssignmentOptions | MaterializeModelProfileAssignmentsOptions,
340
+ includesDefault: boolean,
341
+ ): string | undefined {
342
+ return resolveMaterializationProfileIdentity(
343
+ options.session.getActivatedModelProfile?.(),
344
+ options.settings.get("modelProfile.default"),
345
+ includesDefault,
346
+ );
347
+ }
348
+
349
+ /** Resolve startup-override materialization in activated-chain, profile, then global-default order. */
350
+ export function resolveMaterializedDefaultSelector(
351
+ activatedDefaultChain: readonly string[] | undefined,
352
+ profileDefault: ModelSelectorValue | undefined,
353
+ globalDefault: ModelSelectorValue | undefined,
354
+ ): ModelSelectorValue | undefined {
355
+ if (activatedDefaultChain?.length) {
356
+ return activatedDefaultChain.length === 1 ? activatedDefaultChain[0] : [...activatedDefaultChain];
357
+ }
358
+ return profileDefault ?? globalDefault;
359
+ }
360
+
361
+ function modelRolesForMaterialization(
362
+ options: MaterializeModelProfileAssignmentOptions | MaterializeModelProfileAssignmentsOptions,
363
+ ): Record<string, ModelSelectorValue> {
364
+ const modelRoles = { ...options.settings.get("modelRoles") };
365
+ const defaultChain = options.session.getConfiguredModelChainState?.("default");
366
+ if (defaultChain?.origin !== "startup-override" || !defaultChain.entries[0]) return modelRoles;
367
+
368
+ // The merged runtime roles can still contain the CLI model; materialize the
369
+ // profile's default instead. Profile-derived defaults require an activated
370
+ // profile: a persisted profile that failed activation contributes nothing.
371
+ const activeProfile = options.session.getActivatedModelProfile?.();
372
+ let configuredDefault: ModelSelectorValue | undefined;
373
+ if (activeProfile) {
374
+ const activatedDefaultChain = options.session.getProfileInstalledOverrideState?.().configuredDefaultChain;
375
+ const profile = options.session.modelRegistry?.getModelProfile?.(activeProfile);
376
+ configuredDefault = resolveMaterializedDefaultSelector(
377
+ activatedDefaultChain,
378
+ profile ? resolveProfileBindings(profile).defaultSelector : undefined,
379
+ options.settings.getGlobal("modelRoles")?.default,
380
+ );
381
+ } else {
382
+ configuredDefault = options.settings.getGlobal("modelRoles")?.default;
383
+ }
384
+ if (configuredDefault === undefined) delete modelRoles.default;
385
+ else modelRoles.default = configuredDefault;
386
+ return modelRoles;
387
+ }
388
+
251
389
  function materializeConfiguredDefaultChain(
252
- session: Pick<ModelProfileActivationSession, "model" | "thinkingLevel" | "getConfiguredModelChain">,
390
+ session: Pick<
391
+ ModelProfileActivationSession,
392
+ "model" | "thinkingLevel" | "getConfiguredModelChain" | "getConfiguredModelChainState"
393
+ >,
253
394
  ): ModelSelectorValue | undefined {
395
+ if (session.getConfiguredModelChainState?.("default")?.origin === "startup-override") return undefined;
254
396
  if (!session.model) return undefined;
255
397
  return formatModelSelectorValue(`${session.model.provider}/${session.model.id}`, session.thinkingLevel);
256
398
  }
@@ -266,7 +408,7 @@ function concretizeMaterializedAssignmentValues(
266
408
  // Materialized assignments are persisted into `modelRoles` and
267
409
  // `task.agentModelOverrides` and later consumed by profile execution, so they
268
410
  // resolve against the same catalog profile activation uses.
269
- const availableModels = modelRegistry.getAvailable();
411
+ const availableModels = modelRegistry.getAvailable(credentialSessionId);
270
412
  const authenticatedModels = availableModels.filter(model => {
271
413
  const isCredentiallessProvider = modelRegistry.isCredentiallessProvider?.bind(modelRegistry);
272
414
  const hasUsableAuth = modelRegistry.authStorage?.hasUsableAuth?.bind(modelRegistry.authStorage);
@@ -306,6 +448,7 @@ function commitMaterializedProfileAssignments(
306
448
  options: MaterializeModelProfileAssignmentOptions | MaterializeModelProfileAssignmentsOptions,
307
449
  modelRoles: Record<string, ModelSelectorValue>,
308
450
  agentModelOverrides: Record<string, ModelSelectorValue>,
451
+ preserveStartupOverrideChain = false,
309
452
  ): boolean {
310
453
  const previousPersistedModelRoles = options.settings.getGlobal("modelRoles");
311
454
  const previousPersistedAgentModelOverrides = options.settings.getGlobal("task.agentModelOverrides");
@@ -314,6 +457,7 @@ function commitMaterializedProfileAssignments(
314
457
  const previousAgentModelOverridesOverride = options.settings.getOverride("task.agentModelOverrides");
315
458
  const previousDefaultProfileOverride = options.settings.getOverride("modelProfile.default");
316
459
  const previousActiveProfile = options.session.getActiveModelProfile?.();
460
+ const previousActivatedProfile = options.session.getActivatedModelProfile?.();
317
461
  const previousChain = options.session.getConfiguredModelChainState?.("default");
318
462
  const previousFallbackRuntimeState = options.session.getDefaultFallbackRuntimeState?.();
319
463
  const previousCanonicalVariant = options.session.modelRegistry?.getSessionCanonicalVariant?.(
@@ -328,13 +472,17 @@ function commitMaterializedProfileAssignments(
328
472
  options.settings.clearOverride("modelProfile.default");
329
473
  options.settings.override("modelRoles", nextModelRoles);
330
474
  options.settings.override("task.agentModelOverrides", nextAgentModelOverrides);
331
- options.session.setConfiguredModelChain(
332
- "default",
333
- normalizeModelSelectorValue(nextModelRoles.default),
334
- "modelRoles",
335
- undefined,
336
- true,
337
- );
475
+ // Role-only materialization persists the profile default for future launches,
476
+ // but it must not replace the live run-scoped CLI model chain.
477
+ if (!preserveStartupOverrideChain || previousChain?.origin !== "startup-override") {
478
+ options.session.setConfiguredModelChain(
479
+ "default",
480
+ normalizeModelSelectorValue(nextModelRoles.default),
481
+ "modelRoles",
482
+ undefined,
483
+ true,
484
+ );
485
+ }
338
486
  options.session.setActiveModelProfile?.(undefined);
339
487
  return true;
340
488
  } catch (error) {
@@ -399,7 +547,12 @@ function commitMaterializedProfileAssignments(
399
547
  ? options.settings.clearOverride("modelProfile.default")
400
548
  : options.settings.override("modelProfile.default", previousDefaultProfileOverride),
401
549
  );
402
- restore(() => options.session.setActiveModelProfile?.(previousActiveProfile));
550
+ restore(() =>
551
+ options.session.setActiveModelProfile?.(
552
+ previousActiveProfile,
553
+ previousActivatedProfile === previousActiveProfile,
554
+ ),
555
+ );
403
556
  if (rollbackErrors.length > 0) {
404
557
  throw new AggregateError(
405
558
  [error, ...rollbackErrors],
@@ -411,10 +564,9 @@ function commitMaterializedProfileAssignments(
411
564
  }
412
565
 
413
566
  export function materializeActiveModelProfileAssignment(options: MaterializeModelProfileAssignmentOptions): boolean {
414
- const activeProfile = options.session.getActiveModelProfile?.() ?? options.settings.get("modelProfile.default");
415
- if (!activeProfile) return false;
567
+ if (!resolveMaterializationProfile(options, options.role === "default")) return false;
416
568
 
417
- const nextModelRoles = { ...options.settings.get("modelRoles") };
569
+ const nextModelRoles = modelRolesForMaterialization(options);
418
570
  const nextAgentModelOverrides = { ...options.settings.get("task.agentModelOverrides") };
419
571
  const target = GJC_MODEL_ASSIGNMENT_TARGETS[options.role];
420
572
 
@@ -431,21 +583,24 @@ export function materializeActiveModelProfileAssignment(options: MaterializeMode
431
583
  nextAgentModelOverrides[options.role] = options.selector;
432
584
  }
433
585
 
434
- const committed = commitMaterializedProfileAssignments(options, nextModelRoles, nextAgentModelOverrides);
586
+ const committed = commitMaterializedProfileAssignments(
587
+ options,
588
+ nextModelRoles,
589
+ nextAgentModelOverrides,
590
+ options.role !== "default",
591
+ );
435
592
  if (committed) options.session.clearProfileInstalledOverrides?.();
436
593
  return committed;
437
594
  }
438
595
 
439
596
  export function materializeActiveModelProfileAssignments(options: MaterializeModelProfileAssignmentsOptions): boolean {
440
- const activeProfile = options.session.getActiveModelProfile?.() ?? options.settings.get("modelProfile.default");
441
- if (!activeProfile) return false;
442
-
443
597
  const materializedAssignments = getMaterializedAssignments(options.assignments);
598
+ const includesDefault = materializedAssignments.some(([role]) => role === "default");
599
+ if (!resolveMaterializationProfile(options, includesDefault)) return false;
444
600
  if (materializedAssignments.length === 0) return true;
445
601
 
446
- const nextModelRoles = { ...options.settings.get("modelRoles") };
602
+ const nextModelRoles = modelRolesForMaterialization(options);
447
603
  const nextAgentModelOverrides = { ...options.settings.get("task.agentModelOverrides") };
448
- const includesDefault = materializedAssignments.some(([role]) => role === "default");
449
604
 
450
605
  if (!includesDefault && !nextModelRoles.default) {
451
606
  const defaultChain = materializeConfiguredDefaultChain(options.session);
@@ -461,7 +616,12 @@ export function materializeActiveModelProfileAssignments(options: MaterializeMod
461
616
  }
462
617
  }
463
618
 
464
- const committed = commitMaterializedProfileAssignments(options, nextModelRoles, nextAgentModelOverrides);
619
+ const committed = commitMaterializedProfileAssignments(
620
+ options,
621
+ nextModelRoles,
622
+ nextAgentModelOverrides,
623
+ !includesDefault,
624
+ );
465
625
  if (committed) options.session.clearProfileInstalledOverrides?.();
466
626
  return committed;
467
627
  }
@@ -528,6 +688,17 @@ async function getProfileProviderApiKey(
528
688
  }
529
689
  }
530
690
 
691
+ function isStagedProviderAuthAvailable(
692
+ registry: PrepareModelProfileActivationOptions["modelRegistry"],
693
+ provider: string,
694
+ sessionId: string,
695
+ ): boolean {
696
+ return (
697
+ registry.isStagedReloadCandidate?.() === true &&
698
+ (registry.hasUsableAuthForProvider?.(provider, sessionId) ?? false)
699
+ );
700
+ }
701
+
531
702
  export function formatModelProfileCredentialError(profileLabel: string, providers: readonly string[]): string {
532
703
  return `Model profile "${profileLabel}" requires credentials for: ${providers.join(", ")}. Run /login and configure the missing provider(s), then retry.`;
533
704
  }
@@ -728,7 +899,7 @@ export async function resolveMissingSessionModelRecovery(options: {
728
899
  options.skips.length === options.defaultEntries.length &&
729
900
  options.skips.every(skip => skip.reason === "unknown_model");
730
901
  if (!allSelectorsUnknown) return undefined;
731
- const fullCatalog = options.modelRegistry.getAll();
902
+ const fullCatalog = options.modelRegistry.getAll(options.credentialSessionId);
732
903
  const catalogIndex = createRegistrySelectorIndex(fullCatalog);
733
904
  const savedSelectorsMissingFromCatalog = resolveConfiguredModelPatterns(
734
905
  options.defaultEntries,
@@ -825,7 +996,8 @@ export async function resolveModelProfileDefaultChain(options: {
825
996
  if (requiredProviderSet.has(provider) && !alternativeSet.has(provider)) throw error;
826
997
  continue;
827
998
  }
828
- if (apiKey === kNoAuth || isAuthenticated(apiKey)) authenticatedProviders.add(provider);
999
+ const hasUsableAuth = isStagedProviderAuthAvailable(options.modelRegistry, provider, options.credentialSessionId);
1000
+ if (apiKey === kNoAuth || isAuthenticated(apiKey) || hasUsableAuth) authenticatedProviders.add(provider);
829
1001
  else if (requiredProviderSet.has(provider)) missingProviders.push(provider);
830
1002
  }
831
1003
  const proxyProvider = profile.source !== "user" ? resolveProxyProviderId(options.settings) : undefined;
@@ -851,6 +1023,9 @@ export async function resolveModelProfileDefaultChain(options: {
851
1023
  options.credentialSessionId,
852
1024
  profileLabel,
853
1025
  );
1026
+ const proxyHasUsableAuth =
1027
+ proxyProvider !== undefined &&
1028
+ isStagedProviderAuthAvailable(options.modelRegistry, proxyProvider, options.credentialSessionId);
854
1029
  if (
855
1030
  proxyProvider !== undefined &&
856
1031
  !isModelProfileProxyConfigured(proxyProvider, configuredProviderIds, proxyApiKey === kNoAuth)
@@ -859,7 +1034,8 @@ export async function resolveModelProfileDefaultChain(options: {
859
1034
  `modelProfile.proxyProvider "${proxyProvider}" is not configured. Configure it with \`gjc setup provider\` before activating a preset.`,
860
1035
  );
861
1036
  }
862
- const proxyAuthenticated = proxyApiKey !== undefined && (proxyApiKey === kNoAuth || isAuthenticated(proxyApiKey));
1037
+ const proxyAuthenticated =
1038
+ (proxyApiKey !== undefined && (proxyApiKey === kNoAuth || isAuthenticated(proxyApiKey))) || proxyHasUsableAuth;
863
1039
  if (proxyMode === "always" && !proxyAuthenticated)
864
1040
  throw new ModelProfileCredentialError(profileLabel, [proxyProvider!]);
865
1041
  const strictMissing = missingProviders.filter(
@@ -884,7 +1060,9 @@ export async function resolveModelProfileDefaultChain(options: {
884
1060
  allRoutable && proxyProvider !== undefined ? [proxyProvider] : [...group],
885
1061
  );
886
1062
  }
887
- const availableModels = options.modelRegistry.getAvailable?.() ?? options.modelRegistry.getAll();
1063
+ const availableModels =
1064
+ options.modelRegistry.getAvailable?.(options.credentialSessionId) ??
1065
+ options.modelRegistry.getAll(options.credentialSessionId);
888
1066
  let bindings = resolveProfileBindings(profile);
889
1067
  if (alternativeGroups.length > 0)
890
1068
  bindings = rewriteBindingsProviders(bindings, authenticatedProviders, alternativeGroups);
@@ -901,7 +1079,7 @@ export async function resolveModelProfileDefaultChain(options: {
901
1079
  await preflightModelProfileRoleBindings({
902
1080
  profile,
903
1081
  bindings,
904
- roleCatalogModels: options.modelRegistry.getAll(),
1082
+ roleCatalogModels: options.modelRegistry.getAll(options.credentialSessionId),
905
1083
  settings: options.settings as Settings,
906
1084
  modelRegistry: options.modelRegistry as ModelRegistry,
907
1085
  sessionId: "",
@@ -948,6 +1126,8 @@ export async function resolveModelProfileDefaultChain(options: {
948
1126
  credentialSessionId: options.credentialSessionId,
949
1127
  isCredentialUnavailable: provider =>
950
1128
  isSessionCredentialPinBlocking(options.modelRegistry, provider, options.credentialSessionId),
1129
+ isProviderAuthAvailable: provider =>
1130
+ isStagedProviderAuthAvailable(options.modelRegistry, provider, options.credentialSessionId),
951
1131
  },
952
1132
  );
953
1133
  return { profileName, entries: defaultChain, ...resolution };
@@ -1085,11 +1265,15 @@ function formatMaterializedSelector(selector: string, model: Model<Api>): string
1085
1265
  if (!explicitThinkingLevel || parseModelString(clampedSelector)?.thinkingLevel) return clampedSelector;
1086
1266
  return formatModelSelectorValue(clampedSelector, explicitThinkingLevel);
1087
1267
  }
1088
- function getBareSelectorCredentialProviders(selector: string, modelRegistry: ModelRegistry): string[] {
1268
+ function getBareSelectorCredentialProviders(
1269
+ selector: string,
1270
+ modelRegistry: ModelRegistry,
1271
+ credentialSessionId?: string,
1272
+ ): string[] {
1089
1273
  const suffix = splitSelectorThinkingSuffix(selector);
1090
1274
  const alias = (suffix.thinkingLevel ? suffix.selector : selector).trim().toLowerCase();
1091
1275
  const providers = modelRegistry
1092
- .getAll()
1276
+ .getAll(credentialSessionId)
1093
1277
  .filter(model => {
1094
1278
  const modelId = model.id.trim().toLowerCase();
1095
1279
  return modelId === alias || modelId.slice(modelId.lastIndexOf("/") + 1) === alias;
@@ -1147,6 +1331,8 @@ async function resolveAndClampSelectorValue(
1147
1331
  credentialSessionId: options.credentialSessionId,
1148
1332
  isCredentialUnavailable: provider =>
1149
1333
  isSessionCredentialPinBlocking(options.modelRegistry, provider, options.credentialSessionId),
1334
+ isProviderAuthAvailable: provider =>
1335
+ isStagedProviderAuthAvailable(options.modelRegistry, provider, options.credentialSessionId),
1150
1336
  },
1151
1337
  );
1152
1338
  resolved = {
@@ -1158,7 +1344,11 @@ async function resolveAndClampSelectorValue(
1158
1344
  }
1159
1345
  if (!resolved.model) {
1160
1346
  if (bareAlias) {
1161
- const providers = getBareSelectorCredentialProviders(selector, options.modelRegistry);
1347
+ const providers = getBareSelectorCredentialProviders(
1348
+ selector,
1349
+ options.modelRegistry,
1350
+ options.credentialSessionId,
1351
+ );
1162
1352
  const selectorSuffix = splitSelectorThinkingSuffix(selector);
1163
1353
  const bareSelector = selectorSuffix.thinkingLevel ? selectorSuffix.selector : selector;
1164
1354
  const aliasKnown =
@@ -1250,8 +1440,9 @@ async function concretizeProfileSelectorValue(
1250
1440
  selectorValue: ModelSelectorValue,
1251
1441
  prepared: PreparedModelProfileActivation,
1252
1442
  ): Promise<ModelSelectorValue> {
1253
- const candidates = prepared.modelRegistry.getAvailable?.() ?? prepared.modelRegistry.getAll();
1254
1443
  const credentialSessionId = prepared.session.credentialSessionId ?? prepared.session.sessionId;
1444
+ const candidates =
1445
+ prepared.modelRegistry.getAvailable?.(credentialSessionId) ?? prepared.modelRegistry.getAll(credentialSessionId);
1255
1446
  const concrete = await Promise.all(
1256
1447
  normalizeModelSelectorValue(selectorValue).map(async selector => {
1257
1448
  const bareAlias = !splitSelectorThinkingSuffix(selector).selector.includes("/");
@@ -1279,6 +1470,8 @@ async function concretizeProfileSelectorValue(
1279
1470
  credentialSessionId,
1280
1471
  isCredentialUnavailable: provider =>
1281
1472
  isSessionCredentialPinBlocking(prepared.modelRegistry, provider, credentialSessionId),
1473
+ isProviderAuthAvailable: provider =>
1474
+ isStagedProviderAuthAvailable(prepared.modelRegistry, provider, credentialSessionId),
1282
1475
  },
1283
1476
  )
1284
1477
  : resolveModelRoleValue(selector, candidates, {
@@ -1314,7 +1507,7 @@ async function concretizeProfileSelectorValue(
1314
1507
  * variant is cleared so a previous provider cannot silently resurrect.
1315
1508
  */
1316
1509
  function restoreCanonicalVariant(
1317
- modelRegistry: PrepareModelProfileActivationOptions["modelRegistry"],
1510
+ modelRegistry: Partial<Pick<ModelRegistry, "restoreSessionCanonicalVariant" | "clearCanonicalVariant">>,
1318
1511
  sessionId: string,
1319
1512
  previousCanonicalVariant: string | undefined,
1320
1513
  ): void {
@@ -1326,8 +1519,18 @@ function restoreCanonicalVariant(
1326
1519
  }
1327
1520
  }
1328
1521
 
1522
+ function selectorValuesEqual(left: ModelSelectorValue | undefined, right: ModelSelectorValue | undefined): boolean {
1523
+ if (left === undefined || right === undefined) return left === right;
1524
+ const leftSelectors = normalizeModelSelectorValue(left);
1525
+ const rightSelectors = normalizeModelSelectorValue(right);
1526
+ return (
1527
+ leftSelectors.length === rightSelectors.length &&
1528
+ leftSelectors.every((selector, index) => selector === rightSelectors[index])
1529
+ );
1530
+ }
1531
+
1329
1532
  function restoreCanonicalVariantAfterPreparation(
1330
- modelRegistry: PrepareModelProfileActivationOptions["modelRegistry"],
1533
+ modelRegistry: Partial<Pick<ModelRegistry, "restoreSessionCanonicalVariant" | "clearCanonicalVariant">>,
1331
1534
  session: ModelProfileActivationSession,
1332
1535
  previousCanonicalVariant: string | undefined,
1333
1536
  previousUserSelection: UserCanonicalVariantSelection | undefined,
@@ -1343,7 +1546,6 @@ function restoreCanonicalVariantAfterPreparation(
1343
1546
  userSelectionChanged ? currentUserSelection.canonicalVariant : previousCanonicalVariant,
1344
1547
  );
1345
1548
  }
1346
-
1347
1549
  export async function prepareModelProfileActivation(
1348
1550
  options: PrepareModelProfileActivationOptions,
1349
1551
  ): Promise<PreparedModelProfileActivation> {
@@ -1353,6 +1555,7 @@ export async function prepareModelProfileActivation(
1353
1555
  const profileLabel = formatModelProfileDisplayLabel(profile);
1354
1556
 
1355
1557
  const previousModel = options.session.model;
1558
+ const applySettings = options.settingsForApply ?? (options.settings as Settings);
1356
1559
  // Snapshot the exact pre-clear sticky selector (verbatim, not re-derived from
1357
1560
  // the live model) so a failed prepare/apply/materialize rollback restores the
1358
1561
  // genuinely-sticky provider even when the live model is a transient switch.
@@ -1363,7 +1566,7 @@ export async function prepareModelProfileActivation(
1363
1566
  // Explicit profile activation/reselection invalidates the session's sticky
1364
1567
  // canonical variant BEFORE the new profile's aliases resolve, so the old
1365
1568
  // provider's sticky variant cannot win the new profile's resolution.
1366
- options.modelRegistry.clearCanonicalVariant?.(options.session.sessionId);
1569
+ if (!options.preserveCanonicalVariant) options.modelRegistry.clearCanonicalVariant?.(options.session.sessionId);
1367
1570
 
1368
1571
  try {
1369
1572
  const requiredProviders = aggregateModelProfileRequiredProviders(profile.requiredProviders, profile);
@@ -1413,7 +1616,8 @@ export async function prepareModelProfileActivation(
1413
1616
  if (requiredProviderSet.has(provider) && !alternativeSet.has(provider)) throw error;
1414
1617
  continue;
1415
1618
  }
1416
- if (apiKey !== kNoAuth && !isAuthenticated(apiKey)) {
1619
+ const hasUsableAuth = isStagedProviderAuthAvailable(options.modelRegistry, provider, credentialSessionId);
1620
+ if (apiKey !== kNoAuth && !isAuthenticated(apiKey) && !hasUsableAuth) {
1417
1621
  if (requiredProviderSet.has(provider)) missingProviders.push(provider);
1418
1622
  } else {
1419
1623
  authenticatedProviders.push(provider);
@@ -1444,6 +1648,9 @@ export async function prepareModelProfileActivation(
1444
1648
  proxyProvider === undefined
1445
1649
  ? undefined
1446
1650
  : await getProfileProviderApiKey(options.modelRegistry, proxyProvider, credentialSessionId, profileLabel);
1651
+ const proxyHasUsableAuth =
1652
+ proxyProvider !== undefined &&
1653
+ isStagedProviderAuthAvailable(options.modelRegistry, proxyProvider, credentialSessionId);
1447
1654
  if (proxyProvider !== undefined) {
1448
1655
  const configuredProxyProviders = options.modelRegistry.getConfiguredProviderIds?.();
1449
1656
  if (!isModelProfileProxyConfigured(proxyProvider, configuredProxyProviders, proxyApiKey === kNoAuth)) {
@@ -1454,8 +1661,8 @@ export async function prepareModelProfileActivation(
1454
1661
  }
1455
1662
  const proxyAuthenticated =
1456
1663
  proxyProvider !== undefined &&
1457
- proxyApiKey !== undefined &&
1458
- (proxyApiKey === kNoAuth || isAuthenticated(proxyApiKey));
1664
+ ((proxyApiKey !== undefined && (proxyApiKey === kNoAuth || isAuthenticated(proxyApiKey))) ||
1665
+ proxyHasUsableAuth);
1459
1666
  if (proxyMode === "always" && !proxyAuthenticated) {
1460
1667
  throw new ModelProfileCredentialError(profileLabel, [proxyProvider!]);
1461
1668
  }
@@ -1486,8 +1693,9 @@ export async function prepareModelProfileActivation(
1486
1693
  );
1487
1694
  }
1488
1695
 
1489
- const availableModels = options.modelRegistry.getAvailable?.() ?? options.modelRegistry.getAll();
1490
- const roleCatalogModels = options.modelRegistry.getAll();
1696
+ const availableModels =
1697
+ options.modelRegistry.getAvailable?.(credentialSessionId) ?? options.modelRegistry.getAll(credentialSessionId);
1698
+ const roleCatalogModels = options.modelRegistry.getAll(credentialSessionId);
1491
1699
  let bindings = resolveProfileBindings(profile);
1492
1700
  if (alternativeGroups.length > 0) {
1493
1701
  bindings = rewriteBindingsProviders(bindings, new Set(authenticatedProviders), alternativeGroups);
@@ -1547,6 +1755,8 @@ export async function prepareModelProfileActivation(
1547
1755
  credentialSessionId,
1548
1756
  isCredentialUnavailable: provider =>
1549
1757
  isSessionCredentialPinBlocking(options.modelRegistry, provider, credentialSessionId),
1758
+ isProviderAuthAvailable: provider =>
1759
+ isStagedProviderAuthAvailable(options.modelRegistry, provider, credentialSessionId),
1550
1760
  },
1551
1761
  );
1552
1762
  const defaultModel = defaultResolution.model;
@@ -1573,50 +1783,131 @@ export async function prepareModelProfileActivation(
1573
1783
  credentialSessionId,
1574
1784
  profileLabel,
1575
1785
  });
1786
+ const installedKeys = options.session.getProfileInstalledOverrideKeys?.();
1787
+ const installedState = options.session.getProfileInstalledOverrideState?.();
1788
+ const baseModelRoles = { ...(options.settings.getOverride("modelRoles") ?? {}) };
1789
+ const baseAgentModelOverrides = { ...(options.settings.getOverride("task.agentModelOverrides") ?? {}) };
1790
+ const manualModelRoleOverrides = new Set(
1791
+ options.preserveManualOverrides ? (installedState?.manualModelRoles ?? []) : [],
1792
+ );
1793
+ const manualAgentModelOverrides = new Set(
1794
+ options.preserveManualOverrides ? (installedState?.manualAgentModelOverrides ?? []) : [],
1795
+ );
1796
+ const configuredAgentModelOverrides = options.modelRegistry?.getConfiguredModelBindings?.()?.agentModelOverrides;
1797
+ for (const role of installedKeys?.modelRoles ?? []) {
1798
+ if (options.preserveManualOverrides && manualModelRoleOverrides.has(role)) continue;
1799
+ const installed = installedState?.installedModelRoles.get(role);
1800
+ if (
1801
+ options.preserveManualOverrides &&
1802
+ installed !== undefined &&
1803
+ !selectorValuesEqual(baseModelRoles[role], installed)
1804
+ ) {
1805
+ manualModelRoleOverrides.add(role);
1806
+ continue;
1807
+ }
1808
+ delete baseModelRoles[role];
1809
+ }
1810
+ for (const role of installedKeys?.agentModelOverrides ?? []) {
1811
+ if (options.preserveManualOverrides && manualAgentModelOverrides.has(role)) continue;
1812
+ const installed = installedState?.installedAgentModelOverrides.get(role);
1813
+ if (
1814
+ options.preserveManualOverrides &&
1815
+ installed !== undefined &&
1816
+ !selectorValuesEqual(baseAgentModelOverrides[role], installed)
1817
+ ) {
1818
+ manualAgentModelOverrides.add(role);
1819
+ continue;
1820
+ }
1821
+ delete baseAgentModelOverrides[role];
1822
+ }
1823
+ // Compare the current session override with the installed profile value
1824
+ // before excluding configured bindings from the successor baseline. The
1825
+ // current value may itself be the installed profile value; filtering first
1826
+ // would misclassify that role as a manual override during configuration reload.
1827
+ for (const role of Object.keys(configuredAgentModelOverrides ?? {})) {
1828
+ if (!manualAgentModelOverrides.has(role)) delete baseAgentModelOverrides[role];
1829
+ }
1830
+ for (const [role, value] of installedState?.modelRoles ?? []) {
1831
+ if (manualModelRoleOverrides.has(role)) continue;
1832
+ if (value === undefined) delete baseModelRoles[role];
1833
+ else baseModelRoles[role] = value;
1834
+ }
1835
+ for (const [role, value] of installedState?.agentModelOverrides ?? []) {
1836
+ if (manualAgentModelOverrides.has(role)) continue;
1837
+ const configuredBinding = configuredAgentModelOverrides?.[role];
1838
+ if (configuredBinding !== undefined) {
1839
+ // Configured bindings are reapplied when the active profile is cleared;
1840
+ // leaving them in this baseline makes omitted successor roles stay live.
1841
+ delete baseAgentModelOverrides[role];
1842
+ } else if (installedState?.configuredAgentModelOverrides.has(role)) {
1843
+ // A removed binding must not revive its former value from the profile
1844
+ // baseline after a configuration reload.
1845
+ delete baseAgentModelOverrides[role];
1846
+ } else if (value === undefined) {
1847
+ delete baseAgentModelOverrides[role];
1848
+ } else {
1849
+ baseAgentModelOverrides[role] = value;
1850
+ }
1851
+ }
1852
+ const preparedDefaultModelSelection =
1853
+ options.prepareSessionModelSelection && defaultModel
1854
+ ? await options.session.prepareModelSelectionForProfileActivation?.(
1855
+ defaultModel,
1856
+ defaultThinkingLevel,
1857
+ options.signal,
1858
+ )
1859
+ : undefined;
1860
+ if (options.prepareSessionModelSelection && defaultModel && !preparedDefaultModelSelection) {
1861
+ throw new Error("Session cannot prepare a synchronous profile model selection");
1862
+ }
1863
+ options.signal?.throwIfAborted();
1864
+ const nextModelRoles = { ...modelRoles };
1865
+ const nextAgentModelOverrides = { ...agentModelOverrides };
1866
+ for (const role of manualModelRoleOverrides) delete nextModelRoles[role];
1867
+ for (const role of manualAgentModelOverrides) delete nextAgentModelOverrides[role];
1576
1868
 
1577
1869
  return {
1578
1870
  profileName,
1579
1871
  session: options.session as PreparedModelProfileActivation["session"],
1580
- settings: options.settings as PreparedModelProfileActivation["settings"],
1872
+ settings: applySettings as PreparedModelProfileActivation["settings"],
1581
1873
  modelRegistry: options.modelRegistry,
1582
1874
  previousModel,
1583
1875
  previousCanonicalVariant,
1584
1876
  previousUserCanonicalVariantSelection,
1585
1877
  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
1878
+ previousAgentModelOverrides: { ...applySettings.get("task.agentModelOverrides") },
1879
+ previousModelRoles: { ...applySettings.get("modelRoles") },
1880
+ baseAgentModelOverrides,
1881
+ baseModelRoles,
1882
+ previousPersistedModelRoles: applySettings.getGlobal("modelRoles"),
1883
+ previousPersistedAgentModelOverrides: applySettings.getGlobal("task.agentModelOverrides"),
1884
+ previousModelRolesOverride: applySettings.getOverride("modelRoles"),
1885
+ previousAgentModelOverridesOverride: applySettings.getOverride("task.agentModelOverrides"),
1886
+ previousDefaultProfileOverride: applySettings.getOverride("modelProfile.default"),
1887
+ previousPersistedDefaultProfile: applySettings.getGlobal("modelProfile.default"),
1888
+ previousPersistedDefaultThinkingLevel: applySettings.getGlobal("defaultThinkingLevel") as
1606
1889
  | Exclude<ThinkingLevel, "inherit">
1607
1890
  | undefined,
1608
- previousDefaultThinkingLevelOverride: options.settings.getOverride("defaultThinkingLevel"),
1891
+ previousDefaultThinkingLevelOverride: applySettings.getOverride("defaultThinkingLevel"),
1609
1892
  previousDefaultChain: options.session.getConfiguredModelChain("default"),
1610
1893
  previousDefaultChainState: options.session.getConfiguredModelChainState?.("default"),
1894
+ previousProfileInstalledOverrideState: installedState,
1895
+ preparedDefaultModelSelection,
1896
+ defaultChainPublished: false,
1897
+ defaultModelPublished: false,
1898
+ publishedState: undefined,
1611
1899
 
1612
1900
  defaultModel,
1613
1901
  defaultThinkingLevel,
1614
1902
  defaultActiveIndex,
1615
1903
  defaultResolutionSkips,
1616
1904
  defaultChain,
1617
- modelRoles,
1618
- agentModelOverrides,
1905
+ modelRoles: nextModelRoles,
1906
+ agentModelOverrides: nextAgentModelOverrides,
1907
+ manualModelRoleOverrides,
1908
+ manualAgentModelOverrides,
1619
1909
  previousActiveModelProfile: options.session.getActiveModelProfile?.(),
1910
+ previousActivatedModelProfile: options.session.getActivatedModelProfile?.(),
1620
1911
  previousSessionDefaultModel: options.session.getSessionDefaultModelSelector?.(),
1621
1912
  previousDefaultFallbackRuntimeState: options.session.getDefaultFallbackRuntimeState?.(),
1622
1913
  };
@@ -1707,34 +1998,45 @@ function restoreConcurrentProfileOverrides(
1707
1998
  return Object.keys(restored).length > 0 || previous !== undefined ? restored : undefined;
1708
1999
  }
1709
2000
 
1710
- export async function applyPreparedModelProfileActivation(
2001
+ function capturePublishedModelProfileActivationState(
1711
2002
  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;
2003
+ ): PublishedModelProfileActivationState {
2004
+ return {
2005
+ model: prepared.session.model,
2006
+ thinkingLevel: prepared.session.thinkingLevel,
2007
+ modelRolesOverride: prepared.settings.getOverride("modelRoles"),
2008
+ agentModelOverridesOverride: prepared.settings.getOverride("task.agentModelOverrides"),
2009
+ defaultChainState: prepared.session.getConfiguredModelChainState?.("default"),
2010
+ defaultFallbackRuntimeState: prepared.session.getDefaultFallbackRuntimeState?.(),
2011
+ activeModelProfile: prepared.session.getActiveModelProfile?.(),
2012
+ activatedModelProfile: prepared.session.getActivatedModelProfile?.(),
2013
+ profileInstalledOverrideState: prepared.session.getProfileInstalledOverrideState?.(),
2014
+ sessionDefaultModel: prepared.session.getSessionDefaultModelSelector?.(),
2015
+ persistedModelRoles: prepared.settings.getGlobal("modelRoles"),
2016
+ persistedAgentModelOverrides: prepared.settings.getGlobal("task.agentModelOverrides"),
2017
+ persistedDefaultProfile: prepared.settings.getGlobal("modelProfile.default"),
2018
+ persistedDefaultThinkingLevel: prepared.settings.getGlobal("defaultThinkingLevel"),
2019
+ defaultThinkingLevelOverride: prepared.settings.getOverride("defaultThinkingLevel"),
2020
+ defaultProfileOverride: prepared.settings.getOverride("modelProfile.default"),
2021
+ };
2022
+ }
1726
2023
 
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.
2024
+ /** Publish the prepared model selection and role layer without awaiting. */
2025
+ export function publishPreparedModelProfileActivation(
2026
+ prepared: PreparedModelProfileActivation,
2027
+ options: Pick<
2028
+ ApplyModelProfileActivationOptions,
2029
+ "preserveDefaultModelSelection" | "preserveLiveModelSelection"
2030
+ > = {},
2031
+ ): void {
2032
+ if (!options.preserveDefaultModelSelection && prepared.preparedDefaultModelSelection) {
1731
2033
  const ownedDefaultChain =
1732
2034
  prepared.defaultChain.length > 0
1733
2035
  ? prepared.defaultChain
1734
2036
  : (prepared.previousDefaultChain ??
1735
2037
  (prepared.previousModel ? [formatModelString(prepared.previousModel)] : []));
1736
2038
  if (ownedDefaultChain.length > 0) {
1737
- defaultChainChanged = true;
2039
+ prepared.defaultChainPublished = true;
1738
2040
  prepared.session.setConfiguredModelChain(
1739
2041
  "default",
1740
2042
  ownedDefaultChain,
@@ -1748,44 +2050,152 @@ export async function applyPreparedModelProfileActivation(
1748
2050
  prepared.defaultResolutionSkips,
1749
2051
  );
1750
2052
  }
1751
- activatedDefaultChainState = prepared.session.getConfiguredModelChainState?.("default");
1752
- activatedFallbackRuntimeState = prepared.session.getDefaultFallbackRuntimeState?.();
1753
2053
  }
1754
- if (prepared.defaultModel) {
2054
+ }
2055
+ if (
2056
+ !options.preserveDefaultModelSelection &&
2057
+ !options.preserveLiveModelSelection &&
2058
+ prepared.preparedDefaultModelSelection
2059
+ ) {
2060
+ prepared.defaultModelPublished = true;
2061
+ if (!prepared.session.commitPreparedProfileModelSelection) {
2062
+ throw new Error("Session cannot commit a prepared profile model selection");
2063
+ }
2064
+ prepared.session.commitPreparedProfileModelSelection(prepared.preparedDefaultModelSelection);
2065
+ (prepared.session.modelRegistry ?? prepared.modelRegistry).seedCanonicalVariant?.(
2066
+ prepared.session.sessionId,
2067
+ prepared.defaultModel!,
2068
+ );
2069
+ prepared.session.recordResumeDefaultModel?.(`${prepared.defaultModel!.provider}/${prepared.defaultModel!.id}`);
2070
+ }
2071
+ prepared.settings.override("modelRoles", {
2072
+ ...prepared.baseModelRoles,
2073
+ ...prepared.modelRoles,
2074
+ });
2075
+ prepared.settings.override("task.agentModelOverrides", {
2076
+ ...prepared.baseAgentModelOverrides,
2077
+ ...prepared.agentModelOverrides,
2078
+ });
2079
+ prepared.session.setActiveModelProfile?.(prepared.profileName, true);
2080
+ prepared.session.noteProfileInstalledOverrides?.(
2081
+ Object.keys(prepared.modelRoles),
2082
+ Object.keys(prepared.agentModelOverrides),
2083
+ prepared.previousModel,
2084
+ { modelRoles: prepared.baseModelRoles, agentModelOverrides: prepared.baseAgentModelOverrides },
2085
+ { modelRoles: prepared.modelRoles, agentModelOverrides: prepared.agentModelOverrides },
2086
+ {
2087
+ modelRoles: [...prepared.manualModelRoleOverrides],
2088
+ agentModelOverrides: [...prepared.manualAgentModelOverrides],
2089
+ },
2090
+ prepared.defaultChain,
2091
+ );
2092
+ prepared.publishedState = capturePublishedModelProfileActivationState(prepared);
2093
+ }
2094
+
2095
+ export async function finishPreparedModelProfileActivation(prepared: PreparedModelProfileActivation): Promise<void> {
2096
+ if (prepared.defaultModelPublished && prepared.preparedDefaultModelSelection) {
2097
+ try {
2098
+ await prepared.session.finishPreparedProfileModelSelection?.(prepared.preparedDefaultModelSelection);
2099
+ } catch (error) {
2100
+ logger.warn("Failed to refresh session tools after model profile activation", {
2101
+ profile: prepared.profileName,
2102
+ error: error instanceof Error ? error.message : String(error),
2103
+ });
2104
+ }
2105
+ }
2106
+ try {
2107
+ await prepared.session.syncEagerDelegation?.();
2108
+ } catch (error) {
2109
+ logger.warn("Failed to sync eager delegation after model profile activation", {
2110
+ profile: prepared.profileName,
2111
+ error: error instanceof Error ? error.message : String(error),
2112
+ });
2113
+ }
2114
+ }
2115
+
2116
+ export async function applyPreparedModelProfileActivation(
2117
+ prepared: PreparedModelProfileActivation,
2118
+ options: ApplyModelProfileActivationOptions = {},
2119
+ ): Promise<void> {
2120
+ let activationStage = "default chain";
2121
+ let modelMutationStarted = false;
2122
+ let overridesChanged = false;
2123
+ let modelRolesChanged = false;
2124
+ let persistentMutationStarted = false;
2125
+ let defaultChainChanged = false;
2126
+ let resumeDefaultChanged = false;
2127
+ let activatedDefaultChainState: ConfiguredModelChainState | undefined;
2128
+ let activatedFallbackRuntimeState: DefaultFallbackRuntimeState | undefined;
2129
+ let activatedModelRolesOverride: unknown;
2130
+ let activatedAgentModelOverridesOverride: unknown;
2131
+ let activatedProfileInstalledOverrideState: AgentSessionProfileInstalledOverrideState | undefined;
2132
+ let activatedResumeDefaultModel: string | undefined;
2133
+
2134
+ try {
2135
+ if (!options.deferRuntimePublication && !options.preserveDefaultModelSelection) {
2136
+ const ownedDefaultChain =
2137
+ prepared.defaultChain.length > 0
2138
+ ? prepared.defaultChain
2139
+ : (prepared.previousDefaultChain ??
2140
+ (prepared.previousModel ? [formatModelString(prepared.previousModel)] : []));
2141
+ if (ownedDefaultChain.length > 0) {
2142
+ defaultChainChanged = true;
2143
+ prepared.session.setConfiguredModelChain(
2144
+ "default",
2145
+ ownedDefaultChain,
2146
+ "profile-activation",
2147
+ prepared.profileName,
2148
+ true,
2149
+ );
2150
+ if (prepared.defaultActiveIndex !== undefined) {
2151
+ prepared.session.seedDefaultFallbackResolution?.(
2152
+ prepared.defaultActiveIndex,
2153
+ prepared.defaultResolutionSkips,
2154
+ );
2155
+ }
2156
+ activatedDefaultChainState = prepared.session.getConfiguredModelChainState?.("default");
2157
+ activatedFallbackRuntimeState = prepared.session.getDefaultFallbackRuntimeState?.();
2158
+ }
2159
+ }
2160
+ if (
2161
+ !options.deferRuntimePublication &&
2162
+ !options.preserveDefaultModelSelection &&
2163
+ !options.preserveLiveModelSelection &&
2164
+ prepared.defaultModel
2165
+ ) {
1755
2166
  activationStage = "model selection";
1756
2167
  await prepared.session.setModelTemporary(
1757
2168
  prepared.defaultModel,
1758
2169
  options.thinkingLevelOverride ?? prepared.defaultThinkingLevel,
1759
2170
  {
1760
2171
  cause: "profile-activation",
2172
+ signal: options.signal,
1761
2173
  shouldMutate: options.isCurrent,
1762
2174
  onMutationStarted: () => {
1763
2175
  modelMutationStarted = true;
1764
2176
  },
1765
2177
  },
1766
2178
  );
2179
+ options.signal?.throwIfAborted();
1767
2180
  if (options.isCurrent && !modelMutationStarted) throw new ModelProfileActivationSupersededError();
1768
2181
  if (options.isCurrent && !options.isCurrent()) throw new ModelProfileActivationSupersededError();
1769
2182
  }
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;
2183
+ if (!options.deferRuntimePublication) {
2184
+ activationStage = "model role overrides";
2185
+ prepared.settings.override("modelRoles", {
2186
+ ...prepared.baseModelRoles,
2187
+ ...prepared.modelRoles,
2188
+ });
2189
+ activatedModelRolesOverride = prepared.settings.getOverride("modelRoles");
2190
+ modelRolesChanged = true;
2191
+ activationStage = "agent role overrides";
2192
+ prepared.settings.override("task.agentModelOverrides", {
2193
+ ...prepared.baseAgentModelOverrides,
2194
+ ...prepared.agentModelOverrides,
2195
+ });
2196
+ activatedAgentModelOverridesOverride = prepared.settings.getOverride("task.agentModelOverrides");
2197
+ overridesChanged = true;
2198
+ }
1789
2199
  if (options.persistDefault) {
1790
2200
  persistentMutationStarted = true;
1791
2201
  activationStage = "persist model roles";
@@ -1802,28 +2212,47 @@ export async function applyPreparedModelProfileActivation(
1802
2212
  await prepared.settings.flushOrThrow();
1803
2213
  }
1804
2214
  if (options.isCurrent && !options.isCurrent()) throw new ModelProfileActivationSupersededError();
1805
- activationStage = "active profile marker";
1806
- prepared.session.setActiveModelProfile?.(prepared.profileName);
1807
- if (prepared.defaultModel) {
2215
+ if (!options.deferRuntimePublication) {
2216
+ activationStage = "active profile marker";
2217
+ prepared.session.setActiveModelProfile?.(prepared.profileName, true);
2218
+ activationStage = "installed role tracking";
2219
+ prepared.session.noteProfileInstalledOverrides?.(
2220
+ Object.keys(prepared.modelRoles),
2221
+ Object.keys(prepared.agentModelOverrides),
2222
+ prepared.previousModel,
2223
+ { modelRoles: prepared.baseModelRoles, agentModelOverrides: prepared.baseAgentModelOverrides },
2224
+ { modelRoles: prepared.modelRoles, agentModelOverrides: prepared.agentModelOverrides },
2225
+ {
2226
+ modelRoles: [...prepared.manualModelRoleOverrides],
2227
+ agentModelOverrides: [...prepared.manualAgentModelOverrides],
2228
+ },
2229
+ prepared.defaultChain,
2230
+ );
2231
+ activatedProfileInstalledOverrideState = prepared.session.getProfileInstalledOverrideState?.();
2232
+ }
2233
+ if (
2234
+ !options.deferRuntimePublication &&
2235
+ !options.preserveDefaultModelSelection &&
2236
+ !options.preserveLiveModelSelection &&
2237
+ prepared.defaultModel
2238
+ ) {
1808
2239
  activationStage = "canonical model variant";
1809
2240
  prepared.modelRegistry.seedCanonicalVariant?.(prepared.session.sessionId, prepared.defaultModel);
1810
2241
  resumeDefaultChanged = true;
1811
2242
  activationStage = "resume default model";
1812
- prepared.session.recordResumeDefaultModel?.(`${prepared.defaultModel.provider}/${prepared.defaultModel.id}`);
2243
+ activatedResumeDefaultModel = `${prepared.defaultModel.provider}/${prepared.defaultModel.id}`;
2244
+ prepared.session.recordResumeDefaultModel?.(activatedResumeDefaultModel);
1813
2245
  }
1814
- activationStage = "installed role tracking";
1815
- prepared.session.noteProfileInstalledOverrides?.(
1816
- Object.keys(prepared.modelRoles),
1817
- Object.keys(prepared.agentModelOverrides),
1818
- prepared.previousModel,
1819
- );
2246
+ prepared.publishedState = capturePublishedModelProfileActivationState(prepared);
1820
2247
  } catch (error) {
1821
2248
  const activationWasCancelled = error instanceof ModelProfileActivationSupersededError;
1822
- const selectionSuperseded = options.isCurrent?.() === false;
2249
+ const selectionIsCurrent = () => options.isCurrent?.() !== false;
1823
2250
  const shouldRestoreFallbackRuntimeState =
1824
2251
  prepared.previousDefaultFallbackRuntimeState !== undefined &&
1825
- (!selectionSuperseded ||
2252
+ (selectionIsCurrent() ||
1826
2253
  sameSerializedValue(prepared.session.getDefaultFallbackRuntimeState?.(), activatedFallbackRuntimeState));
2254
+ let modelRollbackSelectionRevision: number | undefined;
2255
+ let modelRollbackSelectionWasCurrent = false;
1827
2256
  const rollbackErrors: Array<{ stage: string; error: unknown }> = [];
1828
2257
  const restore = (stage: string, action: () => void): void => {
1829
2258
  try {
@@ -1834,7 +2263,7 @@ export async function applyPreparedModelProfileActivation(
1834
2263
  };
1835
2264
  if (persistentMutationStarted) {
1836
2265
  if (
1837
- !selectionSuperseded ||
2266
+ selectionIsCurrent() ||
1838
2267
  sameSerializedValue(prepared.settings.getGlobal("modelProfile.default"), prepared.profileName)
1839
2268
  ) {
1840
2269
  restore("restore default profile setting", () =>
@@ -1845,7 +2274,7 @@ export async function applyPreparedModelProfileActivation(
1845
2274
  }
1846
2275
  const currentPersistedModelRoles = prepared.settings.getGlobal("modelRoles");
1847
2276
  const restoredModelRoles =
1848
- selectionSuperseded && currentPersistedModelRoles && !sameSerializedValue(currentPersistedModelRoles, {})
2277
+ !selectionIsCurrent() && currentPersistedModelRoles && !sameSerializedValue(currentPersistedModelRoles, {})
1849
2278
  ? { ...(prepared.previousPersistedModelRoles ?? {}), ...currentPersistedModelRoles }
1850
2279
  : prepared.previousPersistedModelRoles;
1851
2280
  restore("restore model role setting", () =>
@@ -1855,7 +2284,7 @@ export async function applyPreparedModelProfileActivation(
1855
2284
  );
1856
2285
  const currentPersistedAgentModelOverrides = prepared.settings.getGlobal("task.agentModelOverrides");
1857
2286
  const restoredPersistedAgentModelOverrides =
1858
- selectionSuperseded &&
2287
+ !selectionIsCurrent() &&
1859
2288
  currentPersistedAgentModelOverrides &&
1860
2289
  !sameSerializedValue(currentPersistedAgentModelOverrides, {})
1861
2290
  ? {
@@ -1873,7 +2302,7 @@ export async function applyPreparedModelProfileActivation(
1873
2302
  ? prepared.defaultThinkingLevel
1874
2303
  : prepared.previousPersistedDefaultThinkingLevel;
1875
2304
  if (
1876
- !selectionSuperseded ||
2305
+ selectionIsCurrent() ||
1877
2306
  sameSerializedValue(prepared.settings.getGlobal("defaultThinkingLevel"), activatedThinkingLevel)
1878
2307
  ) {
1879
2308
  restore("restore thinking level setting", () =>
@@ -1885,7 +2314,7 @@ export async function applyPreparedModelProfileActivation(
1885
2314
  }
1886
2315
  if (
1887
2316
  modelRolesChanged &&
1888
- (!selectionSuperseded ||
2317
+ (selectionIsCurrent() ||
1889
2318
  sameSerializedValue(prepared.settings.getOverride("modelRoles"), activatedModelRolesOverride))
1890
2319
  ) {
1891
2320
  restore("restore model role overrides", () =>
@@ -1896,7 +2325,7 @@ export async function applyPreparedModelProfileActivation(
1896
2325
  }
1897
2326
  if (
1898
2327
  overridesChanged &&
1899
- (!selectionSuperseded ||
2328
+ (selectionIsCurrent() ||
1900
2329
  sameSerializedValue(
1901
2330
  prepared.settings.getOverride("task.agentModelOverrides"),
1902
2331
  activatedAgentModelOverridesOverride,
@@ -1908,7 +2337,9 @@ export async function applyPreparedModelProfileActivation(
1908
2337
  : prepared.settings.override("task.agentModelOverrides", prepared.previousAgentModelOverridesOverride),
1909
2338
  );
1910
2339
  }
1911
- if (modelMutationStarted && !selectionSuperseded) {
2340
+ if (modelMutationStarted && selectionIsCurrent()) {
2341
+ modelRollbackSelectionWasCurrent = selectionIsCurrent();
2342
+ modelRollbackSelectionRevision = prepared.session.getUserModelSelectionRevision?.();
1912
2343
  try {
1913
2344
  if (prepared.session.restoreModelSelectionForRollback) {
1914
2345
  await prepared.session.restoreModelSelectionForRollback(
@@ -1918,6 +2349,7 @@ export async function applyPreparedModelProfileActivation(
1918
2349
  } else if (prepared.previousModel) {
1919
2350
  await prepared.session.setModelTemporary(prepared.previousModel, prepared.previousThinkingLevel, {
1920
2351
  cause: "rollback",
2352
+ shouldMutate: selectionIsCurrent,
1921
2353
  });
1922
2354
  } else {
1923
2355
  throw new Error("Model-less profile activation rollback is unavailable");
@@ -1926,14 +2358,18 @@ export async function applyPreparedModelProfileActivation(
1926
2358
  rollbackErrors.push({ stage: "restore live model", error: rollbackError });
1927
2359
  }
1928
2360
  }
1929
- if (resumeDefaultChanged) {
2361
+ if (
2362
+ resumeDefaultChanged &&
2363
+ selectionIsCurrent() &&
2364
+ sameSerializedValue(prepared.session.getSessionDefaultModelSelector?.(), activatedResumeDefaultModel)
2365
+ ) {
1930
2366
  restore("restore resume default", () =>
1931
2367
  prepared.session.recordResumeDefaultModel?.(prepared.previousSessionDefaultModel),
1932
2368
  );
1933
2369
  }
1934
2370
  if (
1935
2371
  defaultChainChanged &&
1936
- (!selectionSuperseded ||
2372
+ (selectionIsCurrent() ||
1937
2373
  sameSerializedValue(prepared.session.getConfiguredModelChainState?.("default"), activatedDefaultChainState))
1938
2374
  ) {
1939
2375
  const previousChain = prepared.previousDefaultChainState;
@@ -1949,14 +2385,34 @@ export async function applyPreparedModelProfileActivation(
1949
2385
  ),
1950
2386
  );
1951
2387
  }
1952
- if (shouldRestoreFallbackRuntimeState) {
2388
+ const userSelectionChangedDuringModelRollback =
2389
+ modelRollbackSelectionRevision !== undefined
2390
+ ? prepared.session.getUserModelSelectionRevision?.() !== modelRollbackSelectionRevision
2391
+ : modelRollbackSelectionWasCurrent && !selectionIsCurrent();
2392
+ if (shouldRestoreFallbackRuntimeState && !userSelectionChangedDuringModelRollback) {
1953
2393
  restore("restore fallback runtime", () =>
1954
2394
  prepared.session.restoreDefaultFallbackRuntimeState?.(prepared.previousDefaultFallbackRuntimeState!),
1955
2395
  );
1956
2396
  }
1957
- if (!selectionSuperseded) {
2397
+ if (selectionIsCurrent()) {
1958
2398
  restore("restore active profile", () =>
1959
- prepared.session.setActiveModelProfile?.(prepared.previousActiveModelProfile),
2399
+ prepared.session.setActiveModelProfile?.(
2400
+ prepared.previousActiveModelProfile,
2401
+ prepared.previousActivatedModelProfile === prepared.previousActiveModelProfile,
2402
+ ),
2403
+ );
2404
+ }
2405
+ if (
2406
+ prepared.previousProfileInstalledOverrideState &&
2407
+ (selectionIsCurrent() ||
2408
+ (activatedProfileInstalledOverrideState !== undefined &&
2409
+ util.isDeepStrictEqual(
2410
+ prepared.session.getProfileInstalledOverrideState?.(),
2411
+ activatedProfileInstalledOverrideState,
2412
+ )))
2413
+ ) {
2414
+ restore("restore installed role tracking", () =>
2415
+ prepared.session.restoreProfileInstalledOverrideState?.(prepared.previousProfileInstalledOverrideState!),
1960
2416
  );
1961
2417
  }
1962
2418
  restore("restore canonical model variant", () =>
@@ -1980,17 +2436,258 @@ export async function applyPreparedModelProfileActivation(
1980
2436
  if (activationWasCancelled) return;
1981
2437
  throw error;
1982
2438
  }
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
- });
2439
+ if (!options.deferRuntimePublication) await finishPreparedModelProfileActivation(prepared);
2440
+ }
2441
+
2442
+ /** Restore an applied activation when its enclosing settings/registry commit fails. */
2443
+ export async function rollbackPreparedModelProfileActivation(
2444
+ prepared: PreparedModelProfileActivation,
2445
+ options: ApplyModelProfileActivationOptions = {},
2446
+ ): Promise<void> {
2447
+ const failures: Array<{ stage: string; error: unknown }> = [];
2448
+ const restore = (stage: string, action: () => void): void => {
2449
+ try {
2450
+ action();
2451
+ } catch (error) {
2452
+ failures.push({ stage, error });
2453
+ }
2454
+ };
2455
+ const restoreAsync = async (stage: string, action: () => Promise<void>): Promise<void> => {
2456
+ try {
2457
+ await action();
2458
+ } catch (error) {
2459
+ failures.push({ stage, error });
2460
+ }
2461
+ };
2462
+ const selectionIsCurrent = () => options.isCurrent?.() !== false;
2463
+ const publishedState = prepared.publishedState;
2464
+ const canRestorePublishedState = (current: unknown, activated: unknown): boolean =>
2465
+ publishedState === undefined ? selectionIsCurrent() : util.isDeepStrictEqual(current, activated);
2466
+ const canRollbackPublishedProfile = (): boolean =>
2467
+ publishedState === undefined
2468
+ ? selectionIsCurrent()
2469
+ : util.isDeepStrictEqual(prepared.session.getActiveModelProfile?.(), publishedState.activeModelProfile) &&
2470
+ util.isDeepStrictEqual(prepared.session.getActivatedModelProfile?.(), publishedState.activatedModelProfile);
2471
+
2472
+ if (options.persistDefault) {
2473
+ if (
2474
+ canRestorePublishedState(
2475
+ prepared.settings.getGlobal("modelProfile.default"),
2476
+ publishedState?.persistedDefaultProfile,
2477
+ )
2478
+ ) {
2479
+ restore("restore persisted default profile", () =>
2480
+ prepared.previousPersistedDefaultProfile === undefined
2481
+ ? prepared.settings.unset("modelProfile.default")
2482
+ : prepared.settings.set("modelProfile.default", prepared.previousPersistedDefaultProfile),
2483
+ );
2484
+ }
2485
+ const currentPersistedModelRoles = prepared.settings.getGlobal("modelRoles");
2486
+ const restoredPersistedModelRoles =
2487
+ publishedState && canRollbackPublishedProfile()
2488
+ ? restoreConcurrentProfileOverrides(
2489
+ prepared.previousPersistedModelRoles,
2490
+ publishedState.persistedModelRoles,
2491
+ currentPersistedModelRoles,
2492
+ )
2493
+ : publishedState === undefined && selectionIsCurrent()
2494
+ ? prepared.previousPersistedModelRoles
2495
+ : currentPersistedModelRoles;
2496
+ if (!util.isDeepStrictEqual(restoredPersistedModelRoles, currentPersistedModelRoles)) {
2497
+ restore("restore persisted model roles", () =>
2498
+ restoredPersistedModelRoles === undefined
2499
+ ? prepared.settings.unset("modelRoles")
2500
+ : prepared.settings.set("modelRoles", restoredPersistedModelRoles),
2501
+ );
2502
+ }
2503
+ const currentPersistedAgentOverrides = prepared.settings.getGlobal("task.agentModelOverrides");
2504
+ const restoredPersistedAgentOverrides =
2505
+ publishedState && canRollbackPublishedProfile()
2506
+ ? restoreConcurrentProfileOverrides(
2507
+ prepared.previousPersistedAgentModelOverrides,
2508
+ publishedState.persistedAgentModelOverrides,
2509
+ currentPersistedAgentOverrides,
2510
+ )
2511
+ : publishedState === undefined && selectionIsCurrent()
2512
+ ? prepared.previousPersistedAgentModelOverrides
2513
+ : currentPersistedAgentOverrides;
2514
+ if (!util.isDeepStrictEqual(restoredPersistedAgentOverrides, currentPersistedAgentOverrides)) {
2515
+ restore("restore persisted agent overrides", () =>
2516
+ restoredPersistedAgentOverrides === undefined
2517
+ ? prepared.settings.unset("task.agentModelOverrides")
2518
+ : prepared.settings.set("task.agentModelOverrides", restoredPersistedAgentOverrides),
2519
+ );
2520
+ }
2521
+ if (
2522
+ canRestorePublishedState(
2523
+ prepared.settings.getGlobal("defaultThinkingLevel"),
2524
+ publishedState?.persistedDefaultThinkingLevel,
2525
+ )
2526
+ ) {
2527
+ restore("restore persisted thinking level", () =>
2528
+ prepared.previousPersistedDefaultThinkingLevel === undefined
2529
+ ? prepared.settings.unset("defaultThinkingLevel")
2530
+ : prepared.settings.set("defaultThinkingLevel", prepared.previousPersistedDefaultThinkingLevel),
2531
+ );
2532
+ }
2533
+ if (
2534
+ canRestorePublishedState(
2535
+ prepared.settings.getOverride("defaultThinkingLevel"),
2536
+ publishedState?.defaultThinkingLevelOverride,
2537
+ )
2538
+ ) {
2539
+ restore("restore thinking level override", () =>
2540
+ prepared.previousDefaultThinkingLevelOverride === undefined
2541
+ ? prepared.settings.clearOverride("defaultThinkingLevel")
2542
+ : prepared.settings.override("defaultThinkingLevel", prepared.previousDefaultThinkingLevelOverride),
2543
+ );
2544
+ }
2545
+ }
2546
+ const currentModelRolesOverride = prepared.settings.getOverride("modelRoles");
2547
+ const restoredModelRolesOverride =
2548
+ publishedState && canRollbackPublishedProfile()
2549
+ ? restoreConcurrentProfileOverrides(
2550
+ prepared.previousModelRolesOverride,
2551
+ publishedState.modelRolesOverride,
2552
+ currentModelRolesOverride,
2553
+ )
2554
+ : publishedState === undefined && selectionIsCurrent()
2555
+ ? prepared.previousModelRolesOverride
2556
+ : currentModelRolesOverride;
2557
+ if (!util.isDeepStrictEqual(restoredModelRolesOverride, currentModelRolesOverride)) {
2558
+ restore("restore model role overrides", () =>
2559
+ restoredModelRolesOverride === undefined
2560
+ ? prepared.settings.clearOverride("modelRoles")
2561
+ : prepared.settings.override("modelRoles", restoredModelRolesOverride),
2562
+ );
2563
+ }
2564
+ const currentAgentModelOverridesOverride = prepared.settings.getOverride("task.agentModelOverrides");
2565
+ const restoredAgentModelOverridesOverride =
2566
+ publishedState && canRollbackPublishedProfile()
2567
+ ? restoreConcurrentProfileOverrides(
2568
+ prepared.previousAgentModelOverridesOverride,
2569
+ publishedState.agentModelOverridesOverride,
2570
+ currentAgentModelOverridesOverride,
2571
+ )
2572
+ : publishedState === undefined && selectionIsCurrent()
2573
+ ? prepared.previousAgentModelOverridesOverride
2574
+ : currentAgentModelOverridesOverride;
2575
+ if (!util.isDeepStrictEqual(restoredAgentModelOverridesOverride, currentAgentModelOverridesOverride)) {
2576
+ restore("restore agent role overrides", () =>
2577
+ restoredAgentModelOverridesOverride === undefined
2578
+ ? prepared.settings.clearOverride("task.agentModelOverrides")
2579
+ : prepared.settings.override("task.agentModelOverrides", restoredAgentModelOverridesOverride),
2580
+ );
2581
+ }
2582
+ if (!options.preserveDefaultModelSelection) {
2583
+ const previousChain = prepared.previousDefaultChainState;
2584
+ if (
2585
+ prepared.defaultChainPublished &&
2586
+ canRollbackPublishedProfile() &&
2587
+ canRestorePublishedState(
2588
+ prepared.session.getConfiguredModelChainState?.("default"),
2589
+ publishedState?.defaultChainState,
2590
+ )
2591
+ ) {
2592
+ restore("restore default chain", () =>
2593
+ prepared.session.setConfiguredModelChain(
2594
+ "default",
2595
+ previousChain?.entries ??
2596
+ prepared.previousDefaultChain ??
2597
+ (prepared.previousModel ? [`${prepared.previousModel.provider}/${prepared.previousModel.id}`] : []),
2598
+ previousChain?.origin ?? "rollback",
2599
+ previousChain?.identity,
2600
+ previousChain?.explicitHead ?? true,
2601
+ ),
2602
+ );
2603
+ }
2604
+ if (
2605
+ !options.preserveLiveModelSelection &&
2606
+ selectionIsCurrent() &&
2607
+ prepared.defaultModelPublished &&
2608
+ prepared.defaultModel
2609
+ ) {
2610
+ await restoreAsync("restore live model", async () => {
2611
+ if (prepared.session.restoreModelSelectionForRollback) {
2612
+ await prepared.session.restoreModelSelectionForRollback(
2613
+ prepared.previousModel,
2614
+ prepared.previousThinkingLevel,
2615
+ );
2616
+ } else if (prepared.previousModel) {
2617
+ await prepared.session.setModelTemporary(prepared.previousModel, prepared.previousThinkingLevel, {
2618
+ cause: "rollback",
2619
+ shouldMutate: selectionIsCurrent,
2620
+ });
2621
+ } else {
2622
+ throw new Error("Model-less profile activation rollback is unavailable");
2623
+ }
2624
+ });
2625
+ if (
2626
+ selectionIsCurrent() &&
2627
+ canRestorePublishedState(
2628
+ prepared.session.getSessionDefaultModelSelector?.(),
2629
+ publishedState?.sessionDefaultModel,
2630
+ )
2631
+ ) {
2632
+ restore("restore session resume default", () =>
2633
+ prepared.session.recordResumeDefaultModel?.(prepared.previousSessionDefaultModel),
2634
+ );
2635
+ }
2636
+ }
2637
+ if (
2638
+ (prepared.defaultChainPublished || prepared.defaultModelPublished) &&
2639
+ prepared.previousDefaultFallbackRuntimeState &&
2640
+ canRollbackPublishedProfile() &&
2641
+ canRestorePublishedState(
2642
+ prepared.session.getDefaultFallbackRuntimeState?.(),
2643
+ publishedState?.defaultFallbackRuntimeState,
2644
+ )
2645
+ ) {
2646
+ restore("restore fallback runtime", () =>
2647
+ prepared.session.restoreDefaultFallbackRuntimeState?.(prepared.previousDefaultFallbackRuntimeState!),
2648
+ );
2649
+ }
2650
+ }
2651
+ if (
2652
+ selectionIsCurrent() &&
2653
+ canRollbackPublishedProfile() &&
2654
+ canRestorePublishedState(prepared.session.getActiveModelProfile?.(), publishedState?.activeModelProfile)
2655
+ ) {
2656
+ restore("restore active profile", () =>
2657
+ prepared.session.setActiveModelProfile?.(
2658
+ prepared.previousActiveModelProfile,
2659
+ prepared.previousActivatedModelProfile === prepared.previousActiveModelProfile,
2660
+ ),
2661
+ );
2662
+ }
2663
+ if (
2664
+ prepared.previousProfileInstalledOverrideState &&
2665
+ canRollbackPublishedProfile() &&
2666
+ canRestorePublishedState(
2667
+ prepared.session.getProfileInstalledOverrideState?.(),
2668
+ publishedState?.profileInstalledOverrideState,
2669
+ )
2670
+ ) {
2671
+ restore("restore installed role tracking", () =>
2672
+ prepared.session.restoreProfileInstalledOverrideState?.(prepared.previousProfileInstalledOverrideState!),
2673
+ );
2674
+ }
2675
+ restore("restore canonical model variant", () =>
2676
+ restoreCanonicalVariantAfterPreparation(
2677
+ prepared.session.modelRegistry ?? prepared.modelRegistry,
2678
+ prepared.session,
2679
+ prepared.previousCanonicalVariant,
2680
+ prepared.previousUserCanonicalVariantSelection,
2681
+ ),
2682
+ );
2683
+ if (options.persistDefault) {
2684
+ await restoreAsync("restore persisted settings", () => prepared.settings.flushOrThrow());
2685
+ }
2686
+ if (failures.length > 0) {
2687
+ throw new AggregateError(
2688
+ failures.map(failure => failure.error),
2689
+ `Failed to fully roll back model profile activation: ${failures.map(failure => failure.stage).join(", ")}`,
2690
+ );
1994
2691
  }
1995
2692
  }
1996
2693
 
@@ -2007,8 +2704,10 @@ export interface MaterializeModelProfileForDeletionResult {
2007
2704
  previousDefaultProfile: string | undefined;
2008
2705
  previousPersistedDefaultProfile: string | undefined;
2009
2706
  previousActiveModelProfile: string | undefined;
2707
+ previousActivatedModelProfile: string | undefined;
2010
2708
  previousDefaultChainState: ConfiguredModelChainState | undefined;
2011
2709
  previousDefaultFallbackRuntimeState: DefaultFallbackRuntimeState | undefined;
2710
+ previousProfileInstalledOverrideState: AgentSessionProfileInstalledOverrideState | undefined;
2012
2711
  /**
2013
2712
  * Restores the session sticky canonical variant that was snapshotted before
2014
2713
  * materialization cleared it. Internal closure capturing the registry,
@@ -2137,7 +2836,17 @@ export async function materializeModelProfileForDeletion(
2137
2836
  ? prepared.settings.clearOverride("modelProfile.default")
2138
2837
  : prepared.settings.override("modelProfile.default", prepared.previousDefaultProfileOverride),
2139
2838
  );
2140
- restore(() => prepared.session.setActiveModelProfile?.(prepared.previousActiveModelProfile));
2839
+ restore(() =>
2840
+ prepared.session.setActiveModelProfile?.(
2841
+ prepared.previousActiveModelProfile,
2842
+ prepared.previousActivatedModelProfile === prepared.previousActiveModelProfile,
2843
+ ),
2844
+ );
2845
+ if (prepared.previousProfileInstalledOverrideState) {
2846
+ restore(() =>
2847
+ prepared.session.restoreProfileInstalledOverrideState?.(prepared.previousProfileInstalledOverrideState!),
2848
+ );
2849
+ }
2141
2850
  try {
2142
2851
  await prepared.settings.flushOrThrow();
2143
2852
  } catch (rollbackError) {
@@ -2165,8 +2874,10 @@ export async function materializeModelProfileForDeletion(
2165
2874
  previousDefaultProfile,
2166
2875
  previousPersistedDefaultProfile,
2167
2876
  previousActiveModelProfile: prepared.previousActiveModelProfile,
2877
+ previousActivatedModelProfile: prepared.previousActivatedModelProfile,
2168
2878
  previousDefaultChainState: prepared.previousDefaultChainState,
2169
2879
  previousDefaultFallbackRuntimeState: prepared.previousDefaultFallbackRuntimeState,
2880
+ previousProfileInstalledOverrideState: prepared.previousProfileInstalledOverrideState,
2170
2881
  restoreSessionCanonicalVariant: () =>
2171
2882
  restoreCanonicalVariant(prepared.modelRegistry, prepared.session.sessionId, prepared.previousCanonicalVariant),
2172
2883
  };
@@ -2176,7 +2887,10 @@ export async function restoreMaterializedModelProfileForDeletion(options: {
2176
2887
  settings: Pick<Settings, "clearOverride" | "flushOrThrow" | "override" | "set" | "unset">;
2177
2888
  session: Pick<
2178
2889
  ModelProfileActivationSession,
2179
- "setActiveModelProfile" | "setConfiguredModelChain" | "restoreDefaultFallbackRuntimeState"
2890
+ | "setActiveModelProfile"
2891
+ | "setConfiguredModelChain"
2892
+ | "restoreDefaultFallbackRuntimeState"
2893
+ | "restoreProfileInstalledOverrideState"
2180
2894
  >;
2181
2895
  snapshot: MaterializeModelProfileForDeletionResult;
2182
2896
  }): Promise<void> {
@@ -2230,7 +2944,19 @@ export async function restoreMaterializedModelProfileForDeletion(options: {
2230
2944
  ? options.settings.clearOverride("modelProfile.default")
2231
2945
  : options.settings.override("modelProfile.default", options.snapshot.previousDefaultProfileOverride),
2232
2946
  );
2233
- restore(() => options.session.setActiveModelProfile?.(options.snapshot.previousActiveModelProfile));
2947
+ restore(() =>
2948
+ options.session.setActiveModelProfile?.(
2949
+ options.snapshot.previousActiveModelProfile,
2950
+ options.snapshot.previousActivatedModelProfile === options.snapshot.previousActiveModelProfile,
2951
+ ),
2952
+ );
2953
+ if (options.snapshot.previousProfileInstalledOverrideState) {
2954
+ restore(() =>
2955
+ options.session.restoreProfileInstalledOverrideState?.(
2956
+ options.snapshot.previousProfileInstalledOverrideState!,
2957
+ ),
2958
+ );
2959
+ }
2234
2960
  try {
2235
2961
  await options.settings.flushOrThrow();
2236
2962
  } catch (error) {
@@ -2270,6 +2996,8 @@ export async function applyModelProfileRuntimeBindings(
2270
2996
  const previousModelRolesOverride = prepared.settings.getOverride("modelRoles");
2271
2997
  const previousAgentModelOverridesOverride = prepared.settings.getOverride("task.agentModelOverrides");
2272
2998
  const previousActiveModelProfile = prepared.session.getActiveModelProfile?.();
2999
+ const previousActivatedModelProfile = prepared.session.getActivatedModelProfile?.();
3000
+ const previousProfileInstalledOverrideState = prepared.session.getProfileInstalledOverrideState?.();
2273
3001
  let activatedModelRolesOverride: Readonly<Record<string, ModelSelectorValue>> | undefined;
2274
3002
  let activatedAgentModelOverridesOverride: Readonly<Record<string, ModelSelectorValue>> | undefined;
2275
3003
  try {
@@ -2284,7 +3012,7 @@ export async function applyModelProfileRuntimeBindings(
2284
3012
  ...prepared.agentModelOverrides,
2285
3013
  });
2286
3014
  activatedAgentModelOverridesOverride = prepared.settings.getOverride("task.agentModelOverrides");
2287
- prepared.session.setActiveModelProfile?.(prepared.profileName);
3015
+ prepared.session.setActiveModelProfile?.(prepared.profileName, true);
2288
3016
  try {
2289
3017
  await prepared.session.syncEagerDelegation?.();
2290
3018
  } catch (error) {
@@ -2310,7 +3038,13 @@ export async function applyModelProfileRuntimeBindings(
2310
3038
  prepared.settings.clearOverride("task.agentModelOverrides");
2311
3039
  else prepared.settings.override("task.agentModelOverrides", restoredAgentModelOverridesOverride);
2312
3040
  if (prepared.session.getActiveModelProfile?.() === prepared.profileName) {
2313
- prepared.session.setActiveModelProfile?.(previousActiveModelProfile);
3041
+ prepared.session.setActiveModelProfile?.(
3042
+ previousActiveModelProfile,
3043
+ previousActivatedModelProfile === previousActiveModelProfile,
3044
+ );
3045
+ }
3046
+ if (previousProfileInstalledOverrideState) {
3047
+ prepared.session.restoreProfileInstalledOverrideState?.(previousProfileInstalledOverrideState);
2314
3048
  }
2315
3049
  try {
2316
3050
  await prepared.session.syncEagerDelegation?.();
@@ -2326,6 +3060,13 @@ export async function applyModelProfileRuntimeBindings(
2326
3060
  Object.keys(prepared.modelRoles),
2327
3061
  Object.keys(prepared.agentModelOverrides),
2328
3062
  prepared.previousModel,
3063
+ { modelRoles: prepared.baseModelRoles, agentModelOverrides: prepared.baseAgentModelOverrides },
3064
+ { modelRoles: prepared.modelRoles, agentModelOverrides: prepared.agentModelOverrides },
3065
+ {
3066
+ modelRoles: [...prepared.manualModelRoleOverrides],
3067
+ agentModelOverrides: [...prepared.manualAgentModelOverrides],
3068
+ },
3069
+ prepared.defaultChain,
2329
3070
  );
2330
3071
  } finally {
2331
3072
  if (isCurrent && !isCurrent()) {