@gajae-code/coding-agent 0.18.10 → 0.18.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (270) hide show
  1. package/CHANGELOG.md +295 -0
  2. package/dist/types/async/job-manager.d.ts +7 -3
  3. package/dist/types/async/result-formatting.d.ts +2 -0
  4. package/dist/types/autoresearch/git.d.ts +1 -1
  5. package/dist/types/capability/fs.d.ts +2 -2
  6. package/dist/types/cli/accounts-cli.d.ts +2 -2
  7. package/dist/types/cli/doctor/install-repairs.d.ts +4 -1
  8. package/dist/types/cli/install-activation.d.ts +10 -4
  9. package/dist/types/cli/public-command-errors.d.ts +1 -0
  10. package/dist/types/cli/update-cli.d.ts +14 -1
  11. package/dist/types/cli-ordinary.d.ts +2 -2
  12. package/dist/types/commands/accounts.d.ts +2 -1
  13. package/dist/types/commands/migrate.d.ts +1 -1
  14. package/dist/types/commands/sdk.d.ts +5 -0
  15. package/dist/types/commit/changelog/index.d.ts +1 -0
  16. package/dist/types/config/autorouting-tier-map.d.ts +30 -0
  17. package/dist/types/config/config-hot-reload-trust.d.ts +12 -0
  18. package/dist/types/config/config-hot-reload.d.ts +39 -0
  19. package/dist/types/config/file-lock.d.ts +8 -1
  20. package/dist/types/config/model-discovery-manager.d.ts +2 -0
  21. package/dist/types/config/model-profile-activation.d.ts +62 -6
  22. package/dist/types/config/model-registry.d.ts +56 -5
  23. package/dist/types/config/model-resolver.d.ts +2 -0
  24. package/dist/types/config/models-config-schema.d.ts +10 -0
  25. package/dist/types/config/settings-schema.d.ts +46 -3
  26. package/dist/types/config/settings.d.ts +33 -1
  27. package/dist/types/dap/config.d.ts +8 -4
  28. package/dist/types/debug/system-info.d.ts +6 -1
  29. package/dist/types/discovery/mcp-compat.d.ts +3 -1
  30. package/dist/types/edit/modes/patch.d.ts +5 -0
  31. package/dist/types/edit/modes/replace.d.ts +3 -0
  32. package/dist/types/exec/bash-executor.d.ts +12 -0
  33. package/dist/types/gjc-runtime/managed-owner-admission.d.ts +3 -0
  34. package/dist/types/gjc-runtime/session-state-lock.d.ts +2 -0
  35. package/dist/types/gjc-runtime/state-runtime.d.ts +0 -2
  36. package/dist/types/migrate/adapters/codex.d.ts +2 -1
  37. package/dist/types/migrate/adapters/index.d.ts +10 -1
  38. package/dist/types/migrate/executor.d.ts +3 -1
  39. package/dist/types/migrate/types.d.ts +15 -3
  40. package/dist/types/modes/components/settings-selector.d.ts +6 -0
  41. package/dist/types/modes/controllers/selector-controller.d.ts +2 -0
  42. package/dist/types/modes/theme/theme.d.ts +1 -0
  43. package/dist/types/runtime-mcp/content-limits.d.ts +4 -0
  44. package/dist/types/runtime-mcp/manager.d.ts +7 -0
  45. package/dist/types/runtime-mcp/plugin-mcp-name-filter.d.ts +22 -0
  46. package/dist/types/runtime-mcp/tool-bridge.d.ts +4 -0
  47. package/dist/types/sdk/broker/broker-exit.d.ts +2 -2
  48. package/dist/types/sdk/broker/broker.d.ts +7 -1
  49. package/dist/types/sdk/broker/daemon-entry.d.ts +11 -1
  50. package/dist/types/sdk/broker/doctor-restart.d.ts +1 -1
  51. package/dist/types/sdk/broker/ensure.d.ts +136 -7
  52. package/dist/types/sdk/broker/hop.d.ts +36 -0
  53. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +18 -10
  54. package/dist/types/sdk/broker/lock-host-identity.d.ts +5 -0
  55. package/dist/types/sdk/broker/session-index.d.ts +4 -1
  56. package/dist/types/sdk/bus/chat-command-policy.d.ts +2 -1
  57. package/dist/types/sdk/bus/index.d.ts +23 -0
  58. package/dist/types/sdk/bus/reconciliation-store.d.ts +10 -0
  59. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +3 -1
  60. package/dist/types/sdk/cli/session-cli.d.ts +0 -1
  61. package/dist/types/sdk/host/session-runtime.d.ts +32 -0
  62. package/dist/types/sdk/prompt-deadline-flush.d.ts +42 -4
  63. package/dist/types/sdk/prompt-deadline-manager.d.ts +2 -0
  64. package/dist/types/sdk/protocol/adapter-validation.d.ts +10 -4
  65. package/dist/types/sdk/reconciliation-extensions.d.ts +1 -1
  66. package/dist/types/sdk/session-list.d.ts +1 -1
  67. package/dist/types/session/agent-session.d.ts +58 -2
  68. package/dist/types/session/internal/managed-session-scope.d.ts +2 -2
  69. package/dist/types/session/internal/managed-session-storage.d.ts +1 -1
  70. package/dist/types/session/internal/native-publish-outcome.d.ts +1 -1
  71. package/dist/types/session/messages.d.ts +5 -1
  72. package/dist/types/session/session-manager.d.ts +13 -3
  73. package/dist/types/session/startup-auth-config.d.ts +24 -2
  74. package/dist/types/session/streaming-output.d.ts +11 -0
  75. package/dist/types/session/terminal-abort.d.ts +3 -1
  76. package/dist/types/session-import/service.d.ts +1 -1
  77. package/dist/types/skill-state/workflow-mutation-guard.d.ts +7 -3
  78. package/dist/types/slash-commands/helpers/provider-auth-status.d.ts +8 -0
  79. package/dist/types/task/executor.d.ts +19 -2
  80. package/dist/types/tools/ast-edit-write-path.d.ts +10 -0
  81. package/dist/types/tools/atomic-file-write.d.ts +9 -7
  82. package/dist/types/tools/bash-allowed-prefixes.d.ts +11 -0
  83. package/dist/types/tools/descriptors.d.ts +2 -0
  84. package/dist/types/tools/fetch.d.ts +1 -1
  85. package/dist/types/tools/index.d.ts +2 -0
  86. package/dist/types/tools/python.d.ts +1 -1
  87. package/dist/types/tools/report-issue.d.ts +41 -0
  88. package/dist/types/tools/subagent.d.ts +23 -0
  89. package/dist/types/web/scrapers/types.d.ts +8 -1
  90. package/dist/types/web/search/providers/insane.d.ts +7 -1
  91. package/dist/types/web/search/providers/searxng.d.ts +5 -0
  92. package/package.json +8 -8
  93. package/scripts/build-binary.ts +2 -1
  94. package/scripts/capture-subagent-compact-panel-showcase.ts +105 -0
  95. package/scripts/generate-docs-index.ts +8 -1
  96. package/scripts/generate-sdk-operation-inventory.ts +22 -4
  97. package/src/async/job-manager.ts +356 -56
  98. package/src/async/result-formatting.ts +40 -0
  99. package/src/autoresearch/git.ts +9 -2
  100. package/src/autoresearch/prompts/prompt-setup.md +1 -1
  101. package/src/capability/fs.ts +27 -19
  102. package/src/capability/index.ts +2 -2
  103. package/src/cli/accounts-cli.ts +176 -8
  104. package/src/cli/auth-broker-cli.ts +21 -11
  105. package/src/cli/doctor/install-repairs.ts +35 -5
  106. package/src/cli/fast-help.ts +1 -1
  107. package/src/cli/install-activation.ts +47 -4
  108. package/src/cli/migrate-cli.ts +7 -5
  109. package/src/cli/public-command-errors.ts +4 -0
  110. package/src/cli/root-flags.ts +2 -2
  111. package/src/cli/stats-cli.ts +9 -5
  112. package/src/cli/update-cli.ts +224 -7
  113. package/src/cli-ordinary.ts +7 -2
  114. package/src/commands/accounts.ts +10 -5
  115. package/src/commands/migrate.ts +3 -3
  116. package/src/commands/sdk.ts +199 -42
  117. package/src/commands/ultragoal.ts +16 -8
  118. package/src/commit/changelog/index.ts +39 -1
  119. package/src/commit/message.ts +10 -3
  120. package/src/config/autorouting-tier-map.ts +30 -0
  121. package/src/config/config-hot-reload-trust.ts +148 -0
  122. package/src/config/config-hot-reload.ts +734 -0
  123. package/src/config/file-lock.ts +216 -56
  124. package/src/config/model-bindings-applier.ts +85 -47
  125. package/src/config/model-discovery-manager.ts +10 -0
  126. package/src/config/model-profile-activation.ts +716 -113
  127. package/src/config/model-registry.ts +1334 -158
  128. package/src/config/model-resolver.ts +3 -1
  129. package/src/config/models-config-schema.ts +6 -0
  130. package/src/config/settings-schema.ts +60 -3
  131. package/src/config/settings.ts +458 -24
  132. package/src/coordinator-mcp/policy.ts +26 -1
  133. package/src/crash/sanitize.ts +4 -0
  134. package/src/customization/import.ts +3 -1
  135. package/src/dap/config.ts +31 -11
  136. package/src/dap/defaults.json +1 -1
  137. package/src/debug/crash-diagnostics.ts +2 -1
  138. package/src/debug/report-bundle.ts +19 -0
  139. package/src/debug/system-info.ts +8 -3
  140. package/src/defaults/gjc/extensions/grok-cli-vendor/src/shared/base-url.ts +21 -7
  141. package/src/discovery/agents-md.ts +1 -1
  142. package/src/discovery/helpers.ts +1 -2
  143. package/src/discovery/mcp-compat.ts +120 -17
  144. package/src/edit/apply-patch/index.ts +12 -1
  145. package/src/edit/index.ts +13 -17
  146. package/src/edit/modes/patch.ts +187 -8
  147. package/src/edit/modes/replace.ts +136 -0
  148. package/src/eval/js/context-manager.ts +29 -3
  149. package/src/eval/js/worker-entry.ts +51 -18
  150. package/src/eval/py/executor.ts +14 -4
  151. package/src/eval/py/runner.py +144 -5
  152. package/src/exec/bash-executor.ts +81 -4
  153. package/src/exec/bash-shell-guardian.ts +4 -1
  154. package/src/exec/bash-shell-supervisor.ts +8 -4
  155. package/src/exec/isolated-shell.ts +5 -1
  156. package/src/export/html/index.ts +46 -6
  157. package/src/extensibility/custom-tools/wrapper.ts +3 -1
  158. package/src/extensibility/extensions/loader.ts +8 -1
  159. package/src/extensibility/plugins/marketplace/fetcher.ts +2 -2
  160. package/src/gjc-runtime/gc-runtime.ts +33 -6
  161. package/src/gjc-runtime/launch-tmux.ts +7 -2
  162. package/src/gjc-runtime/launch-worktree.test.ts +148 -0
  163. package/src/gjc-runtime/launch-worktree.ts +101 -14
  164. package/src/gjc-runtime/managed-owner-admission.ts +46 -2
  165. package/src/gjc-runtime/session-resolution.ts +38 -6
  166. package/src/gjc-runtime/session-state-lock.ts +59 -14
  167. package/src/gjc-runtime/state-runtime.ts +23 -19
  168. package/src/hooks/native-skill-hook.ts +14 -11
  169. package/src/internal-urls/docs-index.generated.ts +14 -13
  170. package/src/internal-urls/local-protocol.ts +142 -8
  171. package/src/lsp/index.ts +29 -0
  172. package/src/lsp/workspace-path.ts +23 -0
  173. package/src/migrate/action-planner.ts +57 -0
  174. package/src/migrate/adapters/claude-code.ts +23 -4
  175. package/src/migrate/adapters/codex.ts +22 -4
  176. package/src/migrate/adapters/index.ts +22 -4
  177. package/src/migrate/adapters/opencode.ts +6 -1
  178. package/src/migrate/executor.ts +84 -7
  179. package/src/migrate/report.ts +19 -2
  180. package/src/migrate/types.ts +14 -4
  181. package/src/modes/DESIGN.md +51 -0
  182. package/src/modes/acp/permission-mode.ts +18 -1
  183. package/src/modes/components/assistant-message.ts +55 -1
  184. package/src/modes/components/settings-selector.ts +103 -37
  185. package/src/modes/controllers/command-controller.ts +38 -2
  186. package/src/modes/controllers/plan-mode-controller.ts +17 -7
  187. package/src/modes/controllers/selector-controller.ts +62 -6
  188. package/src/modes/interactive-mode.ts +68 -0
  189. package/src/modes/theme/theme.ts +15 -0
  190. package/src/prompts/tools/debug.md +1 -1
  191. package/src/prompts/tools/read.md +1 -1
  192. package/src/prompts/tools/report-issue.md +13 -0
  193. package/src/prompts/tools/task.md +1 -1
  194. package/src/runtime-credential-selector.ts +2 -2
  195. package/src/runtime-mcp/content-limits.ts +7 -0
  196. package/src/runtime-mcp/manager-pool.test.ts +5 -4
  197. package/src/runtime-mcp/manager.ts +32 -0
  198. package/src/runtime-mcp/oauth-flow.ts +32 -6
  199. package/src/runtime-mcp/plugin-mcp-name-filter.ts +62 -0
  200. package/src/runtime-mcp/render.ts +16 -1
  201. package/src/runtime-mcp/tool-bridge.ts +126 -17
  202. package/src/sdk/broker/broker-exit.ts +8 -5
  203. package/src/sdk/broker/broker.ts +342 -82
  204. package/src/sdk/broker/daemon-entry.ts +338 -116
  205. package/src/sdk/broker/doctor-restart.ts +2 -1
  206. package/src/sdk/broker/ensure.ts +1143 -265
  207. package/src/sdk/broker/hop.ts +274 -0
  208. package/src/sdk/broker/lifecycle-ledger.ts +35 -11
  209. package/src/sdk/broker/lock-host-identity.ts +29 -0
  210. package/src/sdk/broker/session-index.ts +62 -12
  211. package/src/sdk/bus/chat-command-policy.ts +16 -3
  212. package/src/sdk/bus/index.ts +52 -21
  213. package/src/sdk/bus/reconciliation-store.ts +201 -8
  214. package/src/sdk/bus/telegram-daemon-contract.ts +3 -1
  215. package/src/sdk/cli/session-cli.ts +21 -6
  216. package/src/sdk/host/session-runtime.test.ts +118 -23
  217. package/src/sdk/host/session-runtime.ts +352 -47
  218. package/src/sdk/lifecycle/service.ts +1 -0
  219. package/src/sdk/prompt-deadline-flush.ts +78 -5
  220. package/src/sdk/prompt-deadline-manager.ts +68 -14
  221. package/src/sdk/protocol/adapter-validation.ts +53 -11
  222. package/src/sdk/protocol/operation-inventory.generated.json +101 -2
  223. package/src/sdk/reconciliation-extensions.ts +1 -0
  224. package/src/sdk/session-list.ts +1 -0
  225. package/src/sdk/session.ts +114 -61
  226. package/src/session/agent-session.ts +1043 -81
  227. package/src/session/internal/managed-session-scope.ts +126 -39
  228. package/src/session/internal/managed-session-storage.ts +14 -11
  229. package/src/session/internal/native-publish-outcome.ts +5 -1
  230. package/src/session/messages.ts +47 -6
  231. package/src/session/session-manager.ts +236 -14
  232. package/src/session/startup-auth-config.ts +116 -8
  233. package/src/session/streaming-output.ts +262 -4
  234. package/src/session/terminal-abort.ts +14 -2
  235. package/src/session-import/service.ts +1 -0
  236. package/src/skill-state/workflow-mutation-guard.ts +491 -32
  237. package/src/slash-commands/builtin-registry.ts +15 -3
  238. package/src/slash-commands/helpers/credential-switch.ts +10 -5
  239. package/src/slash-commands/helpers/provider-auth-status.ts +456 -0
  240. package/src/task/executor.ts +103 -12
  241. package/src/task/index.ts +389 -175
  242. package/src/tools/ast-edit-write-path.ts +64 -0
  243. package/src/tools/ast-edit.ts +18 -1
  244. package/src/tools/atomic-file-write.ts +46 -15
  245. package/src/tools/auto-generated-guard.ts +3 -0
  246. package/src/tools/bash-allowed-prefixes.ts +153 -5
  247. package/src/tools/bash-interactive.ts +14 -2
  248. package/src/tools/bash.ts +14 -0
  249. package/src/tools/debug.ts +7 -6
  250. package/src/tools/descriptors.test.ts +44 -0
  251. package/src/tools/descriptors.ts +34 -2
  252. package/src/tools/fetch.ts +3 -2
  253. package/src/tools/index.ts +2 -0
  254. package/src/tools/output-meta.ts +23 -2
  255. package/src/tools/python.ts +1 -1
  256. package/src/tools/read.ts +17 -35
  257. package/src/tools/report-issue.ts +184 -0
  258. package/src/tools/subagent-render.ts +114 -12
  259. package/src/tools/subagent.ts +108 -1
  260. package/src/tools/todo-write.ts +41 -3
  261. package/src/tools/tool-catalog.generated.ts +31 -3
  262. package/src/tools/write.ts +35 -3
  263. package/src/utils/commit-message-generator.ts +9 -2
  264. package/src/utils/file-mentions.ts +29 -1
  265. package/src/utils/title-generator.ts +7 -2
  266. package/src/web/scrapers/index.ts +3 -1
  267. package/src/web/scrapers/types.ts +8 -0
  268. package/src/web/scrapers/youtube.ts +3 -2
  269. package/src/web/search/providers/insane.ts +91 -28
  270. package/src/web/search/providers/searxng.ts +62 -10
@@ -221,14 +221,21 @@ import {
221
221
  type SubagentLifecycle,
222
222
  } from "../async";
223
223
  import { asyncJobEndpointId as deriveAsyncJobEndpointId } from "../async/endpoint-id";
224
+ import { formatAsyncResultForFollowUp } from "../async/result-formatting";
224
225
  import { reset as resetCapabilities } from "../capability";
225
226
  import type { Rule } from "../capability/rule";
226
227
  import type { CasReceipt } from "../config/atomic-yaml-patch";
228
+ import type { ConfigHotReloadCandidate, ConfigHotReloadPaths } from "../config/config-hot-reload";
227
229
  import {
228
230
  activateModelProfile,
229
231
  applyModelProfileRuntimeBindings,
232
+ finishPreparedModelProfileActivation,
230
233
  materializeActiveModelProfileAssignment,
234
+ type PreparedModelProfileActivation,
235
+ prepareModelProfileActivation,
236
+ publishPreparedModelProfileActivation,
231
237
  resolveMissingSessionModelRecovery,
238
+ rollbackPreparedModelProfileActivation,
232
239
  } from "../config/model-profile-activation";
233
240
  import {
234
241
  ModelProfileRegistryError,
@@ -249,6 +256,7 @@ import {
249
256
  kNoAuth,
250
257
  MODEL_ROLE_IDS,
251
258
  type ModelRegistry,
259
+ type ModelsConfigReloadCandidate,
252
260
  } from "../config/model-registry";
253
261
  import {
254
262
  extractExplicitThinkingSelector,
@@ -264,10 +272,14 @@ import {
264
272
  } from "../config/model-resolver";
265
273
  import { type ModelSelectorValue, normalizeModelSelectorValue } from "../config/model-selector-value";
266
274
  import { expandPromptTemplate, type PromptTemplate } from "../config/prompt-templates";
267
- import type { Settings, SkillsSettings } from "../config/settings";
275
+ import type {
276
+ Settings,
277
+ SettingsConfigReloadPublication,
278
+ SettingsGlobalConfigCandidate,
279
+ SkillsSettings,
280
+ } from "../config/settings";
268
281
  import { onAppendOnlyModeChanged } from "../config/settings";
269
- import type { SettingPath } from "../config/settings-schema";
270
- import { getDefault } from "../config/settings-schema";
282
+ import { getDefault, SETTINGS_SCHEMA, type SettingPath } from "../config/settings-schema";
271
283
  import { resolveEagerTaskDelegation } from "../config/task-delegation";
272
284
  import { RawSseDebugBuffer } from "../debug/raw-sse-buffer";
273
285
  import { loadCapability } from "../discovery";
@@ -412,11 +424,15 @@ import type { LazyService } from "../runtime/lazy-service";
412
424
  import type { NetworkPrewarmRuntime } from "../runtime/network-prewarm-service";
413
425
  import type { WorkspaceTreeRuntime } from "../runtime/workspace-tree-service";
414
426
  import { type ExactMcpServerControlResult, MCPManager } from "../runtime-mcp/manager";
427
+ import {
428
+ omitPluginMcpNameShadows,
429
+ retainPluginMcpMandatoryNames,
430
+ } from "../runtime-mcp/plugin-mcp-name-filter";
415
431
  import { attachExactMcpControls, getExactMcpControls, revokeExactMcpControls } from "../runtime-mcp/redaction";
416
432
  import type { NotificationSessionController } from "../sdk/bus/session-control";
417
433
  import { buildSyntheticModelId, syntheticNamespaceCollision } from "../sdk/model-profile-model";
418
434
  import { sanitizePromptFailure } from "../sdk/prompt-failure";
419
- import type { SecretObfuscator } from "../secrets/obfuscator";
435
+ import { obfuscateMessages, type SecretObfuscator } from "../secrets/obfuscator";
420
436
  import { formatNoCredentialOnboardingError, NoModelSelectedError } from "../setup/model-onboarding-guidance";
421
437
  import {
422
438
  isCanonicalGjcWorkflowSkill,
@@ -566,31 +582,6 @@ import { ToolChoiceQueue } from "./tool-choice-queue";
566
582
  import { pruneSupersededMaintenanceReminders, pruneSupersededVolatileProjectContext } from "./volatile-context-pruning";
567
583
  import { YieldQueue } from "./yield-queue";
568
584
 
569
- const ASYNC_INLINE_RESULT_MAX_CHARS = 12_000;
570
- const ASYNC_PREVIEW_MAX_CHARS = 4_000;
571
-
572
- async function formatParkedAsyncResult(
573
- sessionManager: SessionManager,
574
- result: string,
575
- allowArtifact = true,
576
- ): Promise<string> {
577
- if (result.length <= ASYNC_INLINE_RESULT_MAX_CHARS) return result;
578
- const preview = `${result.slice(0, ASYNC_PREVIEW_MAX_CHARS)}\n\n[Output truncated. Showing first ${ASYNC_PREVIEW_MAX_CHARS.toLocaleString()} characters.]`;
579
- try {
580
- if (!allowArtifact) return preview;
581
- const { path: artifactPath, id: artifactId } = await sessionManager.allocateArtifactPath("async");
582
- if (artifactPath && artifactId) {
583
- await Bun.write(artifactPath, result);
584
- return `${preview}\nFull output: artifact://${artifactId}`;
585
- }
586
- } catch (error) {
587
- logger.warn("Failed to persist parked async follow-up artifact", {
588
- error: error instanceof Error ? error.message : String(error),
589
- });
590
- }
591
- return preview;
592
- }
593
-
594
585
  /**
595
586
  * #4560: structured workflow recovery projection from canonical durable
596
587
  * Ralplan/Ultragoal state, consumed by the compaction summary context and
@@ -2010,14 +2001,30 @@ async function discardPreparedNewSessionAfterFailure(
2010
2001
  // ============================================================================
2011
2002
 
2012
2003
  /** Tools that require user permission before execution when an ACP client is connected. */
2013
- const PERMISSION_REQUIRED_TOOLS = new Set(["bash", "monitor", "eval", "edit", "delete", "move", "github"]);
2004
+ const PERMISSION_REQUIRED_TOOLS = new Set([
2005
+ "bash",
2006
+ "monitor",
2007
+ "eval",
2008
+ "edit",
2009
+ "delete",
2010
+ "move",
2011
+ "github",
2012
+ "computer",
2013
+ "python",
2014
+ "recipe",
2015
+ ]);
2014
2016
 
2015
2017
  function isShellExecutionPermissionTool(toolName: string): boolean {
2016
2018
  return toolName === "bash" || toolName === "monitor";
2017
2019
  }
2018
2020
 
2019
2021
  function isExecutionPermissionTool(toolName: string): boolean {
2020
- return isShellExecutionPermissionTool(toolName) || toolName === "eval";
2022
+ return (
2023
+ isShellExecutionPermissionTool(toolName) ||
2024
+ toolName === "eval" ||
2025
+ toolName === "python" ||
2026
+ toolName === "recipe"
2027
+ );
2021
2028
  }
2022
2029
 
2023
2030
  /** Permission options presented to the client on each gated tool call. */
@@ -2099,6 +2106,16 @@ function getPermissionIntent(
2099
2106
  cacheKey: toolName,
2100
2107
  };
2101
2108
  }
2109
+ if (toolName === "python") {
2110
+ const action = getStringProperty(a, "action");
2111
+ if (action === "clear") return { toolName, title: "python clear", cacheKey: toolName };
2112
+ const code = getStringProperty(a, "code")?.slice(0, 80);
2113
+ return { toolName, title: code && code.length > 0 ? code : "python", cacheKey: toolName };
2114
+ }
2115
+ if (toolName === "recipe") {
2116
+ const op = getStringProperty(a, "op")?.slice(0, 80);
2117
+ return { toolName, title: op && op.length > 0 ? op : "recipe", cacheKey: toolName };
2118
+ }
2102
2119
  if (toolName === "delete") {
2103
2120
  const p = getStringProperty(a, "path");
2104
2121
  return { toolName, title: p ? `Delete ${p}` : toolName, paths: p ? [p] : undefined, cacheKey: toolName };
@@ -2135,6 +2152,10 @@ function getPermissionIntent(
2135
2152
  };
2136
2153
  }
2137
2154
  if (toolName === "github") return githubPermissionIntent(args);
2155
+ if (toolName === "computer") {
2156
+ const action = getStringProperty(a, "action");
2157
+ return { toolName, title: action ? `Computer ${action}` : toolName, cacheKey: toolName };
2158
+ }
2138
2159
  return undefined;
2139
2160
  }
2140
2161
 
@@ -2335,8 +2356,10 @@ export class WorkerIntegrationRequestScheduler {
2335
2356
  this.#ownedRequests.add(request);
2336
2357
  void request.finally(() => this.#ownedRequests.delete(request)).catch(() => {});
2337
2358
  let timeout: ReturnType<typeof setTimeout> | undefined;
2359
+ let timedOut = false;
2338
2360
  const deadline = new Promise<WorkerIntegrationOutcome>(resolve => {
2339
2361
  timeout = setTimeout(() => {
2362
+ timedOut = true;
2340
2363
  controller.abort(new Error("Worker integration request timed out"));
2341
2364
  resolve({ status: "timed_out" });
2342
2365
  }, this.timeoutMs);
@@ -2349,7 +2372,12 @@ export class WorkerIntegrationRequestScheduler {
2349
2372
  deadline,
2350
2373
  ]).finally(() => {
2351
2374
  if (timeout) clearTimeout(timeout);
2352
- controller.abort();
2375
+ // Only abort if the timeout actually fired. If the request settled before the
2376
+ // timeout (timedOut=false), don't send the abort signal again: this ensures
2377
+ // SDK/ACP settlement is not delayed by recovery requests that ignore their
2378
+ // abort signal (#6508). The request may still be running, but that work
2379
+ // should not block client-visible completion.
2380
+ if (timedOut) controller.abort();
2353
2381
  this.#inFlight = undefined;
2354
2382
  if (this.#pending) this.#start();
2355
2383
  });
@@ -2681,6 +2709,68 @@ export interface DefaultFallbackRuntimeState {
2681
2709
  exhaustedLastTurn: boolean;
2682
2710
  }
2683
2711
 
2712
+ export interface AgentSessionProfileInstalledOverrideState {
2713
+ modelRoles: ReadonlyMap<string, ModelSelectorValue | undefined>;
2714
+ agentModelOverrides: ReadonlyMap<string, ModelSelectorValue | undefined>;
2715
+ installedModelRoles: ReadonlyMap<string, ModelSelectorValue>;
2716
+ installedAgentModelOverrides: ReadonlyMap<string, ModelSelectorValue>;
2717
+ manualModelRoles: ReadonlySet<string>;
2718
+ manualAgentModelOverrides: ReadonlySet<string>;
2719
+ configuredAgentModelOverrides: ReadonlySet<string>;
2720
+ preProfileModel: Model | undefined;
2721
+ }
2722
+
2723
+ export interface PreparedProfileModelSelection {
2724
+ readonly sessionId: string;
2725
+ readonly model: Model;
2726
+ readonly thinkingLevel: ThinkingLevel | undefined;
2727
+ readonly committedThinkingLevel: ThinkingLevel | undefined;
2728
+ readonly previousModel: Model | undefined;
2729
+ readonly previousThinkingLevel: ThinkingLevel | undefined;
2730
+ readonly previousEditMode: EditMode;
2731
+ readonly signal: AbortSignal | undefined;
2732
+ }
2733
+
2734
+ export interface ConfigurationReloadResult {
2735
+ readonly applied: boolean;
2736
+ readonly settingsChanged: boolean;
2737
+ readonly modelsChanged: boolean;
2738
+ readonly changedSettings: readonly SettingPath[];
2739
+ }
2740
+
2741
+ export type ConfigurationReloadErrorCode =
2742
+ | "SESSION_UNAVAILABLE"
2743
+ | "PATH_CHANGED"
2744
+ | "SETTINGS_INVALID"
2745
+ | "MODELS_INVALID"
2746
+ | "MODEL_UNAVAILABLE"
2747
+ | "ACTIVE_PROFILE_INVALID"
2748
+ | "PUBLICATION_FAILED"
2749
+ | "VALIDATION_CLEANUP_FAILED";
2750
+
2751
+ const CONFIGURATION_RELOAD_ERROR_MESSAGES: Record<ConfigurationReloadErrorCode, string> = {
2752
+ SESSION_UNAVAILABLE: "Configuration reload is unavailable for this session.",
2753
+ PATH_CHANGED: "Configuration files changed while reload was pending.",
2754
+ SETTINGS_INVALID: "The updated settings configuration is invalid.",
2755
+ MODELS_INVALID: "The updated model catalog is invalid.",
2756
+ MODEL_UNAVAILABLE: "The selected model is unavailable in the updated configuration.",
2757
+ ACTIVE_PROFILE_INVALID: "The active model preset is invalid for the updated configuration.",
2758
+ PUBLICATION_FAILED: "Configuration reload could not be applied.",
2759
+ VALIDATION_CLEANUP_FAILED: "Configuration validation could not release its staged catalog.",
2760
+ };
2761
+
2762
+ export class ConfigurationReloadError extends Error {
2763
+ readonly code: ConfigurationReloadErrorCode;
2764
+ readonly retryable: boolean;
2765
+
2766
+ constructor(code: ConfigurationReloadErrorCode, cause?: unknown, options?: { readonly retryable?: boolean }) {
2767
+ super(CONFIGURATION_RELOAD_ERROR_MESSAGES[code], cause === undefined ? undefined : { cause });
2768
+ this.name = "ConfigurationReloadError";
2769
+ this.code = code;
2770
+ this.retryable = options?.retryable === true;
2771
+ }
2772
+ }
2773
+
2684
2774
  /**
2685
2775
  * Fire-and-forget continuations (auto-compaction retries, queued follow-ups) race a
2686
2776
  * still-busy agent whose current turn can legitimately hold the run for minutes.
@@ -2787,6 +2877,11 @@ export class AgentSession {
2787
2877
  #unavailableModelProfile: string | undefined;
2788
2878
  #activeProfileInstalledRoles = new Map<string, ModelSelectorValue | undefined>();
2789
2879
  #activeProfileInstalledAgentOverrides = new Map<string, ModelSelectorValue | undefined>();
2880
+ #activeProfileInstalledRoleValues = new Map<string, ModelSelectorValue>();
2881
+ #activeProfileInstalledAgentOverrideValues = new Map<string, ModelSelectorValue>();
2882
+ #activeProfileManualModelRoles = new Set<string>();
2883
+ #activeProfileManualAgentModelOverrides = new Set<string>();
2884
+ #activeProfileConfiguredAgentModelOverrides = new Set<string>();
2790
2885
  #preProfileModel: Model | undefined;
2791
2886
  #sessionAdmissionQueue: SessionAdmissionEntry[] = [];
2792
2887
  #activeSessionAdmission: SessionAdmissionEntry | undefined;
@@ -2797,6 +2892,7 @@ export class AgentSession {
2797
2892
  #successorSessionAdmissionContext = new AsyncLocalStorage<SuccessorSessionAdmission>();
2798
2893
  #selectionFenceGenerationContext = new AsyncLocalStorage<number>();
2799
2894
  #selectionFenceTail: Promise<void> = Promise.resolve();
2895
+ #configurationReloadControllers = new Set<AbortController>();
2800
2896
  #pendingSelectionFences = 0;
2801
2897
  #selectionAwaitingMutationTransaction: symbol | undefined;
2802
2898
  #authorizedClosedSelectionTransaction: symbol | undefined;
@@ -3716,7 +3812,7 @@ export class AgentSession {
3716
3812
  }
3717
3813
  : undefined;
3718
3814
  const allowArtifact = ownedCompletion === undefined || isOwnedCompletionEnvelopeAllowed(ownedCompletion);
3719
- void formatParkedAsyncResult(this.sessionManager, disposition.text, allowArtifact)
3815
+ void formatAsyncResultForFollowUp(this.sessionManager, disposition.text, allowArtifact)
3720
3816
  .then(formattedResult => {
3721
3817
  const manager = this.#ownedAsyncJobManager;
3722
3818
  if (manager?.isDeliverySuppressed(job.id, job.generation)) {
@@ -4235,6 +4331,10 @@ export class AgentSession {
4235
4331
  * exposes an unowned history rewrite.
4236
4332
  */
4237
4333
  #sessionTransitionKind: string | undefined;
4334
+ // Detects transitions that begin and finish while an async preflight is staging.
4335
+ #sessionTransitionGeneration = 0;
4336
+ #sessionTransitionCompletion: Promise<void> = Promise.resolve();
4337
+ #resolveSessionTransitionCompletion: (() => void) | undefined;
4238
4338
  #queuedDeliveryPendingWhileTransition = false;
4239
4339
  #deferredAutoContinueDuringTransition: (() => void) | undefined;
4240
4340
  #coordinatorPersistGeneration = 0;
@@ -4312,11 +4412,24 @@ export class AgentSession {
4312
4412
  { code: "busy" },
4313
4413
  );
4314
4414
  }
4415
+ if (["new-session", "switch-session", "clear-context", "fork", "branch", "navigate-tree"].includes(kind)) {
4416
+ for (const controller of this.#configurationReloadControllers) controller.abort();
4417
+ }
4418
+ this.#sessionTransitionGeneration += 1;
4315
4419
  this.#sessionTransitionKind = kind;
4420
+ const completion = Promise.withResolvers<void>();
4421
+ this.#sessionTransitionCompletion = completion.promise;
4422
+ this.#resolveSessionTransitionCompletion = completion.resolve;
4316
4423
  this.#coordinatorPersistGeneration += 1;
4317
4424
  this.#wakeFollowUpReservationTransitionWaiters();
4318
4425
  }
4319
4426
 
4427
+ async #waitForSessionTransitionEnd(signal: AbortSignal): Promise<void> {
4428
+ while (this.#sessionTransitionKind !== undefined) {
4429
+ await awaitPromptInvocationPreflight(this.#sessionTransitionCompletion, signal);
4430
+ }
4431
+ }
4432
+
4320
4433
  async #settleActivePromptForHistoryTransition(): Promise<void> {
4321
4434
  if (this.#livePromptsInFlight() > 0 || this.agent.state.isStreaming) {
4322
4435
  await this.abort({ cause: "session_switch" });
@@ -4337,6 +4450,10 @@ export class AgentSession {
4337
4450
 
4338
4451
  #endSessionTransition(): void {
4339
4452
  this.#sessionTransitionKind = undefined;
4453
+ const resolveCompletion = this.#resolveSessionTransitionCompletion;
4454
+ this.#resolveSessionTransitionCompletion = undefined;
4455
+ this.#sessionTransitionCompletion = Promise.resolve();
4456
+ resolveCompletion?.();
4340
4457
  const deferredAutoContinue = this.#deferredAutoContinueDuringTransition;
4341
4458
  this.#deferredAutoContinueDuringTransition = undefined;
4342
4459
  if (!this.#isDisposed && !this.#sessionAdmissionClosing) deferredAutoContinue?.();
@@ -4476,6 +4593,7 @@ export class AgentSession {
4476
4593
  allowPromptContinuationReentry?: boolean;
4477
4594
  idleDelivery?: boolean;
4478
4595
  startupPromptWaiterRelease?: () => void;
4596
+ skipModelRegistryLease?: boolean;
4479
4597
  },
4480
4598
  ): Promise<T> {
4481
4599
  const owner = this.#sessionAdmissionContext.getStore();
@@ -4589,11 +4707,13 @@ export class AgentSession {
4589
4707
  { code: "busy" },
4590
4708
  );
4591
4709
  }
4592
-
4593
4710
  const release = () => {
4594
4711
  releaseEntry();
4595
4712
  };
4596
- return await this.#sessionAdmissionContext.run(entry, () => body({ release }));
4713
+ const admittedBody = () => this.#sessionAdmissionContext.run(entry, () => body({ release }));
4714
+ return await (options?.skipModelRegistryLease
4715
+ ? admittedBody()
4716
+ : this.#modelRegistry.withConsumerLease(admittedBody, signal));
4597
4717
  } finally {
4598
4718
  releaseEntry();
4599
4719
  }
@@ -5175,16 +5295,26 @@ export class AgentSession {
5175
5295
  }
5176
5296
 
5177
5297
  #flushPendingAgentEnd(): void {
5298
+ // Collect SDK agent_ends to publish before modifying the set to avoid iterator issues (PR #6388).
5299
+ // Publish only SDK agent_ends that have been decided by their handler.
5300
+ const sdkPendingsToPublish: Array<[AgentSessionEvent, RunResourceProducerLease | undefined]> = [];
5178
5301
  for (const sdkPending of this.#pendingSdkAgentEnds) {
5302
+ // Skip if the handler hasn't decided yet (decision is recorded by handler finally block).
5179
5303
  if (!this.#agentEndContinuationDecisions.has(sdkPending)) continue;
5304
+ // Also skip if it's being held for a continuation
5180
5305
  if ([...this.#pendingAgentEndContinuationHolds.values()].includes(sdkPending)) continue;
5306
+ const sdkLease = this.#deferredAgentEndLeases.get(sdkPending);
5307
+ sdkPendingsToPublish.push([sdkPending, sdkLease]);
5308
+ }
5309
+ // Now publish collected items.
5310
+ for (const [sdkPending, sdkLease] of sdkPendingsToPublish) {
5181
5311
  this.#pendingSdkAgentEnds.delete(sdkPending);
5182
5312
  if (this.#pendingAgentEndEmit === sdkPending) this.#pendingAgentEndEmit = undefined;
5183
- const sdkLease = this.#deferredAgentEndLeases.get(sdkPending);
5184
5313
  this.#deferredAgentEndLeases.delete(sdkPending);
5185
5314
  this.#startAgentEndPublication(sdkPending, sdkLease);
5186
5315
  }
5187
5316
  const pending = this.#pendingAgentEndEmit;
5317
+ // Skip fallback publish if there are still SDK agent_ends pending (they will be handled by the loop above once decided).
5188
5318
  if (pending && this.#pendingSdkAgentEnds.has(pending)) return;
5189
5319
  if (
5190
5320
  this.#livePromptsInFlight() > 0 ||
@@ -6237,7 +6367,7 @@ export class AgentSession {
6237
6367
  for (const name of previous) this.#toolRegistry.delete(name);
6238
6368
  const getCustomToolContext = () => this.#getCustomToolContext();
6239
6369
  const added: string[] = [];
6240
- for (const customTool of nextTools) {
6370
+ for (const customTool of omitPluginMcpNameShadows(nextTools)) {
6241
6371
  const wrapped = CustomToolAdapter.wrap(customTool, getCustomToolContext) as AgentTool;
6242
6372
  const finalTool = (
6243
6373
  this.#extensionRunner ? new ExtensionToolWrapper(wrapped, this.#extensionRunner) : wrapped
@@ -6245,11 +6375,15 @@ export class AgentSession {
6245
6375
  this.#toolRegistry.set(finalTool.name, finalTool);
6246
6376
  added.push(finalTool.name);
6247
6377
  }
6248
- if (options?.mandatoryMCPToolNames) {
6249
- this.#mandatoryMCPToolNames = new Set(
6250
- options.mandatoryMCPToolNames.map(name => name.toLowerCase()).filter(name => this.#toolRegistry.has(name)),
6251
- );
6252
- }
6378
+ const requestedMandatory = options?.mandatoryMCPToolNames ?? [...this.#mandatoryMCPToolNames];
6379
+ const retainedMandatory = retainPluginMcpMandatoryNames(nextTools, requestedMandatory).map(name =>
6380
+ name.toLowerCase(),
6381
+ );
6382
+ this.#mandatoryMCPToolNames = new Set(
6383
+ options?.mandatoryMCPToolNames !== undefined
6384
+ ? retainedMandatory.filter(name => this.#toolRegistry.has(name))
6385
+ : retainedMandatory,
6386
+ );
6253
6387
  this.#setDiscoverableMCPTools(this.#collectDiscoverableMCPToolsFromRegistry());
6254
6388
  await this.#applyActiveToolsByName(
6255
6389
  [
@@ -6886,7 +7020,11 @@ export class AgentSession {
6886
7020
  throw new Error("Owned async jobs did not settle before artifact retirement.");
6887
7021
  committed = true;
6888
7022
  } finally {
6889
- manager.finishOwnerSubagentShutdown(lease, committed ? "commit" : "release");
7023
+ if (committed) {
7024
+ manager.finishOwnerSubagentShutdown(lease, "commit");
7025
+ } else {
7026
+ manager.finishOwnerSubagentShutdown(lease, "release", { drainResumeQueue: !this.#isDisposed });
7027
+ }
6890
7028
  }
6891
7029
  }
6892
7030
 
@@ -7494,7 +7632,14 @@ export class AgentSession {
7494
7632
  const artifactId = await this.sessionManager.saveArtifact(fullText, "tool-result");
7495
7633
  if (!artifactId) return;
7496
7634
  const digest = crypto.createHash("sha256").update(fullText).digest("hex");
7497
- const preview = createPreAdmissionArtifactSpillPreview(fullText, artifactId, digest);
7635
+ // Cut the preview from redacted text. The artifact stores fullText.
7636
+ const obfuscator = this.#obfuscator;
7637
+ const preview = createPreAdmissionArtifactSpillPreview(
7638
+ fullText,
7639
+ artifactId,
7640
+ digest,
7641
+ obfuscator?.hasSecrets() ? text => obfuscator.obfuscate(text) : undefined,
7642
+ );
7498
7643
  const spillMeta = outputMeta()
7499
7644
  .truncationFromText(preview, {
7500
7645
  direction: "middle",
@@ -10457,6 +10602,8 @@ export class AgentSession {
10457
10602
  const publicationFailure = Object.assign(new Error("Session disposed before SDK terminal publication."), {
10458
10603
  code: "cancelled",
10459
10604
  });
10605
+ // Reject every SDK caller even if internal terminal or extension delivery is
10606
+ // already in flight; the publication task can still drain independently.
10460
10607
  for (const publication of this.#sdkTerminalPublications.values()) publication.reject(publicationFailure);
10461
10608
  this.#sdkTerminalPublications.clear();
10462
10609
  // Invalidate every coordinator event admitted before disposal. Handlers may
@@ -12337,7 +12484,7 @@ export class AgentSession {
12337
12484
  },
12338
12485
  });
12339
12486
 
12340
- for (const customTool of mcpTools) {
12487
+ for (const customTool of omitPluginMcpNameShadows(mcpTools)) {
12341
12488
  const wrapped = CustomToolAdapter.wrap(customTool, getCustomToolContext) as AgentTool;
12342
12489
  const finalTool = (
12343
12490
  this.#extensionRunner ? new ExtensionToolWrapper(wrapped, this.#extensionRunner) : wrapped
@@ -12346,11 +12493,15 @@ export class AgentSession {
12346
12493
  }
12347
12494
 
12348
12495
  this.#setDiscoverableMCPTools(this.#collectDiscoverableMCPToolsFromRegistry());
12349
- if (options.mandatoryMCPToolNames !== undefined) {
12350
- this.#mandatoryMCPToolNames = new Set(
12351
- options.mandatoryMCPToolNames.map(name => name.toLowerCase()).filter(name => this.#toolRegistry.has(name)),
12352
- );
12353
- }
12496
+ const requestedMandatory = options.mandatoryMCPToolNames ?? [...this.#mandatoryMCPToolNames];
12497
+ const retainedMandatory = retainPluginMcpMandatoryNames(mcpTools, requestedMandatory).map(name =>
12498
+ name.toLowerCase(),
12499
+ );
12500
+ this.#mandatoryMCPToolNames = new Set(
12501
+ options.mandatoryMCPToolNames !== undefined
12502
+ ? retainedMandatory.filter(name => this.#toolRegistry.has(name))
12503
+ : retainedMandatory,
12504
+ );
12354
12505
  this.#pruneSelectedMCPToolNames();
12355
12506
  const hasPersistedMCPToolSelection = this.buildDisplaySessionContext().hasPersistedMCPToolSelection;
12356
12507
  if (options.selectedMCPToolNames) {
@@ -17840,7 +17991,7 @@ export class AgentSession {
17840
17991
  "Unable to confirm owned subagent cleanup; session was not replaced. Wait for or inspect remaining subagents, then retry /new.",
17841
17992
  "new-session-subagent-cleanup",
17842
17993
  );
17843
- manager.finishOwnerSubagentShutdown(lease, "release");
17994
+ manager.finishOwnerSubagentShutdown(lease, "release", { drainResumeQueue: !this.#isDisposed });
17844
17995
  return false;
17845
17996
  }
17846
17997
  } catch {
@@ -17849,7 +18000,7 @@ export class AgentSession {
17849
18000
  "Unable to confirm owned subagent cleanup; session was not replaced. Wait for or inspect remaining subagents, then retry /new.",
17850
18001
  "new-session-subagent-cleanup",
17851
18002
  );
17852
- manager.finishOwnerSubagentShutdown(lease, "release");
18003
+ manager.finishOwnerSubagentShutdown(lease, "release", { drainResumeQueue: !this.#isDisposed });
17853
18004
  return false;
17854
18005
  }
17855
18006
 
@@ -17907,10 +18058,11 @@ export class AgentSession {
17907
18058
  manager.finishOwnerSubagentShutdown(lease, "commit");
17908
18059
  return true;
17909
18060
  } catch (error) {
17910
- manager.finishOwnerSubagentShutdown(
17911
- lease,
17912
- this.sessionManager.getSessionId() !== previousSessionIdentity ? "commit" : "release",
17913
- );
18061
+ if (this.sessionManager.getSessionId() !== previousSessionIdentity) {
18062
+ manager.finishOwnerSubagentShutdown(lease, "commit");
18063
+ } else {
18064
+ manager.finishOwnerSubagentShutdown(lease, "release", { drainResumeQueue: !this.#isDisposed });
18065
+ }
17914
18066
  throw error;
17915
18067
  }
17916
18068
  }
@@ -17949,7 +18101,7 @@ export class AgentSession {
17949
18101
  if (this.#activeModelProfile && this.#activeModelProfile !== configuredDefaultProfileIdentity) {
17950
18102
  this.settings.clearOverride("modelRoles");
17951
18103
  this.settings.clearOverride("task.agentModelOverrides");
17952
- this.#activeModelProfile = undefined;
18104
+ this.setActiveModelProfile(undefined);
17953
18105
  }
17954
18106
  const inheritedThinkingLevel = resolveThinkingLevelForModel(this.model, this.#getInheritedThinkingLevel());
17955
18107
  this.#thinkingLevelMutationRevision++;
@@ -18245,6 +18397,7 @@ export class AgentSession {
18245
18397
  role,
18246
18398
  this.#formatRoleModelValue(role, model, options?.selector, options?.thinkingLevel),
18247
18399
  );
18400
+ if (cause === "user-selection") this.markProfileRoleOverrideManual("modelRoles", role);
18248
18401
  // Only an explicit user selection starts a new fallback epoch. Internal
18249
18402
  // fallback switches must preserve the exhausted-model set while advancing.
18250
18403
  if (role === "default" && cause === "user-selection") {
@@ -18283,12 +18436,22 @@ export class AgentSession {
18283
18436
  }
18284
18437
 
18285
18438
  setActiveModelProfile(name: string | undefined): void {
18439
+ if (name !== this.#activeModelProfile) {
18440
+ this.#activeProfileManualModelRoles.clear();
18441
+ this.#activeProfileManualAgentModelOverrides.clear();
18442
+ }
18286
18443
  this.#activeModelProfile = name;
18287
18444
  if (name !== undefined) {
18288
18445
  this.#unavailableModelProfile = undefined;
18289
18446
  }
18290
18447
  }
18291
18448
 
18449
+ markProfileRoleOverrideManual(path: "modelRoles" | "task.agentModelOverrides", role: string): void {
18450
+ if (this.#activeModelProfile === undefined) return;
18451
+ if (path === "modelRoles") this.#activeProfileManualModelRoles.add(role);
18452
+ else this.#activeProfileManualAgentModelOverrides.add(role);
18453
+ }
18454
+
18292
18455
  getActiveModelProfile(): string | undefined {
18293
18456
  return this.#activeModelProfile;
18294
18457
  }
@@ -18319,6 +18482,592 @@ export class AgentSession {
18319
18482
  this.#userModelSelectionRevision++;
18320
18483
  }
18321
18484
 
18485
+ getConfigurationPaths(): ConfigHotReloadPaths | undefined {
18486
+ const configPath = this.settings.getGlobalConfigPath();
18487
+ const modelsPath = this.#modelRegistry.getModelsConfigPath();
18488
+ if (!configPath || !modelsPath) return undefined;
18489
+ return {
18490
+ configPath: path.resolve(configPath),
18491
+ modelsPath: path.resolve(modelsPath),
18492
+ };
18493
+ }
18494
+
18495
+ #assertConfigurationReloadAvailable(candidate: ConfigHotReloadCandidate): void {
18496
+ if (this.#sessionAdmissionClosing || this.#sessionAdmissionClosed || this.#isDisposed) {
18497
+ throw new ConfigurationReloadError("SESSION_UNAVAILABLE");
18498
+ }
18499
+ const paths = this.getConfigurationPaths();
18500
+ if (
18501
+ !paths ||
18502
+ path.resolve(candidate.config.path) !== paths.configPath ||
18503
+ path.resolve(candidate.models.path) !== paths.modelsPath
18504
+ ) {
18505
+ throw new ConfigurationReloadError(paths ? "PATH_CHANGED" : "SESSION_UNAVAILABLE");
18506
+ }
18507
+ }
18508
+
18509
+ #readOnlyModelsCandidate(candidate: ModelsConfigReloadCandidate): ModelRegistry {
18510
+ const liveRegistry = this.#modelRegistry;
18511
+ return new Proxy(candidate.registry, {
18512
+ get(target, property) {
18513
+ if (property === "getSessionCanonicalVariant") {
18514
+ return liveRegistry.getSessionCanonicalVariant.bind(liveRegistry);
18515
+ }
18516
+ if (
18517
+ property === "clearCanonicalVariant" ||
18518
+ property === "restoreSessionCanonicalVariant" ||
18519
+ property === "seedCanonicalVariant"
18520
+ ) {
18521
+ return () => undefined;
18522
+ }
18523
+ const value = Reflect.get(target, property, target);
18524
+ return typeof value === "function" ? value.bind(target) : value;
18525
+ },
18526
+ }) as ModelRegistry;
18527
+ }
18528
+
18529
+ #settingsCandidateView(candidate: SettingsGlobalConfigCandidate) {
18530
+ return {
18531
+ get: <P extends SettingPath>(settingPath: P) => candidate.get(settingPath),
18532
+ getGlobal: <P extends SettingPath>(settingPath: P) => candidate.getGlobal(settingPath),
18533
+ getOverride: <P extends SettingPath>(settingPath: P) => this.settings.getOverride(settingPath),
18534
+ getStorage: () => this.settings.getStorage(),
18535
+ };
18536
+ }
18537
+
18538
+ async #preflightActiveProfile(
18539
+ settingsCandidate: SettingsGlobalConfigCandidate,
18540
+ modelsCandidate: ModelsConfigReloadCandidate,
18541
+ options?: {
18542
+ prepareSessionModelSelection?: boolean;
18543
+ settingsForApply?: Settings;
18544
+ signal?: AbortSignal;
18545
+ },
18546
+ ): Promise<PreparedModelProfileActivation | undefined> {
18547
+ const activeProfile = this.getActiveModelProfile();
18548
+ if (!activeProfile) return undefined;
18549
+ try {
18550
+ return await prepareModelProfileActivation({
18551
+ session: this,
18552
+ modelRegistry: this.#readOnlyModelsCandidate(modelsCandidate),
18553
+ settings: this.#settingsCandidateView(settingsCandidate),
18554
+ ...(options?.settingsForApply ? { settingsForApply: options.settingsForApply } : {}),
18555
+ ...(options?.prepareSessionModelSelection ? { prepareSessionModelSelection: true } : {}),
18556
+ preserveManualOverrides: true,
18557
+ preserveCanonicalVariant: true,
18558
+ ...(options?.signal ? { signal: options.signal } : {}),
18559
+ profileName: activeProfile,
18560
+ });
18561
+ } catch (error) {
18562
+ throw new ConfigurationReloadError("ACTIVE_PROFILE_INVALID", error);
18563
+ }
18564
+ }
18565
+
18566
+ async validateConfiguration(candidate: ConfigHotReloadCandidate): Promise<void> {
18567
+ while (true) {
18568
+ if (this.#sessionTransitionKind !== undefined) {
18569
+ await this.#waitForSessionTransitionEnd(this.#disposeAbortController.signal);
18570
+ }
18571
+ this.#assertConfigurationReloadAvailable(candidate);
18572
+ const transitionGeneration = this.#sessionTransitionGeneration;
18573
+ let modelsCandidate: ModelsConfigReloadCandidate | undefined;
18574
+ let validationError: ConfigurationReloadError | undefined;
18575
+ try {
18576
+ let settingsCandidate: SettingsGlobalConfigCandidate;
18577
+ try {
18578
+ settingsCandidate = await this.settings.stageGlobalConfigReload(candidate.config);
18579
+ } catch (error) {
18580
+ throw new ConfigurationReloadError("SETTINGS_INVALID", error);
18581
+ }
18582
+ if (!settingsCandidate.diagnostics.valid) throw new ConfigurationReloadError("SETTINGS_INVALID");
18583
+ try {
18584
+ modelsCandidate = await this.#modelRegistry.stageModelsConfigReload(
18585
+ candidate.models,
18586
+ settingsCandidate,
18587
+ );
18588
+ } catch (error) {
18589
+ throw new ConfigurationReloadError("MODELS_INVALID", error);
18590
+ }
18591
+ if (!modelsCandidate.valid || !modelsCandidate.diagnostics.valid) {
18592
+ throw new ConfigurationReloadError("MODELS_INVALID");
18593
+ }
18594
+ const prepared = await this.#preflightActiveProfile(settingsCandidate, modelsCandidate);
18595
+ const chain = this.getConfiguredModelChainState("default");
18596
+ if (!prepared || chain?.origin !== "profile-activation" || chain.identity !== this.getActiveModelProfile()) {
18597
+ this.#updatedCurrentModel(modelsCandidate.registry);
18598
+ }
18599
+ } catch (error) {
18600
+ validationError =
18601
+ error instanceof ConfigurationReloadError
18602
+ ? error
18603
+ : new ConfigurationReloadError("ACTIVE_PROFILE_INVALID", error);
18604
+ } finally {
18605
+ if (modelsCandidate) {
18606
+ try {
18607
+ modelsCandidate.rollback();
18608
+ } catch (error) {
18609
+ validationError ??= new ConfigurationReloadError("VALIDATION_CLEANUP_FAILED", error);
18610
+ }
18611
+ }
18612
+ }
18613
+
18614
+ // A transition may have completed while staging, so checking only the active kind is insufficient.
18615
+ if (transitionGeneration !== this.#sessionTransitionGeneration) {
18616
+ await this.#waitForSessionTransitionEnd(this.#disposeAbortController.signal);
18617
+ this.#assertConfigurationReloadAvailable(candidate);
18618
+ continue;
18619
+ }
18620
+
18621
+ this.#assertConfigurationReloadAvailable(candidate);
18622
+ if (validationError) throw validationError;
18623
+ return;
18624
+ }
18625
+ }
18626
+
18627
+ reloadConfiguration(
18628
+ candidate: ConfigHotReloadCandidate,
18629
+ signal: AbortSignal,
18630
+ ): Promise<ConfigurationReloadResult> {
18631
+ return this.#reloadConfiguration(
18632
+ candidate,
18633
+ signal,
18634
+ this.getUserModelSelectionRevision(),
18635
+ this.model,
18636
+ );
18637
+ }
18638
+
18639
+ async #reloadConfiguration(
18640
+ candidate: ConfigHotReloadCandidate,
18641
+ signal: AbortSignal,
18642
+ userModelSelectionRevision: number,
18643
+ userModelSelectionModel: Model | undefined,
18644
+ ): Promise<ConfigurationReloadResult> {
18645
+ const owner = this.#sessionAdmissionContext.getStore();
18646
+ if (owner && !owner.released) throw new ConfigurationReloadError("SESSION_UNAVAILABLE");
18647
+ if (this.#sessionTransitionKind !== undefined) {
18648
+ await this.#waitForSessionTransitionEnd(AbortSignal.any([signal, this.#disposeAbortController.signal]));
18649
+ }
18650
+ this.#assertConfigurationReloadAvailable(candidate);
18651
+
18652
+ const transitionAbortController = new AbortController();
18653
+ this.#configurationReloadControllers.add(transitionAbortController);
18654
+ const reloadSignal = AbortSignal.any([
18655
+ signal,
18656
+ this.#disposeAbortController.signal,
18657
+ transitionAbortController.signal,
18658
+ ]);
18659
+ const expectedSessionId = this.sessionId;
18660
+ const selectionTransaction = Symbol("configuration-reload");
18661
+ const priorSelectionFence = this.#selectionFenceTail;
18662
+ const selectionFence = Promise.withResolvers<void>();
18663
+ this.#selectionFenceGeneration += 1;
18664
+ this.#pendingSelectionFences += 1;
18665
+ const selectionFenceGeneration = this.#selectionFenceGeneration;
18666
+ if (this.#oldestPendingSelectionFenceGeneration === 0) {
18667
+ this.#oldestPendingSelectionFenceGeneration = selectionFenceGeneration;
18668
+ }
18669
+ this.#selectionFenceTail = priorSelectionFence.then(() => selectionFence.promise);
18670
+ void this.#selectionFenceTail.catch(() => {});
18671
+ let publicationFenceRelease: (() => void) | undefined;
18672
+ let stagedModels: ModelsConfigReloadCandidate | undefined;
18673
+ let modelRollbackAttempted = false;
18674
+ let modelCommitSucceeded = false;
18675
+ let retryAfterSessionTransition = false;
18676
+ try {
18677
+ reloadSignal.throwIfAborted();
18678
+ await awaitPromptInvocationPreflight(priorSelectionFence, reloadSignal);
18679
+ await this.#withSessionAdmission("selection", async () => {}, reloadSignal, undefined, {
18680
+ selectionTransaction,
18681
+ skipModelRegistryLease: true,
18682
+ });
18683
+ if (this.sessionId !== expectedSessionId) throw new Error("Session changed during configuration reload");
18684
+ this.#selectionAwaitingMutationTransaction = selectionTransaction;
18685
+ await awaitPromptInvocationPreflight(this.waitForIdle(selectionFenceGeneration), reloadSignal);
18686
+
18687
+ return await this.#withSessionAdmission(
18688
+ "selection",
18689
+ async () => {
18690
+ reloadSignal.throwIfAborted();
18691
+ if (this.sessionId !== expectedSessionId) {
18692
+ throw new ConfigurationReloadError("SESSION_UNAVAILABLE");
18693
+ }
18694
+ this.#assertConfigurationReloadAvailable(candidate);
18695
+ let stagedSettings: SettingsGlobalConfigCandidate;
18696
+ try {
18697
+ // Stage after admission and idle drain so settings written by the prior turn are rebased.
18698
+ stagedSettings = await this.settings.stageGlobalConfigReload(candidate.config);
18699
+ } catch (error) {
18700
+ throw new ConfigurationReloadError("SETTINGS_INVALID", error);
18701
+ }
18702
+ if (!stagedSettings.diagnostics.valid) throw new ConfigurationReloadError("SETTINGS_INVALID");
18703
+ try {
18704
+ stagedModels = await this.#modelRegistry.stageModelsConfigReload(
18705
+ candidate.models,
18706
+ stagedSettings,
18707
+ );
18708
+ } catch (error) {
18709
+ throw new ConfigurationReloadError("MODELS_INVALID", error);
18710
+ }
18711
+ if (!stagedModels.valid || !stagedModels.diagnostics.valid) {
18712
+ throw new ConfigurationReloadError("MODELS_INVALID");
18713
+ }
18714
+ reloadSignal.throwIfAborted();
18715
+
18716
+ const changedSettings = (Object.keys(SETTINGS_SCHEMA) as SettingPath[]).filter(
18717
+ settingPath =>
18718
+ !util.isDeepStrictEqual(this.settings.get(settingPath), stagedSettings.get(settingPath)) ||
18719
+ !util.isDeepStrictEqual(
18720
+ this.settings.getGlobal(settingPath),
18721
+ stagedSettings.getGlobal(settingPath),
18722
+ ),
18723
+ );
18724
+ let prepared: PreparedModelProfileActivation | undefined;
18725
+ let preparedLiveModelSelection: PreparedProfileModelSelection | undefined;
18726
+ let preparedLiveModelSelectionRevision = userModelSelectionRevision;
18727
+ let preserveDefaultModelSelection = true;
18728
+ const prepareCandidateSelections = async (modelsCandidate: ModelsConfigReloadCandidate): Promise<void> => {
18729
+ prepared = undefined;
18730
+ preparedLiveModelSelection = undefined;
18731
+ preparedLiveModelSelectionRevision = userModelSelectionRevision;
18732
+ preserveDefaultModelSelection = true;
18733
+ const activeProfile = this.getActiveModelProfile();
18734
+ if (activeProfile) {
18735
+ prepared = await this.#preflightActiveProfile(stagedSettings, modelsCandidate, {
18736
+ prepareSessionModelSelection: true,
18737
+ signal: reloadSignal,
18738
+ settingsForApply: this.settings,
18739
+ });
18740
+ const previousChain = prepared!.previousDefaultChainState;
18741
+ const profileOwnsDefaultChain =
18742
+ previousChain?.origin === "profile-activation" && previousChain.identity === activeProfile;
18743
+ const chainIsUnchanged =
18744
+ prepared!.defaultChain.length === (previousChain?.entries.length ?? 0) &&
18745
+ prepared!.defaultChain.every((selector, index) => selector === previousChain?.entries[index]);
18746
+ preserveDefaultModelSelection =
18747
+ prepared!.defaultChain.length === 0 || !profileOwnsDefaultChain || chainIsUnchanged;
18748
+ }
18749
+ if (
18750
+ preserveDefaultModelSelection &&
18751
+ this.#currentModelConfigurationChanged(modelsCandidate.registry)
18752
+ ) {
18753
+ const updatedModel = this.#updatedCurrentModel(modelsCandidate.registry);
18754
+ if (updatedModel) {
18755
+ const previousModel = this.model;
18756
+ const previousThinkingLevel = this.thinkingLevel;
18757
+ const selectionRevision = this.getUserModelSelectionRevision();
18758
+ const preparedSelection = await this.prepareModelSelectionForProfileActivation(
18759
+ updatedModel,
18760
+ previousThinkingLevel,
18761
+ reloadSignal,
18762
+ );
18763
+ if (
18764
+ this.getUserModelSelectionRevision() === selectionRevision &&
18765
+ this.model === previousModel &&
18766
+ this.thinkingLevel === previousThinkingLevel
18767
+ ) {
18768
+ preparedLiveModelSelectionRevision = selectionRevision;
18769
+ preparedLiveModelSelection = preparedSelection;
18770
+ }
18771
+ }
18772
+ }
18773
+ };
18774
+ await prepareCandidateSelections(stagedModels);
18775
+ reloadSignal.throwIfAborted();
18776
+ publicationFenceRelease = await this.#modelRegistry.acquirePublicationFence(reloadSignal);
18777
+ reloadSignal.throwIfAborted();
18778
+ let preserveLiveModelSelection = false;
18779
+ const prepareLiveModelSelection = async (modelsCandidate: ModelsConfigReloadCandidate): Promise<void> => {
18780
+ preserveLiveModelSelection =
18781
+ this.getUserModelSelectionRevision() !== userModelSelectionRevision ||
18782
+ this.model !== userModelSelectionModel;
18783
+ if (
18784
+ preparedLiveModelSelection &&
18785
+ (this.getUserModelSelectionRevision() !== preparedLiveModelSelectionRevision ||
18786
+ this.model !== preparedLiveModelSelection.previousModel ||
18787
+ this.thinkingLevel !== preparedLiveModelSelection.previousThinkingLevel)
18788
+ ) {
18789
+ preparedLiveModelSelection = undefined;
18790
+ }
18791
+ if (preserveLiveModelSelection && this.model) {
18792
+ const currentModel = this.model;
18793
+ const currentThinkingLevel = this.thinkingLevel;
18794
+ const updatedModel = this.#updatedCurrentModel(modelsCandidate.registry);
18795
+ if (!updatedModel) {
18796
+ throw new ConfigurationReloadError(
18797
+ "MODEL_UNAVAILABLE",
18798
+ new Error("The current model is unavailable in the staged registry"),
18799
+ );
18800
+ }
18801
+ if (!util.isDeepStrictEqual(currentModel, updatedModel)) {
18802
+ const selectionRevision = this.getUserModelSelectionRevision();
18803
+ preparedLiveModelSelection = await this.prepareModelSelectionForProfileActivation(
18804
+ updatedModel,
18805
+ currentThinkingLevel,
18806
+ reloadSignal,
18807
+ );
18808
+ preparedLiveModelSelectionRevision = selectionRevision;
18809
+ reloadSignal.throwIfAborted();
18810
+ if (
18811
+ this.getUserModelSelectionRevision() !== selectionRevision ||
18812
+ this.model !== currentModel ||
18813
+ this.thinkingLevel !== currentThinkingLevel
18814
+ ) {
18815
+ throw new ConfigurationReloadError(
18816
+ "PUBLICATION_FAILED",
18817
+ new Error("The live model selection changed while preparing the reload model"),
18818
+ );
18819
+ }
18820
+ }
18821
+ }
18822
+ };
18823
+ let catalogRestages = 0;
18824
+ for (;;) {
18825
+ const modelsCandidate = stagedModels;
18826
+ if (!modelsCandidate) throw new ConfigurationReloadError("MODELS_INVALID");
18827
+ if (!modelsCandidate.isCurrent()) {
18828
+ if (catalogRestages >= 2) {
18829
+ throw new ConfigurationReloadError(
18830
+ "PUBLICATION_FAILED",
18831
+ new Error("The model catalog kept changing during configuration reload preflight"),
18832
+ { retryable: true },
18833
+ );
18834
+ }
18835
+ catalogRestages++;
18836
+ modelsCandidate.rollback();
18837
+ stagedModels = undefined;
18838
+ let restagedModels: ModelsConfigReloadCandidate;
18839
+ try {
18840
+ restagedModels = await this.#modelRegistry.stageModelsConfigReload(
18841
+ candidate.models,
18842
+ stagedSettings,
18843
+ );
18844
+ } catch (error) {
18845
+ throw new ConfigurationReloadError("MODELS_INVALID", error);
18846
+ }
18847
+ stagedModels = restagedModels;
18848
+ if (!restagedModels.valid || !restagedModels.diagnostics.valid) {
18849
+ throw new ConfigurationReloadError("MODELS_INVALID");
18850
+ }
18851
+ await prepareCandidateSelections(restagedModels);
18852
+ reloadSignal.throwIfAborted();
18853
+ continue;
18854
+ }
18855
+ await prepareLiveModelSelection(modelsCandidate);
18856
+ reloadSignal.throwIfAborted();
18857
+ if (modelsCandidate.isCurrent()) break;
18858
+ }
18859
+ const modelsChanged = stagedModels.changed;
18860
+ const settingsChanged = changedSettings.length > 0;
18861
+ if (!settingsChanged && !modelsChanged) {
18862
+ stagedModels!.rollback();
18863
+ modelRollbackAttempted = true;
18864
+ return { applied: false, settingsChanged: false, modelsChanged: false, changedSettings: [] };
18865
+ }
18866
+
18867
+ let publication: SettingsConfigReloadPublication | undefined;
18868
+ let modelCommitStarted = false;
18869
+ let activationPublicationStarted = false;
18870
+ let liveModelSelectionCommitted = false;
18871
+ let liveModelSelectionCommitFinished = false;
18872
+ let committedThinkingLevelLiveMutationRevision: number | undefined;
18873
+ try {
18874
+ publication = this.settings.publishGlobalConfigReload(stagedSettings, () => {
18875
+ modelCommitStarted = true;
18876
+ stagedModels!.commit();
18877
+ if (prepared) {
18878
+ activationPublicationStarted = true;
18879
+ publishPreparedModelProfileActivation(prepared, {
18880
+ preserveDefaultModelSelection,
18881
+ preserveLiveModelSelection,
18882
+ });
18883
+ }
18884
+ if (preparedLiveModelSelection) {
18885
+ this.#assertPreparedProfileModelSelectionCurrent(preparedLiveModelSelection);
18886
+ liveModelSelectionCommitted = true;
18887
+ this.commitPreparedProfileModelSelection(
18888
+ preparedLiveModelSelection,
18889
+ mutationRevision => {
18890
+ committedThinkingLevelLiveMutationRevision = mutationRevision;
18891
+ },
18892
+ );
18893
+ liveModelSelectionCommitFinished = true;
18894
+ }
18895
+ });
18896
+ stagedModels.finalize();
18897
+ publication.finalize();
18898
+ modelCommitSucceeded = true;
18899
+ } catch (error) {
18900
+ const rollbackErrors: unknown[] = [];
18901
+ if (liveModelSelectionCommitted && committedThinkingLevelLiveMutationRevision === undefined) {
18902
+ committedThinkingLevelLiveMutationRevision = this.#thinkingLevelLiveMutationRevision;
18903
+ }
18904
+ try {
18905
+ publication?.rollback();
18906
+ } catch (rollbackError) {
18907
+ rollbackErrors.push(rollbackError);
18908
+ }
18909
+ if (prepared && activationPublicationStarted) {
18910
+ try {
18911
+ await rollbackPreparedModelProfileActivation(prepared, {
18912
+ preserveDefaultModelSelection,
18913
+ preserveLiveModelSelection,
18914
+ isCurrent: () => this.getUserModelSelectionRevision() === userModelSelectionRevision,
18915
+ });
18916
+ } catch (rollbackError) {
18917
+ rollbackErrors.push(rollbackError);
18918
+ }
18919
+ }
18920
+ const thinkingLevelMutationSuperseded =
18921
+ committedThinkingLevelLiveMutationRevision !== undefined &&
18922
+ this.#thinkingLevelLiveMutationRevision !== committedThinkingLevelLiveMutationRevision;
18923
+ if (
18924
+ liveModelSelectionCommitted &&
18925
+ preparedLiveModelSelection &&
18926
+ this.getUserModelSelectionRevision() === preparedLiveModelSelectionRevision &&
18927
+ this.model === preparedLiveModelSelection.model &&
18928
+ (!liveModelSelectionCommitFinished ||
18929
+ thinkingLevelMutationSuperseded ||
18930
+ this.thinkingLevel === preparedLiveModelSelection.committedThinkingLevel)
18931
+ ) {
18932
+ try {
18933
+ await this.restoreModelSelectionForRollback(
18934
+ preparedLiveModelSelection.previousModel,
18935
+ thinkingLevelMutationSuperseded
18936
+ ? this.thinkingLevel
18937
+ : preparedLiveModelSelection.previousThinkingLevel,
18938
+ );
18939
+ } catch (rollbackError) {
18940
+ rollbackErrors.push(rollbackError);
18941
+ }
18942
+ }
18943
+ if (modelCommitStarted) {
18944
+ const selectedModelBeforeRollback = this.model;
18945
+ const selectedThinkingLevelBeforeRollback = this.thinkingLevel;
18946
+ const selectionRevisionBeforeRollback = this.getUserModelSelectionRevision();
18947
+ try {
18948
+ stagedModels!.rollback();
18949
+ modelRollbackAttempted = true;
18950
+ if (
18951
+ selectedModelBeforeRollback &&
18952
+ this.model === selectedModelBeforeRollback &&
18953
+ this.getUserModelSelectionRevision() === selectionRevisionBeforeRollback
18954
+ ) {
18955
+ const restoredModel = this.#modelRegistry
18956
+ .getAvailable()
18957
+ .find(
18958
+ model =>
18959
+ model.provider === selectedModelBeforeRollback.provider &&
18960
+ model.id === selectedModelBeforeRollback.id,
18961
+ );
18962
+ if (restoredModel && !util.isDeepStrictEqual(selectedModelBeforeRollback, restoredModel)) {
18963
+ await this.restoreModelSelectionForRollback(
18964
+ restoredModel,
18965
+ selectedThinkingLevelBeforeRollback,
18966
+ );
18967
+ }
18968
+ }
18969
+ } catch (rollbackError) {
18970
+ rollbackErrors.push(rollbackError);
18971
+ modelRollbackAttempted = true;
18972
+ }
18973
+ }
18974
+ if (rollbackErrors.length > 0) {
18975
+ throw new ConfigurationReloadError(
18976
+ "PUBLICATION_FAILED",
18977
+ new AggregateError([error, ...rollbackErrors]),
18978
+ );
18979
+ }
18980
+ throw new ConfigurationReloadError("PUBLICATION_FAILED", error);
18981
+ } finally {
18982
+ publicationFenceRelease?.();
18983
+ publicationFenceRelease = undefined;
18984
+ }
18985
+ const preparedActivation = prepared;
18986
+ if (preparedActivation) {
18987
+ await this.#modelRegistry.withConsumerLease(async () => {
18988
+ await finishPreparedModelProfileActivation(preparedActivation);
18989
+ });
18990
+ }
18991
+ if (preparedLiveModelSelection) {
18992
+ try {
18993
+ await this.finishPreparedProfileModelSelection(preparedLiveModelSelection);
18994
+ } catch (error) {
18995
+ logger.warn("Failed to refresh session tools after model catalog reload", {
18996
+ error: error instanceof Error ? error.message : String(error),
18997
+ });
18998
+ }
18999
+ }
19000
+ return {
19001
+ applied: true,
19002
+ settingsChanged,
19003
+ modelsChanged,
19004
+ changedSettings,
19005
+ };
19006
+ },
19007
+ reloadSignal,
19008
+ undefined,
19009
+ { selectionTransaction, skipModelRegistryLease: true },
19010
+ );
19011
+ } catch (error) {
19012
+ if (stagedModels && !modelRollbackAttempted && !modelCommitSucceeded) {
19013
+ try {
19014
+ stagedModels.rollback();
19015
+ } catch (rollbackError) {
19016
+ throw new ConfigurationReloadError("PUBLICATION_FAILED", new AggregateError([error, rollbackError]));
19017
+ }
19018
+ }
19019
+ if (
19020
+ transitionAbortController.signal.aborted &&
19021
+ !signal.aborted &&
19022
+ !this.#disposeAbortController.signal.aborted
19023
+ ) {
19024
+ retryAfterSessionTransition = true;
19025
+ } else if (error instanceof ConfigurationReloadError) {
19026
+ throw error;
19027
+ } else {
19028
+ throw new ConfigurationReloadError("PUBLICATION_FAILED", error);
19029
+ }
19030
+ } finally {
19031
+ publicationFenceRelease?.();
19032
+ if (this.#selectionAwaitingMutationTransaction === selectionTransaction) {
19033
+ this.#selectionAwaitingMutationTransaction = undefined;
19034
+ }
19035
+ selectionFence.resolve();
19036
+ this.#pendingSelectionFences -= 1;
19037
+ if (this.#oldestPendingSelectionFenceGeneration === selectionFenceGeneration) {
19038
+ this.#oldestPendingSelectionFenceGeneration = 0;
19039
+ }
19040
+ this.#configurationReloadControllers.delete(transitionAbortController);
19041
+ this.#resolveSessionSettlement();
19042
+ }
19043
+ if (retryAfterSessionTransition) {
19044
+ await this.#waitForSessionTransitionEnd(AbortSignal.any([signal, this.#disposeAbortController.signal]));
19045
+ return await this.#reloadConfiguration(
19046
+ candidate,
19047
+ signal,
19048
+ userModelSelectionRevision,
19049
+ userModelSelectionModel,
19050
+ );
19051
+ }
19052
+ throw new Error("Configuration reload exited without a result or a session-transition retry.");
19053
+ }
19054
+
19055
+ #updatedCurrentModel(registry: ModelRegistry): Model | undefined {
19056
+ const current = this.model;
19057
+ if (!current) return undefined;
19058
+ const updated = registry
19059
+ .getAvailable()
19060
+ .find(model => model.provider === current.provider && model.id === current.id);
19061
+ if (!updated) throw new ConfigurationReloadError("MODEL_UNAVAILABLE");
19062
+ return updated;
19063
+ }
19064
+
19065
+ #currentModelConfigurationChanged(registry: ModelRegistry): boolean {
19066
+ const current = this.model;
19067
+ const updated = this.#updatedCurrentModel(registry);
19068
+ return current !== undefined && updated !== undefined && !util.isDeepStrictEqual(current, updated);
19069
+ }
19070
+
18322
19071
  /**
18323
19072
  * Re-apply vendor-separated delegation after a profile activation changed the
18324
19073
  * role layer. `eagerTasks` is resolved at prompt build time, but under
@@ -18412,10 +19161,28 @@ export class AgentSession {
18412
19161
  const modelRoles = { ...this.settings.get("modelRoles") };
18413
19162
  const agentOverrides = { ...this.settings.get("task.agentModelOverrides") };
18414
19163
  for (const [role, baseline] of this.#activeProfileInstalledRoles) {
19164
+ const installed = this.#activeProfileInstalledRoleValues.get(role);
19165
+ if (
19166
+ installed !== undefined &&
19167
+ !util.isDeepStrictEqual(
19168
+ normalizeModelSelectorValue(modelRoles[role]),
19169
+ normalizeModelSelectorValue(installed),
19170
+ )
19171
+ )
19172
+ continue;
18415
19173
  if (role === "default" || baseline === undefined) delete modelRoles[role];
18416
19174
  else modelRoles[role] = baseline;
18417
19175
  }
18418
19176
  for (const [role, baseline] of this.#activeProfileInstalledAgentOverrides) {
19177
+ const installed = this.#activeProfileInstalledAgentOverrideValues.get(role);
19178
+ if (
19179
+ installed !== undefined &&
19180
+ !util.isDeepStrictEqual(
19181
+ normalizeModelSelectorValue(agentOverrides[role]),
19182
+ normalizeModelSelectorValue(installed),
19183
+ )
19184
+ )
19185
+ continue;
18419
19186
  if (baseline === undefined) delete agentOverrides[role];
18420
19187
  else agentOverrides[role] = baseline;
18421
19188
  }
@@ -18423,6 +19190,9 @@ export class AgentSession {
18423
19190
  this.settings.override("task.agentModelOverrides", agentOverrides);
18424
19191
  this.#activeProfileInstalledRoles.clear();
18425
19192
  this.#activeProfileInstalledAgentOverrides.clear();
19193
+ this.#activeProfileInstalledRoleValues.clear();
19194
+ this.#activeProfileInstalledAgentOverrideValues.clear();
19195
+ this.#activeProfileConfiguredAgentModelOverrides.clear();
18426
19196
  }
18427
19197
  if (hadInstalledKeys || this.getActiveModelProfile() !== undefined) {
18428
19198
  this.#modelRegistry.reapplyConfiguredModelBindings(this.settings);
@@ -18440,31 +19210,106 @@ export class AgentSession {
18440
19210
  modelRoles: readonly string[],
18441
19211
  agentModelOverrides: readonly string[],
18442
19212
  preProfileModel: Model | undefined,
19213
+ baseline?: {
19214
+ modelRoles: Readonly<Record<string, ModelSelectorValue>>;
19215
+ agentModelOverrides: Readonly<Record<string, ModelSelectorValue>>;
19216
+ },
19217
+ installed?: {
19218
+ modelRoles: Readonly<Record<string, ModelSelectorValue>>;
19219
+ agentModelOverrides: Readonly<Record<string, ModelSelectorValue>>;
19220
+ },
19221
+ manual?: { modelRoles: readonly string[]; agentModelOverrides: readonly string[] },
18443
19222
  ): void {
18444
19223
  const bindings = this.#modelRegistry.getConfiguredModelBindings?.();
18445
19224
  if (this.#preProfileModel === undefined) this.#preProfileModel = preProfileModel;
19225
+ const modelRoleSet = new Set(modelRoles);
19226
+ const agentOverrideSet = new Set(agentModelOverrides);
19227
+ for (const role of this.#activeProfileInstalledRoles.keys()) {
19228
+ if (!modelRoleSet.has(role)) {
19229
+ this.#activeProfileInstalledRoles.delete(role);
19230
+ this.#activeProfileInstalledRoleValues.delete(role);
19231
+ }
19232
+ }
19233
+ for (const role of this.#activeProfileInstalledAgentOverrides.keys()) {
19234
+ if (!agentOverrideSet.has(role)) {
19235
+ this.#activeProfileInstalledAgentOverrides.delete(role);
19236
+ this.#activeProfileInstalledAgentOverrideValues.delete(role);
19237
+ this.#activeProfileConfiguredAgentModelOverrides.delete(role);
19238
+ }
19239
+ }
18446
19240
  for (const role of modelRoles) {
18447
- if (this.#activeProfileInstalledRoles.has(role)) continue;
18448
- const bindingValue = bindings?.modelRoles?.[role];
18449
- this.#activeProfileInstalledRoles.set(
18450
- role,
18451
- bindingValue ?? this.settings.getGlobal("modelRoles")?.[role as never],
18452
- );
19241
+ if (!this.#activeProfileInstalledRoles.has(role)) {
19242
+ const bindingValue = bindings?.modelRoles?.[role];
19243
+ this.#activeProfileInstalledRoles.set(
19244
+ role,
19245
+ baseline
19246
+ ? Object.hasOwn(baseline.modelRoles, role)
19247
+ ? baseline.modelRoles[role]
19248
+ : undefined
19249
+ : (bindingValue ?? this.settings.getGlobal("modelRoles")?.[role as never]),
19250
+ );
19251
+ }
19252
+ const installedValue = installed?.modelRoles[role] ?? this.settings.get("modelRoles")[role];
19253
+ if (installedValue !== undefined) this.#activeProfileInstalledRoleValues.set(role, installedValue);
18453
19254
  }
18454
19255
  for (const role of agentModelOverrides) {
18455
- if (this.#activeProfileInstalledAgentOverrides.has(role)) continue;
18456
- const bindingValue = bindings?.agentModelOverrides?.[role];
18457
- this.#activeProfileInstalledAgentOverrides.set(
18458
- role,
18459
- bindingValue ?? this.settings.getGlobal("task.agentModelOverrides")?.[role as never],
18460
- );
19256
+ if (Object.hasOwn(bindings?.agentModelOverrides ?? {}, role)) {
19257
+ this.#activeProfileConfiguredAgentModelOverrides.add(role);
19258
+ } else {
19259
+ this.#activeProfileConfiguredAgentModelOverrides.delete(role);
19260
+ }
19261
+ if (!this.#activeProfileInstalledAgentOverrides.has(role)) {
19262
+ const bindingValue = bindings?.agentModelOverrides?.[role];
19263
+ this.#activeProfileInstalledAgentOverrides.set(
19264
+ role,
19265
+ baseline
19266
+ ? Object.hasOwn(baseline.agentModelOverrides, role)
19267
+ ? baseline.agentModelOverrides[role]
19268
+ : undefined
19269
+ : (bindingValue ?? this.settings.getGlobal("task.agentModelOverrides")?.[role as never]),
19270
+ );
19271
+ }
19272
+ const installedValue =
19273
+ installed?.agentModelOverrides[role] ?? this.settings.get("task.agentModelOverrides")[role];
19274
+ if (installedValue !== undefined) this.#activeProfileInstalledAgentOverrideValues.set(role, installedValue);
18461
19275
  }
19276
+ this.#activeProfileManualModelRoles = new Set(manual?.modelRoles ?? []);
19277
+ this.#activeProfileManualAgentModelOverrides = new Set(manual?.agentModelOverrides ?? []);
19278
+ }
19279
+
19280
+ getProfileInstalledOverrideState(): AgentSessionProfileInstalledOverrideState {
19281
+ return {
19282
+ modelRoles: new Map(this.#activeProfileInstalledRoles),
19283
+ agentModelOverrides: new Map(this.#activeProfileInstalledAgentOverrides),
19284
+ installedModelRoles: new Map(this.#activeProfileInstalledRoleValues),
19285
+ installedAgentModelOverrides: new Map(this.#activeProfileInstalledAgentOverrideValues),
19286
+ manualModelRoles: new Set(this.#activeProfileManualModelRoles),
19287
+ manualAgentModelOverrides: new Set(this.#activeProfileManualAgentModelOverrides),
19288
+ configuredAgentModelOverrides: new Set(this.#activeProfileConfiguredAgentModelOverrides),
19289
+ preProfileModel: this.#preProfileModel,
19290
+ };
19291
+ }
19292
+
19293
+ restoreProfileInstalledOverrideState(state: AgentSessionProfileInstalledOverrideState): void {
19294
+ this.#activeProfileInstalledRoles = new Map(state.modelRoles);
19295
+ this.#activeProfileInstalledAgentOverrides = new Map(state.agentModelOverrides);
19296
+ this.#activeProfileInstalledRoleValues = new Map(state.installedModelRoles);
19297
+ this.#activeProfileInstalledAgentOverrideValues = new Map(state.installedAgentModelOverrides);
19298
+ this.#activeProfileManualModelRoles = new Set(state.manualModelRoles);
19299
+ this.#activeProfileManualAgentModelOverrides = new Set(state.manualAgentModelOverrides);
19300
+ this.#activeProfileConfiguredAgentModelOverrides = new Set(state.configuredAgentModelOverrides);
19301
+ this.#preProfileModel = state.preProfileModel;
18462
19302
  }
18463
19303
 
18464
19304
  /** Drop the recorded profile-installed override keys after materialization. */
18465
19305
  clearProfileInstalledOverrides(): void {
18466
19306
  this.#activeProfileInstalledRoles.clear();
18467
19307
  this.#activeProfileInstalledAgentOverrides.clear();
19308
+ this.#activeProfileInstalledRoleValues.clear();
19309
+ this.#activeProfileInstalledAgentOverrideValues.clear();
19310
+ this.#activeProfileManualModelRoles.clear();
19311
+ this.#activeProfileManualAgentModelOverrides.clear();
19312
+ this.#activeProfileConfiguredAgentModelOverrides.clear();
18468
19313
  }
18469
19314
 
18470
19315
  /** Current profile-installed override keys, for deriving the activation base. */
@@ -18897,6 +19742,60 @@ export class AgentSession {
18897
19742
  return scope;
18898
19743
  }
18899
19744
 
19745
+ async prepareModelSelectionForProfileActivation(
19746
+ model: Model,
19747
+ thinkingLevel: ThinkingLevel | undefined,
19748
+ signal?: AbortSignal,
19749
+ ): Promise<PreparedProfileModelSelection> {
19750
+ signal?.throwIfAborted();
19751
+ const sessionId = this.sessionId;
19752
+ const previousModel = this.model;
19753
+ const previousThinkingLevel = this.thinkingLevel;
19754
+ const previousEditMode = this.#resolveActiveEditMode();
19755
+ const committedThinkingLevel = resolveThinkingLevelForModel(
19756
+ model,
19757
+ thinkingLevel ?? model.thinking?.defaultLevel ?? previousThinkingLevel,
19758
+ );
19759
+ if (this.sessionId !== sessionId) throw new Error("Session changed while preparing profile model selection");
19760
+ return {
19761
+ sessionId,
19762
+ model,
19763
+ thinkingLevel,
19764
+ committedThinkingLevel,
19765
+ previousModel,
19766
+ previousThinkingLevel,
19767
+ previousEditMode,
19768
+ signal,
19769
+ };
19770
+ }
19771
+
19772
+ commitPreparedProfileModelSelection(
19773
+ prepared: PreparedProfileModelSelection,
19774
+ onThinkingLevelApplied?: (mutationRevision: number) => void,
19775
+ ): void {
19776
+ this.#assertPreparedProfileModelSelectionCurrent(prepared);
19777
+ this.#setModelAuthoritatively(prepared.model, "profile-activation");
19778
+ this.#syncAppendOnlyContext(prepared.model);
19779
+ this.sessionManager.appendModelChange(`${prepared.model.provider}/${prepared.model.id}`, "temporary");
19780
+ this.settings.getStorage()?.recordModelUsage(`${prepared.model.provider}/${prepared.model.id}`);
19781
+ this.#applyThinkingLevel(prepared.committedThinkingLevel, false, false, onThinkingLevelApplied);
19782
+ }
19783
+
19784
+ #assertPreparedProfileModelSelectionCurrent(prepared: PreparedProfileModelSelection): void {
19785
+ prepared.signal?.throwIfAborted();
19786
+ if (
19787
+ this.sessionId !== prepared.sessionId ||
19788
+ this.model !== prepared.previousModel ||
19789
+ this.thinkingLevel !== prepared.previousThinkingLevel
19790
+ ) {
19791
+ throw new Error("Session model changed while preparing profile activation");
19792
+ }
19793
+ }
19794
+
19795
+ finishPreparedProfileModelSelection(prepared: PreparedProfileModelSelection): Promise<void> {
19796
+ return this.#syncEditToolModeAfterModelChange(prepared.previousEditMode);
19797
+ }
19798
+
18900
19799
  /** Restore the exact live-model state captured before a failed selector transaction. */
18901
19800
  async restoreModelSelectionForRollback(
18902
19801
  model: Model | undefined,
@@ -19449,7 +20348,12 @@ export class AgentSession {
19449
20348
  * effort (issue #4695): only control surfaces may pass true, so model-default
19450
20349
  * appends never mint `session` scope in {@link getThinkingScopeForControl}.
19451
20350
  */
19452
- #applyThinkingLevel(level: ThinkingLevel | undefined, persist: boolean, operatorIntent: boolean): void {
20351
+ #applyThinkingLevel(
20352
+ level: ThinkingLevel | undefined,
20353
+ persist: boolean,
20354
+ operatorIntent: boolean,
20355
+ onApplied?: (mutationRevision: number) => void,
20356
+ ): void {
19453
20357
  if (persist) this.#assertDurableSettingsWritable();
19454
20358
  this.#thinkingLevelMutationRevision++;
19455
20359
  this.#thinkingLevelLiveMutationRevision++;
@@ -19459,6 +20363,7 @@ export class AgentSession {
19459
20363
  const isChanging = effectiveLevel !== this.#thinkingLevel;
19460
20364
 
19461
20365
  this.#thinkingLevel = effectiveLevel;
20366
+ onApplied?.(this.#thinkingLevelLiveMutationRevision);
19462
20367
  this.agent.setThinkingLevel(toReasoningEffort(effectiveLevel));
19463
20368
  if (isChanging) this.#defaultModelSelectionMutationRevision++;
19464
20369
 
@@ -20474,7 +21379,7 @@ export class AgentSession {
20474
21379
  promptOverride: compactionPrep.hookPrompt,
20475
21380
  extraContext: compactionPrep.hookContext,
20476
21381
  remoteInstructions: this.#baseSystemPrompt.join("\n\n"),
20477
- convertToLlm,
21382
+ convertToLlm: (messages) => this.#summaryConvertToLlm(messages),
20478
21383
  },
20479
21384
  );
20480
21385
  summary = result.summary;
@@ -20725,7 +21630,7 @@ export class AgentSession {
20725
21630
  tools: this.agent.state.tools,
20726
21631
  customInstructions,
20727
21632
  promptExtension: this.settings.get("compaction.handoffPromptExtension") || undefined,
20728
- convertToLlm,
21633
+ convertToLlm: (messages) => this.#summaryConvertToLlm(messages),
20729
21634
  initiatorOverride: "agent",
20730
21635
  metadata: this.agent.metadataForProvider(model.provider),
20731
21636
  telemetry: resolveTelemetry(this.agent.telemetry, this.sessionId),
@@ -21273,6 +22178,20 @@ export class AgentSession {
21273
22178
  if (compactionStatus.kind === "aborted") {
21274
22179
  return compactionStatus.source === "hook" ? result("not-needed") : result("aborted");
21275
22180
  }
22181
+ // A skipped compaction (most often: nothing before the keep window to
22182
+ // summarize, e.g. one oversized early message) leaves the context
22183
+ // unchanged. Crossing the threshold is not an overflow: while the
22184
+ // unchanged context still fits the model window with its reserve, send
22185
+ // it, as the agent_end threshold path does. Failing here would end every
22186
+ // later turn of the session with the same error. A prune committed above
22187
+ // has already rewritten durable history, so the run must restart on it
22188
+ // rather than send its pre-prune snapshot.
22189
+ if (
22190
+ compactionStatus.kind === "skipped" &&
22191
+ contextTokens + effectiveReserveTokens(contextWindow, compactionSettings) <= contextWindow
22192
+ ) {
22193
+ return pruneResult?.committed ? result("pruned", true) : result("not-needed");
22194
+ }
21276
22195
  return result(
21277
22196
  "failed",
21278
22197
  false,
@@ -22485,6 +23404,18 @@ export class AgentSession {
22485
23404
  };
22486
23405
  }
22487
23406
 
23407
+ /**
23408
+ * Manual compact, auto compact, handoff, and branch summary keep the
23409
+ * existing conversion and obfuscate with the session registry prompt turns
23410
+ * already use.
23411
+ */
23412
+ #summaryConvertToLlm(messages: AgentMessage[]): Message[] {
23413
+ const converted = convertToLlm(messages);
23414
+ const obfuscator = this.#obfuscator;
23415
+ if (!obfuscator?.hasSecrets()) return converted;
23416
+ return obfuscateMessages(obfuscator, converted);
23417
+ }
23418
+
22488
23419
  async #compactWithFallbackModel(
22489
23420
  preparation: CompactionPreparation,
22490
23421
  customInstructions: string | undefined,
@@ -22513,7 +23444,7 @@ export class AgentSession {
22513
23444
  ...options,
22514
23445
  ...this.#maintenanceProviderTransport(),
22515
23446
  metadata: this.agent.metadataForProvider(candidate.provider),
22516
- convertToLlm,
23447
+ convertToLlm: (messages) => this.#summaryConvertToLlm(messages),
22517
23448
  telemetry,
22518
23449
  authCredentialType: this.#modelRegistry.getSessionCredentialType(
22519
23450
  candidate.provider,
@@ -23038,7 +23969,7 @@ export class AgentSession {
23038
23969
  remoteInstructions: this.#baseSystemPrompt.join("\n\n"),
23039
23970
  metadata: this.agent.metadataForProvider(candidate.provider),
23040
23971
  initiatorOverride: "agent",
23041
- convertToLlm,
23972
+ convertToLlm: (messages) => this.#summaryConvertToLlm(messages),
23042
23973
  telemetry,
23043
23974
  authCredentialType: this.#modelRegistry.getSessionCredentialType(
23044
23975
  candidate.provider,
@@ -26719,6 +27650,7 @@ export class AgentSession {
26719
27650
  let ownerShutdownManager: AsyncJobManager | undefined;
26720
27651
  let ownerShutdownLease: OwnerSubagentShutdownLease | undefined;
26721
27652
  let ownerShutdownTransitionCommitted = false;
27653
+ let ownerShutdownCanDrainResumes = false;
26722
27654
  let ownerShutdownFinalizationDeferred = false;
26723
27655
  this.#beginSessionTransition("switch-session");
26724
27656
  try {
@@ -26757,6 +27689,8 @@ export class AgentSession {
26757
27689
  if (lease && asyncManager) {
26758
27690
  ownerShutdownManager = asyncManager;
26759
27691
  ownerShutdownLease = lease;
27692
+ // Early failures leave the predecessor live, so releasing its fence may drain resumes.
27693
+ ownerShutdownCanDrainResumes = true;
26760
27694
  }
26761
27695
  await this.abort();
26762
27696
  if (this.isCompacting) {
@@ -26787,6 +27721,15 @@ export class AgentSession {
26787
27721
  );
26788
27722
  const previousActiveProfileInstalledRoles = new Map(this.#activeProfileInstalledRoles);
26789
27723
  const previousActiveProfileInstalledAgentOverrides = new Map(this.#activeProfileInstalledAgentOverrides);
27724
+ const previousActiveProfileInstalledRoleValues = new Map(this.#activeProfileInstalledRoleValues);
27725
+ const previousActiveProfileInstalledAgentOverrideValues = new Map(
27726
+ this.#activeProfileInstalledAgentOverrideValues,
27727
+ );
27728
+ const previousActiveProfileManualModelRoles = new Set(this.#activeProfileManualModelRoles);
27729
+ const previousActiveProfileManualAgentModelOverrides = new Set(this.#activeProfileManualAgentModelOverrides);
27730
+ const previousActiveProfileConfiguredAgentModelOverrides = new Set(
27731
+ this.#activeProfileConfiguredAgentModelOverrides,
27732
+ );
26790
27733
  const previousPreProfileModel = this.#preProfileModel;
26791
27734
  const previousServiceTier = this.agent.serviceTier;
26792
27735
  const previousSelectedMCPToolNames = new Set(this.#selectedMCPToolNames);
@@ -26801,6 +27744,8 @@ export class AgentSession {
26801
27744
  const previousActiveAttemptScope = this.#activeAttemptScope;
26802
27745
  const previousActiveLogicalRunId = this.#activeLogicalRunId;
26803
27746
 
27747
+ // Once session state starts changing, resumes are safe only after rollback completes.
27748
+ ownerShutdownCanDrainResumes = false;
26804
27749
  this.#steeringMessages = [];
26805
27750
  this.#followUpMessages = [];
26806
27751
  this.#pendingNextTurnMessages = [];
@@ -26918,7 +27863,7 @@ export class AgentSession {
26918
27863
  ? settingsDefaultEntries
26919
27864
  : (configuredDefaultChain?.entries ??
26920
27865
  (sessionContext.models.default ? [sessionContext.models.default] : []));
26921
- this.#activeModelProfile = targetActiveModelProfile;
27866
+ this.setActiveModelProfile(targetActiveModelProfile);
26922
27867
  this.#defaultFallbackController = undefined;
26923
27868
  if (defaultEntries.length > 0) {
26924
27869
  const resolution = await resolveModelChainWithAuth(
@@ -27014,7 +27959,7 @@ export class AgentSession {
27014
27959
  // a cross-file transition must not advertise that profile after its
27015
27960
  // runtime role layer was removed. Otherwise delegation prompt state and
27016
27961
  // actual role routing diverge.
27017
- if (!targetProfileRuntimeInstalled) this.#activeModelProfile = undefined;
27962
+ if (!targetProfileRuntimeInstalled) this.setActiveModelProfile(undefined);
27018
27963
 
27019
27964
  const hasThinkingEntry = this.sessionManager
27020
27965
  .getBranch()
@@ -27217,6 +28162,15 @@ export class AgentSession {
27217
28162
  else this.settings.override("task.agentModelOverrides", previousAgentModelOverridesOverride);
27218
28163
  this.#activeProfileInstalledRoles = new Map(previousActiveProfileInstalledRoles);
27219
28164
  this.#activeProfileInstalledAgentOverrides = new Map(previousActiveProfileInstalledAgentOverrides);
28165
+ this.#activeProfileInstalledRoleValues = new Map(previousActiveProfileInstalledRoleValues);
28166
+ this.#activeProfileInstalledAgentOverrideValues = new Map(
28167
+ previousActiveProfileInstalledAgentOverrideValues,
28168
+ );
28169
+ this.#activeProfileManualModelRoles = new Set(previousActiveProfileManualModelRoles);
28170
+ this.#activeProfileManualAgentModelOverrides = new Set(previousActiveProfileManualAgentModelOverrides);
28171
+ this.#activeProfileConfiguredAgentModelOverrides = new Set(
28172
+ previousActiveProfileConfiguredAgentModelOverrides,
28173
+ );
27220
28174
  this.#preProfileModel = previousPreProfileModel;
27221
28175
  this.#activeModelProfile = previousActiveModelProfile;
27222
28176
  this.#restoreWorkflowGateEmitter(suspendedWorkflowGateEmitter);
@@ -27280,6 +28234,7 @@ export class AgentSession {
27280
28234
  }
27281
28235
  throw new AggregateError([error, rollbackErrors[0]], "Session switch rollback failed");
27282
28236
  }
28237
+ ownerShutdownCanDrainResumes = true;
27283
28238
  if (unavailableDefaultChainMessage) {
27284
28239
  this.emitNotice(
27285
28240
  "error",
@@ -27292,10 +28247,17 @@ export class AgentSession {
27292
28247
  }
27293
28248
  } finally {
27294
28249
  if (ownerShutdownManager && ownerShutdownLease && !ownerShutdownFinalizationDeferred) {
27295
- ownerShutdownManager.finishOwnerSubagentShutdown(
27296
- ownerShutdownLease,
27297
- ownerShutdownTransitionCommitted ? "commit" : "release",
27298
- );
28250
+ if (ownerShutdownTransitionCommitted) {
28251
+ ownerShutdownManager.finishOwnerSubagentShutdown(ownerShutdownLease, "commit");
28252
+ } else {
28253
+ // A pre-mutation flush or snapshot failure can leave the predecessor
28254
+ // live after the Agent event bridge was disconnected. Reconnect before
28255
+ // releasing the lease, which may synchronously start queued resumes.
28256
+ if (!this.#isDisposed) this.#reconnectToAgent();
28257
+ ownerShutdownManager.finishOwnerSubagentShutdown(ownerShutdownLease, "release", {
28258
+ drainResumeQueue: ownerShutdownCanDrainResumes && !this.#isDisposed,
28259
+ });
28260
+ }
27299
28261
  }
27300
28262
  this.#endSessionTransition();
27301
28263
  }
@@ -27526,7 +28488,7 @@ export class AgentSession {
27526
28488
  customInstructions: options.customInstructions,
27527
28489
  reserveTokens: branchSummarySettings.reserveTokens,
27528
28490
  metadata: this.agent.metadataForProvider(model.provider),
27529
- convertToLlm,
28491
+ convertToLlm: (messages) => this.#summaryConvertToLlm(messages),
27530
28492
  telemetry: resolveTelemetry(this.agent.telemetry, this.sessionId),
27531
28493
  });
27532
28494
  this.#branchSummaryAbortController = undefined;