@gajae-code/coding-agent 0.18.7 → 0.18.9

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 (344) hide show
  1. package/CHANGELOG.md +377 -1
  2. package/dist/types/async/endpoint-id.d.ts +16 -0
  3. package/dist/types/async/job-manager.d.ts +1 -1
  4. package/dist/types/async/support.d.ts +0 -14
  5. package/dist/types/capability/mcp.d.ts +2 -0
  6. package/dist/types/capability/skill.d.ts +2 -0
  7. package/dist/types/capability/types.d.ts +2 -0
  8. package/dist/types/cli/public-command-errors.d.ts +3 -1
  9. package/dist/types/cli/setup-cli.d.ts +1 -1
  10. package/dist/types/commands/deep-interview.d.ts +3 -0
  11. package/dist/types/commands/mcp-serve.d.ts +4 -0
  12. package/dist/types/config/autorouting-tier-map.d.ts +348 -9
  13. package/dist/types/config/file-lock.d.ts +1 -0
  14. package/dist/types/config/model-bindings-applier.d.ts +2 -0
  15. package/dist/types/config/model-profile-activation.d.ts +11 -3
  16. package/dist/types/config/model-registry.d.ts +4 -9
  17. package/dist/types/config/skill-settings-defaults.d.ts +2 -0
  18. package/dist/types/cursor-delete-path.d.ts +1 -0
  19. package/dist/types/cursor-mcp-name.d.ts +8 -0
  20. package/dist/types/customization/inventory.d.ts +2 -0
  21. package/dist/types/debug/report-bundle-parent.d.ts +2 -0
  22. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.d.ts +1 -0
  23. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/version-manager.d.ts +31 -0
  24. package/dist/types/defaults/gjc-skills.generated.d.ts +5 -6
  25. package/dist/types/discovery/helpers.d.ts +6 -1
  26. package/dist/types/discovery/mcp-json.d.ts +1 -0
  27. package/dist/types/eval/py/kernel.d.ts +18 -0
  28. package/dist/types/export/html/css-color.d.ts +2 -0
  29. package/dist/types/export/html/html-export-destination.d.ts +2 -0
  30. package/dist/types/export/html/template.generated.d.ts +1 -1
  31. package/dist/types/extensibility/extensions/function-hooks-internal.d.ts +5 -0
  32. package/dist/types/extensibility/extensions/types.d.ts +7 -0
  33. package/dist/types/extensibility/gjc-plugins/runtime-adapters.d.ts +14 -1
  34. package/dist/types/extensibility/plugins/marketplace/cache.d.ts +2 -0
  35. package/dist/types/extensibility/plugins/plugin-link-path.d.ts +2 -0
  36. package/dist/types/extensibility/runtime-skill-discovery.d.ts +3 -1
  37. package/dist/types/extensibility/skill-management.d.ts +4 -1
  38. package/dist/types/extensibility/skills.d.ts +2 -0
  39. package/dist/types/gjc-runtime/deep-interview-crystallize.d.ts +74 -0
  40. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +11 -0
  41. package/dist/types/gjc-runtime/gc-runtime.d.ts +5 -0
  42. package/dist/types/gjc-runtime/psmux-detect.d.ts +3 -3
  43. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +0 -5
  44. package/dist/types/gjc-runtime/sidecar-bootstrap-env.d.ts +6 -0
  45. package/dist/types/gjc-runtime/state-argv.d.ts +1 -1
  46. package/dist/types/gjc-runtime/state-migrations.d.ts +8 -0
  47. package/dist/types/gjc-runtime/state-runtime.d.ts +95 -0
  48. package/dist/types/gjc-runtime/state-writer.d.ts +14 -1
  49. package/dist/types/gjc-runtime/tmux-common.d.ts +8 -5
  50. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +8 -0
  51. package/dist/types/internal-urls/skill-path.d.ts +1 -0
  52. package/dist/types/lsp/config.d.ts +8 -5
  53. package/dist/types/lsp/workspace-path.d.ts +1 -0
  54. package/dist/types/modes/acp/acp-agent.d.ts +7 -0
  55. package/dist/types/modes/components/custom-editor.d.ts +6 -0
  56. package/dist/types/modes/components/customization/customization-dashboard.d.ts +1 -0
  57. package/dist/types/modes/components/extensions/state-manager.d.ts +4 -3
  58. package/dist/types/modes/controllers/input-controller.d.ts +9 -0
  59. package/dist/types/modes/interactive-mode.d.ts +1 -0
  60. package/dist/types/modes/utils/interrupt-ownership.d.ts +3 -0
  61. package/dist/types/modes/utils/keybinding-matchers.d.ts +1 -0
  62. package/dist/types/modes/utils/macos-option-forwarding.d.ts +17 -0
  63. package/dist/types/progress/collect-project-progress.d.ts +38 -0
  64. package/dist/types/progress/progress-contract.d.ts +154 -0
  65. package/dist/types/progress/progress-snapshot.d.ts +11 -0
  66. package/dist/types/progress/project-progress.d.ts +141 -0
  67. package/dist/types/progress/render-progress.d.ts +28 -0
  68. package/dist/types/runtime-mcp/manager.d.ts +2 -1
  69. package/dist/types/runtime-mcp/mcp-redirect.d.ts +9 -0
  70. package/dist/types/runtime-mcp/oauth-public-url.d.ts +3 -0
  71. package/dist/types/runtime-mcp/plugin-network-boundary.d.ts +1 -0
  72. package/dist/types/runtime-mcp/project-host-values.d.ts +27 -0
  73. package/dist/types/runtime-mcp/smithery-registry.d.ts +24 -0
  74. package/dist/types/runtime-mcp/types.d.ts +6 -0
  75. package/dist/types/sdk/acp/adapter.d.ts +1 -1
  76. package/dist/types/sdk/broker/broker-exit.d.ts +6 -2
  77. package/dist/types/sdk/broker/broker.d.ts +24 -2
  78. package/dist/types/sdk/broker/doctor-restart.d.ts +5 -0
  79. package/dist/types/sdk/broker/ensure.d.ts +14 -1
  80. package/dist/types/sdk/broker/session-index.d.ts +12 -6
  81. package/dist/types/sdk/bus/index.d.ts +12 -1
  82. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +6 -1
  83. package/dist/types/sdk/client/client.d.ts +2 -0
  84. package/dist/types/sdk/host/query/handlers.d.ts +3 -0
  85. package/dist/types/sdk/index.d.ts +1 -0
  86. package/dist/types/sdk/router/session-router.d.ts +2 -0
  87. package/dist/types/sdk/session-list.d.ts +1 -1
  88. package/dist/types/sdk/session.d.ts +8 -0
  89. package/dist/types/session/agent-session.d.ts +38 -9
  90. package/dist/types/session/artifacts.d.ts +2 -2
  91. package/dist/types/session/internal/managed-gc-retirement-codec.d.ts +32 -0
  92. package/dist/types/session/internal/managed-session-scope.d.ts +79 -2
  93. package/dist/types/session/internal/managed-session-storage.d.ts +26 -1
  94. package/dist/types/session/internal/managed-task-owner-cleanup.d.ts +62 -0
  95. package/dist/types/session/internal/task-artifact-owner-access.d.ts +2 -0
  96. package/dist/types/session/internal/task-artifact-owner-transcript.d.ts +9 -1
  97. package/dist/types/session/session-manager.d.ts +31 -5
  98. package/dist/types/session/session-retirement.d.ts +31 -7
  99. package/dist/types/session/session-storage.d.ts +69 -2
  100. package/dist/types/session/task-artifact-owner-codec.d.ts +2 -0
  101. package/dist/types/setup/credential-auto-import.d.ts +1 -1
  102. package/dist/types/setup/credential-import.d.ts +6 -4
  103. package/dist/types/skill-state/active-state.d.ts +38 -0
  104. package/dist/types/skill-state/workflow-mutation-guard.d.ts +3 -0
  105. package/dist/types/slash-commands/helpers/progress-report.d.ts +8 -0
  106. package/dist/types/task/executor.d.ts +3 -0
  107. package/dist/types/task/isolation-dir.d.ts +26 -0
  108. package/dist/types/tools/ast-edit-write-path.d.ts +13 -0
  109. package/dist/types/tools/bash-skill-urls.d.ts +1 -1
  110. package/dist/types/tools/browser/cdp-url.d.ts +2 -0
  111. package/dist/types/tools/github-side-effect.d.ts +22 -0
  112. package/dist/types/tools/index.d.ts +3 -0
  113. package/dist/types/tools/monitor-mutation.d.ts +9 -0
  114. package/dist/types/tools/python.d.ts +9 -1
  115. package/dist/types/tools/recipe/recipe-guard.d.ts +7 -0
  116. package/dist/types/tools/restricted-cwd.d.ts +1 -0
  117. package/dist/types/tools/todo-write.d.ts +6 -0
  118. package/dist/types/utils/tools-checksum.d.ts +7 -0
  119. package/package.json +7 -7
  120. package/scripts/generate-sdk-operation-inventory.ts +21 -0
  121. package/scripts/verify-managed-empty-stop.ts +28 -0
  122. package/src/async/endpoint-id.ts +26 -0
  123. package/src/async/job-manager.ts +247 -132
  124. package/src/async/support.ts +0 -25
  125. package/src/capability/index.ts +15 -0
  126. package/src/capability/mcp.ts +2 -0
  127. package/src/capability/skill.ts +2 -0
  128. package/src/capability/types.ts +2 -0
  129. package/src/cli/auth-broker-cli.ts +1 -1
  130. package/src/cli/customize-doctor.ts +15 -2
  131. package/src/cli/doctor/repairs.ts +4 -1
  132. package/src/cli/doctor-supervisor.ts +3 -0
  133. package/src/cli/public-command-errors.ts +21 -1
  134. package/src/cli/public-command-registry.ts +28 -6
  135. package/src/cli/setup-cli.ts +4 -4
  136. package/src/cli/skills-cli.ts +5 -0
  137. package/src/commands/deep-interview.ts +47 -8
  138. package/src/commands/mcp-serve.ts +7 -0
  139. package/src/commands/sdk.ts +455 -397
  140. package/src/config/autorouting-tier-map.ts +117 -7
  141. package/src/config/file-lock.ts +316 -79
  142. package/src/config/mcp-schema.json +4 -0
  143. package/src/config/model-bindings-applier.ts +20 -0
  144. package/src/config/model-profile-activation.ts +240 -46
  145. package/src/config/model-registry.ts +47 -153
  146. package/src/config/skill-settings-defaults.ts +7 -0
  147. package/src/coordinator-mcp/policy.ts +3 -2
  148. package/src/coordinator-mcp/server.ts +2 -0
  149. package/src/cursor-delete-path.ts +10 -0
  150. package/src/cursor-mcp-name.ts +15 -0
  151. package/src/cursor.ts +13 -2
  152. package/src/customization/inventory.ts +4 -13
  153. package/src/dap/config.ts +2 -2
  154. package/src/debug/report-bundle-parent.ts +22 -0
  155. package/src/debug/report-bundle.ts +3 -0
  156. package/src/debug/system-info.ts +13 -1
  157. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.ts +10 -6
  158. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.ts +67 -4
  159. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/version-manager.ts +80 -0
  160. package/src/defaults/gjc/skills/deep-interview/SKILL.md +105 -10
  161. package/src/defaults/gjc/skills/ralplan/SKILL.md +6 -4
  162. package/src/defaults/gjc-defaults.test.ts +18 -1
  163. package/src/defaults/gjc-defaults.ts +3 -0
  164. package/src/defaults/gjc-skills.generated.ts +58 -36
  165. package/src/discovery/agents.ts +6 -1
  166. package/src/discovery/builtin.ts +13 -1
  167. package/src/discovery/helpers.ts +171 -29
  168. package/src/discovery/mcp-json.ts +26 -12
  169. package/src/discovery/ssh.ts +1 -1
  170. package/src/discovery/substitute-plugin-root.ts +3 -1
  171. package/src/edit/file-read-cache.ts +1 -1
  172. package/src/eval/py/executor.ts +796 -154
  173. package/src/eval/py/kernel.ts +79 -13
  174. package/src/eval/py/tool-bridge.ts +4 -0
  175. package/src/export/html/css-color.ts +9 -0
  176. package/src/export/html/html-export-destination.ts +16 -0
  177. package/src/export/html/index.ts +7 -10
  178. package/src/export/html/template.generated.ts +1 -1
  179. package/src/extensibility/extensions/function-hooks-internal.ts +27 -0
  180. package/src/extensibility/extensions/runner.ts +15 -1
  181. package/src/extensibility/extensions/types.ts +7 -0
  182. package/src/extensibility/gjc-plugins/mcp-policy.ts +2 -1
  183. package/src/extensibility/gjc-plugins/runtime-adapters.ts +85 -8
  184. package/src/extensibility/plugins/manager.ts +3 -2
  185. package/src/extensibility/plugins/marketplace/cache.ts +15 -0
  186. package/src/extensibility/plugins/marketplace/manager.ts +11 -3
  187. package/src/extensibility/plugins/plugin-link-path.ts +12 -0
  188. package/src/extensibility/runtime-skill-discovery.ts +20 -2
  189. package/src/extensibility/skill-management.ts +9 -2
  190. package/src/extensibility/skills.ts +6 -1
  191. package/src/gjc-runtime/deep-interview-crystallize.ts +1814 -0
  192. package/src/gjc-runtime/deep-interview-recorder.ts +20 -2
  193. package/src/gjc-runtime/deep-interview-runtime.ts +1415 -13
  194. package/src/gjc-runtime/deep-interview-stage.ts +98 -21
  195. package/src/gjc-runtime/gc-runtime.ts +206 -9
  196. package/src/gjc-runtime/launch-worktree.ts +70 -9
  197. package/src/gjc-runtime/psmux-detect.ts +28 -4
  198. package/src/gjc-runtime/ralplan-runtime.ts +447 -52
  199. package/src/gjc-runtime/session-state-sidecar.ts +3 -2
  200. package/src/gjc-runtime/sidecar-bootstrap-env.ts +19 -0
  201. package/src/gjc-runtime/state-argv.ts +2 -0
  202. package/src/gjc-runtime/state-migrations.ts +91 -29
  203. package/src/gjc-runtime/state-runtime.ts +3716 -135
  204. package/src/gjc-runtime/state-writer.ts +105 -16
  205. package/src/gjc-runtime/tmux-common.ts +8 -5
  206. package/src/gjc-runtime/ultragoal-runtime.ts +16 -4
  207. package/src/gjc-runtime/workflow-manifest.generated.json +70 -42
  208. package/src/gjc-runtime/workflow-manifest.ts +54 -21
  209. package/src/harness-control-plane/operate.ts +12 -4
  210. package/src/harness-control-plane/owner.ts +5 -1
  211. package/src/hashline/parser.ts +48 -9
  212. package/src/hashline/recovery.ts +13 -4
  213. package/src/internal-urls/docs-index.generated.ts +20 -19
  214. package/src/internal-urls/skill-path.ts +19 -0
  215. package/src/internal-urls/skill-protocol.ts +2 -0
  216. package/src/lsp/config.ts +12 -63
  217. package/src/lsp/edits.ts +6 -0
  218. package/src/lsp/workspace-path.ts +51 -0
  219. package/src/main.ts +195 -31
  220. package/src/memories/index.ts +88 -7
  221. package/src/modes/acp/acp-agent.ts +88 -4
  222. package/src/modes/components/custom-editor.ts +14 -0
  223. package/src/modes/components/customization/customization-dashboard.ts +12 -0
  224. package/src/modes/components/extensions/extension-dashboard.ts +2 -2
  225. package/src/modes/components/extensions/state-manager.ts +15 -5
  226. package/src/modes/components/hook-editor.ts +1 -1
  227. package/src/modes/components/hook-selector.ts +1 -1
  228. package/src/modes/components/model-selector.ts +1 -4
  229. package/src/modes/components/provider-onboarding-selector.ts +1 -1
  230. package/src/modes/components/status-line/segments.ts +4 -0
  231. package/src/modes/controllers/event-controller.ts +11 -4
  232. package/src/modes/controllers/input-controller.ts +76 -31
  233. package/src/modes/controllers/runtime-mcp-command-controller.ts +29 -16
  234. package/src/modes/controllers/selector-controller.ts +24 -15
  235. package/src/modes/controllers/todo-command-controller.ts +1 -1
  236. package/src/modes/interactive-mode.ts +5 -1
  237. package/src/modes/runtime-init.ts +4 -0
  238. package/src/modes/utils/abort-message.ts +3 -1
  239. package/src/modes/utils/interrupt-ownership.ts +12 -0
  240. package/src/modes/utils/keybinding-matchers.ts +4 -0
  241. package/src/modes/utils/macos-option-forwarding.ts +80 -0
  242. package/src/progress/collect-project-progress.ts +153 -0
  243. package/src/progress/progress-contract.ts +172 -0
  244. package/src/progress/progress-snapshot.ts +140 -0
  245. package/src/progress/project-progress.ts +446 -0
  246. package/src/progress/render-progress.ts +209 -0
  247. package/src/prompts/system/master-mode.md +1 -0
  248. package/src/prompts/tools/skill-discovery.md +1 -1
  249. package/src/runtime-mcp/client.ts +4 -2
  250. package/src/runtime-mcp/config-writer.ts +10 -6
  251. package/src/runtime-mcp/config.ts +1 -0
  252. package/src/runtime-mcp/json-rpc.ts +2 -1
  253. package/src/runtime-mcp/manager.ts +29 -11
  254. package/src/runtime-mcp/mcp-redirect.ts +73 -0
  255. package/src/runtime-mcp/oauth-flow.ts +14 -2
  256. package/src/runtime-mcp/oauth-public-url.ts +14 -0
  257. package/src/runtime-mcp/plugin-network-boundary.ts +4 -0
  258. package/src/runtime-mcp/pool.ts +6 -0
  259. package/src/runtime-mcp/project-host-values.ts +46 -0
  260. package/src/runtime-mcp/smithery-registry.ts +22 -3
  261. package/src/runtime-mcp/transports/http.ts +4 -3
  262. package/src/runtime-mcp/types.ts +6 -0
  263. package/src/sdk/acp/adapter.ts +95 -8
  264. package/src/sdk/broker/broker-exit.ts +52 -23
  265. package/src/sdk/broker/broker.ts +81 -6
  266. package/src/sdk/broker/doctor-restart.ts +4 -0
  267. package/src/sdk/broker/ensure.ts +31 -2
  268. package/src/sdk/broker/lifecycle.ts +711 -66
  269. package/src/sdk/broker/session-index.ts +191 -81
  270. package/src/sdk/bus/chat-daemon-cli.ts +38 -1
  271. package/src/sdk/bus/chat-daemon-control.ts +2 -2
  272. package/src/sdk/bus/index.ts +238 -43
  273. package/src/sdk/bus/telegram-daemon-contract.ts +6 -1
  274. package/src/sdk/cli/session-cli.ts +17 -3
  275. package/src/sdk/client/client.ts +28 -8
  276. package/src/sdk/host/query/handlers.ts +9 -1
  277. package/src/sdk/host/session-runtime.test.ts +2705 -303
  278. package/src/sdk/host/session-runtime.ts +490 -143
  279. package/src/sdk/host/surface-policy.ts +1 -0
  280. package/src/sdk/index.ts +8 -0
  281. package/src/sdk/lifecycle-session.ts +3 -1
  282. package/src/sdk/prompt-deadline-manager.ts +15 -0
  283. package/src/sdk/protocol/operation-inventory.generated.json +146 -0
  284. package/src/sdk/protocol/operation-registry.ts +9 -2
  285. package/src/sdk/router/session-router.ts +82 -18
  286. package/src/sdk/session-list.ts +1 -0
  287. package/src/sdk/session.ts +69 -45
  288. package/src/session/agent-session.ts +679 -238
  289. package/src/session/artifacts.ts +176 -71
  290. package/src/session/internal/managed-gc-retirement-codec.ts +319 -0
  291. package/src/session/internal/managed-session-scope.ts +3779 -462
  292. package/src/session/internal/managed-session-storage.ts +501 -78
  293. package/src/session/internal/managed-task-owner-cleanup.ts +473 -0
  294. package/src/session/internal/task-artifact-owner-access.ts +19 -3
  295. package/src/session/internal/task-artifact-owner-transcript.ts +547 -1
  296. package/src/session/session-manager.ts +951 -133
  297. package/src/session/session-retirement.ts +573 -14
  298. package/src/session/session-storage.ts +447 -15
  299. package/src/session/task-artifact-owner-codec.ts +71 -8
  300. package/src/session/task-artifact-owner-retirement.ts +21 -19
  301. package/src/setup/credential-auto-import.ts +12 -3
  302. package/src/setup/credential-import.ts +176 -5
  303. package/src/setup/paseo/announce.ts +6 -1
  304. package/src/skill-state/active-state.ts +145 -2
  305. package/src/skill-state/workflow-mutation-guard.ts +102 -10
  306. package/src/slash-commands/builtin-registry.ts +39 -9
  307. package/src/slash-commands/helpers/mcp.ts +8 -4
  308. package/src/slash-commands/helpers/progress-report.ts +23 -0
  309. package/src/stt/setup.ts +3 -5
  310. package/src/task/commands.ts +3 -1
  311. package/src/task/executor.ts +4 -0
  312. package/src/task/index.ts +6 -0
  313. package/src/task/isolation-dir.ts +143 -0
  314. package/src/task/worktree.ts +16 -7
  315. package/src/tools/ask.ts +205 -6
  316. package/src/tools/ast-edit-write-path.ts +68 -0
  317. package/src/tools/ast-edit.ts +10 -1
  318. package/src/tools/bash-allowed-prefixes.ts +15 -0
  319. package/src/tools/bash-skill-urls.ts +56 -3
  320. package/src/tools/bash.ts +48 -79
  321. package/src/tools/browser/cdp-url.ts +18 -0
  322. package/src/tools/browser.ts +8 -1
  323. package/src/tools/descriptors.ts +5 -1
  324. package/src/tools/eval.ts +56 -36
  325. package/src/tools/gh.ts +10 -0
  326. package/src/tools/github-side-effect.ts +112 -0
  327. package/src/tools/image-gen.ts +1 -1
  328. package/src/tools/index.ts +3 -0
  329. package/src/tools/monitor-mutation.ts +27 -0
  330. package/src/tools/monitor.ts +7 -0
  331. package/src/tools/python.ts +188 -66
  332. package/src/tools/recipe/index.ts +9 -0
  333. package/src/tools/recipe/recipe-guard.ts +19 -0
  334. package/src/tools/restricted-cwd.ts +10 -0
  335. package/src/tools/skill-discovery.ts +5 -0
  336. package/src/tools/skill.ts +28 -1
  337. package/src/tools/todo-write.ts +10 -1
  338. package/src/tools/tool-catalog.generated.ts +1 -1
  339. package/src/tools/write.ts +1 -0
  340. package/src/utils/external-editor.ts +3 -2
  341. package/src/utils/tools-checksum.ts +34 -0
  342. package/src/utils/tools-manager.ts +24 -6
  343. package/src/web/insane/url-guard.ts +2 -1
  344. package/src/web/search/providers/codex.ts +3 -2
@@ -30,6 +30,7 @@ import {
30
30
  type AgentLoopConfig,
31
31
  type AgentMessage,
32
32
  type AgentState,
33
+ type AgentTerminalOwnerContext,
33
34
  type AgentTool,
34
35
  assertImagePlaceholdersHavePayload,
35
36
  type ContextMaintenanceResult,
@@ -134,8 +135,8 @@ import {
134
135
  classifyFallbackTrigger,
135
136
  EMPTY_RESPONSE_PROVIDER_CODE,
136
137
  type FallbackAttemptToken,
137
- PROVIDER_PROTOCOL_MISMATCH_ERROR_CODE,
138
138
  type FallbackTriggerClass,
139
+ PROVIDER_PROTOCOL_MISMATCH_ERROR_CODE,
139
140
  SERVER_OVERLOADED_PROVIDER_CODE,
140
141
  STREAM_FIRST_EVENT_TIMEOUT_PROVIDER_CODE,
141
142
  } from "@gajae-code/ai/utils/fallback-transport";
@@ -214,11 +215,12 @@ import {
214
215
  type AsyncJob,
215
216
  type AsyncJobDeliveryState,
216
217
  AsyncJobManager,
217
- asyncJobEndpointId as deriveAsyncJobEndpointId,
218
218
  type FoldReason,
219
219
  type JobFoldEvent,
220
220
  type OwnerSubagentShutdownLease,
221
+ type SubagentLifecycle,
221
222
  } from "../async";
223
+ import { asyncJobEndpointId as deriveAsyncJobEndpointId } from "../async/endpoint-id";
222
224
  import { reset as resetCapabilities } from "../capability";
223
225
  import type { Rule } from "../capability/rule";
224
226
  import type { CasReceipt } from "../config/atomic-yaml-patch";
@@ -234,6 +236,12 @@ import {
234
236
  UnknownModelProfileError,
235
237
  validateModelProfileName,
236
238
  } from "../config/model-profile-contract";
239
+ import {
240
+ commitDurableModelProfileOwnership,
241
+ InvalidModelProfileOwnershipError,
242
+ ModelProfileOwnershipConflictError,
243
+ readDurableModelProfileOwnership,
244
+ } from "../config/model-profile-ownership";
237
245
  import { resolveProfileBindings } from "../config/model-profiles";
238
246
  import {
239
247
  GJC_MODEL_ASSIGNMENT_TARGETS,
@@ -319,6 +327,7 @@ import {
319
327
  buildSkillPromptMessage,
320
328
  getSkillSlashCommandName,
321
329
  isNamespacedSkillSlashCommandName,
330
+ type ParsedSkillInvocation,
322
331
  parseSkillInvocations,
323
332
  type Skill,
324
333
  type SkillWarning,
@@ -408,18 +417,14 @@ import type { NotificationSessionController } from "../sdk/bus/session-control";
408
417
  import { buildSyntheticModelId, syntheticNamespaceCollision } from "../sdk/model-profile-model";
409
418
  import { sanitizePromptFailure } from "../sdk/prompt-failure";
410
419
  import type { SecretObfuscator } from "../secrets/obfuscator";
411
- import {
412
- formatNoCredentialOnboardingError,
413
- formatNoModelOnboardingError,
414
- NoModelSelectedError,
415
- } from "../setup/model-onboarding-guidance";
420
+ import { formatNoCredentialOnboardingError, NoModelSelectedError } from "../setup/model-onboarding-guidance";
416
421
  import {
417
422
  isCanonicalGjcWorkflowSkill,
418
423
  isWorkflowContinuationInert,
419
424
  readVisibleSkillActiveState,
420
425
  syncSkillActiveState,
421
426
  } from "../skill-state/active-state";
422
- import { assertWorkflowMutationAllowed } from "../skill-state/workflow-mutation-guard";
427
+ import { assertWorkflowMutationAllowed, isWorkflowMutationTool } from "../skill-state/workflow-mutation-guard";
423
428
  import { invalidateHostMetadata } from "../ssh/connection-manager";
424
429
  import { buildVolatileProjectContext } from "../system-prompt";
425
430
  import { DelegationHintController } from "../task/delegation-hint";
@@ -444,6 +449,7 @@ import {
444
449
  import { assertEditableFile } from "../tools/auto-generated-guard";
445
450
  import { releaseTabsForOwner } from "../tools/browser/tab-supervisor";
446
451
  import type { CheckpointState } from "../tools/checkpoint";
452
+ import { githubPermissionIntent } from "../tools/github-side-effect";
447
453
  import { outputMeta, wrapToolWithMetaNotice } from "../tools/output-meta";
448
454
  import { normalizeLocalScheme, resolveReadPath, resolveToCwd } from "../tools/path-utils";
449
455
  import { registerResourceGcSession } from "../tools/resource-gc";
@@ -527,6 +533,7 @@ import type {
527
533
  SessionEntry,
528
534
  SessionManagerCloseOutcome,
529
535
  SessionMemoryStats,
536
+ SessionArtifactPublication,
530
537
  } from "./session-manager";
531
538
  import {
532
539
  createReadonlySessionManager,
@@ -913,6 +920,28 @@ export interface QueuedInputSubmission {
913
920
 
914
921
  /** Listener function for agent session events */
915
922
  export type AgentSessionEventListener = (event: AgentSessionEvent) => void;
923
+
924
+ /**
925
+ * Deliver one event to one listener, containing both synchronous throws and
926
+ * async rejections. Listeners are `void`-typed but interactive mode subscribes
927
+ * with an `async` one whose handlers can persist (plan approval calls
928
+ * `session.prompt()`); a rejection there, e.g. the managed append fence after
929
+ * another process took over the transcript, must not escape as an unhandled
930
+ * rejection and terminate the process.
931
+ */
932
+ function deliverSessionEvent(
933
+ listener: AgentSessionEventListener,
934
+ event: AgentSessionEvent,
935
+ onError: (error: unknown) => void,
936
+ ): void {
937
+ try {
938
+ const result: unknown = listener(event);
939
+ if (result instanceof Promise) result.catch(onError);
940
+ } catch (error) {
941
+ onError(error);
942
+ }
943
+ }
944
+
916
945
  export type AsyncJobSnapshotItem = Pick<
917
946
  AsyncJob,
918
947
  "id" | "type" | "status" | "label" | "startTime" | "endTime" | "metadata"
@@ -1218,6 +1247,11 @@ type SendUserMessageDispatchOptions = SendUserMessageOptions & {
1218
1247
  };
1219
1248
 
1220
1249
  type InternalPromptOptions = PromptOptions & { sdkRunToken?: string };
1250
+ interface SdkContinuationOwnership {
1251
+ scope: AttemptScope;
1252
+ token: string;
1253
+ cohort: string[];
1254
+ }
1221
1255
  type InternalCustomMessageOptions = Pick<
1222
1256
  PromptOptions,
1223
1257
  | "streamingBehavior"
@@ -1427,8 +1461,7 @@ const PROVIDER_FIRST_EVENT_TIMEOUT_WITHOUT_ARTICLE_ERROR = "Provider stream time
1427
1461
  const BARE_DEFAULT_CODEX_RETRYABLE_ERROR =
1428
1462
  /^Codex error event(?:: .*)? \(code=(?:server_is_overloaded|server_error|internal_error)(?:, [^)]+)*\)$/;
1429
1463
  const BARE_DEFAULT_CODEX_RETRYABLE_CODES = new Set(["server_is_overloaded", "server_error", "internal_error"]);
1430
- const BARE_DEFAULT_CODEX_OVERLOAD_ERROR =
1431
- /^Codex error event(?:: .*)? \(code=server_is_overloaded(?:, [^)]+)*\)$/;
1464
+ const BARE_DEFAULT_CODEX_OVERLOAD_ERROR = /^Codex error event(?:: .*)? \(code=server_is_overloaded(?:, [^)]+)*\)$/;
1432
1465
  const BARE_DEFAULT_OVERLOAD_CODES = new Set([SERVER_OVERLOADED_PROVIDER_CODE]);
1433
1466
  /** Anthropic's typed capacity-overload `error.type`, the only Anthropic overload code admitted below. */
1434
1467
  const ANTHROPIC_OVERLOADED_ERROR_TYPE = "overloaded_error";
@@ -1541,14 +1574,7 @@ function isExactTypedOverloadFacts(
1541
1574
  facts.retryMaxAttempts === undefined
1542
1575
  );
1543
1576
  }
1544
- /**
1545
- * True when transport facts are exactly the provider's statusless typed
1546
- * capacity-overload code (the provider code verbatim and, when present, the
1547
- * OpenAI code verbatim, with no status). The agent loop must not treat the new
1548
- * facts as managed transaction authority, and managed session fallback must
1549
- * not gain retry/advance authority from them, so this predicate keeps both
1550
- * guards reading only typed facts, never error prose.
1551
- */
1577
+
1552
1578
  function isStatuslessTypedOverloadFacts(facts: TransportFailureFacts | undefined): boolean {
1553
1579
  if (!facts) return false;
1554
1580
  return (
@@ -1557,7 +1583,6 @@ function isStatuslessTypedOverloadFacts(facts: TransportFailureFacts | undefined
1557
1583
  (facts.openaiErrorCode === undefined || facts.openaiErrorCode === SERVER_OVERLOADED_PROVIDER_CODE)
1558
1584
  );
1559
1585
  }
1560
-
1561
1586
  // Deterministic auth/request/model diagnostics must surface even when a provider
1562
1587
  // labels the failure with a transient code.
1563
1588
  const TERMINAL_ERROR_MESSAGE =
@@ -1798,20 +1823,14 @@ function summarizeAgentBashArtifactSave(
1798
1823
  };
1799
1824
  }
1800
1825
 
1801
- export interface AgentBashArtifactStore {
1802
- saveArtifact(content: string, toolType: string): Promise<string | undefined>;
1803
- getArtifactPath(id: string): Promise<string | null>;
1804
- }
1805
-
1806
1826
  export async function saveAgentBashOriginalArtifact(
1807
- store: AgentBashArtifactStore,
1827
+ publication: SessionArtifactPublication,
1808
1828
  originalText: string,
1809
1829
  ): Promise<BashArtifactSaveResult> {
1810
1830
  try {
1811
- const artifactId = await store.saveArtifact(originalText, "bash-original");
1812
- if (!artifactId) return { status: "failed", diagnostic: "storage returned no artifact id" };
1813
- const artifactPath = await store.getArtifactPath(artifactId);
1814
- return artifactPath ? summarizeAgentBashArtifactSave(artifactId, originalText) : { status: "unavailable" };
1831
+ const artifactId = await publication(originalText, "bash-original");
1832
+ if (!artifactId) return { status: "unavailable" };
1833
+ return summarizeAgentBashArtifactSave(artifactId, originalText);
1815
1834
  } catch (error) {
1816
1835
  return { status: "failed", diagnostic: boundAgentBashArtifactSaveDiagnostic(error) };
1817
1836
  }
@@ -1991,7 +2010,7 @@ async function discardPreparedNewSessionAfterFailure(
1991
2010
  // ============================================================================
1992
2011
 
1993
2012
  /** Tools that require user permission before execution when an ACP client is connected. */
1994
- const PERMISSION_REQUIRED_TOOLS = new Set(["bash", "monitor", "eval", "edit", "delete", "move"]);
2013
+ const PERMISSION_REQUIRED_TOOLS = new Set(["bash", "monitor", "eval", "edit", "delete", "move", "github"]);
1995
2014
 
1996
2015
  function isShellExecutionPermissionTool(toolName: string): boolean {
1997
2016
  return toolName === "bash" || toolName === "monitor";
@@ -2115,6 +2134,7 @@ function getPermissionIntent(
2115
2134
  cacheKey: "edit:move",
2116
2135
  };
2117
2136
  }
2137
+ if (toolName === "github") return githubPermissionIntent(args);
2118
2138
  return undefined;
2119
2139
  }
2120
2140
 
@@ -2764,6 +2784,7 @@ export class AgentSession {
2764
2784
  #scopedModels: ScopedModelSelection[];
2765
2785
  #thinkingLevel: ThinkingLevel | undefined;
2766
2786
  #activeModelProfile: string | undefined;
2787
+ #unavailableModelProfile: string | undefined;
2767
2788
  #activeProfileInstalledRoles = new Map<string, ModelSelectorValue | undefined>();
2768
2789
  #activeProfileInstalledAgentOverrides = new Map<string, ModelSelectorValue | undefined>();
2769
2790
  #preProfileModel: Model | undefined;
@@ -2789,6 +2810,9 @@ export class AgentSession {
2789
2810
  #followUpReservationTransitionWaiters = new Set<() => void>();
2790
2811
  #selectionFenceGeneration = 0;
2791
2812
  #defaultModelSelectionMutationRevision = 0;
2813
+ #userModelSelectionRevision = 0;
2814
+ #userCanonicalVariantSelectionRevision = 0;
2815
+ #userCanonicalVariantSelection: string | undefined;
2792
2816
  #thinkingLevelMutationRevision = 0;
2793
2817
  #thinkingVisibilityMutationRevision = 0;
2794
2818
  #thinkingLevelLiveMutationRevision = 0;
@@ -3439,8 +3463,17 @@ export class AgentSession {
3439
3463
  this.#activeAttemptScope = undefined;
3440
3464
  this.#activeLogicalRunId = undefined;
3441
3465
  }
3442
- #acceptSdkAttemptRun(handle: AttemptRunHandle, sdkRunToken?: string, sdkRunTokens?: string[]): void {
3443
- const predecessorScope = this.#activeAttemptScope;
3466
+ #captureSdkContinuationOwnership(scope: AttemptScope | undefined): SdkContinuationOwnership | undefined {
3467
+ const token = scope ? this.#sdkRunTokensByAttemptScope.get(scope) : undefined;
3468
+ if (!scope || token === undefined) return undefined;
3469
+ return { scope, token, cohort: [...(this.#sdkRunCohortsByAttemptScope.get(scope) ?? [token])] };
3470
+ }
3471
+ #acceptSdkAttemptRun(
3472
+ handle: AttemptRunHandle,
3473
+ sdkRunToken?: string,
3474
+ sdkRunTokens?: string[],
3475
+ predecessorScope?: AttemptScope,
3476
+ ): void {
3444
3477
  const predecessorSdkRunToken =
3445
3478
  predecessorScope === undefined ? undefined : this.#sdkRunTokensByAttemptScope.get(predecessorScope);
3446
3479
  const carryRecoverySkip =
@@ -3450,14 +3483,23 @@ export class AgentSession {
3450
3483
  this.#skipPostPromptRecoveryWaitByAttemptScope.delete(predecessorScope);
3451
3484
  this.#acceptRunHandle(handle);
3452
3485
  this.#settleTrackedOwnRunPromotions(handle);
3486
+ // Logical continuation lineage is causal even without an SDK submission.
3487
+ if (predecessorScope) {
3488
+ this.#lifecycleScopesByAttemptScope.set(
3489
+ handle.scope,
3490
+ this.#lifecycleScopesByAttemptScope.get(predecessorScope) ?? predecessorScope,
3491
+ );
3492
+ }
3453
3493
  if (sdkRunToken !== undefined) {
3454
3494
  this.#activeSdkRunToken = sdkRunToken;
3455
3495
  this.#sdkRunTokensByAttemptScope.set(handle.scope, sdkRunToken);
3456
- const inheritedCohort =
3457
- predecessorScope !== undefined && predecessorSdkRunToken === sdkRunToken
3458
- ? this.#sdkRunCohortsByAttemptScope.get(predecessorScope)
3459
- : undefined;
3460
- this.#sdkRunCohortsByAttemptScope.set(handle.scope, sdkRunTokens ?? inheritedCohort ?? [sdkRunToken]);
3496
+ this.#sdkRunCohortsByAttemptScope.set(handle.scope, sdkRunTokens ?? [sdkRunToken]);
3497
+ for (const token of this.#sdkRunCohortsByAttemptScope.get(handle.scope) ?? []) {
3498
+ if (this.#sdkTerminalPublications.has(token)) continue;
3499
+ const publication = Promise.withResolvers<void>();
3500
+ void publication.promise.catch(() => undefined);
3501
+ this.#sdkTerminalPublications.set(token, publication);
3502
+ }
3461
3503
  }
3462
3504
  if (carryRecoverySkip) this.#skipPostPromptRecoveryWaitByAttemptScope.add(handle.scope);
3463
3505
  }
@@ -3607,6 +3649,7 @@ export class AgentSession {
3607
3649
  #restoreManagedFallbackGetApiKey: (() => void) | undefined;
3608
3650
  // Todo completion reminder state
3609
3651
  #todoReminderCount = 0;
3652
+ #todoReminderContinuationGeneration: number | undefined;
3610
3653
  #deepInterviewUserIntentEpoch = 0;
3611
3654
  #deepInterviewTurnOwnerEpoch = 0;
3612
3655
  #deepInterviewGenuineUserMessageEpochs = new WeakMap<object, number>();
@@ -3800,6 +3843,7 @@ export class AgentSession {
3800
3843
  #skipPostPromptRecoveryWaitByAttemptScope = new WeakSet<AttemptScope>();
3801
3844
  #sdkRunTokensByAttemptScope = new WeakMap<AttemptScope, string>();
3802
3845
  #sdkRunCohortsByAttemptScope = new WeakMap<AttemptScope, string[]>();
3846
+ #lifecycleScopesByAttemptScope = new WeakMap<AttemptScope, AttemptScope>();
3803
3847
  #activeSdkRunToken: string | undefined;
3804
3848
  #activeAttemptScope: AttemptScope | undefined;
3805
3849
  #attemptAuthority!: AttemptScopeAuthority;
@@ -4025,6 +4069,8 @@ export class AgentSession {
4025
4069
  // async event handlers settle. Subscribers treat agent_end as readiness, so
4026
4070
  // publishing it earlier lets a successor corrupt the prior prompt's lifecycle.
4027
4071
  #pendingAgentEndEmit: AgentSessionEvent | undefined;
4072
+ #agentEndContinuationDecisions = new WeakSet<AgentSessionEvent>();
4073
+ #pendingSdkAgentEnds = new Set<AgentSessionEvent>();
4028
4074
  // A scheduled continuation owns this terminal boundary until it either starts
4029
4075
  // the successor or proves it cannot. Holds prevent a false idle event while
4030
4076
  // preserving the predecessor for cancellation and preflight failures.
@@ -4035,6 +4081,8 @@ export class AgentSession {
4035
4081
  #agentEndPublicationInFlight = 0;
4036
4082
  #agentEndPublicationPromise: Promise<void> = Promise.resolve();
4037
4083
  #agentEndHandlingPromise: Promise<void> = Promise.resolve();
4084
+ #sdkTerminalPublications = new Map<string, PromiseWithResolvers<void>>();
4085
+ #publishingSdkTerminals = new Set<PromiseWithResolvers<void>>();
4038
4086
 
4039
4087
  #obfuscator: SecretObfuscator | undefined;
4040
4088
  #checkpointState: CheckpointState | undefined = undefined;
@@ -4073,11 +4121,7 @@ export class AgentSession {
4073
4121
  readonly rawSseDebugBuffer: RawSseDebugBuffer;
4074
4122
 
4075
4123
  #acquirePowerAssertion(): void {
4076
- if (
4077
- process.platform !== "darwin" &&
4078
- process.platform !== "linux" &&
4079
- process.platform !== "win32"
4080
- ) {
4124
+ if (process.platform !== "darwin" && process.platform !== "linux" && process.platform !== "win32") {
4081
4125
  return;
4082
4126
  }
4083
4127
  if (this.#powerAssertion || this.#powerAssertionLoad) return;
@@ -4707,8 +4751,21 @@ export class AgentSession {
4707
4751
  }
4708
4752
  }
4709
4753
 
4710
- #reserveDeferredAgentEndForContinuation(): symbol | undefined {
4711
- const pending = this.#pendingAgentEndEmit;
4754
+ #reserveDeferredAgentEndForContinuation(predecessorScope?: AttemptScope): symbol | undefined {
4755
+ const resourceRunId = this.#runResourceLeaseContext.getStore()?.resourceRunId;
4756
+ const pending = predecessorScope
4757
+ ? [...this.#pendingSdkAgentEnds].find(
4758
+ event => (event as AgentSessionEvent & { scope?: AttemptScope }).scope === predecessorScope,
4759
+ )
4760
+ : resourceRunId === undefined
4761
+ ? this.#pendingAgentEndEmit
4762
+ : ([...this.#pendingSdkAgentEnds].find(
4763
+ event => getAgentTerminalOwnerContext(event)?.resourceRunId === resourceRunId,
4764
+ ) ??
4765
+ (this.#pendingAgentEndEmit &&
4766
+ getAgentTerminalOwnerContext(this.#pendingAgentEndEmit)?.resourceRunId === resourceRunId
4767
+ ? this.#pendingAgentEndEmit
4768
+ : undefined));
4712
4769
  if (!pending) return undefined;
4713
4770
  const hold = Symbol("deferred-agent-end-continuation");
4714
4771
  this.#pendingAgentEndContinuationHolds.set(hold, pending);
@@ -4722,6 +4779,8 @@ export class AgentSession {
4722
4779
  throw new Error("Cannot restore a deferred agent_end over a different pending terminal event");
4723
4780
  }
4724
4781
  this.#pendingAgentEndEmit = pending;
4782
+ const scope = (pending as AgentSessionEvent & { scope?: AttemptScope }).scope;
4783
+ if (scope && this.#sdkRunTokensByAttemptScope.has(scope)) this.#pendingSdkAgentEnds.add(pending);
4725
4784
  const hold = Symbol("deferred-agent-end-continuation");
4726
4785
  this.#pendingAgentEndContinuationHolds.set(hold, pending);
4727
4786
  this.#deferredAgentEndWorkLeases.set(hold, this.#sessionWorkLease.acquire());
@@ -4732,6 +4791,7 @@ export class AgentSession {
4732
4791
  if (!hold) return undefined;
4733
4792
  const pending = this.#pendingAgentEndContinuationHolds.get(hold);
4734
4793
  this.#pendingAgentEndContinuationHolds.delete(hold);
4794
+ if (pending) this.#pendingSdkAgentEnds.delete(pending);
4735
4795
  this.#deferredAgentEndWorkLeases.get(hold)?.release();
4736
4796
  this.#deferredAgentEndWorkLeases.delete(hold);
4737
4797
  if (pending && this.#pendingAgentEndEmit === pending) {
@@ -4749,6 +4809,8 @@ export class AgentSession {
4749
4809
  }
4750
4810
 
4751
4811
  #restoreDeferredAgentEndAfterContinuationFailure(pending: AgentSessionEvent | undefined): void {
4812
+ const scope = (pending as (AgentSessionEvent & { scope?: AttemptScope }) | undefined)?.scope;
4813
+ if (pending && scope && this.#sdkRunTokensByAttemptScope.has(scope)) this.#pendingSdkAgentEnds.add(pending);
4752
4814
  if (pending && !this.#pendingAgentEndEmit) {
4753
4815
  this.#pendingAgentEndEmit = pending;
4754
4816
  }
@@ -4761,6 +4823,8 @@ export class AgentSession {
4761
4823
  ): void {
4762
4824
  if (!hold || !pending) return;
4763
4825
  this.#pendingAgentEndEmit = pending;
4826
+ const scope = (pending as AgentSessionEvent & { scope?: AttemptScope }).scope;
4827
+ if (scope && this.#sdkRunTokensByAttemptScope.has(scope)) this.#pendingSdkAgentEnds.add(pending);
4764
4828
  this.#pendingAgentEndContinuationHolds.set(hold, pending);
4765
4829
  }
4766
4830
 
@@ -5063,11 +5127,15 @@ export class AgentSession {
5063
5127
  this.#flushPendingAgentEnd();
5064
5128
  return flushError;
5065
5129
  }
5066
- async #settleEndedInFlight(token: symbol, promptWait?: "publication" | "full"): Promise<void> {
5130
+ async #settleEndedInFlight(
5131
+ token: symbol,
5132
+ promptWait?: "publication" | "full",
5133
+ sdkPublication?: Promise<void>,
5134
+ ): Promise<void> {
5067
5135
  const flushError = this.#endInFlight(token);
5068
5136
  const predecessorPromptStillInFlight = this.#livePromptsInFlight() > 0;
5069
5137
  if (promptWait === "publication") {
5070
- await this.#agentEndPublicationPromise;
5138
+ await (sdkPublication ?? this.#agentEndPublicationPromise);
5071
5139
  } else if (promptWait === "full") {
5072
5140
  await this.#agentEndHandlingPromise;
5073
5141
  await this.#waitForPostPromptRecovery();
@@ -5107,13 +5175,23 @@ export class AgentSession {
5107
5175
  }
5108
5176
 
5109
5177
  #flushPendingAgentEnd(): void {
5178
+ for (const sdkPending of this.#pendingSdkAgentEnds) {
5179
+ if (!this.#agentEndContinuationDecisions.has(sdkPending)) continue;
5180
+ if ([...this.#pendingAgentEndContinuationHolds.values()].includes(sdkPending)) continue;
5181
+ this.#pendingSdkAgentEnds.delete(sdkPending);
5182
+ if (this.#pendingAgentEndEmit === sdkPending) this.#pendingAgentEndEmit = undefined;
5183
+ const sdkLease = this.#deferredAgentEndLeases.get(sdkPending);
5184
+ this.#deferredAgentEndLeases.delete(sdkPending);
5185
+ this.#startAgentEndPublication(sdkPending, sdkLease);
5186
+ }
5187
+ const pending = this.#pendingAgentEndEmit;
5188
+ if (pending && this.#pendingSdkAgentEnds.has(pending)) return;
5110
5189
  if (
5111
5190
  this.#livePromptsInFlight() > 0 ||
5112
5191
  this.#agentEventHandlersInFlight > 0 ||
5113
5192
  this.#pendingAgentEndContinuationHolds.size > 0
5114
5193
  )
5115
5194
  return;
5116
- const pending = this.#pendingAgentEndEmit;
5117
5195
  if (!pending) {
5118
5196
  this.#resolveSessionSettlement();
5119
5197
  return;
@@ -5125,13 +5203,16 @@ export class AgentSession {
5125
5203
  }
5126
5204
 
5127
5205
  #startAgentEndPublication(pending: AgentSessionEvent, lease?: RunResourceProducerLease): void {
5206
+ this.#agentEndContinuationDecisions.delete(pending);
5128
5207
  this.#agentEndPublicationInFlight++;
5129
5208
  const pendingScope = (pending as AgentSessionEvent & { scope?: AttemptScopeRef }).scope as
5130
5209
  | AttemptScope
5131
5210
  | undefined;
5132
5211
  const sdkTerminal = pendingScope !== undefined && this.#sdkRunTokensByAttemptScope.has(pendingScope);
5133
5212
  this.#agentEndPublicationPromise = this.#publishDeferredAgentEnd(pending, lease, sdkTerminal);
5134
- void this.#agentEndPublicationPromise;
5213
+ void this.#agentEndPublicationPromise.catch(error => {
5214
+ logger.warn("Agent terminal publication failed", { error: sanitizePromptFailure(error) });
5215
+ });
5135
5216
  }
5136
5217
 
5137
5218
  async #publishDeferredAgentEnd(
@@ -5147,6 +5228,13 @@ export class AgentSession {
5147
5228
  const publicationCorrelationId = publicationScope
5148
5229
  ? this.#sdkRunTokensByAttemptScope.get(publicationScope)
5149
5230
  : undefined;
5231
+ const sdkPublications = (publicationScope ? (this.#sdkRunCohortsByAttemptScope.get(publicationScope) ?? []) : [])
5232
+ .map(token => ({ token, publication: this.#sdkTerminalPublications.get(token) }))
5233
+ .filter(
5234
+ (entry): entry is { token: string; publication: PromiseWithResolvers<void> } =>
5235
+ entry.publication !== undefined,
5236
+ );
5237
+ for (const { publication } of sdkPublications) this.#publishingSdkTerminals.add(publication);
5150
5238
  let extensionDelivery: Promise<void> | undefined;
5151
5239
  const releaseLease = () => {
5152
5240
  if (!lease) return;
@@ -5239,7 +5327,15 @@ export class AgentSession {
5239
5327
  try {
5240
5328
  if (lease) await this.#runResourceLeaseContext.run(lease, publish);
5241
5329
  else await publish();
5330
+ for (const { publication } of sdkPublications) publication.resolve();
5331
+ } catch (error) {
5332
+ for (const { publication } of sdkPublications) publication.reject(error);
5333
+ throw error;
5242
5334
  } finally {
5335
+ for (const { token, publication } of sdkPublications) {
5336
+ this.#publishingSdkTerminals.delete(publication);
5337
+ if (this.#sdkTerminalPublications.get(token) === publication) this.#sdkTerminalPublications.delete(token);
5338
+ }
5243
5339
  if (extensionDelivery) void extensionDelivery.then(releaseLease, releaseLease);
5244
5340
  else releaseLease();
5245
5341
  this.#agentEndPublicationInFlight = Math.max(0, this.#agentEndPublicationInFlight - 1);
@@ -5498,12 +5594,7 @@ export class AgentSession {
5498
5594
  controller !== undefined &&
5499
5595
  controller.chain.entries.length > 1;
5500
5596
  if (shouldCapture && modelKey) this.#managedFallbackActiveCredentialRows.delete(modelKey);
5501
- if (
5502
- override &&
5503
- model &&
5504
- storageProvider === override.storageProvider &&
5505
- modelKey === override.modelKey
5506
- ) {
5597
+ if (override && model && storageProvider === override.storageProvider && modelKey === override.modelKey) {
5507
5598
  this.#managedFallbackNextCredentialOverride = undefined;
5508
5599
  const apiKey = await this.#modelRegistry.getApiKey(model, this.credentialSessionId, {
5509
5600
  credentialSelector: { kind: "id", value: String(override.rowId) },
@@ -5513,10 +5604,7 @@ export class AgentSession {
5513
5604
  return apiKey;
5514
5605
  }
5515
5606
  // The preselected credential is no longer available; try to resolve another one of the same kind.
5516
- const fallbackApiKey = await this.#resolveManagedFallbackCredentialRow(
5517
- model,
5518
- override.credentialKind,
5519
- );
5607
+ const fallbackApiKey = await this.#resolveManagedFallbackCredentialRow(model, override.credentialKind);
5520
5608
  if (fallbackApiKey !== undefined) {
5521
5609
  if (shouldCapture && model) this.#captureManagedFallbackActiveCredential(model);
5522
5610
  return fallbackApiKey;
@@ -5524,8 +5612,7 @@ export class AgentSession {
5524
5612
  throw new Error("Selected managed fallback credential is no longer available");
5525
5613
  }
5526
5614
  const apiKey = await invokeOriginalGetApiKey(provider);
5527
- if (shouldCapture && model && isAuthenticated(apiKey))
5528
- this.#captureManagedFallbackActiveCredential(model);
5615
+ if (shouldCapture && model && isAuthenticated(apiKey)) this.#captureManagedFallbackActiveCredential(model);
5529
5616
  return apiKey;
5530
5617
  };
5531
5618
  this.agent.getApiKey = managedGetApiKey;
@@ -5612,12 +5699,14 @@ export class AgentSession {
5612
5699
  void this.#queueFollowUpAfterReservation(message, displayText, {
5613
5700
  createDisplayEntry: false,
5614
5701
  trackExternalFollowUp: false,
5702
+ sdkRunToken: this.#activeSdkRunToken,
5615
5703
  }).catch(error => {
5616
5704
  this.#settleDeliveredOwnedRegistrations([message]);
5617
5705
  logger.warn("Owned streaming follow-up was rejected", { error: String(error) });
5618
5706
  });
5619
5707
  },
5620
5708
  injectIdle: async (messages, signal) => {
5709
+ const sdkRunToken = this.#activeSdkRunToken;
5621
5710
  // Mandated boundary comment (corrected turn semantics): same origin
5622
5711
  // split as the streaming injector — an allowed owned-completion
5623
5712
  // delivery starts a fresh turn attempt/lineage and is not a
@@ -5652,14 +5741,14 @@ export class AgentSession {
5652
5741
  await this.agent.prompt(first, {
5653
5742
  ...this.#managedFallbackPromptOptions(),
5654
5743
  onRunAccepted: (handle: AttemptRunHandle) => {
5655
- if (handle) this.#acceptSdkAttemptRun(handle);
5744
+ if (handle) this.#acceptSdkAttemptRun(handle, sdkRunToken);
5656
5745
  },
5657
5746
  });
5658
5747
  } else {
5659
5748
  await this.agent.prompt(survivors, {
5660
5749
  ...this.#managedFallbackPromptOptions(),
5661
5750
  onRunAccepted: (handle: AttemptRunHandle) => {
5662
- if (handle) this.#acceptSdkAttemptRun(handle);
5751
+ if (handle) this.#acceptSdkAttemptRun(handle, sdkRunToken);
5663
5752
  },
5664
5753
  });
5665
5754
  }
@@ -5831,9 +5920,8 @@ export class AgentSession {
5831
5920
  (largestPhase, phase) =>
5832
5921
  Math.max(
5833
5922
  largestPhase,
5834
- phase.tasks.filter(
5835
- task => task.status === "pending" || task.status === "in_progress",
5836
- ).length,
5923
+ phase.tasks.filter(task => task.status === "pending" || task.status === "in_progress")
5924
+ .length,
5837
5925
  ),
5838
5926
  0,
5839
5927
  );
@@ -6747,6 +6835,17 @@ export class AgentSession {
6747
6835
  return { running, recent, delivery };
6748
6836
  }
6749
6837
 
6838
+ /**
6839
+ * Lifecycle status of every subagent owned by this session, read from the
6840
+ * manager's stable control-plane records (they outlive AsyncJob eviction).
6841
+ */
6842
+ getSubagentLifecycleStatuses(): SubagentLifecycle[] {
6843
+ const manager = this.#ownedAsyncJobManager ?? AsyncJobManager.instance();
6844
+ if (!manager) return [];
6845
+ const ownerFilter = this.#agentId ? { ownerId: this.#agentId } : undefined;
6846
+ return manager.getSubagentRecords(ownerFilter).map(record => record.status);
6847
+ }
6848
+
6750
6849
  /**
6751
6850
  * Cancel async jobs registered by *this* agent only. Used by lifecycle
6752
6851
  * transitions (newSession, switchSession, handoff, dispose) so a subagent
@@ -6853,11 +6952,9 @@ export class AgentSession {
6853
6952
  /** Emit an event to all listeners without letting one subscriber poison lifecycle settlement. */
6854
6953
  #emit(event: AgentSessionEvent): void {
6855
6954
  for (const listener of this.#eventListenerSnapshot) {
6856
- try {
6857
- listener(event);
6858
- } catch (error) {
6955
+ deliverSessionEvent(listener, event, error => {
6859
6956
  logger.warn("Agent session event subscriber failed", { event: event.type, error: String(error) });
6860
- }
6957
+ });
6861
6958
  }
6862
6959
  }
6863
6960
 
@@ -6922,6 +7019,8 @@ export class AgentSession {
6922
7019
  object,
6923
7020
  { scope?: AttemptScope; sdkRunToken?: string; persistGeneration: number; persistBarrier?: Promise<void> }
6924
7021
  >();
7022
+ /** Extension handlers cannot mutate or replace the Agent-claimed run owner. */
7023
+ #terminalOwnerByExtensionEvent = new WeakMap<object, AgentTerminalOwnerContext>();
6925
7024
 
6926
7025
  /**
6927
7026
  * Capture what is true at the SYNCHRONOUS agent-event boundary, before any async work.
@@ -7075,8 +7174,8 @@ export class AgentSession {
7075
7174
  const pendingAgentEnd =
7076
7175
  event.type === "agent_end" &&
7077
7176
  !maintenanceCheckpoint &&
7078
- (this.#pendingAgentEndEmit === event || this.#deferredAgentEndLeases.has(event))
7079
- ? this.#pendingAgentEndEmit
7177
+ (this.#pendingAgentEndEmit === event || this.#pendingSdkAgentEnds.has(event))
7178
+ ? event
7080
7179
  : undefined;
7081
7180
  if (pendingAgentEnd) {
7082
7181
  this.#deferredAgentEndLeases.set(pendingAgentEnd, eventLease);
@@ -7093,6 +7192,7 @@ export class AgentSession {
7093
7192
  }
7094
7193
  }
7095
7194
  this.#agentEventHandlersInFlight = Math.max(0, this.#agentEventHandlersInFlight - 1);
7195
+ if (event.type === "agent_end") this.#agentEndContinuationDecisions.add(event);
7096
7196
  this.#flushPendingAgentEnd();
7097
7197
  agentEndHandled?.resolve();
7098
7198
  // Every other in-flight counter republishes settlement when it drops; this
@@ -7329,16 +7429,10 @@ export class AgentSession {
7329
7429
  // have unwound. Subscribers treat this event as the ready signal; flushing it
7330
7430
  // from abort while either barrier is active permits a successor to race the
7331
7431
  // prior prompt's cleanup.
7332
- const sdkTerminal =
7333
- event.type === "agent_end" && attemptScope !== undefined && this.#sdkRunTokensByAttemptScope.has(attemptScope);
7334
- if (
7335
- event.type === "agent_end" &&
7336
- sdkTerminal &&
7337
- this.#pendingAgentEndContinuationHolds.size === 0 &&
7338
- this.#pendingAgentEndEmit === undefined
7339
- ) {
7432
+ if (event.type === "agent_end" && attemptScope && this.#sdkRunTokensByAttemptScope.has(attemptScope)) {
7433
+ this.#pendingSdkAgentEnds.add(event);
7340
7434
  if (eventLease) this.#deferredAgentEndLeases.set(event, eventLease);
7341
- this.#startAgentEndPublication(event, eventLease);
7435
+ this.#pendingAgentEndEmit = event;
7342
7436
  return;
7343
7437
  }
7344
7438
  if (event.type === "agent_end" && (this.#livePromptsInFlight() > 0 || this.#agentEventHandlersInFlight > 0)) {
@@ -7452,6 +7546,8 @@ export class AgentSession {
7452
7546
  eventLease?: RunResourceProducerLease,
7453
7547
  ): Promise<void> => {
7454
7548
  const attemptScope = (event as AgentEvent & { scope?: AttemptScope }).scope;
7549
+ const terminalSdkOwnership =
7550
+ event.type === "agent_end" ? this.#captureSdkContinuationOwnership(attemptScope) : undefined;
7455
7551
 
7456
7552
  // These lifecycle boundaries can be delivered without awaiting this listener.
7457
7553
  // Revoke streaming-edit cache generations before any admission, spill, or
@@ -8160,7 +8256,7 @@ export class AgentSession {
8160
8256
  skipCompactionCheck: true,
8161
8257
  resourceRunId: activePromptHandle,
8162
8258
  maintenanceContinuation: true,
8163
- sdkRunToken: attemptScope ? this.#sdkRunTokensByAttemptScope.get(attemptScope) : undefined,
8259
+ sdkOwnership: terminalSdkOwnership,
8164
8260
  });
8165
8261
  }
8166
8262
  return;
@@ -8244,7 +8340,9 @@ export class AgentSession {
8244
8340
  }
8245
8341
 
8246
8342
  // Check for retryable errors first (overloaded, rate limit, server errors)
8247
- if (this.#isRetryableError(msg)) {
8343
+ // Skip retry for todo reminder continuations - they should fail silently without retrying
8344
+ const isReminderContinuationError = this.#todoReminderContinuationGeneration === agentEndGeneration && agentEndGeneration !== undefined;
8345
+ if (!isReminderContinuationError && this.#isRetryableError(msg)) {
8248
8346
  const transportFailure = (msg as AssistantMessage & { transportFailure?: TransportFailureFacts })
8249
8347
  .transportFailure;
8250
8348
  const messageScope = this.#assistantAttemptScopes.get(msg);
@@ -8257,6 +8355,10 @@ export class AgentSession {
8257
8355
  );
8258
8356
  if (didRetry) return; // Retry was initiated, don't proceed to compaction
8259
8357
  }
8358
+ // Clear the reminder continuation flag after processing
8359
+ if (isReminderContinuationError) {
8360
+ this.#todoReminderContinuationGeneration = undefined;
8361
+ }
8260
8362
  if (this.#retryAttempt > 0) {
8261
8363
  // A prior retry ended on a non-retryable (terminal) message: emit
8262
8364
  // the terminal retry-end and reset so observers clear retry state.
@@ -8275,7 +8377,7 @@ export class AgentSession {
8275
8377
  const compactionTask = this.#schedulePostPromptTask(
8276
8378
  async () => {
8277
8379
  if (this.#isDisposed || this.#sessionAdmissionClosing) return;
8278
- await this.#checkCompaction(msg, true, undefined, activePromptHandle);
8380
+ await this.#checkCompaction(msg, true, undefined, activePromptHandle, undefined, terminalSdkOwnership);
8279
8381
  },
8280
8382
  { resourceRunId: activePromptHandle },
8281
8383
  );
@@ -8501,10 +8603,12 @@ export class AgentSession {
8501
8603
  /** Internal predecessor terminal event sequestered while waiting behind selection. */
8502
8604
  deferredPredecessorAgentEnd?: AgentSessionEvent;
8503
8605
  /** Internal causal SDK owner captured when this continuation was scheduled. */
8504
- sdkRunToken?: string;
8606
+ sdkOwnership?: SdkContinuationOwnership;
8607
+ /** Disable managed fallback retries for this continuation (used for terminal server-initiated turns). */
8608
+ disableManagedFallback?: boolean;
8505
8609
  }): Promise<void> {
8506
8610
  const continuationAdmission = this.#captureScheduledContinuationAdmission();
8507
- const scheduledSdkRunToken = options?.sdkRunToken;
8611
+ const scheduledSdkOwnership = options?.sdkOwnership;
8508
8612
  const selectionFenceGeneration =
8509
8613
  options?.selectionFenceGeneration ??
8510
8614
  this.#selectionFenceGenerationContext.getStore() ??
@@ -8526,7 +8630,7 @@ export class AgentSession {
8526
8630
  generation: deferredPromptGeneration,
8527
8631
  selectionFenceGeneration,
8528
8632
  deferredPredecessorAgentEnd,
8529
- sdkRunToken: scheduledSdkRunToken,
8633
+ sdkOwnership: scheduledSdkOwnership,
8530
8634
  });
8531
8635
  } finally {
8532
8636
  // The recursive call synchronously re-reserved its settlement
@@ -8646,14 +8750,21 @@ export class AgentSession {
8646
8750
  }
8647
8751
  const predecessorAgentEnd =
8648
8752
  this.#claimDeferredAgentEndForContinuation(predecessorAgentEndHold);
8753
+ const predecessorScope = (
8754
+ predecessorAgentEnd as (AgentSessionEvent & { scope?: AttemptScope }) | undefined
8755
+ )?.scope;
8756
+ const predecessorSdkOwnership = this.#captureSdkContinuationOwnership(predecessorScope);
8649
8757
  let predecessorAccepted = false;
8650
8758
  const releasePredecessor = () => {
8651
8759
  if (predecessorAccepted) return;
8652
8760
  predecessorAccepted = true;
8653
- this.#releaseDeferredAgentEndLease(predecessorAgentEnd);
8761
+ if (startsQueuedSuccessor)
8762
+ this.#restoreDeferredAgentEndAfterContinuationFailure(predecessorAgentEnd);
8763
+ else this.#releaseDeferredAgentEndLease(predecessorAgentEnd);
8654
8764
  };
8655
8765
  const hasQueuedMessages = this.agent.hasQueuedMessages();
8656
8766
  const startsQueuedSuccessor =
8767
+ options?.maintenanceContinuation !== true &&
8657
8768
  hasQueuedMessages &&
8658
8769
  (options?.continueQueuedOnly === true ||
8659
8770
  this.agent.state.messages.at(-1)?.role === "assistant");
@@ -8662,7 +8773,7 @@ export class AgentSession {
8662
8773
  : this.agent.continue.bind(this.agent);
8663
8774
  try {
8664
8775
  await continueQueued({
8665
- ...this.#managedFallbackPromptOptions(),
8776
+ ...(options?.disableManagedFallback ? { fallbackManaged: false } : this.#managedFallbackPromptOptions()),
8666
8777
  maintenanceContinuation: options?.maintenanceContinuation,
8667
8778
  // Reset only after continue() has claimed the queued turn. Skipped or stale
8668
8779
  // continuations retain predecessor accounting, and resetAttemptBudget keeps
@@ -8672,13 +8783,21 @@ export class AgentSession {
8672
8783
  // agent_start (review P1); their queued messages are in-run
8673
8784
  // consumptions, not own-run promotions.
8674
8785
  const startsOwn = options?.maintenanceContinuation !== true;
8675
- const inheritedSdkRunToken = options?.continueQueuedOnly
8676
- ? undefined
8677
- : (options?.sdkRunToken ?? scheduledSdkRunToken);
8786
+ const inheritedSdkOwnership =
8787
+ options?.continueQueuedOnly || startsQueuedSuccessor
8788
+ ? undefined
8789
+ : (scheduledSdkOwnership ?? predecessorSdkOwnership);
8790
+ const inheritedPredecessorScope =
8791
+ options?.continueQueuedOnly || startsQueuedSuccessor
8792
+ ? undefined
8793
+ : (inheritedSdkOwnership?.scope ?? predecessorScope);
8678
8794
  const consumedSdkRunTokens = acceptance.consumedQueuedMessages
8679
8795
  .map(message => this.#sdkRunTokensByQueuedMessage.get(message))
8680
8796
  .filter((token): token is string => token !== undefined);
8681
- const sdkRunToken = consumedSdkRunTokens[0] ?? inheritedSdkRunToken;
8797
+ const sdkRunToken = inheritedSdkOwnership?.token ?? consumedSdkRunTokens[0];
8798
+ const sdkRunTokens = inheritedSdkOwnership
8799
+ ? [...new Set([...inheritedSdkOwnership.cohort, ...consumedSdkRunTokens])]
8800
+ : consumedSdkRunTokens;
8682
8801
  this.#fireQueuedPromotionHooks(acceptance.consumedQueuedMessages, {
8683
8802
  startsOwnRun: startsOwn,
8684
8803
  });
@@ -8686,7 +8805,8 @@ export class AgentSession {
8686
8805
  this.#acceptSdkAttemptRun(
8687
8806
  handle,
8688
8807
  sdkRunToken,
8689
- consumedSdkRunTokens.length > 0 ? consumedSdkRunTokens : undefined,
8808
+ sdkRunTokens.length > 0 ? sdkRunTokens : undefined,
8809
+ inheritedPredecessorScope,
8690
8810
  );
8691
8811
  options?.onRunAccepted?.(handle);
8692
8812
  // Keep the queued token available through the acceptance callback;
@@ -8813,7 +8933,11 @@ export class AgentSession {
8813
8933
  return compactionSettings.autoContinue === false ? "auto_continue_disabled_non_resumable_tail" : undefined;
8814
8934
  }
8815
8935
 
8816
- async #scheduleOverflowRetryContinuation(generation: number, resourceRunId?: string): Promise<boolean> {
8936
+ async #scheduleOverflowRetryContinuation(
8937
+ generation: number,
8938
+ resourceRunId?: string,
8939
+ sdkOwnership?: SdkContinuationOwnership,
8940
+ ): Promise<boolean> {
8817
8941
  this.#stripOverflowFailedTurnForRetry();
8818
8942
  const snapshot = await this.#compactionStateSnapshot();
8819
8943
  if (
@@ -8830,6 +8954,7 @@ export class AgentSession {
8830
8954
  generation,
8831
8955
  suppressPredecessorAgentEnd: true,
8832
8956
  resourceRunId,
8957
+ sdkOwnership,
8833
8958
  onSkip: reason => this.#logCompactionContinuationSkipped("overflow_retry", reason),
8834
8959
  onError: error => this.#logCompactionContinuationError("overflow_retry", error),
8835
8960
  });
@@ -8838,7 +8963,7 @@ export class AgentSession {
8838
8963
 
8839
8964
  const compactionSettings = this.settings.getGroup("compaction");
8840
8965
  if (compactionSettings.autoContinue !== false) {
8841
- this.#scheduleAutoContinuePrompt(generation, false, resourceRunId);
8966
+ this.#scheduleAutoContinuePrompt(generation, false, resourceRunId, undefined, undefined, sdkOwnership);
8842
8967
  return true;
8843
8968
  }
8844
8969
 
@@ -8852,7 +8977,7 @@ export class AgentSession {
8852
8977
  resourceRunId?: string,
8853
8978
  deferredSelectionFenceGeneration?: number,
8854
8979
  deferredPredecessorAgentEnd?: AgentSessionEvent,
8855
- sdkRunToken?: string,
8980
+ sdkOwnership?: SdkContinuationOwnership,
8856
8981
  ): void {
8857
8982
  if (this.#sessionTransitionKind !== undefined || this.#handoffTransitionActive) {
8858
8983
  this.#deferredAutoContinueDuringTransition = () =>
@@ -8862,14 +8987,16 @@ export class AgentSession {
8862
8987
  resourceRunId,
8863
8988
  deferredSelectionFenceGeneration,
8864
8989
  deferredPredecessorAgentEnd,
8865
- sdkRunToken,
8990
+ sdkOwnership,
8866
8991
  );
8867
8992
  return;
8868
8993
  }
8869
8994
  const scheduledGeneration = generation;
8870
- const scheduledSdkRunToken =
8871
- sdkRunToken ??
8872
- (this.#activeAttemptScope ? this.#sdkRunTokensByAttemptScope.get(this.#activeAttemptScope) : undefined);
8995
+ const scheduledSdkOwnership =
8996
+ sdkOwnership ??
8997
+ this.#captureSdkContinuationOwnership(
8998
+ (deferredPredecessorAgentEnd as (AgentSessionEvent & { scope?: AttemptScope }) | undefined)?.scope,
8999
+ );
8873
9000
  const continuationAuthorized = async (
8874
9001
  signal: AbortSignal,
8875
9002
  hasPendingNextTurnMessages = false,
@@ -8926,7 +9053,7 @@ export class AgentSession {
8926
9053
  resourceRunId,
8927
9054
  selectionFenceGeneration,
8928
9055
  predecessorAgentEnd,
8929
- scheduledSdkRunToken,
9056
+ scheduledSdkOwnership,
8930
9057
  );
8931
9058
  } finally {
8932
9059
  this.#endSelectionFenceDeferralTracking(selectionFenceGeneration);
@@ -8974,7 +9101,9 @@ export class AgentSession {
8974
9101
  promptText,
8975
9102
  {
8976
9103
  skipPostPromptRecoveryWait: true,
8977
- sdkRunToken: scheduledSdkRunToken,
9104
+ sdkRunToken: scheduledSdkOwnership?.token,
9105
+ sdkRunTokens: scheduledSdkOwnership?.cohort,
9106
+ sdkPredecessorScope: scheduledSdkOwnership?.scope,
8978
9107
  skipCompactionCheck: true,
8979
9108
  predecessorAgentEndHold,
8980
9109
  onFinalPreflight: ({ hasPendingNextTurnMessages }) =>
@@ -9910,7 +10039,12 @@ export class AgentSession {
9910
10039
  type: "agent_start",
9911
10040
  ...(sdkRunToken ? { sdkRunToken } : {}),
9912
10041
  ...(deliveryScope
9913
- ? { sdkRunTokens: this.#sdkRunCohortsByAttemptScope.get(deliveryScope as AttemptScope) }
10042
+ ? {
10043
+ sdkRunTokens: this.#sdkRunCohortsByAttemptScope.get(deliveryScope as AttemptScope),
10044
+ lifecycleScope:
10045
+ this.#lifecycleScopesByAttemptScope.get(deliveryScope as AttemptScope) ??
10046
+ (deliveryScope as AttemptScope),
10047
+ }
9914
10048
  : {}),
9915
10049
  },
9916
10050
  undefined,
@@ -9928,17 +10062,19 @@ export class AgentSession {
9928
10062
  deliveryScope,
9929
10063
  );
9930
10064
  } else if (event.type === "agent_end") {
9931
- await this.#extensionRunner.emit(
9932
- {
9933
- type: "agent_end",
9934
- messages: event.messages,
9935
- stopReason: event.stopReason,
9936
- maintenanceOutcome: event.maintenanceOutcome,
9937
- ...(sdkRunToken ? { sdkRunToken } : {}),
9938
- },
9939
- undefined,
9940
- deliveryScope,
9941
- );
10065
+ const extensionEvent = {
10066
+ type: "agent_end" as const,
10067
+ messages: event.messages,
10068
+ stopReason: event.stopReason,
10069
+ maintenanceOutcome: event.maintenanceOutcome,
10070
+ ...(sdkRunToken ? { sdkRunToken } : {}),
10071
+ };
10072
+ // Keep the Agent-claimed owner OUTSIDE the public extension event and
10073
+ // its replaceable side channel. Only the owning session may attest
10074
+ // this exact projected event to the SDK bus.
10075
+ const terminalOwner = getAgentTerminalOwnerContext(event);
10076
+ if (terminalOwner) this.#terminalOwnerByExtensionEvent.set(extensionEvent, terminalOwner);
10077
+ await this.#extensionRunner.emit(extensionEvent, undefined, deliveryScope);
9942
10078
  } else if (event.type === "turn_start") {
9943
10079
  const hookEvent: TurnStartEvent = {
9944
10080
  type: "turn_start",
@@ -10145,6 +10281,22 @@ export class AgentSession {
10145
10281
  this.#abortActiveMidRunBarriers();
10146
10282
  this.#compactionQueuedDeliveryArmed = false;
10147
10283
  if (this.#unsubscribeAgent) {
10284
+ // This accepted cohort cannot observe its terminal after the bridge is
10285
+ // removed. Reject only that captured owner; queued roots and already
10286
+ // publishing terminals retain their independently owned publication.
10287
+ const ownership = this.#captureSdkContinuationOwnership(this.#activeAttemptScope);
10288
+ if (ownership) {
10289
+ const failure = Object.assign(
10290
+ new Error("Agent event bridge disconnected before SDK terminal publication."),
10291
+ { code: "cancelled" },
10292
+ );
10293
+ for (const token of ownership.cohort) {
10294
+ const publication = this.#sdkTerminalPublications.get(token);
10295
+ if (!publication || this.#publishingSdkTerminals.has(publication)) continue;
10296
+ publication.reject(failure);
10297
+ this.#sdkTerminalPublications.delete(token);
10298
+ }
10299
+ }
10148
10300
  this.#unsubscribeAgent();
10149
10301
  this.#unsubscribeAgent = undefined;
10150
10302
  }
@@ -10302,6 +10454,11 @@ export class AgentSession {
10302
10454
  // settle every accepted SDK handle now so consumed submissions cannot wait
10303
10455
  // forever for an agent_end that teardown deliberately suppresses.
10304
10456
  this.#terminalizeQueuedSdkWorkForSessionTransition(this.#queuedMessagesForSessionTransition());
10457
+ const publicationFailure = Object.assign(new Error("Session disposed before SDK terminal publication."), {
10458
+ code: "cancelled",
10459
+ });
10460
+ for (const publication of this.#sdkTerminalPublications.values()) publication.reject(publicationFailure);
10461
+ this.#sdkTerminalPublications.clear();
10305
10462
  // Invalidate every coordinator event admitted before disposal. Handlers may
10306
10463
  // still unwind, but their captured generation can no longer enqueue a write.
10307
10464
  this.#coordinatorPersistGeneration += 1;
@@ -10671,6 +10828,11 @@ export class AgentSession {
10671
10828
  * at `timeoutMs` so a wedged subprocess can't stall process exit.
10672
10829
  */
10673
10830
  async disposeChildSubprocesses(timeoutMs = SIGNAL_TEARDOWN_TIMEOUT_MS): Promise<void> {
10831
+ this.#evalExecutionDisposing = true;
10832
+ this.abortEval();
10833
+ const evalExecutionsSettled = this.#waitForEvalExecutionsToSettle(timeoutMs).then(settled => {
10834
+ if (!settled) logger.warn("signal teardown: active eval executions remain unsettled");
10835
+ });
10674
10836
  const sessionId = this.sessionManager.getSessionId();
10675
10837
  const kernelOwnerId = this.#evalKernelOwnerId;
10676
10838
  this.#unregisterResourceGc?.();
@@ -10680,6 +10842,7 @@ export class AgentSession {
10680
10842
  this.#unregisterDelegationHintSettings?.();
10681
10843
  this.#unregisterDelegationHintSettings = undefined;
10682
10844
  const work = Promise.allSettled([
10845
+ evalExecutionsSettled,
10683
10846
  // kill:true so a forced exit also reaps spawned-app Chrome we own (headless
10684
10847
  // always closes; connected/attached browsers only disconnect — never killed).
10685
10848
  releaseTabsForOwner(sessionId, { kill: true }).catch((error: unknown) =>
@@ -10708,7 +10871,8 @@ export class AgentSession {
10708
10871
  logger.warn("signal teardown: tool session transition cleanups failed", { error }),
10709
10872
  ),
10710
10873
  ]);
10711
- await Promise.race([work, Bun.sleep(timeoutMs)]);
10874
+ const completed = await Promise.race([work.then(() => true), Bun.sleep(timeoutMs).then(() => false)]);
10875
+ if (!completed) logger.warn("signal teardown: cleanup exceeded its bounded caller deadline");
10712
10876
  }
10713
10877
 
10714
10878
  #rebindProviderSessionState(providerSessionState: Map<string, ProviderSessionState>): void {
@@ -11670,7 +11834,9 @@ export class AgentSession {
11670
11834
  : undefined;
11671
11835
  const commandContent = command
11672
11836
  ? [{ type: "content" as const, content: { type: "text" as const, text: `$ ${command}` } }]
11673
- : undefined;
11837
+ : target.name === "github"
11838
+ ? [{ type: "content" as const, content: { type: "text" as const, text: permissionIntent.title } }]
11839
+ : undefined;
11674
11840
  if (this.#sdkPermissionMode === "allow") {
11675
11841
  return await target.execute(toolCallId, args as never, signal, onUpdate, ctx);
11676
11842
  }
@@ -11783,7 +11949,7 @@ export class AgentSession {
11783
11949
  /**
11784
11950
  /** Wrap a tool with the workflow mutation guard before permissions or execution. */
11785
11951
  #wrapToolForWorkflowMutationGuard<T extends AgentTool>(tool: T): T {
11786
- if (!["edit", "write", "ast_edit", "bash"].includes(tool.name)) return tool;
11952
+ if (!isWorkflowMutationTool(tool.name)) return tool;
11787
11953
  return new Proxy(tool, {
11788
11954
  get: (target, prop) => {
11789
11955
  if (prop !== "execute") return Reflect.get(target, prop, target);
@@ -12357,7 +12523,9 @@ export class AgentSession {
12357
12523
  */
12358
12524
  get messages(): AgentMessage[] {
12359
12525
  const messages = this.agent.state.messages;
12360
- return messages.some(message => message.role === "custom" && this.#isEphemeralCustomMessageType(message.customType))
12526
+ return messages.some(
12527
+ message => message.role === "custom" && this.#isEphemeralCustomMessageType(message.customType),
12528
+ )
12361
12529
  ? this.#withoutEphemeralCustomMessages(messages)
12362
12530
  : messages;
12363
12531
  }
@@ -13498,6 +13666,36 @@ export class AgentSession {
13498
13666
  }
13499
13667
  }
13500
13668
 
13669
+ /**
13670
+ * The loaded skill a prompt's text dispatches to, or undefined when `prompt`
13671
+ * would submit it as ordinary text (unknown or disabled `/skill:` names,
13672
+ * chains, or attached images). Single source for dispatch and for callers
13673
+ * that must mirror it, such as startup title seeding.
13674
+ */
13675
+ resolvePromptSkillInvocation(text: string, images?: readonly unknown[]): ParsedSkillInvocation | undefined {
13676
+ if (images?.length || !text.startsWith("/") || !isNamespacedSkillSlashCommandName(text.slice(1))) return;
13677
+ const skillCommands = new Map(this.skills.map(skill => [getSkillSlashCommandName(skill), skill]));
13678
+ const invocations = parseSkillInvocations(text, skillCommands);
13679
+ return invocations.length === 1 ? invocations[0] : undefined;
13680
+ }
13681
+
13682
+ /**
13683
+ * Whether `prompt` intercepts this slash text as an extension or custom/MCP
13684
+ * command. Used to keep command arguments away from the title model. Mirrors the
13685
+ * command lookup in `#tryExecuteExtensionCommand` / `#tryExecuteCustomCommand`
13686
+ * without executing anything.
13687
+ */
13688
+ isLocallyHandledSlashCommand(text: string): boolean {
13689
+ if (!text.startsWith("/")) return false;
13690
+ const spaceIndex = text.indexOf(" ");
13691
+ const commandName = spaceIndex === -1 ? text.slice(1) : text.slice(1, spaceIndex);
13692
+ return (
13693
+ this.#extensionRunner?.getCommand(commandName) !== undefined ||
13694
+ this.#customCommands.some(c => c.command.name === commandName) ||
13695
+ this.#mcpPromptCommands.some(c => c.command.name === commandName)
13696
+ );
13697
+ }
13698
+
13501
13699
  async #promptInternal(
13502
13700
  text: string,
13503
13701
  options: PromptOptions | undefined,
@@ -13519,38 +13717,29 @@ export class AgentSession {
13519
13717
  if (owner && !owner.released) throw this.#sessionAdmissionBusyError();
13520
13718
  const expandPromptTemplates = options?.expandPromptTemplates ?? true;
13521
13719
 
13522
- if (
13523
- expandPromptTemplates &&
13524
- text.startsWith("/") &&
13525
- isNamespacedSkillSlashCommandName(text.slice(1)) &&
13526
- !options?.images?.length
13527
- ) {
13528
- const skillCommands = new Map(this.skills.map(skill => [getSkillSlashCommandName(skill), skill]));
13529
- const invocations = parseSkillInvocations(text, skillCommands);
13530
- if (invocations.length === 1) {
13531
- const invocation = invocations[0];
13532
- if (invocation) {
13533
- await this.invokeSkill(
13534
- invocation.skill.name,
13535
- invocation.args,
13536
- options?.onPreflightAccepted ||
13537
- options?.onPreflightAcceptCommit ||
13538
- options?.preflightSignal ||
13539
- internalOptions?.sdkRunToken
13540
- ? {
13541
- ...(options?.onPreflightAccepted
13542
- ? { onPreflightAccepted: options.onPreflightAccepted }
13543
- : {}),
13544
- ...(options?.onPreflightAcceptCommit
13545
- ? { onPreflightAcceptCommit: options.onPreflightAcceptCommit }
13546
- : {}),
13547
- ...(options?.preflightSignal ? { preflightSignal: options.preflightSignal } : {}),
13548
- ...(options?.sdkRunCapability ? { sdkRunCapability: options.sdkRunCapability } : {}),
13549
- }
13550
- : undefined,
13551
- );
13552
- return;
13553
- }
13720
+ if (expandPromptTemplates) {
13721
+ const invocation = this.resolvePromptSkillInvocation(text, options?.images);
13722
+ if (invocation) {
13723
+ await this.invokeSkill(
13724
+ invocation.skill.name,
13725
+ invocation.args,
13726
+ options?.onPreflightAccepted ||
13727
+ options?.onPreflightAcceptCommit ||
13728
+ options?.preflightSignal ||
13729
+ internalOptions?.sdkRunToken
13730
+ ? {
13731
+ ...(options?.onPreflightAccepted
13732
+ ? { onPreflightAccepted: options.onPreflightAccepted }
13733
+ : {}),
13734
+ ...(options?.onPreflightAcceptCommit
13735
+ ? { onPreflightAcceptCommit: options.onPreflightAcceptCommit }
13736
+ : {}),
13737
+ ...(options?.preflightSignal ? { preflightSignal: options.preflightSignal } : {}),
13738
+ ...(options?.sdkRunCapability ? { sdkRunCapability: options.sdkRunCapability } : {}),
13739
+ }
13740
+ : undefined,
13741
+ );
13742
+ return;
13554
13743
  }
13555
13744
  }
13556
13745
 
@@ -13967,6 +14156,8 @@ export class AgentSession {
13967
14156
  | "preflightSignal"
13968
14157
  > & {
13969
14158
  sdkRunToken?: string;
14159
+ sdkRunTokens?: string[];
14160
+ sdkPredecessorScope?: AttemptScope;
13970
14161
  prependMessages?: AgentMessage[];
13971
14162
  skipPostPromptRecoveryWait?: boolean;
13972
14163
  predecessorAgentEndHold?: symbol;
@@ -14037,6 +14228,7 @@ export class AgentSession {
14037
14228
  let skipPostPromptRecoveryWait =
14038
14229
  options?.skipPostPromptRecoveryWait === true || options?.sdkRunToken !== undefined;
14039
14230
  let promptAttemptScope: AttemptScope | undefined;
14231
+ let promptSdkPublication: Promise<void> | undefined;
14040
14232
  let hindsightRecall: string | undefined;
14041
14233
  try {
14042
14234
  this.#throwIfPromptPreflightCancelled(generation, preflightSignal);
@@ -14337,10 +14529,26 @@ export class AgentSession {
14337
14529
  ...(options?.sdkRunToken ? { sdkRunToken: options.sdkRunToken } : {}),
14338
14530
  onRunAccepted: (handle: AttemptRunHandle) => {
14339
14531
  promptAttemptScope = handle?.scope;
14340
- if (handle) this.#acceptSdkAttemptRun(handle, options?.sdkRunToken);
14532
+ // A synthetic recovery prompt can make Agent retire the old managed
14533
+ // run at method entry. Suppress only that captured predecessor once
14534
+ // the replacement accepts; pre-acceptance failures still publish it.
14535
+ if (handle && options?.sdkPredecessorScope) {
14536
+ const predecessor = this.#claimDeferredAgentEndForContinuation(
14537
+ this.#reserveDeferredAgentEndForContinuation(options.sdkPredecessorScope),
14538
+ );
14539
+ this.#releaseDeferredAgentEndLease(predecessor);
14540
+ }
14541
+ if (handle)
14542
+ this.#acceptSdkAttemptRun(
14543
+ handle,
14544
+ options?.sdkRunToken,
14545
+ options?.sdkRunTokens,
14546
+ options?.sdkPredecessorScope,
14547
+ );
14341
14548
  else this.#acceptRunHandle(handle);
14342
14549
  if (handle && options?.sdkRunToken) {
14343
14550
  this.#activeSdkRunToken = options.sdkRunToken;
14551
+ promptSdkPublication = this.#sdkTerminalPublications.get(options.sdkRunToken)?.promise;
14344
14552
  }
14345
14553
  options?.onRunAccepted?.(handle);
14346
14554
  options?.admissionLease?.release();
@@ -14423,7 +14631,11 @@ export class AgentSession {
14423
14631
  this.#releaseIrcRosterClaim(rosterClaim.token, rosterClaim.epoch);
14424
14632
  }
14425
14633
  this.#releaseDeferredAgentEndContinuation(predecessorAgentEndHold);
14426
- await this.#settleEndedInFlight(inFlightPrompt, skipPostPromptRecoveryWait ? "publication" : "full");
14634
+ await this.#settleEndedInFlight(
14635
+ inFlightPrompt,
14636
+ skipPostPromptRecoveryWait ? "publication" : "full",
14637
+ promptSdkPublication,
14638
+ );
14427
14639
  }
14428
14640
  }
14429
14641
 
@@ -14711,10 +14923,7 @@ export class AgentSession {
14711
14923
  // prompt still in preflight, aborting) has no run to steer. Ordinary steers
14712
14924
  // become a sequential follow-up owned by the next turn; SDK turn-bound
14713
14925
  // steers fail closed rather than being delivered to a successor run.
14714
- if (
14715
- options?.expectedSdkRunToken !== undefined &&
14716
- this.#activeSdkRunToken !== options.expectedSdkRunToken
14717
- )
14926
+ if (options?.expectedSdkRunToken !== undefined && this.#activeSdkRunToken !== options.expectedSdkRunToken)
14718
14927
  throw Object.assign(new Error("The expected SDK run is not active."), { code: "turn_not_active" });
14719
14928
  const admission = this.agent.steer(message, options?.forceOneAtATime ? { forceOneAtATime: true } : undefined);
14720
14929
  if (!admission.admitted) {
@@ -14771,8 +14980,8 @@ export class AgentSession {
14771
14980
  /**
14772
14981
  * One-shot preflight-abort binding: an invocation cancelled after its queue
14773
14982
  * admission must cancel the submission it admitted, so an aborted dispatch
14774
- * can never execute later. Both explicit delivery paths (follow-up and
14775
- * steer) share this helper so their cancellation semantics cannot diverge
14983
+ * can never execute later. Explicit and implicitly diverted queue admissions
14984
+ * share this helper so their cancellation semantics cannot diverge
14776
14985
  * (exact-head review P1).
14777
14986
  */
14778
14987
  #bindPreflightAbortCancellation(
@@ -15453,6 +15662,7 @@ export class AgentSession {
15453
15662
  forceOneAtATime: sequential !== undefined,
15454
15663
  createDisplayEntry: false,
15455
15664
  trackExternalFollowUp: false,
15665
+ sdkRunToken: this.#activeSdkRunToken,
15456
15666
  });
15457
15667
  if (this.#abortUnwind && !sequential) this.#abortUnwindSteerFallbacks.push(appMessage);
15458
15668
  // The chip now describes follow-up work: keep its mode and identity
@@ -15483,6 +15693,7 @@ export class AgentSession {
15483
15693
  forceOneAtATime: options?.followUpQueuePolicy === "sequential",
15484
15694
  createDisplayEntry: false,
15485
15695
  trackExternalFollowUp: false,
15696
+ sdkRunToken: this.#activeSdkRunToken,
15486
15697
  });
15487
15698
  this.#bindCustomDisplayEntry(appMessage, "followUp");
15488
15699
  // The session can report streaming while no agent loop owns the queue
@@ -15904,12 +16115,13 @@ export class AgentSession {
15904
16115
  if (this.#isLiveTurnBusy() && !waitedForAbortUnwind) {
15905
16116
  if (options?.onPreflightAcceptCommit) await options.onPreflightAcceptCommit();
15906
16117
  assertPreflightStillOpen();
15907
- await this.#queueSteer(text, images, {
16118
+ const queuedSteer = await this.#queueSteer(text, images, {
15908
16119
  claimsGenuineUserIntent: true,
15909
- onPromoted: options?.onQueuedPromoted,
16120
+ onPromoted: onQueuedPromoted,
15910
16121
  external: true,
15911
16122
  sdkRunToken: internalOptions?.sdkRunToken,
15912
16123
  });
16124
+ this.#bindPreflightAbortCancellation(options?.preflightSignal, queuedSteer);
15913
16125
  // Dispatch-race disposition (#4668 review P1): the SDK snapshot-decided
15914
16126
  // this submission starts its own turn (idle at dispatch), but the
15915
16127
  // session began streaming before sendUserMessage ran, so the message
@@ -15919,8 +16131,15 @@ export class AgentSession {
15919
16131
  // would terminalize the accepted request as an own-run completion
15920
16132
  // before it is consumed. Report the internal in-run disposition so the
15921
16133
  // runtime attaches the correlation to the in-flight run instead.
15922
- options?.onDispatchDisposition?.({ startsOwnRun: false });
15923
- options?.onPreflightAccepted?.();
16134
+ try {
16135
+ assertPreflightStillOpen();
16136
+ options?.onDispatchDisposition?.({ startsOwnRun: false });
16137
+ options?.onPreflightAccepted?.();
16138
+ assertPreflightStillOpen();
16139
+ } catch (error) {
16140
+ queuedSteer.cancel();
16141
+ throw error;
16142
+ }
15924
16143
  return;
15925
16144
  }
15926
16145
 
@@ -16851,6 +17070,12 @@ export class AgentSession {
16851
17070
  if (!lineageIdHash) return undefined;
16852
17071
  return this.#promptGeneration;
16853
17072
  }
17073
+ getTerminalRunOwnerForEvent(event: object): AgentTerminalOwnerContext | undefined {
17074
+ return this.#terminalOwnerByExtensionEvent.get(event);
17075
+ }
17076
+ getRunOwnerDomain(handle: string): RunCancellationDomain | undefined {
17077
+ return this.agent.resourceLedger.lookupDomain(handle);
17078
+ }
16854
17079
  /**
16855
17080
  * Logical endpoint used to key owned-registration lineage bindings.
16856
17081
  * Subagents inherit the parent's AsyncJobManager, whose completion callback
@@ -18009,8 +18234,12 @@ export class AgentSession {
18009
18234
  }
18010
18235
 
18011
18236
  options?.onMutationStarted?.();
18012
- this.#setModelAuthoritatively(model, options?.cause ?? "user-selection");
18237
+ const cause = options?.cause ?? "user-selection";
18238
+ if (cause === "user-selection") this.markUserModelSelection();
18239
+ this.#setModelAuthoritatively(model, cause);
18240
+ if (cause === "user-selection") this.#unavailableModelProfile = undefined;
18013
18241
  this.#seedSessionCanonicalVariant(model);
18242
+ if (cause === "user-selection") this.#recordUserCanonicalVariantSelection();
18014
18243
  this.sessionManager.appendModelChange(`${model.provider}/${model.id}`, role);
18015
18244
  this.settings.setModelRole(
18016
18245
  role,
@@ -18018,7 +18247,7 @@ export class AgentSession {
18018
18247
  );
18019
18248
  // Only an explicit user selection starts a new fallback epoch. Internal
18020
18249
  // fallback switches must preserve the exhausted-model set while advancing.
18021
- if (role === "default" && (options?.cause ?? "user-selection") === "user-selection") {
18250
+ if (role === "default" && cause === "user-selection") {
18022
18251
  this.#fallbackTransitionGeneration++;
18023
18252
  this.#defaultFallbackController = undefined;
18024
18253
  this.#defaultFallbackExhaustedLastTurn = false;
@@ -18055,12 +18284,41 @@ export class AgentSession {
18055
18284
 
18056
18285
  setActiveModelProfile(name: string | undefined): void {
18057
18286
  this.#activeModelProfile = name;
18287
+ if (name !== undefined) {
18288
+ this.#unavailableModelProfile = undefined;
18289
+ }
18058
18290
  }
18059
18291
 
18060
18292
  getActiveModelProfile(): string | undefined {
18061
18293
  return this.#activeModelProfile;
18062
18294
  }
18063
18295
 
18296
+ setUnavailableModelProfile(name: string | undefined): void {
18297
+ this.#unavailableModelProfile = name;
18298
+ }
18299
+
18300
+ getUnavailableModelProfile(): string | undefined {
18301
+ return this.#unavailableModelProfile;
18302
+ }
18303
+
18304
+ /** Revision fence for deferred activation after a user model selection. */
18305
+ getUserModelSelectionRevision(): number {
18306
+ return this.#userModelSelectionRevision;
18307
+ }
18308
+
18309
+ /** Latest concrete user choice that seeded or cleared the session's sticky canonical variant. */
18310
+ getUserCanonicalVariantSelection(): { revision: number; canonicalVariant: string | undefined } {
18311
+ return {
18312
+ revision: this.#userCanonicalVariantSelectionRevision,
18313
+ canonicalVariant: this.#userCanonicalVariantSelection,
18314
+ };
18315
+ }
18316
+
18317
+ /** Fence deferred startup profile recovery before an explicit control-surface selection. */
18318
+ markUserModelSelection(): void {
18319
+ this.#userModelSelectionRevision++;
18320
+ }
18321
+
18064
18322
  /**
18065
18323
  * Re-apply vendor-separated delegation after a profile activation changed the
18066
18324
  * role layer. `eagerTasks` is resolved at prompt build time, but under
@@ -18222,12 +18480,15 @@ export class AgentSession {
18222
18480
  * Session-scoped only: does not persist `modelProfile.default`.
18223
18481
  */
18224
18482
  async activateModelProfileForControl(profileName: string): Promise<boolean> {
18225
- await activateModelProfile({
18226
- session: this,
18227
- modelRegistry: this.#modelRegistry,
18228
- settings: this.settings,
18229
- profileName,
18230
- });
18483
+ this.markUserModelSelection();
18484
+ await this.withSdkControlMutation(() =>
18485
+ activateModelProfile({
18486
+ session: this,
18487
+ modelRegistry: this.#modelRegistry,
18488
+ settings: this.settings,
18489
+ profileName,
18490
+ }),
18491
+ );
18231
18492
  return this.getActiveModelProfile() === profileName;
18232
18493
  }
18233
18494
 
@@ -18262,6 +18523,7 @@ export class AgentSession {
18262
18523
  onAfterActivation?: () => void;
18263
18524
  },
18264
18525
  ): Promise<{ changed: boolean; id: string }> {
18526
+ this.markUserModelSelection();
18265
18527
  // Do not hold selection admission while waiting for a scheduled continuation:
18266
18528
  // the continuation may need prompt admission to settle the current turn.
18267
18529
  await this.waitForIdle();
@@ -18540,6 +18802,7 @@ export class AgentSession {
18540
18802
  reason?: TemporaryModelReason;
18541
18803
  providerSessionScope?: TemporaryProviderSessionScope;
18542
18804
  signal?: AbortSignal;
18805
+ shouldMutate?: () => boolean;
18543
18806
  onMutationStarted?: () => void;
18544
18807
  },
18545
18808
  // biome-ignore lint/suspicious/noConfusingVoidType: Existing session adapters return Promise<void>; a scope is optional.
@@ -18558,6 +18821,8 @@ export class AgentSession {
18558
18821
  throw new Error(`No API key for ${model.provider}/${model.id}`);
18559
18822
  }
18560
18823
  if (suppliedScope && this.#temporaryProviderSessionScopes.at(-1)?.token !== suppliedScope) return;
18824
+ if (options?.shouldMutate && !options.shouldMutate()) return;
18825
+ if (options?.cause === "user-selection") this.markUserModelSelection();
18561
18826
  options?.onMutationStarted?.();
18562
18827
 
18563
18828
  const isTemporaryOperation = options?.cause === undefined || options.cause === "temporary-operation";
@@ -18584,6 +18849,9 @@ export class AgentSession {
18584
18849
  } else {
18585
18850
  this.#setModelAuthoritatively(model, options?.cause ?? "temporary-operation");
18586
18851
  }
18852
+ if (options?.cause === "user-selection") {
18853
+ this.#unavailableModelProfile = undefined;
18854
+ }
18587
18855
  this.sessionManager.appendModelChange(
18588
18856
  `${model.provider}/${model.id}`,
18589
18857
  options?.persistAsSessionDefault ? "default" : "temporary",
@@ -18591,12 +18859,36 @@ export class AgentSession {
18591
18859
  this.settings.getStorage()?.recordModelUsage(`${model.provider}/${model.id}`);
18592
18860
  if (options?.persistAsSessionDefault) {
18593
18861
  this.#seedSessionCanonicalVariant(model);
18862
+ if (options.cause === "user-selection") this.#recordUserCanonicalVariantSelection();
18594
18863
  }
18595
18864
 
18596
18865
  // Apply explicit thinking level if given; otherwise prefer the model's
18597
18866
  // configured defaultLevel; otherwise re-clamp the current level.
18598
18867
  this.setThinkingLevel(thinkingLevel ?? model.thinking?.defaultLevel ?? this.thinkingLevel);
18599
- if (options?.persistAsSessionDefault === true) this.#clearActiveModelProfileForConcreteDefault(options?.cause);
18868
+ if (options?.persistAsSessionDefault === true && options.cause !== "profile-activation") {
18869
+ // Concrete model selection clears session-scoped profile state (#5919).
18870
+ // Materialize durable profiles first (if persisted and active), then reset
18871
+ // without force to preserve durable semantics.
18872
+ if (this.model && this.settings.get("modelProfile.default") !== undefined) {
18873
+ this.materializeActiveDefaultModelProfileAssignment(this.model);
18874
+ }
18875
+ this.#resetSessionScopedModelProfileState({ preserveDefaultConfiguredChain: true });
18876
+ // For user-selection and startup-override causes, also clear stale persisted
18877
+ // defaults via the legacy path when there is no ownership record.
18878
+ if (options.cause === "user-selection" || options.cause === "startup-override") {
18879
+ const ownership = readDurableModelProfileOwnership(this.settings);
18880
+ if (ownership.version === 0) {
18881
+ this.#clearActiveModelProfileForConcreteDefault(options.cause);
18882
+ }
18883
+ }
18884
+ const origin = options.cause === "startup-override" ? "startup-override" : "model_selection";
18885
+ const effectiveLevel = thinkingLevel ?? model.thinking?.defaultLevel ?? this.thinkingLevel;
18886
+ this.setConfiguredModelChain(
18887
+ "default",
18888
+ [formatModelSelectorValue(`${model.provider}/${model.id}`, effectiveLevel)],
18889
+ origin,
18890
+ );
18891
+ }
18600
18892
  await this.#syncEditToolModeAfterModelChange(previousEditMode);
18601
18893
  } catch (error) {
18602
18894
  if (ownsScope) await this.restoreTemporaryProviderSessionScope(scope);
@@ -18687,8 +18979,15 @@ export class AgentSession {
18687
18979
 
18688
18980
  #publishDefaultModelSelection(model: Model, thinkingLevel: ThinkingLevel, systemPrompt: string[] | undefined): void {
18689
18981
  this.#clearActiveRetryFallback();
18982
+ // Concrete model selection clears session-scoped profile state (#5919).
18983
+ // IMPORTANT: This is called AFTER durable persistence completes, so the
18984
+ // session-scoped updates occur during the promotion phase after ownership
18985
+ // is committed. Session ownership is captured in the promotion logic above.
18986
+ this.#resetSessionScopedModelProfileState({ preserveDefaultConfiguredChain: true });
18987
+ this.markUserModelSelection();
18690
18988
  this.#setModelWithProviderSessionReset(model);
18691
18989
  this.#seedSessionCanonicalVariant(model);
18990
+ this.#recordUserCanonicalVariantSelection();
18692
18991
  const thinkingLevelChanged = this.#thinkingLevel !== thinkingLevel;
18693
18992
  this.#thinkingLevelMutationRevision++;
18694
18993
  this.#thinkingLevelLiveMutationRevision++;
@@ -18698,17 +18997,21 @@ export class AgentSession {
18698
18997
  this.#pendingThinkingVisibilityControlFailure = undefined;
18699
18998
  this.#thinkingLevel = thinkingLevel;
18700
18999
  this.agent.setThinkingLevel(toReasoningEffort(thinkingLevel));
19000
+ // Materialize durable profiles after model and thinking level installation
19001
+ // so the persisted default selector matches both the newly installed model
19002
+ // and the selected thinking level.
19003
+ if (this.model && this.settings.get("modelProfile.default") !== undefined) {
19004
+ this.materializeActiveDefaultModelProfileAssignment(this.model);
19005
+ }
18701
19006
  if (thinkingLevelChanged) {
18702
19007
  const event: AgentSessionEvent = { type: "thinking_level_changed", thinkingLevel };
18703
19008
  for (const listener of this.#eventListenerSnapshot) {
18704
- try {
18705
- listener(event);
18706
- } catch {
19009
+ deliverSessionEvent(listener, event, () => {
18707
19010
  logger.warn("Default model selection event listener failed", {
18708
19011
  code: "default_model_selection_listener_failed",
18709
19012
  disposition: "continue",
18710
19013
  });
18711
- }
19014
+ });
18712
19015
  }
18713
19016
  }
18714
19017
  this.#applyPreparedDefaultModelSelectionPrompt(systemPrompt);
@@ -18883,7 +19186,40 @@ export class AgentSession {
18883
19186
  });
18884
19187
  }
18885
19188
  }
18886
- this.#clearActiveModelProfileForConcreteDefault("user-selection");
19189
+ // Concrete model selection clears durable profile ownership (#5919).
19190
+ // If there is an existing ownership record (version > 0), clear it.
19191
+ // If there is no ownership record (version === 0), call the legacy path to
19192
+ // clear stale persisted profiles that no longer match the selection.
19193
+ // The ownership clear is attempted atomically with other model role updates.
19194
+ // If it fails due to concurrent access, we log and continue: the model
19195
+ // selection has been promoted and committed, so the concrete pick is durable.
19196
+ // A stale profile ownership marker will be resolved on the next session startup.
19197
+ const currentOwnership = readDurableModelProfileOwnership(this.settings);
19198
+ if (currentOwnership.version > 0) {
19199
+ try {
19200
+ await commitDurableModelProfileOwnership(this.settings, { kind: "cleared" });
19201
+ } catch (error) {
19202
+ if (error instanceof ModelProfileOwnershipConflictError) {
19203
+ logger.info("Model selection ownership clear deferred due to concurrent access", {
19204
+ code: "default_model_selection_ownership_conflict",
19205
+ disposition: "continue",
19206
+ expectedVersion: error.expectedVersion,
19207
+ actualVersion: error.actualVersion,
19208
+ });
19209
+ } else if (error instanceof InvalidModelProfileOwnershipError) {
19210
+ logger.warn("Invalid model profile ownership marker during selection", {
19211
+ code: "default_model_selection_ownership_invalid",
19212
+ disposition: "continue",
19213
+ });
19214
+ } else {
19215
+ throw error;
19216
+ }
19217
+ }
19218
+ } else {
19219
+ // Legacy path: no ownership record exists. Clear stale persisted defaults
19220
+ // that no longer match the concrete selection.
19221
+ this.#clearActiveModelProfileForConcreteDefault("user-selection");
19222
+ }
18887
19223
  options?.onAfterMutation?.();
18888
19224
  return { provider: model.provider, modelId: model.id, thinkingLevel: effectiveLevel };
18889
19225
  },
@@ -18989,6 +19325,7 @@ export class AgentSession {
18989
19325
  const next = roleModels[nextIndex];
18990
19326
 
18991
19327
  if (options?.temporary) {
19328
+ this.markUserModelSelection();
18992
19329
  await this.setModelTemporary(next.model, next.explicitThinkingLevel ? next.thinkingLevel : undefined, {
18993
19330
  cause: "temporary-operation",
18994
19331
  reason: "temporary-cycle",
@@ -20638,6 +20975,7 @@ export class AgentSession {
20638
20975
  onTerminalOverflowNoop?: () => void,
20639
20976
  resourceRunId?: string,
20640
20977
  ownershipSignal?: AbortSignal,
20978
+ sdkOwnership?: SdkContinuationOwnership,
20641
20979
  ): Promise<boolean> {
20642
20980
  // Safety stops are terminal and must not trigger context maintenance.
20643
20981
  if (
@@ -20664,7 +21002,28 @@ export class AgentSession {
20664
21002
  const compactionEntry = getLatestCompactionEntry(this.sessionManager.getBranch());
20665
21003
  const errorIsFromBeforeCompaction =
20666
21004
  compactionEntry !== null && assistantMessage.timestamp < new Date(compactionEntry.timestamp).getTime();
21005
+ // A successful empty stop with nonzero usage is not overflow evidence by usage
21006
+ // alone. Zero-usage empty stops stay on the legacy proxy-overflow path, and typed
21007
+ // overflow classification is preserved; only the empty-stop heuristic is disabled.
21008
+ const hasOverflowEvidenceBeyondEmptyStopHeuristic =
21009
+ assistantMessage.stopReason === "stop" &&
21010
+ assistantMessage.content.length === 0 &&
21011
+ classifyContextOverflow(
21012
+ { ...assistantMessage, content: [{ type: "text", text: "" }] },
21013
+ assistantMessage.transportFailure,
21014
+ contextWindow,
21015
+ );
21016
+ const emptyStopPromptTokens =
21017
+ assistantMessage.usage.input + assistantMessage.usage.cacheRead + assistantMessage.usage.cacheWrite;
21018
+ const successfulEmptyStop =
21019
+ assistantMessage.stopReason === "stop" &&
21020
+ assistantMessage.content.length === 0 &&
21021
+ (emptyStopPromptTokens + assistantMessage.usage.output > 0 ||
21022
+ assistantMessage.usage.totalTokens > 0) &&
21023
+ emptyStopPromptTokens <= contextWindow &&
21024
+ !hasOverflowEvidenceBeyondEmptyStopHeuristic;
20667
21025
  if (
21026
+ !successfulEmptyStop &&
20668
21027
  sameModel &&
20669
21028
  !errorIsFromBeforeCompaction &&
20670
21029
  classifyContextOverflow(assistantMessage, assistantMessage.transportFailure, contextWindow)
@@ -20692,6 +21051,7 @@ export class AgentSession {
20692
21051
  generation,
20693
21052
  suppressPredecessorAgentEnd: true,
20694
21053
  resourceRunId,
21054
+ sdkOwnership,
20695
21055
  });
20696
21056
 
20697
21057
  return true;
@@ -20710,10 +21070,11 @@ export class AgentSession {
20710
21070
  },
20711
21071
  resourceRunId,
20712
21072
  signal: ownershipSignal,
21073
+ sdkOwnership,
20713
21074
  });
20714
21075
  return "continuationScheduled" in status && status.continuationScheduled === true;
20715
21076
  }
20716
- return await this.#scheduleOverflowRetryContinuation(generation, resourceRunId);
21077
+ return await this.#scheduleOverflowRetryContinuation(generation, resourceRunId, sdkOwnership);
20717
21078
  }
20718
21079
  const compactionSettings = this.settings.getGroup("compaction");
20719
21080
  if (!compactionSettings.enabled || compactionSettings.strategy === "off") return false;
@@ -20759,7 +21120,11 @@ export class AgentSession {
20759
21120
  const promoted = await this.#tryContextPromotion(assistantMessage, ownershipSignal);
20760
21121
  if (ownershipSignal?.aborted) return false;
20761
21122
  if (!promoted) {
20762
- await this.#runAutoCompaction("threshold", false, false, { resourceRunId, signal: ownershipSignal });
21123
+ await this.#runAutoCompaction("threshold", false, false, {
21124
+ resourceRunId,
21125
+ signal: ownershipSignal,
21126
+ sdkOwnership,
21127
+ });
20763
21128
  }
20764
21129
  }
20765
21130
  return true;
@@ -21113,10 +21478,7 @@ export class AgentSession {
21113
21478
  const isContextPromoted = this.#temporaryProviderSessionScopes.some(
21114
21479
  scope => scope.token.reason === "context-promotion",
21115
21480
  );
21116
- if (
21117
- !isContextPromoted ||
21118
- !isDefaultAutoThresholdCeilingApplied(contextWindow, settings)
21119
- ) {
21481
+ if (!isContextPromoted || !isDefaultAutoThresholdCeilingApplied(contextWindow, settings)) {
21120
21482
  return settings;
21121
21483
  }
21122
21484
 
@@ -21525,7 +21887,9 @@ export class AgentSession {
21525
21887
  // The reminder continues the current prompt, so the predecessor `agent_end`
21526
21888
  // must stay held until the continuation turn produces the real terminal.
21527
21889
  // Publishing it here would settle the caller's prompt mid-reminder.
21528
- this.#scheduleAgentContinue({ skipCompactionCheck: true, suppressPredecessorAgentEnd: true });
21890
+ // Disable managed fallback to prevent indefinite retries for this server-initiated turn.
21891
+ this.#todoReminderContinuationGeneration = this.#promptGeneration;
21892
+ this.#scheduleAgentContinue({ skipCompactionCheck: true, suppressPredecessorAgentEnd: true, disableManagedFallback: true });
21529
21893
  }
21530
21894
 
21531
21895
  /**
@@ -21639,6 +22003,11 @@ export class AgentSession {
21639
22003
  }
21640
22004
  }
21641
22005
 
22006
+ #recordUserCanonicalVariantSelection(): void {
22007
+ this.#userCanonicalVariantSelectionRevision++;
22008
+ this.#userCanonicalVariantSelection = this.#modelRegistry.getSessionCanonicalVariant?.(this.sessionId);
22009
+ }
22010
+
21642
22011
  #closeCodexProviderSessionsForHistoryRewrite(): void {
21643
22012
  const currentModel = this.model;
21644
22013
  if (currentModel?.api !== "openai-codex-responses") return;
@@ -22242,6 +22611,7 @@ export class AgentSession {
22242
22611
  signal?: AbortSignal;
22243
22612
  beforeTerminalOverflowNoop?: () => void;
22244
22613
  resourceRunId?: string;
22614
+ sdkOwnership?: SdkContinuationOwnership;
22245
22615
  },
22246
22616
  ): Promise<AutoCompactionTerminalStatus> {
22247
22617
  if (this.#isDisposed || this.#sessionAdmissionClosing) return Promise.resolve({ kind: "skipped" });
@@ -22262,6 +22632,7 @@ export class AgentSession {
22262
22632
  signal?: AbortSignal;
22263
22633
  beforeTerminalOverflowNoop?: () => void;
22264
22634
  resourceRunId?: string;
22635
+ sdkOwnership?: SdkContinuationOwnership;
22265
22636
  },
22266
22637
  ): Promise<AutoCompactionTerminalStatus> {
22267
22638
  const compactionSettings = this.settings.getGroup("compaction");
@@ -22404,7 +22775,14 @@ export class AgentSession {
22404
22775
  });
22405
22776
  if (autoCompactionSignal.aborted) return { kind: "aborted", source: "signal" };
22406
22777
  if (continueAfterMaintenance && reason !== "idle" && compactionSettings.autoContinue !== false) {
22407
- this.#scheduleAutoContinuePrompt(generation, true, options?.resourceRunId);
22778
+ this.#scheduleAutoContinuePrompt(
22779
+ generation,
22780
+ true,
22781
+ options?.resourceRunId,
22782
+ undefined,
22783
+ undefined,
22784
+ options?.sdkOwnership,
22785
+ );
22408
22786
  }
22409
22787
 
22410
22788
  if (autoCompactionSignal.aborted) return { kind: "aborted", source: "signal" };
@@ -22459,7 +22837,11 @@ export class AgentSession {
22459
22837
  }
22460
22838
  const overflowContinuationScheduled =
22461
22839
  !overflowNoopWouldReplay && willRetry && !continuationSkipReason
22462
- ? await this.#scheduleOverflowRetryContinuation(generation, options?.resourceRunId)
22840
+ ? await this.#scheduleOverflowRetryContinuation(
22841
+ generation,
22842
+ options?.resourceRunId,
22843
+ options?.sdkOwnership,
22844
+ )
22463
22845
  : false;
22464
22846
  await this.#emitSessionEvent({
22465
22847
  type: "auto_compaction_end",
@@ -22509,7 +22891,14 @@ export class AgentSession {
22509
22891
  resourceRunId: options?.resourceRunId,
22510
22892
  });
22511
22893
  } else if (continueAfterMaintenance && reason !== "idle" && compactionSettings.autoContinue !== false) {
22512
- this.#scheduleAutoContinuePrompt(generation, true, options?.resourceRunId);
22894
+ this.#scheduleAutoContinuePrompt(
22895
+ generation,
22896
+ true,
22897
+ options?.resourceRunId,
22898
+ undefined,
22899
+ undefined,
22900
+ options?.sdkOwnership,
22901
+ );
22513
22902
  }
22514
22903
  return { kind: "skipped" };
22515
22904
  }
@@ -22794,7 +23183,11 @@ export class AgentSession {
22794
23183
  }
22795
23184
  const overflowContinuationScheduled =
22796
23185
  willRetry && !continuationSkipReason
22797
- ? await this.#scheduleOverflowRetryContinuation(generation, options?.resourceRunId)
23186
+ ? await this.#scheduleOverflowRetryContinuation(
23187
+ generation,
23188
+ options?.resourceRunId,
23189
+ options?.sdkOwnership,
23190
+ )
22798
23191
  : false;
22799
23192
 
22800
23193
  await this.#emitSessionEvent({
@@ -22828,7 +23221,14 @@ export class AgentSession {
22828
23221
  resourceRunId: options?.resourceRunId,
22829
23222
  });
22830
23223
  } else if (continueAfterMaintenance && reason !== "idle" && compactionSettings.autoContinue !== false) {
22831
- this.#scheduleAutoContinuePrompt(generation, true, options?.resourceRunId);
23224
+ this.#scheduleAutoContinuePrompt(
23225
+ generation,
23226
+ true,
23227
+ options?.resourceRunId,
23228
+ undefined,
23229
+ undefined,
23230
+ options?.sdkOwnership,
23231
+ );
22832
23232
  }
22833
23233
  return { kind: "compacted" };
22834
23234
  } catch (error) {
@@ -23021,7 +23421,8 @@ export class AgentSession {
23021
23421
  isAuthenticated(apiKey) &&
23022
23422
  authStorage.getSessionCredentialRowId(model.provider, this.credentialSessionId) === credential.id &&
23023
23423
  authStorage.getSessionCredentialType(model.provider, this.credentialSessionId) === credentialKind &&
23024
- currentCredential && !currentCredential.disabled
23424
+ currentCredential &&
23425
+ !currentCredential.disabled
23025
23426
  )
23026
23427
  return apiKey;
23027
23428
  }
@@ -23191,6 +23592,7 @@ export class AgentSession {
23191
23592
  if (message.errorKind === "local_snapshot_failure") return "local_snapshot";
23192
23593
  if (message.errorKind === "local_buffer_overflow") return "local_buffer_overflow";
23193
23594
  if (this.#isTypedFirstEventTimeout(message)) return "first_event_timeout";
23595
+ if (message.errorKind === "local_empty_response") return "empty_response";
23194
23596
  if (this.#isTypedEmptyResponse(message)) return "empty_response";
23195
23597
  if (this.#isCodexCredentialModelUnavailable(message)) {
23196
23598
  return this.#canRotateCodexCredential(message) ? "unknown" : "terminal";
@@ -23315,6 +23717,7 @@ export class AgentSession {
23315
23717
 
23316
23718
  #managedFallbackPromptOptions(): {
23317
23719
  fallbackManaged?: boolean;
23720
+ contextOverflowManaged?: boolean;
23318
23721
  nextFallbackAttempt?: (model: Model) => FallbackAttemptToken;
23319
23722
  onManagedAttemptAccepted?: () => void;
23320
23723
  onManagedAttemptOutcome?: (
@@ -23322,9 +23725,10 @@ export class AgentSession {
23322
23725
  ) => ManagedAttemptDecision | Promise<ManagedAttemptDecision>;
23323
23726
  } {
23324
23727
  const controller = this.#defaultFallbackChain();
23325
- if (controller.chain.entries.length < 2) return {};
23728
+ if (controller.chain.entries.length < 2) return { contextOverflowManaged: true };
23326
23729
  return {
23327
23730
  fallbackManaged: true,
23731
+ contextOverflowManaged: true,
23328
23732
  nextFallbackAttempt: model => {
23329
23733
  controller.onAttemptStarted();
23330
23734
  this.#managedFallbackProviderAttemptCount++;
@@ -23445,6 +23849,7 @@ export class AgentSession {
23445
23849
  }
23446
23850
 
23447
23851
  async #handleManagedAttemptOutcome(outcome: ManagedAttemptOutcome): Promise<ManagedAttemptDecision> {
23852
+ const sdkOwnership = this.#captureSdkContinuationOwnership(outcome.scope);
23448
23853
  const attemptAbortEpoch = this.#abortAdmissionEpoch;
23449
23854
  const attemptCancelled = () =>
23450
23855
  this.#isDisposed || this.#sessionAdmissionClosing || this.#abortAdmissionEpoch !== attemptAbortEpoch;
@@ -23494,6 +23899,8 @@ export class AgentSession {
23494
23899
  return {
23495
23900
  type: "retry",
23496
23901
  continuation: async ownership => {
23902
+ const continuationSdkOwnership =
23903
+ sdkOwnership ?? this.#captureSdkContinuationOwnership(ownership.handle.scope);
23497
23904
  const restoreOwnedTransition = async () => {
23498
23905
  if (this.#fallbackTransitionGeneration !== transitionGeneration) return;
23499
23906
  this.#fallbackTransitionGeneration++;
@@ -23580,7 +23987,12 @@ export class AgentSession {
23580
23987
  ...this.#managedFallbackPromptOptions(),
23581
23988
  transientRecoveryMessage: this.#escapedNonAsciiRecoveryMessage(),
23582
23989
  onRunAccepted: (handle: AttemptRunHandle) => {
23583
- this.#acceptSdkAttemptRun(handle, this.#activeSdkRunToken);
23990
+ this.#acceptSdkAttemptRun(
23991
+ handle,
23992
+ continuationSdkOwnership?.token,
23993
+ continuationSdkOwnership?.cohort,
23994
+ continuationSdkOwnership?.scope,
23995
+ );
23584
23996
  },
23585
23997
  });
23586
23998
  if (
@@ -23619,11 +24031,18 @@ export class AgentSession {
23619
24031
  type: "retry",
23620
24032
  continuation: async ownership => {
23621
24033
  if (attemptCancelled() || !ownership.isCurrent() || ownership.lease.signal.aborted) return;
24034
+ const continuationSdkOwnership =
24035
+ sdkOwnership ?? this.#captureSdkContinuationOwnership(ownership.handle.scope);
23622
24036
  await this.agent.continue({
23623
24037
  ...this.#managedFallbackPromptOptions(),
23624
24038
  transientRecoveryMessage: this.#escapedNonAsciiRecoveryMessage(),
23625
24039
  onRunAccepted: (handle: AttemptRunHandle) => {
23626
- this.#acceptSdkAttemptRun(handle, this.#activeSdkRunToken);
24040
+ this.#acceptSdkAttemptRun(
24041
+ handle,
24042
+ continuationSdkOwnership?.token,
24043
+ continuationSdkOwnership?.cohort,
24044
+ continuationSdkOwnership?.scope,
24045
+ );
23627
24046
  },
23628
24047
  });
23629
24048
  },
@@ -23638,6 +24057,10 @@ export class AgentSession {
23638
24057
  type: "maintenance",
23639
24058
  continuation: async ownership => {
23640
24059
  if (attemptCancelled() || !ownership.isCurrent() || ownership.lease.signal.aborted) return;
24060
+ // A discarded in-loop attempt need not have published turn_start;
24061
+ // the Agent-supplied handle still identifies its accepted SDK cohort.
24062
+ const continuationSdkOwnership =
24063
+ sdkOwnership ?? this.#captureSdkContinuationOwnership(ownership.handle.scope);
23641
24064
  const resourceRunId = String(ownership.logicalRunId);
23642
24065
  const previousLease = this.#postPromptLeases.get(resourceRunId);
23643
24066
  this.#postPromptLeases.set(resourceRunId, ownership.lease);
@@ -23655,6 +24078,7 @@ export class AgentSession {
23655
24078
  },
23656
24079
  resourceRunId,
23657
24080
  ownership.lease.signal,
24081
+ continuationSdkOwnership,
23658
24082
  );
23659
24083
  if (
23660
24084
  terminalized ||
@@ -23689,12 +24113,14 @@ export class AgentSession {
23689
24113
  },
23690
24114
  };
23691
24115
  }
23692
- // Issue #5018 preserves managed behavior for the typed statusless
23693
- // Responses overload: before the code survived transport, this failure
23694
- // reached the session as an ordinary committed error, so the chain never
23695
- // discarded or advanced on it. Route it to the exhaustion decision
23696
- // directly, before the retryable path can classify its new facts.
23697
- if (isStatuslessTypedOverloadFacts(outcome.failure.transportFailure)) {
24116
+ // Content-free typed statusless overloads are safe for managed retry when
24117
+ // this discarded attempt is clean, even after earlier committed work in
24118
+ // the same prompt. Preserve the committed failure path when this attempt
24119
+ // itself is not replay-safe.
24120
+ if (
24121
+ isStatuslessTypedOverloadFacts(outcome.failure.transportFailure) &&
24122
+ !this.#isRetryScopeClean(outcome.scope)
24123
+ ) {
23698
24124
  this.#defaultFallbackChain().resetAttemptBudget();
23699
24125
  return this.#managedFallbackExhaustionDecision(
23700
24126
  outcome.failure.message,
@@ -23759,6 +24185,10 @@ export class AgentSession {
23759
24185
  if (classification === "transient" || classification === "first_event_timeout") {
23760
24186
  return { class: "server" };
23761
24187
  }
24188
+ // A locally promoted empty response is retryable only when the managed
24189
+ // attempt stayed clean; authorize the configured fallback chain without
24190
+ // fabricating provider transport facts for the runtime-owned failure.
24191
+ if (classification === "empty_response") return { class: "server" };
23762
24192
  if (classification === "unknown") return { class: "unknown" };
23763
24193
  return undefined;
23764
24194
  }
@@ -23938,9 +24368,7 @@ export class AgentSession {
23938
24368
  !authStorage.isCredentialAvailable(resolvedModel.provider, credential.id)
23939
24369
  )
23940
24370
  continue;
23941
- let credentialKeyResult:
23942
- | { kind: "resolved"; value: string | undefined }
23943
- | { kind: "aborted" };
24371
+ let credentialKeyResult: { kind: "resolved"; value: string | undefined } | { kind: "aborted" };
23944
24372
  try {
23945
24373
  credentialKeyResult = await awaitWithCancellation(
23946
24374
  this.#modelRegistry.getApiKey(resolvedModel, this.credentialSessionId, {
@@ -24038,7 +24466,11 @@ export class AgentSession {
24038
24466
  !transitionStillOwned()
24039
24467
  )
24040
24468
  return await rollbackCancelled();
24041
- if (selectedCredentialRowId !== undefined && selectedCredentialKind !== undefined && this.#restoreManagedFallbackGetApiKey) {
24469
+ if (
24470
+ selectedCredentialRowId !== undefined &&
24471
+ selectedCredentialKind !== undefined &&
24472
+ this.#restoreManagedFallbackGetApiKey
24473
+ ) {
24042
24474
  this.#managedFallbackNextCredentialOverride = {
24043
24475
  modelKey: canonicalModelKey,
24044
24476
  storageProvider: resolveOAuthStorageProvider(resolvedModel.provider),
@@ -24332,6 +24764,10 @@ export class AgentSession {
24332
24764
  scope?: AttemptScope,
24333
24765
  scopeWasClean = this.#isRetryScopeClean(scope),
24334
24766
  ): Promise<boolean | ManagedAttemptDecision> {
24767
+ // Capture the SDK owner before the failed attempt retires its active scope.
24768
+ // Retry/continuation terminals must remain attributed to the prompt that
24769
+ // admitted the provider call, even when the retry starts after agent_end.
24770
+ const retrySdkOwnership = this.#captureSdkContinuationOwnership(scope);
24335
24771
  const retryAbortEpoch = this.#abortAdmissionEpoch;
24336
24772
  const retryCancelled = () =>
24337
24773
  this.#isDisposed || this.#sessionAdmissionClosing || this.#abortAdmissionEpoch !== retryAbortEpoch;
@@ -24349,13 +24785,22 @@ export class AgentSession {
24349
24785
  // Managed outcomes can reach retry handling without the ordinary admission
24350
24786
  // check. Do not let retained transport facts bypass the same terminal veto.
24351
24787
  if (isTerminalCodexProviderFailure(message)) {
24352
- return managedOutcome
24353
- ? { type: "terminal", terminal: { stopReason: "error", messages: [message] } }
24354
- : false;
24788
+ return managedOutcome ? { type: "terminal", terminal: { stopReason: "error", messages: [message] } } : false;
24355
24789
  }
24356
24790
  if (retryCancelled()) {
24357
24791
  return managedOutcome ? { type: "terminal", terminal: { stopReason: "cancelled" } } : false;
24358
24792
  }
24793
+ if (
24794
+ transportFailure?.status === undefined &&
24795
+ transportFailure?.providerCode === SERVER_OVERLOADED_PROVIDER_CODE &&
24796
+ (transportFailure.openaiErrorCode === undefined ||
24797
+ transportFailure.openaiErrorCode === SERVER_OVERLOADED_PROVIDER_CODE) &&
24798
+ (managedOutcome ? !scopeWasClean : !this.#hasCleanRetryReplaySafety)
24799
+ ) {
24800
+ return managedOutcome
24801
+ ? { type: "terminal", terminal: { stopReason: "error", messages: [message] } }
24802
+ : false;
24803
+ }
24359
24804
  // A local machinery failure must never stay charged against the provider
24360
24805
  // fallback budget, no matter which local exit follows (disabled retry,
24361
24806
  // visible-content surface, bounded retry, exhaustion, or the immediate
@@ -24462,21 +24907,6 @@ export class AgentSession {
24462
24907
  | undefined = canReplayCodexProviderOverload || canReplayBareDefaultCodexFailure
24463
24908
  ? { class: "server" }
24464
24909
  : fallbackTrigger;
24465
- // OpenAI's typed statusless capacity-overload code (issue #5018) must not
24466
- // gain managed-chain retry/advance authority from its new facts. Before
24467
- // the code survived transport, this failure reached the session as an
24468
- // ordinary committed error and surfaced immediately, so mirror that
24469
- // behavior with the existing exhaustion decision. However, when called from
24470
- // the agent_end path (managedOutcome=false), we must return false to allow
24471
- // proper session termination handling.
24472
- if (managedFallback && isStatuslessTypedOverloadFacts(transportFailure)) {
24473
- return managedOutcome
24474
- ? this.#managedFallbackExhaustionDecision(
24475
- message,
24476
- message.errorMessage || "Model fallback attempt failed",
24477
- )
24478
- : false;
24479
- }
24480
24910
  if (!trigger) {
24481
24911
  return managedOutcome
24482
24912
  ? this.#managedFallbackExhaustionDecision(message, message.errorMessage || "Model fallback attempt failed")
@@ -24689,10 +25119,7 @@ export class AgentSession {
24689
25119
  if (outcome === "exhausted") {
24690
25120
  if (managedFallback) {
24691
25121
  let errorMessage = this.#fallbackExhaustionError(controller);
24692
- if (
24693
- !providerRetryCeilingReached &&
24694
- (trigger.class === "quota" || trigger.class === "rate_limit")
24695
- ) {
25122
+ if (!providerRetryCeilingReached && (trigger.class === "quota" || trigger.class === "rate_limit")) {
24696
25123
  if (!assistantMessageHasVisibleOrToolContent(message)) {
24697
25124
  const mark =
24698
25125
  quotaCredentialMark ??
@@ -24737,6 +25164,8 @@ export class AgentSession {
24737
25164
  : cappedExponentialWithFullJitter(retrySettings.baseDelayMs, retrySettings.maxDelayMs, attemptsUsed);
24738
25165
 
24739
25166
  const retry = async (ownership?: ManagedAttemptContinuationOwnership): Promise<void> => {
25167
+ const continuationSdkOwnership =
25168
+ retrySdkOwnership ?? this.#captureSdkContinuationOwnership(ownership?.handle.scope);
24740
25169
  const activePromptHandle = this.activePromptHandle;
24741
25170
  const cancellationSignal =
24742
25171
  ownership?.domain.signal ??
@@ -24951,7 +25380,12 @@ export class AgentSession {
24951
25380
  await this.agent.continue({
24952
25381
  ...this.#managedFallbackPromptOptions(),
24953
25382
  onRunAccepted: (handle: AttemptRunHandle) => {
24954
- this.#acceptSdkAttemptRun(handle, this.#activeSdkRunToken);
25383
+ this.#acceptSdkAttemptRun(
25384
+ handle,
25385
+ continuationSdkOwnership?.token,
25386
+ continuationSdkOwnership?.cohort,
25387
+ continuationSdkOwnership?.scope,
25388
+ );
24955
25389
  },
24956
25390
  });
24957
25391
  return;
@@ -24979,6 +25413,7 @@ export class AgentSession {
24979
25413
  allowDuringCancelAndSubmit: true,
24980
25414
  suppressPredecessorAgentEnd: resourceRunId !== undefined,
24981
25415
  resourceRunId,
25416
+ sdkOwnership: continuationSdkOwnership,
24982
25417
  onError: () => this.#failRetryRecovery("Retry continuation failed to start"),
24983
25418
  onSkip: () => this.#failRetryRecovery("Retry continuation was superseded"),
24984
25419
  });
@@ -25206,10 +25641,6 @@ export class AgentSession {
25206
25641
  // Bash Execution
25207
25642
  // =========================================================================
25208
25643
 
25209
- async #saveBashOriginalArtifact(originalText: string): Promise<BashArtifactSaveResult> {
25210
- return saveAgentBashOriginalArtifact(this.sessionManager, originalText);
25211
- }
25212
-
25213
25644
  /**
25214
25645
  * Execute a bash command.
25215
25646
  * Adds result to agent context and session.
@@ -25224,6 +25655,7 @@ export class AgentSession {
25224
25655
  onChunk?: (chunk: string) => void,
25225
25656
  options?: { excludeFromContext?: boolean; onPersisted?: () => void },
25226
25657
  ): Promise<BashResult> {
25658
+ const publishArtifact = this.sessionManager.captureArtifactPublication();
25227
25659
  const excludeFromContext = options?.excludeFromContext === true;
25228
25660
  this.#markRetryReplayUnsafe();
25229
25661
 
@@ -25261,7 +25693,7 @@ export class AgentSession {
25261
25693
  sessionId: this.sessionId,
25262
25694
  cwd,
25263
25695
  }),
25264
- onMinimizedSave: originalText => this.#saveBashOriginalArtifact(originalText),
25696
+ onMinimizedSave: originalText => saveAgentBashOriginalArtifact(publishArtifact, originalText),
25265
25697
  });
25266
25698
 
25267
25699
  this.recordBashResult(command, result, options);
@@ -25365,9 +25797,18 @@ export class AgentSession {
25365
25797
  this.#markRetryReplayUnsafe();
25366
25798
  const cwd = this.sessionManager.getCwd();
25367
25799
  this.assertEvalExecutionAllowed();
25800
+ const sessionFile = this.sessionManager.getSessionFile();
25801
+ const sessionId = sessionFile ? `session:${sessionFile}:cwd:${cwd}` : `cwd:${cwd}`;
25802
+ const kernelOwnerId = this.#evalKernelOwnerId;
25803
+ const kernelMode = this.settings.get("python.kernelMode");
25804
+ const settings = this.settings;
25368
25805
 
25369
25806
  const abortController = new AbortController();
25370
- const execution = (async (): Promise<PythonResult> => {
25807
+ const execution = Promise.resolve().then(async (): Promise<PythonResult> => {
25808
+ if (abortController.signal.aborted) {
25809
+ throw abortController.signal.reason ?? new DOMException("Aborted", "AbortError");
25810
+ }
25811
+ this.assertEvalExecutionAllowed();
25371
25812
  if (this.#extensionRunner?.hasHandlers("user_python")) {
25372
25813
  const hookResult = await this.#extensionRunner.emitUserPython({
25373
25814
  type: "user_python",
@@ -25375,6 +25816,9 @@ export class AgentSession {
25375
25816
  excludeFromContext,
25376
25817
  cwd,
25377
25818
  });
25819
+ if (abortController.signal.aborted) {
25820
+ throw abortController.signal.reason ?? new DOMException("Aborted", "AbortError");
25821
+ }
25378
25822
  this.assertEvalExecutionAllowed();
25379
25823
  if (hookResult?.result) {
25380
25824
  this.recordPythonResult(code, hookResult.result, options);
@@ -25382,15 +25826,12 @@ export class AgentSession {
25382
25826
  }
25383
25827
  }
25384
25828
 
25385
- // Use the same session ID as eval's Python backend for kernel sharing
25386
- const sessionFile = this.sessionManager.getSessionFile();
25387
- const sessionId = sessionFile ? `session:${sessionFile}:cwd:${cwd}` : `cwd:${cwd}`;
25388
25829
  const result = await executePythonCommand(code, {
25389
25830
  cwd,
25390
25831
  sessionId,
25391
- kernelOwnerId: this.#evalKernelOwnerId,
25392
- kernelMode: this.settings.get("python.kernelMode"),
25393
- settings: this.settings,
25832
+ kernelOwnerId,
25833
+ kernelMode,
25834
+ settings,
25394
25835
  onChunk,
25395
25836
  signal: abortController.signal,
25396
25837
  });
@@ -25399,7 +25840,7 @@ export class AgentSession {
25399
25840
  // must not reopen a closing session to append history.
25400
25841
  if (!this.#evalExecutionDisposing) this.recordPythonResult(code, result, options);
25401
25842
  return result;
25402
- })();
25843
+ });
25403
25844
  return await this.trackEvalExecution(execution, abortController);
25404
25845
  }
25405
25846