@gajae-code/coding-agent 0.18.6 → 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 (302) hide show
  1. package/CHANGELOG.md +318 -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 +6 -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/settings.d.ts +4 -0
  15. package/dist/types/config/skill-settings-defaults.d.ts +2 -0
  16. package/dist/types/cursor-delete-path.d.ts +1 -0
  17. package/dist/types/cursor-mcp-name.d.ts +8 -0
  18. package/dist/types/customization/inventory.d.ts +2 -0
  19. package/dist/types/debug/report-bundle-parent.d.ts +2 -0
  20. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.d.ts +1 -0
  21. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/version-manager.d.ts +31 -0
  22. package/dist/types/defaults/gjc-skills.generated.d.ts +5 -6
  23. package/dist/types/discovery/helpers.d.ts +8 -3
  24. package/dist/types/export/html/css-color.d.ts +2 -0
  25. package/dist/types/export/html/template.generated.d.ts +1 -1
  26. package/dist/types/extensibility/extensions/types.d.ts +7 -0
  27. package/dist/types/extensibility/gjc-plugins/runtime-adapters.d.ts +14 -1
  28. package/dist/types/extensibility/plugins/marketplace/cache.d.ts +2 -0
  29. package/dist/types/extensibility/plugins/plugin-link-path.d.ts +2 -0
  30. package/dist/types/extensibility/runtime-skill-discovery.d.ts +3 -1
  31. package/dist/types/extensibility/skill-management.d.ts +3 -1
  32. package/dist/types/extensibility/skills.d.ts +2 -0
  33. package/dist/types/gjc-runtime/deep-interview-crystallize.d.ts +74 -0
  34. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +11 -0
  35. package/dist/types/gjc-runtime/gc-runtime.d.ts +5 -0
  36. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +0 -5
  37. package/dist/types/gjc-runtime/state-argv.d.ts +1 -1
  38. package/dist/types/gjc-runtime/state-migrations.d.ts +8 -0
  39. package/dist/types/gjc-runtime/state-runtime.d.ts +95 -0
  40. package/dist/types/gjc-runtime/state-writer.d.ts +11 -1
  41. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +8 -0
  42. package/dist/types/internal-urls/skill-path.d.ts +1 -0
  43. package/dist/types/lsp/workspace-path.d.ts +1 -0
  44. package/dist/types/modes/acp/acp-agent.d.ts +7 -0
  45. package/dist/types/modes/components/custom-editor.d.ts +6 -0
  46. package/dist/types/modes/components/customization/customization-dashboard.d.ts +1 -0
  47. package/dist/types/modes/components/extensions/state-manager.d.ts +4 -3
  48. package/dist/types/modes/controllers/input-controller.d.ts +9 -0
  49. package/dist/types/modes/interactive-mode.d.ts +1 -0
  50. package/dist/types/modes/utils/interrupt-ownership.d.ts +3 -0
  51. package/dist/types/modes/utils/keybinding-matchers.d.ts +1 -0
  52. package/dist/types/modes/utils/macos-option-forwarding.d.ts +17 -0
  53. package/dist/types/progress/collect-project-progress.d.ts +38 -0
  54. package/dist/types/progress/progress-contract.d.ts +154 -0
  55. package/dist/types/progress/progress-snapshot.d.ts +11 -0
  56. package/dist/types/progress/project-progress.d.ts +141 -0
  57. package/dist/types/progress/render-progress.d.ts +28 -0
  58. package/dist/types/runtime-mcp/elicitation.d.ts +12 -0
  59. package/dist/types/runtime-mcp/oauth-public-url.d.ts +1 -0
  60. package/dist/types/runtime-mcp/plugin-network-boundary.d.ts +1 -0
  61. package/dist/types/runtime-mcp/smithery-registry.d.ts +24 -0
  62. package/dist/types/runtime-mcp/types.d.ts +6 -0
  63. package/dist/types/sdk/acp/adapter.d.ts +1 -1
  64. package/dist/types/sdk/broker/broker-exit.d.ts +6 -2
  65. package/dist/types/sdk/broker/broker.d.ts +18 -1
  66. package/dist/types/sdk/broker/lifecycle.d.ts +9 -0
  67. package/dist/types/sdk/broker/session-index.d.ts +12 -6
  68. package/dist/types/sdk/broker/task-artifact-owner-validation.d.ts +55 -0
  69. package/dist/types/sdk/bus/index.d.ts +12 -1
  70. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +6 -1
  71. package/dist/types/sdk/bus/telegram-daemon.d.ts +3 -2
  72. package/dist/types/sdk/client/client.d.ts +2 -0
  73. package/dist/types/sdk/host/host.d.ts +6 -0
  74. package/dist/types/sdk/host/query/handlers.d.ts +3 -0
  75. package/dist/types/sdk/index.d.ts +1 -0
  76. package/dist/types/sdk/router/session-router.d.ts +9 -0
  77. package/dist/types/sdk/session-list.d.ts +1 -1
  78. package/dist/types/sdk/session.d.ts +8 -0
  79. package/dist/types/session/agent-session.d.ts +38 -9
  80. package/dist/types/session/artifacts.d.ts +2 -2
  81. package/dist/types/session/internal/managed-gc-retirement-codec.d.ts +32 -0
  82. package/dist/types/session/internal/managed-session-scope.d.ts +79 -2
  83. package/dist/types/session/internal/managed-session-storage.d.ts +36 -1
  84. package/dist/types/session/internal/managed-task-owner-cleanup.d.ts +62 -0
  85. package/dist/types/session/internal/task-artifact-owner-access.d.ts +19 -0
  86. package/dist/types/session/internal/task-artifact-owner-transcript.d.ts +11 -0
  87. package/dist/types/session/session-manager.d.ts +31 -5
  88. package/dist/types/session/session-retirement.d.ts +71 -0
  89. package/dist/types/session/session-storage.d.ts +62 -48
  90. package/dist/types/session/session-transcript-header.d.ts +2 -0
  91. package/dist/types/session/task-artifact-owner-codec.d.ts +109 -0
  92. package/dist/types/session/task-artifact-owner-retirement.d.ts +7 -0
  93. package/dist/types/session/task-artifact-owner.d.ts +10 -0
  94. package/dist/types/setup/credential-auto-import.d.ts +1 -1
  95. package/dist/types/setup/credential-import.d.ts +6 -4
  96. package/dist/types/skill-state/active-state.d.ts +38 -0
  97. package/dist/types/slash-commands/helpers/progress-report.d.ts +8 -0
  98. package/dist/types/task/executor.d.ts +3 -0
  99. package/dist/types/tools/bash-skill-urls.d.ts +1 -1
  100. package/dist/types/tools/index.d.ts +3 -0
  101. package/dist/types/tools/python.d.ts +9 -1
  102. package/dist/types/tools/recipe/recipe-guard.d.ts +7 -0
  103. package/dist/types/tools/restricted-cwd.d.ts +1 -0
  104. package/dist/types/tools/todo-write.d.ts +6 -0
  105. package/dist/types/utils/tools-checksum.d.ts +7 -0
  106. package/package.json +7 -7
  107. package/scripts/generate-sdk-operation-inventory.ts +21 -0
  108. package/scripts/resident-memory-bench.ts +174 -20
  109. package/scripts/verify-managed-empty-stop.ts +28 -0
  110. package/src/async/job-manager.ts +228 -122
  111. package/src/capability/index.ts +18 -0
  112. package/src/capability/mcp.ts +2 -0
  113. package/src/capability/skill.ts +2 -0
  114. package/src/capability/types.ts +6 -0
  115. package/src/cli/auth-broker-cli.ts +1 -1
  116. package/src/cli/config-cli.ts +8 -18
  117. package/src/cli/customize-doctor.ts +15 -2
  118. package/src/cli/public-command-errors.ts +21 -1
  119. package/src/cli/setup-cli.ts +4 -4
  120. package/src/cli/skills-cli.ts +5 -0
  121. package/src/cli-main.ts +7 -8
  122. package/src/commands/deep-interview.ts +47 -8
  123. package/src/commands/sdk.ts +140 -29
  124. package/src/config/autorouting-tier-map.ts +117 -7
  125. package/src/config/file-lock.ts +307 -68
  126. package/src/config/mcp-schema.json +4 -0
  127. package/src/config/model-bindings-applier.ts +20 -0
  128. package/src/config/model-profile-activation.ts +240 -46
  129. package/src/config/model-registry.ts +47 -153
  130. package/src/config/settings.ts +44 -4
  131. package/src/config/skill-settings-defaults.ts +7 -0
  132. package/src/coordinator-mcp/policy.ts +3 -2
  133. package/src/cursor-delete-path.ts +10 -0
  134. package/src/cursor-mcp-name.ts +15 -0
  135. package/src/cursor.ts +13 -2
  136. package/src/customization/inventory.ts +3 -0
  137. package/src/debug/report-bundle-parent.ts +22 -0
  138. package/src/debug/report-bundle.ts +3 -0
  139. package/src/debug/system-info.ts +13 -1
  140. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.ts +67 -4
  141. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/version-manager.ts +80 -0
  142. package/src/defaults/gjc/skills/deep-interview/SKILL.md +105 -10
  143. package/src/defaults/gjc/skills/ralplan/SKILL.md +6 -4
  144. package/src/defaults/gjc-defaults.test.ts +18 -1
  145. package/src/defaults/gjc-defaults.ts +3 -0
  146. package/src/defaults/gjc-skills.generated.ts +58 -36
  147. package/src/discovery/agents.ts +6 -1
  148. package/src/discovery/builtin.ts +10 -0
  149. package/src/discovery/helpers.ts +168 -23
  150. package/src/discovery/mcp-json.ts +6 -0
  151. package/src/discovery/ssh.ts +9 -4
  152. package/src/discovery/substitute-plugin-root.ts +3 -1
  153. package/src/edit/file-read-cache.ts +1 -1
  154. package/src/eval/py/executor.ts +380 -120
  155. package/src/export/html/css-color.ts +9 -0
  156. package/src/export/html/index.ts +5 -4
  157. package/src/export/html/template.generated.ts +1 -1
  158. package/src/extensibility/extensions/runner.ts +4 -0
  159. package/src/extensibility/extensions/types.ts +7 -0
  160. package/src/extensibility/gjc-plugins/runtime-adapters.ts +73 -8
  161. package/src/extensibility/plugins/manager.ts +3 -2
  162. package/src/extensibility/plugins/marketplace/cache.ts +15 -0
  163. package/src/extensibility/plugins/marketplace/manager.ts +11 -3
  164. package/src/extensibility/plugins/plugin-link-path.ts +12 -0
  165. package/src/extensibility/runtime-skill-discovery.ts +20 -2
  166. package/src/extensibility/skill-management.ts +6 -2
  167. package/src/extensibility/skills.ts +6 -1
  168. package/src/gjc-runtime/deep-interview-crystallize.ts +1814 -0
  169. package/src/gjc-runtime/deep-interview-recorder.ts +20 -2
  170. package/src/gjc-runtime/deep-interview-runtime.ts +1415 -13
  171. package/src/gjc-runtime/deep-interview-stage.ts +98 -21
  172. package/src/gjc-runtime/gc-runtime.ts +206 -8
  173. package/src/gjc-runtime/launch-worktree.ts +14 -4
  174. package/src/gjc-runtime/ralplan-runtime.ts +447 -52
  175. package/src/gjc-runtime/state-argv.ts +2 -0
  176. package/src/gjc-runtime/state-migrations.ts +91 -29
  177. package/src/gjc-runtime/state-runtime.ts +3716 -135
  178. package/src/gjc-runtime/state-writer.ts +83 -8
  179. package/src/gjc-runtime/ultragoal-runtime.ts +16 -4
  180. package/src/gjc-runtime/workflow-manifest.generated.json +70 -42
  181. package/src/gjc-runtime/workflow-manifest.ts +54 -21
  182. package/src/harness-control-plane/operate.ts +12 -4
  183. package/src/harness-control-plane/owner.ts +5 -1
  184. package/src/hashline/parser.ts +48 -9
  185. package/src/hashline/recovery.ts +13 -4
  186. package/src/hindsight/config.ts +9 -3
  187. package/src/internal-urls/docs-index.generated.ts +19 -18
  188. package/src/internal-urls/skill-path.ts +19 -0
  189. package/src/internal-urls/skill-protocol.ts +2 -0
  190. package/src/lsp/edits.ts +6 -0
  191. package/src/lsp/workspace-path.ts +51 -0
  192. package/src/main.ts +195 -31
  193. package/src/modes/acp/acp-agent.ts +163 -7
  194. package/src/modes/components/custom-editor.ts +14 -0
  195. package/src/modes/components/customization/customization-dashboard.ts +12 -0
  196. package/src/modes/components/extensions/extension-dashboard.ts +2 -2
  197. package/src/modes/components/extensions/state-manager.ts +15 -5
  198. package/src/modes/components/model-selector.ts +1 -4
  199. package/src/modes/components/provider-onboarding-selector.ts +1 -1
  200. package/src/modes/components/status-line/segments.ts +4 -0
  201. package/src/modes/controllers/event-controller.ts +11 -4
  202. package/src/modes/controllers/input-controller.ts +74 -29
  203. package/src/modes/controllers/runtime-mcp-command-controller.ts +8 -3
  204. package/src/modes/controllers/selector-controller.ts +28 -15
  205. package/src/modes/interactive-mode.ts +4 -0
  206. package/src/modes/runtime-init.ts +4 -0
  207. package/src/modes/utils/abort-message.ts +3 -1
  208. package/src/modes/utils/interrupt-ownership.ts +12 -0
  209. package/src/modes/utils/keybinding-matchers.ts +4 -0
  210. package/src/modes/utils/macos-option-forwarding.ts +80 -0
  211. package/src/progress/collect-project-progress.ts +153 -0
  212. package/src/progress/progress-contract.ts +172 -0
  213. package/src/progress/progress-snapshot.ts +140 -0
  214. package/src/progress/project-progress.ts +446 -0
  215. package/src/progress/render-progress.ts +209 -0
  216. package/src/prompts/system/master-mode.md +1 -0
  217. package/src/prompts/tools/skill-discovery.md +1 -1
  218. package/src/runtime-mcp/client.ts +13 -4
  219. package/src/runtime-mcp/config-writer.ts +10 -6
  220. package/src/runtime-mcp/config.ts +1 -0
  221. package/src/runtime-mcp/elicitation.ts +142 -0
  222. package/src/runtime-mcp/oauth-flow.ts +5 -0
  223. package/src/runtime-mcp/oauth-public-url.ts +8 -0
  224. package/src/runtime-mcp/plugin-network-boundary.ts +4 -0
  225. package/src/runtime-mcp/smithery-registry.ts +22 -3
  226. package/src/runtime-mcp/transports/http.ts +2 -2
  227. package/src/runtime-mcp/types.ts +6 -0
  228. package/src/sdk/acp/adapter.ts +93 -8
  229. package/src/sdk/broker/broker-exit.ts +52 -23
  230. package/src/sdk/broker/broker.ts +83 -7
  231. package/src/sdk/broker/ensure.ts +2 -0
  232. package/src/sdk/broker/lifecycle.ts +820 -73
  233. package/src/sdk/broker/session-index.ts +191 -81
  234. package/src/sdk/broker/task-artifact-owner-validation.ts +259 -0
  235. package/src/sdk/bus/chat-daemon-cli.ts +38 -1
  236. package/src/sdk/bus/chat-daemon-control.ts +2 -2
  237. package/src/sdk/bus/chat-daemon-runtime.ts +3 -0
  238. package/src/sdk/bus/index.ts +241 -46
  239. package/src/sdk/bus/telegram-daemon-contract.ts +6 -1
  240. package/src/sdk/bus/telegram-daemon.ts +4 -2
  241. package/src/sdk/cli/session-cli.ts +17 -3
  242. package/src/sdk/client/client.ts +28 -8
  243. package/src/sdk/host/host.ts +6 -0
  244. package/src/sdk/host/query/handlers.ts +9 -1
  245. package/src/sdk/host/session-runtime.test.ts +2805 -411
  246. package/src/sdk/host/session-runtime.ts +428 -111
  247. package/src/sdk/host/surface-policy.ts +1 -0
  248. package/src/sdk/index.ts +8 -0
  249. package/src/sdk/lifecycle-session.ts +3 -1
  250. package/src/sdk/mcp/server.ts +15 -3
  251. package/src/sdk/prompt-deadline-manager.ts +15 -0
  252. package/src/sdk/protocol/operation-inventory.generated.json +146 -0
  253. package/src/sdk/protocol/operation-registry.ts +9 -2
  254. package/src/sdk/router/session-router.ts +94 -14
  255. package/src/sdk/session-list.ts +1 -0
  256. package/src/sdk/session.ts +81 -38
  257. package/src/session/agent-session.ts +674 -224
  258. package/src/session/artifacts.ts +174 -70
  259. package/src/session/internal/managed-gc-retirement-codec.ts +319 -0
  260. package/src/session/internal/managed-session-scope.ts +3457 -462
  261. package/src/session/internal/managed-session-storage.ts +482 -61
  262. package/src/session/internal/managed-task-owner-cleanup.ts +473 -0
  263. package/src/session/internal/task-artifact-owner-access.ts +245 -0
  264. package/src/session/internal/task-artifact-owner-transcript.ts +614 -0
  265. package/src/session/session-manager.ts +939 -133
  266. package/src/session/session-retirement.ts +714 -0
  267. package/src/session/session-storage.ts +418 -174
  268. package/src/session/session-transcript-header.ts +17 -0
  269. package/src/session/task-artifact-owner-codec.ts +641 -0
  270. package/src/session/task-artifact-owner-retirement.ts +477 -0
  271. package/src/session/task-artifact-owner.ts +289 -0
  272. package/src/setup/credential-auto-import.ts +12 -3
  273. package/src/setup/credential-import.ts +176 -5
  274. package/src/setup/paseo/announce.ts +6 -1
  275. package/src/skill-state/active-state.ts +132 -2
  276. package/src/slash-commands/builtin-registry.ts +39 -9
  277. package/src/slash-commands/helpers/progress-report.ts +23 -0
  278. package/src/stt/setup.ts +3 -5
  279. package/src/task/commands.ts +3 -1
  280. package/src/task/executor.ts +4 -0
  281. package/src/task/index.ts +6 -0
  282. package/src/tools/ask.ts +205 -6
  283. package/src/tools/ast-edit.ts +4 -0
  284. package/src/tools/bash-allowed-prefixes.ts +15 -0
  285. package/src/tools/bash-skill-urls.ts +56 -3
  286. package/src/tools/bash.ts +48 -79
  287. package/src/tools/descriptors.ts +5 -1
  288. package/src/tools/eval.ts +56 -36
  289. package/src/tools/image-gen.ts +1 -1
  290. package/src/tools/index.ts +3 -0
  291. package/src/tools/python.ts +187 -66
  292. package/src/tools/recipe/index.ts +9 -0
  293. package/src/tools/recipe/recipe-guard.ts +19 -0
  294. package/src/tools/restricted-cwd.ts +10 -0
  295. package/src/tools/skill-discovery.ts +5 -0
  296. package/src/tools/skill.ts +28 -1
  297. package/src/tools/todo-write.ts +10 -1
  298. package/src/tools/tool-catalog.generated.ts +1 -1
  299. package/src/tools/write.ts +1 -0
  300. package/src/utils/tools-checksum.ts +34 -0
  301. package/src/utils/tools-manager.ts +24 -6
  302. 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,
@@ -127,14 +128,15 @@ import {
127
128
  streamSimple,
128
129
  } from "@gajae-code/ai/core";
129
130
  import { normalizeAnthropicBaseUrl } from "@gajae-code/ai/providers/anthropic";
131
+ import { isExplicitCodexTerminalVeto } from "@gajae-code/ai/providers/openai-codex-responses";
130
132
  import {
131
133
  type AuthDisposition,
132
134
  beginAttempt,
133
135
  classifyFallbackTrigger,
134
136
  EMPTY_RESPONSE_PROVIDER_CODE,
135
137
  type FallbackAttemptToken,
136
- PROVIDER_PROTOCOL_MISMATCH_ERROR_CODE,
137
138
  type FallbackTriggerClass,
139
+ PROVIDER_PROTOCOL_MISMATCH_ERROR_CODE,
138
140
  SERVER_OVERLOADED_PROVIDER_CODE,
139
141
  STREAM_FIRST_EVENT_TIMEOUT_PROVIDER_CODE,
140
142
  } from "@gajae-code/ai/utils/fallback-transport";
@@ -217,6 +219,7 @@ import {
217
219
  type FoldReason,
218
220
  type JobFoldEvent,
219
221
  type OwnerSubagentShutdownLease,
222
+ type SubagentLifecycle,
220
223
  } from "../async";
221
224
  import { reset as resetCapabilities } from "../capability";
222
225
  import type { Rule } from "../capability/rule";
@@ -233,6 +236,15 @@ import {
233
236
  UnknownModelProfileError,
234
237
  validateModelProfileName,
235
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";
236
248
  import { resolveProfileBindings } from "../config/model-profiles";
237
249
  import {
238
250
  GJC_MODEL_ASSIGNMENT_TARGETS,
@@ -318,6 +330,7 @@ import {
318
330
  buildSkillPromptMessage,
319
331
  getSkillSlashCommandName,
320
332
  isNamespacedSkillSlashCommandName,
333
+ type ParsedSkillInvocation,
321
334
  parseSkillInvocations,
322
335
  type Skill,
323
336
  type SkillWarning,
@@ -407,11 +420,7 @@ import type { NotificationSessionController } from "../sdk/bus/session-control";
407
420
  import { buildSyntheticModelId, syntheticNamespaceCollision } from "../sdk/model-profile-model";
408
421
  import { sanitizePromptFailure } from "../sdk/prompt-failure";
409
422
  import type { SecretObfuscator } from "../secrets/obfuscator";
410
- import {
411
- formatNoCredentialOnboardingError,
412
- formatNoModelOnboardingError,
413
- NoModelSelectedError,
414
- } from "../setup/model-onboarding-guidance";
423
+ import { formatNoCredentialOnboardingError, NoModelSelectedError } from "../setup/model-onboarding-guidance";
415
424
  import {
416
425
  isCanonicalGjcWorkflowSkill,
417
426
  isWorkflowContinuationInert,
@@ -526,6 +535,7 @@ import type {
526
535
  SessionEntry,
527
536
  SessionManagerCloseOutcome,
528
537
  SessionMemoryStats,
538
+ SessionArtifactPublication,
529
539
  } from "./session-manager";
530
540
  import {
531
541
  createReadonlySessionManager,
@@ -1217,6 +1227,11 @@ type SendUserMessageDispatchOptions = SendUserMessageOptions & {
1217
1227
  };
1218
1228
 
1219
1229
  type InternalPromptOptions = PromptOptions & { sdkRunToken?: string };
1230
+ interface SdkContinuationOwnership {
1231
+ scope: AttemptScope;
1232
+ token: string;
1233
+ cohort: string[];
1234
+ }
1220
1235
  type InternalCustomMessageOptions = Pick<
1221
1236
  PromptOptions,
1222
1237
  | "streamingBehavior"
@@ -1426,6 +1441,7 @@ const PROVIDER_FIRST_EVENT_TIMEOUT_WITHOUT_ARTICLE_ERROR = "Provider stream time
1426
1441
  const BARE_DEFAULT_CODEX_RETRYABLE_ERROR =
1427
1442
  /^Codex error event(?:: .*)? \(code=(?:server_is_overloaded|server_error|internal_error)(?:, [^)]+)*\)$/;
1428
1443
  const BARE_DEFAULT_CODEX_RETRYABLE_CODES = new Set(["server_is_overloaded", "server_error", "internal_error"]);
1444
+ const BARE_DEFAULT_CODEX_OVERLOAD_ERROR = /^Codex error event(?:: .*)? \(code=server_is_overloaded(?:, [^)]+)*\)$/;
1429
1445
  const BARE_DEFAULT_OVERLOAD_CODES = new Set([SERVER_OVERLOADED_PROVIDER_CODE]);
1430
1446
  /** Anthropic's typed capacity-overload `error.type`, the only Anthropic overload code admitted below. */
1431
1447
  const ANTHROPIC_OVERLOADED_ERROR_TYPE = "overloaded_error";
@@ -1538,14 +1554,7 @@ function isExactTypedOverloadFacts(
1538
1554
  facts.retryMaxAttempts === undefined
1539
1555
  );
1540
1556
  }
1541
- /**
1542
- * True when transport facts are exactly the provider's statusless typed
1543
- * capacity-overload code (the provider code verbatim and, when present, the
1544
- * OpenAI code verbatim, with no status). The agent loop must not treat the new
1545
- * facts as managed transaction authority, and managed session fallback must
1546
- * not gain retry/advance authority from them, so this predicate keeps both
1547
- * guards reading only typed facts, never error prose.
1548
- */
1557
+
1549
1558
  function isStatuslessTypedOverloadFacts(facts: TransportFailureFacts | undefined): boolean {
1550
1559
  if (!facts) return false;
1551
1560
  return (
@@ -1554,10 +1563,38 @@ function isStatuslessTypedOverloadFacts(facts: TransportFailureFacts | undefined
1554
1563
  (facts.openaiErrorCode === undefined || facts.openaiErrorCode === SERVER_OVERLOADED_PROVIDER_CODE)
1555
1564
  );
1556
1565
  }
1566
+ // Deterministic auth/request/model diagnostics must surface even when a provider
1567
+ // labels the failure with a transient code.
1568
+ const TERMINAL_ERROR_MESSAGE =
1569
+ /unauthorized|forbidden|authentication_error|permission_error|permission denied|invalid api key|invalid_request_error|invalid request|bad request|bad_request|validation_error|unprocessable|payload too large|payment required|insufficient_quota|insufficient credits|missing required (parameter|field)|invalid schema|invalid tool_choice|unsupported (parameter|value|model)|model_not_found|no such model|unknown model|does not (exist|support)|request was aborted|request aborted|the user aborted/i;
1570
+
1571
+ function isTerminalCodexProviderFailure(message: AssistantMessage): boolean {
1572
+ if (message.api !== "openai-codex-responses") return false;
1573
+ if (isExplicitCodexTerminalVeto(message.transportFailure?.providerCode, message.errorMessage)) return true;
1574
+ return (
1575
+ (BARE_DEFAULT_CODEX_RETRYABLE_CODES.has(message.transportFailure?.providerCode ?? "") ||
1576
+ BARE_DEFAULT_CODEX_RETRYABLE_ERROR.test(message.errorMessage ?? "")) &&
1577
+ TERMINAL_ERROR_MESSAGE.test(message.errorMessage ?? "")
1578
+ );
1579
+ }
1557
1580
 
1558
1581
  function isBareDefaultCodexOverload(message: AssistantMessage): boolean {
1559
1582
  return (
1560
1583
  message.api === "openai-codex-responses" &&
1584
+ !TERMINAL_ERROR_MESSAGE.test(message.errorMessage ?? "") &&
1585
+ !isExplicitCodexTerminalVeto(undefined, message.errorMessage) &&
1586
+ (isExactTypedOverloadFacts(message, BARE_DEFAULT_OVERLOAD_CODES) ||
1587
+ (BARE_DEFAULT_CODEX_OVERLOAD_ERROR.test(message.errorMessage ?? "") &&
1588
+ !hasBareDefaultRetryDisqualifyingFacts(message))) &&
1589
+ !assistantMessageHasVisibleOrToolContent(message)
1590
+ );
1591
+ }
1592
+
1593
+ function isBareDefaultCodexProviderFailure(message: AssistantMessage): boolean {
1594
+ return (
1595
+ message.api === "openai-codex-responses" &&
1596
+ !TERMINAL_ERROR_MESSAGE.test(message.errorMessage ?? "") &&
1597
+ !isExplicitCodexTerminalVeto(message.transportFailure?.providerCode, message.errorMessage) &&
1561
1598
  (isExactTypedOverloadFacts(message, BARE_DEFAULT_CODEX_RETRYABLE_CODES) ||
1562
1599
  (BARE_DEFAULT_CODEX_RETRYABLE_ERROR.test(message.errorMessage ?? "") &&
1563
1600
  !hasBareDefaultRetryDisqualifyingFacts(message))) &&
@@ -1620,7 +1657,10 @@ function isBareDefaultAnthropicOverload(message: AssistantMessage): boolean {
1620
1657
  function assistantMessageHasVisibleOrToolContent(message: AssistantMessage): boolean {
1621
1658
  return message.content.some(content => {
1622
1659
  if (content.type === "text") return content.text.length > 0;
1623
- return content.type === "thinking" || content.type === "redactedThinking" || content.type === "toolCall";
1660
+ if (content.type === "thinking") {
1661
+ return content.thinking.length > 0 || (content.thinkingSignature?.length ?? 0) > 0;
1662
+ }
1663
+ return content.type === "redactedThinking" || content.type === "toolCall";
1624
1664
  });
1625
1665
  }
1626
1666
 
@@ -1763,20 +1803,14 @@ function summarizeAgentBashArtifactSave(
1763
1803
  };
1764
1804
  }
1765
1805
 
1766
- export interface AgentBashArtifactStore {
1767
- saveArtifact(content: string, toolType: string): Promise<string | undefined>;
1768
- getArtifactPath(id: string): Promise<string | null>;
1769
- }
1770
-
1771
1806
  export async function saveAgentBashOriginalArtifact(
1772
- store: AgentBashArtifactStore,
1807
+ publication: SessionArtifactPublication,
1773
1808
  originalText: string,
1774
1809
  ): Promise<BashArtifactSaveResult> {
1775
1810
  try {
1776
- const artifactId = await store.saveArtifact(originalText, "bash-original");
1777
- if (!artifactId) return { status: "failed", diagnostic: "storage returned no artifact id" };
1778
- const artifactPath = await store.getArtifactPath(artifactId);
1779
- 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);
1780
1814
  } catch (error) {
1781
1815
  return { status: "failed", diagnostic: boundAgentBashArtifactSaveDiagnostic(error) };
1782
1816
  }
@@ -2729,6 +2763,7 @@ export class AgentSession {
2729
2763
  #scopedModels: ScopedModelSelection[];
2730
2764
  #thinkingLevel: ThinkingLevel | undefined;
2731
2765
  #activeModelProfile: string | undefined;
2766
+ #unavailableModelProfile: string | undefined;
2732
2767
  #activeProfileInstalledRoles = new Map<string, ModelSelectorValue | undefined>();
2733
2768
  #activeProfileInstalledAgentOverrides = new Map<string, ModelSelectorValue | undefined>();
2734
2769
  #preProfileModel: Model | undefined;
@@ -2754,6 +2789,9 @@ export class AgentSession {
2754
2789
  #followUpReservationTransitionWaiters = new Set<() => void>();
2755
2790
  #selectionFenceGeneration = 0;
2756
2791
  #defaultModelSelectionMutationRevision = 0;
2792
+ #userModelSelectionRevision = 0;
2793
+ #userCanonicalVariantSelectionRevision = 0;
2794
+ #userCanonicalVariantSelection: string | undefined;
2757
2795
  #thinkingLevelMutationRevision = 0;
2758
2796
  #thinkingVisibilityMutationRevision = 0;
2759
2797
  #thinkingLevelLiveMutationRevision = 0;
@@ -3404,8 +3442,17 @@ export class AgentSession {
3404
3442
  this.#activeAttemptScope = undefined;
3405
3443
  this.#activeLogicalRunId = undefined;
3406
3444
  }
3407
- #acceptSdkAttemptRun(handle: AttemptRunHandle, sdkRunToken?: string, sdkRunTokens?: string[]): void {
3408
- 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 {
3409
3456
  const predecessorSdkRunToken =
3410
3457
  predecessorScope === undefined ? undefined : this.#sdkRunTokensByAttemptScope.get(predecessorScope);
3411
3458
  const carryRecoverySkip =
@@ -3415,14 +3462,23 @@ export class AgentSession {
3415
3462
  this.#skipPostPromptRecoveryWaitByAttemptScope.delete(predecessorScope);
3416
3463
  this.#acceptRunHandle(handle);
3417
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
+ }
3418
3472
  if (sdkRunToken !== undefined) {
3419
3473
  this.#activeSdkRunToken = sdkRunToken;
3420
3474
  this.#sdkRunTokensByAttemptScope.set(handle.scope, sdkRunToken);
3421
- const inheritedCohort =
3422
- predecessorScope !== undefined && predecessorSdkRunToken === sdkRunToken
3423
- ? this.#sdkRunCohortsByAttemptScope.get(predecessorScope)
3424
- : undefined;
3425
- 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
+ }
3426
3482
  }
3427
3483
  if (carryRecoverySkip) this.#skipPostPromptRecoveryWaitByAttemptScope.add(handle.scope);
3428
3484
  }
@@ -3572,6 +3628,7 @@ export class AgentSession {
3572
3628
  #restoreManagedFallbackGetApiKey: (() => void) | undefined;
3573
3629
  // Todo completion reminder state
3574
3630
  #todoReminderCount = 0;
3631
+ #todoReminderContinuationGeneration: number | undefined;
3575
3632
  #deepInterviewUserIntentEpoch = 0;
3576
3633
  #deepInterviewTurnOwnerEpoch = 0;
3577
3634
  #deepInterviewGenuineUserMessageEpochs = new WeakMap<object, number>();
@@ -3765,6 +3822,7 @@ export class AgentSession {
3765
3822
  #skipPostPromptRecoveryWaitByAttemptScope = new WeakSet<AttemptScope>();
3766
3823
  #sdkRunTokensByAttemptScope = new WeakMap<AttemptScope, string>();
3767
3824
  #sdkRunCohortsByAttemptScope = new WeakMap<AttemptScope, string[]>();
3825
+ #lifecycleScopesByAttemptScope = new WeakMap<AttemptScope, AttemptScope>();
3768
3826
  #activeSdkRunToken: string | undefined;
3769
3827
  #activeAttemptScope: AttemptScope | undefined;
3770
3828
  #attemptAuthority!: AttemptScopeAuthority;
@@ -3990,6 +4048,8 @@ export class AgentSession {
3990
4048
  // async event handlers settle. Subscribers treat agent_end as readiness, so
3991
4049
  // publishing it earlier lets a successor corrupt the prior prompt's lifecycle.
3992
4050
  #pendingAgentEndEmit: AgentSessionEvent | undefined;
4051
+ #agentEndContinuationDecisions = new WeakSet<AgentSessionEvent>();
4052
+ #pendingSdkAgentEnds = new Set<AgentSessionEvent>();
3993
4053
  // A scheduled continuation owns this terminal boundary until it either starts
3994
4054
  // the successor or proves it cannot. Holds prevent a false idle event while
3995
4055
  // preserving the predecessor for cancellation and preflight failures.
@@ -4000,6 +4060,8 @@ export class AgentSession {
4000
4060
  #agentEndPublicationInFlight = 0;
4001
4061
  #agentEndPublicationPromise: Promise<void> = Promise.resolve();
4002
4062
  #agentEndHandlingPromise: Promise<void> = Promise.resolve();
4063
+ #sdkTerminalPublications = new Map<string, PromiseWithResolvers<void>>();
4064
+ #publishingSdkTerminals = new Set<PromiseWithResolvers<void>>();
4003
4065
 
4004
4066
  #obfuscator: SecretObfuscator | undefined;
4005
4067
  #checkpointState: CheckpointState | undefined = undefined;
@@ -4038,11 +4100,7 @@ export class AgentSession {
4038
4100
  readonly rawSseDebugBuffer: RawSseDebugBuffer;
4039
4101
 
4040
4102
  #acquirePowerAssertion(): void {
4041
- if (
4042
- process.platform !== "darwin" &&
4043
- process.platform !== "linux" &&
4044
- process.platform !== "win32"
4045
- ) {
4103
+ if (process.platform !== "darwin" && process.platform !== "linux" && process.platform !== "win32") {
4046
4104
  return;
4047
4105
  }
4048
4106
  if (this.#powerAssertion || this.#powerAssertionLoad) return;
@@ -4672,8 +4730,21 @@ export class AgentSession {
4672
4730
  }
4673
4731
  }
4674
4732
 
4675
- #reserveDeferredAgentEndForContinuation(): symbol | undefined {
4676
- 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));
4677
4748
  if (!pending) return undefined;
4678
4749
  const hold = Symbol("deferred-agent-end-continuation");
4679
4750
  this.#pendingAgentEndContinuationHolds.set(hold, pending);
@@ -4687,6 +4758,8 @@ export class AgentSession {
4687
4758
  throw new Error("Cannot restore a deferred agent_end over a different pending terminal event");
4688
4759
  }
4689
4760
  this.#pendingAgentEndEmit = pending;
4761
+ const scope = (pending as AgentSessionEvent & { scope?: AttemptScope }).scope;
4762
+ if (scope && this.#sdkRunTokensByAttemptScope.has(scope)) this.#pendingSdkAgentEnds.add(pending);
4690
4763
  const hold = Symbol("deferred-agent-end-continuation");
4691
4764
  this.#pendingAgentEndContinuationHolds.set(hold, pending);
4692
4765
  this.#deferredAgentEndWorkLeases.set(hold, this.#sessionWorkLease.acquire());
@@ -4697,6 +4770,7 @@ export class AgentSession {
4697
4770
  if (!hold) return undefined;
4698
4771
  const pending = this.#pendingAgentEndContinuationHolds.get(hold);
4699
4772
  this.#pendingAgentEndContinuationHolds.delete(hold);
4773
+ if (pending) this.#pendingSdkAgentEnds.delete(pending);
4700
4774
  this.#deferredAgentEndWorkLeases.get(hold)?.release();
4701
4775
  this.#deferredAgentEndWorkLeases.delete(hold);
4702
4776
  if (pending && this.#pendingAgentEndEmit === pending) {
@@ -4714,6 +4788,8 @@ export class AgentSession {
4714
4788
  }
4715
4789
 
4716
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);
4717
4793
  if (pending && !this.#pendingAgentEndEmit) {
4718
4794
  this.#pendingAgentEndEmit = pending;
4719
4795
  }
@@ -4726,6 +4802,8 @@ export class AgentSession {
4726
4802
  ): void {
4727
4803
  if (!hold || !pending) return;
4728
4804
  this.#pendingAgentEndEmit = pending;
4805
+ const scope = (pending as AgentSessionEvent & { scope?: AttemptScope }).scope;
4806
+ if (scope && this.#sdkRunTokensByAttemptScope.has(scope)) this.#pendingSdkAgentEnds.add(pending);
4729
4807
  this.#pendingAgentEndContinuationHolds.set(hold, pending);
4730
4808
  }
4731
4809
 
@@ -5028,11 +5106,15 @@ export class AgentSession {
5028
5106
  this.#flushPendingAgentEnd();
5029
5107
  return flushError;
5030
5108
  }
5031
- 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> {
5032
5114
  const flushError = this.#endInFlight(token);
5033
5115
  const predecessorPromptStillInFlight = this.#livePromptsInFlight() > 0;
5034
5116
  if (promptWait === "publication") {
5035
- await this.#agentEndPublicationPromise;
5117
+ await (sdkPublication ?? this.#agentEndPublicationPromise);
5036
5118
  } else if (promptWait === "full") {
5037
5119
  await this.#agentEndHandlingPromise;
5038
5120
  await this.#waitForPostPromptRecovery();
@@ -5072,13 +5154,23 @@ export class AgentSession {
5072
5154
  }
5073
5155
 
5074
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;
5075
5168
  if (
5076
5169
  this.#livePromptsInFlight() > 0 ||
5077
5170
  this.#agentEventHandlersInFlight > 0 ||
5078
5171
  this.#pendingAgentEndContinuationHolds.size > 0
5079
5172
  )
5080
5173
  return;
5081
- const pending = this.#pendingAgentEndEmit;
5082
5174
  if (!pending) {
5083
5175
  this.#resolveSessionSettlement();
5084
5176
  return;
@@ -5090,13 +5182,16 @@ export class AgentSession {
5090
5182
  }
5091
5183
 
5092
5184
  #startAgentEndPublication(pending: AgentSessionEvent, lease?: RunResourceProducerLease): void {
5185
+ this.#agentEndContinuationDecisions.delete(pending);
5093
5186
  this.#agentEndPublicationInFlight++;
5094
5187
  const pendingScope = (pending as AgentSessionEvent & { scope?: AttemptScopeRef }).scope as
5095
5188
  | AttemptScope
5096
5189
  | undefined;
5097
5190
  const sdkTerminal = pendingScope !== undefined && this.#sdkRunTokensByAttemptScope.has(pendingScope);
5098
5191
  this.#agentEndPublicationPromise = this.#publishDeferredAgentEnd(pending, lease, sdkTerminal);
5099
- void this.#agentEndPublicationPromise;
5192
+ void this.#agentEndPublicationPromise.catch(error => {
5193
+ logger.warn("Agent terminal publication failed", { error: sanitizePromptFailure(error) });
5194
+ });
5100
5195
  }
5101
5196
 
5102
5197
  async #publishDeferredAgentEnd(
@@ -5112,6 +5207,13 @@ export class AgentSession {
5112
5207
  const publicationCorrelationId = publicationScope
5113
5208
  ? this.#sdkRunTokensByAttemptScope.get(publicationScope)
5114
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);
5115
5217
  let extensionDelivery: Promise<void> | undefined;
5116
5218
  const releaseLease = () => {
5117
5219
  if (!lease) return;
@@ -5204,7 +5306,15 @@ export class AgentSession {
5204
5306
  try {
5205
5307
  if (lease) await this.#runResourceLeaseContext.run(lease, publish);
5206
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;
5207
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
+ }
5208
5318
  if (extensionDelivery) void extensionDelivery.then(releaseLease, releaseLease);
5209
5319
  else releaseLease();
5210
5320
  this.#agentEndPublicationInFlight = Math.max(0, this.#agentEndPublicationInFlight - 1);
@@ -5463,12 +5573,7 @@ export class AgentSession {
5463
5573
  controller !== undefined &&
5464
5574
  controller.chain.entries.length > 1;
5465
5575
  if (shouldCapture && modelKey) this.#managedFallbackActiveCredentialRows.delete(modelKey);
5466
- if (
5467
- override &&
5468
- model &&
5469
- storageProvider === override.storageProvider &&
5470
- modelKey === override.modelKey
5471
- ) {
5576
+ if (override && model && storageProvider === override.storageProvider && modelKey === override.modelKey) {
5472
5577
  this.#managedFallbackNextCredentialOverride = undefined;
5473
5578
  const apiKey = await this.#modelRegistry.getApiKey(model, this.credentialSessionId, {
5474
5579
  credentialSelector: { kind: "id", value: String(override.rowId) },
@@ -5478,10 +5583,7 @@ export class AgentSession {
5478
5583
  return apiKey;
5479
5584
  }
5480
5585
  // The preselected credential is no longer available; try to resolve another one of the same kind.
5481
- const fallbackApiKey = await this.#resolveManagedFallbackCredentialRow(
5482
- model,
5483
- override.credentialKind,
5484
- );
5586
+ const fallbackApiKey = await this.#resolveManagedFallbackCredentialRow(model, override.credentialKind);
5485
5587
  if (fallbackApiKey !== undefined) {
5486
5588
  if (shouldCapture && model) this.#captureManagedFallbackActiveCredential(model);
5487
5589
  return fallbackApiKey;
@@ -5489,8 +5591,7 @@ export class AgentSession {
5489
5591
  throw new Error("Selected managed fallback credential is no longer available");
5490
5592
  }
5491
5593
  const apiKey = await invokeOriginalGetApiKey(provider);
5492
- if (shouldCapture && model && isAuthenticated(apiKey))
5493
- this.#captureManagedFallbackActiveCredential(model);
5594
+ if (shouldCapture && model && isAuthenticated(apiKey)) this.#captureManagedFallbackActiveCredential(model);
5494
5595
  return apiKey;
5495
5596
  };
5496
5597
  this.agent.getApiKey = managedGetApiKey;
@@ -5577,12 +5678,14 @@ export class AgentSession {
5577
5678
  void this.#queueFollowUpAfterReservation(message, displayText, {
5578
5679
  createDisplayEntry: false,
5579
5680
  trackExternalFollowUp: false,
5681
+ sdkRunToken: this.#activeSdkRunToken,
5580
5682
  }).catch(error => {
5581
5683
  this.#settleDeliveredOwnedRegistrations([message]);
5582
5684
  logger.warn("Owned streaming follow-up was rejected", { error: String(error) });
5583
5685
  });
5584
5686
  },
5585
5687
  injectIdle: async (messages, signal) => {
5688
+ const sdkRunToken = this.#activeSdkRunToken;
5586
5689
  // Mandated boundary comment (corrected turn semantics): same origin
5587
5690
  // split as the streaming injector — an allowed owned-completion
5588
5691
  // delivery starts a fresh turn attempt/lineage and is not a
@@ -5617,14 +5720,14 @@ export class AgentSession {
5617
5720
  await this.agent.prompt(first, {
5618
5721
  ...this.#managedFallbackPromptOptions(),
5619
5722
  onRunAccepted: (handle: AttemptRunHandle) => {
5620
- if (handle) this.#acceptSdkAttemptRun(handle);
5723
+ if (handle) this.#acceptSdkAttemptRun(handle, sdkRunToken);
5621
5724
  },
5622
5725
  });
5623
5726
  } else {
5624
5727
  await this.agent.prompt(survivors, {
5625
5728
  ...this.#managedFallbackPromptOptions(),
5626
5729
  onRunAccepted: (handle: AttemptRunHandle) => {
5627
- if (handle) this.#acceptSdkAttemptRun(handle);
5730
+ if (handle) this.#acceptSdkAttemptRun(handle, sdkRunToken);
5628
5731
  },
5629
5732
  });
5630
5733
  }
@@ -5796,9 +5899,8 @@ export class AgentSession {
5796
5899
  (largestPhase, phase) =>
5797
5900
  Math.max(
5798
5901
  largestPhase,
5799
- phase.tasks.filter(
5800
- task => task.status === "pending" || task.status === "in_progress",
5801
- ).length,
5902
+ phase.tasks.filter(task => task.status === "pending" || task.status === "in_progress")
5903
+ .length,
5802
5904
  ),
5803
5905
  0,
5804
5906
  );
@@ -6712,6 +6814,17 @@ export class AgentSession {
6712
6814
  return { running, recent, delivery };
6713
6815
  }
6714
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
+
6715
6828
  /**
6716
6829
  * Cancel async jobs registered by *this* agent only. Used by lifecycle
6717
6830
  * transitions (newSession, switchSession, handoff, dispose) so a subagent
@@ -6887,6 +7000,8 @@ export class AgentSession {
6887
7000
  object,
6888
7001
  { scope?: AttemptScope; sdkRunToken?: string; persistGeneration: number; persistBarrier?: Promise<void> }
6889
7002
  >();
7003
+ /** Extension handlers cannot mutate or replace the Agent-claimed run owner. */
7004
+ #terminalOwnerByExtensionEvent = new WeakMap<object, AgentTerminalOwnerContext>();
6890
7005
 
6891
7006
  /**
6892
7007
  * Capture what is true at the SYNCHRONOUS agent-event boundary, before any async work.
@@ -7040,8 +7155,8 @@ export class AgentSession {
7040
7155
  const pendingAgentEnd =
7041
7156
  event.type === "agent_end" &&
7042
7157
  !maintenanceCheckpoint &&
7043
- (this.#pendingAgentEndEmit === event || this.#deferredAgentEndLeases.has(event))
7044
- ? this.#pendingAgentEndEmit
7158
+ (this.#pendingAgentEndEmit === event || this.#pendingSdkAgentEnds.has(event))
7159
+ ? event
7045
7160
  : undefined;
7046
7161
  if (pendingAgentEnd) {
7047
7162
  this.#deferredAgentEndLeases.set(pendingAgentEnd, eventLease);
@@ -7058,6 +7173,7 @@ export class AgentSession {
7058
7173
  }
7059
7174
  }
7060
7175
  this.#agentEventHandlersInFlight = Math.max(0, this.#agentEventHandlersInFlight - 1);
7176
+ if (event.type === "agent_end") this.#agentEndContinuationDecisions.add(event);
7061
7177
  this.#flushPendingAgentEnd();
7062
7178
  agentEndHandled?.resolve();
7063
7179
  // Every other in-flight counter republishes settlement when it drops; this
@@ -7294,16 +7410,10 @@ export class AgentSession {
7294
7410
  // have unwound. Subscribers treat this event as the ready signal; flushing it
7295
7411
  // from abort while either barrier is active permits a successor to race the
7296
7412
  // prior prompt's cleanup.
7297
- const sdkTerminal =
7298
- event.type === "agent_end" && attemptScope !== undefined && this.#sdkRunTokensByAttemptScope.has(attemptScope);
7299
- if (
7300
- event.type === "agent_end" &&
7301
- sdkTerminal &&
7302
- this.#pendingAgentEndContinuationHolds.size === 0 &&
7303
- this.#pendingAgentEndEmit === undefined
7304
- ) {
7413
+ if (event.type === "agent_end" && attemptScope && this.#sdkRunTokensByAttemptScope.has(attemptScope)) {
7414
+ this.#pendingSdkAgentEnds.add(event);
7305
7415
  if (eventLease) this.#deferredAgentEndLeases.set(event, eventLease);
7306
- this.#startAgentEndPublication(event, eventLease);
7416
+ this.#pendingAgentEndEmit = event;
7307
7417
  return;
7308
7418
  }
7309
7419
  if (event.type === "agent_end" && (this.#livePromptsInFlight() > 0 || this.#agentEventHandlersInFlight > 0)) {
@@ -7417,6 +7527,8 @@ export class AgentSession {
7417
7527
  eventLease?: RunResourceProducerLease,
7418
7528
  ): Promise<void> => {
7419
7529
  const attemptScope = (event as AgentEvent & { scope?: AttemptScope }).scope;
7530
+ const terminalSdkOwnership =
7531
+ event.type === "agent_end" ? this.#captureSdkContinuationOwnership(attemptScope) : undefined;
7420
7532
 
7421
7533
  // These lifecycle boundaries can be delivered without awaiting this listener.
7422
7534
  // Revoke streaming-edit cache generations before any admission, spill, or
@@ -8125,7 +8237,7 @@ export class AgentSession {
8125
8237
  skipCompactionCheck: true,
8126
8238
  resourceRunId: activePromptHandle,
8127
8239
  maintenanceContinuation: true,
8128
- sdkRunToken: attemptScope ? this.#sdkRunTokensByAttemptScope.get(attemptScope) : undefined,
8240
+ sdkOwnership: terminalSdkOwnership,
8129
8241
  });
8130
8242
  }
8131
8243
  return;
@@ -8209,7 +8321,9 @@ export class AgentSession {
8209
8321
  }
8210
8322
 
8211
8323
  // Check for retryable errors first (overloaded, rate limit, server errors)
8212
- 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)) {
8213
8327
  const transportFailure = (msg as AssistantMessage & { transportFailure?: TransportFailureFacts })
8214
8328
  .transportFailure;
8215
8329
  const messageScope = this.#assistantAttemptScopes.get(msg);
@@ -8222,6 +8336,10 @@ export class AgentSession {
8222
8336
  );
8223
8337
  if (didRetry) return; // Retry was initiated, don't proceed to compaction
8224
8338
  }
8339
+ // Clear the reminder continuation flag after processing
8340
+ if (isReminderContinuationError) {
8341
+ this.#todoReminderContinuationGeneration = undefined;
8342
+ }
8225
8343
  if (this.#retryAttempt > 0) {
8226
8344
  // A prior retry ended on a non-retryable (terminal) message: emit
8227
8345
  // the terminal retry-end and reset so observers clear retry state.
@@ -8240,7 +8358,7 @@ export class AgentSession {
8240
8358
  const compactionTask = this.#schedulePostPromptTask(
8241
8359
  async () => {
8242
8360
  if (this.#isDisposed || this.#sessionAdmissionClosing) return;
8243
- await this.#checkCompaction(msg, true, undefined, activePromptHandle);
8361
+ await this.#checkCompaction(msg, true, undefined, activePromptHandle, undefined, terminalSdkOwnership);
8244
8362
  },
8245
8363
  { resourceRunId: activePromptHandle },
8246
8364
  );
@@ -8466,10 +8584,12 @@ export class AgentSession {
8466
8584
  /** Internal predecessor terminal event sequestered while waiting behind selection. */
8467
8585
  deferredPredecessorAgentEnd?: AgentSessionEvent;
8468
8586
  /** Internal causal SDK owner captured when this continuation was scheduled. */
8469
- sdkRunToken?: string;
8587
+ sdkOwnership?: SdkContinuationOwnership;
8588
+ /** Disable managed fallback retries for this continuation (used for terminal server-initiated turns). */
8589
+ disableManagedFallback?: boolean;
8470
8590
  }): Promise<void> {
8471
8591
  const continuationAdmission = this.#captureScheduledContinuationAdmission();
8472
- const scheduledSdkRunToken = options?.sdkRunToken;
8592
+ const scheduledSdkOwnership = options?.sdkOwnership;
8473
8593
  const selectionFenceGeneration =
8474
8594
  options?.selectionFenceGeneration ??
8475
8595
  this.#selectionFenceGenerationContext.getStore() ??
@@ -8491,7 +8611,7 @@ export class AgentSession {
8491
8611
  generation: deferredPromptGeneration,
8492
8612
  selectionFenceGeneration,
8493
8613
  deferredPredecessorAgentEnd,
8494
- sdkRunToken: scheduledSdkRunToken,
8614
+ sdkOwnership: scheduledSdkOwnership,
8495
8615
  });
8496
8616
  } finally {
8497
8617
  // The recursive call synchronously re-reserved its settlement
@@ -8611,14 +8731,21 @@ export class AgentSession {
8611
8731
  }
8612
8732
  const predecessorAgentEnd =
8613
8733
  this.#claimDeferredAgentEndForContinuation(predecessorAgentEndHold);
8734
+ const predecessorScope = (
8735
+ predecessorAgentEnd as (AgentSessionEvent & { scope?: AttemptScope }) | undefined
8736
+ )?.scope;
8737
+ const predecessorSdkOwnership = this.#captureSdkContinuationOwnership(predecessorScope);
8614
8738
  let predecessorAccepted = false;
8615
8739
  const releasePredecessor = () => {
8616
8740
  if (predecessorAccepted) return;
8617
8741
  predecessorAccepted = true;
8618
- this.#releaseDeferredAgentEndLease(predecessorAgentEnd);
8742
+ if (startsQueuedSuccessor)
8743
+ this.#restoreDeferredAgentEndAfterContinuationFailure(predecessorAgentEnd);
8744
+ else this.#releaseDeferredAgentEndLease(predecessorAgentEnd);
8619
8745
  };
8620
8746
  const hasQueuedMessages = this.agent.hasQueuedMessages();
8621
8747
  const startsQueuedSuccessor =
8748
+ options?.maintenanceContinuation !== true &&
8622
8749
  hasQueuedMessages &&
8623
8750
  (options?.continueQueuedOnly === true ||
8624
8751
  this.agent.state.messages.at(-1)?.role === "assistant");
@@ -8627,7 +8754,7 @@ export class AgentSession {
8627
8754
  : this.agent.continue.bind(this.agent);
8628
8755
  try {
8629
8756
  await continueQueued({
8630
- ...this.#managedFallbackPromptOptions(),
8757
+ ...(options?.disableManagedFallback ? { fallbackManaged: false } : this.#managedFallbackPromptOptions()),
8631
8758
  maintenanceContinuation: options?.maintenanceContinuation,
8632
8759
  // Reset only after continue() has claimed the queued turn. Skipped or stale
8633
8760
  // continuations retain predecessor accounting, and resetAttemptBudget keeps
@@ -8637,13 +8764,21 @@ export class AgentSession {
8637
8764
  // agent_start (review P1); their queued messages are in-run
8638
8765
  // consumptions, not own-run promotions.
8639
8766
  const startsOwn = options?.maintenanceContinuation !== true;
8640
- const inheritedSdkRunToken = options?.continueQueuedOnly
8641
- ? undefined
8642
- : (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);
8643
8775
  const consumedSdkRunTokens = acceptance.consumedQueuedMessages
8644
8776
  .map(message => this.#sdkRunTokensByQueuedMessage.get(message))
8645
8777
  .filter((token): token is string => token !== undefined);
8646
- const sdkRunToken = consumedSdkRunTokens[0] ?? inheritedSdkRunToken;
8778
+ const sdkRunToken = inheritedSdkOwnership?.token ?? consumedSdkRunTokens[0];
8779
+ const sdkRunTokens = inheritedSdkOwnership
8780
+ ? [...new Set([...inheritedSdkOwnership.cohort, ...consumedSdkRunTokens])]
8781
+ : consumedSdkRunTokens;
8647
8782
  this.#fireQueuedPromotionHooks(acceptance.consumedQueuedMessages, {
8648
8783
  startsOwnRun: startsOwn,
8649
8784
  });
@@ -8651,7 +8786,8 @@ export class AgentSession {
8651
8786
  this.#acceptSdkAttemptRun(
8652
8787
  handle,
8653
8788
  sdkRunToken,
8654
- consumedSdkRunTokens.length > 0 ? consumedSdkRunTokens : undefined,
8789
+ sdkRunTokens.length > 0 ? sdkRunTokens : undefined,
8790
+ inheritedPredecessorScope,
8655
8791
  );
8656
8792
  options?.onRunAccepted?.(handle);
8657
8793
  // Keep the queued token available through the acceptance callback;
@@ -8778,7 +8914,11 @@ export class AgentSession {
8778
8914
  return compactionSettings.autoContinue === false ? "auto_continue_disabled_non_resumable_tail" : undefined;
8779
8915
  }
8780
8916
 
8781
- async #scheduleOverflowRetryContinuation(generation: number, resourceRunId?: string): Promise<boolean> {
8917
+ async #scheduleOverflowRetryContinuation(
8918
+ generation: number,
8919
+ resourceRunId?: string,
8920
+ sdkOwnership?: SdkContinuationOwnership,
8921
+ ): Promise<boolean> {
8782
8922
  this.#stripOverflowFailedTurnForRetry();
8783
8923
  const snapshot = await this.#compactionStateSnapshot();
8784
8924
  if (
@@ -8795,6 +8935,7 @@ export class AgentSession {
8795
8935
  generation,
8796
8936
  suppressPredecessorAgentEnd: true,
8797
8937
  resourceRunId,
8938
+ sdkOwnership,
8798
8939
  onSkip: reason => this.#logCompactionContinuationSkipped("overflow_retry", reason),
8799
8940
  onError: error => this.#logCompactionContinuationError("overflow_retry", error),
8800
8941
  });
@@ -8803,7 +8944,7 @@ export class AgentSession {
8803
8944
 
8804
8945
  const compactionSettings = this.settings.getGroup("compaction");
8805
8946
  if (compactionSettings.autoContinue !== false) {
8806
- this.#scheduleAutoContinuePrompt(generation, false, resourceRunId);
8947
+ this.#scheduleAutoContinuePrompt(generation, false, resourceRunId, undefined, undefined, sdkOwnership);
8807
8948
  return true;
8808
8949
  }
8809
8950
 
@@ -8817,7 +8958,7 @@ export class AgentSession {
8817
8958
  resourceRunId?: string,
8818
8959
  deferredSelectionFenceGeneration?: number,
8819
8960
  deferredPredecessorAgentEnd?: AgentSessionEvent,
8820
- sdkRunToken?: string,
8961
+ sdkOwnership?: SdkContinuationOwnership,
8821
8962
  ): void {
8822
8963
  if (this.#sessionTransitionKind !== undefined || this.#handoffTransitionActive) {
8823
8964
  this.#deferredAutoContinueDuringTransition = () =>
@@ -8827,14 +8968,16 @@ export class AgentSession {
8827
8968
  resourceRunId,
8828
8969
  deferredSelectionFenceGeneration,
8829
8970
  deferredPredecessorAgentEnd,
8830
- sdkRunToken,
8971
+ sdkOwnership,
8831
8972
  );
8832
8973
  return;
8833
8974
  }
8834
8975
  const scheduledGeneration = generation;
8835
- const scheduledSdkRunToken =
8836
- sdkRunToken ??
8837
- (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
+ );
8838
8981
  const continuationAuthorized = async (
8839
8982
  signal: AbortSignal,
8840
8983
  hasPendingNextTurnMessages = false,
@@ -8891,7 +9034,7 @@ export class AgentSession {
8891
9034
  resourceRunId,
8892
9035
  selectionFenceGeneration,
8893
9036
  predecessorAgentEnd,
8894
- scheduledSdkRunToken,
9037
+ scheduledSdkOwnership,
8895
9038
  );
8896
9039
  } finally {
8897
9040
  this.#endSelectionFenceDeferralTracking(selectionFenceGeneration);
@@ -8939,7 +9082,9 @@ export class AgentSession {
8939
9082
  promptText,
8940
9083
  {
8941
9084
  skipPostPromptRecoveryWait: true,
8942
- sdkRunToken: scheduledSdkRunToken,
9085
+ sdkRunToken: scheduledSdkOwnership?.token,
9086
+ sdkRunTokens: scheduledSdkOwnership?.cohort,
9087
+ sdkPredecessorScope: scheduledSdkOwnership?.scope,
8943
9088
  skipCompactionCheck: true,
8944
9089
  predecessorAgentEndHold,
8945
9090
  onFinalPreflight: ({ hasPendingNextTurnMessages }) =>
@@ -9875,7 +10020,12 @@ export class AgentSession {
9875
10020
  type: "agent_start",
9876
10021
  ...(sdkRunToken ? { sdkRunToken } : {}),
9877
10022
  ...(deliveryScope
9878
- ? { 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
+ }
9879
10029
  : {}),
9880
10030
  },
9881
10031
  undefined,
@@ -9893,17 +10043,19 @@ export class AgentSession {
9893
10043
  deliveryScope,
9894
10044
  );
9895
10045
  } else if (event.type === "agent_end") {
9896
- await this.#extensionRunner.emit(
9897
- {
9898
- type: "agent_end",
9899
- messages: event.messages,
9900
- stopReason: event.stopReason,
9901
- maintenanceOutcome: event.maintenanceOutcome,
9902
- ...(sdkRunToken ? { sdkRunToken } : {}),
9903
- },
9904
- undefined,
9905
- deliveryScope,
9906
- );
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);
9907
10059
  } else if (event.type === "turn_start") {
9908
10060
  const hookEvent: TurnStartEvent = {
9909
10061
  type: "turn_start",
@@ -10110,6 +10262,22 @@ export class AgentSession {
10110
10262
  this.#abortActiveMidRunBarriers();
10111
10263
  this.#compactionQueuedDeliveryArmed = false;
10112
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
+ }
10113
10281
  this.#unsubscribeAgent();
10114
10282
  this.#unsubscribeAgent = undefined;
10115
10283
  }
@@ -10267,6 +10435,11 @@ export class AgentSession {
10267
10435
  // settle every accepted SDK handle now so consumed submissions cannot wait
10268
10436
  // forever for an agent_end that teardown deliberately suppresses.
10269
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();
10270
10443
  // Invalidate every coordinator event admitted before disposal. Handlers may
10271
10444
  // still unwind, but their captured generation can no longer enqueue a write.
10272
10445
  this.#coordinatorPersistGeneration += 1;
@@ -10636,6 +10809,11 @@ export class AgentSession {
10636
10809
  * at `timeoutMs` so a wedged subprocess can't stall process exit.
10637
10810
  */
10638
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
+ });
10639
10817
  const sessionId = this.sessionManager.getSessionId();
10640
10818
  const kernelOwnerId = this.#evalKernelOwnerId;
10641
10819
  this.#unregisterResourceGc?.();
@@ -10645,6 +10823,7 @@ export class AgentSession {
10645
10823
  this.#unregisterDelegationHintSettings?.();
10646
10824
  this.#unregisterDelegationHintSettings = undefined;
10647
10825
  const work = Promise.allSettled([
10826
+ evalExecutionsSettled,
10648
10827
  // kill:true so a forced exit also reaps spawned-app Chrome we own (headless
10649
10828
  // always closes; connected/attached browsers only disconnect — never killed).
10650
10829
  releaseTabsForOwner(sessionId, { kill: true }).catch((error: unknown) =>
@@ -10673,7 +10852,8 @@ export class AgentSession {
10673
10852
  logger.warn("signal teardown: tool session transition cleanups failed", { error }),
10674
10853
  ),
10675
10854
  ]);
10676
- 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");
10677
10857
  }
10678
10858
 
10679
10859
  #rebindProviderSessionState(providerSessionState: Map<string, ProviderSessionState>): void {
@@ -12322,7 +12502,9 @@ export class AgentSession {
12322
12502
  */
12323
12503
  get messages(): AgentMessage[] {
12324
12504
  const messages = this.agent.state.messages;
12325
- 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
+ )
12326
12508
  ? this.#withoutEphemeralCustomMessages(messages)
12327
12509
  : messages;
12328
12510
  }
@@ -13463,6 +13645,36 @@ export class AgentSession {
13463
13645
  }
13464
13646
  }
13465
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
+
13466
13678
  async #promptInternal(
13467
13679
  text: string,
13468
13680
  options: PromptOptions | undefined,
@@ -13484,38 +13696,29 @@ export class AgentSession {
13484
13696
  if (owner && !owner.released) throw this.#sessionAdmissionBusyError();
13485
13697
  const expandPromptTemplates = options?.expandPromptTemplates ?? true;
13486
13698
 
13487
- if (
13488
- expandPromptTemplates &&
13489
- text.startsWith("/") &&
13490
- isNamespacedSkillSlashCommandName(text.slice(1)) &&
13491
- !options?.images?.length
13492
- ) {
13493
- const skillCommands = new Map(this.skills.map(skill => [getSkillSlashCommandName(skill), skill]));
13494
- const invocations = parseSkillInvocations(text, skillCommands);
13495
- if (invocations.length === 1) {
13496
- const invocation = invocations[0];
13497
- if (invocation) {
13498
- await this.invokeSkill(
13499
- invocation.skill.name,
13500
- invocation.args,
13501
- options?.onPreflightAccepted ||
13502
- options?.onPreflightAcceptCommit ||
13503
- options?.preflightSignal ||
13504
- internalOptions?.sdkRunToken
13505
- ? {
13506
- ...(options?.onPreflightAccepted
13507
- ? { onPreflightAccepted: options.onPreflightAccepted }
13508
- : {}),
13509
- ...(options?.onPreflightAcceptCommit
13510
- ? { onPreflightAcceptCommit: options.onPreflightAcceptCommit }
13511
- : {}),
13512
- ...(options?.preflightSignal ? { preflightSignal: options.preflightSignal } : {}),
13513
- ...(options?.sdkRunCapability ? { sdkRunCapability: options.sdkRunCapability } : {}),
13514
- }
13515
- : undefined,
13516
- );
13517
- return;
13518
- }
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;
13519
13722
  }
13520
13723
  }
13521
13724
 
@@ -13932,6 +14135,8 @@ export class AgentSession {
13932
14135
  | "preflightSignal"
13933
14136
  > & {
13934
14137
  sdkRunToken?: string;
14138
+ sdkRunTokens?: string[];
14139
+ sdkPredecessorScope?: AttemptScope;
13935
14140
  prependMessages?: AgentMessage[];
13936
14141
  skipPostPromptRecoveryWait?: boolean;
13937
14142
  predecessorAgentEndHold?: symbol;
@@ -14002,6 +14207,7 @@ export class AgentSession {
14002
14207
  let skipPostPromptRecoveryWait =
14003
14208
  options?.skipPostPromptRecoveryWait === true || options?.sdkRunToken !== undefined;
14004
14209
  let promptAttemptScope: AttemptScope | undefined;
14210
+ let promptSdkPublication: Promise<void> | undefined;
14005
14211
  let hindsightRecall: string | undefined;
14006
14212
  try {
14007
14213
  this.#throwIfPromptPreflightCancelled(generation, preflightSignal);
@@ -14302,10 +14508,26 @@ export class AgentSession {
14302
14508
  ...(options?.sdkRunToken ? { sdkRunToken: options.sdkRunToken } : {}),
14303
14509
  onRunAccepted: (handle: AttemptRunHandle) => {
14304
14510
  promptAttemptScope = handle?.scope;
14305
- 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
+ );
14306
14527
  else this.#acceptRunHandle(handle);
14307
14528
  if (handle && options?.sdkRunToken) {
14308
14529
  this.#activeSdkRunToken = options.sdkRunToken;
14530
+ promptSdkPublication = this.#sdkTerminalPublications.get(options.sdkRunToken)?.promise;
14309
14531
  }
14310
14532
  options?.onRunAccepted?.(handle);
14311
14533
  options?.admissionLease?.release();
@@ -14388,7 +14610,11 @@ export class AgentSession {
14388
14610
  this.#releaseIrcRosterClaim(rosterClaim.token, rosterClaim.epoch);
14389
14611
  }
14390
14612
  this.#releaseDeferredAgentEndContinuation(predecessorAgentEndHold);
14391
- await this.#settleEndedInFlight(inFlightPrompt, skipPostPromptRecoveryWait ? "publication" : "full");
14613
+ await this.#settleEndedInFlight(
14614
+ inFlightPrompt,
14615
+ skipPostPromptRecoveryWait ? "publication" : "full",
14616
+ promptSdkPublication,
14617
+ );
14392
14618
  }
14393
14619
  }
14394
14620
 
@@ -14676,10 +14902,7 @@ export class AgentSession {
14676
14902
  // prompt still in preflight, aborting) has no run to steer. Ordinary steers
14677
14903
  // become a sequential follow-up owned by the next turn; SDK turn-bound
14678
14904
  // steers fail closed rather than being delivered to a successor run.
14679
- if (
14680
- options?.expectedSdkRunToken !== undefined &&
14681
- this.#activeSdkRunToken !== options.expectedSdkRunToken
14682
- )
14905
+ if (options?.expectedSdkRunToken !== undefined && this.#activeSdkRunToken !== options.expectedSdkRunToken)
14683
14906
  throw Object.assign(new Error("The expected SDK run is not active."), { code: "turn_not_active" });
14684
14907
  const admission = this.agent.steer(message, options?.forceOneAtATime ? { forceOneAtATime: true } : undefined);
14685
14908
  if (!admission.admitted) {
@@ -14736,8 +14959,8 @@ export class AgentSession {
14736
14959
  /**
14737
14960
  * One-shot preflight-abort binding: an invocation cancelled after its queue
14738
14961
  * admission must cancel the submission it admitted, so an aborted dispatch
14739
- * can never execute later. Both explicit delivery paths (follow-up and
14740
- * 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
14741
14964
  * (exact-head review P1).
14742
14965
  */
14743
14966
  #bindPreflightAbortCancellation(
@@ -15418,6 +15641,7 @@ export class AgentSession {
15418
15641
  forceOneAtATime: sequential !== undefined,
15419
15642
  createDisplayEntry: false,
15420
15643
  trackExternalFollowUp: false,
15644
+ sdkRunToken: this.#activeSdkRunToken,
15421
15645
  });
15422
15646
  if (this.#abortUnwind && !sequential) this.#abortUnwindSteerFallbacks.push(appMessage);
15423
15647
  // The chip now describes follow-up work: keep its mode and identity
@@ -15448,6 +15672,7 @@ export class AgentSession {
15448
15672
  forceOneAtATime: options?.followUpQueuePolicy === "sequential",
15449
15673
  createDisplayEntry: false,
15450
15674
  trackExternalFollowUp: false,
15675
+ sdkRunToken: this.#activeSdkRunToken,
15451
15676
  });
15452
15677
  this.#bindCustomDisplayEntry(appMessage, "followUp");
15453
15678
  // The session can report streaming while no agent loop owns the queue
@@ -15869,12 +16094,13 @@ export class AgentSession {
15869
16094
  if (this.#isLiveTurnBusy() && !waitedForAbortUnwind) {
15870
16095
  if (options?.onPreflightAcceptCommit) await options.onPreflightAcceptCommit();
15871
16096
  assertPreflightStillOpen();
15872
- await this.#queueSteer(text, images, {
16097
+ const queuedSteer = await this.#queueSteer(text, images, {
15873
16098
  claimsGenuineUserIntent: true,
15874
- onPromoted: options?.onQueuedPromoted,
16099
+ onPromoted: onQueuedPromoted,
15875
16100
  external: true,
15876
16101
  sdkRunToken: internalOptions?.sdkRunToken,
15877
16102
  });
16103
+ this.#bindPreflightAbortCancellation(options?.preflightSignal, queuedSteer);
15878
16104
  // Dispatch-race disposition (#4668 review P1): the SDK snapshot-decided
15879
16105
  // this submission starts its own turn (idle at dispatch), but the
15880
16106
  // session began streaming before sendUserMessage ran, so the message
@@ -15884,8 +16110,15 @@ export class AgentSession {
15884
16110
  // would terminalize the accepted request as an own-run completion
15885
16111
  // before it is consumed. Report the internal in-run disposition so the
15886
16112
  // runtime attaches the correlation to the in-flight run instead.
15887
- options?.onDispatchDisposition?.({ startsOwnRun: false });
15888
- 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
+ }
15889
16122
  return;
15890
16123
  }
15891
16124
 
@@ -16816,6 +17049,12 @@ export class AgentSession {
16816
17049
  if (!lineageIdHash) return undefined;
16817
17050
  return this.#promptGeneration;
16818
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
+ }
16819
17058
  /**
16820
17059
  * Logical endpoint used to key owned-registration lineage bindings.
16821
17060
  * Subagents inherit the parent's AsyncJobManager, whose completion callback
@@ -17974,8 +18213,12 @@ export class AgentSession {
17974
18213
  }
17975
18214
 
17976
18215
  options?.onMutationStarted?.();
17977
- 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;
17978
18220
  this.#seedSessionCanonicalVariant(model);
18221
+ if (cause === "user-selection") this.#recordUserCanonicalVariantSelection();
17979
18222
  this.sessionManager.appendModelChange(`${model.provider}/${model.id}`, role);
17980
18223
  this.settings.setModelRole(
17981
18224
  role,
@@ -17983,7 +18226,7 @@ export class AgentSession {
17983
18226
  );
17984
18227
  // Only an explicit user selection starts a new fallback epoch. Internal
17985
18228
  // fallback switches must preserve the exhausted-model set while advancing.
17986
- if (role === "default" && (options?.cause ?? "user-selection") === "user-selection") {
18229
+ if (role === "default" && cause === "user-selection") {
17987
18230
  this.#fallbackTransitionGeneration++;
17988
18231
  this.#defaultFallbackController = undefined;
17989
18232
  this.#defaultFallbackExhaustedLastTurn = false;
@@ -18020,12 +18263,41 @@ export class AgentSession {
18020
18263
 
18021
18264
  setActiveModelProfile(name: string | undefined): void {
18022
18265
  this.#activeModelProfile = name;
18266
+ if (name !== undefined) {
18267
+ this.#unavailableModelProfile = undefined;
18268
+ }
18023
18269
  }
18024
18270
 
18025
18271
  getActiveModelProfile(): string | undefined {
18026
18272
  return this.#activeModelProfile;
18027
18273
  }
18028
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
+
18029
18301
  /**
18030
18302
  * Re-apply vendor-separated delegation after a profile activation changed the
18031
18303
  * role layer. `eagerTasks` is resolved at prompt build time, but under
@@ -18187,12 +18459,15 @@ export class AgentSession {
18187
18459
  * Session-scoped only: does not persist `modelProfile.default`.
18188
18460
  */
18189
18461
  async activateModelProfileForControl(profileName: string): Promise<boolean> {
18190
- await activateModelProfile({
18191
- session: this,
18192
- modelRegistry: this.#modelRegistry,
18193
- settings: this.settings,
18194
- profileName,
18195
- });
18462
+ this.markUserModelSelection();
18463
+ await this.withSdkControlMutation(() =>
18464
+ activateModelProfile({
18465
+ session: this,
18466
+ modelRegistry: this.#modelRegistry,
18467
+ settings: this.settings,
18468
+ profileName,
18469
+ }),
18470
+ );
18196
18471
  return this.getActiveModelProfile() === profileName;
18197
18472
  }
18198
18473
 
@@ -18227,6 +18502,7 @@ export class AgentSession {
18227
18502
  onAfterActivation?: () => void;
18228
18503
  },
18229
18504
  ): Promise<{ changed: boolean; id: string }> {
18505
+ this.markUserModelSelection();
18230
18506
  // Do not hold selection admission while waiting for a scheduled continuation:
18231
18507
  // the continuation may need prompt admission to settle the current turn.
18232
18508
  await this.waitForIdle();
@@ -18505,6 +18781,7 @@ export class AgentSession {
18505
18781
  reason?: TemporaryModelReason;
18506
18782
  providerSessionScope?: TemporaryProviderSessionScope;
18507
18783
  signal?: AbortSignal;
18784
+ shouldMutate?: () => boolean;
18508
18785
  onMutationStarted?: () => void;
18509
18786
  },
18510
18787
  // biome-ignore lint/suspicious/noConfusingVoidType: Existing session adapters return Promise<void>; a scope is optional.
@@ -18523,6 +18800,8 @@ export class AgentSession {
18523
18800
  throw new Error(`No API key for ${model.provider}/${model.id}`);
18524
18801
  }
18525
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();
18526
18805
  options?.onMutationStarted?.();
18527
18806
 
18528
18807
  const isTemporaryOperation = options?.cause === undefined || options.cause === "temporary-operation";
@@ -18549,6 +18828,9 @@ export class AgentSession {
18549
18828
  } else {
18550
18829
  this.#setModelAuthoritatively(model, options?.cause ?? "temporary-operation");
18551
18830
  }
18831
+ if (options?.cause === "user-selection") {
18832
+ this.#unavailableModelProfile = undefined;
18833
+ }
18552
18834
  this.sessionManager.appendModelChange(
18553
18835
  `${model.provider}/${model.id}`,
18554
18836
  options?.persistAsSessionDefault ? "default" : "temporary",
@@ -18556,12 +18838,36 @@ export class AgentSession {
18556
18838
  this.settings.getStorage()?.recordModelUsage(`${model.provider}/${model.id}`);
18557
18839
  if (options?.persistAsSessionDefault) {
18558
18840
  this.#seedSessionCanonicalVariant(model);
18841
+ if (options.cause === "user-selection") this.#recordUserCanonicalVariantSelection();
18559
18842
  }
18560
18843
 
18561
18844
  // Apply explicit thinking level if given; otherwise prefer the model's
18562
18845
  // configured defaultLevel; otherwise re-clamp the current level.
18563
18846
  this.setThinkingLevel(thinkingLevel ?? model.thinking?.defaultLevel ?? this.thinkingLevel);
18564
- 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
+ }
18565
18871
  await this.#syncEditToolModeAfterModelChange(previousEditMode);
18566
18872
  } catch (error) {
18567
18873
  if (ownsScope) await this.restoreTemporaryProviderSessionScope(scope);
@@ -18652,8 +18958,15 @@ export class AgentSession {
18652
18958
 
18653
18959
  #publishDefaultModelSelection(model: Model, thinkingLevel: ThinkingLevel, systemPrompt: string[] | undefined): void {
18654
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();
18655
18967
  this.#setModelWithProviderSessionReset(model);
18656
18968
  this.#seedSessionCanonicalVariant(model);
18969
+ this.#recordUserCanonicalVariantSelection();
18657
18970
  const thinkingLevelChanged = this.#thinkingLevel !== thinkingLevel;
18658
18971
  this.#thinkingLevelMutationRevision++;
18659
18972
  this.#thinkingLevelLiveMutationRevision++;
@@ -18663,6 +18976,12 @@ export class AgentSession {
18663
18976
  this.#pendingThinkingVisibilityControlFailure = undefined;
18664
18977
  this.#thinkingLevel = thinkingLevel;
18665
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
+ }
18666
18985
  if (thinkingLevelChanged) {
18667
18986
  const event: AgentSessionEvent = { type: "thinking_level_changed", thinkingLevel };
18668
18987
  for (const listener of this.#eventListenerSnapshot) {
@@ -18848,7 +19167,40 @@ export class AgentSession {
18848
19167
  });
18849
19168
  }
18850
19169
  }
18851
- 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
+ }
18852
19204
  options?.onAfterMutation?.();
18853
19205
  return { provider: model.provider, modelId: model.id, thinkingLevel: effectiveLevel };
18854
19206
  },
@@ -18954,6 +19306,7 @@ export class AgentSession {
18954
19306
  const next = roleModels[nextIndex];
18955
19307
 
18956
19308
  if (options?.temporary) {
19309
+ this.markUserModelSelection();
18957
19310
  await this.setModelTemporary(next.model, next.explicitThinkingLevel ? next.thinkingLevel : undefined, {
18958
19311
  cause: "temporary-operation",
18959
19312
  reason: "temporary-cycle",
@@ -20603,6 +20956,7 @@ export class AgentSession {
20603
20956
  onTerminalOverflowNoop?: () => void,
20604
20957
  resourceRunId?: string,
20605
20958
  ownershipSignal?: AbortSignal,
20959
+ sdkOwnership?: SdkContinuationOwnership,
20606
20960
  ): Promise<boolean> {
20607
20961
  // Safety stops are terminal and must not trigger context maintenance.
20608
20962
  if (
@@ -20657,6 +21011,7 @@ export class AgentSession {
20657
21011
  generation,
20658
21012
  suppressPredecessorAgentEnd: true,
20659
21013
  resourceRunId,
21014
+ sdkOwnership,
20660
21015
  });
20661
21016
 
20662
21017
  return true;
@@ -20675,10 +21030,11 @@ export class AgentSession {
20675
21030
  },
20676
21031
  resourceRunId,
20677
21032
  signal: ownershipSignal,
21033
+ sdkOwnership,
20678
21034
  });
20679
21035
  return "continuationScheduled" in status && status.continuationScheduled === true;
20680
21036
  }
20681
- return await this.#scheduleOverflowRetryContinuation(generation, resourceRunId);
21037
+ return await this.#scheduleOverflowRetryContinuation(generation, resourceRunId, sdkOwnership);
20682
21038
  }
20683
21039
  const compactionSettings = this.settings.getGroup("compaction");
20684
21040
  if (!compactionSettings.enabled || compactionSettings.strategy === "off") return false;
@@ -20724,7 +21080,11 @@ export class AgentSession {
20724
21080
  const promoted = await this.#tryContextPromotion(assistantMessage, ownershipSignal);
20725
21081
  if (ownershipSignal?.aborted) return false;
20726
21082
  if (!promoted) {
20727
- await this.#runAutoCompaction("threshold", false, false, { resourceRunId, signal: ownershipSignal });
21083
+ await this.#runAutoCompaction("threshold", false, false, {
21084
+ resourceRunId,
21085
+ signal: ownershipSignal,
21086
+ sdkOwnership,
21087
+ });
20728
21088
  }
20729
21089
  }
20730
21090
  return true;
@@ -21078,10 +21438,7 @@ export class AgentSession {
21078
21438
  const isContextPromoted = this.#temporaryProviderSessionScopes.some(
21079
21439
  scope => scope.token.reason === "context-promotion",
21080
21440
  );
21081
- if (
21082
- !isContextPromoted ||
21083
- !isDefaultAutoThresholdCeilingApplied(contextWindow, settings)
21084
- ) {
21441
+ if (!isContextPromoted || !isDefaultAutoThresholdCeilingApplied(contextWindow, settings)) {
21085
21442
  return settings;
21086
21443
  }
21087
21444
 
@@ -21490,7 +21847,9 @@ export class AgentSession {
21490
21847
  // The reminder continues the current prompt, so the predecessor `agent_end`
21491
21848
  // must stay held until the continuation turn produces the real terminal.
21492
21849
  // Publishing it here would settle the caller's prompt mid-reminder.
21493
- 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 });
21494
21853
  }
21495
21854
 
21496
21855
  /**
@@ -21604,6 +21963,11 @@ export class AgentSession {
21604
21963
  }
21605
21964
  }
21606
21965
 
21966
+ #recordUserCanonicalVariantSelection(): void {
21967
+ this.#userCanonicalVariantSelectionRevision++;
21968
+ this.#userCanonicalVariantSelection = this.#modelRegistry.getSessionCanonicalVariant?.(this.sessionId);
21969
+ }
21970
+
21607
21971
  #closeCodexProviderSessionsForHistoryRewrite(): void {
21608
21972
  const currentModel = this.model;
21609
21973
  if (currentModel?.api !== "openai-codex-responses") return;
@@ -22207,6 +22571,7 @@ export class AgentSession {
22207
22571
  signal?: AbortSignal;
22208
22572
  beforeTerminalOverflowNoop?: () => void;
22209
22573
  resourceRunId?: string;
22574
+ sdkOwnership?: SdkContinuationOwnership;
22210
22575
  },
22211
22576
  ): Promise<AutoCompactionTerminalStatus> {
22212
22577
  if (this.#isDisposed || this.#sessionAdmissionClosing) return Promise.resolve({ kind: "skipped" });
@@ -22227,6 +22592,7 @@ export class AgentSession {
22227
22592
  signal?: AbortSignal;
22228
22593
  beforeTerminalOverflowNoop?: () => void;
22229
22594
  resourceRunId?: string;
22595
+ sdkOwnership?: SdkContinuationOwnership;
22230
22596
  },
22231
22597
  ): Promise<AutoCompactionTerminalStatus> {
22232
22598
  const compactionSettings = this.settings.getGroup("compaction");
@@ -22369,7 +22735,14 @@ export class AgentSession {
22369
22735
  });
22370
22736
  if (autoCompactionSignal.aborted) return { kind: "aborted", source: "signal" };
22371
22737
  if (continueAfterMaintenance && reason !== "idle" && compactionSettings.autoContinue !== false) {
22372
- this.#scheduleAutoContinuePrompt(generation, true, options?.resourceRunId);
22738
+ this.#scheduleAutoContinuePrompt(
22739
+ generation,
22740
+ true,
22741
+ options?.resourceRunId,
22742
+ undefined,
22743
+ undefined,
22744
+ options?.sdkOwnership,
22745
+ );
22373
22746
  }
22374
22747
 
22375
22748
  if (autoCompactionSignal.aborted) return { kind: "aborted", source: "signal" };
@@ -22424,7 +22797,11 @@ export class AgentSession {
22424
22797
  }
22425
22798
  const overflowContinuationScheduled =
22426
22799
  !overflowNoopWouldReplay && willRetry && !continuationSkipReason
22427
- ? await this.#scheduleOverflowRetryContinuation(generation, options?.resourceRunId)
22800
+ ? await this.#scheduleOverflowRetryContinuation(
22801
+ generation,
22802
+ options?.resourceRunId,
22803
+ options?.sdkOwnership,
22804
+ )
22428
22805
  : false;
22429
22806
  await this.#emitSessionEvent({
22430
22807
  type: "auto_compaction_end",
@@ -22474,7 +22851,14 @@ export class AgentSession {
22474
22851
  resourceRunId: options?.resourceRunId,
22475
22852
  });
22476
22853
  } else if (continueAfterMaintenance && reason !== "idle" && compactionSettings.autoContinue !== false) {
22477
- this.#scheduleAutoContinuePrompt(generation, true, options?.resourceRunId);
22854
+ this.#scheduleAutoContinuePrompt(
22855
+ generation,
22856
+ true,
22857
+ options?.resourceRunId,
22858
+ undefined,
22859
+ undefined,
22860
+ options?.sdkOwnership,
22861
+ );
22478
22862
  }
22479
22863
  return { kind: "skipped" };
22480
22864
  }
@@ -22759,7 +23143,11 @@ export class AgentSession {
22759
23143
  }
22760
23144
  const overflowContinuationScheduled =
22761
23145
  willRetry && !continuationSkipReason
22762
- ? await this.#scheduleOverflowRetryContinuation(generation, options?.resourceRunId)
23146
+ ? await this.#scheduleOverflowRetryContinuation(
23147
+ generation,
23148
+ options?.resourceRunId,
23149
+ options?.sdkOwnership,
23150
+ )
22763
23151
  : false;
22764
23152
 
22765
23153
  await this.#emitSessionEvent({
@@ -22793,7 +23181,14 @@ export class AgentSession {
22793
23181
  resourceRunId: options?.resourceRunId,
22794
23182
  });
22795
23183
  } else if (continueAfterMaintenance && reason !== "idle" && compactionSettings.autoContinue !== false) {
22796
- this.#scheduleAutoContinuePrompt(generation, true, options?.resourceRunId);
23184
+ this.#scheduleAutoContinuePrompt(
23185
+ generation,
23186
+ true,
23187
+ options?.resourceRunId,
23188
+ undefined,
23189
+ undefined,
23190
+ options?.sdkOwnership,
23191
+ );
22797
23192
  }
22798
23193
  return { kind: "compacted" };
22799
23194
  } catch (error) {
@@ -22986,7 +23381,8 @@ export class AgentSession {
22986
23381
  isAuthenticated(apiKey) &&
22987
23382
  authStorage.getSessionCredentialRowId(model.provider, this.credentialSessionId) === credential.id &&
22988
23383
  authStorage.getSessionCredentialType(model.provider, this.credentialSessionId) === credentialKind &&
22989
- currentCredential && !currentCredential.disabled
23384
+ currentCredential &&
23385
+ !currentCredential.disabled
22990
23386
  )
22991
23387
  return apiKey;
22992
23388
  }
@@ -22994,6 +23390,7 @@ export class AgentSession {
22994
23390
  }
22995
23391
 
22996
23392
  #isRetryableError(message: AssistantMessage): boolean {
23393
+ if (isTerminalCodexProviderFailure(message)) return false;
22997
23394
  if (this.#isTerminalProviderFirstEventTimeout(message)) return false;
22998
23395
  if (message.errorMessage?.startsWith("Model fallback chain exhausted;")) return false;
22999
23396
  if (message.errorMessage?.startsWith("Managed fallback retried the escaped non-ASCII")) return false;
@@ -23004,7 +23401,11 @@ export class AgentSession {
23004
23401
  return false;
23005
23402
  const contextWindow = this.model?.contextWindow ?? 0;
23006
23403
  if (classifyContextOverflow(message, transportFailure, contextWindow)) return false;
23007
- if (isBareDefaultCodexOverload(message) && this.settings.get("retry.enabled") !== false) return true;
23404
+ if (
23405
+ (isBareDefaultCodexOverload(message) || isBareDefaultCodexProviderFailure(message)) &&
23406
+ this.settings.get("retry.enabled") !== false
23407
+ )
23408
+ return true;
23008
23409
  const managedFallback = this.#defaultFallbackChain().chain.entries.length > 1;
23009
23410
  // An account-specific model rejection that cannot rotate to another
23010
23411
  // credential stays terminal only on the session's own retry path; managed
@@ -23117,9 +23518,7 @@ export class AgentSession {
23117
23518
  // Errors that will never succeed on retry (auth/permission, malformed
23118
23519
  // request, unknown/unsupported model). These surface immediately rather
23119
23520
  // than retry forever.
23120
- return /unauthorized|forbidden|authentication_error|permission_error|permission denied|invalid api key|invalid_request_error|invalid request|bad request|bad_request|validation_error|unprocessable|payload too large|payment required|insufficient_quota|insufficient credits|missing required (parameter|field)|invalid schema|invalid tool_choice|unsupported (parameter|value|model)|model_not_found|no such model|unknown model|does not (exist|support)|request was aborted|request aborted|the user aborted/i.test(
23121
- errorMessage,
23122
- );
23521
+ return TERMINAL_ERROR_MESSAGE.test(errorMessage);
23123
23522
  }
23124
23523
 
23125
23524
  #extractExplicitHttpStatusFromErrorMessage(errorMessage: string): number | undefined {
@@ -23153,6 +23552,7 @@ export class AgentSession {
23153
23552
  if (message.errorKind === "local_snapshot_failure") return "local_snapshot";
23154
23553
  if (message.errorKind === "local_buffer_overflow") return "local_buffer_overflow";
23155
23554
  if (this.#isTypedFirstEventTimeout(message)) return "first_event_timeout";
23555
+ if (message.errorKind === "local_empty_response") return "empty_response";
23156
23556
  if (this.#isTypedEmptyResponse(message)) return "empty_response";
23157
23557
  if (this.#isCodexCredentialModelUnavailable(message)) {
23158
23558
  return this.#canRotateCodexCredential(message) ? "unknown" : "terminal";
@@ -23407,6 +23807,7 @@ export class AgentSession {
23407
23807
  }
23408
23808
 
23409
23809
  async #handleManagedAttemptOutcome(outcome: ManagedAttemptOutcome): Promise<ManagedAttemptDecision> {
23810
+ const sdkOwnership = this.#captureSdkContinuationOwnership(outcome.scope);
23410
23811
  const attemptAbortEpoch = this.#abortAdmissionEpoch;
23411
23812
  const attemptCancelled = () =>
23412
23813
  this.#isDisposed || this.#sessionAdmissionClosing || this.#abortAdmissionEpoch !== attemptAbortEpoch;
@@ -23456,6 +23857,8 @@ export class AgentSession {
23456
23857
  return {
23457
23858
  type: "retry",
23458
23859
  continuation: async ownership => {
23860
+ const continuationSdkOwnership =
23861
+ sdkOwnership ?? this.#captureSdkContinuationOwnership(ownership.handle.scope);
23459
23862
  const restoreOwnedTransition = async () => {
23460
23863
  if (this.#fallbackTransitionGeneration !== transitionGeneration) return;
23461
23864
  this.#fallbackTransitionGeneration++;
@@ -23542,7 +23945,12 @@ export class AgentSession {
23542
23945
  ...this.#managedFallbackPromptOptions(),
23543
23946
  transientRecoveryMessage: this.#escapedNonAsciiRecoveryMessage(),
23544
23947
  onRunAccepted: (handle: AttemptRunHandle) => {
23545
- this.#acceptSdkAttemptRun(handle, this.#activeSdkRunToken);
23948
+ this.#acceptSdkAttemptRun(
23949
+ handle,
23950
+ continuationSdkOwnership?.token,
23951
+ continuationSdkOwnership?.cohort,
23952
+ continuationSdkOwnership?.scope,
23953
+ );
23546
23954
  },
23547
23955
  });
23548
23956
  if (
@@ -23581,11 +23989,18 @@ export class AgentSession {
23581
23989
  type: "retry",
23582
23990
  continuation: async ownership => {
23583
23991
  if (attemptCancelled() || !ownership.isCurrent() || ownership.lease.signal.aborted) return;
23992
+ const continuationSdkOwnership =
23993
+ sdkOwnership ?? this.#captureSdkContinuationOwnership(ownership.handle.scope);
23584
23994
  await this.agent.continue({
23585
23995
  ...this.#managedFallbackPromptOptions(),
23586
23996
  transientRecoveryMessage: this.#escapedNonAsciiRecoveryMessage(),
23587
23997
  onRunAccepted: (handle: AttemptRunHandle) => {
23588
- this.#acceptSdkAttemptRun(handle, this.#activeSdkRunToken);
23998
+ this.#acceptSdkAttemptRun(
23999
+ handle,
24000
+ continuationSdkOwnership?.token,
24001
+ continuationSdkOwnership?.cohort,
24002
+ continuationSdkOwnership?.scope,
24003
+ );
23589
24004
  },
23590
24005
  });
23591
24006
  },
@@ -23600,6 +24015,10 @@ export class AgentSession {
23600
24015
  type: "maintenance",
23601
24016
  continuation: async ownership => {
23602
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);
23603
24022
  const resourceRunId = String(ownership.logicalRunId);
23604
24023
  const previousLease = this.#postPromptLeases.get(resourceRunId);
23605
24024
  this.#postPromptLeases.set(resourceRunId, ownership.lease);
@@ -23617,6 +24036,7 @@ export class AgentSession {
23617
24036
  },
23618
24037
  resourceRunId,
23619
24038
  ownership.lease.signal,
24039
+ continuationSdkOwnership,
23620
24040
  );
23621
24041
  if (
23622
24042
  terminalized ||
@@ -23651,12 +24071,14 @@ export class AgentSession {
23651
24071
  },
23652
24072
  };
23653
24073
  }
23654
- // Issue #5018 preserves managed behavior for the typed statusless
23655
- // Responses overload: before the code survived transport, this failure
23656
- // reached the session as an ordinary committed error, so the chain never
23657
- // discarded or advanced on it. Route it to the exhaustion decision
23658
- // directly, before the retryable path can classify its new facts.
23659
- 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
+ ) {
23660
24082
  this.#defaultFallbackChain().resetAttemptBudget();
23661
24083
  return this.#managedFallbackExhaustionDecision(
23662
24084
  outcome.failure.message,
@@ -23721,6 +24143,10 @@ export class AgentSession {
23721
24143
  if (classification === "transient" || classification === "first_event_timeout") {
23722
24144
  return { class: "server" };
23723
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" };
23724
24150
  if (classification === "unknown") return { class: "unknown" };
23725
24151
  return undefined;
23726
24152
  }
@@ -23900,9 +24326,7 @@ export class AgentSession {
23900
24326
  !authStorage.isCredentialAvailable(resolvedModel.provider, credential.id)
23901
24327
  )
23902
24328
  continue;
23903
- let credentialKeyResult:
23904
- | { kind: "resolved"; value: string | undefined }
23905
- | { kind: "aborted" };
24329
+ let credentialKeyResult: { kind: "resolved"; value: string | undefined } | { kind: "aborted" };
23906
24330
  try {
23907
24331
  credentialKeyResult = await awaitWithCancellation(
23908
24332
  this.#modelRegistry.getApiKey(resolvedModel, this.credentialSessionId, {
@@ -24000,7 +24424,11 @@ export class AgentSession {
24000
24424
  !transitionStillOwned()
24001
24425
  )
24002
24426
  return await rollbackCancelled();
24003
- if (selectedCredentialRowId !== undefined && selectedCredentialKind !== undefined && this.#restoreManagedFallbackGetApiKey) {
24427
+ if (
24428
+ selectedCredentialRowId !== undefined &&
24429
+ selectedCredentialKind !== undefined &&
24430
+ this.#restoreManagedFallbackGetApiKey
24431
+ ) {
24004
24432
  this.#managedFallbackNextCredentialOverride = {
24005
24433
  modelKey: canonicalModelKey,
24006
24434
  storageProvider: resolveOAuthStorageProvider(resolvedModel.provider),
@@ -24294,6 +24722,10 @@ export class AgentSession {
24294
24722
  scope?: AttemptScope,
24295
24723
  scopeWasClean = this.#isRetryScopeClean(scope),
24296
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);
24297
24729
  const retryAbortEpoch = this.#abortAdmissionEpoch;
24298
24730
  const retryCancelled = () =>
24299
24731
  this.#isDisposed || this.#sessionAdmissionClosing || this.#abortAdmissionEpoch !== retryAbortEpoch;
@@ -24308,9 +24740,25 @@ export class AgentSession {
24308
24740
  const classification = this.#classifyErrorForRetry(message);
24309
24741
  const localSnapshot = classification === "local_snapshot";
24310
24742
  const localBufferOverflow = classification === "local_buffer_overflow";
24743
+ // Managed outcomes can reach retry handling without the ordinary admission
24744
+ // check. Do not let retained transport facts bypass the same terminal veto.
24745
+ if (isTerminalCodexProviderFailure(message)) {
24746
+ return managedOutcome ? { type: "terminal", terminal: { stopReason: "error", messages: [message] } } : false;
24747
+ }
24311
24748
  if (retryCancelled()) {
24312
24749
  return managedOutcome ? { type: "terminal", terminal: { stopReason: "cancelled" } } : false;
24313
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
+ }
24314
24762
  // A local machinery failure must never stay charged against the provider
24315
24763
  // fallback budget, no matter which local exit follows (disabled retry,
24316
24764
  // visible-content surface, bounded retry, exhaustion, or the immediate
@@ -24369,6 +24817,8 @@ export class AgentSession {
24369
24817
  isBareDefaultCodexOverload(message) ||
24370
24818
  isBareDefaultAnthropicOverload(message) ||
24371
24819
  isBareDefaultOpenAIResponsesOverload(message);
24820
+ const canReplayBareDefaultCodexFailure = isBareDefaultCodexProviderFailure(message);
24821
+ const boundBareDefaultCodexFailure = canReplayBareDefaultCodexFailure && !legacyRetryConfigured;
24372
24822
  const canReplayCodexProviderOverload = isBareDefaultCodexOverload(message);
24373
24823
  const reportedRetryMaxAttempts = transportFailure?.retryMaxAttempts;
24374
24824
  if (reportedRetryMaxAttempts !== undefined) {
@@ -24412,22 +24862,9 @@ export class AgentSession {
24412
24862
  const fallbackTrigger = this.#fallbackTriggerFor(message, !managedFallback, transportFailure);
24413
24863
  const trigger:
24414
24864
  | { class: FallbackTriggerClass; retryAfterMs?: number; authDisposition?: AuthDisposition }
24415
- | undefined = canReplayCodexProviderOverload ? { class: "server" } : fallbackTrigger;
24416
- // OpenAI's typed statusless capacity-overload code (issue #5018) must not
24417
- // gain managed-chain retry/advance authority from its new facts. Before
24418
- // the code survived transport, this failure reached the session as an
24419
- // ordinary committed error and surfaced immediately, so mirror that
24420
- // behavior with the existing exhaustion decision. However, when called from
24421
- // the agent_end path (managedOutcome=false), we must return false to allow
24422
- // proper session termination handling.
24423
- if (managedFallback && isStatuslessTypedOverloadFacts(transportFailure)) {
24424
- return managedOutcome
24425
- ? this.#managedFallbackExhaustionDecision(
24426
- message,
24427
- message.errorMessage || "Model fallback attempt failed",
24428
- )
24429
- : false;
24430
- }
24865
+ | undefined = canReplayCodexProviderOverload || canReplayBareDefaultCodexFailure
24866
+ ? { class: "server" }
24867
+ : fallbackTrigger;
24431
24868
  if (!trigger) {
24432
24869
  return managedOutcome
24433
24870
  ? this.#managedFallbackExhaustionDecision(message, message.errorMessage || "Model fallback attempt failed")
@@ -24516,6 +24953,7 @@ export class AgentSession {
24516
24953
  if (!managedFallback && !legacyRetryConfigured && !canReplayRotatedCredential && !canReplayEmptyResponse) {
24517
24954
  if (
24518
24955
  (!canReplayProviderOverload &&
24956
+ !canReplayBareDefaultCodexFailure &&
24519
24957
  !canReplayUnexpectedSocketClose &&
24520
24958
  !this.#isTypedFirstEventTimeout(message) &&
24521
24959
  !messageOnlyWatchdogTimeout &&
@@ -24534,6 +24972,7 @@ export class AgentSession {
24534
24972
  !managedFallback &&
24535
24973
  classification === "transient" &&
24536
24974
  !canReplayProviderOverload &&
24975
+ !boundBareDefaultCodexFailure &&
24537
24976
  !canReplayUnexpectedSocketClose &&
24538
24977
  !this.#isIdleStreamStallErrorMessage(message.errorMessage ?? "");
24539
24978
 
@@ -24638,10 +25077,7 @@ export class AgentSession {
24638
25077
  if (outcome === "exhausted") {
24639
25078
  if (managedFallback) {
24640
25079
  let errorMessage = this.#fallbackExhaustionError(controller);
24641
- if (
24642
- !providerRetryCeilingReached &&
24643
- (trigger.class === "quota" || trigger.class === "rate_limit")
24644
- ) {
25080
+ if (!providerRetryCeilingReached && (trigger.class === "quota" || trigger.class === "rate_limit")) {
24645
25081
  if (!assistantMessageHasVisibleOrToolContent(message)) {
24646
25082
  const mark =
24647
25083
  quotaCredentialMark ??
@@ -24686,6 +25122,8 @@ export class AgentSession {
24686
25122
  : cappedExponentialWithFullJitter(retrySettings.baseDelayMs, retrySettings.maxDelayMs, attemptsUsed);
24687
25123
 
24688
25124
  const retry = async (ownership?: ManagedAttemptContinuationOwnership): Promise<void> => {
25125
+ const continuationSdkOwnership =
25126
+ retrySdkOwnership ?? this.#captureSdkContinuationOwnership(ownership?.handle.scope);
24689
25127
  const activePromptHandle = this.activePromptHandle;
24690
25128
  const cancellationSignal =
24691
25129
  ownership?.domain.signal ??
@@ -24900,7 +25338,12 @@ export class AgentSession {
24900
25338
  await this.agent.continue({
24901
25339
  ...this.#managedFallbackPromptOptions(),
24902
25340
  onRunAccepted: (handle: AttemptRunHandle) => {
24903
- this.#acceptSdkAttemptRun(handle, this.#activeSdkRunToken);
25341
+ this.#acceptSdkAttemptRun(
25342
+ handle,
25343
+ continuationSdkOwnership?.token,
25344
+ continuationSdkOwnership?.cohort,
25345
+ continuationSdkOwnership?.scope,
25346
+ );
24904
25347
  },
24905
25348
  });
24906
25349
  return;
@@ -24928,6 +25371,7 @@ export class AgentSession {
24928
25371
  allowDuringCancelAndSubmit: true,
24929
25372
  suppressPredecessorAgentEnd: resourceRunId !== undefined,
24930
25373
  resourceRunId,
25374
+ sdkOwnership: continuationSdkOwnership,
24931
25375
  onError: () => this.#failRetryRecovery("Retry continuation failed to start"),
24932
25376
  onSkip: () => this.#failRetryRecovery("Retry continuation was superseded"),
24933
25377
  });
@@ -25155,10 +25599,6 @@ export class AgentSession {
25155
25599
  // Bash Execution
25156
25600
  // =========================================================================
25157
25601
 
25158
- async #saveBashOriginalArtifact(originalText: string): Promise<BashArtifactSaveResult> {
25159
- return saveAgentBashOriginalArtifact(this.sessionManager, originalText);
25160
- }
25161
-
25162
25602
  /**
25163
25603
  * Execute a bash command.
25164
25604
  * Adds result to agent context and session.
@@ -25173,6 +25613,7 @@ export class AgentSession {
25173
25613
  onChunk?: (chunk: string) => void,
25174
25614
  options?: { excludeFromContext?: boolean; onPersisted?: () => void },
25175
25615
  ): Promise<BashResult> {
25616
+ const publishArtifact = this.sessionManager.captureArtifactPublication();
25176
25617
  const excludeFromContext = options?.excludeFromContext === true;
25177
25618
  this.#markRetryReplayUnsafe();
25178
25619
 
@@ -25210,7 +25651,7 @@ export class AgentSession {
25210
25651
  sessionId: this.sessionId,
25211
25652
  cwd,
25212
25653
  }),
25213
- onMinimizedSave: originalText => this.#saveBashOriginalArtifact(originalText),
25654
+ onMinimizedSave: originalText => saveAgentBashOriginalArtifact(publishArtifact, originalText),
25214
25655
  });
25215
25656
 
25216
25657
  this.recordBashResult(command, result, options);
@@ -25314,9 +25755,18 @@ export class AgentSession {
25314
25755
  this.#markRetryReplayUnsafe();
25315
25756
  const cwd = this.sessionManager.getCwd();
25316
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;
25317
25763
 
25318
25764
  const abortController = new AbortController();
25319
- 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();
25320
25770
  if (this.#extensionRunner?.hasHandlers("user_python")) {
25321
25771
  const hookResult = await this.#extensionRunner.emitUserPython({
25322
25772
  type: "user_python",
@@ -25324,6 +25774,9 @@ export class AgentSession {
25324
25774
  excludeFromContext,
25325
25775
  cwd,
25326
25776
  });
25777
+ if (abortController.signal.aborted) {
25778
+ throw abortController.signal.reason ?? new DOMException("Aborted", "AbortError");
25779
+ }
25327
25780
  this.assertEvalExecutionAllowed();
25328
25781
  if (hookResult?.result) {
25329
25782
  this.recordPythonResult(code, hookResult.result, options);
@@ -25331,15 +25784,12 @@ export class AgentSession {
25331
25784
  }
25332
25785
  }
25333
25786
 
25334
- // Use the same session ID as eval's Python backend for kernel sharing
25335
- const sessionFile = this.sessionManager.getSessionFile();
25336
- const sessionId = sessionFile ? `session:${sessionFile}:cwd:${cwd}` : `cwd:${cwd}`;
25337
25787
  const result = await executePythonCommand(code, {
25338
25788
  cwd,
25339
25789
  sessionId,
25340
- kernelOwnerId: this.#evalKernelOwnerId,
25341
- kernelMode: this.settings.get("python.kernelMode"),
25342
- settings: this.settings,
25790
+ kernelOwnerId,
25791
+ kernelMode,
25792
+ settings,
25343
25793
  onChunk,
25344
25794
  signal: abortController.signal,
25345
25795
  });
@@ -25348,7 +25798,7 @@ export class AgentSession {
25348
25798
  // must not reopen a closing session to append history.
25349
25799
  if (!this.#evalExecutionDisposing) this.recordPythonResult(code, result, options);
25350
25800
  return result;
25351
- })();
25801
+ });
25352
25802
  return await this.trackEvalExecution(execution, abortController);
25353
25803
  }
25354
25804