@gajae-code/coding-agent 0.18.7 → 0.18.8

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 (279) hide show
  1. package/CHANGELOG.md +279 -1
  2. package/dist/types/async/job-manager.d.ts +1 -1
  3. package/dist/types/capability/mcp.d.ts +2 -0
  4. package/dist/types/capability/skill.d.ts +2 -0
  5. package/dist/types/capability/types.d.ts +2 -0
  6. package/dist/types/cli/public-command-errors.d.ts +3 -1
  7. package/dist/types/cli/setup-cli.d.ts +1 -1
  8. package/dist/types/commands/deep-interview.d.ts +3 -0
  9. package/dist/types/config/autorouting-tier-map.d.ts +348 -9
  10. package/dist/types/config/file-lock.d.ts +1 -0
  11. package/dist/types/config/model-bindings-applier.d.ts +2 -0
  12. package/dist/types/config/model-profile-activation.d.ts +11 -3
  13. package/dist/types/config/model-registry.d.ts +4 -9
  14. package/dist/types/config/skill-settings-defaults.d.ts +2 -0
  15. package/dist/types/cursor-delete-path.d.ts +1 -0
  16. package/dist/types/cursor-mcp-name.d.ts +8 -0
  17. package/dist/types/customization/inventory.d.ts +2 -0
  18. package/dist/types/debug/report-bundle-parent.d.ts +2 -0
  19. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.d.ts +1 -0
  20. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/version-manager.d.ts +31 -0
  21. package/dist/types/defaults/gjc-skills.generated.d.ts +5 -6
  22. package/dist/types/discovery/helpers.d.ts +6 -1
  23. package/dist/types/export/html/css-color.d.ts +2 -0
  24. package/dist/types/export/html/template.generated.d.ts +1 -1
  25. package/dist/types/extensibility/extensions/types.d.ts +7 -0
  26. package/dist/types/extensibility/gjc-plugins/runtime-adapters.d.ts +14 -1
  27. package/dist/types/extensibility/plugins/marketplace/cache.d.ts +2 -0
  28. package/dist/types/extensibility/plugins/plugin-link-path.d.ts +2 -0
  29. package/dist/types/extensibility/runtime-skill-discovery.d.ts +3 -1
  30. package/dist/types/extensibility/skill-management.d.ts +3 -1
  31. package/dist/types/extensibility/skills.d.ts +2 -0
  32. package/dist/types/gjc-runtime/deep-interview-crystallize.d.ts +74 -0
  33. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +11 -0
  34. package/dist/types/gjc-runtime/gc-runtime.d.ts +5 -0
  35. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +0 -5
  36. package/dist/types/gjc-runtime/state-argv.d.ts +1 -1
  37. package/dist/types/gjc-runtime/state-migrations.d.ts +8 -0
  38. package/dist/types/gjc-runtime/state-runtime.d.ts +95 -0
  39. package/dist/types/gjc-runtime/state-writer.d.ts +11 -1
  40. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +8 -0
  41. package/dist/types/internal-urls/skill-path.d.ts +1 -0
  42. package/dist/types/lsp/workspace-path.d.ts +1 -0
  43. package/dist/types/modes/acp/acp-agent.d.ts +7 -0
  44. package/dist/types/modes/components/custom-editor.d.ts +6 -0
  45. package/dist/types/modes/components/customization/customization-dashboard.d.ts +1 -0
  46. package/dist/types/modes/components/extensions/state-manager.d.ts +4 -3
  47. package/dist/types/modes/controllers/input-controller.d.ts +9 -0
  48. package/dist/types/modes/interactive-mode.d.ts +1 -0
  49. package/dist/types/modes/utils/interrupt-ownership.d.ts +3 -0
  50. package/dist/types/modes/utils/keybinding-matchers.d.ts +1 -0
  51. package/dist/types/modes/utils/macos-option-forwarding.d.ts +17 -0
  52. package/dist/types/progress/collect-project-progress.d.ts +38 -0
  53. package/dist/types/progress/progress-contract.d.ts +154 -0
  54. package/dist/types/progress/progress-snapshot.d.ts +11 -0
  55. package/dist/types/progress/project-progress.d.ts +141 -0
  56. package/dist/types/progress/render-progress.d.ts +28 -0
  57. package/dist/types/runtime-mcp/oauth-public-url.d.ts +1 -0
  58. package/dist/types/runtime-mcp/plugin-network-boundary.d.ts +1 -0
  59. package/dist/types/runtime-mcp/smithery-registry.d.ts +24 -0
  60. package/dist/types/runtime-mcp/types.d.ts +6 -0
  61. package/dist/types/sdk/acp/adapter.d.ts +1 -1
  62. package/dist/types/sdk/broker/broker-exit.d.ts +6 -2
  63. package/dist/types/sdk/broker/broker.d.ts +18 -1
  64. package/dist/types/sdk/broker/session-index.d.ts +12 -6
  65. package/dist/types/sdk/bus/index.d.ts +12 -1
  66. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +6 -1
  67. package/dist/types/sdk/client/client.d.ts +2 -0
  68. package/dist/types/sdk/host/query/handlers.d.ts +3 -0
  69. package/dist/types/sdk/index.d.ts +1 -0
  70. package/dist/types/sdk/router/session-router.d.ts +2 -0
  71. package/dist/types/sdk/session-list.d.ts +1 -1
  72. package/dist/types/sdk/session.d.ts +8 -0
  73. package/dist/types/session/agent-session.d.ts +38 -9
  74. package/dist/types/session/artifacts.d.ts +2 -2
  75. package/dist/types/session/internal/managed-gc-retirement-codec.d.ts +32 -0
  76. package/dist/types/session/internal/managed-session-scope.d.ts +79 -2
  77. package/dist/types/session/internal/managed-session-storage.d.ts +26 -1
  78. package/dist/types/session/internal/managed-task-owner-cleanup.d.ts +62 -0
  79. package/dist/types/session/internal/task-artifact-owner-access.d.ts +2 -0
  80. package/dist/types/session/internal/task-artifact-owner-transcript.d.ts +9 -1
  81. package/dist/types/session/session-manager.d.ts +31 -5
  82. package/dist/types/session/session-retirement.d.ts +31 -7
  83. package/dist/types/session/session-storage.d.ts +62 -2
  84. package/dist/types/session/task-artifact-owner-codec.d.ts +2 -0
  85. package/dist/types/setup/credential-auto-import.d.ts +1 -1
  86. package/dist/types/setup/credential-import.d.ts +6 -4
  87. package/dist/types/skill-state/active-state.d.ts +38 -0
  88. package/dist/types/slash-commands/helpers/progress-report.d.ts +8 -0
  89. package/dist/types/task/executor.d.ts +3 -0
  90. package/dist/types/tools/bash-skill-urls.d.ts +1 -1
  91. package/dist/types/tools/index.d.ts +3 -0
  92. package/dist/types/tools/python.d.ts +9 -1
  93. package/dist/types/tools/recipe/recipe-guard.d.ts +7 -0
  94. package/dist/types/tools/restricted-cwd.d.ts +1 -0
  95. package/dist/types/tools/todo-write.d.ts +6 -0
  96. package/dist/types/utils/tools-checksum.d.ts +7 -0
  97. package/package.json +7 -7
  98. package/scripts/generate-sdk-operation-inventory.ts +21 -0
  99. package/scripts/verify-managed-empty-stop.ts +28 -0
  100. package/src/async/job-manager.ts +228 -122
  101. package/src/capability/index.ts +17 -0
  102. package/src/capability/mcp.ts +2 -0
  103. package/src/capability/skill.ts +2 -0
  104. package/src/capability/types.ts +2 -0
  105. package/src/cli/auth-broker-cli.ts +1 -1
  106. package/src/cli/customize-doctor.ts +15 -2
  107. package/src/cli/public-command-errors.ts +21 -1
  108. package/src/cli/setup-cli.ts +4 -4
  109. package/src/cli/skills-cli.ts +5 -0
  110. package/src/commands/deep-interview.ts +47 -8
  111. package/src/commands/sdk.ts +28 -8
  112. package/src/config/autorouting-tier-map.ts +117 -7
  113. package/src/config/file-lock.ts +307 -68
  114. package/src/config/mcp-schema.json +4 -0
  115. package/src/config/model-bindings-applier.ts +20 -0
  116. package/src/config/model-profile-activation.ts +240 -46
  117. package/src/config/model-registry.ts +47 -153
  118. package/src/config/skill-settings-defaults.ts +7 -0
  119. package/src/coordinator-mcp/policy.ts +3 -2
  120. package/src/cursor-delete-path.ts +10 -0
  121. package/src/cursor-mcp-name.ts +15 -0
  122. package/src/cursor.ts +13 -2
  123. package/src/customization/inventory.ts +3 -0
  124. package/src/debug/report-bundle-parent.ts +22 -0
  125. package/src/debug/report-bundle.ts +3 -0
  126. package/src/debug/system-info.ts +13 -1
  127. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.ts +67 -4
  128. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/version-manager.ts +80 -0
  129. package/src/defaults/gjc/skills/deep-interview/SKILL.md +105 -10
  130. package/src/defaults/gjc/skills/ralplan/SKILL.md +6 -4
  131. package/src/defaults/gjc-defaults.test.ts +18 -1
  132. package/src/defaults/gjc-defaults.ts +3 -0
  133. package/src/defaults/gjc-skills.generated.ts +58 -36
  134. package/src/discovery/agents.ts +6 -1
  135. package/src/discovery/builtin.ts +10 -0
  136. package/src/discovery/helpers.ts +159 -20
  137. package/src/discovery/mcp-json.ts +6 -0
  138. package/src/discovery/ssh.ts +1 -1
  139. package/src/discovery/substitute-plugin-root.ts +3 -1
  140. package/src/edit/file-read-cache.ts +1 -1
  141. package/src/eval/py/executor.ts +380 -120
  142. package/src/export/html/css-color.ts +9 -0
  143. package/src/export/html/index.ts +5 -4
  144. package/src/export/html/template.generated.ts +1 -1
  145. package/src/extensibility/extensions/runner.ts +4 -0
  146. package/src/extensibility/extensions/types.ts +7 -0
  147. package/src/extensibility/gjc-plugins/runtime-adapters.ts +73 -8
  148. package/src/extensibility/plugins/manager.ts +3 -2
  149. package/src/extensibility/plugins/marketplace/cache.ts +15 -0
  150. package/src/extensibility/plugins/marketplace/manager.ts +11 -3
  151. package/src/extensibility/plugins/plugin-link-path.ts +12 -0
  152. package/src/extensibility/runtime-skill-discovery.ts +20 -2
  153. package/src/extensibility/skill-management.ts +6 -2
  154. package/src/extensibility/skills.ts +6 -1
  155. package/src/gjc-runtime/deep-interview-crystallize.ts +1814 -0
  156. package/src/gjc-runtime/deep-interview-recorder.ts +20 -2
  157. package/src/gjc-runtime/deep-interview-runtime.ts +1415 -13
  158. package/src/gjc-runtime/deep-interview-stage.ts +98 -21
  159. package/src/gjc-runtime/gc-runtime.ts +206 -9
  160. package/src/gjc-runtime/launch-worktree.ts +14 -4
  161. package/src/gjc-runtime/ralplan-runtime.ts +447 -52
  162. package/src/gjc-runtime/state-argv.ts +2 -0
  163. package/src/gjc-runtime/state-migrations.ts +91 -29
  164. package/src/gjc-runtime/state-runtime.ts +3716 -135
  165. package/src/gjc-runtime/state-writer.ts +83 -8
  166. package/src/gjc-runtime/ultragoal-runtime.ts +16 -4
  167. package/src/gjc-runtime/workflow-manifest.generated.json +70 -42
  168. package/src/gjc-runtime/workflow-manifest.ts +54 -21
  169. package/src/harness-control-plane/operate.ts +12 -4
  170. package/src/harness-control-plane/owner.ts +5 -1
  171. package/src/hashline/parser.ts +48 -9
  172. package/src/hashline/recovery.ts +13 -4
  173. package/src/internal-urls/docs-index.generated.ts +18 -17
  174. package/src/internal-urls/skill-path.ts +19 -0
  175. package/src/internal-urls/skill-protocol.ts +2 -0
  176. package/src/lsp/edits.ts +6 -0
  177. package/src/lsp/workspace-path.ts +51 -0
  178. package/src/main.ts +195 -31
  179. package/src/modes/acp/acp-agent.ts +88 -4
  180. package/src/modes/components/custom-editor.ts +14 -0
  181. package/src/modes/components/customization/customization-dashboard.ts +12 -0
  182. package/src/modes/components/extensions/extension-dashboard.ts +2 -2
  183. package/src/modes/components/extensions/state-manager.ts +15 -5
  184. package/src/modes/components/model-selector.ts +1 -4
  185. package/src/modes/components/provider-onboarding-selector.ts +1 -1
  186. package/src/modes/components/status-line/segments.ts +4 -0
  187. package/src/modes/controllers/event-controller.ts +11 -4
  188. package/src/modes/controllers/input-controller.ts +74 -29
  189. package/src/modes/controllers/runtime-mcp-command-controller.ts +8 -3
  190. package/src/modes/controllers/selector-controller.ts +28 -15
  191. package/src/modes/interactive-mode.ts +4 -0
  192. package/src/modes/runtime-init.ts +4 -0
  193. package/src/modes/utils/abort-message.ts +3 -1
  194. package/src/modes/utils/interrupt-ownership.ts +12 -0
  195. package/src/modes/utils/keybinding-matchers.ts +4 -0
  196. package/src/modes/utils/macos-option-forwarding.ts +80 -0
  197. package/src/progress/collect-project-progress.ts +153 -0
  198. package/src/progress/progress-contract.ts +172 -0
  199. package/src/progress/progress-snapshot.ts +140 -0
  200. package/src/progress/project-progress.ts +446 -0
  201. package/src/progress/render-progress.ts +209 -0
  202. package/src/prompts/system/master-mode.md +1 -0
  203. package/src/prompts/tools/skill-discovery.md +1 -1
  204. package/src/runtime-mcp/config-writer.ts +10 -6
  205. package/src/runtime-mcp/config.ts +1 -0
  206. package/src/runtime-mcp/oauth-flow.ts +5 -0
  207. package/src/runtime-mcp/oauth-public-url.ts +8 -0
  208. package/src/runtime-mcp/plugin-network-boundary.ts +4 -0
  209. package/src/runtime-mcp/smithery-registry.ts +22 -3
  210. package/src/runtime-mcp/transports/http.ts +2 -2
  211. package/src/runtime-mcp/types.ts +6 -0
  212. package/src/sdk/acp/adapter.ts +93 -8
  213. package/src/sdk/broker/broker-exit.ts +52 -23
  214. package/src/sdk/broker/broker.ts +75 -6
  215. package/src/sdk/broker/ensure.ts +2 -0
  216. package/src/sdk/broker/lifecycle.ts +711 -66
  217. package/src/sdk/broker/session-index.ts +191 -81
  218. package/src/sdk/bus/chat-daemon-cli.ts +38 -1
  219. package/src/sdk/bus/chat-daemon-control.ts +2 -2
  220. package/src/sdk/bus/index.ts +238 -43
  221. package/src/sdk/bus/telegram-daemon-contract.ts +6 -1
  222. package/src/sdk/cli/session-cli.ts +17 -3
  223. package/src/sdk/client/client.ts +28 -8
  224. package/src/sdk/host/query/handlers.ts +9 -1
  225. package/src/sdk/host/session-runtime.test.ts +2647 -294
  226. package/src/sdk/host/session-runtime.ts +426 -109
  227. package/src/sdk/host/surface-policy.ts +1 -0
  228. package/src/sdk/index.ts +8 -0
  229. package/src/sdk/lifecycle-session.ts +3 -1
  230. package/src/sdk/prompt-deadline-manager.ts +15 -0
  231. package/src/sdk/protocol/operation-inventory.generated.json +146 -0
  232. package/src/sdk/protocol/operation-registry.ts +9 -2
  233. package/src/sdk/router/session-router.ts +75 -12
  234. package/src/sdk/session-list.ts +1 -0
  235. package/src/sdk/session.ts +67 -38
  236. package/src/session/agent-session.ts +623 -224
  237. package/src/session/artifacts.ts +174 -70
  238. package/src/session/internal/managed-gc-retirement-codec.ts +319 -0
  239. package/src/session/internal/managed-session-scope.ts +3457 -462
  240. package/src/session/internal/managed-session-storage.ts +376 -61
  241. package/src/session/internal/managed-task-owner-cleanup.ts +473 -0
  242. package/src/session/internal/task-artifact-owner-access.ts +19 -3
  243. package/src/session/internal/task-artifact-owner-transcript.ts +547 -1
  244. package/src/session/session-manager.ts +939 -133
  245. package/src/session/session-retirement.ts +573 -14
  246. package/src/session/session-storage.ts +417 -14
  247. package/src/session/task-artifact-owner-codec.ts +71 -8
  248. package/src/session/task-artifact-owner-retirement.ts +21 -19
  249. package/src/setup/credential-auto-import.ts +12 -3
  250. package/src/setup/credential-import.ts +176 -5
  251. package/src/setup/paseo/announce.ts +6 -1
  252. package/src/skill-state/active-state.ts +132 -2
  253. package/src/slash-commands/builtin-registry.ts +39 -9
  254. package/src/slash-commands/helpers/progress-report.ts +23 -0
  255. package/src/stt/setup.ts +3 -5
  256. package/src/task/commands.ts +3 -1
  257. package/src/task/executor.ts +4 -0
  258. package/src/task/index.ts +6 -0
  259. package/src/tools/ask.ts +205 -6
  260. package/src/tools/ast-edit.ts +4 -0
  261. package/src/tools/bash-allowed-prefixes.ts +15 -0
  262. package/src/tools/bash-skill-urls.ts +56 -3
  263. package/src/tools/bash.ts +48 -79
  264. package/src/tools/descriptors.ts +5 -1
  265. package/src/tools/eval.ts +56 -36
  266. package/src/tools/image-gen.ts +1 -1
  267. package/src/tools/index.ts +3 -0
  268. package/src/tools/python.ts +187 -66
  269. package/src/tools/recipe/index.ts +9 -0
  270. package/src/tools/recipe/recipe-guard.ts +19 -0
  271. package/src/tools/restricted-cwd.ts +10 -0
  272. package/src/tools/skill-discovery.ts +5 -0
  273. package/src/tools/skill.ts +28 -1
  274. package/src/tools/todo-write.ts +10 -1
  275. package/src/tools/tool-catalog.generated.ts +1 -1
  276. package/src/tools/write.ts +1 -0
  277. package/src/utils/tools-checksum.ts +34 -0
  278. package/src/utils/tools-manager.ts +24 -6
  279. package/src/web/search/providers/codex.ts +3 -2
@@ -30,6 +30,7 @@ import {
30
30
  type AgentLoopConfig,
31
31
  type AgentMessage,
32
32
  type AgentState,
33
+ type AgentTerminalOwnerContext,
33
34
  type AgentTool,
34
35
  assertImagePlaceholdersHavePayload,
35
36
  type ContextMaintenanceResult,
@@ -134,8 +135,8 @@ import {
134
135
  classifyFallbackTrigger,
135
136
  EMPTY_RESPONSE_PROVIDER_CODE,
136
137
  type FallbackAttemptToken,
137
- PROVIDER_PROTOCOL_MISMATCH_ERROR_CODE,
138
138
  type FallbackTriggerClass,
139
+ PROVIDER_PROTOCOL_MISMATCH_ERROR_CODE,
139
140
  SERVER_OVERLOADED_PROVIDER_CODE,
140
141
  STREAM_FIRST_EVENT_TIMEOUT_PROVIDER_CODE,
141
142
  } from "@gajae-code/ai/utils/fallback-transport";
@@ -218,6 +219,7 @@ import {
218
219
  type FoldReason,
219
220
  type JobFoldEvent,
220
221
  type OwnerSubagentShutdownLease,
222
+ type SubagentLifecycle,
221
223
  } from "../async";
222
224
  import { reset as resetCapabilities } from "../capability";
223
225
  import type { Rule } from "../capability/rule";
@@ -234,6 +236,15 @@ import {
234
236
  UnknownModelProfileError,
235
237
  validateModelProfileName,
236
238
  } from "../config/model-profile-contract";
239
+ import {
240
+ commitDurableModelProfileOwnership,
241
+ type DurableModelProfileOwnership,
242
+ InvalidModelProfileOwnershipError,
243
+ ModelProfileOwnershipConflictError,
244
+ type ModelProfileOwnershipMarker,
245
+ modelProfileOwnershipMarkersEqual,
246
+ readDurableModelProfileOwnership,
247
+ } from "../config/model-profile-ownership";
237
248
  import { resolveProfileBindings } from "../config/model-profiles";
238
249
  import {
239
250
  GJC_MODEL_ASSIGNMENT_TARGETS,
@@ -319,6 +330,7 @@ import {
319
330
  buildSkillPromptMessage,
320
331
  getSkillSlashCommandName,
321
332
  isNamespacedSkillSlashCommandName,
333
+ type ParsedSkillInvocation,
322
334
  parseSkillInvocations,
323
335
  type Skill,
324
336
  type SkillWarning,
@@ -408,11 +420,7 @@ import type { NotificationSessionController } from "../sdk/bus/session-control";
408
420
  import { buildSyntheticModelId, syntheticNamespaceCollision } from "../sdk/model-profile-model";
409
421
  import { sanitizePromptFailure } from "../sdk/prompt-failure";
410
422
  import type { SecretObfuscator } from "../secrets/obfuscator";
411
- import {
412
- formatNoCredentialOnboardingError,
413
- formatNoModelOnboardingError,
414
- NoModelSelectedError,
415
- } from "../setup/model-onboarding-guidance";
423
+ import { formatNoCredentialOnboardingError, NoModelSelectedError } from "../setup/model-onboarding-guidance";
416
424
  import {
417
425
  isCanonicalGjcWorkflowSkill,
418
426
  isWorkflowContinuationInert,
@@ -527,6 +535,7 @@ import type {
527
535
  SessionEntry,
528
536
  SessionManagerCloseOutcome,
529
537
  SessionMemoryStats,
538
+ SessionArtifactPublication,
530
539
  } from "./session-manager";
531
540
  import {
532
541
  createReadonlySessionManager,
@@ -1218,6 +1227,11 @@ type SendUserMessageDispatchOptions = SendUserMessageOptions & {
1218
1227
  };
1219
1228
 
1220
1229
  type InternalPromptOptions = PromptOptions & { sdkRunToken?: string };
1230
+ interface SdkContinuationOwnership {
1231
+ scope: AttemptScope;
1232
+ token: string;
1233
+ cohort: string[];
1234
+ }
1221
1235
  type InternalCustomMessageOptions = Pick<
1222
1236
  PromptOptions,
1223
1237
  | "streamingBehavior"
@@ -1427,8 +1441,7 @@ const PROVIDER_FIRST_EVENT_TIMEOUT_WITHOUT_ARTICLE_ERROR = "Provider stream time
1427
1441
  const BARE_DEFAULT_CODEX_RETRYABLE_ERROR =
1428
1442
  /^Codex error event(?:: .*)? \(code=(?:server_is_overloaded|server_error|internal_error)(?:, [^)]+)*\)$/;
1429
1443
  const BARE_DEFAULT_CODEX_RETRYABLE_CODES = new Set(["server_is_overloaded", "server_error", "internal_error"]);
1430
- const BARE_DEFAULT_CODEX_OVERLOAD_ERROR =
1431
- /^Codex error event(?:: .*)? \(code=server_is_overloaded(?:, [^)]+)*\)$/;
1444
+ const BARE_DEFAULT_CODEX_OVERLOAD_ERROR = /^Codex error event(?:: .*)? \(code=server_is_overloaded(?:, [^)]+)*\)$/;
1432
1445
  const BARE_DEFAULT_OVERLOAD_CODES = new Set([SERVER_OVERLOADED_PROVIDER_CODE]);
1433
1446
  /** Anthropic's typed capacity-overload `error.type`, the only Anthropic overload code admitted below. */
1434
1447
  const ANTHROPIC_OVERLOADED_ERROR_TYPE = "overloaded_error";
@@ -1541,14 +1554,7 @@ function isExactTypedOverloadFacts(
1541
1554
  facts.retryMaxAttempts === undefined
1542
1555
  );
1543
1556
  }
1544
- /**
1545
- * True when transport facts are exactly the provider's statusless typed
1546
- * capacity-overload code (the provider code verbatim and, when present, the
1547
- * OpenAI code verbatim, with no status). The agent loop must not treat the new
1548
- * facts as managed transaction authority, and managed session fallback must
1549
- * not gain retry/advance authority from them, so this predicate keeps both
1550
- * guards reading only typed facts, never error prose.
1551
- */
1557
+
1552
1558
  function isStatuslessTypedOverloadFacts(facts: TransportFailureFacts | undefined): boolean {
1553
1559
  if (!facts) return false;
1554
1560
  return (
@@ -1557,7 +1563,6 @@ function isStatuslessTypedOverloadFacts(facts: TransportFailureFacts | undefined
1557
1563
  (facts.openaiErrorCode === undefined || facts.openaiErrorCode === SERVER_OVERLOADED_PROVIDER_CODE)
1558
1564
  );
1559
1565
  }
1560
-
1561
1566
  // Deterministic auth/request/model diagnostics must surface even when a provider
1562
1567
  // labels the failure with a transient code.
1563
1568
  const TERMINAL_ERROR_MESSAGE =
@@ -1798,20 +1803,14 @@ function summarizeAgentBashArtifactSave(
1798
1803
  };
1799
1804
  }
1800
1805
 
1801
- export interface AgentBashArtifactStore {
1802
- saveArtifact(content: string, toolType: string): Promise<string | undefined>;
1803
- getArtifactPath(id: string): Promise<string | null>;
1804
- }
1805
-
1806
1806
  export async function saveAgentBashOriginalArtifact(
1807
- store: AgentBashArtifactStore,
1807
+ publication: SessionArtifactPublication,
1808
1808
  originalText: string,
1809
1809
  ): Promise<BashArtifactSaveResult> {
1810
1810
  try {
1811
- const artifactId = await store.saveArtifact(originalText, "bash-original");
1812
- if (!artifactId) return { status: "failed", diagnostic: "storage returned no artifact id" };
1813
- const artifactPath = await store.getArtifactPath(artifactId);
1814
- return artifactPath ? summarizeAgentBashArtifactSave(artifactId, originalText) : { status: "unavailable" };
1811
+ const artifactId = await publication(originalText, "bash-original");
1812
+ if (!artifactId) return { status: "unavailable" };
1813
+ return summarizeAgentBashArtifactSave(artifactId, originalText);
1815
1814
  } catch (error) {
1816
1815
  return { status: "failed", diagnostic: boundAgentBashArtifactSaveDiagnostic(error) };
1817
1816
  }
@@ -2764,6 +2763,7 @@ export class AgentSession {
2764
2763
  #scopedModels: ScopedModelSelection[];
2765
2764
  #thinkingLevel: ThinkingLevel | undefined;
2766
2765
  #activeModelProfile: string | undefined;
2766
+ #unavailableModelProfile: string | undefined;
2767
2767
  #activeProfileInstalledRoles = new Map<string, ModelSelectorValue | undefined>();
2768
2768
  #activeProfileInstalledAgentOverrides = new Map<string, ModelSelectorValue | undefined>();
2769
2769
  #preProfileModel: Model | undefined;
@@ -2789,6 +2789,9 @@ export class AgentSession {
2789
2789
  #followUpReservationTransitionWaiters = new Set<() => void>();
2790
2790
  #selectionFenceGeneration = 0;
2791
2791
  #defaultModelSelectionMutationRevision = 0;
2792
+ #userModelSelectionRevision = 0;
2793
+ #userCanonicalVariantSelectionRevision = 0;
2794
+ #userCanonicalVariantSelection: string | undefined;
2792
2795
  #thinkingLevelMutationRevision = 0;
2793
2796
  #thinkingVisibilityMutationRevision = 0;
2794
2797
  #thinkingLevelLiveMutationRevision = 0;
@@ -3439,8 +3442,17 @@ export class AgentSession {
3439
3442
  this.#activeAttemptScope = undefined;
3440
3443
  this.#activeLogicalRunId = undefined;
3441
3444
  }
3442
- #acceptSdkAttemptRun(handle: AttemptRunHandle, sdkRunToken?: string, sdkRunTokens?: string[]): void {
3443
- const predecessorScope = this.#activeAttemptScope;
3445
+ #captureSdkContinuationOwnership(scope: AttemptScope | undefined): SdkContinuationOwnership | undefined {
3446
+ const token = scope ? this.#sdkRunTokensByAttemptScope.get(scope) : undefined;
3447
+ if (!scope || token === undefined) return undefined;
3448
+ return { scope, token, cohort: [...(this.#sdkRunCohortsByAttemptScope.get(scope) ?? [token])] };
3449
+ }
3450
+ #acceptSdkAttemptRun(
3451
+ handle: AttemptRunHandle,
3452
+ sdkRunToken?: string,
3453
+ sdkRunTokens?: string[],
3454
+ predecessorScope?: AttemptScope,
3455
+ ): void {
3444
3456
  const predecessorSdkRunToken =
3445
3457
  predecessorScope === undefined ? undefined : this.#sdkRunTokensByAttemptScope.get(predecessorScope);
3446
3458
  const carryRecoverySkip =
@@ -3450,14 +3462,23 @@ export class AgentSession {
3450
3462
  this.#skipPostPromptRecoveryWaitByAttemptScope.delete(predecessorScope);
3451
3463
  this.#acceptRunHandle(handle);
3452
3464
  this.#settleTrackedOwnRunPromotions(handle);
3465
+ // Logical continuation lineage is causal even without an SDK submission.
3466
+ if (predecessorScope) {
3467
+ this.#lifecycleScopesByAttemptScope.set(
3468
+ handle.scope,
3469
+ this.#lifecycleScopesByAttemptScope.get(predecessorScope) ?? predecessorScope,
3470
+ );
3471
+ }
3453
3472
  if (sdkRunToken !== undefined) {
3454
3473
  this.#activeSdkRunToken = sdkRunToken;
3455
3474
  this.#sdkRunTokensByAttemptScope.set(handle.scope, sdkRunToken);
3456
- const inheritedCohort =
3457
- predecessorScope !== undefined && predecessorSdkRunToken === sdkRunToken
3458
- ? this.#sdkRunCohortsByAttemptScope.get(predecessorScope)
3459
- : undefined;
3460
- this.#sdkRunCohortsByAttemptScope.set(handle.scope, sdkRunTokens ?? inheritedCohort ?? [sdkRunToken]);
3475
+ this.#sdkRunCohortsByAttemptScope.set(handle.scope, sdkRunTokens ?? [sdkRunToken]);
3476
+ for (const token of this.#sdkRunCohortsByAttemptScope.get(handle.scope) ?? []) {
3477
+ if (this.#sdkTerminalPublications.has(token)) continue;
3478
+ const publication = Promise.withResolvers<void>();
3479
+ void publication.promise.catch(() => undefined);
3480
+ this.#sdkTerminalPublications.set(token, publication);
3481
+ }
3461
3482
  }
3462
3483
  if (carryRecoverySkip) this.#skipPostPromptRecoveryWaitByAttemptScope.add(handle.scope);
3463
3484
  }
@@ -3607,6 +3628,7 @@ export class AgentSession {
3607
3628
  #restoreManagedFallbackGetApiKey: (() => void) | undefined;
3608
3629
  // Todo completion reminder state
3609
3630
  #todoReminderCount = 0;
3631
+ #todoReminderContinuationGeneration: number | undefined;
3610
3632
  #deepInterviewUserIntentEpoch = 0;
3611
3633
  #deepInterviewTurnOwnerEpoch = 0;
3612
3634
  #deepInterviewGenuineUserMessageEpochs = new WeakMap<object, number>();
@@ -3800,6 +3822,7 @@ export class AgentSession {
3800
3822
  #skipPostPromptRecoveryWaitByAttemptScope = new WeakSet<AttemptScope>();
3801
3823
  #sdkRunTokensByAttemptScope = new WeakMap<AttemptScope, string>();
3802
3824
  #sdkRunCohortsByAttemptScope = new WeakMap<AttemptScope, string[]>();
3825
+ #lifecycleScopesByAttemptScope = new WeakMap<AttemptScope, AttemptScope>();
3803
3826
  #activeSdkRunToken: string | undefined;
3804
3827
  #activeAttemptScope: AttemptScope | undefined;
3805
3828
  #attemptAuthority!: AttemptScopeAuthority;
@@ -4025,6 +4048,8 @@ export class AgentSession {
4025
4048
  // async event handlers settle. Subscribers treat agent_end as readiness, so
4026
4049
  // publishing it earlier lets a successor corrupt the prior prompt's lifecycle.
4027
4050
  #pendingAgentEndEmit: AgentSessionEvent | undefined;
4051
+ #agentEndContinuationDecisions = new WeakSet<AgentSessionEvent>();
4052
+ #pendingSdkAgentEnds = new Set<AgentSessionEvent>();
4028
4053
  // A scheduled continuation owns this terminal boundary until it either starts
4029
4054
  // the successor or proves it cannot. Holds prevent a false idle event while
4030
4055
  // preserving the predecessor for cancellation and preflight failures.
@@ -4035,6 +4060,8 @@ export class AgentSession {
4035
4060
  #agentEndPublicationInFlight = 0;
4036
4061
  #agentEndPublicationPromise: Promise<void> = Promise.resolve();
4037
4062
  #agentEndHandlingPromise: Promise<void> = Promise.resolve();
4063
+ #sdkTerminalPublications = new Map<string, PromiseWithResolvers<void>>();
4064
+ #publishingSdkTerminals = new Set<PromiseWithResolvers<void>>();
4038
4065
 
4039
4066
  #obfuscator: SecretObfuscator | undefined;
4040
4067
  #checkpointState: CheckpointState | undefined = undefined;
@@ -4073,11 +4100,7 @@ export class AgentSession {
4073
4100
  readonly rawSseDebugBuffer: RawSseDebugBuffer;
4074
4101
 
4075
4102
  #acquirePowerAssertion(): void {
4076
- if (
4077
- process.platform !== "darwin" &&
4078
- process.platform !== "linux" &&
4079
- process.platform !== "win32"
4080
- ) {
4103
+ if (process.platform !== "darwin" && process.platform !== "linux" && process.platform !== "win32") {
4081
4104
  return;
4082
4105
  }
4083
4106
  if (this.#powerAssertion || this.#powerAssertionLoad) return;
@@ -4707,8 +4730,21 @@ export class AgentSession {
4707
4730
  }
4708
4731
  }
4709
4732
 
4710
- #reserveDeferredAgentEndForContinuation(): symbol | undefined {
4711
- const pending = this.#pendingAgentEndEmit;
4733
+ #reserveDeferredAgentEndForContinuation(predecessorScope?: AttemptScope): symbol | undefined {
4734
+ const resourceRunId = this.#runResourceLeaseContext.getStore()?.resourceRunId;
4735
+ const pending = predecessorScope
4736
+ ? [...this.#pendingSdkAgentEnds].find(
4737
+ event => (event as AgentSessionEvent & { scope?: AttemptScope }).scope === predecessorScope,
4738
+ )
4739
+ : resourceRunId === undefined
4740
+ ? this.#pendingAgentEndEmit
4741
+ : ([...this.#pendingSdkAgentEnds].find(
4742
+ event => getAgentTerminalOwnerContext(event)?.resourceRunId === resourceRunId,
4743
+ ) ??
4744
+ (this.#pendingAgentEndEmit &&
4745
+ getAgentTerminalOwnerContext(this.#pendingAgentEndEmit)?.resourceRunId === resourceRunId
4746
+ ? this.#pendingAgentEndEmit
4747
+ : undefined));
4712
4748
  if (!pending) return undefined;
4713
4749
  const hold = Symbol("deferred-agent-end-continuation");
4714
4750
  this.#pendingAgentEndContinuationHolds.set(hold, pending);
@@ -4722,6 +4758,8 @@ export class AgentSession {
4722
4758
  throw new Error("Cannot restore a deferred agent_end over a different pending terminal event");
4723
4759
  }
4724
4760
  this.#pendingAgentEndEmit = pending;
4761
+ const scope = (pending as AgentSessionEvent & { scope?: AttemptScope }).scope;
4762
+ if (scope && this.#sdkRunTokensByAttemptScope.has(scope)) this.#pendingSdkAgentEnds.add(pending);
4725
4763
  const hold = Symbol("deferred-agent-end-continuation");
4726
4764
  this.#pendingAgentEndContinuationHolds.set(hold, pending);
4727
4765
  this.#deferredAgentEndWorkLeases.set(hold, this.#sessionWorkLease.acquire());
@@ -4732,6 +4770,7 @@ export class AgentSession {
4732
4770
  if (!hold) return undefined;
4733
4771
  const pending = this.#pendingAgentEndContinuationHolds.get(hold);
4734
4772
  this.#pendingAgentEndContinuationHolds.delete(hold);
4773
+ if (pending) this.#pendingSdkAgentEnds.delete(pending);
4735
4774
  this.#deferredAgentEndWorkLeases.get(hold)?.release();
4736
4775
  this.#deferredAgentEndWorkLeases.delete(hold);
4737
4776
  if (pending && this.#pendingAgentEndEmit === pending) {
@@ -4749,6 +4788,8 @@ export class AgentSession {
4749
4788
  }
4750
4789
 
4751
4790
  #restoreDeferredAgentEndAfterContinuationFailure(pending: AgentSessionEvent | undefined): void {
4791
+ const scope = (pending as (AgentSessionEvent & { scope?: AttemptScope }) | undefined)?.scope;
4792
+ if (pending && scope && this.#sdkRunTokensByAttemptScope.has(scope)) this.#pendingSdkAgentEnds.add(pending);
4752
4793
  if (pending && !this.#pendingAgentEndEmit) {
4753
4794
  this.#pendingAgentEndEmit = pending;
4754
4795
  }
@@ -4761,6 +4802,8 @@ export class AgentSession {
4761
4802
  ): void {
4762
4803
  if (!hold || !pending) return;
4763
4804
  this.#pendingAgentEndEmit = pending;
4805
+ const scope = (pending as AgentSessionEvent & { scope?: AttemptScope }).scope;
4806
+ if (scope && this.#sdkRunTokensByAttemptScope.has(scope)) this.#pendingSdkAgentEnds.add(pending);
4764
4807
  this.#pendingAgentEndContinuationHolds.set(hold, pending);
4765
4808
  }
4766
4809
 
@@ -5063,11 +5106,15 @@ export class AgentSession {
5063
5106
  this.#flushPendingAgentEnd();
5064
5107
  return flushError;
5065
5108
  }
5066
- async #settleEndedInFlight(token: symbol, promptWait?: "publication" | "full"): Promise<void> {
5109
+ async #settleEndedInFlight(
5110
+ token: symbol,
5111
+ promptWait?: "publication" | "full",
5112
+ sdkPublication?: Promise<void>,
5113
+ ): Promise<void> {
5067
5114
  const flushError = this.#endInFlight(token);
5068
5115
  const predecessorPromptStillInFlight = this.#livePromptsInFlight() > 0;
5069
5116
  if (promptWait === "publication") {
5070
- await this.#agentEndPublicationPromise;
5117
+ await (sdkPublication ?? this.#agentEndPublicationPromise);
5071
5118
  } else if (promptWait === "full") {
5072
5119
  await this.#agentEndHandlingPromise;
5073
5120
  await this.#waitForPostPromptRecovery();
@@ -5107,13 +5154,23 @@ export class AgentSession {
5107
5154
  }
5108
5155
 
5109
5156
  #flushPendingAgentEnd(): void {
5157
+ for (const sdkPending of this.#pendingSdkAgentEnds) {
5158
+ if (!this.#agentEndContinuationDecisions.has(sdkPending)) continue;
5159
+ if ([...this.#pendingAgentEndContinuationHolds.values()].includes(sdkPending)) continue;
5160
+ this.#pendingSdkAgentEnds.delete(sdkPending);
5161
+ if (this.#pendingAgentEndEmit === sdkPending) this.#pendingAgentEndEmit = undefined;
5162
+ const sdkLease = this.#deferredAgentEndLeases.get(sdkPending);
5163
+ this.#deferredAgentEndLeases.delete(sdkPending);
5164
+ this.#startAgentEndPublication(sdkPending, sdkLease);
5165
+ }
5166
+ const pending = this.#pendingAgentEndEmit;
5167
+ if (pending && this.#pendingSdkAgentEnds.has(pending)) return;
5110
5168
  if (
5111
5169
  this.#livePromptsInFlight() > 0 ||
5112
5170
  this.#agentEventHandlersInFlight > 0 ||
5113
5171
  this.#pendingAgentEndContinuationHolds.size > 0
5114
5172
  )
5115
5173
  return;
5116
- const pending = this.#pendingAgentEndEmit;
5117
5174
  if (!pending) {
5118
5175
  this.#resolveSessionSettlement();
5119
5176
  return;
@@ -5125,13 +5182,16 @@ export class AgentSession {
5125
5182
  }
5126
5183
 
5127
5184
  #startAgentEndPublication(pending: AgentSessionEvent, lease?: RunResourceProducerLease): void {
5185
+ this.#agentEndContinuationDecisions.delete(pending);
5128
5186
  this.#agentEndPublicationInFlight++;
5129
5187
  const pendingScope = (pending as AgentSessionEvent & { scope?: AttemptScopeRef }).scope as
5130
5188
  | AttemptScope
5131
5189
  | undefined;
5132
5190
  const sdkTerminal = pendingScope !== undefined && this.#sdkRunTokensByAttemptScope.has(pendingScope);
5133
5191
  this.#agentEndPublicationPromise = this.#publishDeferredAgentEnd(pending, lease, sdkTerminal);
5134
- void this.#agentEndPublicationPromise;
5192
+ void this.#agentEndPublicationPromise.catch(error => {
5193
+ logger.warn("Agent terminal publication failed", { error: sanitizePromptFailure(error) });
5194
+ });
5135
5195
  }
5136
5196
 
5137
5197
  async #publishDeferredAgentEnd(
@@ -5147,6 +5207,13 @@ export class AgentSession {
5147
5207
  const publicationCorrelationId = publicationScope
5148
5208
  ? this.#sdkRunTokensByAttemptScope.get(publicationScope)
5149
5209
  : undefined;
5210
+ const sdkPublications = (publicationScope ? (this.#sdkRunCohortsByAttemptScope.get(publicationScope) ?? []) : [])
5211
+ .map(token => ({ token, publication: this.#sdkTerminalPublications.get(token) }))
5212
+ .filter(
5213
+ (entry): entry is { token: string; publication: PromiseWithResolvers<void> } =>
5214
+ entry.publication !== undefined,
5215
+ );
5216
+ for (const { publication } of sdkPublications) this.#publishingSdkTerminals.add(publication);
5150
5217
  let extensionDelivery: Promise<void> | undefined;
5151
5218
  const releaseLease = () => {
5152
5219
  if (!lease) return;
@@ -5239,7 +5306,15 @@ export class AgentSession {
5239
5306
  try {
5240
5307
  if (lease) await this.#runResourceLeaseContext.run(lease, publish);
5241
5308
  else await publish();
5309
+ for (const { publication } of sdkPublications) publication.resolve();
5310
+ } catch (error) {
5311
+ for (const { publication } of sdkPublications) publication.reject(error);
5312
+ throw error;
5242
5313
  } finally {
5314
+ for (const { token, publication } of sdkPublications) {
5315
+ this.#publishingSdkTerminals.delete(publication);
5316
+ if (this.#sdkTerminalPublications.get(token) === publication) this.#sdkTerminalPublications.delete(token);
5317
+ }
5243
5318
  if (extensionDelivery) void extensionDelivery.then(releaseLease, releaseLease);
5244
5319
  else releaseLease();
5245
5320
  this.#agentEndPublicationInFlight = Math.max(0, this.#agentEndPublicationInFlight - 1);
@@ -5498,12 +5573,7 @@ export class AgentSession {
5498
5573
  controller !== undefined &&
5499
5574
  controller.chain.entries.length > 1;
5500
5575
  if (shouldCapture && modelKey) this.#managedFallbackActiveCredentialRows.delete(modelKey);
5501
- if (
5502
- override &&
5503
- model &&
5504
- storageProvider === override.storageProvider &&
5505
- modelKey === override.modelKey
5506
- ) {
5576
+ if (override && model && storageProvider === override.storageProvider && modelKey === override.modelKey) {
5507
5577
  this.#managedFallbackNextCredentialOverride = undefined;
5508
5578
  const apiKey = await this.#modelRegistry.getApiKey(model, this.credentialSessionId, {
5509
5579
  credentialSelector: { kind: "id", value: String(override.rowId) },
@@ -5513,10 +5583,7 @@ export class AgentSession {
5513
5583
  return apiKey;
5514
5584
  }
5515
5585
  // The preselected credential is no longer available; try to resolve another one of the same kind.
5516
- const fallbackApiKey = await this.#resolveManagedFallbackCredentialRow(
5517
- model,
5518
- override.credentialKind,
5519
- );
5586
+ const fallbackApiKey = await this.#resolveManagedFallbackCredentialRow(model, override.credentialKind);
5520
5587
  if (fallbackApiKey !== undefined) {
5521
5588
  if (shouldCapture && model) this.#captureManagedFallbackActiveCredential(model);
5522
5589
  return fallbackApiKey;
@@ -5524,8 +5591,7 @@ export class AgentSession {
5524
5591
  throw new Error("Selected managed fallback credential is no longer available");
5525
5592
  }
5526
5593
  const apiKey = await invokeOriginalGetApiKey(provider);
5527
- if (shouldCapture && model && isAuthenticated(apiKey))
5528
- this.#captureManagedFallbackActiveCredential(model);
5594
+ if (shouldCapture && model && isAuthenticated(apiKey)) this.#captureManagedFallbackActiveCredential(model);
5529
5595
  return apiKey;
5530
5596
  };
5531
5597
  this.agent.getApiKey = managedGetApiKey;
@@ -5612,12 +5678,14 @@ export class AgentSession {
5612
5678
  void this.#queueFollowUpAfterReservation(message, displayText, {
5613
5679
  createDisplayEntry: false,
5614
5680
  trackExternalFollowUp: false,
5681
+ sdkRunToken: this.#activeSdkRunToken,
5615
5682
  }).catch(error => {
5616
5683
  this.#settleDeliveredOwnedRegistrations([message]);
5617
5684
  logger.warn("Owned streaming follow-up was rejected", { error: String(error) });
5618
5685
  });
5619
5686
  },
5620
5687
  injectIdle: async (messages, signal) => {
5688
+ const sdkRunToken = this.#activeSdkRunToken;
5621
5689
  // Mandated boundary comment (corrected turn semantics): same origin
5622
5690
  // split as the streaming injector — an allowed owned-completion
5623
5691
  // delivery starts a fresh turn attempt/lineage and is not a
@@ -5652,14 +5720,14 @@ export class AgentSession {
5652
5720
  await this.agent.prompt(first, {
5653
5721
  ...this.#managedFallbackPromptOptions(),
5654
5722
  onRunAccepted: (handle: AttemptRunHandle) => {
5655
- if (handle) this.#acceptSdkAttemptRun(handle);
5723
+ if (handle) this.#acceptSdkAttemptRun(handle, sdkRunToken);
5656
5724
  },
5657
5725
  });
5658
5726
  } else {
5659
5727
  await this.agent.prompt(survivors, {
5660
5728
  ...this.#managedFallbackPromptOptions(),
5661
5729
  onRunAccepted: (handle: AttemptRunHandle) => {
5662
- if (handle) this.#acceptSdkAttemptRun(handle);
5730
+ if (handle) this.#acceptSdkAttemptRun(handle, sdkRunToken);
5663
5731
  },
5664
5732
  });
5665
5733
  }
@@ -5831,9 +5899,8 @@ export class AgentSession {
5831
5899
  (largestPhase, phase) =>
5832
5900
  Math.max(
5833
5901
  largestPhase,
5834
- phase.tasks.filter(
5835
- task => task.status === "pending" || task.status === "in_progress",
5836
- ).length,
5902
+ phase.tasks.filter(task => task.status === "pending" || task.status === "in_progress")
5903
+ .length,
5837
5904
  ),
5838
5905
  0,
5839
5906
  );
@@ -6747,6 +6814,17 @@ export class AgentSession {
6747
6814
  return { running, recent, delivery };
6748
6815
  }
6749
6816
 
6817
+ /**
6818
+ * Lifecycle status of every subagent owned by this session, read from the
6819
+ * manager's stable control-plane records (they outlive AsyncJob eviction).
6820
+ */
6821
+ getSubagentLifecycleStatuses(): SubagentLifecycle[] {
6822
+ const manager = this.#ownedAsyncJobManager ?? AsyncJobManager.instance();
6823
+ if (!manager) return [];
6824
+ const ownerFilter = this.#agentId ? { ownerId: this.#agentId } : undefined;
6825
+ return manager.getSubagentRecords(ownerFilter).map(record => record.status);
6826
+ }
6827
+
6750
6828
  /**
6751
6829
  * Cancel async jobs registered by *this* agent only. Used by lifecycle
6752
6830
  * transitions (newSession, switchSession, handoff, dispose) so a subagent
@@ -6922,6 +7000,8 @@ export class AgentSession {
6922
7000
  object,
6923
7001
  { scope?: AttemptScope; sdkRunToken?: string; persistGeneration: number; persistBarrier?: Promise<void> }
6924
7002
  >();
7003
+ /** Extension handlers cannot mutate or replace the Agent-claimed run owner. */
7004
+ #terminalOwnerByExtensionEvent = new WeakMap<object, AgentTerminalOwnerContext>();
6925
7005
 
6926
7006
  /**
6927
7007
  * Capture what is true at the SYNCHRONOUS agent-event boundary, before any async work.
@@ -7075,8 +7155,8 @@ export class AgentSession {
7075
7155
  const pendingAgentEnd =
7076
7156
  event.type === "agent_end" &&
7077
7157
  !maintenanceCheckpoint &&
7078
- (this.#pendingAgentEndEmit === event || this.#deferredAgentEndLeases.has(event))
7079
- ? this.#pendingAgentEndEmit
7158
+ (this.#pendingAgentEndEmit === event || this.#pendingSdkAgentEnds.has(event))
7159
+ ? event
7080
7160
  : undefined;
7081
7161
  if (pendingAgentEnd) {
7082
7162
  this.#deferredAgentEndLeases.set(pendingAgentEnd, eventLease);
@@ -7093,6 +7173,7 @@ export class AgentSession {
7093
7173
  }
7094
7174
  }
7095
7175
  this.#agentEventHandlersInFlight = Math.max(0, this.#agentEventHandlersInFlight - 1);
7176
+ if (event.type === "agent_end") this.#agentEndContinuationDecisions.add(event);
7096
7177
  this.#flushPendingAgentEnd();
7097
7178
  agentEndHandled?.resolve();
7098
7179
  // Every other in-flight counter republishes settlement when it drops; this
@@ -7329,16 +7410,10 @@ export class AgentSession {
7329
7410
  // have unwound. Subscribers treat this event as the ready signal; flushing it
7330
7411
  // from abort while either barrier is active permits a successor to race the
7331
7412
  // prior prompt's cleanup.
7332
- const sdkTerminal =
7333
- event.type === "agent_end" && attemptScope !== undefined && this.#sdkRunTokensByAttemptScope.has(attemptScope);
7334
- if (
7335
- event.type === "agent_end" &&
7336
- sdkTerminal &&
7337
- this.#pendingAgentEndContinuationHolds.size === 0 &&
7338
- this.#pendingAgentEndEmit === undefined
7339
- ) {
7413
+ if (event.type === "agent_end" && attemptScope && this.#sdkRunTokensByAttemptScope.has(attemptScope)) {
7414
+ this.#pendingSdkAgentEnds.add(event);
7340
7415
  if (eventLease) this.#deferredAgentEndLeases.set(event, eventLease);
7341
- this.#startAgentEndPublication(event, eventLease);
7416
+ this.#pendingAgentEndEmit = event;
7342
7417
  return;
7343
7418
  }
7344
7419
  if (event.type === "agent_end" && (this.#livePromptsInFlight() > 0 || this.#agentEventHandlersInFlight > 0)) {
@@ -7452,6 +7527,8 @@ export class AgentSession {
7452
7527
  eventLease?: RunResourceProducerLease,
7453
7528
  ): Promise<void> => {
7454
7529
  const attemptScope = (event as AgentEvent & { scope?: AttemptScope }).scope;
7530
+ const terminalSdkOwnership =
7531
+ event.type === "agent_end" ? this.#captureSdkContinuationOwnership(attemptScope) : undefined;
7455
7532
 
7456
7533
  // These lifecycle boundaries can be delivered without awaiting this listener.
7457
7534
  // Revoke streaming-edit cache generations before any admission, spill, or
@@ -8160,7 +8237,7 @@ export class AgentSession {
8160
8237
  skipCompactionCheck: true,
8161
8238
  resourceRunId: activePromptHandle,
8162
8239
  maintenanceContinuation: true,
8163
- sdkRunToken: attemptScope ? this.#sdkRunTokensByAttemptScope.get(attemptScope) : undefined,
8240
+ sdkOwnership: terminalSdkOwnership,
8164
8241
  });
8165
8242
  }
8166
8243
  return;
@@ -8244,7 +8321,9 @@ export class AgentSession {
8244
8321
  }
8245
8322
 
8246
8323
  // Check for retryable errors first (overloaded, rate limit, server errors)
8247
- if (this.#isRetryableError(msg)) {
8324
+ // Skip retry for todo reminder continuations - they should fail silently without retrying
8325
+ const isReminderContinuationError = this.#todoReminderContinuationGeneration === agentEndGeneration && agentEndGeneration !== undefined;
8326
+ if (!isReminderContinuationError && this.#isRetryableError(msg)) {
8248
8327
  const transportFailure = (msg as AssistantMessage & { transportFailure?: TransportFailureFacts })
8249
8328
  .transportFailure;
8250
8329
  const messageScope = this.#assistantAttemptScopes.get(msg);
@@ -8257,6 +8336,10 @@ export class AgentSession {
8257
8336
  );
8258
8337
  if (didRetry) return; // Retry was initiated, don't proceed to compaction
8259
8338
  }
8339
+ // Clear the reminder continuation flag after processing
8340
+ if (isReminderContinuationError) {
8341
+ this.#todoReminderContinuationGeneration = undefined;
8342
+ }
8260
8343
  if (this.#retryAttempt > 0) {
8261
8344
  // A prior retry ended on a non-retryable (terminal) message: emit
8262
8345
  // the terminal retry-end and reset so observers clear retry state.
@@ -8275,7 +8358,7 @@ export class AgentSession {
8275
8358
  const compactionTask = this.#schedulePostPromptTask(
8276
8359
  async () => {
8277
8360
  if (this.#isDisposed || this.#sessionAdmissionClosing) return;
8278
- await this.#checkCompaction(msg, true, undefined, activePromptHandle);
8361
+ await this.#checkCompaction(msg, true, undefined, activePromptHandle, undefined, terminalSdkOwnership);
8279
8362
  },
8280
8363
  { resourceRunId: activePromptHandle },
8281
8364
  );
@@ -8501,10 +8584,12 @@ export class AgentSession {
8501
8584
  /** Internal predecessor terminal event sequestered while waiting behind selection. */
8502
8585
  deferredPredecessorAgentEnd?: AgentSessionEvent;
8503
8586
  /** Internal causal SDK owner captured when this continuation was scheduled. */
8504
- sdkRunToken?: string;
8587
+ sdkOwnership?: SdkContinuationOwnership;
8588
+ /** Disable managed fallback retries for this continuation (used for terminal server-initiated turns). */
8589
+ disableManagedFallback?: boolean;
8505
8590
  }): Promise<void> {
8506
8591
  const continuationAdmission = this.#captureScheduledContinuationAdmission();
8507
- const scheduledSdkRunToken = options?.sdkRunToken;
8592
+ const scheduledSdkOwnership = options?.sdkOwnership;
8508
8593
  const selectionFenceGeneration =
8509
8594
  options?.selectionFenceGeneration ??
8510
8595
  this.#selectionFenceGenerationContext.getStore() ??
@@ -8526,7 +8611,7 @@ export class AgentSession {
8526
8611
  generation: deferredPromptGeneration,
8527
8612
  selectionFenceGeneration,
8528
8613
  deferredPredecessorAgentEnd,
8529
- sdkRunToken: scheduledSdkRunToken,
8614
+ sdkOwnership: scheduledSdkOwnership,
8530
8615
  });
8531
8616
  } finally {
8532
8617
  // The recursive call synchronously re-reserved its settlement
@@ -8646,14 +8731,21 @@ export class AgentSession {
8646
8731
  }
8647
8732
  const predecessorAgentEnd =
8648
8733
  this.#claimDeferredAgentEndForContinuation(predecessorAgentEndHold);
8734
+ const predecessorScope = (
8735
+ predecessorAgentEnd as (AgentSessionEvent & { scope?: AttemptScope }) | undefined
8736
+ )?.scope;
8737
+ const predecessorSdkOwnership = this.#captureSdkContinuationOwnership(predecessorScope);
8649
8738
  let predecessorAccepted = false;
8650
8739
  const releasePredecessor = () => {
8651
8740
  if (predecessorAccepted) return;
8652
8741
  predecessorAccepted = true;
8653
- this.#releaseDeferredAgentEndLease(predecessorAgentEnd);
8742
+ if (startsQueuedSuccessor)
8743
+ this.#restoreDeferredAgentEndAfterContinuationFailure(predecessorAgentEnd);
8744
+ else this.#releaseDeferredAgentEndLease(predecessorAgentEnd);
8654
8745
  };
8655
8746
  const hasQueuedMessages = this.agent.hasQueuedMessages();
8656
8747
  const startsQueuedSuccessor =
8748
+ options?.maintenanceContinuation !== true &&
8657
8749
  hasQueuedMessages &&
8658
8750
  (options?.continueQueuedOnly === true ||
8659
8751
  this.agent.state.messages.at(-1)?.role === "assistant");
@@ -8662,7 +8754,7 @@ export class AgentSession {
8662
8754
  : this.agent.continue.bind(this.agent);
8663
8755
  try {
8664
8756
  await continueQueued({
8665
- ...this.#managedFallbackPromptOptions(),
8757
+ ...(options?.disableManagedFallback ? { fallbackManaged: false } : this.#managedFallbackPromptOptions()),
8666
8758
  maintenanceContinuation: options?.maintenanceContinuation,
8667
8759
  // Reset only after continue() has claimed the queued turn. Skipped or stale
8668
8760
  // continuations retain predecessor accounting, and resetAttemptBudget keeps
@@ -8672,13 +8764,21 @@ export class AgentSession {
8672
8764
  // agent_start (review P1); their queued messages are in-run
8673
8765
  // consumptions, not own-run promotions.
8674
8766
  const startsOwn = options?.maintenanceContinuation !== true;
8675
- const inheritedSdkRunToken = options?.continueQueuedOnly
8676
- ? undefined
8677
- : (options?.sdkRunToken ?? scheduledSdkRunToken);
8767
+ const inheritedSdkOwnership =
8768
+ options?.continueQueuedOnly || startsQueuedSuccessor
8769
+ ? undefined
8770
+ : (scheduledSdkOwnership ?? predecessorSdkOwnership);
8771
+ const inheritedPredecessorScope =
8772
+ options?.continueQueuedOnly || startsQueuedSuccessor
8773
+ ? undefined
8774
+ : (inheritedSdkOwnership?.scope ?? predecessorScope);
8678
8775
  const consumedSdkRunTokens = acceptance.consumedQueuedMessages
8679
8776
  .map(message => this.#sdkRunTokensByQueuedMessage.get(message))
8680
8777
  .filter((token): token is string => token !== undefined);
8681
- const sdkRunToken = consumedSdkRunTokens[0] ?? inheritedSdkRunToken;
8778
+ const sdkRunToken = inheritedSdkOwnership?.token ?? consumedSdkRunTokens[0];
8779
+ const sdkRunTokens = inheritedSdkOwnership
8780
+ ? [...new Set([...inheritedSdkOwnership.cohort, ...consumedSdkRunTokens])]
8781
+ : consumedSdkRunTokens;
8682
8782
  this.#fireQueuedPromotionHooks(acceptance.consumedQueuedMessages, {
8683
8783
  startsOwnRun: startsOwn,
8684
8784
  });
@@ -8686,7 +8786,8 @@ export class AgentSession {
8686
8786
  this.#acceptSdkAttemptRun(
8687
8787
  handle,
8688
8788
  sdkRunToken,
8689
- consumedSdkRunTokens.length > 0 ? consumedSdkRunTokens : undefined,
8789
+ sdkRunTokens.length > 0 ? sdkRunTokens : undefined,
8790
+ inheritedPredecessorScope,
8690
8791
  );
8691
8792
  options?.onRunAccepted?.(handle);
8692
8793
  // Keep the queued token available through the acceptance callback;
@@ -8813,7 +8914,11 @@ export class AgentSession {
8813
8914
  return compactionSettings.autoContinue === false ? "auto_continue_disabled_non_resumable_tail" : undefined;
8814
8915
  }
8815
8916
 
8816
- async #scheduleOverflowRetryContinuation(generation: number, resourceRunId?: string): Promise<boolean> {
8917
+ async #scheduleOverflowRetryContinuation(
8918
+ generation: number,
8919
+ resourceRunId?: string,
8920
+ sdkOwnership?: SdkContinuationOwnership,
8921
+ ): Promise<boolean> {
8817
8922
  this.#stripOverflowFailedTurnForRetry();
8818
8923
  const snapshot = await this.#compactionStateSnapshot();
8819
8924
  if (
@@ -8830,6 +8935,7 @@ export class AgentSession {
8830
8935
  generation,
8831
8936
  suppressPredecessorAgentEnd: true,
8832
8937
  resourceRunId,
8938
+ sdkOwnership,
8833
8939
  onSkip: reason => this.#logCompactionContinuationSkipped("overflow_retry", reason),
8834
8940
  onError: error => this.#logCompactionContinuationError("overflow_retry", error),
8835
8941
  });
@@ -8838,7 +8944,7 @@ export class AgentSession {
8838
8944
 
8839
8945
  const compactionSettings = this.settings.getGroup("compaction");
8840
8946
  if (compactionSettings.autoContinue !== false) {
8841
- this.#scheduleAutoContinuePrompt(generation, false, resourceRunId);
8947
+ this.#scheduleAutoContinuePrompt(generation, false, resourceRunId, undefined, undefined, sdkOwnership);
8842
8948
  return true;
8843
8949
  }
8844
8950
 
@@ -8852,7 +8958,7 @@ export class AgentSession {
8852
8958
  resourceRunId?: string,
8853
8959
  deferredSelectionFenceGeneration?: number,
8854
8960
  deferredPredecessorAgentEnd?: AgentSessionEvent,
8855
- sdkRunToken?: string,
8961
+ sdkOwnership?: SdkContinuationOwnership,
8856
8962
  ): void {
8857
8963
  if (this.#sessionTransitionKind !== undefined || this.#handoffTransitionActive) {
8858
8964
  this.#deferredAutoContinueDuringTransition = () =>
@@ -8862,14 +8968,16 @@ export class AgentSession {
8862
8968
  resourceRunId,
8863
8969
  deferredSelectionFenceGeneration,
8864
8970
  deferredPredecessorAgentEnd,
8865
- sdkRunToken,
8971
+ sdkOwnership,
8866
8972
  );
8867
8973
  return;
8868
8974
  }
8869
8975
  const scheduledGeneration = generation;
8870
- const scheduledSdkRunToken =
8871
- sdkRunToken ??
8872
- (this.#activeAttemptScope ? this.#sdkRunTokensByAttemptScope.get(this.#activeAttemptScope) : undefined);
8976
+ const scheduledSdkOwnership =
8977
+ sdkOwnership ??
8978
+ this.#captureSdkContinuationOwnership(
8979
+ (deferredPredecessorAgentEnd as (AgentSessionEvent & { scope?: AttemptScope }) | undefined)?.scope,
8980
+ );
8873
8981
  const continuationAuthorized = async (
8874
8982
  signal: AbortSignal,
8875
8983
  hasPendingNextTurnMessages = false,
@@ -8926,7 +9034,7 @@ export class AgentSession {
8926
9034
  resourceRunId,
8927
9035
  selectionFenceGeneration,
8928
9036
  predecessorAgentEnd,
8929
- scheduledSdkRunToken,
9037
+ scheduledSdkOwnership,
8930
9038
  );
8931
9039
  } finally {
8932
9040
  this.#endSelectionFenceDeferralTracking(selectionFenceGeneration);
@@ -8974,7 +9082,9 @@ export class AgentSession {
8974
9082
  promptText,
8975
9083
  {
8976
9084
  skipPostPromptRecoveryWait: true,
8977
- sdkRunToken: scheduledSdkRunToken,
9085
+ sdkRunToken: scheduledSdkOwnership?.token,
9086
+ sdkRunTokens: scheduledSdkOwnership?.cohort,
9087
+ sdkPredecessorScope: scheduledSdkOwnership?.scope,
8978
9088
  skipCompactionCheck: true,
8979
9089
  predecessorAgentEndHold,
8980
9090
  onFinalPreflight: ({ hasPendingNextTurnMessages }) =>
@@ -9910,7 +10020,12 @@ export class AgentSession {
9910
10020
  type: "agent_start",
9911
10021
  ...(sdkRunToken ? { sdkRunToken } : {}),
9912
10022
  ...(deliveryScope
9913
- ? { sdkRunTokens: this.#sdkRunCohortsByAttemptScope.get(deliveryScope as AttemptScope) }
10023
+ ? {
10024
+ sdkRunTokens: this.#sdkRunCohortsByAttemptScope.get(deliveryScope as AttemptScope),
10025
+ lifecycleScope:
10026
+ this.#lifecycleScopesByAttemptScope.get(deliveryScope as AttemptScope) ??
10027
+ (deliveryScope as AttemptScope),
10028
+ }
9914
10029
  : {}),
9915
10030
  },
9916
10031
  undefined,
@@ -9928,17 +10043,19 @@ export class AgentSession {
9928
10043
  deliveryScope,
9929
10044
  );
9930
10045
  } else if (event.type === "agent_end") {
9931
- await this.#extensionRunner.emit(
9932
- {
9933
- type: "agent_end",
9934
- messages: event.messages,
9935
- stopReason: event.stopReason,
9936
- maintenanceOutcome: event.maintenanceOutcome,
9937
- ...(sdkRunToken ? { sdkRunToken } : {}),
9938
- },
9939
- undefined,
9940
- deliveryScope,
9941
- );
10046
+ const extensionEvent = {
10047
+ type: "agent_end" as const,
10048
+ messages: event.messages,
10049
+ stopReason: event.stopReason,
10050
+ maintenanceOutcome: event.maintenanceOutcome,
10051
+ ...(sdkRunToken ? { sdkRunToken } : {}),
10052
+ };
10053
+ // Keep the Agent-claimed owner OUTSIDE the public extension event and
10054
+ // its replaceable side channel. Only the owning session may attest
10055
+ // this exact projected event to the SDK bus.
10056
+ const terminalOwner = getAgentTerminalOwnerContext(event);
10057
+ if (terminalOwner) this.#terminalOwnerByExtensionEvent.set(extensionEvent, terminalOwner);
10058
+ await this.#extensionRunner.emit(extensionEvent, undefined, deliveryScope);
9942
10059
  } else if (event.type === "turn_start") {
9943
10060
  const hookEvent: TurnStartEvent = {
9944
10061
  type: "turn_start",
@@ -10145,6 +10262,22 @@ export class AgentSession {
10145
10262
  this.#abortActiveMidRunBarriers();
10146
10263
  this.#compactionQueuedDeliveryArmed = false;
10147
10264
  if (this.#unsubscribeAgent) {
10265
+ // This accepted cohort cannot observe its terminal after the bridge is
10266
+ // removed. Reject only that captured owner; queued roots and already
10267
+ // publishing terminals retain their independently owned publication.
10268
+ const ownership = this.#captureSdkContinuationOwnership(this.#activeAttemptScope);
10269
+ if (ownership) {
10270
+ const failure = Object.assign(
10271
+ new Error("Agent event bridge disconnected before SDK terminal publication."),
10272
+ { code: "cancelled" },
10273
+ );
10274
+ for (const token of ownership.cohort) {
10275
+ const publication = this.#sdkTerminalPublications.get(token);
10276
+ if (!publication || this.#publishingSdkTerminals.has(publication)) continue;
10277
+ publication.reject(failure);
10278
+ this.#sdkTerminalPublications.delete(token);
10279
+ }
10280
+ }
10148
10281
  this.#unsubscribeAgent();
10149
10282
  this.#unsubscribeAgent = undefined;
10150
10283
  }
@@ -10302,6 +10435,11 @@ export class AgentSession {
10302
10435
  // settle every accepted SDK handle now so consumed submissions cannot wait
10303
10436
  // forever for an agent_end that teardown deliberately suppresses.
10304
10437
  this.#terminalizeQueuedSdkWorkForSessionTransition(this.#queuedMessagesForSessionTransition());
10438
+ const publicationFailure = Object.assign(new Error("Session disposed before SDK terminal publication."), {
10439
+ code: "cancelled",
10440
+ });
10441
+ for (const publication of this.#sdkTerminalPublications.values()) publication.reject(publicationFailure);
10442
+ this.#sdkTerminalPublications.clear();
10305
10443
  // Invalidate every coordinator event admitted before disposal. Handlers may
10306
10444
  // still unwind, but their captured generation can no longer enqueue a write.
10307
10445
  this.#coordinatorPersistGeneration += 1;
@@ -10671,6 +10809,11 @@ export class AgentSession {
10671
10809
  * at `timeoutMs` so a wedged subprocess can't stall process exit.
10672
10810
  */
10673
10811
  async disposeChildSubprocesses(timeoutMs = SIGNAL_TEARDOWN_TIMEOUT_MS): Promise<void> {
10812
+ this.#evalExecutionDisposing = true;
10813
+ this.abortEval();
10814
+ const evalExecutionsSettled = this.#waitForEvalExecutionsToSettle(timeoutMs).then(settled => {
10815
+ if (!settled) logger.warn("signal teardown: active eval executions remain unsettled");
10816
+ });
10674
10817
  const sessionId = this.sessionManager.getSessionId();
10675
10818
  const kernelOwnerId = this.#evalKernelOwnerId;
10676
10819
  this.#unregisterResourceGc?.();
@@ -10680,6 +10823,7 @@ export class AgentSession {
10680
10823
  this.#unregisterDelegationHintSettings?.();
10681
10824
  this.#unregisterDelegationHintSettings = undefined;
10682
10825
  const work = Promise.allSettled([
10826
+ evalExecutionsSettled,
10683
10827
  // kill:true so a forced exit also reaps spawned-app Chrome we own (headless
10684
10828
  // always closes; connected/attached browsers only disconnect — never killed).
10685
10829
  releaseTabsForOwner(sessionId, { kill: true }).catch((error: unknown) =>
@@ -10708,7 +10852,8 @@ export class AgentSession {
10708
10852
  logger.warn("signal teardown: tool session transition cleanups failed", { error }),
10709
10853
  ),
10710
10854
  ]);
10711
- await Promise.race([work, Bun.sleep(timeoutMs)]);
10855
+ const completed = await Promise.race([work.then(() => true), Bun.sleep(timeoutMs).then(() => false)]);
10856
+ if (!completed) logger.warn("signal teardown: cleanup exceeded its bounded caller deadline");
10712
10857
  }
10713
10858
 
10714
10859
  #rebindProviderSessionState(providerSessionState: Map<string, ProviderSessionState>): void {
@@ -12357,7 +12502,9 @@ export class AgentSession {
12357
12502
  */
12358
12503
  get messages(): AgentMessage[] {
12359
12504
  const messages = this.agent.state.messages;
12360
- return messages.some(message => message.role === "custom" && this.#isEphemeralCustomMessageType(message.customType))
12505
+ return messages.some(
12506
+ message => message.role === "custom" && this.#isEphemeralCustomMessageType(message.customType),
12507
+ )
12361
12508
  ? this.#withoutEphemeralCustomMessages(messages)
12362
12509
  : messages;
12363
12510
  }
@@ -13498,6 +13645,36 @@ export class AgentSession {
13498
13645
  }
13499
13646
  }
13500
13647
 
13648
+ /**
13649
+ * The loaded skill a prompt's text dispatches to, or undefined when `prompt`
13650
+ * would submit it as ordinary text (unknown or disabled `/skill:` names,
13651
+ * chains, or attached images). Single source for dispatch and for callers
13652
+ * that must mirror it, such as startup title seeding.
13653
+ */
13654
+ resolvePromptSkillInvocation(text: string, images?: readonly unknown[]): ParsedSkillInvocation | undefined {
13655
+ if (images?.length || !text.startsWith("/") || !isNamespacedSkillSlashCommandName(text.slice(1))) return;
13656
+ const skillCommands = new Map(this.skills.map(skill => [getSkillSlashCommandName(skill), skill]));
13657
+ const invocations = parseSkillInvocations(text, skillCommands);
13658
+ return invocations.length === 1 ? invocations[0] : undefined;
13659
+ }
13660
+
13661
+ /**
13662
+ * Whether `prompt` intercepts this slash text as an extension or custom/MCP
13663
+ * command. Used to keep command arguments away from the title model. Mirrors the
13664
+ * command lookup in `#tryExecuteExtensionCommand` / `#tryExecuteCustomCommand`
13665
+ * without executing anything.
13666
+ */
13667
+ isLocallyHandledSlashCommand(text: string): boolean {
13668
+ if (!text.startsWith("/")) return false;
13669
+ const spaceIndex = text.indexOf(" ");
13670
+ const commandName = spaceIndex === -1 ? text.slice(1) : text.slice(1, spaceIndex);
13671
+ return (
13672
+ this.#extensionRunner?.getCommand(commandName) !== undefined ||
13673
+ this.#customCommands.some(c => c.command.name === commandName) ||
13674
+ this.#mcpPromptCommands.some(c => c.command.name === commandName)
13675
+ );
13676
+ }
13677
+
13501
13678
  async #promptInternal(
13502
13679
  text: string,
13503
13680
  options: PromptOptions | undefined,
@@ -13519,38 +13696,29 @@ export class AgentSession {
13519
13696
  if (owner && !owner.released) throw this.#sessionAdmissionBusyError();
13520
13697
  const expandPromptTemplates = options?.expandPromptTemplates ?? true;
13521
13698
 
13522
- if (
13523
- expandPromptTemplates &&
13524
- text.startsWith("/") &&
13525
- isNamespacedSkillSlashCommandName(text.slice(1)) &&
13526
- !options?.images?.length
13527
- ) {
13528
- const skillCommands = new Map(this.skills.map(skill => [getSkillSlashCommandName(skill), skill]));
13529
- const invocations = parseSkillInvocations(text, skillCommands);
13530
- if (invocations.length === 1) {
13531
- const invocation = invocations[0];
13532
- if (invocation) {
13533
- await this.invokeSkill(
13534
- invocation.skill.name,
13535
- invocation.args,
13536
- options?.onPreflightAccepted ||
13537
- options?.onPreflightAcceptCommit ||
13538
- options?.preflightSignal ||
13539
- internalOptions?.sdkRunToken
13540
- ? {
13541
- ...(options?.onPreflightAccepted
13542
- ? { onPreflightAccepted: options.onPreflightAccepted }
13543
- : {}),
13544
- ...(options?.onPreflightAcceptCommit
13545
- ? { onPreflightAcceptCommit: options.onPreflightAcceptCommit }
13546
- : {}),
13547
- ...(options?.preflightSignal ? { preflightSignal: options.preflightSignal } : {}),
13548
- ...(options?.sdkRunCapability ? { sdkRunCapability: options.sdkRunCapability } : {}),
13549
- }
13550
- : undefined,
13551
- );
13552
- return;
13553
- }
13699
+ if (expandPromptTemplates) {
13700
+ const invocation = this.resolvePromptSkillInvocation(text, options?.images);
13701
+ if (invocation) {
13702
+ await this.invokeSkill(
13703
+ invocation.skill.name,
13704
+ invocation.args,
13705
+ options?.onPreflightAccepted ||
13706
+ options?.onPreflightAcceptCommit ||
13707
+ options?.preflightSignal ||
13708
+ internalOptions?.sdkRunToken
13709
+ ? {
13710
+ ...(options?.onPreflightAccepted
13711
+ ? { onPreflightAccepted: options.onPreflightAccepted }
13712
+ : {}),
13713
+ ...(options?.onPreflightAcceptCommit
13714
+ ? { onPreflightAcceptCommit: options.onPreflightAcceptCommit }
13715
+ : {}),
13716
+ ...(options?.preflightSignal ? { preflightSignal: options.preflightSignal } : {}),
13717
+ ...(options?.sdkRunCapability ? { sdkRunCapability: options.sdkRunCapability } : {}),
13718
+ }
13719
+ : undefined,
13720
+ );
13721
+ return;
13554
13722
  }
13555
13723
  }
13556
13724
 
@@ -13967,6 +14135,8 @@ export class AgentSession {
13967
14135
  | "preflightSignal"
13968
14136
  > & {
13969
14137
  sdkRunToken?: string;
14138
+ sdkRunTokens?: string[];
14139
+ sdkPredecessorScope?: AttemptScope;
13970
14140
  prependMessages?: AgentMessage[];
13971
14141
  skipPostPromptRecoveryWait?: boolean;
13972
14142
  predecessorAgentEndHold?: symbol;
@@ -14037,6 +14207,7 @@ export class AgentSession {
14037
14207
  let skipPostPromptRecoveryWait =
14038
14208
  options?.skipPostPromptRecoveryWait === true || options?.sdkRunToken !== undefined;
14039
14209
  let promptAttemptScope: AttemptScope | undefined;
14210
+ let promptSdkPublication: Promise<void> | undefined;
14040
14211
  let hindsightRecall: string | undefined;
14041
14212
  try {
14042
14213
  this.#throwIfPromptPreflightCancelled(generation, preflightSignal);
@@ -14337,10 +14508,26 @@ export class AgentSession {
14337
14508
  ...(options?.sdkRunToken ? { sdkRunToken: options.sdkRunToken } : {}),
14338
14509
  onRunAccepted: (handle: AttemptRunHandle) => {
14339
14510
  promptAttemptScope = handle?.scope;
14340
- if (handle) this.#acceptSdkAttemptRun(handle, options?.sdkRunToken);
14511
+ // A synthetic recovery prompt can make Agent retire the old managed
14512
+ // run at method entry. Suppress only that captured predecessor once
14513
+ // the replacement accepts; pre-acceptance failures still publish it.
14514
+ if (handle && options?.sdkPredecessorScope) {
14515
+ const predecessor = this.#claimDeferredAgentEndForContinuation(
14516
+ this.#reserveDeferredAgentEndForContinuation(options.sdkPredecessorScope),
14517
+ );
14518
+ this.#releaseDeferredAgentEndLease(predecessor);
14519
+ }
14520
+ if (handle)
14521
+ this.#acceptSdkAttemptRun(
14522
+ handle,
14523
+ options?.sdkRunToken,
14524
+ options?.sdkRunTokens,
14525
+ options?.sdkPredecessorScope,
14526
+ );
14341
14527
  else this.#acceptRunHandle(handle);
14342
14528
  if (handle && options?.sdkRunToken) {
14343
14529
  this.#activeSdkRunToken = options.sdkRunToken;
14530
+ promptSdkPublication = this.#sdkTerminalPublications.get(options.sdkRunToken)?.promise;
14344
14531
  }
14345
14532
  options?.onRunAccepted?.(handle);
14346
14533
  options?.admissionLease?.release();
@@ -14423,7 +14610,11 @@ export class AgentSession {
14423
14610
  this.#releaseIrcRosterClaim(rosterClaim.token, rosterClaim.epoch);
14424
14611
  }
14425
14612
  this.#releaseDeferredAgentEndContinuation(predecessorAgentEndHold);
14426
- await this.#settleEndedInFlight(inFlightPrompt, skipPostPromptRecoveryWait ? "publication" : "full");
14613
+ await this.#settleEndedInFlight(
14614
+ inFlightPrompt,
14615
+ skipPostPromptRecoveryWait ? "publication" : "full",
14616
+ promptSdkPublication,
14617
+ );
14427
14618
  }
14428
14619
  }
14429
14620
 
@@ -14711,10 +14902,7 @@ export class AgentSession {
14711
14902
  // prompt still in preflight, aborting) has no run to steer. Ordinary steers
14712
14903
  // become a sequential follow-up owned by the next turn; SDK turn-bound
14713
14904
  // steers fail closed rather than being delivered to a successor run.
14714
- if (
14715
- options?.expectedSdkRunToken !== undefined &&
14716
- this.#activeSdkRunToken !== options.expectedSdkRunToken
14717
- )
14905
+ if (options?.expectedSdkRunToken !== undefined && this.#activeSdkRunToken !== options.expectedSdkRunToken)
14718
14906
  throw Object.assign(new Error("The expected SDK run is not active."), { code: "turn_not_active" });
14719
14907
  const admission = this.agent.steer(message, options?.forceOneAtATime ? { forceOneAtATime: true } : undefined);
14720
14908
  if (!admission.admitted) {
@@ -14771,8 +14959,8 @@ export class AgentSession {
14771
14959
  /**
14772
14960
  * One-shot preflight-abort binding: an invocation cancelled after its queue
14773
14961
  * admission must cancel the submission it admitted, so an aborted dispatch
14774
- * can never execute later. Both explicit delivery paths (follow-up and
14775
- * steer) share this helper so their cancellation semantics cannot diverge
14962
+ * can never execute later. Explicit and implicitly diverted queue admissions
14963
+ * share this helper so their cancellation semantics cannot diverge
14776
14964
  * (exact-head review P1).
14777
14965
  */
14778
14966
  #bindPreflightAbortCancellation(
@@ -15453,6 +15641,7 @@ export class AgentSession {
15453
15641
  forceOneAtATime: sequential !== undefined,
15454
15642
  createDisplayEntry: false,
15455
15643
  trackExternalFollowUp: false,
15644
+ sdkRunToken: this.#activeSdkRunToken,
15456
15645
  });
15457
15646
  if (this.#abortUnwind && !sequential) this.#abortUnwindSteerFallbacks.push(appMessage);
15458
15647
  // The chip now describes follow-up work: keep its mode and identity
@@ -15483,6 +15672,7 @@ export class AgentSession {
15483
15672
  forceOneAtATime: options?.followUpQueuePolicy === "sequential",
15484
15673
  createDisplayEntry: false,
15485
15674
  trackExternalFollowUp: false,
15675
+ sdkRunToken: this.#activeSdkRunToken,
15486
15676
  });
15487
15677
  this.#bindCustomDisplayEntry(appMessage, "followUp");
15488
15678
  // The session can report streaming while no agent loop owns the queue
@@ -15904,12 +16094,13 @@ export class AgentSession {
15904
16094
  if (this.#isLiveTurnBusy() && !waitedForAbortUnwind) {
15905
16095
  if (options?.onPreflightAcceptCommit) await options.onPreflightAcceptCommit();
15906
16096
  assertPreflightStillOpen();
15907
- await this.#queueSteer(text, images, {
16097
+ const queuedSteer = await this.#queueSteer(text, images, {
15908
16098
  claimsGenuineUserIntent: true,
15909
- onPromoted: options?.onQueuedPromoted,
16099
+ onPromoted: onQueuedPromoted,
15910
16100
  external: true,
15911
16101
  sdkRunToken: internalOptions?.sdkRunToken,
15912
16102
  });
16103
+ this.#bindPreflightAbortCancellation(options?.preflightSignal, queuedSteer);
15913
16104
  // Dispatch-race disposition (#4668 review P1): the SDK snapshot-decided
15914
16105
  // this submission starts its own turn (idle at dispatch), but the
15915
16106
  // session began streaming before sendUserMessage ran, so the message
@@ -15919,8 +16110,15 @@ export class AgentSession {
15919
16110
  // would terminalize the accepted request as an own-run completion
15920
16111
  // before it is consumed. Report the internal in-run disposition so the
15921
16112
  // runtime attaches the correlation to the in-flight run instead.
15922
- options?.onDispatchDisposition?.({ startsOwnRun: false });
15923
- options?.onPreflightAccepted?.();
16113
+ try {
16114
+ assertPreflightStillOpen();
16115
+ options?.onDispatchDisposition?.({ startsOwnRun: false });
16116
+ options?.onPreflightAccepted?.();
16117
+ assertPreflightStillOpen();
16118
+ } catch (error) {
16119
+ queuedSteer.cancel();
16120
+ throw error;
16121
+ }
15924
16122
  return;
15925
16123
  }
15926
16124
 
@@ -16851,6 +17049,12 @@ export class AgentSession {
16851
17049
  if (!lineageIdHash) return undefined;
16852
17050
  return this.#promptGeneration;
16853
17051
  }
17052
+ getTerminalRunOwnerForEvent(event: object): AgentTerminalOwnerContext | undefined {
17053
+ return this.#terminalOwnerByExtensionEvent.get(event);
17054
+ }
17055
+ getRunOwnerDomain(handle: string): RunCancellationDomain | undefined {
17056
+ return this.agent.resourceLedger.lookupDomain(handle);
17057
+ }
16854
17058
  /**
16855
17059
  * Logical endpoint used to key owned-registration lineage bindings.
16856
17060
  * Subagents inherit the parent's AsyncJobManager, whose completion callback
@@ -18009,8 +18213,12 @@ export class AgentSession {
18009
18213
  }
18010
18214
 
18011
18215
  options?.onMutationStarted?.();
18012
- this.#setModelAuthoritatively(model, options?.cause ?? "user-selection");
18216
+ const cause = options?.cause ?? "user-selection";
18217
+ if (cause === "user-selection") this.markUserModelSelection();
18218
+ this.#setModelAuthoritatively(model, cause);
18219
+ if (cause === "user-selection") this.#unavailableModelProfile = undefined;
18013
18220
  this.#seedSessionCanonicalVariant(model);
18221
+ if (cause === "user-selection") this.#recordUserCanonicalVariantSelection();
18014
18222
  this.sessionManager.appendModelChange(`${model.provider}/${model.id}`, role);
18015
18223
  this.settings.setModelRole(
18016
18224
  role,
@@ -18018,7 +18226,7 @@ export class AgentSession {
18018
18226
  );
18019
18227
  // Only an explicit user selection starts a new fallback epoch. Internal
18020
18228
  // fallback switches must preserve the exhausted-model set while advancing.
18021
- if (role === "default" && (options?.cause ?? "user-selection") === "user-selection") {
18229
+ if (role === "default" && cause === "user-selection") {
18022
18230
  this.#fallbackTransitionGeneration++;
18023
18231
  this.#defaultFallbackController = undefined;
18024
18232
  this.#defaultFallbackExhaustedLastTurn = false;
@@ -18055,12 +18263,41 @@ export class AgentSession {
18055
18263
 
18056
18264
  setActiveModelProfile(name: string | undefined): void {
18057
18265
  this.#activeModelProfile = name;
18266
+ if (name !== undefined) {
18267
+ this.#unavailableModelProfile = undefined;
18268
+ }
18058
18269
  }
18059
18270
 
18060
18271
  getActiveModelProfile(): string | undefined {
18061
18272
  return this.#activeModelProfile;
18062
18273
  }
18063
18274
 
18275
+ setUnavailableModelProfile(name: string | undefined): void {
18276
+ this.#unavailableModelProfile = name;
18277
+ }
18278
+
18279
+ getUnavailableModelProfile(): string | undefined {
18280
+ return this.#unavailableModelProfile;
18281
+ }
18282
+
18283
+ /** Revision fence for deferred activation after a user model selection. */
18284
+ getUserModelSelectionRevision(): number {
18285
+ return this.#userModelSelectionRevision;
18286
+ }
18287
+
18288
+ /** Latest concrete user choice that seeded or cleared the session's sticky canonical variant. */
18289
+ getUserCanonicalVariantSelection(): { revision: number; canonicalVariant: string | undefined } {
18290
+ return {
18291
+ revision: this.#userCanonicalVariantSelectionRevision,
18292
+ canonicalVariant: this.#userCanonicalVariantSelection,
18293
+ };
18294
+ }
18295
+
18296
+ /** Fence deferred startup profile recovery before an explicit control-surface selection. */
18297
+ markUserModelSelection(): void {
18298
+ this.#userModelSelectionRevision++;
18299
+ }
18300
+
18064
18301
  /**
18065
18302
  * Re-apply vendor-separated delegation after a profile activation changed the
18066
18303
  * role layer. `eagerTasks` is resolved at prompt build time, but under
@@ -18222,12 +18459,15 @@ export class AgentSession {
18222
18459
  * Session-scoped only: does not persist `modelProfile.default`.
18223
18460
  */
18224
18461
  async activateModelProfileForControl(profileName: string): Promise<boolean> {
18225
- await activateModelProfile({
18226
- session: this,
18227
- modelRegistry: this.#modelRegistry,
18228
- settings: this.settings,
18229
- profileName,
18230
- });
18462
+ this.markUserModelSelection();
18463
+ await this.withSdkControlMutation(() =>
18464
+ activateModelProfile({
18465
+ session: this,
18466
+ modelRegistry: this.#modelRegistry,
18467
+ settings: this.settings,
18468
+ profileName,
18469
+ }),
18470
+ );
18231
18471
  return this.getActiveModelProfile() === profileName;
18232
18472
  }
18233
18473
 
@@ -18262,6 +18502,7 @@ export class AgentSession {
18262
18502
  onAfterActivation?: () => void;
18263
18503
  },
18264
18504
  ): Promise<{ changed: boolean; id: string }> {
18505
+ this.markUserModelSelection();
18265
18506
  // Do not hold selection admission while waiting for a scheduled continuation:
18266
18507
  // the continuation may need prompt admission to settle the current turn.
18267
18508
  await this.waitForIdle();
@@ -18540,6 +18781,7 @@ export class AgentSession {
18540
18781
  reason?: TemporaryModelReason;
18541
18782
  providerSessionScope?: TemporaryProviderSessionScope;
18542
18783
  signal?: AbortSignal;
18784
+ shouldMutate?: () => boolean;
18543
18785
  onMutationStarted?: () => void;
18544
18786
  },
18545
18787
  // biome-ignore lint/suspicious/noConfusingVoidType: Existing session adapters return Promise<void>; a scope is optional.
@@ -18558,6 +18800,8 @@ export class AgentSession {
18558
18800
  throw new Error(`No API key for ${model.provider}/${model.id}`);
18559
18801
  }
18560
18802
  if (suppliedScope && this.#temporaryProviderSessionScopes.at(-1)?.token !== suppliedScope) return;
18803
+ if (options?.shouldMutate && !options.shouldMutate()) return;
18804
+ if (options?.cause === "user-selection") this.markUserModelSelection();
18561
18805
  options?.onMutationStarted?.();
18562
18806
 
18563
18807
  const isTemporaryOperation = options?.cause === undefined || options.cause === "temporary-operation";
@@ -18584,6 +18828,9 @@ export class AgentSession {
18584
18828
  } else {
18585
18829
  this.#setModelAuthoritatively(model, options?.cause ?? "temporary-operation");
18586
18830
  }
18831
+ if (options?.cause === "user-selection") {
18832
+ this.#unavailableModelProfile = undefined;
18833
+ }
18587
18834
  this.sessionManager.appendModelChange(
18588
18835
  `${model.provider}/${model.id}`,
18589
18836
  options?.persistAsSessionDefault ? "default" : "temporary",
@@ -18591,12 +18838,36 @@ export class AgentSession {
18591
18838
  this.settings.getStorage()?.recordModelUsage(`${model.provider}/${model.id}`);
18592
18839
  if (options?.persistAsSessionDefault) {
18593
18840
  this.#seedSessionCanonicalVariant(model);
18841
+ if (options.cause === "user-selection") this.#recordUserCanonicalVariantSelection();
18594
18842
  }
18595
18843
 
18596
18844
  // Apply explicit thinking level if given; otherwise prefer the model's
18597
18845
  // configured defaultLevel; otherwise re-clamp the current level.
18598
18846
  this.setThinkingLevel(thinkingLevel ?? model.thinking?.defaultLevel ?? this.thinkingLevel);
18599
- if (options?.persistAsSessionDefault === true) this.#clearActiveModelProfileForConcreteDefault(options?.cause);
18847
+ if (options?.persistAsSessionDefault === true && options.cause !== "profile-activation") {
18848
+ // Concrete model selection clears session-scoped profile state (#5919).
18849
+ // Materialize durable profiles first (if persisted and active), then reset
18850
+ // without force to preserve durable semantics.
18851
+ if (this.model && this.settings.get("modelProfile.default") !== undefined) {
18852
+ this.materializeActiveDefaultModelProfileAssignment(this.model);
18853
+ }
18854
+ this.#resetSessionScopedModelProfileState({ preserveDefaultConfiguredChain: true });
18855
+ // For user-selection and startup-override causes, also clear stale persisted
18856
+ // defaults via the legacy path when there is no ownership record.
18857
+ if (options.cause === "user-selection" || options.cause === "startup-override") {
18858
+ const ownership = readDurableModelProfileOwnership(this.settings);
18859
+ if (ownership.version === 0) {
18860
+ this.#clearActiveModelProfileForConcreteDefault(options.cause);
18861
+ }
18862
+ }
18863
+ const origin = options.cause === "startup-override" ? "startup-override" : "model_selection";
18864
+ const effectiveLevel = thinkingLevel ?? model.thinking?.defaultLevel ?? this.thinkingLevel;
18865
+ this.setConfiguredModelChain(
18866
+ "default",
18867
+ [formatModelSelectorValue(`${model.provider}/${model.id}`, effectiveLevel)],
18868
+ origin,
18869
+ );
18870
+ }
18600
18871
  await this.#syncEditToolModeAfterModelChange(previousEditMode);
18601
18872
  } catch (error) {
18602
18873
  if (ownsScope) await this.restoreTemporaryProviderSessionScope(scope);
@@ -18687,8 +18958,15 @@ export class AgentSession {
18687
18958
 
18688
18959
  #publishDefaultModelSelection(model: Model, thinkingLevel: ThinkingLevel, systemPrompt: string[] | undefined): void {
18689
18960
  this.#clearActiveRetryFallback();
18961
+ // Concrete model selection clears session-scoped profile state (#5919).
18962
+ // IMPORTANT: This is called AFTER durable persistence completes, so the
18963
+ // session-scoped updates occur during the promotion phase after ownership
18964
+ // is committed. Session ownership is captured in the promotion logic above.
18965
+ this.#resetSessionScopedModelProfileState({ preserveDefaultConfiguredChain: true });
18966
+ this.markUserModelSelection();
18690
18967
  this.#setModelWithProviderSessionReset(model);
18691
18968
  this.#seedSessionCanonicalVariant(model);
18969
+ this.#recordUserCanonicalVariantSelection();
18692
18970
  const thinkingLevelChanged = this.#thinkingLevel !== thinkingLevel;
18693
18971
  this.#thinkingLevelMutationRevision++;
18694
18972
  this.#thinkingLevelLiveMutationRevision++;
@@ -18698,6 +18976,12 @@ export class AgentSession {
18698
18976
  this.#pendingThinkingVisibilityControlFailure = undefined;
18699
18977
  this.#thinkingLevel = thinkingLevel;
18700
18978
  this.agent.setThinkingLevel(toReasoningEffort(thinkingLevel));
18979
+ // Materialize durable profiles after model and thinking level installation
18980
+ // so the persisted default selector matches both the newly installed model
18981
+ // and the selected thinking level.
18982
+ if (this.model && this.settings.get("modelProfile.default") !== undefined) {
18983
+ this.materializeActiveDefaultModelProfileAssignment(this.model);
18984
+ }
18701
18985
  if (thinkingLevelChanged) {
18702
18986
  const event: AgentSessionEvent = { type: "thinking_level_changed", thinkingLevel };
18703
18987
  for (const listener of this.#eventListenerSnapshot) {
@@ -18883,7 +19167,40 @@ export class AgentSession {
18883
19167
  });
18884
19168
  }
18885
19169
  }
18886
- this.#clearActiveModelProfileForConcreteDefault("user-selection");
19170
+ // Concrete model selection clears durable profile ownership (#5919).
19171
+ // If there is an existing ownership record (version > 0), clear it.
19172
+ // If there is no ownership record (version === 0), call the legacy path to
19173
+ // clear stale persisted profiles that no longer match the selection.
19174
+ // The ownership clear is attempted atomically with other model role updates.
19175
+ // If it fails due to concurrent access, we log and continue: the model
19176
+ // selection has been promoted and committed, so the concrete pick is durable.
19177
+ // A stale profile ownership marker will be resolved on the next session startup.
19178
+ const currentOwnership = readDurableModelProfileOwnership(this.settings);
19179
+ if (currentOwnership.version > 0) {
19180
+ try {
19181
+ await commitDurableModelProfileOwnership(this.settings, { kind: "cleared" });
19182
+ } catch (error) {
19183
+ if (error instanceof ModelProfileOwnershipConflictError) {
19184
+ logger.info("Model selection ownership clear deferred due to concurrent access", {
19185
+ code: "default_model_selection_ownership_conflict",
19186
+ disposition: "continue",
19187
+ expectedVersion: error.expectedVersion,
19188
+ actualVersion: error.actualVersion,
19189
+ });
19190
+ } else if (error instanceof InvalidModelProfileOwnershipError) {
19191
+ logger.warn("Invalid model profile ownership marker during selection", {
19192
+ code: "default_model_selection_ownership_invalid",
19193
+ disposition: "continue",
19194
+ });
19195
+ } else {
19196
+ throw error;
19197
+ }
19198
+ }
19199
+ } else {
19200
+ // Legacy path: no ownership record exists. Clear stale persisted defaults
19201
+ // that no longer match the concrete selection.
19202
+ this.#clearActiveModelProfileForConcreteDefault("user-selection");
19203
+ }
18887
19204
  options?.onAfterMutation?.();
18888
19205
  return { provider: model.provider, modelId: model.id, thinkingLevel: effectiveLevel };
18889
19206
  },
@@ -18989,6 +19306,7 @@ export class AgentSession {
18989
19306
  const next = roleModels[nextIndex];
18990
19307
 
18991
19308
  if (options?.temporary) {
19309
+ this.markUserModelSelection();
18992
19310
  await this.setModelTemporary(next.model, next.explicitThinkingLevel ? next.thinkingLevel : undefined, {
18993
19311
  cause: "temporary-operation",
18994
19312
  reason: "temporary-cycle",
@@ -20638,6 +20956,7 @@ export class AgentSession {
20638
20956
  onTerminalOverflowNoop?: () => void,
20639
20957
  resourceRunId?: string,
20640
20958
  ownershipSignal?: AbortSignal,
20959
+ sdkOwnership?: SdkContinuationOwnership,
20641
20960
  ): Promise<boolean> {
20642
20961
  // Safety stops are terminal and must not trigger context maintenance.
20643
20962
  if (
@@ -20692,6 +21011,7 @@ export class AgentSession {
20692
21011
  generation,
20693
21012
  suppressPredecessorAgentEnd: true,
20694
21013
  resourceRunId,
21014
+ sdkOwnership,
20695
21015
  });
20696
21016
 
20697
21017
  return true;
@@ -20710,10 +21030,11 @@ export class AgentSession {
20710
21030
  },
20711
21031
  resourceRunId,
20712
21032
  signal: ownershipSignal,
21033
+ sdkOwnership,
20713
21034
  });
20714
21035
  return "continuationScheduled" in status && status.continuationScheduled === true;
20715
21036
  }
20716
- return await this.#scheduleOverflowRetryContinuation(generation, resourceRunId);
21037
+ return await this.#scheduleOverflowRetryContinuation(generation, resourceRunId, sdkOwnership);
20717
21038
  }
20718
21039
  const compactionSettings = this.settings.getGroup("compaction");
20719
21040
  if (!compactionSettings.enabled || compactionSettings.strategy === "off") return false;
@@ -20759,7 +21080,11 @@ export class AgentSession {
20759
21080
  const promoted = await this.#tryContextPromotion(assistantMessage, ownershipSignal);
20760
21081
  if (ownershipSignal?.aborted) return false;
20761
21082
  if (!promoted) {
20762
- await this.#runAutoCompaction("threshold", false, false, { resourceRunId, signal: ownershipSignal });
21083
+ await this.#runAutoCompaction("threshold", false, false, {
21084
+ resourceRunId,
21085
+ signal: ownershipSignal,
21086
+ sdkOwnership,
21087
+ });
20763
21088
  }
20764
21089
  }
20765
21090
  return true;
@@ -21113,10 +21438,7 @@ export class AgentSession {
21113
21438
  const isContextPromoted = this.#temporaryProviderSessionScopes.some(
21114
21439
  scope => scope.token.reason === "context-promotion",
21115
21440
  );
21116
- if (
21117
- !isContextPromoted ||
21118
- !isDefaultAutoThresholdCeilingApplied(contextWindow, settings)
21119
- ) {
21441
+ if (!isContextPromoted || !isDefaultAutoThresholdCeilingApplied(contextWindow, settings)) {
21120
21442
  return settings;
21121
21443
  }
21122
21444
 
@@ -21525,7 +21847,9 @@ export class AgentSession {
21525
21847
  // The reminder continues the current prompt, so the predecessor `agent_end`
21526
21848
  // must stay held until the continuation turn produces the real terminal.
21527
21849
  // Publishing it here would settle the caller's prompt mid-reminder.
21528
- this.#scheduleAgentContinue({ skipCompactionCheck: true, suppressPredecessorAgentEnd: true });
21850
+ // Disable managed fallback to prevent indefinite retries for this server-initiated turn.
21851
+ this.#todoReminderContinuationGeneration = this.#promptGeneration;
21852
+ this.#scheduleAgentContinue({ skipCompactionCheck: true, suppressPredecessorAgentEnd: true, disableManagedFallback: true });
21529
21853
  }
21530
21854
 
21531
21855
  /**
@@ -21639,6 +21963,11 @@ export class AgentSession {
21639
21963
  }
21640
21964
  }
21641
21965
 
21966
+ #recordUserCanonicalVariantSelection(): void {
21967
+ this.#userCanonicalVariantSelectionRevision++;
21968
+ this.#userCanonicalVariantSelection = this.#modelRegistry.getSessionCanonicalVariant?.(this.sessionId);
21969
+ }
21970
+
21642
21971
  #closeCodexProviderSessionsForHistoryRewrite(): void {
21643
21972
  const currentModel = this.model;
21644
21973
  if (currentModel?.api !== "openai-codex-responses") return;
@@ -22242,6 +22571,7 @@ export class AgentSession {
22242
22571
  signal?: AbortSignal;
22243
22572
  beforeTerminalOverflowNoop?: () => void;
22244
22573
  resourceRunId?: string;
22574
+ sdkOwnership?: SdkContinuationOwnership;
22245
22575
  },
22246
22576
  ): Promise<AutoCompactionTerminalStatus> {
22247
22577
  if (this.#isDisposed || this.#sessionAdmissionClosing) return Promise.resolve({ kind: "skipped" });
@@ -22262,6 +22592,7 @@ export class AgentSession {
22262
22592
  signal?: AbortSignal;
22263
22593
  beforeTerminalOverflowNoop?: () => void;
22264
22594
  resourceRunId?: string;
22595
+ sdkOwnership?: SdkContinuationOwnership;
22265
22596
  },
22266
22597
  ): Promise<AutoCompactionTerminalStatus> {
22267
22598
  const compactionSettings = this.settings.getGroup("compaction");
@@ -22404,7 +22735,14 @@ export class AgentSession {
22404
22735
  });
22405
22736
  if (autoCompactionSignal.aborted) return { kind: "aborted", source: "signal" };
22406
22737
  if (continueAfterMaintenance && reason !== "idle" && compactionSettings.autoContinue !== false) {
22407
- this.#scheduleAutoContinuePrompt(generation, true, options?.resourceRunId);
22738
+ this.#scheduleAutoContinuePrompt(
22739
+ generation,
22740
+ true,
22741
+ options?.resourceRunId,
22742
+ undefined,
22743
+ undefined,
22744
+ options?.sdkOwnership,
22745
+ );
22408
22746
  }
22409
22747
 
22410
22748
  if (autoCompactionSignal.aborted) return { kind: "aborted", source: "signal" };
@@ -22459,7 +22797,11 @@ export class AgentSession {
22459
22797
  }
22460
22798
  const overflowContinuationScheduled =
22461
22799
  !overflowNoopWouldReplay && willRetry && !continuationSkipReason
22462
- ? await this.#scheduleOverflowRetryContinuation(generation, options?.resourceRunId)
22800
+ ? await this.#scheduleOverflowRetryContinuation(
22801
+ generation,
22802
+ options?.resourceRunId,
22803
+ options?.sdkOwnership,
22804
+ )
22463
22805
  : false;
22464
22806
  await this.#emitSessionEvent({
22465
22807
  type: "auto_compaction_end",
@@ -22509,7 +22851,14 @@ export class AgentSession {
22509
22851
  resourceRunId: options?.resourceRunId,
22510
22852
  });
22511
22853
  } else if (continueAfterMaintenance && reason !== "idle" && compactionSettings.autoContinue !== false) {
22512
- this.#scheduleAutoContinuePrompt(generation, true, options?.resourceRunId);
22854
+ this.#scheduleAutoContinuePrompt(
22855
+ generation,
22856
+ true,
22857
+ options?.resourceRunId,
22858
+ undefined,
22859
+ undefined,
22860
+ options?.sdkOwnership,
22861
+ );
22513
22862
  }
22514
22863
  return { kind: "skipped" };
22515
22864
  }
@@ -22794,7 +23143,11 @@ export class AgentSession {
22794
23143
  }
22795
23144
  const overflowContinuationScheduled =
22796
23145
  willRetry && !continuationSkipReason
22797
- ? await this.#scheduleOverflowRetryContinuation(generation, options?.resourceRunId)
23146
+ ? await this.#scheduleOverflowRetryContinuation(
23147
+ generation,
23148
+ options?.resourceRunId,
23149
+ options?.sdkOwnership,
23150
+ )
22798
23151
  : false;
22799
23152
 
22800
23153
  await this.#emitSessionEvent({
@@ -22828,7 +23181,14 @@ export class AgentSession {
22828
23181
  resourceRunId: options?.resourceRunId,
22829
23182
  });
22830
23183
  } else if (continueAfterMaintenance && reason !== "idle" && compactionSettings.autoContinue !== false) {
22831
- this.#scheduleAutoContinuePrompt(generation, true, options?.resourceRunId);
23184
+ this.#scheduleAutoContinuePrompt(
23185
+ generation,
23186
+ true,
23187
+ options?.resourceRunId,
23188
+ undefined,
23189
+ undefined,
23190
+ options?.sdkOwnership,
23191
+ );
22832
23192
  }
22833
23193
  return { kind: "compacted" };
22834
23194
  } catch (error) {
@@ -23021,7 +23381,8 @@ export class AgentSession {
23021
23381
  isAuthenticated(apiKey) &&
23022
23382
  authStorage.getSessionCredentialRowId(model.provider, this.credentialSessionId) === credential.id &&
23023
23383
  authStorage.getSessionCredentialType(model.provider, this.credentialSessionId) === credentialKind &&
23024
- currentCredential && !currentCredential.disabled
23384
+ currentCredential &&
23385
+ !currentCredential.disabled
23025
23386
  )
23026
23387
  return apiKey;
23027
23388
  }
@@ -23191,6 +23552,7 @@ export class AgentSession {
23191
23552
  if (message.errorKind === "local_snapshot_failure") return "local_snapshot";
23192
23553
  if (message.errorKind === "local_buffer_overflow") return "local_buffer_overflow";
23193
23554
  if (this.#isTypedFirstEventTimeout(message)) return "first_event_timeout";
23555
+ if (message.errorKind === "local_empty_response") return "empty_response";
23194
23556
  if (this.#isTypedEmptyResponse(message)) return "empty_response";
23195
23557
  if (this.#isCodexCredentialModelUnavailable(message)) {
23196
23558
  return this.#canRotateCodexCredential(message) ? "unknown" : "terminal";
@@ -23445,6 +23807,7 @@ export class AgentSession {
23445
23807
  }
23446
23808
 
23447
23809
  async #handleManagedAttemptOutcome(outcome: ManagedAttemptOutcome): Promise<ManagedAttemptDecision> {
23810
+ const sdkOwnership = this.#captureSdkContinuationOwnership(outcome.scope);
23448
23811
  const attemptAbortEpoch = this.#abortAdmissionEpoch;
23449
23812
  const attemptCancelled = () =>
23450
23813
  this.#isDisposed || this.#sessionAdmissionClosing || this.#abortAdmissionEpoch !== attemptAbortEpoch;
@@ -23494,6 +23857,8 @@ export class AgentSession {
23494
23857
  return {
23495
23858
  type: "retry",
23496
23859
  continuation: async ownership => {
23860
+ const continuationSdkOwnership =
23861
+ sdkOwnership ?? this.#captureSdkContinuationOwnership(ownership.handle.scope);
23497
23862
  const restoreOwnedTransition = async () => {
23498
23863
  if (this.#fallbackTransitionGeneration !== transitionGeneration) return;
23499
23864
  this.#fallbackTransitionGeneration++;
@@ -23580,7 +23945,12 @@ export class AgentSession {
23580
23945
  ...this.#managedFallbackPromptOptions(),
23581
23946
  transientRecoveryMessage: this.#escapedNonAsciiRecoveryMessage(),
23582
23947
  onRunAccepted: (handle: AttemptRunHandle) => {
23583
- this.#acceptSdkAttemptRun(handle, this.#activeSdkRunToken);
23948
+ this.#acceptSdkAttemptRun(
23949
+ handle,
23950
+ continuationSdkOwnership?.token,
23951
+ continuationSdkOwnership?.cohort,
23952
+ continuationSdkOwnership?.scope,
23953
+ );
23584
23954
  },
23585
23955
  });
23586
23956
  if (
@@ -23619,11 +23989,18 @@ export class AgentSession {
23619
23989
  type: "retry",
23620
23990
  continuation: async ownership => {
23621
23991
  if (attemptCancelled() || !ownership.isCurrent() || ownership.lease.signal.aborted) return;
23992
+ const continuationSdkOwnership =
23993
+ sdkOwnership ?? this.#captureSdkContinuationOwnership(ownership.handle.scope);
23622
23994
  await this.agent.continue({
23623
23995
  ...this.#managedFallbackPromptOptions(),
23624
23996
  transientRecoveryMessage: this.#escapedNonAsciiRecoveryMessage(),
23625
23997
  onRunAccepted: (handle: AttemptRunHandle) => {
23626
- this.#acceptSdkAttemptRun(handle, this.#activeSdkRunToken);
23998
+ this.#acceptSdkAttemptRun(
23999
+ handle,
24000
+ continuationSdkOwnership?.token,
24001
+ continuationSdkOwnership?.cohort,
24002
+ continuationSdkOwnership?.scope,
24003
+ );
23627
24004
  },
23628
24005
  });
23629
24006
  },
@@ -23638,6 +24015,10 @@ export class AgentSession {
23638
24015
  type: "maintenance",
23639
24016
  continuation: async ownership => {
23640
24017
  if (attemptCancelled() || !ownership.isCurrent() || ownership.lease.signal.aborted) return;
24018
+ // A discarded in-loop attempt need not have published turn_start;
24019
+ // the Agent-supplied handle still identifies its accepted SDK cohort.
24020
+ const continuationSdkOwnership =
24021
+ sdkOwnership ?? this.#captureSdkContinuationOwnership(ownership.handle.scope);
23641
24022
  const resourceRunId = String(ownership.logicalRunId);
23642
24023
  const previousLease = this.#postPromptLeases.get(resourceRunId);
23643
24024
  this.#postPromptLeases.set(resourceRunId, ownership.lease);
@@ -23655,6 +24036,7 @@ export class AgentSession {
23655
24036
  },
23656
24037
  resourceRunId,
23657
24038
  ownership.lease.signal,
24039
+ continuationSdkOwnership,
23658
24040
  );
23659
24041
  if (
23660
24042
  terminalized ||
@@ -23689,12 +24071,14 @@ export class AgentSession {
23689
24071
  },
23690
24072
  };
23691
24073
  }
23692
- // Issue #5018 preserves managed behavior for the typed statusless
23693
- // Responses overload: before the code survived transport, this failure
23694
- // reached the session as an ordinary committed error, so the chain never
23695
- // discarded or advanced on it. Route it to the exhaustion decision
23696
- // directly, before the retryable path can classify its new facts.
23697
- if (isStatuslessTypedOverloadFacts(outcome.failure.transportFailure)) {
24074
+ // Content-free typed statusless overloads are safe for managed retry when
24075
+ // this discarded attempt is clean, even after earlier committed work in
24076
+ // the same prompt. Preserve the committed failure path when this attempt
24077
+ // itself is not replay-safe.
24078
+ if (
24079
+ isStatuslessTypedOverloadFacts(outcome.failure.transportFailure) &&
24080
+ !this.#isRetryScopeClean(outcome.scope)
24081
+ ) {
23698
24082
  this.#defaultFallbackChain().resetAttemptBudget();
23699
24083
  return this.#managedFallbackExhaustionDecision(
23700
24084
  outcome.failure.message,
@@ -23759,6 +24143,10 @@ export class AgentSession {
23759
24143
  if (classification === "transient" || classification === "first_event_timeout") {
23760
24144
  return { class: "server" };
23761
24145
  }
24146
+ // A locally promoted empty response is retryable only when the managed
24147
+ // attempt stayed clean; authorize the configured fallback chain without
24148
+ // fabricating provider transport facts for the runtime-owned failure.
24149
+ if (classification === "empty_response") return { class: "server" };
23762
24150
  if (classification === "unknown") return { class: "unknown" };
23763
24151
  return undefined;
23764
24152
  }
@@ -23938,9 +24326,7 @@ export class AgentSession {
23938
24326
  !authStorage.isCredentialAvailable(resolvedModel.provider, credential.id)
23939
24327
  )
23940
24328
  continue;
23941
- let credentialKeyResult:
23942
- | { kind: "resolved"; value: string | undefined }
23943
- | { kind: "aborted" };
24329
+ let credentialKeyResult: { kind: "resolved"; value: string | undefined } | { kind: "aborted" };
23944
24330
  try {
23945
24331
  credentialKeyResult = await awaitWithCancellation(
23946
24332
  this.#modelRegistry.getApiKey(resolvedModel, this.credentialSessionId, {
@@ -24038,7 +24424,11 @@ export class AgentSession {
24038
24424
  !transitionStillOwned()
24039
24425
  )
24040
24426
  return await rollbackCancelled();
24041
- if (selectedCredentialRowId !== undefined && selectedCredentialKind !== undefined && this.#restoreManagedFallbackGetApiKey) {
24427
+ if (
24428
+ selectedCredentialRowId !== undefined &&
24429
+ selectedCredentialKind !== undefined &&
24430
+ this.#restoreManagedFallbackGetApiKey
24431
+ ) {
24042
24432
  this.#managedFallbackNextCredentialOverride = {
24043
24433
  modelKey: canonicalModelKey,
24044
24434
  storageProvider: resolveOAuthStorageProvider(resolvedModel.provider),
@@ -24332,6 +24722,10 @@ export class AgentSession {
24332
24722
  scope?: AttemptScope,
24333
24723
  scopeWasClean = this.#isRetryScopeClean(scope),
24334
24724
  ): Promise<boolean | ManagedAttemptDecision> {
24725
+ // Capture the SDK owner before the failed attempt retires its active scope.
24726
+ // Retry/continuation terminals must remain attributed to the prompt that
24727
+ // admitted the provider call, even when the retry starts after agent_end.
24728
+ const retrySdkOwnership = this.#captureSdkContinuationOwnership(scope);
24335
24729
  const retryAbortEpoch = this.#abortAdmissionEpoch;
24336
24730
  const retryCancelled = () =>
24337
24731
  this.#isDisposed || this.#sessionAdmissionClosing || this.#abortAdmissionEpoch !== retryAbortEpoch;
@@ -24349,13 +24743,22 @@ export class AgentSession {
24349
24743
  // Managed outcomes can reach retry handling without the ordinary admission
24350
24744
  // check. Do not let retained transport facts bypass the same terminal veto.
24351
24745
  if (isTerminalCodexProviderFailure(message)) {
24352
- return managedOutcome
24353
- ? { type: "terminal", terminal: { stopReason: "error", messages: [message] } }
24354
- : false;
24746
+ return managedOutcome ? { type: "terminal", terminal: { stopReason: "error", messages: [message] } } : false;
24355
24747
  }
24356
24748
  if (retryCancelled()) {
24357
24749
  return managedOutcome ? { type: "terminal", terminal: { stopReason: "cancelled" } } : false;
24358
24750
  }
24751
+ if (
24752
+ transportFailure?.status === undefined &&
24753
+ transportFailure?.providerCode === SERVER_OVERLOADED_PROVIDER_CODE &&
24754
+ (transportFailure.openaiErrorCode === undefined ||
24755
+ transportFailure.openaiErrorCode === SERVER_OVERLOADED_PROVIDER_CODE) &&
24756
+ (managedOutcome ? !scopeWasClean : !this.#hasCleanRetryReplaySafety)
24757
+ ) {
24758
+ return managedOutcome
24759
+ ? { type: "terminal", terminal: { stopReason: "error", messages: [message] } }
24760
+ : false;
24761
+ }
24359
24762
  // A local machinery failure must never stay charged against the provider
24360
24763
  // fallback budget, no matter which local exit follows (disabled retry,
24361
24764
  // visible-content surface, bounded retry, exhaustion, or the immediate
@@ -24462,21 +24865,6 @@ export class AgentSession {
24462
24865
  | undefined = canReplayCodexProviderOverload || canReplayBareDefaultCodexFailure
24463
24866
  ? { class: "server" }
24464
24867
  : fallbackTrigger;
24465
- // OpenAI's typed statusless capacity-overload code (issue #5018) must not
24466
- // gain managed-chain retry/advance authority from its new facts. Before
24467
- // the code survived transport, this failure reached the session as an
24468
- // ordinary committed error and surfaced immediately, so mirror that
24469
- // behavior with the existing exhaustion decision. However, when called from
24470
- // the agent_end path (managedOutcome=false), we must return false to allow
24471
- // proper session termination handling.
24472
- if (managedFallback && isStatuslessTypedOverloadFacts(transportFailure)) {
24473
- return managedOutcome
24474
- ? this.#managedFallbackExhaustionDecision(
24475
- message,
24476
- message.errorMessage || "Model fallback attempt failed",
24477
- )
24478
- : false;
24479
- }
24480
24868
  if (!trigger) {
24481
24869
  return managedOutcome
24482
24870
  ? this.#managedFallbackExhaustionDecision(message, message.errorMessage || "Model fallback attempt failed")
@@ -24689,10 +25077,7 @@ export class AgentSession {
24689
25077
  if (outcome === "exhausted") {
24690
25078
  if (managedFallback) {
24691
25079
  let errorMessage = this.#fallbackExhaustionError(controller);
24692
- if (
24693
- !providerRetryCeilingReached &&
24694
- (trigger.class === "quota" || trigger.class === "rate_limit")
24695
- ) {
25080
+ if (!providerRetryCeilingReached && (trigger.class === "quota" || trigger.class === "rate_limit")) {
24696
25081
  if (!assistantMessageHasVisibleOrToolContent(message)) {
24697
25082
  const mark =
24698
25083
  quotaCredentialMark ??
@@ -24737,6 +25122,8 @@ export class AgentSession {
24737
25122
  : cappedExponentialWithFullJitter(retrySettings.baseDelayMs, retrySettings.maxDelayMs, attemptsUsed);
24738
25123
 
24739
25124
  const retry = async (ownership?: ManagedAttemptContinuationOwnership): Promise<void> => {
25125
+ const continuationSdkOwnership =
25126
+ retrySdkOwnership ?? this.#captureSdkContinuationOwnership(ownership?.handle.scope);
24740
25127
  const activePromptHandle = this.activePromptHandle;
24741
25128
  const cancellationSignal =
24742
25129
  ownership?.domain.signal ??
@@ -24951,7 +25338,12 @@ export class AgentSession {
24951
25338
  await this.agent.continue({
24952
25339
  ...this.#managedFallbackPromptOptions(),
24953
25340
  onRunAccepted: (handle: AttemptRunHandle) => {
24954
- this.#acceptSdkAttemptRun(handle, this.#activeSdkRunToken);
25341
+ this.#acceptSdkAttemptRun(
25342
+ handle,
25343
+ continuationSdkOwnership?.token,
25344
+ continuationSdkOwnership?.cohort,
25345
+ continuationSdkOwnership?.scope,
25346
+ );
24955
25347
  },
24956
25348
  });
24957
25349
  return;
@@ -24979,6 +25371,7 @@ export class AgentSession {
24979
25371
  allowDuringCancelAndSubmit: true,
24980
25372
  suppressPredecessorAgentEnd: resourceRunId !== undefined,
24981
25373
  resourceRunId,
25374
+ sdkOwnership: continuationSdkOwnership,
24982
25375
  onError: () => this.#failRetryRecovery("Retry continuation failed to start"),
24983
25376
  onSkip: () => this.#failRetryRecovery("Retry continuation was superseded"),
24984
25377
  });
@@ -25206,10 +25599,6 @@ export class AgentSession {
25206
25599
  // Bash Execution
25207
25600
  // =========================================================================
25208
25601
 
25209
- async #saveBashOriginalArtifact(originalText: string): Promise<BashArtifactSaveResult> {
25210
- return saveAgentBashOriginalArtifact(this.sessionManager, originalText);
25211
- }
25212
-
25213
25602
  /**
25214
25603
  * Execute a bash command.
25215
25604
  * Adds result to agent context and session.
@@ -25224,6 +25613,7 @@ export class AgentSession {
25224
25613
  onChunk?: (chunk: string) => void,
25225
25614
  options?: { excludeFromContext?: boolean; onPersisted?: () => void },
25226
25615
  ): Promise<BashResult> {
25616
+ const publishArtifact = this.sessionManager.captureArtifactPublication();
25227
25617
  const excludeFromContext = options?.excludeFromContext === true;
25228
25618
  this.#markRetryReplayUnsafe();
25229
25619
 
@@ -25261,7 +25651,7 @@ export class AgentSession {
25261
25651
  sessionId: this.sessionId,
25262
25652
  cwd,
25263
25653
  }),
25264
- onMinimizedSave: originalText => this.#saveBashOriginalArtifact(originalText),
25654
+ onMinimizedSave: originalText => saveAgentBashOriginalArtifact(publishArtifact, originalText),
25265
25655
  });
25266
25656
 
25267
25657
  this.recordBashResult(command, result, options);
@@ -25365,9 +25755,18 @@ export class AgentSession {
25365
25755
  this.#markRetryReplayUnsafe();
25366
25756
  const cwd = this.sessionManager.getCwd();
25367
25757
  this.assertEvalExecutionAllowed();
25758
+ const sessionFile = this.sessionManager.getSessionFile();
25759
+ const sessionId = sessionFile ? `session:${sessionFile}:cwd:${cwd}` : `cwd:${cwd}`;
25760
+ const kernelOwnerId = this.#evalKernelOwnerId;
25761
+ const kernelMode = this.settings.get("python.kernelMode");
25762
+ const settings = this.settings;
25368
25763
 
25369
25764
  const abortController = new AbortController();
25370
- const execution = (async (): Promise<PythonResult> => {
25765
+ const execution = Promise.resolve().then(async (): Promise<PythonResult> => {
25766
+ if (abortController.signal.aborted) {
25767
+ throw abortController.signal.reason ?? new DOMException("Aborted", "AbortError");
25768
+ }
25769
+ this.assertEvalExecutionAllowed();
25371
25770
  if (this.#extensionRunner?.hasHandlers("user_python")) {
25372
25771
  const hookResult = await this.#extensionRunner.emitUserPython({
25373
25772
  type: "user_python",
@@ -25375,6 +25774,9 @@ export class AgentSession {
25375
25774
  excludeFromContext,
25376
25775
  cwd,
25377
25776
  });
25777
+ if (abortController.signal.aborted) {
25778
+ throw abortController.signal.reason ?? new DOMException("Aborted", "AbortError");
25779
+ }
25378
25780
  this.assertEvalExecutionAllowed();
25379
25781
  if (hookResult?.result) {
25380
25782
  this.recordPythonResult(code, hookResult.result, options);
@@ -25382,15 +25784,12 @@ export class AgentSession {
25382
25784
  }
25383
25785
  }
25384
25786
 
25385
- // Use the same session ID as eval's Python backend for kernel sharing
25386
- const sessionFile = this.sessionManager.getSessionFile();
25387
- const sessionId = sessionFile ? `session:${sessionFile}:cwd:${cwd}` : `cwd:${cwd}`;
25388
25787
  const result = await executePythonCommand(code, {
25389
25788
  cwd,
25390
25789
  sessionId,
25391
- kernelOwnerId: this.#evalKernelOwnerId,
25392
- kernelMode: this.settings.get("python.kernelMode"),
25393
- settings: this.settings,
25790
+ kernelOwnerId,
25791
+ kernelMode,
25792
+ settings,
25394
25793
  onChunk,
25395
25794
  signal: abortController.signal,
25396
25795
  });
@@ -25399,7 +25798,7 @@ export class AgentSession {
25399
25798
  // must not reopen a closing session to append history.
25400
25799
  if (!this.#evalExecutionDisposing) this.recordPythonResult(code, result, options);
25401
25800
  return result;
25402
- })();
25801
+ });
25403
25802
  return await this.trackEvalExecution(execution, abortController);
25404
25803
  }
25405
25804