@gajae-code/coding-agent 0.18.12 → 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 (305) hide show
  1. package/CHANGELOG.md +467 -0
  2. package/dist/types/capability/rule.d.ts +7 -2
  3. package/dist/types/cli/append-system-prompt.d.ts +6 -0
  4. package/dist/types/cli/args.d.ts +7 -2
  5. package/dist/types/cli/customize-doctor-precedence.d.ts +16 -0
  6. package/dist/types/cli/customize-doctor.d.ts +4 -3
  7. package/dist/types/cli/public-command-errors.d.ts +1 -0
  8. package/dist/types/cli/root-flags.d.ts +0 -12
  9. package/dist/types/cli/update-cli.d.ts +7 -2
  10. package/dist/types/cli-main.d.ts +0 -6
  11. package/dist/types/commands/launch.d.ts +0 -6
  12. package/dist/types/commands/migrate.d.ts +1 -1
  13. package/dist/types/commands/sdk.d.ts +8 -0
  14. package/dist/types/commit/map-reduce/index.d.ts +4 -4
  15. package/dist/types/config/autorouting-tier-map.d.ts +23 -1
  16. package/dist/types/config/config-hot-reload.d.ts +5 -0
  17. package/dist/types/config/file-lock.d.ts +44 -2
  18. package/dist/types/config/model-profile-activation.d.ts +19 -5
  19. package/dist/types/config/model-registry.d.ts +16 -3
  20. package/dist/types/config/model-resolver.d.ts +5 -4
  21. package/dist/types/config/settings-schema.d.ts +11 -1
  22. package/dist/types/coordinator-mcp/server.d.ts +4 -0
  23. package/dist/types/customization/import.d.ts +2 -0
  24. package/dist/types/customization/types.d.ts +1 -1
  25. package/dist/types/dap/session.d.ts +11 -2
  26. package/dist/types/discovery/helpers.d.ts +7 -2
  27. package/dist/types/edit/index.d.ts +5 -0
  28. package/dist/types/edit/modes/patch.d.ts +9 -0
  29. package/dist/types/eval/py/runtime.d.ts +9 -0
  30. package/dist/types/export/ttsr.d.ts +19 -0
  31. package/dist/types/extensibility/extensions/types.d.ts +5 -1
  32. package/dist/types/extensibility/slash-commands.d.ts +1 -0
  33. package/dist/types/gjc-runtime/ralplan-current-final.d.ts +54 -0
  34. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +5 -0
  35. package/dist/types/gjc-runtime/tmux-sessions.d.ts +24 -0
  36. package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +6 -0
  37. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +1 -0
  38. package/dist/types/harness-control-plane/receipt-spool.d.ts +4 -0
  39. package/dist/types/hindsight/content.d.ts +5 -6
  40. package/dist/types/internal-urls/types.d.ts +2 -0
  41. package/dist/types/lsp/clients/swiftlint-client.d.ts +0 -4
  42. package/dist/types/main.d.ts +23 -18
  43. package/dist/types/migrate/action-planner.d.ts +10 -1
  44. package/dist/types/migrate/adapters/codex.d.ts +1 -1
  45. package/dist/types/migrate/adapters/index.d.ts +35 -3
  46. package/dist/types/migrate/hook-module.d.ts +23 -0
  47. package/dist/types/migrate/report.d.ts +2 -1
  48. package/dist/types/migrate/types.d.ts +69 -2
  49. package/dist/types/modes/components/agent-dashboard.d.ts +1 -1
  50. package/dist/types/modes/components/model-selector.d.ts +43 -0
  51. package/dist/types/modes/prompt-suggestion-controller.d.ts +1 -1
  52. package/dist/types/modes/utils/host-status.d.ts +15 -11
  53. package/dist/types/runtime-mcp/project-host-values.d.ts +11 -5
  54. package/dist/types/sdk/broker/broker-exit.d.ts +1 -1
  55. package/dist/types/sdk/broker/broker.d.ts +22 -1
  56. package/dist/types/sdk/broker/ensure.d.ts +5 -2
  57. package/dist/types/sdk/broker/hop.d.ts +9 -0
  58. package/dist/types/sdk/broker/lifecycle.d.ts +22 -0
  59. package/dist/types/sdk/broker/session-index.d.ts +2 -0
  60. package/dist/types/sdk/broker/transport.d.ts +2 -0
  61. package/dist/types/sdk/bus/index.d.ts +21 -0
  62. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +7 -1
  63. package/dist/types/sdk/bus/telegram-daemon.d.ts +2 -0
  64. package/dist/types/sdk/bus/threaded-render.d.ts +2 -1
  65. package/dist/types/sdk/host/control/operations.d.ts +5 -1
  66. package/dist/types/sdk/host/events.d.ts +4 -0
  67. package/dist/types/sdk/host/host.d.ts +2 -1
  68. package/dist/types/sdk/host/prompt-image-upload.d.ts +43 -0
  69. package/dist/types/sdk/host/session-runtime.d.ts +8 -8
  70. package/dist/types/sdk/host/surface-policy.d.ts +2 -1
  71. package/dist/types/sdk/lifecycle/service.d.ts +12 -0
  72. package/dist/types/sdk/prompt-status.d.ts +11 -4
  73. package/dist/types/session/agent-session.d.ts +50 -8
  74. package/dist/types/session/session-manager.d.ts +16 -0
  75. package/dist/types/skill-state/workflow-mutation-guard.d.ts +13 -1
  76. package/dist/types/slash-commands/helpers/mcp.d.ts +5 -0
  77. package/dist/types/ssh/ssh-executor.d.ts +8 -1
  78. package/dist/types/system-prompt.d.ts +3 -2
  79. package/dist/types/task/role-body.d.ts +11 -0
  80. package/dist/types/tools/awaiting-input.d.ts +28 -0
  81. package/dist/types/tools/bash-allowed-prefixes.d.ts +6 -0
  82. package/dist/types/tools/bash-skill-urls.d.ts +4 -0
  83. package/dist/types/tools/bash.d.ts +7 -0
  84. package/dist/types/tools/descriptors.d.ts +13 -2
  85. package/dist/types/tools/fetch.d.ts +6 -0
  86. package/dist/types/tools/irc.d.ts +5 -3
  87. package/dist/types/tools/tmux-self-injection-guard.d.ts +2 -0
  88. package/dist/types/tools/ultragoal-ask-guard.d.ts +1 -1
  89. package/dist/types/tools/write.d.ts +2 -0
  90. package/dist/types/utils/file-mentions.d.ts +5 -0
  91. package/dist/types/utils/image-limits.d.ts +7 -0
  92. package/dist/types/utils/image-loading.d.ts +0 -1
  93. package/dist/types/utils/open.d.ts +0 -1
  94. package/dist/types/utils/pasted-image-loading.d.ts +1 -5
  95. package/dist/types/utils/prompt-suggestion.d.ts +2 -2
  96. package/dist/types/utils/shell-snapshot.d.ts +2 -0
  97. package/dist/types/utils/title-generator.d.ts +13 -3
  98. package/dist/types/web/search/providers/codex.d.ts +6 -7
  99. package/package.json +9 -8
  100. package/scripts/acp-conformance-agent.ts +12 -2
  101. package/scripts/generate-sdk-operation-inventory.ts +16 -0
  102. package/src/capability/fs.ts +31 -3
  103. package/src/capability/rule.ts +11 -2
  104. package/src/cli/append-system-prompt.ts +12 -0
  105. package/src/cli/args.ts +11 -7
  106. package/src/cli/customize-doctor-precedence.ts +31 -0
  107. package/src/cli/customize-doctor.ts +49 -39
  108. package/src/cli/fast-help.ts +0 -2
  109. package/src/cli/migrate-cli.ts +14 -3
  110. package/src/cli/public-command-errors.ts +2 -0
  111. package/src/cli/root-flags.ts +0 -2
  112. package/src/cli/update-cli.ts +247 -6
  113. package/src/commands/harness.ts +25 -15
  114. package/src/commands/migrate.ts +3 -3
  115. package/src/commands/sdk.ts +81 -11
  116. package/src/commit/agentic/agent.ts +6 -6
  117. package/src/commit/analysis/conventional.ts +14 -5
  118. package/src/commit/map-reduce/index.ts +7 -7
  119. package/src/commit/pipeline.ts +12 -12
  120. package/src/config/autorouting-tier-map.ts +20 -1
  121. package/src/config/config-file.ts +106 -3
  122. package/src/config/config-hot-reload.ts +8 -0
  123. package/src/config/file-lock.ts +1431 -304
  124. package/src/config/keybindings.ts +154 -7
  125. package/src/config/model-profile-activation.ts +177 -39
  126. package/src/config/model-registry.ts +380 -55
  127. package/src/config/model-resolver.ts +49 -21
  128. package/src/config/settings-schema.ts +13 -1
  129. package/src/config/settings.ts +2 -0
  130. package/src/coordinator-mcp/server.ts +37 -2
  131. package/src/crash/sanitize.ts +3 -1
  132. package/src/customization/import.ts +276 -7
  133. package/src/customization/inventory.ts +24 -8
  134. package/src/customization/types.ts +1 -0
  135. package/src/dap/client.ts +3 -2
  136. package/src/dap/session.ts +161 -36
  137. package/src/debug/crash-diagnostics.ts +87 -5
  138. package/src/debug/report-bundle.ts +71 -2
  139. package/src/discovery/builtin.ts +4 -3
  140. package/src/discovery/helpers.ts +24 -14
  141. package/src/edit/index.ts +191 -5
  142. package/src/edit/modes/patch.ts +27 -0
  143. package/src/eval/py/runtime.ts +44 -4
  144. package/src/exec/non-interactive-env.ts +0 -1
  145. package/src/export/html/index.ts +60 -20
  146. package/src/export/ttsr.ts +176 -1
  147. package/src/extensibility/extensions/loader.ts +2 -0
  148. package/src/extensibility/extensions/ouroboros-ooo-bridge.ts +63 -1
  149. package/src/extensibility/extensions/types.ts +5 -1
  150. package/src/extensibility/hooks/loader.ts +10 -2
  151. package/src/extensibility/plugins/marketplace/manager.ts +36 -10
  152. package/src/extensibility/slash-commands.ts +5 -0
  153. package/src/gjc-runtime/gc-runtime.ts +4 -1
  154. package/src/gjc-runtime/launch-tmux.ts +23 -13
  155. package/src/gjc-runtime/ralplan-current-final.ts +294 -0
  156. package/src/gjc-runtime/ralplan-runtime.ts +95 -1
  157. package/src/gjc-runtime/state-writer.ts +109 -5
  158. package/src/gjc-runtime/tmux-sessions.ts +93 -13
  159. package/src/gjc-runtime/ultragoal-change-set.ts +90 -11
  160. package/src/gjc-runtime/ultragoal-guard.ts +9 -1
  161. package/src/gjc-runtime/ultragoal-runtime.ts +15 -5
  162. package/src/harness-control-plane/finalize.ts +11 -0
  163. package/src/harness-control-plane/receipt-spool.ts +105 -28
  164. package/src/harness-control-plane/storage.ts +124 -6
  165. package/src/hindsight/content.ts +116 -16
  166. package/src/hindsight/mental-models.ts +22 -10
  167. package/src/hindsight/state.ts +7 -4
  168. package/src/hooks/native-skill-hook.ts +3 -1
  169. package/src/internal-urls/artifact-protocol.ts +3 -1
  170. package/src/internal-urls/docs-index.generated.ts +14 -14
  171. package/src/internal-urls/local-protocol.ts +28 -3
  172. package/src/internal-urls/types.ts +2 -0
  173. package/src/lsp/client.ts +2 -1
  174. package/src/lsp/clients/biome-client.ts +2 -0
  175. package/src/lsp/clients/swiftlint-client.ts +2 -0
  176. package/src/main.ts +330 -92
  177. package/src/memories/index.ts +25 -2
  178. package/src/migrate/action-planner.ts +517 -2
  179. package/src/migrate/adapters/claude-code.ts +141 -3
  180. package/src/migrate/adapters/codex.ts +57 -5
  181. package/src/migrate/adapters/index.ts +240 -5
  182. package/src/migrate/adapters/opencode.ts +4 -0
  183. package/src/migrate/executor.ts +141 -3
  184. package/src/migrate/hook-module.ts +193 -0
  185. package/src/migrate/report.ts +45 -1
  186. package/src/migrate/types.ts +73 -3
  187. package/src/modes/acp/acp-agent.ts +10 -1
  188. package/src/modes/components/agent-dashboard.ts +20 -4
  189. package/src/modes/components/customization/customization-dashboard.ts +7 -0
  190. package/src/modes/components/customization/import-wizard.ts +29 -3
  191. package/src/modes/components/model-selector.ts +157 -103
  192. package/src/modes/components/welcome.ts +3 -1
  193. package/src/modes/controllers/command-controller.ts +271 -16
  194. package/src/modes/controllers/event-controller.ts +1 -1
  195. package/src/modes/controllers/extension-ui-controller.ts +31 -1
  196. package/src/modes/controllers/goal-mode-controller.ts +79 -10
  197. package/src/modes/controllers/input-controller.ts +2 -0
  198. package/src/modes/controllers/plan-mode-controller.ts +49 -4
  199. package/src/modes/controllers/runtime-mcp-command-controller.ts +7 -6
  200. package/src/modes/controllers/selector-controller.ts +14 -1
  201. package/src/modes/interactive-mode.ts +55 -4
  202. package/src/modes/prompt-suggestion-controller.ts +1 -1
  203. package/src/modes/runtime-init.ts +5 -1
  204. package/src/modes/utils/host-status.ts +21 -10
  205. package/src/prompts/agent-fragments/browser-aside.md +2 -2
  206. package/src/prompts/system/irc-incoming.md +3 -1
  207. package/src/prompts/tools/irc.md +1 -1
  208. package/src/runtime-mcp/manager.ts +8 -3
  209. package/src/runtime-mcp/project-host-values.ts +12 -8
  210. package/src/runtime-mcp/tool-bridge.ts +68 -4
  211. package/src/runtime-mcp/transports/stdio.ts +2 -1
  212. package/src/sdk/broker/broker-exit.ts +2 -0
  213. package/src/sdk/broker/broker.ts +143 -4
  214. package/src/sdk/broker/ensure.ts +74 -6
  215. package/src/sdk/broker/hop.ts +13 -1
  216. package/src/sdk/broker/lifecycle-ledger.ts +3 -0
  217. package/src/sdk/broker/lifecycle.ts +102 -6
  218. package/src/sdk/broker/session-index.ts +29 -4
  219. package/src/sdk/broker/transport.ts +17 -1
  220. package/src/sdk/bus/chat-daemon-control.ts +2 -2
  221. package/src/sdk/bus/index.ts +408 -14
  222. package/src/sdk/bus/session-control.ts +2 -0
  223. package/src/sdk/bus/slack-daemon.ts +105 -9
  224. package/src/sdk/bus/telegram-daemon-contract.ts +7 -1
  225. package/src/sdk/bus/telegram-daemon.ts +10 -2
  226. package/src/sdk/bus/threaded-render.ts +20 -1
  227. package/src/sdk/cli/session-cli.ts +2 -0
  228. package/src/sdk/host/control/dispatch.ts +13 -1
  229. package/src/sdk/host/control/operations.ts +10 -1
  230. package/src/sdk/host/events.ts +67 -4
  231. package/src/sdk/host/host.ts +8 -2
  232. package/src/sdk/host/prompt-image-upload.ts +438 -0
  233. package/src/sdk/host/sdk-surface-parity.test.ts +15 -0
  234. package/src/sdk/host/session-runtime.test.ts +1537 -24
  235. package/src/sdk/host/session-runtime.ts +332 -36
  236. package/src/sdk/host/surface-policy.ts +4 -0
  237. package/src/sdk/host/websocket-transport.lifecycle.test.ts +90 -2
  238. package/src/sdk/host/websocket-transport.ts +19 -7
  239. package/src/sdk/lifecycle/service.ts +14 -1
  240. package/src/sdk/prompt-status.ts +6 -4
  241. package/src/sdk/protocol/adapter-validation.ts +28 -1
  242. package/src/sdk/protocol/operation-inventory.generated.json +160 -0
  243. package/src/sdk/protocol/operation-registry.ts +33 -3
  244. package/src/sdk/session.ts +123 -36
  245. package/src/session/agent-session.ts +1614 -329
  246. package/src/session/history-storage.ts +182 -12
  247. package/src/session/internal/managed-session-scope.ts +47 -18
  248. package/src/session/internal/managed-session-storage.ts +71 -1
  249. package/src/session/session-manager.ts +90 -5
  250. package/src/session-import/redact.ts +53 -2
  251. package/src/skill-state/workflow-mutation-guard.ts +1498 -18
  252. package/src/slash-commands/builtin-registry.ts +6 -3
  253. package/src/slash-commands/helpers/mcp.ts +11 -3
  254. package/src/ssh/ssh-executor.ts +9 -2
  255. package/src/stt/recorder.ts +26 -9
  256. package/src/system-prompt.ts +61 -16
  257. package/src/task/executor.ts +12 -8
  258. package/src/task/role-body.ts +84 -0
  259. package/src/task/token-log-mode.test.ts +162 -0
  260. package/src/task/token-log.ts +91 -2
  261. package/src/tools/archive-reader.ts +650 -37
  262. package/src/tools/ask.ts +1 -0
  263. package/src/tools/ast-grep.ts +7 -2
  264. package/src/tools/auto-generated-guard.ts +12 -3
  265. package/src/tools/awaiting-input.ts +114 -0
  266. package/src/tools/bash-allowed-prefixes.ts +61 -1
  267. package/src/tools/bash-skill-urls.ts +58 -5
  268. package/src/tools/bash.ts +99 -26
  269. package/src/tools/browser/tab-worker.ts +39 -13
  270. package/src/tools/browser.ts +17 -7
  271. package/src/tools/cron.ts +10 -1
  272. package/src/tools/debug.ts +9 -2
  273. package/src/tools/descriptors.test.ts +4 -4
  274. package/src/tools/descriptors.ts +16 -4
  275. package/src/tools/eval.ts +15 -0
  276. package/src/tools/fetch.ts +90 -2
  277. package/src/tools/gh.ts +27 -12
  278. package/src/tools/image-gen.ts +20 -6
  279. package/src/tools/irc.ts +5 -3
  280. package/src/tools/monitor.ts +9 -1
  281. package/src/tools/output-meta.ts +51 -3
  282. package/src/tools/read.ts +9 -2
  283. package/src/tools/recipe/runner.ts +23 -12
  284. package/src/tools/render-utils.ts +2 -2
  285. package/src/tools/search.ts +3 -2
  286. package/src/tools/ssh.ts +274 -8
  287. package/src/tools/subagent.ts +10 -4
  288. package/src/tools/tmux-self-injection-guard.ts +3301 -122
  289. package/src/tools/tool-catalog.generated.ts +1 -1
  290. package/src/tools/ultragoal-ask-guard.ts +13 -1
  291. package/src/tools/write.ts +7 -0
  292. package/src/tui/tree-list.ts +15 -1
  293. package/src/utils/commit-message-generator.ts +3 -2
  294. package/src/utils/external-editor.ts +21 -7
  295. package/src/utils/file-mentions.ts +46 -6
  296. package/src/utils/git.ts +38 -7
  297. package/src/utils/image-limits.ts +8 -0
  298. package/src/utils/image-loading.ts +1 -1
  299. package/src/utils/open.ts +73 -4
  300. package/src/utils/pasted-image-loading.ts +9 -8
  301. package/src/utils/prompt-suggestion.ts +5 -4
  302. package/src/utils/shell-snapshot.ts +9 -1
  303. package/src/utils/title-generator.ts +65 -20
  304. package/src/web/search/index.ts +2 -1
  305. package/src/web/search/providers/codex.ts +29 -32
@@ -1,4 +1,17 @@
1
- import { copyFileSync, existsSync, readFileSync, renameSync, writeFileSync } from "node:fs";
1
+ import {
2
+ closeSync,
3
+ constants,
4
+ existsSync,
5
+ fchmodSync,
6
+ fstatSync,
7
+ ftruncateSync,
8
+ lstatSync,
9
+ openSync,
10
+ readFileSync,
11
+ renameSync,
12
+ unlinkSync,
13
+ writeSync,
14
+ } from "node:fs";
2
15
  import * as path from "node:path";
3
16
  import {
4
17
  type Keybinding,
@@ -469,22 +482,156 @@ function loadRawConfig(filePath: string): unknown {
469
482
  }
470
483
  }
471
484
 
485
+ const KEYBINDINGS_MARKER_MODE = 0o600;
486
+
487
+ type KeybindingsLeaf = "absent" | "file" | "symlink" | "other";
488
+
489
+ function keybindingsLeaf(filePath: string): KeybindingsLeaf {
490
+ try {
491
+ const stat = lstatSync(filePath);
492
+ if (stat.isSymbolicLink()) return "symlink";
493
+ if (stat.isFile()) return "file";
494
+ return "other";
495
+ } catch (error) {
496
+ if (isEnoent(error)) return "absent";
497
+ throw error;
498
+ }
499
+ }
500
+
501
+ function writeAllSync(fd: number, content: string | Buffer): void {
502
+ const buffer = typeof content === "string" ? Buffer.from(content) : content;
503
+ let offset = 0;
504
+ while (offset < buffer.length) {
505
+ const wrote = writeSync(fd, buffer, offset, buffer.length - offset);
506
+ if (wrote <= 0) throw new Error("keybindings migration write was incomplete");
507
+ offset += wrote;
508
+ }
509
+ }
510
+
511
+ /** Create a new file. O_NOFOLLOW refuses a final symlink. A set mode is fchmod'd. */
512
+ function writeExclusiveNoFollow(filePath: string, content: string | Buffer, mode: number | undefined): void {
513
+ const noFollow = process.platform === "win32" ? 0 : (constants.O_NOFOLLOW ?? 0);
514
+ const fd = openSync(filePath, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | noFollow, mode ?? 0o666);
515
+ try {
516
+ writeAllSync(fd, content);
517
+ if (mode !== undefined && process.platform !== "win32") fchmodSync(fd, mode);
518
+ } finally {
519
+ closeSync(fd);
520
+ }
521
+ }
522
+
523
+ /** Copy bytes to the backup. A final symlink at either path is not opened for write. */
524
+ function copyKeybindingsBackup(
525
+ source: string,
526
+ dest: string,
527
+ sourceKind: KeybindingsLeaf,
528
+ destKind: KeybindingsLeaf,
529
+ ): void {
530
+ const noFollow = process.platform === "win32" ? 0 : (constants.O_NOFOLLOW ?? 0);
531
+ // A symlinked config is still backed up from its target. The backup path itself stays no-follow.
532
+ const sourceFlags = constants.O_RDONLY | (sourceKind === "file" ? noFollow : 0);
533
+ const srcFd = openSync(source, sourceFlags);
534
+ try {
535
+ const stat = fstatSync(srcFd);
536
+ if (!stat.isFile()) throw new Error("keybindings migration target is not a regular file");
537
+ const bytes = readFileSync(srcFd);
538
+ const mode = stat.mode & 0o777;
539
+ if (destKind === "absent") {
540
+ writeExclusiveNoFollow(dest, bytes, mode);
541
+ return;
542
+ }
543
+ const fd = openSync(dest, constants.O_WRONLY | noFollow);
544
+ try {
545
+ const destStat = fstatSync(fd);
546
+ if (!destStat.isFile()) throw new Error("keybindings migration target is not a regular file");
547
+ if (destStat.dev === stat.dev && destStat.ino === stat.ino) return;
548
+ ftruncateSync(fd, 0);
549
+ writeAllSync(fd, bytes);
550
+ if (process.platform !== "win32") fchmodSync(fd, mode);
551
+ } finally {
552
+ closeSync(fd);
553
+ }
554
+ } finally {
555
+ closeSync(srcFd);
556
+ }
557
+ }
558
+
559
+ function migrationMarkerState(markerPath: string): { present: boolean; kind: KeybindingsLeaf } {
560
+ const kind = keybindingsLeaf(markerPath);
561
+ // A symlink is not a completed marker. existsSync only preserves the historical
562
+ // skip when that link already names a real target. The write path never opens it.
563
+ const present = kind === "file" || kind === "other" || (kind === "symlink" && existsSync(markerPath));
564
+ return { present, kind };
565
+ }
566
+
567
+ function writeMigratedKeybindings(
568
+ filePath: string,
569
+ markerPath: string,
570
+ markerKind: KeybindingsLeaf,
571
+ body: string,
572
+ ): void {
573
+ const tempPath = `${filePath}.${process.pid}.tmp`;
574
+ const bakPath = `${filePath}.bak`;
575
+ const bakKind = keybindingsLeaf(bakPath);
576
+ const tempKind = keybindingsLeaf(tempPath);
577
+ const sourceKind = keybindingsLeaf(filePath);
578
+ if (
579
+ bakKind === "symlink" ||
580
+ bakKind === "other" ||
581
+ tempKind === "symlink" ||
582
+ tempKind === "other" ||
583
+ (sourceKind !== "file" && sourceKind !== "symlink")
584
+ ) {
585
+ throw new Error("keybindings migration target is not a regular file");
586
+ }
587
+ if (tempKind === "file") unlinkSync(tempPath);
588
+ if (keybindingsLeaf(filePath) !== sourceKind) {
589
+ throw new Error("keybindings migration target is not a regular file");
590
+ }
591
+ const mode = sourceKind === "file" ? lstatSync(filePath).mode & 0o777 : undefined;
592
+ try {
593
+ copyKeybindingsBackup(filePath, bakPath, sourceKind, bakKind);
594
+ writeExclusiveNoFollow(tempPath, body, mode);
595
+ const current = lstatSync(filePath);
596
+ if (sourceKind === "file" && (current.isSymbolicLink() || !current.isFile())) {
597
+ throw new Error("keybindings migration target is not a regular file");
598
+ }
599
+ renameSync(tempPath, filePath);
600
+ if (markerKind === "absent") writeExclusiveNoFollow(markerPath, "v1\n", KEYBINDINGS_MARKER_MODE);
601
+ } catch (error) {
602
+ if (keybindingsLeaf(tempPath) === "file") {
603
+ try {
604
+ unlinkSync(tempPath);
605
+ } catch {
606
+ // Best-effort removal of the unreplaced temp file.
607
+ }
608
+ }
609
+ throw error;
610
+ }
611
+ }
612
+
472
613
  /** Migrate legacy keybindings once, preserving the original file as .bak. */
473
614
  function loadKeybindingsConfig(filePath: string, writeBack: boolean): KeybindingsConfig {
474
615
  const rawConfig = loadRawConfig(filePath);
475
616
  if (rawConfig === null) return {};
476
617
  const markerPath = `${filePath}.migration-v1`;
477
- const { config: migratedConfig, migrated } = existsSync(markerPath)
618
+ let markerKind: KeybindingsLeaf = "absent";
619
+ let markerPresent = false;
620
+ try {
621
+ const marker = migrationMarkerState(markerPath);
622
+ markerKind = marker.kind;
623
+ markerPresent = marker.present;
624
+ } catch (error) {
625
+ logger.warn("Failed to read keybindings migration marker", { path: markerPath, error: String(error) });
626
+ return toKeybindingsConfig(rawConfig);
627
+ }
628
+ const { config: migratedConfig, migrated } = markerPresent
478
629
  ? { config: toKeybindingsConfig(rawConfig), migrated: false }
479
630
  : migrateKeybindingNames(rawConfig);
480
631
  if (writeBack && migrated) {
481
632
  const ordered = orderKeybindingsConfig(migratedConfig);
482
- const tempPath = `${filePath}.${process.pid}.tmp`;
483
633
  try {
484
- copyFileSync(filePath, `${filePath}.bak`);
485
- writeFileSync(tempPath, `${JSON.stringify(ordered, null, 2)}\n`, "utf-8");
486
- renameSync(tempPath, filePath);
487
- writeFileSync(markerPath, "v1\n", "utf-8");
634
+ writeMigratedKeybindings(filePath, markerPath, markerKind, `${JSON.stringify(ordered, null, 2)}\n`);
488
635
  logger.debug("Migrated keybindings config", { path: filePath });
489
636
  } catch (error) {
490
637
  logger.warn("Failed to write migrated keybindings config", { path: filePath, error: String(error) });
@@ -53,8 +53,9 @@ type ModelProfileActivationSession = Pick<
53
53
  > & {
54
54
  credentialSessionId?: string;
55
55
  setModelTemporary?: AgentSession["setModelTemporary"];
56
- setActiveModelProfile?: (name: string | undefined) => void;
56
+ setActiveModelProfile?: (name: string | undefined, activatedByThisProcess?: boolean) => void;
57
57
  getActiveModelProfile?: () => string | undefined;
58
+ getActivatedModelProfile?: () => string | undefined;
58
59
  /** Record which runtime override keys this activation installed (session-scoped). */
59
60
  noteProfileInstalledOverrides?: (
60
61
  modelRoles: readonly string[],
@@ -69,6 +70,7 @@ type ModelProfileActivationSession = Pick<
69
70
  agentModelOverrides: Readonly<Record<string, ModelSelectorValue>>;
70
71
  },
71
72
  manual?: { modelRoles: readonly string[]; agentModelOverrides: readonly string[] },
73
+ configuredDefaultChain?: readonly string[],
72
74
  ) => void;
73
75
  /** Drop the recorded profile-installed override keys (e.g. after materialization). */
74
76
  clearProfileInstalledOverrides?: () => void;
@@ -95,6 +97,7 @@ type ModelProfileActivationSession = Pick<
95
97
  | "getSessionCanonicalVariant"
96
98
  | "restoreSessionCanonicalVariant"
97
99
  | "clearCanonicalVariant"
100
+ | "getModelProfile"
98
101
  | "getAvailable"
99
102
  | "lookupAliasExists"
100
103
  | "resolveModelByLookupAlias"
@@ -120,6 +123,7 @@ type PublishedModelProfileActivationState = {
120
123
  defaultChainState: ConfiguredModelChainState | undefined;
121
124
  defaultFallbackRuntimeState: DefaultFallbackRuntimeState | undefined;
122
125
  activeModelProfile: string | undefined;
126
+ activatedModelProfile: string | undefined;
123
127
  profileInstalledOverrideState: AgentSessionProfileInstalledOverrideState | undefined;
124
128
  sessionDefaultModel: string | undefined;
125
129
  persistedModelRoles: Readonly<Record<string, ModelSelectorValue>> | undefined;
@@ -227,6 +231,7 @@ export interface PreparedModelProfileActivation {
227
231
  manualModelRoleOverrides: ReadonlySet<string>;
228
232
  manualAgentModelOverrides: ReadonlySet<string>;
229
233
  previousActiveModelProfile: string | undefined;
234
+ previousActivatedModelProfile: string | undefined;
230
235
  /**
231
236
  * The session resume default ("provider/id") captured BEFORE activation —
232
237
  * the model resume would restore prior to this profile. Snapshotted
@@ -260,6 +265,8 @@ export interface MaterializeModelProfileAssignmentOptions {
260
265
  | "setConfiguredModelChain"
261
266
  | "setActiveModelProfile"
262
267
  | "getActiveModelProfile"
268
+ | "getActivatedModelProfile"
269
+ | "getProfileInstalledOverrideState"
263
270
  | "getDefaultFallbackRuntimeState"
264
271
  | "restoreDefaultFallbackRuntimeState"
265
272
  | "modelRegistry"
@@ -282,6 +289,8 @@ export interface MaterializeModelProfileAssignmentsOptions {
282
289
  | "setConfiguredModelChain"
283
290
  | "setActiveModelProfile"
284
291
  | "getActiveModelProfile"
292
+ | "getActivatedModelProfile"
293
+ | "getProfileInstalledOverrideState"
285
294
  | "getDefaultFallbackRuntimeState"
286
295
  | "restoreDefaultFallbackRuntimeState"
287
296
  | "modelRegistry"
@@ -310,9 +319,80 @@ function getMaterializedAssignments(
310
319
  return result;
311
320
  }
312
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
+
313
389
  function materializeConfiguredDefaultChain(
314
- session: Pick<ModelProfileActivationSession, "model" | "thinkingLevel" | "getConfiguredModelChain">,
390
+ session: Pick<
391
+ ModelProfileActivationSession,
392
+ "model" | "thinkingLevel" | "getConfiguredModelChain" | "getConfiguredModelChainState"
393
+ >,
315
394
  ): ModelSelectorValue | undefined {
395
+ if (session.getConfiguredModelChainState?.("default")?.origin === "startup-override") return undefined;
316
396
  if (!session.model) return undefined;
317
397
  return formatModelSelectorValue(`${session.model.provider}/${session.model.id}`, session.thinkingLevel);
318
398
  }
@@ -328,7 +408,7 @@ function concretizeMaterializedAssignmentValues(
328
408
  // Materialized assignments are persisted into `modelRoles` and
329
409
  // `task.agentModelOverrides` and later consumed by profile execution, so they
330
410
  // resolve against the same catalog profile activation uses.
331
- const availableModels = modelRegistry.getAvailable();
411
+ const availableModels = modelRegistry.getAvailable(credentialSessionId);
332
412
  const authenticatedModels = availableModels.filter(model => {
333
413
  const isCredentiallessProvider = modelRegistry.isCredentiallessProvider?.bind(modelRegistry);
334
414
  const hasUsableAuth = modelRegistry.authStorage?.hasUsableAuth?.bind(modelRegistry.authStorage);
@@ -368,6 +448,7 @@ function commitMaterializedProfileAssignments(
368
448
  options: MaterializeModelProfileAssignmentOptions | MaterializeModelProfileAssignmentsOptions,
369
449
  modelRoles: Record<string, ModelSelectorValue>,
370
450
  agentModelOverrides: Record<string, ModelSelectorValue>,
451
+ preserveStartupOverrideChain = false,
371
452
  ): boolean {
372
453
  const previousPersistedModelRoles = options.settings.getGlobal("modelRoles");
373
454
  const previousPersistedAgentModelOverrides = options.settings.getGlobal("task.agentModelOverrides");
@@ -376,6 +457,7 @@ function commitMaterializedProfileAssignments(
376
457
  const previousAgentModelOverridesOverride = options.settings.getOverride("task.agentModelOverrides");
377
458
  const previousDefaultProfileOverride = options.settings.getOverride("modelProfile.default");
378
459
  const previousActiveProfile = options.session.getActiveModelProfile?.();
460
+ const previousActivatedProfile = options.session.getActivatedModelProfile?.();
379
461
  const previousChain = options.session.getConfiguredModelChainState?.("default");
380
462
  const previousFallbackRuntimeState = options.session.getDefaultFallbackRuntimeState?.();
381
463
  const previousCanonicalVariant = options.session.modelRegistry?.getSessionCanonicalVariant?.(
@@ -390,13 +472,17 @@ function commitMaterializedProfileAssignments(
390
472
  options.settings.clearOverride("modelProfile.default");
391
473
  options.settings.override("modelRoles", nextModelRoles);
392
474
  options.settings.override("task.agentModelOverrides", nextAgentModelOverrides);
393
- options.session.setConfiguredModelChain(
394
- "default",
395
- normalizeModelSelectorValue(nextModelRoles.default),
396
- "modelRoles",
397
- undefined,
398
- true,
399
- );
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
+ }
400
486
  options.session.setActiveModelProfile?.(undefined);
401
487
  return true;
402
488
  } catch (error) {
@@ -461,7 +547,12 @@ function commitMaterializedProfileAssignments(
461
547
  ? options.settings.clearOverride("modelProfile.default")
462
548
  : options.settings.override("modelProfile.default", previousDefaultProfileOverride),
463
549
  );
464
- restore(() => options.session.setActiveModelProfile?.(previousActiveProfile));
550
+ restore(() =>
551
+ options.session.setActiveModelProfile?.(
552
+ previousActiveProfile,
553
+ previousActivatedProfile === previousActiveProfile,
554
+ ),
555
+ );
465
556
  if (rollbackErrors.length > 0) {
466
557
  throw new AggregateError(
467
558
  [error, ...rollbackErrors],
@@ -473,10 +564,9 @@ function commitMaterializedProfileAssignments(
473
564
  }
474
565
 
475
566
  export function materializeActiveModelProfileAssignment(options: MaterializeModelProfileAssignmentOptions): boolean {
476
- const activeProfile = options.session.getActiveModelProfile?.() ?? options.settings.get("modelProfile.default");
477
- if (!activeProfile) return false;
567
+ if (!resolveMaterializationProfile(options, options.role === "default")) return false;
478
568
 
479
- const nextModelRoles = { ...options.settings.get("modelRoles") };
569
+ const nextModelRoles = modelRolesForMaterialization(options);
480
570
  const nextAgentModelOverrides = { ...options.settings.get("task.agentModelOverrides") };
481
571
  const target = GJC_MODEL_ASSIGNMENT_TARGETS[options.role];
482
572
 
@@ -493,21 +583,24 @@ export function materializeActiveModelProfileAssignment(options: MaterializeMode
493
583
  nextAgentModelOverrides[options.role] = options.selector;
494
584
  }
495
585
 
496
- const committed = commitMaterializedProfileAssignments(options, nextModelRoles, nextAgentModelOverrides);
586
+ const committed = commitMaterializedProfileAssignments(
587
+ options,
588
+ nextModelRoles,
589
+ nextAgentModelOverrides,
590
+ options.role !== "default",
591
+ );
497
592
  if (committed) options.session.clearProfileInstalledOverrides?.();
498
593
  return committed;
499
594
  }
500
595
 
501
596
  export function materializeActiveModelProfileAssignments(options: MaterializeModelProfileAssignmentsOptions): boolean {
502
- const activeProfile = options.session.getActiveModelProfile?.() ?? options.settings.get("modelProfile.default");
503
- if (!activeProfile) return false;
504
-
505
597
  const materializedAssignments = getMaterializedAssignments(options.assignments);
598
+ const includesDefault = materializedAssignments.some(([role]) => role === "default");
599
+ if (!resolveMaterializationProfile(options, includesDefault)) return false;
506
600
  if (materializedAssignments.length === 0) return true;
507
601
 
508
- const nextModelRoles = { ...options.settings.get("modelRoles") };
602
+ const nextModelRoles = modelRolesForMaterialization(options);
509
603
  const nextAgentModelOverrides = { ...options.settings.get("task.agentModelOverrides") };
510
- const includesDefault = materializedAssignments.some(([role]) => role === "default");
511
604
 
512
605
  if (!includesDefault && !nextModelRoles.default) {
513
606
  const defaultChain = materializeConfiguredDefaultChain(options.session);
@@ -523,7 +616,12 @@ export function materializeActiveModelProfileAssignments(options: MaterializeMod
523
616
  }
524
617
  }
525
618
 
526
- const committed = commitMaterializedProfileAssignments(options, nextModelRoles, nextAgentModelOverrides);
619
+ const committed = commitMaterializedProfileAssignments(
620
+ options,
621
+ nextModelRoles,
622
+ nextAgentModelOverrides,
623
+ !includesDefault,
624
+ );
527
625
  if (committed) options.session.clearProfileInstalledOverrides?.();
528
626
  return committed;
529
627
  }
@@ -801,7 +899,7 @@ export async function resolveMissingSessionModelRecovery(options: {
801
899
  options.skips.length === options.defaultEntries.length &&
802
900
  options.skips.every(skip => skip.reason === "unknown_model");
803
901
  if (!allSelectorsUnknown) return undefined;
804
- const fullCatalog = options.modelRegistry.getAll();
902
+ const fullCatalog = options.modelRegistry.getAll(options.credentialSessionId);
805
903
  const catalogIndex = createRegistrySelectorIndex(fullCatalog);
806
904
  const savedSelectorsMissingFromCatalog = resolveConfiguredModelPatterns(
807
905
  options.defaultEntries,
@@ -962,7 +1060,9 @@ export async function resolveModelProfileDefaultChain(options: {
962
1060
  allRoutable && proxyProvider !== undefined ? [proxyProvider] : [...group],
963
1061
  );
964
1062
  }
965
- const availableModels = options.modelRegistry.getAvailable?.() ?? options.modelRegistry.getAll();
1063
+ const availableModels =
1064
+ options.modelRegistry.getAvailable?.(options.credentialSessionId) ??
1065
+ options.modelRegistry.getAll(options.credentialSessionId);
966
1066
  let bindings = resolveProfileBindings(profile);
967
1067
  if (alternativeGroups.length > 0)
968
1068
  bindings = rewriteBindingsProviders(bindings, authenticatedProviders, alternativeGroups);
@@ -979,7 +1079,7 @@ export async function resolveModelProfileDefaultChain(options: {
979
1079
  await preflightModelProfileRoleBindings({
980
1080
  profile,
981
1081
  bindings,
982
- roleCatalogModels: options.modelRegistry.getAll(),
1082
+ roleCatalogModels: options.modelRegistry.getAll(options.credentialSessionId),
983
1083
  settings: options.settings as Settings,
984
1084
  modelRegistry: options.modelRegistry as ModelRegistry,
985
1085
  sessionId: "",
@@ -1165,11 +1265,15 @@ function formatMaterializedSelector(selector: string, model: Model<Api>): string
1165
1265
  if (!explicitThinkingLevel || parseModelString(clampedSelector)?.thinkingLevel) return clampedSelector;
1166
1266
  return formatModelSelectorValue(clampedSelector, explicitThinkingLevel);
1167
1267
  }
1168
- function getBareSelectorCredentialProviders(selector: string, modelRegistry: ModelRegistry): string[] {
1268
+ function getBareSelectorCredentialProviders(
1269
+ selector: string,
1270
+ modelRegistry: ModelRegistry,
1271
+ credentialSessionId?: string,
1272
+ ): string[] {
1169
1273
  const suffix = splitSelectorThinkingSuffix(selector);
1170
1274
  const alias = (suffix.thinkingLevel ? suffix.selector : selector).trim().toLowerCase();
1171
1275
  const providers = modelRegistry
1172
- .getAll()
1276
+ .getAll(credentialSessionId)
1173
1277
  .filter(model => {
1174
1278
  const modelId = model.id.trim().toLowerCase();
1175
1279
  return modelId === alias || modelId.slice(modelId.lastIndexOf("/") + 1) === alias;
@@ -1240,7 +1344,11 @@ async function resolveAndClampSelectorValue(
1240
1344
  }
1241
1345
  if (!resolved.model) {
1242
1346
  if (bareAlias) {
1243
- const providers = getBareSelectorCredentialProviders(selector, options.modelRegistry);
1347
+ const providers = getBareSelectorCredentialProviders(
1348
+ selector,
1349
+ options.modelRegistry,
1350
+ options.credentialSessionId,
1351
+ );
1244
1352
  const selectorSuffix = splitSelectorThinkingSuffix(selector);
1245
1353
  const bareSelector = selectorSuffix.thinkingLevel ? selectorSuffix.selector : selector;
1246
1354
  const aliasKnown =
@@ -1332,8 +1440,9 @@ async function concretizeProfileSelectorValue(
1332
1440
  selectorValue: ModelSelectorValue,
1333
1441
  prepared: PreparedModelProfileActivation,
1334
1442
  ): Promise<ModelSelectorValue> {
1335
- const candidates = prepared.modelRegistry.getAvailable?.() ?? prepared.modelRegistry.getAll();
1336
1443
  const credentialSessionId = prepared.session.credentialSessionId ?? prepared.session.sessionId;
1444
+ const candidates =
1445
+ prepared.modelRegistry.getAvailable?.(credentialSessionId) ?? prepared.modelRegistry.getAll(credentialSessionId);
1337
1446
  const concrete = await Promise.all(
1338
1447
  normalizeModelSelectorValue(selectorValue).map(async selector => {
1339
1448
  const bareAlias = !splitSelectorThinkingSuffix(selector).selector.includes("/");
@@ -1584,8 +1693,9 @@ export async function prepareModelProfileActivation(
1584
1693
  );
1585
1694
  }
1586
1695
 
1587
- const availableModels = options.modelRegistry.getAvailable?.() ?? options.modelRegistry.getAll();
1588
- const roleCatalogModels = options.modelRegistry.getAll();
1696
+ const availableModels =
1697
+ options.modelRegistry.getAvailable?.(credentialSessionId) ?? options.modelRegistry.getAll(credentialSessionId);
1698
+ const roleCatalogModels = options.modelRegistry.getAll(credentialSessionId);
1589
1699
  let bindings = resolveProfileBindings(profile);
1590
1700
  if (alternativeGroups.length > 0) {
1591
1701
  bindings = rewriteBindingsProviders(bindings, new Set(authenticatedProviders), alternativeGroups);
@@ -1797,6 +1907,7 @@ export async function prepareModelProfileActivation(
1797
1907
  manualModelRoleOverrides,
1798
1908
  manualAgentModelOverrides,
1799
1909
  previousActiveModelProfile: options.session.getActiveModelProfile?.(),
1910
+ previousActivatedModelProfile: options.session.getActivatedModelProfile?.(),
1800
1911
  previousSessionDefaultModel: options.session.getSessionDefaultModelSelector?.(),
1801
1912
  previousDefaultFallbackRuntimeState: options.session.getDefaultFallbackRuntimeState?.(),
1802
1913
  };
@@ -1898,6 +2009,7 @@ function capturePublishedModelProfileActivationState(
1898
2009
  defaultChainState: prepared.session.getConfiguredModelChainState?.("default"),
1899
2010
  defaultFallbackRuntimeState: prepared.session.getDefaultFallbackRuntimeState?.(),
1900
2011
  activeModelProfile: prepared.session.getActiveModelProfile?.(),
2012
+ activatedModelProfile: prepared.session.getActivatedModelProfile?.(),
1901
2013
  profileInstalledOverrideState: prepared.session.getProfileInstalledOverrideState?.(),
1902
2014
  sessionDefaultModel: prepared.session.getSessionDefaultModelSelector?.(),
1903
2015
  persistedModelRoles: prepared.settings.getGlobal("modelRoles"),
@@ -1964,7 +2076,7 @@ export function publishPreparedModelProfileActivation(
1964
2076
  ...prepared.baseAgentModelOverrides,
1965
2077
  ...prepared.agentModelOverrides,
1966
2078
  });
1967
- prepared.session.setActiveModelProfile?.(prepared.profileName);
2079
+ prepared.session.setActiveModelProfile?.(prepared.profileName, true);
1968
2080
  prepared.session.noteProfileInstalledOverrides?.(
1969
2081
  Object.keys(prepared.modelRoles),
1970
2082
  Object.keys(prepared.agentModelOverrides),
@@ -1975,6 +2087,7 @@ export function publishPreparedModelProfileActivation(
1975
2087
  modelRoles: [...prepared.manualModelRoleOverrides],
1976
2088
  agentModelOverrides: [...prepared.manualAgentModelOverrides],
1977
2089
  },
2090
+ prepared.defaultChain,
1978
2091
  );
1979
2092
  prepared.publishedState = capturePublishedModelProfileActivationState(prepared);
1980
2093
  }
@@ -2101,7 +2214,7 @@ export async function applyPreparedModelProfileActivation(
2101
2214
  if (options.isCurrent && !options.isCurrent()) throw new ModelProfileActivationSupersededError();
2102
2215
  if (!options.deferRuntimePublication) {
2103
2216
  activationStage = "active profile marker";
2104
- prepared.session.setActiveModelProfile?.(prepared.profileName);
2217
+ prepared.session.setActiveModelProfile?.(prepared.profileName, true);
2105
2218
  activationStage = "installed role tracking";
2106
2219
  prepared.session.noteProfileInstalledOverrides?.(
2107
2220
  Object.keys(prepared.modelRoles),
@@ -2113,6 +2226,7 @@ export async function applyPreparedModelProfileActivation(
2113
2226
  modelRoles: [...prepared.manualModelRoleOverrides],
2114
2227
  agentModelOverrides: [...prepared.manualAgentModelOverrides],
2115
2228
  },
2229
+ prepared.defaultChain,
2116
2230
  );
2117
2231
  activatedProfileInstalledOverrideState = prepared.session.getProfileInstalledOverrideState?.();
2118
2232
  }
@@ -2282,7 +2396,10 @@ export async function applyPreparedModelProfileActivation(
2282
2396
  }
2283
2397
  if (selectionIsCurrent()) {
2284
2398
  restore("restore active profile", () =>
2285
- prepared.session.setActiveModelProfile?.(prepared.previousActiveModelProfile),
2399
+ prepared.session.setActiveModelProfile?.(
2400
+ prepared.previousActiveModelProfile,
2401
+ prepared.previousActivatedModelProfile === prepared.previousActiveModelProfile,
2402
+ ),
2286
2403
  );
2287
2404
  }
2288
2405
  if (
@@ -2349,7 +2466,8 @@ export async function rollbackPreparedModelProfileActivation(
2349
2466
  const canRollbackPublishedProfile = (): boolean =>
2350
2467
  publishedState === undefined
2351
2468
  ? selectionIsCurrent()
2352
- : util.isDeepStrictEqual(prepared.session.getActiveModelProfile?.(), publishedState.activeModelProfile);
2469
+ : util.isDeepStrictEqual(prepared.session.getActiveModelProfile?.(), publishedState.activeModelProfile) &&
2470
+ util.isDeepStrictEqual(prepared.session.getActivatedModelProfile?.(), publishedState.activatedModelProfile);
2353
2471
 
2354
2472
  if (options.persistDefault) {
2355
2473
  if (
@@ -2536,7 +2654,10 @@ export async function rollbackPreparedModelProfileActivation(
2536
2654
  canRestorePublishedState(prepared.session.getActiveModelProfile?.(), publishedState?.activeModelProfile)
2537
2655
  ) {
2538
2656
  restore("restore active profile", () =>
2539
- prepared.session.setActiveModelProfile?.(prepared.previousActiveModelProfile),
2657
+ prepared.session.setActiveModelProfile?.(
2658
+ prepared.previousActiveModelProfile,
2659
+ prepared.previousActivatedModelProfile === prepared.previousActiveModelProfile,
2660
+ ),
2540
2661
  );
2541
2662
  }
2542
2663
  if (
@@ -2583,6 +2704,7 @@ export interface MaterializeModelProfileForDeletionResult {
2583
2704
  previousDefaultProfile: string | undefined;
2584
2705
  previousPersistedDefaultProfile: string | undefined;
2585
2706
  previousActiveModelProfile: string | undefined;
2707
+ previousActivatedModelProfile: string | undefined;
2586
2708
  previousDefaultChainState: ConfiguredModelChainState | undefined;
2587
2709
  previousDefaultFallbackRuntimeState: DefaultFallbackRuntimeState | undefined;
2588
2710
  previousProfileInstalledOverrideState: AgentSessionProfileInstalledOverrideState | undefined;
@@ -2714,7 +2836,12 @@ export async function materializeModelProfileForDeletion(
2714
2836
  ? prepared.settings.clearOverride("modelProfile.default")
2715
2837
  : prepared.settings.override("modelProfile.default", prepared.previousDefaultProfileOverride),
2716
2838
  );
2717
- restore(() => prepared.session.setActiveModelProfile?.(prepared.previousActiveModelProfile));
2839
+ restore(() =>
2840
+ prepared.session.setActiveModelProfile?.(
2841
+ prepared.previousActiveModelProfile,
2842
+ prepared.previousActivatedModelProfile === prepared.previousActiveModelProfile,
2843
+ ),
2844
+ );
2718
2845
  if (prepared.previousProfileInstalledOverrideState) {
2719
2846
  restore(() =>
2720
2847
  prepared.session.restoreProfileInstalledOverrideState?.(prepared.previousProfileInstalledOverrideState!),
@@ -2747,6 +2874,7 @@ export async function materializeModelProfileForDeletion(
2747
2874
  previousDefaultProfile,
2748
2875
  previousPersistedDefaultProfile,
2749
2876
  previousActiveModelProfile: prepared.previousActiveModelProfile,
2877
+ previousActivatedModelProfile: prepared.previousActivatedModelProfile,
2750
2878
  previousDefaultChainState: prepared.previousDefaultChainState,
2751
2879
  previousDefaultFallbackRuntimeState: prepared.previousDefaultFallbackRuntimeState,
2752
2880
  previousProfileInstalledOverrideState: prepared.previousProfileInstalledOverrideState,
@@ -2816,7 +2944,12 @@ export async function restoreMaterializedModelProfileForDeletion(options: {
2816
2944
  ? options.settings.clearOverride("modelProfile.default")
2817
2945
  : options.settings.override("modelProfile.default", options.snapshot.previousDefaultProfileOverride),
2818
2946
  );
2819
- 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
+ );
2820
2953
  if (options.snapshot.previousProfileInstalledOverrideState) {
2821
2954
  restore(() =>
2822
2955
  options.session.restoreProfileInstalledOverrideState?.(
@@ -2863,6 +2996,7 @@ export async function applyModelProfileRuntimeBindings(
2863
2996
  const previousModelRolesOverride = prepared.settings.getOverride("modelRoles");
2864
2997
  const previousAgentModelOverridesOverride = prepared.settings.getOverride("task.agentModelOverrides");
2865
2998
  const previousActiveModelProfile = prepared.session.getActiveModelProfile?.();
2999
+ const previousActivatedModelProfile = prepared.session.getActivatedModelProfile?.();
2866
3000
  const previousProfileInstalledOverrideState = prepared.session.getProfileInstalledOverrideState?.();
2867
3001
  let activatedModelRolesOverride: Readonly<Record<string, ModelSelectorValue>> | undefined;
2868
3002
  let activatedAgentModelOverridesOverride: Readonly<Record<string, ModelSelectorValue>> | undefined;
@@ -2878,7 +3012,7 @@ export async function applyModelProfileRuntimeBindings(
2878
3012
  ...prepared.agentModelOverrides,
2879
3013
  });
2880
3014
  activatedAgentModelOverridesOverride = prepared.settings.getOverride("task.agentModelOverrides");
2881
- prepared.session.setActiveModelProfile?.(prepared.profileName);
3015
+ prepared.session.setActiveModelProfile?.(prepared.profileName, true);
2882
3016
  try {
2883
3017
  await prepared.session.syncEagerDelegation?.();
2884
3018
  } catch (error) {
@@ -2904,7 +3038,10 @@ export async function applyModelProfileRuntimeBindings(
2904
3038
  prepared.settings.clearOverride("task.agentModelOverrides");
2905
3039
  else prepared.settings.override("task.agentModelOverrides", restoredAgentModelOverridesOverride);
2906
3040
  if (prepared.session.getActiveModelProfile?.() === prepared.profileName) {
2907
- prepared.session.setActiveModelProfile?.(previousActiveModelProfile);
3041
+ prepared.session.setActiveModelProfile?.(
3042
+ previousActiveModelProfile,
3043
+ previousActivatedModelProfile === previousActiveModelProfile,
3044
+ );
2908
3045
  }
2909
3046
  if (previousProfileInstalledOverrideState) {
2910
3047
  prepared.session.restoreProfileInstalledOverrideState?.(previousProfileInstalledOverrideState);
@@ -2929,6 +3066,7 @@ export async function applyModelProfileRuntimeBindings(
2929
3066
  modelRoles: [...prepared.manualModelRoleOverrides],
2930
3067
  agentModelOverrides: [...prepared.manualAgentModelOverrides],
2931
3068
  },
3069
+ prepared.defaultChain,
2932
3070
  );
2933
3071
  } finally {
2934
3072
  if (isCurrent && !isCurrent()) {