@gajae-code/coding-agent 0.11.11 → 0.12.1

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 +131 -4
  2. package/dist/types/async/job-manager.d.ts +17 -3
  3. package/dist/types/cli/read-cli.d.ts +1 -0
  4. package/dist/types/commands/deep-interview.d.ts +9 -0
  5. package/dist/types/commands/harness.d.ts +22 -0
  6. package/dist/types/commands/read.d.ts +6 -0
  7. package/dist/types/config/model-registry.d.ts +4 -0
  8. package/dist/types/config/provider-auth-health.d.ts +14 -0
  9. package/dist/types/config/provider-ranking.d.ts +59 -0
  10. package/dist/types/config/settings-schema.d.ts +83 -1
  11. package/dist/types/config/settings.d.ts +2 -0
  12. package/dist/types/exa/mcp-client.d.ts +10 -1
  13. package/dist/types/exec/bash-executor.d.ts +45 -12
  14. package/dist/types/extensibility/extensions/types.d.ts +38 -4
  15. package/dist/types/extensibility/gjc-plugins/index.d.ts +12 -2
  16. package/dist/types/extensibility/gjc-plugins/installer.d.ts +63 -7
  17. package/dist/types/extensibility/gjc-plugins/lifecycle-reconciliation.d.ts +55 -0
  18. package/dist/types/extensibility/gjc-plugins/lifecycle.d.ts +64 -0
  19. package/dist/types/extensibility/gjc-plugins/runtime-quarantine.d.ts +67 -0
  20. package/dist/types/extensibility/gjc-plugins/session-validation.d.ts +3 -1
  21. package/dist/types/extensibility/gjc-plugins/types.d.ts +124 -0
  22. package/dist/types/extensibility/shared-events.d.ts +6 -0
  23. package/dist/types/gjc-runtime/deep-interview-ambiguity.d.ts +5 -12
  24. package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +17 -32
  25. package/dist/types/gjc-runtime/deep-interview-stage.d.ts +37 -0
  26. package/dist/types/gjc-runtime/deep-interview-state.d.ts +4 -135
  27. package/dist/types/gjc-runtime/gc-runtime.d.ts +1 -1
  28. package/dist/types/gjc-runtime/launch-tmux.d.ts +1 -0
  29. package/dist/types/gjc-runtime/memory-guard-owner-claims.d.ts +47 -0
  30. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +70 -1
  31. package/dist/types/gjc-runtime/state-writer.d.ts +5 -46
  32. package/dist/types/gjc-runtime/team-launch.d.ts +1 -1
  33. package/dist/types/gjc-runtime/team-runtime.d.ts +27 -1
  34. package/dist/types/gjc-runtime/team-store.d.ts +12 -0
  35. package/dist/types/gjc-runtime/team-worker-heartbeat.d.ts +43 -0
  36. package/dist/types/gjc-runtime/team-worker-memory-guard.d.ts +92 -0
  37. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +17 -0
  38. package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +8 -0
  39. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +31 -80
  40. package/dist/types/gjc-runtime/workflow-manifest.d.ts +3 -5
  41. package/dist/types/internal-urls/local-root-gc.d.ts +2 -0
  42. package/dist/types/main.d.ts +1 -1
  43. package/dist/types/memories/index.d.ts +2 -0
  44. package/dist/types/modes/acp/acp-agent.d.ts +10 -19
  45. package/dist/types/modes/components/assistant-message.d.ts +3 -1
  46. package/dist/types/modes/components/gjc-bundle-settings.d.ts +34 -0
  47. package/dist/types/modes/components/irc-sidebar.d.ts +41 -0
  48. package/dist/types/modes/components/notifications-settings-editor.d.ts +1 -0
  49. package/dist/types/modes/components/read-tool-group.d.ts +4 -4
  50. package/dist/types/modes/components/session-observer-overlay.d.ts +22 -0
  51. package/dist/types/modes/components/settings-selector.d.ts +11 -0
  52. package/dist/types/modes/components/status-line/gh.d.ts +5 -0
  53. package/dist/types/modes/components/tool-execution.d.ts +8 -0
  54. package/dist/types/modes/components/transcript-viewer-overlay.d.ts +18 -1
  55. package/dist/types/modes/controllers/event-controller.d.ts +3 -0
  56. package/dist/types/modes/interactive-mode.d.ts +2 -0
  57. package/dist/types/modes/irc-observation-ledger.d.ts +17 -1
  58. package/dist/types/modes/types.d.ts +2 -0
  59. package/dist/types/modes/utils/ui-helpers.d.ts +16 -0
  60. package/dist/types/reminders/star-reminder.d.ts +3 -13
  61. package/dist/types/runtime-mcp/manager.d.ts +1 -0
  62. package/dist/types/runtime-mcp/smithery-auth.d.ts +5 -0
  63. package/dist/types/sdk/acp/adapter.d.ts +3 -1
  64. package/dist/types/sdk/broker/lifecycle.d.ts +5 -0
  65. package/dist/types/sdk/broker/transport.d.ts +2 -0
  66. package/dist/types/sdk/bus/attachment-registry.d.ts +1 -0
  67. package/dist/types/sdk/bus/chat-daemon-control.d.ts +4 -1
  68. package/dist/types/sdk/bus/config.d.ts +6 -2
  69. package/dist/types/sdk/bus/control-drain-lease.d.ts +52 -0
  70. package/dist/types/sdk/bus/index.d.ts +44 -1
  71. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +44 -0
  72. package/dist/types/sdk/bus/lifecycle-control-runtime.d.ts +4 -0
  73. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +2 -1
  74. package/dist/types/sdk/bus/reconciliation-store.d.ts +69 -0
  75. package/dist/types/sdk/bus/rich-draft.d.ts +1 -0
  76. package/dist/types/sdk/bus/rich-render.d.ts +2 -0
  77. package/dist/types/sdk/bus/telegram-adoption-intent.d.ts +165 -0
  78. package/dist/types/sdk/bus/telegram-cli.d.ts +12 -0
  79. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +11 -4
  80. package/dist/types/sdk/bus/telegram-daemon.d.ts +25 -1
  81. package/dist/types/sdk/bus/telegram-reference.d.ts +11 -1
  82. package/dist/types/sdk/bus/threaded-render.d.ts +3 -2
  83. package/dist/types/sdk/bus/topic-registry.d.ts +11 -1
  84. package/dist/types/sdk/host/control/operations.d.ts +1 -1
  85. package/dist/types/sdk/host/host.d.ts +2 -0
  86. package/dist/types/sdk/host/query/handlers.d.ts +5 -0
  87. package/dist/types/sdk/host/query/revision-store.d.ts +2 -0
  88. package/dist/types/sdk/host/reverse-leases.d.ts +1 -1
  89. package/dist/types/sdk/prompt-status.d.ts +23 -3
  90. package/dist/types/sdk/session.d.ts +6 -0
  91. package/dist/types/session/agent-session.d.ts +82 -12
  92. package/dist/types/session/artifacts.d.ts +8 -0
  93. package/dist/types/session/blob-store.d.ts +57 -4
  94. package/dist/types/session/internal/managed-session-storage.d.ts +8 -1
  95. package/dist/types/session/memory-guard-checkpoint-participant.d.ts +69 -0
  96. package/dist/types/session/session-manager.d.ts +48 -19
  97. package/dist/types/session/session-storage.d.ts +4 -0
  98. package/dist/types/session/streaming-output.d.ts +96 -12
  99. package/dist/types/setup/credential-import.d.ts +12 -0
  100. package/dist/types/skill-state/active-state.d.ts +12 -4
  101. package/dist/types/skill-state/workflow-hud.d.ts +9 -0
  102. package/dist/types/task/executor.d.ts +3 -2
  103. package/dist/types/task/index.d.ts +3 -1
  104. package/dist/types/task/provider-retry-status.d.ts +8 -0
  105. package/dist/types/task/receipt.d.ts +7 -1
  106. package/dist/types/task/types.d.ts +23 -0
  107. package/dist/types/tools/ask-answer-registry.d.ts +2 -2
  108. package/dist/types/tools/bash-interactive.d.ts +4 -1
  109. package/dist/types/tools/bash.d.ts +3 -1
  110. package/dist/types/tools/browser/launch.d.ts +7 -0
  111. package/dist/types/tools/fetch.d.ts +17 -1
  112. package/dist/types/tools/image-gen.d.ts +4 -0
  113. package/dist/types/tools/index.d.ts +4 -0
  114. package/dist/types/tools/output-meta.d.ts +44 -3
  115. package/dist/types/tools/path-utils.d.ts +1 -0
  116. package/dist/types/tools/read-internals.d.ts +12 -0
  117. package/dist/types/tools/read.d.ts +45 -7
  118. package/dist/types/tools/resource-gc.d.ts +4 -0
  119. package/dist/types/tools/review.d.ts +5 -7
  120. package/dist/types/tools/subagent.d.ts +2 -0
  121. package/dist/types/tools/tool-result.d.ts +7 -1
  122. package/dist/types/utils/gh.d.ts +13 -0
  123. package/dist/types/utils/image-resize.d.ts +1 -0
  124. package/dist/types/web/search/providers/kimi.d.ts +2 -0
  125. package/dist/types/web/search/providers/searxng.d.ts +7 -0
  126. package/dist/types/web/search/providers/xai.d.ts +2 -0
  127. package/package.json +11 -13
  128. package/scripts/acp-conformance-agent.ts +218 -0
  129. package/scripts/benchmark-sticky-viewport-pr1.ts +239 -0
  130. package/scripts/capture-gjc-bundle-settings.ts +306 -0
  131. package/scripts/capture-sticky-viewport-showcase.ts +292 -0
  132. package/scripts/generate-sdk-operation-inventory.ts +5 -0
  133. package/scripts/resident-memory-bench.ts +1341 -0
  134. package/scripts/run-acp-conformance.ts +285 -0
  135. package/scripts/verify-gjc-sdk-canonicalization.ts +122 -18
  136. package/scripts/verify-sticky-viewport-showcase.ts +611 -0
  137. package/src/async/job-manager.ts +78 -20
  138. package/src/capability/index.ts +8 -0
  139. package/src/cli/fast-help.ts +1 -1
  140. package/src/cli/plugin-cli.ts +176 -18
  141. package/src/cli/read-cli.ts +2 -1
  142. package/src/cli.ts +1 -0
  143. package/src/commands/deep-interview.ts +29 -9
  144. package/src/commands/harness.ts +35 -12
  145. package/src/commands/read.ts +11 -2
  146. package/src/commands/team.ts +20 -2
  147. package/src/config/file-lock.ts +11 -2
  148. package/src/config/model-registry.ts +21 -2
  149. package/src/config/provider-auth-health.ts +42 -0
  150. package/src/config/provider-ranking.ts +119 -0
  151. package/src/config/settings-schema.ts +74 -1
  152. package/src/config/settings.ts +246 -108
  153. package/src/coordinator-mcp/question-gate-codec.ts +1 -1
  154. package/src/coordinator-mcp/server.ts +5 -12
  155. package/src/defaults/gjc/skills/deep-interview/SKILL.md +230 -82
  156. package/src/defaults/gjc/skills/ralplan/SKILL.md +77 -41
  157. package/src/defaults/gjc/skills/team/SKILL.md +3 -1
  158. package/src/defaults/gjc/skills/ultragoal/SKILL.md +62 -39
  159. package/src/defaults/gjc/skills/ultragoal/validation-batch-contracts.md +12 -0
  160. package/src/defaults/gjc-defaults.ts +4 -4
  161. package/src/eval/py/executor.ts +7 -2
  162. package/src/exa/mcp-client.ts +12 -3
  163. package/src/exec/bash-executor.ts +144 -27
  164. package/src/extensibility/extensions/runner.ts +15 -0
  165. package/src/extensibility/extensions/types.ts +46 -5
  166. package/src/extensibility/gjc-plugins/constrained-hooks.ts +8 -2
  167. package/src/extensibility/gjc-plugins/index.ts +19 -2
  168. package/src/extensibility/gjc-plugins/installer.ts +189 -50
  169. package/src/extensibility/gjc-plugins/lifecycle-reconciliation.ts +199 -0
  170. package/src/extensibility/gjc-plugins/lifecycle.ts +626 -0
  171. package/src/extensibility/gjc-plugins/observability.ts +3 -3
  172. package/src/extensibility/gjc-plugins/prompt-appendix.ts +5 -1
  173. package/src/extensibility/gjc-plugins/runtime-adapters.ts +12 -2
  174. package/src/extensibility/gjc-plugins/runtime-quarantine.ts +121 -0
  175. package/src/extensibility/gjc-plugins/schema.ts +84 -12
  176. package/src/extensibility/gjc-plugins/session-validation.ts +9 -3
  177. package/src/extensibility/gjc-plugins/types.ts +141 -0
  178. package/src/extensibility/gjc-plugins/validation.ts +9 -0
  179. package/src/extensibility/shared-events.ts +5 -0
  180. package/src/gjc-runtime/deep-interview-ambiguity.ts +47 -82
  181. package/src/gjc-runtime/deep-interview-recorder.ts +227 -310
  182. package/src/gjc-runtime/deep-interview-runtime.ts +109 -270
  183. package/src/gjc-runtime/deep-interview-stage.ts +1016 -0
  184. package/src/gjc-runtime/deep-interview-state.ts +12 -943
  185. package/src/gjc-runtime/gc-render.ts +1 -0
  186. package/src/gjc-runtime/gc-runtime.ts +11 -1
  187. package/src/gjc-runtime/launch-tmux.ts +25 -6
  188. package/src/gjc-runtime/managed-owner-admission.ts +1 -1
  189. package/src/gjc-runtime/memory-guard-owner-claims.ts +389 -0
  190. package/src/gjc-runtime/ralplan-runtime.ts +1149 -98
  191. package/src/gjc-runtime/state-runtime.ts +2 -1
  192. package/src/gjc-runtime/state-writer.ts +70 -222
  193. package/src/gjc-runtime/team-launch.ts +42 -5
  194. package/src/gjc-runtime/team-runtime.ts +1085 -40
  195. package/src/gjc-runtime/team-store.ts +32 -0
  196. package/src/gjc-runtime/team-worker-heartbeat.ts +149 -0
  197. package/src/gjc-runtime/team-worker-memory-guard.ts +363 -0
  198. package/src/gjc-runtime/team-workers.ts +1 -0
  199. package/src/gjc-runtime/tmux-owner-isolation.ts +28 -0
  200. package/src/gjc-runtime/tmux-sessions.ts +58 -13
  201. package/src/gjc-runtime/ultragoal-receipt-freshness.ts +20 -0
  202. package/src/gjc-runtime/ultragoal-runtime.ts +414 -618
  203. package/src/gjc-runtime/workflow-manifest.generated.json +105 -449
  204. package/src/gjc-runtime/workflow-manifest.ts +38 -247
  205. package/src/hooks/native-skill-hook.ts +17 -4
  206. package/src/hooks/skill-state.ts +1 -23
  207. package/src/internal-urls/docs-index.generated.ts +21 -22
  208. package/src/internal-urls/local-root-gc.ts +127 -0
  209. package/src/main.ts +9 -5
  210. package/src/memories/index.ts +10 -0
  211. package/src/modes/DESIGN.md +98 -0
  212. package/src/modes/acp/acp-agent.ts +339 -191
  213. package/src/modes/acp/acp-event-mapper.ts +14 -7
  214. package/src/modes/acp/acp-mode.ts +39 -3
  215. package/src/modes/components/assistant-message.ts +123 -22
  216. package/src/modes/components/gjc-bundle-settings.ts +508 -0
  217. package/src/modes/components/irc-sidebar.ts +176 -62
  218. package/src/modes/components/model-selector.ts +61 -4
  219. package/src/modes/components/notifications-settings-editor.ts +12 -1
  220. package/src/modes/components/oauth-selector.ts +96 -28
  221. package/src/modes/components/read-tool-group.ts +82 -14
  222. package/src/modes/components/session-observer-overlay.ts +457 -83
  223. package/src/modes/components/settings-selector.ts +116 -23
  224. package/src/modes/components/status-line/gh.ts +16 -0
  225. package/src/modes/components/tool-execution.ts +138 -27
  226. package/src/modes/components/tool-status-header.ts +3 -14
  227. package/src/modes/components/transcript-viewer-overlay.ts +99 -24
  228. package/src/modes/controllers/command-controller.ts +4 -0
  229. package/src/modes/controllers/event-controller.ts +109 -27
  230. package/src/modes/controllers/extension-ui-controller.ts +26 -2
  231. package/src/modes/controllers/input-controller.ts +18 -3
  232. package/src/modes/controllers/selector-controller.ts +127 -36
  233. package/src/modes/interactive-mode.ts +62 -3
  234. package/src/modes/irc-observation-ledger.ts +71 -15
  235. package/src/modes/runtime-init.ts +22 -2
  236. package/src/modes/types.ts +2 -0
  237. package/src/modes/utils/ui-helpers.ts +94 -1
  238. package/src/prompts/agent-fragments/ralplan-persistence.md +4 -3
  239. package/src/prompts/agents/architect.md +9 -1
  240. package/src/prompts/agents/critic.md +9 -0
  241. package/src/prompts/system/eager-todo.md +8 -4
  242. package/src/prompts/system/system-prompt.md +11 -5
  243. package/src/prompts/tools/bash.md +5 -5
  244. package/src/prompts/tools/read.md +4 -2
  245. package/src/prompts/tools/search-tool-bm25.md +5 -0
  246. package/src/prompts/tools/task.md +1 -1
  247. package/src/reminders/star-reminder.ts +4 -42
  248. package/src/runtime-mcp/manager.ts +19 -4
  249. package/src/runtime-mcp/smithery-auth.ts +17 -2
  250. package/src/runtime-mcp/smithery-connect.ts +11 -1
  251. package/src/sdk/acp/adapter.ts +22 -6
  252. package/src/sdk/broker/lifecycle.ts +25 -6
  253. package/src/sdk/broker/transport.ts +35 -3
  254. package/src/sdk/bus/attachment-registry.ts +5 -1
  255. package/src/sdk/bus/chat-daemon-cli.ts +4 -39
  256. package/src/sdk/bus/chat-daemon-control.ts +6 -3
  257. package/src/sdk/bus/config.ts +18 -2
  258. package/src/sdk/bus/control-drain-lease.ts +104 -0
  259. package/src/sdk/bus/index.ts +984 -146
  260. package/src/sdk/bus/kind-aware-reconciliation.ts +317 -0
  261. package/src/sdk/bus/lifecycle-commands.ts +4 -1
  262. package/src/sdk/bus/lifecycle-control-runtime.ts +403 -88
  263. package/src/sdk/bus/prompt-reconciliation.ts +2 -1
  264. package/src/sdk/bus/reconciliation-store.ts +303 -0
  265. package/src/sdk/bus/rich-draft.ts +1 -1
  266. package/src/sdk/bus/rich-render.ts +2 -2
  267. package/src/sdk/bus/telegram-adoption-intent.ts +574 -0
  268. package/src/sdk/bus/telegram-cli.ts +33 -8
  269. package/src/sdk/bus/telegram-daemon-cli.ts +1 -0
  270. package/src/sdk/bus/telegram-daemon-contract.ts +11 -4
  271. package/src/sdk/bus/telegram-daemon.ts +916 -23
  272. package/src/sdk/bus/telegram-reference.ts +40 -3
  273. package/src/sdk/bus/threaded-render.ts +48 -3
  274. package/src/sdk/bus/topic-registry.ts +20 -0
  275. package/src/sdk/host/control/dispatch.ts +5 -1
  276. package/src/sdk/host/control/operations.ts +1 -1
  277. package/src/sdk/host/host.ts +23 -1
  278. package/src/sdk/host/query/handlers.ts +55 -0
  279. package/src/sdk/host/query/revision-store.ts +36 -2
  280. package/src/sdk/host/reverse-leases.ts +44 -7
  281. package/src/sdk/mcp/server.ts +46 -18
  282. package/src/sdk/prompt-status.ts +16 -3
  283. package/src/sdk/protocol/operation-inventory.generated.json +51 -0
  284. package/src/sdk/protocol/operation-registry.ts +7 -2
  285. package/src/sdk/session.ts +101 -10
  286. package/src/session/agent-session.ts +1286 -220
  287. package/src/session/artifacts.ts +22 -0
  288. package/src/session/auth-broker-config.ts +10 -3
  289. package/src/session/blob-store.ts +915 -24
  290. package/src/session/internal/managed-session-scope.ts +44 -6
  291. package/src/session/internal/managed-session-storage.ts +48 -6
  292. package/src/session/memory-guard-checkpoint-participant.ts +131 -0
  293. package/src/session/session-manager.ts +2364 -438
  294. package/src/session/session-storage.ts +9 -1
  295. package/src/session/streaming-output.ts +500 -111
  296. package/src/setup/credential-import.ts +75 -13
  297. package/src/setup/provider-onboarding.ts +19 -4
  298. package/src/skill-state/active-state.ts +32 -10
  299. package/src/skill-state/workflow-hud.ts +42 -2
  300. package/src/skill-state/workflow-mutation-guard.ts +560 -8
  301. package/src/task/executor.ts +122 -39
  302. package/src/task/index.ts +23 -14
  303. package/src/task/provider-retry-status.ts +16 -0
  304. package/src/task/receipt.ts +23 -7
  305. package/src/task/render.ts +6 -2
  306. package/src/task/types.ts +108 -0
  307. package/src/tools/ask-answer-registry.ts +12 -6
  308. package/src/tools/ask.ts +169 -118
  309. package/src/tools/ast-edit.ts +1 -0
  310. package/src/tools/ast-grep.ts +1 -0
  311. package/src/tools/bash-interactive.ts +8 -3
  312. package/src/tools/bash.ts +503 -60
  313. package/src/tools/browser/launch.ts +41 -6
  314. package/src/tools/fetch.ts +114 -23
  315. package/src/tools/find.ts +1 -0
  316. package/src/tools/image-gen.ts +29 -5
  317. package/src/tools/index.ts +4 -0
  318. package/src/tools/output-meta.ts +281 -20
  319. package/src/tools/path-utils.ts +8 -3
  320. package/src/tools/read-internals.ts +44 -0
  321. package/src/tools/read.ts +1560 -310
  322. package/src/tools/resource-gc.ts +182 -18
  323. package/src/tools/review.ts +5 -7
  324. package/src/tools/search.ts +1 -0
  325. package/src/tools/subagent-render.ts +6 -2
  326. package/src/tools/subagent.ts +6 -0
  327. package/src/tools/telegram-send.ts +6 -3
  328. package/src/tools/tool-result.ts +14 -1
  329. package/src/utils/gh.ts +39 -0
  330. package/src/utils/image-resize.ts +3 -0
  331. package/src/web/search/providers/anthropic.ts +4 -4
  332. package/src/web/search/providers/codex.ts +1 -1
  333. package/src/web/search/providers/kimi.ts +15 -5
  334. package/src/web/search/providers/searxng.ts +47 -4
  335. package/src/web/search/providers/xai.ts +7 -2
  336. package/dist/types/gjc-runtime/deep-interview-draft.d.ts +0 -8
  337. package/dist/types/gjc-runtime/deep-interview-payload.d.ts +0 -41
  338. package/dist/types/gjc-runtime/deep-interview-repair.d.ts +0 -17
  339. package/dist/types/gjc-runtime/ultragoal-pipeline.d.ts +0 -23
  340. package/src/defaults/gjc/skills/ultragoal/pipeline-validation-contracts.md +0 -30
  341. package/src/gjc-runtime/deep-interview-draft.ts +0 -843
  342. package/src/gjc-runtime/deep-interview-payload.ts +0 -262
  343. package/src/gjc-runtime/deep-interview-repair.ts +0 -1674
  344. package/src/gjc-runtime/ultragoal-pipeline.ts +0 -306
@@ -18,6 +18,7 @@ import * as crypto from "node:crypto";
18
18
  import * as fs from "node:fs";
19
19
  import * as path from "node:path";
20
20
  import { scheduler } from "node:timers/promises";
21
+ import * as util from "node:util";
21
22
  import {
22
23
  type AfterToolCallContext,
23
24
  type AfterToolCallResult,
@@ -35,6 +36,7 @@ import {
35
36
  type ManagedAttemptDecision,
36
37
  type ManagedAttemptOutcome,
37
38
  type MidRunMaintenanceOutcome,
39
+ type RunSettlementProof,
38
40
  resolveTelemetry,
39
41
  type StablePrefixSnapshot,
40
42
  ThinkingLevel,
@@ -81,6 +83,7 @@ import type {
81
83
  ProviderSessionState,
82
84
  ServiceTier,
83
85
  SimpleStreamOptions,
86
+ StopReason,
84
87
  TextContent,
85
88
  ToolCall,
86
89
  ToolChoice,
@@ -104,6 +107,7 @@ import {
104
107
  classifyFallbackTrigger,
105
108
  type FallbackAttemptToken,
106
109
  type FallbackTriggerClass,
110
+ STREAM_FIRST_EVENT_TIMEOUT_PROVIDER_CODE,
107
111
  } from "@gajae-code/ai/utils/fallback-transport";
108
112
  import {
109
113
  BTW_MAX_ANSWER_UTF8_BYTES,
@@ -115,6 +119,7 @@ import {
115
119
  truncateUtf8,
116
120
  utf8ByteLength,
117
121
  } from "./btw-contract";
122
+ import { DEFAULT_ARTIFACT_MAX_BYTES, truncateHeadBytes } from "./streaming-output";
118
123
 
119
124
  export interface ForkContextSeedMetadata {
120
125
  sourceSessionId: string;
@@ -161,12 +166,15 @@ export interface ForkContextSeedOptions {
161
166
  import { MacOSPowerAssertion } from "@gajae-code/natives";
162
167
  import {
163
168
  extractRetryHint,
169
+ hasFsCode,
170
+ isEacces,
164
171
  isEnoent,
165
172
  isUnexpectedSocketCloseMessage,
166
173
  logger,
167
174
  prompt,
168
175
  Snowflake,
169
176
  } from "@gajae-code/utils";
177
+
170
178
  import { createAppendOnlyContextManager, resolveAppendOnlyMode } from "../append-only-mode";
171
179
  import { type AsyncJob, type AsyncJobDeliveryState, AsyncJobManager } from "../async";
172
180
  import { reset as resetCapabilities } from "../capability";
@@ -207,7 +215,7 @@ import {
207
215
  executePython as executePythonCommand,
208
216
  type PythonResult,
209
217
  } from "../eval/py/executor";
210
- import { type BashResult, executeBash as executeBashCommand } from "../exec/bash-executor";
218
+ import { type BashArtifactSaveResult, type BashResult, executeBash as executeBashCommand } from "../exec/bash-executor";
211
219
  import { exportSessionToHtml } from "../export/html";
212
220
  import type { TtsrManager, TtsrMatchContext } from "../export/ttsr";
213
221
  import type { LoadedCustomCommand } from "../extensibility/custom-commands";
@@ -241,9 +249,11 @@ import {
241
249
  resolveSubskillActivationForSkillInvocation,
242
250
  } from "../extensibility/gjc-plugins";
243
251
  import { resolveCurrentPhaseForParent } from "../extensibility/gjc-plugins/injection";
252
+ import type { GjcRuntimeSnapshotProvider } from "../extensibility/gjc-plugins/runtime-quarantine";
244
253
  import { readActiveSubskillsForParent, toActiveSubskillEntry } from "../extensibility/gjc-plugins/state";
245
254
  import { loadActiveSubskillTools } from "../extensibility/gjc-plugins/tools";
246
255
  import type { HookCommandContext } from "../extensibility/hooks/types";
256
+ import type { SessionSwitchEvent } from "../extensibility/shared-events";
247
257
  import {
248
258
  buildSkillPromptMessage,
249
259
  getSkillSlashCommandName,
@@ -254,6 +264,11 @@ import {
254
264
  import { expandSlashCommand, type FileSlashCommand } from "../extensibility/slash-commands";
255
265
  import { assertDeepInterviewIntentManifest } from "../gjc-runtime/deep-interview-state";
256
266
  import { buildGjcRuntimeSessionEnv, consumePendingGoalModeRequest } from "../gjc-runtime/goal-mode-request";
267
+ import {
268
+ isMemoryGuardClaimsLease,
269
+ isMemoryGuardClaimsLeaseForStateDir,
270
+ type MemoryGuardClaimsLease,
271
+ } from "../gjc-runtime/memory-guard-owner-claims";
257
272
  import {
258
273
  assertNonEmptyGjcSessionId,
259
274
  modeStatePath as sessionModeStatePath,
@@ -265,6 +280,7 @@ import {
265
280
  registerCoordinatorRuntimeStateFinalizer,
266
281
  } from "../gjc-runtime/session-state-sidecar";
267
282
  import { requestGjcWorkerIntegrationAttempt } from "../gjc-runtime/team-runtime";
283
+ import { GjcTeamWorkerHeartbeatReporter } from "../gjc-runtime/team-worker-heartbeat";
268
284
  import { GoalRuntime } from "../goals/runtime";
269
285
  import type { Goal, GoalModeState } from "../goals/state";
270
286
  import type { HindsightSessionState } from "../hindsight/state";
@@ -298,6 +314,7 @@ import { deobfuscateSessionContext, type SecretObfuscator } from "../secrets/obf
298
314
  import { formatNoCredentialOnboardingError, formatNoModelOnboardingError } from "../setup/model-onboarding-guidance";
299
315
  import {
300
316
  isCanonicalGjcWorkflowSkill,
317
+ isWorkflowContinuationInert,
301
318
  readVisibleSkillActiveState,
302
319
  syncSkillActiveState,
303
320
  } from "../skill-state/active-state";
@@ -368,6 +385,7 @@ import {
368
385
  prepareContributionPrep,
369
386
  } from "./contribution-prep";
370
387
  import { pruneStaleFileMentions } from "./file-mention-pruning";
388
+ import type { MemoryGuardRestoreResult } from "./memory-guard-checkpoint-participant";
371
389
  import {
372
390
  type BashExecutionMessage,
373
391
  type CompactionSummaryMessage,
@@ -409,6 +427,32 @@ import { ToolChoiceQueue } from "./tool-choice-queue";
409
427
  import { pruneSupersededMaintenanceReminders, pruneSupersededVolatileProjectContext } from "./volatile-context-pruning";
410
428
  import { YieldQueue } from "./yield-queue";
411
429
 
430
+ interface CompactionStateSnapshot {
431
+ goal: { objective: string; status: Goal["status"]; enabled: boolean } | undefined;
432
+ openTodos: string[];
433
+ activeSkills: Array<{ skill: string; phase: string }>;
434
+ queuedMessages: boolean;
435
+ lastAssistantStopReason: StopReason | undefined;
436
+ }
437
+
438
+ /** Escape XML-ish metacharacters and flatten newlines so state text cannot break compaction prompt framing. */
439
+ function sanitizeCompactionStateText(value: string, maxLength: number): string {
440
+ return value
441
+ .replace(/&/g, "&amp;")
442
+ .replace(/</g, "&lt;")
443
+ .replace(/>/g, "&gt;")
444
+ .replace(/\r\n/g, " ")
445
+ .replace(/[\r\n]/g, " ")
446
+ .slice(0, maxLength);
447
+ }
448
+
449
+ function appendCompactionStateContext(summary: string, stateContext: string[]): string {
450
+ if (stateContext.length === 0) return summary;
451
+ return `${summary}\n\n<compaction-state>\n${stateContext.join("\n")}\n</compaction-state>`;
452
+ }
453
+
454
+ const PRUNED_ARTIFACT_REF_MAX_CHARS = 64;
455
+
412
456
  /** Session-specific events that extend the core AgentEvent */
413
457
  export type AutoCompactionContinuationSkipReason = "auto_continue_disabled_non_resumable_tail";
414
458
 
@@ -498,6 +542,11 @@ export interface AgentSessionConfig {
498
542
  workerIntegrationRequest?: (signal: AbortSignal) => Promise<void>;
499
543
  /** Bound terminal worker-integration settlement for embedded hosts and deterministic lifecycle tests. */
500
544
  workerIntegrationTimeoutMs?: number;
545
+ /**
546
+ * Override the runtime-owned `gjc team` worker heartbeat reporter. Defaults to a
547
+ * reporter derived from the team worker environment, or none outside a worker pane.
548
+ */
549
+ teamWorkerHeartbeatReporter?: GjcTeamWorkerHeartbeatReporter;
501
550
 
502
551
  /** Loaded skills (already discovered by SDK) */
503
552
  skills?: Skill[];
@@ -613,6 +662,24 @@ export interface AgentSessionConfig {
613
662
  providerCacheSessionId?: string;
614
663
  }
615
664
 
665
+ export interface AgentSessionMemoryGuardRestoreInput
666
+ extends Omit<AgentSessionConfig, "sessionManager" | "recoveryHydrationContext"> {
667
+ staged: Extract<MemoryGuardRestoreResult, { kind: "staged" }>;
668
+ claimsLease: MemoryGuardClaimsLease;
669
+ claimsStateDir: string;
670
+ }
671
+
672
+ export interface AgentMemoryGuardPromotionFence extends RecoveryHydrationPromotionFence {
673
+ readonly claimsLease: MemoryGuardClaimsLease;
674
+ }
675
+
676
+ export type AgentMemoryGuardRestoreResult =
677
+ | { kind: "staged"; session: AgentSession; promotionFence: AgentMemoryGuardPromotionFence }
678
+ | {
679
+ kind: "blocked";
680
+ reason: "transcript-mismatch" | "hydration-context-mismatch" | "claim-mismatch" | "agent-messages-mismatch";
681
+ };
682
+
616
683
  type MidRunMaintenanceLifecycle = Parameters<NonNullable<AgentLoopConfig["maintainContext"]>>[1];
617
684
 
618
685
  type AutoCompactionTerminalStatus =
@@ -642,8 +709,16 @@ export interface PromptOptions {
642
709
  /**
643
710
  * Invoked after all prompt preflight checks pass and immediately before agent execution begins.
644
711
  * Cancellation before this callback rejects the prompt.
712
+ * Prefer `onPreflightAcceptCommit` for async durable acceptance (#3031/#3032).
645
713
  */
646
714
  onPreflightAccepted?: () => void;
715
+ /**
716
+ * Awaitable durable-accept fence. Called after preflight and before queue mutation
717
+ * or `#promptAgentWithIdleRetry`. SDK bus installs a closure that fsyncs acceptance.
718
+ */
719
+ onPreflightAcceptCommit?: () => void | Promise<void>;
720
+ /** Skill-only: prepared metadata before the durable fence (path/lineCount/cleanedArgs). */
721
+ onSkillPrepared?: (meta: { name: string; path: string; lineCount?: number; cleanedArgs?: string }) => void;
647
722
  }
648
723
 
649
724
  function promptPreflightCancelledError(): Error {
@@ -794,6 +869,7 @@ type RetryErrorClassification =
794
869
 
795
870
  const BARE_DEFAULT_WATCHDOG_ERROR =
796
871
  /^(?:[A-Za-z][A-Za-z0-9-]*(?: [A-Za-z][A-Za-z0-9-]*){0,3} )stream (?:timed out while waiting for the first event|stalled while waiting for the next event)$/;
872
+ const BARE_DEFAULT_CODEX_OVERLOAD_ERROR = /^Codex error event(?:: .*)? \(code=server_is_overloaded(?:, [^)]+)*\)$/;
797
873
  const KIMI_CODE_FIRST_EVENT_TIMEOUT_MESSAGES = {
798
874
  "anthropic-messages": new Set([
799
875
  "Provider stream timed out while waiting for the first event",
@@ -830,6 +906,14 @@ function hasBareDefaultRetryDisqualifyingFacts(message: AssistantMessage): boole
830
906
  (facts.headers !== undefined && Object.keys(facts.headers).length > 0)
831
907
  );
832
908
  }
909
+ function isBareDefaultCodexOverload(message: AssistantMessage): boolean {
910
+ return (
911
+ message.api === "openai-codex-responses" &&
912
+ BARE_DEFAULT_CODEX_OVERLOAD_ERROR.test(message.errorMessage ?? "") &&
913
+ !hasBareDefaultRetryDisqualifyingFacts(message) &&
914
+ !assistantMessageHasVisibleOrToolContent(message)
915
+ );
916
+ }
833
917
 
834
918
  function assistantMessageHasVisibleOrToolContent(message: AssistantMessage): boolean {
835
919
  return message.content.some(content => {
@@ -932,6 +1016,50 @@ interface EphemeralTurnResult {
932
1016
  */
933
1017
  const SIGNAL_TEARDOWN_TIMEOUT_MS = 5_000;
934
1018
 
1019
+ const AGENT_BASH_ARTIFACT_SAVE_DIAGNOSTIC_MAX_BYTES = 256;
1020
+
1021
+ function boundAgentBashArtifactSaveDiagnostic(error: unknown): string {
1022
+ const message = (error instanceof Error ? error.message : String(error)).replace(/\s+/gu, " ").trim();
1023
+ const normalized = message || "unknown storage error";
1024
+ return truncateHeadBytes(normalized, AGENT_BASH_ARTIFACT_SAVE_DIAGNOSTIC_MAX_BYTES).text;
1025
+ }
1026
+
1027
+ function summarizeAgentBashArtifactSave(
1028
+ artifactId: string,
1029
+ originalText: string,
1030
+ ): Extract<BashArtifactSaveResult, { status: "saved" }> {
1031
+ const originalBytes = utf8ByteLength(originalText);
1032
+ if (originalBytes <= DEFAULT_ARTIFACT_MAX_BYTES) {
1033
+ return { status: "saved", artifactId, complete: true };
1034
+ }
1035
+ const retainedBytes = truncateHeadBytes(originalText, DEFAULT_ARTIFACT_MAX_BYTES).bytes;
1036
+ return {
1037
+ status: "saved",
1038
+ artifactId,
1039
+ complete: false,
1040
+ omittedBytes: originalBytes - retainedBytes,
1041
+ };
1042
+ }
1043
+
1044
+ export interface AgentBashArtifactStore {
1045
+ saveArtifact(content: string, toolType: string): Promise<string | undefined>;
1046
+ getArtifactPath(id: string): Promise<string | null>;
1047
+ }
1048
+
1049
+ export async function saveAgentBashOriginalArtifact(
1050
+ store: AgentBashArtifactStore,
1051
+ originalText: string,
1052
+ ): Promise<BashArtifactSaveResult> {
1053
+ try {
1054
+ const artifactId = await store.saveArtifact(originalText, "bash-original");
1055
+ if (!artifactId) return { status: "failed", diagnostic: "storage returned no artifact id" };
1056
+ const artifactPath = await store.getArtifactPath(artifactId);
1057
+ return artifactPath ? summarizeAgentBashArtifactSave(artifactId, originalText) : { status: "unavailable" };
1058
+ } catch (error) {
1059
+ return { status: "failed", diagnostic: boundAgentBashArtifactSaveDiagnostic(error) };
1060
+ }
1061
+ }
1062
+
935
1063
  /**
936
1064
  * Throttle window for the per-turn volatile workspace-tree scan. Date/cwd are
937
1065
  * refreshed every turn; the mtime-sorted tree is rescanned + re-embedded at most
@@ -1538,6 +1666,47 @@ export class AgentSession {
1538
1666
  #sessionAdmissionClosed = false;
1539
1667
  #sessionAdmissionContext = new AsyncLocalStorage<SessionAdmissionEntry>();
1540
1668
  #defaultModelSelectionMutationRevision = 0;
1669
+ #thinkingLevelMutationRevision = 0;
1670
+ #thinkingVisibilityMutationRevision = 0;
1671
+ #thinkingLevelLiveMutationRevision = 0;
1672
+ #thinkingVisibilityLiveMutationRevision = 0;
1673
+ #reasoningControlContextGeneration = 0;
1674
+ #pendingThinkingLevelControlSuccess:
1675
+ | {
1676
+ level: ThinkingLevel;
1677
+ mutationRevision: number;
1678
+ sessionId: string;
1679
+ model: Model | undefined;
1680
+ contextGeneration: number;
1681
+ }
1682
+ | undefined;
1683
+ #pendingThinkingVisibilityControlSuccess:
1684
+ | {
1685
+ visibility: "visible" | "hidden";
1686
+ mutationRevision: number;
1687
+ sessionId: string;
1688
+ model: Model | undefined;
1689
+ contextGeneration: number;
1690
+ }
1691
+ | undefined;
1692
+ #pendingThinkingLevelControlFailure:
1693
+ | {
1694
+ mutationRevision: number;
1695
+ liveMutationRevision: number;
1696
+ sessionId: string;
1697
+ model: Model | undefined;
1698
+ contextGeneration: number;
1699
+ }
1700
+ | undefined;
1701
+ #pendingThinkingVisibilityControlFailure:
1702
+ | {
1703
+ mutationRevision: number;
1704
+ liveMutationRevision: number;
1705
+ sessionId: string;
1706
+ model: Model | undefined;
1707
+ contextGeneration: number;
1708
+ }
1709
+ | undefined;
1541
1710
  #promptTemplates: PromptTemplate[];
1542
1711
  #slashCommands: FileSlashCommand[];
1543
1712
 
@@ -1623,6 +1792,7 @@ export class AgentSession {
1623
1792
  #resetRetryReplaySafety(): void {
1624
1793
  this.#retryReplayEpoch++;
1625
1794
  this.#retryReplayUnsafeEpoch = undefined;
1795
+ this.#firstEventTimeoutRetryStartedAt = Date.now();
1626
1796
  if (
1627
1797
  this.#extensionRunner?.hasHandlers("context") ||
1628
1798
  this.#extensionRunner?.hasHandlers("before_provider_request") ||
@@ -1656,6 +1826,7 @@ export class AgentSession {
1656
1826
  // Retry state
1657
1827
  #retryAbortController: AbortController | undefined = undefined;
1658
1828
  #retryNowRequested = false;
1829
+ #firstEventTimeoutRetryStartedAt: number | undefined;
1659
1830
  #retryAttempt = 0;
1660
1831
  #retryPromise: Promise<void> | undefined = undefined;
1661
1832
  #retryResolve: (() => void) | undefined = undefined;
@@ -1720,6 +1891,10 @@ export class AgentSession {
1720
1891
  #turnIndex = 0;
1721
1892
  #workerIntegrationScheduler: WorkerIntegrationRequestScheduler;
1722
1893
  #workerIntegrationRequestedForTurn = false;
1894
+ /** Runtime-owned team worker heartbeat; `undefined` outside a `gjc team` worker pane. */
1895
+ #teamWorkerHeartbeat: GjcTeamWorkerHeartbeatReporter | undefined;
1896
+ #teamWorkerTurnsInFlight = 0;
1897
+ #unregisterTeamWorkerAsyncJobChange: (() => void) | undefined;
1723
1898
  // First-party internal before-agent-start contributors (not user hooks).
1724
1899
  #beforeAgentStartContributors: BeforeAgentStartContributor[] = [];
1725
1900
 
@@ -1789,6 +1964,7 @@ export class AgentSession {
1789
1964
  #constructorMCPToolSelection: string[] | undefined;
1790
1965
  #constructorDiscoveredBuiltinToolSelection: string[] | undefined;
1791
1966
  #recoveryHydrationContext: RecoveryHydrationContext | undefined;
1967
+ #memoryGuardClaimsLease: MemoryGuardClaimsLease | undefined;
1792
1968
 
1793
1969
  // TTSR manager for time-traveling stream rules
1794
1970
  #ttsrManager: TtsrManager | undefined = undefined;
@@ -2073,6 +2249,8 @@ export class AgentSession {
2073
2249
  this.#promptInFlightCount++;
2074
2250
  if (this.#promptInFlightCount === 1) {
2075
2251
  this.#acquirePowerAssertion();
2252
+ // Runtime-owned worker liveness follows Agent turns and background jobs
2253
+ // in #refreshTeamWorkerHeartbeat(), including internally dispatched turns.
2076
2254
  }
2077
2255
  }
2078
2256
 
@@ -2129,6 +2307,15 @@ export class AgentSession {
2129
2307
  this.#flushPendingAgentEnd();
2130
2308
  }
2131
2309
 
2310
+ #retainDeferredAgentEndAfterContinuationBusy(
2311
+ hold: symbol | undefined,
2312
+ pending: AgentSessionEvent | undefined,
2313
+ ): void {
2314
+ if (!hold || !pending) return;
2315
+ this.#pendingAgentEndEmit = pending;
2316
+ this.#pendingAgentEndContinuationHolds.set(hold, pending);
2317
+ }
2318
+
2132
2319
  #releaseDeferredAgentEndContinuation(hold: symbol | undefined): void {
2133
2320
  if (!hold) return;
2134
2321
  const pending = this.#pendingAgentEndContinuationHolds.get(hold);
@@ -2179,11 +2366,23 @@ export class AgentSession {
2179
2366
  this.#promptInFlightCount = Math.max(0, this.#promptInFlightCount - 1);
2180
2367
  if (this.#promptInFlightCount === 0) {
2181
2368
  this.#releasePowerAssertion();
2369
+ this.#refreshTeamWorkerHeartbeat();
2182
2370
  this.#flushPendingBackgroundExchanges();
2183
2371
  this.#flushPendingAgentEnd();
2184
2372
  }
2185
2373
  }
2186
2374
 
2375
+ #refreshTeamWorkerHeartbeat(): void {
2376
+ const hasOwnedBackgroundJob =
2377
+ this.#agentId !== undefined &&
2378
+ (this.#ownedAsyncJobManager?.getRunningJobs({ ownerId: this.#agentId }).length ?? 0) > 0;
2379
+ if (this.#teamWorkerTurnsInFlight > 0 || hasOwnedBackgroundJob) {
2380
+ this.#teamWorkerHeartbeat?.start();
2381
+ } else {
2382
+ this.#teamWorkerHeartbeat?.stop();
2383
+ }
2384
+ }
2385
+
2187
2386
  #flushPendingAgentEnd(): void {
2188
2387
  if (
2189
2388
  this.#promptInFlightCount > 0 ||
@@ -2235,6 +2434,14 @@ export class AgentSession {
2235
2434
  }),
2236
2435
  config.workerIntegrationTimeoutMs,
2237
2436
  );
2437
+ // One publisher per worker process: subagent sessions share this process and its
2438
+ // team env, so letting each create a reporter would run N timers writing the same
2439
+ // record. The top-level session owns the pane's liveness.
2440
+ this.#teamWorkerHeartbeat =
2441
+ config.teamWorkerHeartbeatReporter ??
2442
+ ((config.taskDepth ?? 0) === 0
2443
+ ? GjcTeamWorkerHeartbeatReporter.forProcess(() => this.sessionManager.getCwd())
2444
+ : undefined);
2238
2445
  this.notificationSessionController = config.notificationSessionController;
2239
2446
  this.taskDepth = config.taskDepth ?? 0;
2240
2447
  // Register this session with the process-wide resource GC (idle/RSS browser-tab eviction
@@ -2244,6 +2451,7 @@ export class AgentSession {
2244
2451
  this.#unregisterResourceGc = registerResourceGcSession({
2245
2452
  sessionId: resourceGcSessionId,
2246
2453
  settings: this.settings,
2454
+ cwd: () => this.sessionManager.getCwd(),
2247
2455
  });
2248
2456
  }
2249
2457
  this.#unregisterRuntimeStateFinalizer = registerCoordinatorRuntimeStateFinalizer({
@@ -2395,6 +2603,9 @@ export class AgentSession {
2395
2603
  this.#ttsrManager = config.ttsrManager;
2396
2604
  this.#obfuscator = config.obfuscator;
2397
2605
  this.#agentId = config.agentId;
2606
+ this.#unregisterTeamWorkerAsyncJobChange = this.#ownedAsyncJobManager?.onChange(() => {
2607
+ this.#refreshTeamWorkerHeartbeat();
2608
+ });
2398
2609
  this.#agentRegistry = config.agentRegistry;
2399
2610
  this.#providerSessionId = config.providerSessionId;
2400
2611
  this.#providerCacheSessionId = config.providerCacheSessionId;
@@ -2489,7 +2700,24 @@ export class AgentSession {
2489
2700
 
2490
2701
  /** Advance the tool-choice queue and return the next directive for the upcoming LLM call. */
2491
2702
  nextToolChoice(): ToolChoice | undefined {
2492
- return this.#toolChoiceQueue.nextToolChoice();
2703
+ const choice = this.#toolChoiceQueue.nextToolChoice();
2704
+ if (!choice || typeof choice === "string") return choice;
2705
+
2706
+ const toolName = choice.type === "tool" ? choice.name : "function" in choice ? choice.function.name : choice.name;
2707
+ const activeTool = this.agent.state.tools.find(
2708
+ tool => tool.name === toolName || tool.customWireName === toolName,
2709
+ );
2710
+ if (!activeTool) {
2711
+ this.#toolChoiceQueue.degradeInFlight(`Tool "${toolName}" is no longer active.`);
2712
+ return undefined;
2713
+ }
2714
+
2715
+ const refreshed = buildNamedToolChoiceResult(activeTool.name, this.model);
2716
+ if (!refreshed.exactNamed || !refreshed.choice) {
2717
+ this.#toolChoiceQueue.degradeInFlight(refreshed.resolved?.reason ?? "Named tool choice is unavailable.");
2718
+ return undefined;
2719
+ }
2720
+ return refreshed.choice;
2493
2721
  }
2494
2722
 
2495
2723
  /**
@@ -2671,9 +2899,15 @@ export class AgentSession {
2671
2899
  this.#defaultFallbackController = scope.fallbackController;
2672
2900
  const previousEditMode = this.#resolveActiveEditMode();
2673
2901
  if (scope.model) {
2674
- this.agent.setModel(scope.model);
2902
+ this.#setAgentModelWithReasoningContext(scope.model);
2675
2903
  this.#syncAppendOnlyContext(scope.model);
2676
2904
  }
2905
+ this.#thinkingLevelMutationRevision++;
2906
+ this.#thinkingLevelLiveMutationRevision++;
2907
+ this.#pendingThinkingLevelControlSuccess = undefined;
2908
+ this.#pendingThinkingLevelControlFailure = undefined;
2909
+ this.#pendingThinkingVisibilityControlSuccess = undefined;
2910
+ this.#pendingThinkingVisibilityControlFailure = undefined;
2677
2911
  this.#thinkingLevel = scope.thinkingLevel;
2678
2912
  this.agent.setThinkingLevel(toReasoningEffort(scope.thinkingLevel));
2679
2913
  void this.#syncEditToolModeAfterModelChange(previousEditMode);
@@ -3113,19 +3347,26 @@ export class AgentSession {
3113
3347
  return queued;
3114
3348
  }
3115
3349
 
3116
- #trackAgentEvent = async (event: AgentEvent): Promise<void> => {
3350
+ #trackAgentEvent = (event: AgentEvent): Promise<void> => {
3351
+ const activePromptHandle = this.activePromptHandle;
3117
3352
  const agentEndHandled = event.type === "agent_end" ? Promise.withResolvers<void>() : undefined;
3118
3353
  if (agentEndHandled) this.#agentEndHandlingPromise = agentEndHandled.promise;
3119
3354
  this.#agentEventHandlersInFlight++;
3120
- try {
3121
- await this.#handleAgentEvent(event);
3122
- } catch (error) {
3123
- logger.warn("Agent event handler failed", { event: event.type, error: String(error) });
3124
- } finally {
3125
- this.#agentEventHandlersInFlight = Math.max(0, this.#agentEventHandlersInFlight - 1);
3126
- this.#flushPendingAgentEnd();
3127
- agentEndHandled?.resolve();
3355
+ const handler = (async (): Promise<void> => {
3356
+ try {
3357
+ await this.#handleAgentEvent(event, activePromptHandle);
3358
+ } catch (error) {
3359
+ logger.warn("Agent event handler failed", { event: event.type, error: String(error) });
3360
+ } finally {
3361
+ this.#agentEventHandlersInFlight = Math.max(0, this.#agentEventHandlersInFlight - 1);
3362
+ this.#flushPendingAgentEnd();
3363
+ agentEndHandled?.resolve();
3364
+ }
3365
+ })();
3366
+ if (activePromptHandle) {
3367
+ this.agent.resourceLedger.track(activePromptHandle, "post_prompt", "agent-session-event", handler);
3128
3368
  }
3369
+ return handler;
3129
3370
  };
3130
3371
 
3131
3372
  #persistRuntimeStateInBackground(event: AgentSessionEvent): void {
@@ -3145,6 +3386,18 @@ export class AgentSession {
3145
3386
  } else if (event.type === "turn_end") {
3146
3387
  this.#closedExtensionTurnGeneration = this.#extensionTurnGeneration;
3147
3388
  }
3389
+ if (event.type === "turn_start") {
3390
+ this.#teamWorkerTurnsInFlight++;
3391
+ this.#refreshTeamWorkerHeartbeat();
3392
+ } else if (event.type === "turn_end") {
3393
+ this.#teamWorkerTurnsInFlight = Math.max(0, this.#teamWorkerTurnsInFlight - 1);
3394
+ this.#refreshTeamWorkerHeartbeat();
3395
+ } else if (event.type === "agent_end") {
3396
+ // Failed Agent runs can emit agent_end without turn_end. Reset rather than
3397
+ // leaking a positive count that would report an idle/failed worker forever.
3398
+ this.#teamWorkerTurnsInFlight = 0;
3399
+ this.#refreshTeamWorkerHeartbeat();
3400
+ }
3148
3401
  if (event.type === "message_update") {
3149
3402
  // Fast path: message_update maps to no sidecar state, so we must not
3150
3403
  // build the persistRuntimeState closure here (per-token hot path).
@@ -3264,7 +3517,7 @@ export class AgentSession {
3264
3517
  }
3265
3518
 
3266
3519
  /** Internal handler for agent events - shared by subscribe and reconnect */
3267
- #handleAgentEvent = async (event: AgentEvent): Promise<void> => {
3520
+ #handleAgentEvent = async (event: AgentEvent, activePromptHandle?: string): Promise<void> => {
3268
3521
  if (this.#extensionRunner?.hasHandlers(event.type)) this.#markRetryReplayUnsafe();
3269
3522
  if (
3270
3523
  event.type === "tool_execution_start" ||
@@ -3613,9 +3866,10 @@ export class AgentSession {
3613
3866
  this.#ttsrAbortPending = false;
3614
3867
  this.#resolveTtsrResume();
3615
3868
  },
3869
+ resourceRunId: activePromptHandle,
3616
3870
  });
3617
3871
  },
3618
- { delayMs: 50 },
3872
+ { delayMs: 50, resourceRunId: activePromptHandle },
3619
3873
  );
3620
3874
  return;
3621
3875
  }
@@ -3774,7 +4028,11 @@ export class AgentSession {
3774
4028
  maintenanceGeneration !== undefined &&
3775
4029
  this.#promptGeneration === maintenanceGeneration
3776
4030
  ) {
3777
- this.#scheduleAgentContinue({ generation: maintenanceGeneration, skipCompactionCheck: true });
4031
+ this.#scheduleAgentContinue({
4032
+ generation: maintenanceGeneration,
4033
+ skipCompactionCheck: true,
4034
+ resourceRunId: activePromptHandle,
4035
+ });
3778
4036
  }
3779
4037
  return;
3780
4038
  }
@@ -3854,8 +4112,11 @@ export class AgentSession {
3854
4112
  }
3855
4113
  this.#resolveRetry();
3856
4114
 
3857
- const compactionTask = this.#checkCompaction(msg);
3858
- this.#trackPostPromptTask(compactionTask.then(() => undefined));
4115
+ const compactionTask = this.#checkCompaction(msg, true, undefined, activePromptHandle);
4116
+ this.#trackPostPromptTask(
4117
+ compactionTask.then(() => undefined),
4118
+ activePromptHandle,
4119
+ );
3859
4120
  await compactionTask;
3860
4121
  // Check for incomplete todos only after a final assistant stop, not intermediate tool-use turns.
3861
4122
  const hasToolCalls = msg.content.some(content => content.type === "toolCall");
@@ -3919,9 +4180,12 @@ export class AgentSession {
3919
4180
  this.#postPromptTasksPromise = undefined;
3920
4181
  }
3921
4182
 
3922
- #trackPostPromptTask(task: Promise<void>): void {
4183
+ #trackPostPromptTask(task: Promise<void>, resourceRunId = this.activePromptHandle): void {
3923
4184
  this.#postPromptTasks.add(task);
3924
4185
  this.#ensurePostPromptTasksPromise();
4186
+ if (resourceRunId) {
4187
+ this.agent.resourceLedger.track(resourceRunId, "post_prompt", "agent-session", task);
4188
+ }
3925
4189
  void task
3926
4190
  .catch(() => {})
3927
4191
  .finally(() => {
@@ -3934,7 +4198,7 @@ export class AgentSession {
3934
4198
 
3935
4199
  #schedulePostPromptTask(
3936
4200
  task: (signal: AbortSignal) => Promise<void>,
3937
- options?: { delayMs?: number; generation?: number; onSkip?: () => void },
4201
+ options?: { delayMs?: number; generation?: number; onSkip?: () => void; resourceRunId?: string },
3938
4202
  ): Promise<void> {
3939
4203
  const delayMs = options?.delayMs ?? 0;
3940
4204
  const signal = this.#postPromptTasksAbortController.signal;
@@ -3957,10 +4221,14 @@ export class AgentSession {
3957
4221
  }
3958
4222
  await task(signal);
3959
4223
  })();
3960
- this.#trackPostPromptTask(scheduled);
4224
+ this.#trackPostPromptTask(scheduled, options?.resourceRunId);
3961
4225
  return scheduled;
3962
4226
  }
3963
4227
 
4228
+ #isAgentBusyError(error: unknown): error is AgentBusyError {
4229
+ return error instanceof AgentBusyError;
4230
+ }
4231
+
3964
4232
  #scheduleAgentContinue(options?: {
3965
4233
  delayMs?: number;
3966
4234
  generation?: number;
@@ -3969,7 +4237,9 @@ export class AgentSession {
3969
4237
  shouldContinue?: () => boolean;
3970
4238
  onSkip?: (reason: "generation_changed" | "aborted_signal" | "queue_drained" | "handoff_in_progress") => void;
3971
4239
  allowDuringCancelAndSubmit?: boolean;
4240
+ rescheduleOnBusy?: boolean;
3972
4241
  onError?: (error: unknown) => void;
4242
+ resourceRunId?: string;
3973
4243
  }): Promise<void> {
3974
4244
  const predecessorAgentEndHold = options?.suppressPredecessorAgentEnd
3975
4245
  ? this.#reserveDeferredAgentEndForContinuation()
@@ -3992,81 +4262,92 @@ export class AgentSession {
3992
4262
  };
3993
4263
  const scheduledGeneration = options?.generation;
3994
4264
  const signal = this.#postPromptTasksAbortController.signal;
3995
- return this.#schedulePostPromptTask(
3996
- async () => {
3997
- const canContinue = (): boolean => {
3998
- if (signal.aborted || this.#isDisposed) {
3999
- skip("aborted_signal");
4000
- return false;
4001
- }
4002
- if (scheduledGeneration !== undefined && this.#promptGeneration !== scheduledGeneration) {
4003
- skip("generation_changed");
4004
- return false;
4005
- }
4006
- if (this.#cancelAndSubmitInProgress && !options?.allowDuringCancelAndSubmit) {
4007
- skip("queue_drained");
4008
- return false;
4009
- }
4010
- if (options?.shouldContinue && !options.shouldContinue()) {
4011
- skip("queue_drained");
4012
- return false;
4013
- }
4014
- return true;
4015
- };
4016
- if (!canContinue()) return;
4017
- try {
4018
- if (!options?.skipCompactionCheck) {
4019
- await this.#checkEstimatedContextBeforePrompt();
4020
- if (!canContinue()) return;
4021
- }
4022
- if (signal.aborted) {
4023
- skip("aborted_signal");
4024
- return;
4025
- }
4026
- if (scheduledGeneration !== undefined && this.#promptGeneration !== scheduledGeneration) {
4027
- skip("generation_changed");
4028
- return;
4029
- }
4030
- if (options?.shouldContinue && !options.shouldContinue()) {
4031
- skip("queue_drained");
4032
- return;
4033
- }
4034
- // A continuation scheduled before a handoff engaged must not start a
4035
- // turn against the session being handed off (or the restored
4036
- // predecessor). rearmIdle / normal delivery resumes after the fence.
4037
- if (this.#handoffTransitionActive) {
4038
- skip("handoff_in_progress");
4039
- return;
4040
- }
4041
- const predecessorAgentEnd = this.#claimDeferredAgentEndForContinuation(predecessorAgentEndHold);
4042
- const startsQueuedSuccessor =
4043
- this.agent.state.messages.at(-1)?.role === "assistant" && this.agent.hasQueuedMessages();
4265
+ const scheduleAttempt = (delayMs = options?.delayMs): Promise<void> =>
4266
+ this.#schedulePostPromptTask(
4267
+ async () => {
4268
+ const canContinue = (): boolean => {
4269
+ if (signal.aborted || this.#isDisposed) {
4270
+ skip("aborted_signal");
4271
+ return false;
4272
+ }
4273
+ if (scheduledGeneration !== undefined && this.#promptGeneration !== scheduledGeneration) {
4274
+ skip("generation_changed");
4275
+ return false;
4276
+ }
4277
+ if (this.#cancelAndSubmitInProgress && !options?.allowDuringCancelAndSubmit) {
4278
+ skip("queue_drained");
4279
+ return false;
4280
+ }
4281
+ if (options?.shouldContinue && !options.shouldContinue()) {
4282
+ skip("queue_drained");
4283
+ return false;
4284
+ }
4285
+ return true;
4286
+ };
4287
+ if (!canContinue()) return;
4044
4288
  try {
4045
- await this.agent.continue({
4046
- ...this.#managedFallbackPromptOptions(),
4047
- // Reset only after continue() has claimed the queued turn. Skipped or stale
4048
- // continuations retain predecessor accounting, and resetAttemptBudget keeps
4049
- // the sticky fallback cursor unchanged.
4050
- onRunAccepted: startsQueuedSuccessor
4051
- ? () => {
4052
- this.#defaultFallbackChain().resetAttemptBudget();
4053
- this.#overflowMaintenanceAttempts = 0;
4054
- }
4055
- : undefined,
4056
- });
4289
+ if (!options?.skipCompactionCheck) {
4290
+ await this.#checkEstimatedContextBeforePrompt();
4291
+ if (!canContinue()) return;
4292
+ }
4293
+ if (signal.aborted) {
4294
+ skip("aborted_signal");
4295
+ return;
4296
+ }
4297
+ if (scheduledGeneration !== undefined && this.#promptGeneration !== scheduledGeneration) {
4298
+ skip("generation_changed");
4299
+ return;
4300
+ }
4301
+ if (options?.shouldContinue && !options.shouldContinue()) {
4302
+ skip("queue_drained");
4303
+ return;
4304
+ }
4305
+ // A continuation scheduled before a handoff engaged must not start a
4306
+ // turn against the session being handed off (or the restored
4307
+ // predecessor). rearmIdle / normal delivery resumes after the fence.
4308
+ if (this.#handoffTransitionActive) {
4309
+ skip("handoff_in_progress");
4310
+ return;
4311
+ }
4312
+ const predecessorAgentEnd = this.#claimDeferredAgentEndForContinuation(predecessorAgentEndHold);
4313
+ const startsQueuedSuccessor =
4314
+ this.agent.state.messages.at(-1)?.role === "assistant" && this.agent.hasQueuedMessages();
4315
+ try {
4316
+ await this.agent.continue({
4317
+ ...this.#managedFallbackPromptOptions(),
4318
+ // Reset only after continue() has claimed the queued turn. Skipped or stale
4319
+ // continuations retain predecessor accounting, and resetAttemptBudget keeps
4320
+ // the sticky fallback cursor unchanged.
4321
+ onRunAccepted: startsQueuedSuccessor
4322
+ ? () => {
4323
+ this.#defaultFallbackChain().resetAttemptBudget();
4324
+ this.#overflowMaintenanceAttempts = 0;
4325
+ }
4326
+ : undefined,
4327
+ });
4328
+ } catch (error) {
4329
+ if (options?.rescheduleOnBusy && this.#isAgentBusyError(error) && !terminalized && canContinue()) {
4330
+ this.#retainDeferredAgentEndAfterContinuationBusy(predecessorAgentEndHold, predecessorAgentEnd);
4331
+ logger.debug("agent.continue busy after scheduling; rescheduling", {
4332
+ error: error.message,
4333
+ });
4334
+ void scheduleAttempt(100);
4335
+ return;
4336
+ }
4337
+ this.#restoreDeferredAgentEndAfterContinuationFailure(predecessorAgentEnd);
4338
+ throw error;
4339
+ }
4057
4340
  } catch (error) {
4058
- this.#restoreDeferredAgentEndAfterContinuationFailure(predecessorAgentEnd);
4059
- throw error;
4341
+ fail(error);
4060
4342
  }
4061
- } catch (error) {
4062
- fail(error);
4063
- }
4064
- },
4065
- {
4066
- delayMs: options?.delayMs,
4067
- onSkip: () => skip("aborted_signal"),
4068
- },
4069
- );
4343
+ },
4344
+ {
4345
+ delayMs,
4346
+ onSkip: () => skip("aborted_signal"),
4347
+ resourceRunId: options?.resourceRunId,
4348
+ },
4349
+ );
4350
+ return scheduleAttempt();
4070
4351
  }
4071
4352
 
4072
4353
  #logCompactionContinuationSkipped(
@@ -4082,7 +4363,7 @@ export class AgentSession {
4082
4363
  ): void {
4083
4364
  logger.warn("Auto-compaction continuation failed", {
4084
4365
  source,
4085
- reason: error instanceof Error && error.name === "AgentBusyError" ? "queue_drained" : "not_resumable_tail",
4366
+ reason: "not_resumable_tail",
4086
4367
  error: error instanceof Error ? error.message : String(error),
4087
4368
  });
4088
4369
  }
@@ -4110,14 +4391,24 @@ export class AgentSession {
4110
4391
  return compactionSettings.autoContinue === false ? "auto_continue_disabled_non_resumable_tail" : undefined;
4111
4392
  }
4112
4393
 
4113
- #scheduleOverflowRetryContinuation(generation: number): boolean {
4394
+ async #scheduleOverflowRetryContinuation(generation: number, resourceRunId?: string): Promise<boolean> {
4114
4395
  this.#stripOverflowFailedTurnForRetry();
4396
+ const snapshot = await this.#compactionStateSnapshot();
4397
+ if (
4398
+ snapshot.goal?.status === "paused" &&
4399
+ !snapshot.queuedMessages &&
4400
+ snapshot.lastAssistantStopReason !== "length"
4401
+ ) {
4402
+ this.#logCompactionContinuationSkipped("overflow_retry", "paused_goal");
4403
+ return false;
4404
+ }
4115
4405
  if (this.#isResumableAgentTail()) {
4116
4406
  this.#scheduleAgentContinue({
4117
4407
  delayMs: 100,
4118
4408
  generation,
4119
4409
  suppressPredecessorAgentEnd: true,
4120
-
4410
+ resourceRunId,
4411
+ rescheduleOnBusy: true,
4121
4412
  onSkip: reason => this.#logCompactionContinuationSkipped("overflow_retry", reason),
4122
4413
  onError: error => this.#logCompactionContinuationError("overflow_retry", error),
4123
4414
  });
@@ -4126,7 +4417,7 @@ export class AgentSession {
4126
4417
 
4127
4418
  const compactionSettings = this.settings.getGroup("compaction");
4128
4419
  if (compactionSettings.autoContinue !== false) {
4129
- this.#scheduleAutoContinuePrompt(generation);
4420
+ this.#scheduleAutoContinuePrompt(generation, false);
4130
4421
  return true;
4131
4422
  }
4132
4423
 
@@ -4134,8 +4425,32 @@ export class AgentSession {
4134
4425
  return false;
4135
4426
  }
4136
4427
 
4137
- #scheduleAutoContinuePrompt(generation: number): void {
4428
+ #scheduleAutoContinuePrompt(generation: number, requireUnfinishedWork = true): void {
4138
4429
  const predecessorAgentEndHold = this.#reserveDeferredAgentEndForContinuation();
4430
+ const scheduledGeneration = generation;
4431
+ const signal = this.#postPromptTasksAbortController.signal;
4432
+ const continuationAuthorized = async (hasPendingNextTurnMessages = false): Promise<boolean> => {
4433
+ const snapshot = await this.#compactionStateSnapshot();
4434
+ if (signal.aborted) {
4435
+ this.#logCompactionContinuationSkipped("auto_continue_prompt", "aborted_signal");
4436
+ return false;
4437
+ }
4438
+ if (this.#isDisposed || this.#promptGeneration !== scheduledGeneration) {
4439
+ this.#logCompactionContinuationSkipped(
4440
+ "auto_continue_prompt",
4441
+ this.#isDisposed ? "session_disposed" : "generation_changed",
4442
+ );
4443
+ return false;
4444
+ }
4445
+ const authorized = requireUnfinishedWork
4446
+ ? this.#hasUnfinishedWork(snapshot) || hasPendingNextTurnMessages
4447
+ : snapshot.queuedMessages ||
4448
+ hasPendingNextTurnMessages ||
4449
+ snapshot.lastAssistantStopReason === "length" ||
4450
+ snapshot.goal?.status !== "paused";
4451
+ if (!authorized) this.emitNotice("info", "Auto-continue skipped: no unfinished work detected");
4452
+ return authorized;
4453
+ };
4139
4454
  const continuePrompt = async () => {
4140
4455
  await this.#promptWithMessage(
4141
4456
  {
@@ -4149,14 +4464,15 @@ export class AgentSession {
4149
4464
  skipPostPromptRecoveryWait: true,
4150
4465
  skipCompactionCheck: true,
4151
4466
  predecessorAgentEndHold,
4467
+ onFinalPreflight: ({ hasPendingNextTurnMessages }) => continuationAuthorized(hasPendingNextTurnMessages),
4152
4468
  },
4153
4469
  );
4154
4470
  };
4155
- const scheduledGeneration = generation;
4156
- const signal = this.#postPromptTasksAbortController.signal;
4157
- this.#trackPostPromptTask(
4158
- (async () => {
4471
+ const scheduleAttempt = (delayMs = 0) => {
4472
+ const attempt = (async () => {
4473
+ let rescheduled = false;
4159
4474
  try {
4475
+ if (delayMs > 0) await scheduler.wait(delayMs, { signal });
4160
4476
  await Promise.resolve();
4161
4477
  if (signal.aborted) {
4162
4478
  this.#logCompactionContinuationSkipped("auto_continue_prompt", "aborted_signal");
@@ -4166,14 +4482,31 @@ export class AgentSession {
4166
4482
  this.#logCompactionContinuationSkipped("auto_continue_prompt", "generation_changed");
4167
4483
  return;
4168
4484
  }
4485
+ if (!(await continuationAuthorized())) return;
4169
4486
  await continuePrompt();
4170
4487
  } catch (error) {
4488
+ if (signal.aborted) {
4489
+ this.#logCompactionContinuationSkipped("auto_continue_prompt", "aborted_signal");
4490
+ return;
4491
+ }
4492
+ if (this.#isAgentBusyError(error) && this.#promptGeneration === scheduledGeneration) {
4493
+ logger.debug("Auto-compaction continuation busy; rescheduling", {
4494
+ source: "auto_continue_prompt",
4495
+ reason: "queue_drained",
4496
+ error: error.message,
4497
+ });
4498
+ rescheduled = true;
4499
+ scheduleAttempt(100);
4500
+ return;
4501
+ }
4171
4502
  this.#logCompactionContinuationError("auto_continue_prompt", error);
4172
4503
  } finally {
4173
- this.#releaseDeferredAgentEndContinuation(predecessorAgentEndHold);
4504
+ if (!rescheduled) this.#releaseDeferredAgentEndContinuation(predecessorAgentEndHold);
4174
4505
  }
4175
- })(),
4176
- );
4506
+ })();
4507
+ this.#trackPostPromptTask(attempt);
4508
+ };
4509
+ scheduleAttempt();
4177
4510
  }
4178
4511
 
4179
4512
  async #cancelPostPromptTasks(): Promise<void> {
@@ -4666,6 +4999,19 @@ export class AgentSession {
4666
4999
  };
4667
5000
  }
4668
5001
 
5002
+ /** Defer local:// provisioning when a read-only session load cannot create its artifact root. */
5003
+ async #initializeLocalRootForLoadedSession(): Promise<void> {
5004
+ try {
5005
+ await initializeLocalRoot(this.#localProtocolOptions());
5006
+ } catch (error) {
5007
+ if (!isEacces(error) && !hasFsCode(error, "EPERM") && !hasFsCode(error, "EROFS")) throw error;
5008
+ logger.debug("Deferred local root initialization for read-only session load", {
5009
+ sessionFile: this.sessionManager.getSessionFile(),
5010
+ error: String(error),
5011
+ });
5012
+ }
5013
+ }
5014
+
4669
5015
  #maybeAbortStreamingEdit(event: AgentEvent, generation: number): void {
4670
5016
  if (!this.settings.get("edit.streamingAbort")) return;
4671
5017
  if (this.#streamingEditAbortTriggered) return;
@@ -5117,6 +5463,7 @@ export class AgentSession {
5117
5463
  * such event.
5118
5464
  */
5119
5465
  #syncAgentSessionId(sessionId?: string): void {
5466
+ this.#reasoningControlContextGeneration++;
5120
5467
  const sid = this.#providerSessionId ?? sessionId ?? this.sessionManager.getSessionId();
5121
5468
  this.agent.sessionId = sid;
5122
5469
  this.agent.providerSessionId = this.#providerCacheSessionId ?? sid;
@@ -5250,6 +5597,9 @@ export class AgentSession {
5250
5597
  await disposeKernelSessionsByOwner(this.#evalKernelOwnerId);
5251
5598
  await disposeVmContextsByOwner(this.#evalKernelOwnerId);
5252
5599
  this.#releasePowerAssertion();
5600
+ this.#unregisterTeamWorkerAsyncJobChange?.();
5601
+ this.#unregisterTeamWorkerAsyncJobChange = undefined;
5602
+ this.#teamWorkerHeartbeat?.dispose();
5253
5603
  // Disconnect the agent event listener BEFORE closing session resources so a late
5254
5604
  // provider/tool message_end cannot append to the closing SessionManager.
5255
5605
  this.#disconnectFromAgent();
@@ -5294,6 +5644,9 @@ export class AgentSession {
5294
5644
  const kernelOwnerId = this.#evalKernelOwnerId;
5295
5645
  this.#unregisterResourceGc?.();
5296
5646
  this.#unregisterResourceGc = undefined;
5647
+ this.#unregisterTeamWorkerAsyncJobChange?.();
5648
+ this.#unregisterTeamWorkerAsyncJobChange = undefined;
5649
+ this.#teamWorkerHeartbeat?.dispose();
5297
5650
  const work = Promise.allSettled([
5298
5651
  // kill:true so a forced exit also reaps spawned-app Chrome we own (headless
5299
5652
  // always closes; connected/attached browsers only disconnect — never killed).
@@ -6433,6 +6786,11 @@ export class AgentSession {
6433
6786
  return this.#postPromptTasks.size > 0;
6434
6787
  }
6435
6788
 
6789
+ /** Stable resource ownership identifier for the active prompt run. */
6790
+ get activePromptHandle(): string | undefined {
6791
+ return this.agent.activeResourceRunId;
6792
+ }
6793
+
6436
6794
  /** All messages including custom types like BashExecutionMessage */
6437
6795
  get messages(): AgentMessage[] {
6438
6796
  return this.agent.state.messages;
@@ -6441,17 +6799,65 @@ export class AgentSession {
6441
6799
  return this.#promptGeneration;
6442
6800
  }
6443
6801
 
6802
+ static async restoreFromMemoryGuardCheckpoint(
6803
+ input: AgentSessionMemoryGuardRestoreInput,
6804
+ ): Promise<AgentMemoryGuardRestoreResult> {
6805
+ const { staged, claimsLease, claimsStateDir, ...config } = input;
6806
+ if (
6807
+ staged.manager.getSessionId() !== staged.transcriptIdentity.sessionId ||
6808
+ staged.hydrationContext.identity.sessionId !== staged.transcriptIdentity.sessionId
6809
+ ) {
6810
+ await staged.cleanup();
6811
+ return { kind: "blocked", reason: "transcript-mismatch" };
6812
+ }
6813
+ if (
6814
+ !isMemoryGuardClaimsLeaseForStateDir(claimsLease, claimsStateDir) ||
6815
+ claimsLease.owner.sessionId !== staged.transcriptIdentity.sessionId
6816
+ ) {
6817
+ await staged.cleanup();
6818
+ return { kind: "blocked", reason: "claim-mismatch" };
6819
+ }
6820
+ const checkpointMessages = staged.manager.buildSessionContext().messages;
6821
+ if (!util.isDeepStrictEqual(config.agent.state.messages, checkpointMessages)) {
6822
+ await staged.cleanup();
6823
+ return { kind: "blocked", reason: "agent-messages-mismatch" };
6824
+ }
6825
+ const session = new AgentSession({
6826
+ ...config,
6827
+ sessionManager: staged.manager,
6828
+ recoveryHydrationContext: staged.hydrationContext,
6829
+ });
6830
+ session.#memoryGuardClaimsLease = claimsLease;
6831
+ if (session.recoveryHydrationContext !== staged.hydrationContext) {
6832
+ await session.dispose();
6833
+ await staged.cleanup();
6834
+ return { kind: "blocked", reason: "hydration-context-mismatch" };
6835
+ }
6836
+ return {
6837
+ kind: "staged",
6838
+ session,
6839
+ promotionFence: Object.freeze({ ownershipReady: true, claimsLease }),
6840
+ };
6841
+ }
6842
+
6444
6843
  /** The immutable recovery authority, present only before external ownership promotion. */
6445
6844
  get recoveryHydrationContext(): RecoveryHydrationContext | undefined {
6446
6845
  return this.#recoveryHydrationContext;
6447
6846
  }
6448
6847
 
6449
6848
  /** Enables normal session mutations after the owner has published its durable fence and writer lease. */
6450
- async promoteRecoveryHydrationAfterOwnershipReadyFence(fence: RecoveryHydrationPromotionFence): Promise<void> {
6849
+ async promoteRecoveryHydrationAfterOwnershipReadyFence(fence: AgentMemoryGuardPromotionFence): Promise<void> {
6451
6850
  const context = this.#recoveryHydrationContext;
6452
6851
  if (!context) throw new Error("Agent session is not awaiting recovery hydration promotion.");
6852
+ if (!isMemoryGuardClaimsLease(fence.claimsLease)) {
6853
+ throw new Error("Recovery hydration promotion requires a live memory-guard claims lease.");
6854
+ }
6855
+ if (this.#memoryGuardClaimsLease !== fence.claimsLease) {
6856
+ throw new Error("Recovery hydration promotion requires the acquired memory-guard claims lease.");
6857
+ }
6453
6858
  await this.sessionManager.promoteRecoveryHydrationAfterOwnershipReadyFence(context, fence);
6454
6859
  this.#recoveryHydrationContext = undefined;
6860
+ this.#memoryGuardClaimsLease = undefined;
6455
6861
  }
6456
6862
 
6457
6863
  /** Recovery hydration must not start a continuation before ownership promotion. */
@@ -6662,7 +7068,7 @@ export class AgentSession {
6662
7068
  async invokeSkill(
6663
7069
  name: string,
6664
7070
  args = "",
6665
- options?: Pick<PromptOptions, "onPreflightAccepted">,
7071
+ options?: Pick<PromptOptions, "onPreflightAccepted" | "onPreflightAcceptCommit" | "onSkillPrepared">,
6666
7072
  ): Promise<{ name: string; path: string; args?: string; lineCount?: number }> {
6667
7073
  const skillName = name.trim();
6668
7074
  if (!skillName) throw Object.assign(new Error("skill.invoke requires a skill name."), { code: "invalid_input" });
@@ -6695,6 +7101,12 @@ export class AgentSession {
6695
7101
  attribution: "user" as const,
6696
7102
  };
6697
7103
  this.#deepInterviewPreclaimedCustomInputEpochs.set(skillPromptMessage, deepInterviewUserIntentEpoch);
7104
+ options?.onSkillPrepared?.({
7105
+ name: skill.name,
7106
+ path: skill.filePath,
7107
+ lineCount: built.details.lineCount,
7108
+ cleanedArgs: activation.cleanedArgs || undefined,
7109
+ });
6698
7110
  await this.promptCustomMessage(skillPromptMessage, options);
6699
7111
  return {
6700
7112
  name: skill.name,
@@ -7350,6 +7762,7 @@ export class AgentSession {
7350
7762
  * @throws Error if no model selected or no API key available (when not streaming)
7351
7763
  */
7352
7764
  async prompt(text: string, options?: PromptOptions): Promise<void> {
7765
+ this.#assertRecoveryHydrationPromoted();
7353
7766
  const expandPromptTemplates = options?.expandPromptTemplates ?? true;
7354
7767
 
7355
7768
  if (expandPromptTemplates && text.startsWith("/skill:") && !options?.images?.length) {
@@ -7361,7 +7774,14 @@ export class AgentSession {
7361
7774
  await this.invokeSkill(
7362
7775
  invocation.skill.name,
7363
7776
  invocation.args,
7364
- options?.onPreflightAccepted ? { onPreflightAccepted: options.onPreflightAccepted } : undefined,
7777
+ options?.onPreflightAccepted || options?.onPreflightAcceptCommit
7778
+ ? {
7779
+ ...(options.onPreflightAccepted ? { onPreflightAccepted: options.onPreflightAccepted } : {}),
7780
+ ...(options.onPreflightAcceptCommit
7781
+ ? { onPreflightAcceptCommit: options.onPreflightAcceptCommit }
7782
+ : {}),
7783
+ }
7784
+ : undefined,
7365
7785
  );
7366
7786
  return;
7367
7787
  }
@@ -7415,7 +7835,8 @@ export class AgentSession {
7415
7835
  if (workflowIntentDiff) {
7416
7836
  this.sessionManager.appendCustomEntry(WORKFLOW_INTENT_DIFF_CUSTOM_TYPE, workflowIntentDiff);
7417
7837
  }
7418
- options?.onPreflightAccepted?.();
7838
+ if (options?.onPreflightAcceptCommit) await options.onPreflightAcceptCommit();
7839
+ else options?.onPreflightAccepted?.();
7419
7840
  return;
7420
7841
  }
7421
7842
 
@@ -7547,7 +7968,10 @@ export class AgentSession {
7547
7968
 
7548
7969
  async promptCustomMessage<T = unknown>(
7549
7970
  message: Pick<CustomMessage<T>, "customType" | "content" | "display" | "details" | "attribution">,
7550
- options?: Pick<PromptOptions, "streamingBehavior" | "toolChoice" | "followUpQueuePolicy" | "onPreflightAccepted">,
7971
+ options?: Pick<
7972
+ PromptOptions,
7973
+ "streamingBehavior" | "toolChoice" | "followUpQueuePolicy" | "onPreflightAccepted" | "onPreflightAcceptCommit"
7974
+ >,
7551
7975
  ): Promise<void> {
7552
7976
  const textContent =
7553
7977
  typeof message.content === "string"
@@ -7609,12 +8033,16 @@ export class AgentSession {
7609
8033
  async #promptWithMessage(
7610
8034
  message: AgentMessage,
7611
8035
  expandedText: string,
7612
- options?: Pick<PromptOptions, "toolChoice" | "images" | "skipCompactionCheck" | "onPreflightAccepted"> & {
8036
+ options?: Pick<
8037
+ PromptOptions,
8038
+ "toolChoice" | "images" | "skipCompactionCheck" | "onPreflightAccepted" | "onPreflightAcceptCommit"
8039
+ > & {
7613
8040
  prependMessages?: AgentMessage[];
7614
8041
  skipPostPromptRecoveryWait?: boolean;
7615
8042
  predecessorAgentEndHold?: symbol;
7616
8043
  admissionLease?: SessionAdmissionLease;
7617
8044
  onRunAccepted?: () => void;
8045
+ onFinalPreflight?: (context: { hasPendingNextTurnMessages: boolean }) => Promise<boolean>;
7618
8046
  resetRetryReplaySafety?: boolean;
7619
8047
  },
7620
8048
  ): Promise<void> {
@@ -7630,6 +8058,8 @@ export class AgentSession {
7630
8058
  const generation = this.#promptGeneration;
7631
8059
  const preflightSignal = this.#promptPreflightAbortController.signal;
7632
8060
  const rosterClaim = this.#claimIrcRosterCandidate();
8061
+ let hasPendingNextTurnMessages = false;
8062
+ let pendingNextTurnMessageCount = 0;
7633
8063
  let hindsightRecall: string | undefined;
7634
8064
  try {
7635
8065
  this.#throwIfPromptPreflightCancelled(generation, preflightSignal);
@@ -7716,16 +8146,16 @@ export class AgentSession {
7716
8146
  // A newer abort/prompt cycle superseded this preflight. Callers awaiting
7717
8147
  // acceptance (onPreflightAccepted) must be told it never ran; direct
7718
8148
  // callers (e.g. prompt() aborted during a TTSR wait) resolve gracefully.
7719
- if (options?.onPreflightAccepted) throw promptPreflightCancelledError();
8149
+ if (options?.onPreflightAccepted || options?.onPreflightAcceptCommit) throw promptPreflightCancelledError();
7720
8150
  return;
7721
8151
  }
7722
8152
 
7723
8153
  // Inject any pending "nextTurn" messages as context alongside the user message
7724
- for (const msg of this.#pendingNextTurnMessages) {
8154
+ pendingNextTurnMessageCount = this.#pendingNextTurnMessages.length;
8155
+ hasPendingNextTurnMessages = pendingNextTurnMessageCount > 0;
8156
+ for (const msg of this.#pendingNextTurnMessages.slice(0, pendingNextTurnMessageCount)) {
7725
8157
  messages.push(msg);
7726
8158
  }
7727
- this.#pendingNextTurnMessages = [];
7728
- if (this.#cancelAndSubmitInProgress) this.#cancelAndSubmitPendingNextTurnDrained = true;
7729
8159
 
7730
8160
  // Auto-read @filepath mentions
7731
8161
  const fileMentions = extractFileMentions(expandedText);
@@ -7830,7 +8260,7 @@ export class AgentSession {
7830
8260
  this.#resetInjectedContextSignatures();
7831
8261
  // Ack-waiting callers are told the preflight never ran; direct callers
7832
8262
  // (aborted after setup) resolve gracefully as before f24f46ff5.
7833
- if (options?.onPreflightAccepted) throw promptPreflightCancelledError();
8263
+ if (options?.onPreflightAccepted || options?.onPreflightAcceptCommit) throw promptPreflightCancelledError();
7834
8264
  return;
7835
8265
  }
7836
8266
 
@@ -7843,8 +8273,17 @@ export class AgentSession {
7843
8273
  if (hindsightRecall) this.getHindsightSessionState()?.markRecallSnippetInjected(hindsightRecall);
7844
8274
  },
7845
8275
  };
7846
- options?.onPreflightAccepted?.();
8276
+ if (options?.onPreflightAcceptCommit) await options.onPreflightAcceptCommit();
8277
+ else options?.onPreflightAccepted?.();
8278
+ if (options?.onFinalPreflight && !(await options.onFinalPreflight({ hasPendingNextTurnMessages }))) {
8279
+ this.#resetInjectedContextSignatures();
8280
+ return;
8281
+ }
7847
8282
  this.#throwIfPromptPreflightCancelled(generation, preflightSignal);
8283
+ if (pendingNextTurnMessageCount > 0) {
8284
+ this.#pendingNextTurnMessages.splice(0, pendingNextTurnMessageCount);
8285
+ }
8286
+ if (this.#cancelAndSubmitInProgress) this.#cancelAndSubmitPendingNextTurnDrained = true;
7848
8287
  await this.#promptAgentWithIdleRetry(messages, agentPromptOptions, predecessorAgentEndHold);
7849
8288
  const terminalAssistant = this.#findLastAssistantMessage();
7850
8289
  if (
@@ -7862,7 +8301,12 @@ export class AgentSession {
7862
8301
  // Session identity changes historically cancel local setup silently. Only SDK
7863
8302
  // submissions provide an acceptance callback and require an explicit terminal
7864
8303
  // preflight failure for their remote request authority.
7865
- if (isPromptPreflightCancelledError(error) && !options?.onPreflightAccepted) return;
8304
+ if (
8305
+ isPromptPreflightCancelledError(error) &&
8306
+ !options?.onPreflightAccepted &&
8307
+ !options?.onPreflightAcceptCommit
8308
+ )
8309
+ return;
7866
8310
  throw error;
7867
8311
  } finally {
7868
8312
  this.#removeEphemeralCustomMessages();
@@ -7880,6 +8324,7 @@ export class AgentSession {
7880
8324
  await this.#agentEndPublicationPromise;
7881
8325
  } else {
7882
8326
  await this.#agentEndHandlingPromise;
8327
+ await this.#waitForPostPromptRecovery();
7883
8328
  await this.#agentEndPublicationPromise;
7884
8329
  }
7885
8330
  }
@@ -7928,6 +8373,7 @@ export class AgentSession {
7928
8373
  sessionManager: createReadonlySessionManager(this.sessionManager),
7929
8374
  modelRegistry: this.#modelRegistry,
7930
8375
  model: this.model ?? undefined,
8376
+ getActivePromptHandle: () => this.activePromptHandle,
7931
8377
  isIdle: () => !this.isStreaming,
7932
8378
  abort: () => {
7933
8379
  void this.abort();
@@ -7963,7 +8409,7 @@ export class AgentSession {
7963
8409
  }
7964
8410
  return false;
7965
8411
  },
7966
- invokeSkill: (name, args) => this.invokeSkill(name, args),
8412
+ invokeSkill: (name, args, options) => this.invokeSkill(name, args, options),
7967
8413
  setPlanMode: on => this.setSdkPlanMode(on),
7968
8414
  operateGoal: (op, objective) => this.operateGoal(op, objective),
7969
8415
  getSkillState: () => this.skills.map(skill => ({ name: skill.name, description: skill.description })),
@@ -8078,6 +8524,7 @@ export class AgentSession {
8078
8524
  * Queue a steering message to interrupt the agent mid-run.
8079
8525
  */
8080
8526
  async steer(text: string, images?: ImageContent[]): Promise<void> {
8527
+ this.#assertRecoveryHydrationPromoted();
8081
8528
  if (text.startsWith("/")) {
8082
8529
  this.#throwIfExtensionCommand(text);
8083
8530
  }
@@ -8095,6 +8542,7 @@ export class AgentSession {
8095
8542
  images?: ImageContent[],
8096
8543
  options?: Pick<PromptOptions, "followUpQueuePolicy">,
8097
8544
  ): Promise<void> {
8545
+ this.#assertRecoveryHydrationPromoted();
8098
8546
  if (text.startsWith("/")) {
8099
8547
  this.#throwIfExtensionCommand(text);
8100
8548
  }
@@ -8329,6 +8777,7 @@ export class AgentSession {
8329
8777
  followUpQueuePolicy?: "respect-mode" | "sequential";
8330
8778
  },
8331
8779
  ): Promise<void> {
8780
+ this.#assertRecoveryHydrationPromoted();
8332
8781
  const appMessage: CustomMessage<T> = {
8333
8782
  role: "custom",
8334
8783
  customType: message.customType,
@@ -8470,8 +8919,13 @@ export class AgentSession {
8470
8919
  */
8471
8920
  async sendUserMessage(
8472
8921
  content: string | (TextContent | ImageContent)[],
8473
- options?: { deliverAs?: "steer" | "followUp"; onPreflightAccepted?: () => void },
8922
+ options?: {
8923
+ deliverAs?: "steer" | "followUp";
8924
+ onPreflightAccepted?: () => void;
8925
+ onPreflightAcceptCommit?: () => void | Promise<void>;
8926
+ },
8474
8927
  ): Promise<void> {
8928
+ this.#assertRecoveryHydrationPromoted();
8475
8929
  // Normalize content to text string + optional images
8476
8930
  let text: string;
8477
8931
  let images: ImageContent[] | undefined;
@@ -8493,11 +8947,13 @@ export class AgentSession {
8493
8947
  }
8494
8948
 
8495
8949
  if (options?.deliverAs === "followUp") {
8950
+ if (options.onPreflightAcceptCommit) await options.onPreflightAcceptCommit();
8496
8951
  await this.#queueFollowUp(text, images, { claimsGenuineUserIntent: true });
8497
8952
  options.onPreflightAccepted?.();
8498
8953
  return;
8499
8954
  }
8500
8955
  if (options?.deliverAs === "steer") {
8956
+ if (options.onPreflightAcceptCommit) await options.onPreflightAcceptCommit();
8501
8957
  await this.#queueSteer(text, images, { claimsGenuineUserIntent: true });
8502
8958
  options.onPreflightAccepted?.();
8503
8959
  return;
@@ -8509,6 +8965,7 @@ export class AgentSession {
8509
8965
  // in-flight compaction internally, and #queueSteer would otherwise park
8510
8966
  // the message in the steering queue with no turn to consume it.
8511
8967
  if (this.isStreaming) {
8968
+ if (options?.onPreflightAcceptCommit) await options.onPreflightAcceptCommit();
8512
8969
  await this.#queueSteer(text, images, { claimsGenuineUserIntent: true });
8513
8970
  options?.onPreflightAccepted?.();
8514
8971
  return;
@@ -8519,6 +8976,7 @@ export class AgentSession {
8519
8976
  expandPromptTemplates: false,
8520
8977
  images,
8521
8978
  onPreflightAccepted: options?.onPreflightAccepted,
8979
+ onPreflightAcceptCommit: options?.onPreflightAcceptCommit,
8522
8980
  });
8523
8981
  }
8524
8982
 
@@ -8691,6 +9149,103 @@ export class AgentSession {
8691
9149
  this.setTodoPhases(phases.filter(p => p.tasks.length > 0));
8692
9150
  }
8693
9151
 
9152
+ async #compactionStateSnapshot(): Promise<CompactionStateSnapshot> {
9153
+ const snapshot: CompactionStateSnapshot = {
9154
+ goal: undefined,
9155
+ openTodos: [],
9156
+ activeSkills: [],
9157
+ queuedMessages: false,
9158
+ lastAssistantStopReason: undefined,
9159
+ };
9160
+ try {
9161
+ const goalState = this.getGoalModeState();
9162
+ const goal = goalState?.goal;
9163
+ if (goal) snapshot.goal = { objective: goal.objective, status: goal.status, enabled: goalState.enabled };
9164
+ } catch (error) {
9165
+ logger.warn("Failed to read goal state for compaction snapshot", {
9166
+ error: error instanceof Error ? error.message : String(error),
9167
+ });
9168
+ }
9169
+ try {
9170
+ const todos = this.getTodoPhases()
9171
+ .flatMap(phase => phase.tasks)
9172
+ .filter(task => task.status === "pending" || task.status === "in_progress")
9173
+ .slice(0, 10)
9174
+ .map(task => task.content);
9175
+ snapshot.openTodos = todos;
9176
+ } catch (error) {
9177
+ logger.warn("Failed to read todo state for compaction snapshot", {
9178
+ error: error instanceof Error ? error.message : String(error),
9179
+ });
9180
+ }
9181
+ try {
9182
+ const state = await readVisibleSkillActiveState(this.sessionManager.getCwd(), this.sessionId, {
9183
+ bypassCache: true,
9184
+ });
9185
+ snapshot.activeSkills = (state?.active_skills ?? [])
9186
+ .filter(entry => entry.active !== false)
9187
+ .slice(0, 5)
9188
+ .map(entry => ({ skill: entry.skill, phase: entry.phase ?? "unknown" }));
9189
+ } catch (error) {
9190
+ logger.warn("Failed to read workflow state for compaction snapshot", {
9191
+ error: error instanceof Error ? error.message : String(error),
9192
+ });
9193
+ }
9194
+ try {
9195
+ snapshot.queuedMessages = this.agent.hasQueuedMessages() || this.#pendingNextTurnMessages.length > 0;
9196
+ } catch (error) {
9197
+ logger.warn("Failed to read queued-message state for compaction snapshot", {
9198
+ error: error instanceof Error ? error.message : String(error),
9199
+ });
9200
+ }
9201
+ try {
9202
+ for (let index = this.messages.length - 1; index >= 0; index--) {
9203
+ const message = this.messages[index];
9204
+ if (message.role !== "assistant") continue;
9205
+ snapshot.lastAssistantStopReason = (message as AssistantMessage).stopReason;
9206
+ break;
9207
+ }
9208
+ } catch (error) {
9209
+ logger.warn("Failed to read assistant stop state for compaction snapshot", {
9210
+ error: error instanceof Error ? error.message : String(error),
9211
+ });
9212
+ }
9213
+ return snapshot;
9214
+ }
9215
+
9216
+ #compactionStateContext(snapshot: CompactionStateSnapshot): string[] {
9217
+ const context: string[] = [];
9218
+ const goal = snapshot.goal;
9219
+ if (goal?.enabled) {
9220
+ context.push(
9221
+ `Active goal: ${sanitizeCompactionStateText(goal.objective, 160)} (status: ${sanitizeCompactionStateText(goal.status, 40)})`,
9222
+ );
9223
+ } else if (goal?.status === "paused") {
9224
+ context.push(`Paused goal: ${sanitizeCompactionStateText(goal.objective, 160)} (status: paused)`);
9225
+ }
9226
+ for (const entry of snapshot.activeSkills) {
9227
+ const skill = sanitizeCompactionStateText(entry.skill, 100);
9228
+ const phase = sanitizeCompactionStateText(entry.phase, 80);
9229
+ if (skill) context.push(`Active skill: ${skill} phase=${phase}`);
9230
+ }
9231
+ if (snapshot.openTodos.length > 0) {
9232
+ const todos = snapshot.openTodos.map(todo => sanitizeCompactionStateText(todo, 120));
9233
+ context.push(`Open todos: ${todos.join("; ")}`);
9234
+ }
9235
+ return context;
9236
+ }
9237
+
9238
+ #hasUnfinishedWork(snapshot: CompactionStateSnapshot): boolean {
9239
+ if (snapshot.queuedMessages) return true;
9240
+ if (snapshot.lastAssistantStopReason === "length") return true;
9241
+ // A verified paused goal is a human-wait terminus. It suppresses stale
9242
+ // todo/workflow activity until a queued user message resumes the run.
9243
+ if (snapshot.goal?.status === "paused") return false;
9244
+ if (snapshot.goal?.enabled && snapshot.goal.status === "active") return true;
9245
+ if (snapshot.openTodos.length > 0) return true;
9246
+ return snapshot.activeSkills.some(entry => !isWorkflowContinuationInert(entry.skill, entry.phase));
9247
+ }
9248
+
8694
9249
  async #applyCompactionPostAppend(
8695
9250
  compactionEntryId: string,
8696
9251
  firstKeptEntryId: string,
@@ -8889,6 +9444,26 @@ export class AgentSession {
8889
9444
  const outcome = await this.#abortWithOutcome(options);
8890
9445
  if (outcome.kind === "error") throw outcome.cause;
8891
9446
  }
9447
+ /**
9448
+ * Abort a specific active prompt and prove whether its tracked resources settled.
9449
+ */
9450
+ async abortPromptAndWait(handle: string, options: { graceMs: number }): Promise<RunSettlementProof> {
9451
+ // Only the abort trigger is gated on active ownership; settlement is always proven
9452
+ // from the ledger, because a run can go inactive while its resources still run.
9453
+ if (handle === this.agent.activeResourceRunId)
9454
+ // Awaiting the abort unboundedly would let a provider or tool that ignores
9455
+ // cancellation defer the grace forever, so the proof races the fixed grace
9456
+ // independently of abort completion.
9457
+ void this.abort({ timeoutMs: options.graceMs }).catch(() => {});
9458
+
9459
+ const proof = await this.agent.resourceLedger.waitForSettlement(handle, { graceMs: options.graceMs });
9460
+ if (proof.status === "unfenced") {
9461
+ // Detach the unsettled resources so they can never re-enter this run; their real
9462
+ // promises still release them inside the ledger.
9463
+ this.agent.resourceLedger.quarantine(handle);
9464
+ }
9465
+ return proof;
9466
+ }
8892
9467
 
8893
9468
  /** Atomically interrupt the active run and make text the next prompt. */
8894
9469
  async cancelAndSubmit(text: string, options?: { queuedEntryId?: string }): Promise<CancelAndSubmitOutcome> {
@@ -9258,6 +9833,12 @@ export class AgentSession {
9258
9833
  ): Promise<void> {
9259
9834
  this.#clearConstructorToolSelectionAuthority();
9260
9835
  const inheritedThinkingLevel = resolveThinkingLevelForModel(this.model, this.#getInheritedThinkingLevel());
9836
+ this.#thinkingLevelMutationRevision++;
9837
+ this.#thinkingLevelLiveMutationRevision++;
9838
+ this.#pendingThinkingLevelControlSuccess = undefined;
9839
+ this.#pendingThinkingLevelControlFailure = undefined;
9840
+ this.#pendingThinkingVisibilityControlSuccess = undefined;
9841
+ this.#pendingThinkingVisibilityControlFailure = undefined;
9261
9842
  this.#thinkingLevel = inheritedThinkingLevel;
9262
9843
  this.agent.setThinkingLevel(toReasoningEffort(inheritedThinkingLevel));
9263
9844
  this.sessionManager.appendThinkingLevelChange(ThinkingLevel.Inherit);
@@ -9536,6 +10117,16 @@ export class AgentSession {
9536
10117
  getSessionDefaultModelSelector(): string | undefined {
9537
10118
  return getSessionContextForInternalRead(this.sessionManager).models.default;
9538
10119
  }
10120
+ /**
10121
+ * Resolve the model that `modelRoles.default` currently points to, independent
10122
+ * of whatever model this session's log last recorded. Used by the TUI resume
10123
+ * flow's `session.resumeModelBehavior: "ask"` prompt to offer the currently
10124
+ * configured default as an alternative to the session's saved model.
10125
+ */
10126
+ resolveConfiguredDefaultModel(): Model | undefined {
10127
+ const availableModels = this.#modelRegistry.getAvailable();
10128
+ return this.#resolveRoleModelFull("default", availableModels, undefined).model;
10129
+ }
9539
10130
 
9540
10131
  /**
9541
10132
  * Re-assert the session resume default ("provider/id") in the session log
@@ -9607,7 +10198,7 @@ export class AgentSession {
9607
10198
  const ownsScope = scope !== undefined && !suppliedScope;
9608
10199
  try {
9609
10200
  if (isTemporaryOperation) {
9610
- this.agent.setModel(model);
10201
+ this.#setAgentModelWithReasoningContext(model);
9611
10202
  this.#syncAppendOnlyContext(model);
9612
10203
  } else {
9613
10204
  this.#setModelAuthoritatively(model, options?.cause ?? "temporary-operation");
@@ -9692,6 +10283,12 @@ export class AgentSession {
9692
10283
  this.#clearActiveRetryFallback();
9693
10284
  this.#setModelWithProviderSessionReset(model);
9694
10285
  const thinkingLevelChanged = this.#thinkingLevel !== thinkingLevel;
10286
+ this.#thinkingLevelMutationRevision++;
10287
+ this.#thinkingLevelLiveMutationRevision++;
10288
+ this.#pendingThinkingLevelControlSuccess = undefined;
10289
+ this.#pendingThinkingLevelControlFailure = undefined;
10290
+ this.#pendingThinkingVisibilityControlSuccess = undefined;
10291
+ this.#pendingThinkingVisibilityControlFailure = undefined;
9695
10292
  this.#thinkingLevel = thinkingLevel;
9696
10293
  this.agent.setThinkingLevel(toReasoningEffort(thinkingLevel));
9697
10294
  if (thinkingLevelChanged) {
@@ -10000,7 +10597,19 @@ export class AgentSession {
10000
10597
  * Set thinking level.
10001
10598
  * Saves the effective metadata-clamped level to the session, and to settings when requested.
10002
10599
  */
10600
+ #assertDurableSettingsWritable(): void {
10601
+ if (this.settings.canWriteDurableConfig()) return;
10602
+ throw new Error(
10603
+ "Cannot change settings while config.yml has invalid YAML syntax. Repair config.yml and reload settings.",
10604
+ );
10605
+ }
10606
+
10003
10607
  setThinkingLevel(level: ThinkingLevel | undefined, persist: boolean = false): void {
10608
+ if (persist) this.#assertDurableSettingsWritable();
10609
+ this.#thinkingLevelMutationRevision++;
10610
+ this.#thinkingLevelLiveMutationRevision++;
10611
+ this.#pendingThinkingLevelControlSuccess = undefined;
10612
+ this.#pendingThinkingLevelControlFailure = undefined;
10004
10613
  const effectiveLevel = resolveThinkingLevelForModel(this.model, level);
10005
10614
  const isChanging = effectiveLevel !== this.#thinkingLevel;
10006
10615
 
@@ -10020,11 +10629,10 @@ export class AgentSession {
10020
10629
  }
10021
10630
 
10022
10631
  /**
10023
- * Set thinking level from a control surface. Global changes are durable or rolled back.
10632
+ * Set thinking level from a control surface. Global changes commit before affecting live state.
10024
10633
  */
10025
10634
  async setThinkingLevelForControl(level: ThinkingLevel, persist: boolean): Promise<void> {
10026
10635
  const previousThinkingLevel = this.thinkingLevel;
10027
- const previousScope = this.getThinkingScopeForControl();
10028
10636
  if (!persist) {
10029
10637
  this.setThinkingLevel(level === ThinkingLevel.Inherit ? this.#getInheritedThinkingLevel() : level);
10030
10638
  if (level === ThinkingLevel.Inherit || this.thinkingLevel === previousThinkingLevel) {
@@ -10033,26 +10641,99 @@ export class AgentSession {
10033
10641
  return;
10034
10642
  }
10035
10643
 
10036
- const previousDefaultThinkingLevel = this.settings.getGlobal("defaultThinkingLevel");
10037
- if (level === ThinkingLevel.Inherit) {
10038
- const defaultLevel = getDefault("defaultThinkingLevel");
10039
- this.settings.set("defaultThinkingLevel", defaultLevel);
10040
- this.setThinkingLevel(this.#getInheritedThinkingLevel());
10041
- } else {
10042
- this.setThinkingLevel(level, true);
10043
- }
10644
+ this.#assertDurableSettingsWritable();
10645
+ const effectiveLevel = resolveThinkingLevelForModel(
10646
+ this.model,
10647
+ level === ThinkingLevel.Inherit ? this.#getInheritedThinkingLevel() : level,
10648
+ );
10649
+ const persistedLevel = level === ThinkingLevel.Inherit ? getDefault("defaultThinkingLevel") : effectiveLevel;
10650
+ const mutationRevision = ++this.#thinkingLevelMutationRevision;
10651
+ const expectedLiveMutationRevision = this.#thinkingLevelLiveMutationRevision;
10652
+ const expectedSessionId = this.sessionManager.getSessionId();
10653
+ const expectedModel = this.model;
10654
+ const expectedContextGeneration = this.#reasoningControlContextGeneration;
10044
10655
  try {
10045
- await this.settings.flushOrThrow();
10046
- this.sessionManager.appendThinkingLevelChange(ThinkingLevel.Inherit);
10656
+ await this.settings.commitAtomicBatch([{ path: "defaultThinkingLevel", op: "set", value: persistedLevel }]);
10047
10657
  } catch {
10048
- this.settings.set("defaultThinkingLevel", previousDefaultThinkingLevel ?? getDefault("defaultThinkingLevel"));
10049
- this.setThinkingLevel(previousThinkingLevel);
10050
- this.sessionManager.appendThinkingLevelChange(
10051
- previousScope === "global config" ? ThinkingLevel.Inherit : previousThinkingLevel,
10052
- );
10053
- await this.settings.flushOrThrow().catch(() => {});
10658
+ if (
10659
+ mutationRevision === this.#thinkingLevelMutationRevision &&
10660
+ this.#reasoningControlContextGeneration === expectedContextGeneration &&
10661
+ this.sessionManager.getSessionId() === expectedSessionId &&
10662
+ this.model === expectedModel
10663
+ ) {
10664
+ const pending = this.#pendingThinkingLevelControlSuccess;
10665
+ this.#pendingThinkingLevelControlSuccess = undefined;
10666
+ if (
10667
+ pending &&
10668
+ pending.contextGeneration === expectedContextGeneration &&
10669
+ this.sessionManager.getSessionId() === pending.sessionId &&
10670
+ this.model === pending.model
10671
+ ) {
10672
+ this.setThinkingLevel(
10673
+ pending.level === ThinkingLevel.Inherit ? this.#getInheritedThinkingLevel() : pending.level,
10674
+ );
10675
+ this.sessionManager.appendThinkingLevelChange(ThinkingLevel.Inherit);
10676
+ } else {
10677
+ this.#pendingThinkingLevelControlFailure = {
10678
+ mutationRevision,
10679
+ liveMutationRevision: expectedLiveMutationRevision,
10680
+ sessionId: expectedSessionId,
10681
+ model: expectedModel,
10682
+ contextGeneration: expectedContextGeneration,
10683
+ };
10684
+ }
10685
+ }
10054
10686
  throw new Error("Unable to persist reasoning settings.");
10055
10687
  }
10688
+
10689
+ if (
10690
+ mutationRevision === this.#thinkingLevelMutationRevision &&
10691
+ this.#thinkingLevelLiveMutationRevision === expectedLiveMutationRevision
10692
+ ) {
10693
+ this.setThinkingLevel(level === ThinkingLevel.Inherit ? this.#getInheritedThinkingLevel() : level);
10694
+ this.sessionManager.appendThinkingLevelChange(ThinkingLevel.Inherit);
10695
+ return;
10696
+ }
10697
+ if (
10698
+ mutationRevision !== this.#thinkingLevelMutationRevision ||
10699
+ this.#reasoningControlContextGeneration !== expectedContextGeneration ||
10700
+ this.sessionManager.getSessionId() !== expectedSessionId ||
10701
+ this.model !== expectedModel
10702
+ ) {
10703
+ if (
10704
+ this.#thinkingLevelLiveMutationRevision === expectedLiveMutationRevision &&
10705
+ this.#reasoningControlContextGeneration === expectedContextGeneration &&
10706
+ this.sessionManager.getSessionId() === expectedSessionId &&
10707
+ this.model === expectedModel &&
10708
+ (this.#pendingThinkingLevelControlSuccess?.mutationRevision ?? -1) < mutationRevision
10709
+ ) {
10710
+ this.#pendingThinkingLevelControlSuccess = {
10711
+ level,
10712
+ mutationRevision,
10713
+ sessionId: expectedSessionId,
10714
+ model: expectedModel,
10715
+ contextGeneration: expectedContextGeneration,
10716
+ };
10717
+ const failure = this.#pendingThinkingLevelControlFailure;
10718
+ if (
10719
+ failure &&
10720
+ failure.mutationRevision === this.#thinkingLevelMutationRevision &&
10721
+ failure.liveMutationRevision === expectedLiveMutationRevision &&
10722
+ failure.sessionId === expectedSessionId &&
10723
+ failure.model === expectedModel &&
10724
+ failure.contextGeneration === expectedContextGeneration
10725
+ ) {
10726
+ this.#pendingThinkingLevelControlFailure = undefined;
10727
+ this.setThinkingLevel(
10728
+ level === ThinkingLevel.Inherit ? this.#getInheritedThinkingLevel() : effectiveLevel,
10729
+ );
10730
+ this.sessionManager.appendThinkingLevelChange(ThinkingLevel.Inherit);
10731
+ }
10732
+ }
10733
+ return;
10734
+ }
10735
+ this.setThinkingLevel(level === ThinkingLevel.Inherit ? this.#getInheritedThinkingLevel() : effectiveLevel);
10736
+ this.sessionManager.appendThinkingLevelChange(ThinkingLevel.Inherit);
10056
10737
  }
10057
10738
 
10058
10739
  getThinkingScopeForControl(): "session" | "global config" {
@@ -10068,6 +10749,11 @@ export class AgentSession {
10068
10749
  }
10069
10750
 
10070
10751
  setThinkingVisibility(visibility: "visible" | "hidden", persist: boolean = false): void {
10752
+ if (persist) this.#assertDurableSettingsWritable();
10753
+ this.#thinkingVisibilityMutationRevision++;
10754
+ this.#thinkingVisibilityLiveMutationRevision++;
10755
+ this.#pendingThinkingVisibilityControlSuccess = undefined;
10756
+ this.#pendingThinkingVisibilityControlFailure = undefined;
10071
10757
  this.agent.hideThinkingSummary = visibility === "hidden";
10072
10758
  if (persist) {
10073
10759
  this.settings.set("hideThinkingBlock", visibility === "hidden");
@@ -10075,7 +10761,7 @@ export class AgentSession {
10075
10761
  }
10076
10762
 
10077
10763
  /**
10078
- * Set thinking visibility from a control surface. Global changes are durable or rolled back.
10764
+ * Set thinking visibility from a control surface. Global changes commit before affecting live state.
10079
10765
  */
10080
10766
  async setThinkingVisibilityForControl(visibility: "visible" | "hidden", persist: boolean): Promise<void> {
10081
10767
  if (!persist) {
@@ -10083,17 +10769,88 @@ export class AgentSession {
10083
10769
  return;
10084
10770
  }
10085
10771
 
10086
- const previousVisibility = this.getThinkingVisibility();
10087
- const previousHideThinkingBlock = this.settings.getGlobal("hideThinkingBlock");
10088
- this.setThinkingVisibility(visibility, true);
10772
+ this.#assertDurableSettingsWritable();
10773
+ const mutationRevision = ++this.#thinkingVisibilityMutationRevision;
10774
+ const expectedLiveMutationRevision = this.#thinkingVisibilityLiveMutationRevision;
10775
+ const expectedSessionId = this.sessionManager.getSessionId();
10776
+ const expectedModel = this.model;
10777
+ const expectedContextGeneration = this.#reasoningControlContextGeneration;
10089
10778
  try {
10090
- await this.settings.flushOrThrow();
10779
+ await this.settings.commitAtomicBatch([
10780
+ { path: "hideThinkingBlock", op: "set", value: visibility === "hidden" },
10781
+ ]);
10091
10782
  } catch {
10092
- this.settings.set("hideThinkingBlock", previousHideThinkingBlock ?? getDefault("hideThinkingBlock"));
10093
- this.setThinkingVisibility(previousVisibility);
10094
- await this.settings.flushOrThrow().catch(() => {});
10783
+ if (
10784
+ mutationRevision === this.#thinkingVisibilityMutationRevision &&
10785
+ this.#reasoningControlContextGeneration === expectedContextGeneration &&
10786
+ this.sessionManager.getSessionId() === expectedSessionId &&
10787
+ this.model === expectedModel
10788
+ ) {
10789
+ const pending = this.#pendingThinkingVisibilityControlSuccess;
10790
+ this.#pendingThinkingVisibilityControlSuccess = undefined;
10791
+ if (
10792
+ pending &&
10793
+ pending.contextGeneration === expectedContextGeneration &&
10794
+ this.sessionManager.getSessionId() === pending.sessionId &&
10795
+ this.model === pending.model
10796
+ ) {
10797
+ this.setThinkingVisibility(pending.visibility);
10798
+ } else {
10799
+ this.#pendingThinkingVisibilityControlFailure = {
10800
+ mutationRevision,
10801
+ liveMutationRevision: expectedLiveMutationRevision,
10802
+ sessionId: expectedSessionId,
10803
+ model: expectedModel,
10804
+ contextGeneration: expectedContextGeneration,
10805
+ };
10806
+ }
10807
+ }
10095
10808
  throw new Error("Unable to persist reasoning settings.");
10096
10809
  }
10810
+ if (
10811
+ mutationRevision === this.#thinkingVisibilityMutationRevision &&
10812
+ this.#thinkingVisibilityLiveMutationRevision === expectedLiveMutationRevision
10813
+ ) {
10814
+ this.setThinkingVisibility(visibility);
10815
+ return;
10816
+ }
10817
+
10818
+ if (
10819
+ mutationRevision !== this.#thinkingVisibilityMutationRevision ||
10820
+ this.#reasoningControlContextGeneration !== expectedContextGeneration ||
10821
+ this.sessionManager.getSessionId() !== expectedSessionId ||
10822
+ this.model !== expectedModel
10823
+ ) {
10824
+ if (
10825
+ this.#thinkingVisibilityLiveMutationRevision === expectedLiveMutationRevision &&
10826
+ this.#reasoningControlContextGeneration === expectedContextGeneration &&
10827
+ this.sessionManager.getSessionId() === expectedSessionId &&
10828
+ this.model === expectedModel &&
10829
+ (this.#pendingThinkingVisibilityControlSuccess?.mutationRevision ?? -1) < mutationRevision
10830
+ ) {
10831
+ this.#pendingThinkingVisibilityControlSuccess = {
10832
+ visibility,
10833
+ mutationRevision,
10834
+ sessionId: expectedSessionId,
10835
+ model: expectedModel,
10836
+ contextGeneration: expectedContextGeneration,
10837
+ };
10838
+ const failure = this.#pendingThinkingVisibilityControlFailure;
10839
+ if (
10840
+ failure &&
10841
+ failure.mutationRevision === this.#thinkingVisibilityMutationRevision &&
10842
+ failure.liveMutationRevision === expectedLiveMutationRevision &&
10843
+ failure.sessionId === expectedSessionId &&
10844
+ failure.model === expectedModel &&
10845
+ failure.contextGeneration === expectedContextGeneration
10846
+ ) {
10847
+ this.#pendingThinkingVisibilityControlFailure = undefined;
10848
+ this.setThinkingVisibility(visibility);
10849
+ }
10850
+ }
10851
+ return;
10852
+ }
10853
+ this.setThinkingVisibility(visibility);
10097
10854
  }
10098
10855
 
10099
10856
  /**
@@ -10261,6 +11018,16 @@ export class AgentSession {
10261
11018
  return getSupportedEfforts(this.model);
10262
11019
  }
10263
11020
 
11021
+ /**
11022
+ * Runtime evidence published for the current GJC bundle activation
11023
+ * generation, set once by `createAgentSession` after every producer has run.
11024
+ * Undefined until that publication happens, so consumers report runtime
11025
+ * status as unavailable rather than falsely clear.
11026
+ */
11027
+ gjcRuntimeSnapshot?: GjcRuntimeSnapshotProvider;
11028
+ /** Activation generation a published snapshot must match to be merged. */
11029
+ gjcActivationGeneration?: number;
11030
+
10264
11031
  // =========================================================================
10265
11032
  // Message Queue Mode Management
10266
11033
  // =========================================================================
@@ -10270,6 +11037,7 @@ export class AgentSession {
10270
11037
  * Saves to settings.
10271
11038
  */
10272
11039
  setSteeringMode(mode: "all" | "one-at-a-time"): void {
11040
+ this.#assertDurableSettingsWritable();
10273
11041
  this.agent.setSteeringMode(mode);
10274
11042
  this.settings.set("steeringMode", mode);
10275
11043
  }
@@ -10279,6 +11047,7 @@ export class AgentSession {
10279
11047
  * Saves to settings.
10280
11048
  */
10281
11049
  setFollowUpMode(mode: "all" | "one-at-a-time"): void {
11050
+ this.#assertDurableSettingsWritable();
10282
11051
  this.agent.setFollowUpMode(mode);
10283
11052
  this.settings.set("followUpMode", mode);
10284
11053
  }
@@ -10288,6 +11057,7 @@ export class AgentSession {
10288
11057
  * Saves to settings.
10289
11058
  */
10290
11059
  setInterruptMode(mode: "immediate" | "wait"): void {
11060
+ this.#assertDurableSettingsWritable();
10291
11061
  this.agent.setInterruptMode(mode);
10292
11062
  this.settings.set("interruptMode", mode);
10293
11063
  }
@@ -10299,13 +11069,93 @@ export class AgentSession {
10299
11069
  async #pruneToolOutputs(
10300
11070
  signal?: AbortSignal,
10301
11071
  overThreshold = false,
10302
- ): Promise<{ prunedCount: number; tokensSaved: number } | undefined> {
11072
+ options?: { commitGate?: (actual: { prunedCount: number; tokensSaved: number }) => boolean },
11073
+ ): Promise<{ prunedCount: number; tokensSaved: number; committed: boolean } | undefined> {
10303
11074
  const branchEntries = this.sessionManager.getBranch();
10304
- const result = pruneToolOutputs(
10305
- branchEntries,
10306
- DEFAULT_PRUNE_CONFIG,
10307
- overThreshold ? { relaxedMinimum: 0 } : undefined,
10308
- );
11075
+ const artifactManager = this.sessionManager.getArtifactManager();
11076
+ const prunedArtifacts: Array<{ entryId: string; id: string; toolType: string; originalText: string }> = [];
11077
+ let reservedArtifactId: string | undefined;
11078
+ if (artifactManager) {
11079
+ try {
11080
+ reservedArtifactId = (await artifactManager.allocatePath("tool-output")).id;
11081
+ } catch (error) {
11082
+ logger.warn("Failed to reserve artifact ID for pruned tool output", {
11083
+ error: error instanceof Error ? error.message : String(error),
11084
+ });
11085
+ }
11086
+ }
11087
+ const result = pruneToolOutputs(branchEntries, DEFAULT_PRUNE_CONFIG, {
11088
+ relaxedMinimum: overThreshold ? 0 : undefined,
11089
+ artifactRefMaxChars: PRUNED_ARTIFACT_REF_MAX_CHARS,
11090
+ artifactRef: candidate => {
11091
+ if (!artifactManager) return undefined;
11092
+ const id = reservedArtifactId ?? String(artifactManager.allocateId());
11093
+ reservedArtifactId = undefined;
11094
+ const toolType = (candidate.toolName ?? "tool-output").replace(/[^a-zA-Z0-9_-]/g, "_") || "tool-output";
11095
+ prunedArtifacts.push({ entryId: candidate.entryId, id, toolType, originalText: candidate.originalText });
11096
+ return `artifact://${id}`;
11097
+ },
11098
+ });
11099
+ const failedArtifactEntryIds = new Set<string>();
11100
+ const publishedArtifacts: typeof prunedArtifacts = [];
11101
+ for (const artifact of prunedArtifacts.filter(artifact =>
11102
+ result.originals.some(original => original.entryId === artifact.entryId),
11103
+ )) {
11104
+ try {
11105
+ await artifactManager?.publishNamedNoReplace(
11106
+ `${artifact.id}.${artifact.toolType}.log`,
11107
+ new TextEncoder().encode(artifact.originalText),
11108
+ );
11109
+ publishedArtifacts.push(artifact);
11110
+ } catch (error) {
11111
+ failedArtifactEntryIds.add(artifact.entryId);
11112
+ logger.warn("Failed to persist pruned tool output artifact", {
11113
+ artifactId: artifact.id,
11114
+ error: error instanceof Error ? error.message : String(error),
11115
+ });
11116
+ }
11117
+ }
11118
+ const rollbackPublishedArtifacts = async (reason: "aborted" | "commit_gate_rejected") => {
11119
+ let removedArtifacts = 0;
11120
+ let unremovedArtifacts = 0;
11121
+ for (const artifact of publishedArtifacts) {
11122
+ const filename = `${artifact.id}.${artifact.toolType}.log`;
11123
+ let removed = false;
11124
+ for (let attempt = 0; attempt < 3 && !removed; attempt++) {
11125
+ removed = (await artifactManager?.removeNamedBestEffort(filename)) ?? false;
11126
+ if (!removed && attempt < 2) await Bun.sleep(10);
11127
+ }
11128
+ if (removed) removedArtifacts++;
11129
+ else {
11130
+ unremovedArtifacts++;
11131
+ logger.warn("Failed to roll back staged pruned-output artifact after retries", {
11132
+ artifactId: artifact.id,
11133
+ reason,
11134
+ });
11135
+ }
11136
+ }
11137
+ return { removedArtifacts, unremovedArtifacts };
11138
+ };
11139
+ let toolTokensSaved = result.tokensSaved;
11140
+ let toolPrunedCount = result.prunedCount;
11141
+ const committedToolEntries = result.prunedEntries.filter(entry => {
11142
+ if (!failedArtifactEntryIds.has(entry.id)) return true;
11143
+ const original = result.originals.find(candidate => candidate.entryId === entry.id);
11144
+ if (!original) return true;
11145
+ const message = entry.message as ToolResultMessage;
11146
+ toolTokensSaved -=
11147
+ original.tokens -
11148
+ estimateTextTokensHeuristic(
11149
+ message.content
11150
+ .filter((part): part is TextContent => part.type === "text")
11151
+ .map(part => part.text)
11152
+ .join(""),
11153
+ );
11154
+ toolPrunedCount--;
11155
+ message.content = [{ type: "text", text: original.originalText }];
11156
+ delete message.prunedAt;
11157
+ return false;
11158
+ });
10309
11159
  const argumentResult = pruneAssistantToolArguments(branchEntries, DEFAULT_PRUNE_CONFIG);
10310
11160
  const fileMentionResult = pruneStaleFileMentions(branchEntries, p =>
10311
11161
  resolveReadPath(p, this.sessionManager.getCwd()),
@@ -10313,22 +11163,37 @@ export class AgentSession {
10313
11163
  const volatileContextResult = pruneSupersededVolatileProjectContext(branchEntries);
10314
11164
  const reminderResult = pruneSupersededMaintenanceReminders(branchEntries);
10315
11165
  const tokensSaved =
10316
- result.tokensSaved +
11166
+ toolTokensSaved +
10317
11167
  argumentResult.argumentTokensSaved +
10318
11168
  Math.round((fileMentionResult.bytesSaved + volatileContextResult.bytesSaved + reminderResult.bytesSaved) / 4);
10319
11169
  const prunedCount =
10320
- result.prunedCount +
11170
+ toolPrunedCount +
10321
11171
  argumentResult.argumentPrunedCount +
10322
11172
  fileMentionResult.changed.length +
10323
11173
  volatileContextResult.changed.length +
10324
11174
  reminderResult.changed.length;
10325
11175
  if (prunedCount === 0 || signal?.aborted) {
11176
+ if (publishedArtifacts.length > 0) await rollbackPublishedArtifacts("aborted");
11177
+ return undefined;
11178
+ }
11179
+ if (options?.commitGate && !options.commitGate({ prunedCount, tokensSaved })) {
11180
+ const rollback = await rollbackPublishedArtifacts("commit_gate_rejected");
11181
+ logger.info("Below-threshold maintenance pruning staged but not committed", {
11182
+ prunedCount,
11183
+ tokensSaved,
11184
+ rolledBackArtifacts: rollback.removedArtifacts,
11185
+ unremovedArtifacts: rollback.unremovedArtifacts,
11186
+ });
11187
+ return { prunedCount, tokensSaved, committed: false };
11188
+ }
11189
+ if (signal?.aborted) {
11190
+ await rollbackPublishedArtifacts("aborted");
10326
11191
  return undefined;
10327
11192
  }
10328
11193
 
10329
11194
  // getBranch() returns materialized copies for blob-externalized entries, so
10330
11195
  // the pruning mutations must be written back into the canonical store.
10331
- const combined = [...result.prunedEntries, ...argumentResult.prunedEntries, ...fileMentionResult.changed];
11196
+ const combined = [...committedToolEntries, ...argumentResult.prunedEntries, ...fileMentionResult.changed];
10332
11197
  this.sessionManager.applyEntryMessageUpdates(combined);
10333
11198
  this.sessionManager.applyCustomMessageEntryUpdates([...volatileContextResult.changed, ...reminderResult.changed]);
10334
11199
  await this.sessionManager.rewriteEntries();
@@ -10339,7 +11204,7 @@ export class AgentSession {
10339
11204
  this.#resetInjectedContextSignatures();
10340
11205
  this.#syncTodoPhasesFromBranch();
10341
11206
  this.#closeCodexProviderSessionsForHistoryRewrite();
10342
- return { prunedCount, tokensSaved };
11207
+ return { prunedCount, tokensSaved, committed: true };
10343
11208
  }
10344
11209
 
10345
11210
  /**
@@ -10372,7 +11237,9 @@ export class AgentSession {
10372
11237
  const compactionSettings = this.settings.getGroup("compaction");
10373
11238
  if (!compactionSettings.maintenancePruningEnabled) return;
10374
11239
 
10375
- const estimate = estimateToolOutputPruneSavings(this.sessionManager.getBranch(), DEFAULT_PRUNE_CONFIG);
11240
+ const estimate = estimateToolOutputPruneSavings(this.sessionManager.getBranch(), DEFAULT_PRUNE_CONFIG, {
11241
+ artifactRefMaxChars: PRUNED_ARTIFACT_REF_MAX_CHARS,
11242
+ });
10376
11243
  const cacheEpochResetCost = this.#lastCacheEpochResetCost();
10377
11244
  if (
10378
11245
  !shouldRunMaintenancePrune({
@@ -10385,10 +11252,30 @@ export class AgentSession {
10385
11252
  return;
10386
11253
  }
10387
11254
 
10388
- const pruneResult = await this.#pruneToolOutputs();
11255
+ const pruneResult = await this.#pruneToolOutputs(undefined, false, {
11256
+ commitGate: actual => actual.tokensSaved > cacheEpochResetCost,
11257
+ });
10389
11258
  if (!pruneResult || pruneResult.prunedCount === 0) return;
10390
-
10391
- const resetReason = `below-threshold maintenance: reclaimed ${pruneResult.tokensSaved} tokens > cache-epoch reset cost ${cacheEpochResetCost}`;
11259
+ if (!pruneResult.committed) {
11260
+ const message = `Maintenance pruning skipped: actual savings ${pruneResult.tokensSaved} tokens <= cache-epoch reset cost ${cacheEpochResetCost} after artifact notices/publication rollback.`;
11261
+ await this.#emitSessionEvent({
11262
+ type: "notice",
11263
+ level: "info",
11264
+ source: "maintenance-prune",
11265
+ message,
11266
+ });
11267
+ logger.info("Below-threshold maintenance pruning skipped", {
11268
+ tokensSaved: pruneResult.tokensSaved,
11269
+ prunedCount: pruneResult.prunedCount,
11270
+ cacheEpochResetCost,
11271
+ minSavings: compactionSettings.maintenancePruningMinSavingsTokens,
11272
+ });
11273
+ return;
11274
+ }
11275
+ const resetReason =
11276
+ pruneResult.tokensSaved > cacheEpochResetCost
11277
+ ? `below-threshold maintenance: reclaimed ${pruneResult.tokensSaved} tokens > cache-epoch reset cost ${cacheEpochResetCost}`
11278
+ : `below-threshold maintenance: reclaimed ${pruneResult.tokensSaved} tokens <= cache-epoch reset cost ${cacheEpochResetCost}`;
10392
11279
  await this.#emitSessionEvent({
10393
11280
  type: "notice",
10394
11281
  level: "info",
@@ -10422,6 +11309,8 @@ export class AgentSession {
10422
11309
  await this.abort();
10423
11310
  const compactionAbortController = new AbortController();
10424
11311
  this.#compactionAbortController = compactionAbortController;
11312
+ // Take this invocation's state snapshot for the summarizer context.
11313
+ const compactionStateSnapshot = await this.#compactionStateSnapshot();
10425
11314
 
10426
11315
  try {
10427
11316
  if (!this.model) {
@@ -10467,7 +11356,11 @@ export class AgentSession {
10467
11356
  }
10468
11357
  }
10469
11358
 
10470
- const compactionPrep = await this.#prepareCompactionFromHooks(preparation, hookCompaction);
11359
+ const compactionPrep = await this.#prepareCompactionFromHooks(
11360
+ preparation,
11361
+ hookCompaction,
11362
+ compactionStateSnapshot,
11363
+ );
10471
11364
 
10472
11365
  let summary: string;
10473
11366
  let shortSummary: string | undefined;
@@ -10955,7 +11848,12 @@ export class AgentSession {
10955
11848
  * @param assistantMessage The assistant message to check
10956
11849
  * @param skipAbortedCheck If false, include aborted messages (for pre-prompt check). Default: true
10957
11850
  */
10958
- async #checkCompaction(assistantMessage: AssistantMessage, skipAbortedCheck = true): Promise<boolean> {
11851
+ async #checkCompaction(
11852
+ assistantMessage: AssistantMessage,
11853
+ skipAbortedCheck = true,
11854
+ onTerminalOverflowNoop?: () => void,
11855
+ resourceRunId?: string,
11856
+ ): Promise<boolean> {
10959
11857
  // Safety stops are terminal and must not trigger context maintenance.
10960
11858
  if (
10961
11859
  assistantMessage.errorKind === "provider_safety_stop" ||
@@ -10991,15 +11889,22 @@ export class AgentSession {
10991
11889
  // Remove the error message from agent state (it IS saved to session for history,
10992
11890
  // but we don't want it in context for the retry)
10993
11891
  const messages = this.agent.state.messages;
11892
+ let removedOverflowAssistant = false;
10994
11893
  if (messages.length > 0 && messages[messages.length - 1].role === "assistant") {
10995
11894
  this.agent.replaceMessages(messages.slice(0, -1));
11895
+ removedOverflowAssistant = true;
10996
11896
  }
10997
11897
 
10998
11898
  // Try context promotion first - switch to a larger model and retry without compacting
10999
11899
  const promoted = await this.#tryContextPromotion(assistantMessage);
11000
11900
  if (promoted) {
11001
11901
  // Retry on the promoted (larger) model without compacting
11002
- this.#scheduleAgentContinue({ delayMs: 100, generation, suppressPredecessorAgentEnd: true });
11902
+ this.#scheduleAgentContinue({
11903
+ delayMs: 100,
11904
+ generation,
11905
+ suppressPredecessorAgentEnd: true,
11906
+ resourceRunId,
11907
+ });
11003
11908
 
11004
11909
  return true;
11005
11910
  }
@@ -11007,10 +11912,19 @@ export class AgentSession {
11007
11912
  // No promotion target available fall through to compaction
11008
11913
  const compactionSettings = this.settings.getGroup("compaction");
11009
11914
  if (compactionSettings.enabled && compactionSettings.strategy !== "off") {
11010
- const status = await this.#runAutoCompaction("overflow", true);
11915
+ const status = await this.#runAutoCompaction("overflow", true, false, {
11916
+ beforeTerminalOverflowNoop: () => {
11917
+ if (onTerminalOverflowNoop) {
11918
+ onTerminalOverflowNoop();
11919
+ } else if (removedOverflowAssistant) {
11920
+ this.agent.appendMessage(assistantMessage);
11921
+ }
11922
+ },
11923
+ resourceRunId,
11924
+ });
11011
11925
  return "continuationScheduled" in status && status.continuationScheduled === true;
11012
11926
  }
11013
- return this.#scheduleOverflowRetryContinuation(generation);
11927
+ return await this.#scheduleOverflowRetryContinuation(generation, resourceRunId);
11014
11928
  }
11015
11929
  const compactionSettings = this.settings.getGroup("compaction");
11016
11930
  if (!compactionSettings.enabled || compactionSettings.strategy === "off") return false;
@@ -11030,6 +11944,7 @@ export class AgentSession {
11030
11944
  return true;
11031
11945
  const pruneEstimate = estimateToolOutputPruneSavings(this.sessionManager.getBranch(), DEFAULT_PRUNE_CONFIG, {
11032
11946
  relaxedMinimum: 0,
11947
+ artifactRefMaxChars: PRUNED_ARTIFACT_REF_MAX_CHARS,
11033
11948
  });
11034
11949
  if (
11035
11950
  pruneEstimate.tokensSaved > 0 &&
@@ -11134,6 +12049,7 @@ export class AgentSession {
11134
12049
  // prompt-cache epoch via #closeCodexProviderSessionsForHistoryRewrite.
11135
12050
  const pruneEstimate = estimateToolOutputPruneSavings(this.sessionManager.getBranch(), DEFAULT_PRUNE_CONFIG, {
11136
12051
  relaxedMinimum: 0,
12052
+ artifactRefMaxChars: PRUNED_ARTIFACT_REF_MAX_CHARS,
11137
12053
  });
11138
12054
  let pruneResult: { prunedCount: number; tokensSaved: number } | undefined;
11139
12055
  if (
@@ -11315,6 +12231,7 @@ export class AgentSession {
11315
12231
 
11316
12232
  const pruneEstimate = estimateToolOutputPruneSavings(this.sessionManager.getBranch(), DEFAULT_PRUNE_CONFIG, {
11317
12233
  relaxedMinimum: 0,
12234
+ artifactRefMaxChars: PRUNED_ARTIFACT_REF_MAX_CHARS,
11318
12235
  });
11319
12236
  if (
11320
12237
  pruneEstimate.tokensSaved > 0 &&
@@ -11775,13 +12692,18 @@ export class AgentSession {
11775
12692
  this.#defaultFallbackExhaustedLastTurn = false;
11776
12693
  }
11777
12694
 
12695
+ #setAgentModelWithReasoningContext(model: Model | undefined): void {
12696
+ this.#reasoningControlContextGeneration++;
12697
+ this.agent.setModel(model);
12698
+ }
12699
+
11778
12700
  #setModelWithProviderSessionReset(model: Model | undefined): void {
11779
12701
  this.#defaultModelSelectionMutationRevision++;
11780
12702
  const currentModel = this.model;
11781
12703
  if (currentModel) {
11782
12704
  this.#closeProviderSessionsForModelSwitch(currentModel, model);
11783
12705
  }
11784
- this.agent.setModel(model);
12706
+ this.#setAgentModelWithReasoningContext(model);
11785
12707
  }
11786
12708
 
11787
12709
  #setModelAuthoritatively(model: Model, cause: ModelChangeCause): void {
@@ -11790,7 +12712,7 @@ export class AgentSession {
11790
12712
  if (cause !== "temporary-operation") this.#commitAllTemporaryProviderSessionScopes();
11791
12713
  const currentModel = this.model;
11792
12714
  if (currentModel) this.#closeProviderSessionsForModelSwitch(currentModel, model);
11793
- this.agent.setModel(model);
12715
+ this.#setAgentModelWithReasoningContext(model);
11794
12716
  this.#syncAppendOnlyContext(model);
11795
12717
  }
11796
12718
 
@@ -12249,6 +13171,7 @@ export class AgentSession {
12249
13171
  async #prepareCompactionFromHooks(
12250
13172
  preparation: CompactionPreparation,
12251
13173
  hookCompaction: CompactionResult | undefined,
13174
+ stateSnapshot: CompactionStateSnapshot,
12252
13175
  ): Promise<
12253
13176
  | {
12254
13177
  kind: "fromHook";
@@ -12288,11 +13211,17 @@ export class AgentSession {
12288
13211
  hookContext = hookContext ? [...hookContext, memoryBackendContext] : [memoryBackendContext];
12289
13212
  }
12290
13213
 
13214
+ const stateContext = this.#compactionStateContext(stateSnapshot);
13215
+
13216
+ if (stateContext.length > 0) {
13217
+ hookContext = hookContext ? [...hookContext, ...stateContext] : stateContext;
13218
+ }
13219
+
12291
13220
  if (hookCompaction) {
12292
13221
  preserveData ??= hookCompaction.preserveData;
12293
13222
  return {
12294
13223
  kind: "fromHook",
12295
- summary: hookCompaction.summary,
13224
+ summary: appendCompactionStateContext(hookCompaction.summary, stateContext),
12296
13225
  shortSummary: hookCompaction.shortSummary,
12297
13226
  firstKeptEntryId: hookCompaction.firstKeptEntryId,
12298
13227
  tokensBefore: hookCompaction.tokensBefore,
@@ -12316,6 +13245,8 @@ export class AgentSession {
12316
13245
  deferHandoffMaintenance?: boolean;
12317
13246
  force?: boolean;
12318
13247
  signal?: AbortSignal;
13248
+ beforeTerminalOverflowNoop?: () => void;
13249
+ resourceRunId?: string;
12319
13250
  },
12320
13251
  ): Promise<AutoCompactionTerminalStatus> {
12321
13252
  const compactionSettings = this.settings.getGroup("compaction");
@@ -12337,7 +13268,7 @@ export class AgentSession {
12337
13268
  if (signal.aborted) return;
12338
13269
  await this.#runAutoCompaction(reason, willRetry, true, options);
12339
13270
  },
12340
- { generation },
13271
+ { generation, resourceRunId: options?.resourceRunId },
12341
13272
  );
12342
13273
  return { kind: "skipped" };
12343
13274
  }
@@ -12369,6 +13300,10 @@ export class AgentSession {
12369
13300
  if (autoCompactionSignal.aborted) return { kind: "aborted", source: "signal" };
12370
13301
  await this.#emitSessionEvent({ type: "auto_compaction_start", reason, action });
12371
13302
  if (autoCompactionSignal.aborted) return await emitAborted();
13303
+ const compactionStateSnapshot = await this.#compactionStateSnapshot();
13304
+ if (autoCompactionSignal.aborted || this.#isDisposed || this.#promptGeneration !== generation) {
13305
+ return await emitAborted();
13306
+ }
12372
13307
 
12373
13308
  if (compactionSettings.strategy === "handoff" && reason !== "overflow") {
12374
13309
  const handoffFocus = AUTO_HANDOFF_THRESHOLD_FOCUS;
@@ -12408,6 +13343,7 @@ export class AgentSession {
12408
13343
  if (continueAfterMaintenance && reason !== "idle" && compactionSettings.autoContinue !== false) {
12409
13344
  this.#scheduleAutoContinuePrompt(generation);
12410
13345
  }
13346
+
12411
13347
  if (autoCompactionSignal.aborted) return { kind: "aborted", source: "signal" };
12412
13348
  return { kind: "compacted" };
12413
13349
  }
@@ -12453,18 +13389,54 @@ export class AgentSession {
12453
13389
  if (autoCompactionSignal.aborted) return await emitAborted();
12454
13390
 
12455
13391
  if (!preparation) {
12456
- const continuationSkipReason = willRetry ? this.#detectOverflowRetryContinuationSkip() : undefined;
13392
+ // #checkCompaction removes the failed overflow assistant before entering
13393
+ // this path. A resumable tail would therefore resend the same oversized
13394
+ // request even though maintenance made no change. Non-resumable tails
13395
+ // retain the existing synthetic auto-continue recovery, which changes
13396
+ // the request and is covered by the continuation contract.
13397
+ const overflowNoopWouldReplay = reason === "overflow" && willRetry && this.#isResumableAgentTail();
13398
+ const continuationSkipReason =
13399
+ !overflowNoopWouldReplay && willRetry ? this.#detectOverflowRetryContinuationSkip() : undefined;
13400
+ if (continuationSkipReason) {
13401
+ this.#logCompactionContinuationSkipped("overflow_retry", continuationSkipReason);
13402
+ }
13403
+ if (overflowNoopWouldReplay) {
13404
+ options?.beforeTerminalOverflowNoop?.();
13405
+ }
13406
+ const overflowContinuationScheduled =
13407
+ !overflowNoopWouldReplay && willRetry && !continuationSkipReason
13408
+ ? await this.#scheduleOverflowRetryContinuation(generation)
13409
+ : false;
12457
13410
  await this.#emitSessionEvent({
12458
13411
  type: "auto_compaction_end",
12459
13412
  action,
12460
13413
  result: undefined,
12461
13414
  aborted: false,
12462
- willRetry: willRetry && !continuationSkipReason,
13415
+ willRetry: overflowContinuationScheduled,
13416
+ errorMessage: overflowNoopWouldReplay
13417
+ ? "Context overflow recovery skipped: nothing eligible to compact. Run /clear to preserve this session ID, or switch to a larger-context model before retrying."
13418
+ : undefined,
12463
13419
  skipped: true,
12464
13420
  continuationSkipReason,
12465
13421
  });
13422
+ if (overflowNoopWouldReplay) {
13423
+ if (continueAfterMaintenance && this.agent.hasQueuedMessages()) {
13424
+ this.#scheduleAgentContinue({
13425
+ delayMs: 100,
13426
+ generation,
13427
+ shouldContinue: () => this.agent.hasQueuedMessages(),
13428
+ rescheduleOnBusy: true,
13429
+ onSkip: skipReason => this.#logCompactionContinuationSkipped("queued_continue", skipReason),
13430
+ onError: error => this.#logCompactionContinuationError("queued_continue", error),
13431
+ });
13432
+ return { kind: "skipped", continuationScheduled: true };
13433
+ }
13434
+ return { kind: "skipped" };
13435
+ }
12466
13436
  if (willRetry) {
12467
- return { kind: "skipped", continuationScheduled: this.#scheduleOverflowRetryContinuation(generation) };
13437
+ return overflowContinuationScheduled
13438
+ ? { kind: "skipped", continuationScheduled: true }
13439
+ : { kind: "skipped" };
12468
13440
  }
12469
13441
  if (continueAfterMaintenance && reason !== "idle" && this.agent.hasQueuedMessages()) {
12470
13442
  this.#scheduleAgentContinue({
@@ -12473,6 +13445,7 @@ export class AgentSession {
12473
13445
  suppressPredecessorAgentEnd: true,
12474
13446
 
12475
13447
  shouldContinue: () => this.agent.hasQueuedMessages(),
13448
+ rescheduleOnBusy: true,
12476
13449
  onSkip: skipReason => this.#logCompactionContinuationSkipped("queued_continue", skipReason),
12477
13450
  onError: error => this.#logCompactionContinuationError("queued_continue", error),
12478
13451
  });
@@ -12513,7 +13486,11 @@ export class AgentSession {
12513
13486
  }
12514
13487
  }
12515
13488
 
12516
- const compactionPrep = await this.#prepareCompactionFromHooks(preparation, hookCompaction);
13489
+ const compactionPrep = await this.#prepareCompactionFromHooks(
13490
+ preparation,
13491
+ hookCompaction,
13492
+ compactionStateSnapshot,
13493
+ );
12517
13494
  if (autoCompactionSignal.aborted) return await emitAborted();
12518
13495
 
12519
13496
  let summary: string;
@@ -12696,18 +13673,25 @@ export class AgentSession {
12696
13673
  this.#lastOversizedAutoMaintenanceAttemptSignature = undefined;
12697
13674
 
12698
13675
  const continuationSkipReason = willRetry ? this.#detectOverflowRetryContinuationSkip() : undefined;
13676
+ if (continuationSkipReason) {
13677
+ this.#logCompactionContinuationSkipped("overflow_retry", continuationSkipReason);
13678
+ }
13679
+ const overflowContinuationScheduled =
13680
+ willRetry && !continuationSkipReason ? await this.#scheduleOverflowRetryContinuation(generation) : false;
12699
13681
  await this.#emitSessionEvent({
12700
13682
  type: "auto_compaction_end",
12701
13683
  action,
12702
13684
  result,
12703
13685
  aborted: false,
12704
- willRetry: willRetry && !continuationSkipReason,
13686
+ willRetry: overflowContinuationScheduled,
12705
13687
  continuationSkipReason,
12706
13688
  });
12707
13689
  if (autoCompactionSignal.aborted) return { kind: "aborted", source: "signal" };
12708
13690
 
12709
13691
  if (willRetry) {
12710
- return { kind: "compacted", continuationScheduled: this.#scheduleOverflowRetryContinuation(generation) };
13692
+ return overflowContinuationScheduled
13693
+ ? { kind: "compacted", continuationScheduled: true }
13694
+ : { kind: "compacted" };
12711
13695
  }
12712
13696
  if (continueAfterMaintenance && reason !== "idle" && this.agent.hasQueuedMessages()) {
12713
13697
  // Auto-compaction can complete while follow-up/steering/custom messages are waiting.
@@ -12717,6 +13701,7 @@ export class AgentSession {
12717
13701
  generation,
12718
13702
  suppressPredecessorAgentEnd: true,
12719
13703
  shouldContinue: () => this.agent.hasQueuedMessages(),
13704
+ rescheduleOnBusy: true,
12720
13705
  onSkip: reason => this.#logCompactionContinuationSkipped("queued_continue", reason),
12721
13706
  onError: error => this.#logCompactionContinuationError("queued_continue", error),
12722
13707
  });
@@ -12831,6 +13816,10 @@ export class AgentSession {
12831
13816
  return false;
12832
13817
  }
12833
13818
 
13819
+ #isTypedFirstEventTimeout(message: AssistantMessage): boolean {
13820
+ return message.transportFailure?.providerCode?.toLowerCase() === STREAM_FIRST_EVENT_TIMEOUT_PROVIDER_CODE;
13821
+ }
13822
+
12834
13823
  #isTerminalProviderFirstEventTimeout(message: AssistantMessage): boolean {
12835
13824
  return this.#isKimiCodeFirstEventTimeout(message) || this.#isAlibabaTokenPlanFirstEventTimeout(message);
12836
13825
  }
@@ -12956,6 +13945,7 @@ export class AgentSession {
12956
13945
  #classifyErrorForRetry(message: AssistantMessage): RetryErrorClassification {
12957
13946
  if (message.stopReason !== "error") return "none";
12958
13947
  if (message.errorKind === "provider_safety_stop") return "terminal";
13948
+ if (this.#isTypedFirstEventTimeout(message)) return "first_event_timeout";
12959
13949
  if (!message.errorMessage) return "none";
12960
13950
  const err = message.errorMessage;
12961
13951
  // Provider safety refusals (e.g. Anthropic stop_reason "refusal" /
@@ -12996,10 +13986,8 @@ export class AgentSession {
12996
13986
  if (this.#isTerminalProviderFirstEventTimeout(message)) {
12997
13987
  return "terminal";
12998
13988
  }
12999
- // A first-event timeout on ollama-cloud (the ollama-chat API) must not
13000
- // join the unbounded transient class: each continuation retry re-issues
13001
- // the full request to a remote, billable backend, so an unbounded loop
13002
- // can silently spike usage (#713). Bound it to retry.maxRetries instead.
13989
+ // Legacy providers that have not yet stamped the typed timeout fact retain
13990
+ // their existing bounded ollama-cloud behavior.
13003
13991
  if (this.#isFirstEventTimeoutErrorMessage(err) && this.#shouldFailClosedOnFirstEventTimeout(message)) {
13004
13992
  return "first_event_timeout";
13005
13993
  }
@@ -13169,8 +14157,15 @@ export class AgentSession {
13169
14157
  type: "maintenance",
13170
14158
  continuation: async ownership => {
13171
14159
  if (!ownership.isCurrent()) return;
13172
- const successorScheduled = await this.#checkCompaction(outcome.message);
13173
- if (successorScheduled || !ownership.isCurrent()) return;
14160
+ let terminalized = false;
14161
+ const successorScheduled = await this.#checkCompaction(outcome.message, true, () => {
14162
+ terminalized = true;
14163
+ this.agent.requestRunTerminal(ownership.logicalRunId, {
14164
+ stopReason: "error",
14165
+ messages: [outcome.message],
14166
+ });
14167
+ });
14168
+ if (terminalized || successorScheduled || !ownership.isCurrent()) return;
13174
14169
  this.agent.requestRunTerminal(ownership.logicalRunId, {
13175
14170
  stopReason: "error",
13176
14171
  messages: [outcome.message],
@@ -13328,25 +14323,25 @@ export class AgentSession {
13328
14323
  return `Model fallback chain exhausted; models tried: ${tried}; models skipped: ${skipped}`;
13329
14324
  }
13330
14325
 
13331
- async #markFailedManagedCredential(trigger: {
13332
- class: FallbackTriggerClass;
13333
- retryAfterMs?: number;
13334
- }): Promise<boolean> {
14326
+ async #markFailedCredential(trigger: { class: FallbackTriggerClass; retryAfterMs?: number }): Promise<boolean> {
13335
14327
  if (!this.model || (trigger.class !== "auth" && trigger.class !== "quota" && trigger.class !== "rate_limit")) {
13336
14328
  return false;
13337
14329
  }
13338
14330
  const authStorage = this.#modelRegistry.authStorage;
14331
+ const providerAffinitySessionId = this.agent.providerSessionId ?? this.agent.sessionId ?? this.sessionId;
13339
14332
  if (trigger.class === "auth") {
13340
- const apiKey = await this.#modelRegistry.getApiKey(this.model, this.sessionId);
14333
+ const apiKey = await this.#modelRegistry.getApiKey(this.model, providerAffinitySessionId);
13341
14334
  if (!isAuthenticated(apiKey)) return false;
13342
- return authStorage.invalidateCredentialMatching(this.model.provider, apiKey, { sessionId: this.sessionId });
14335
+ return authStorage.invalidateCredentialMatching(this.model.provider, apiKey, {
14336
+ sessionId: providerAffinitySessionId,
14337
+ });
13343
14338
  }
13344
14339
  if (authStorage.hasRuntimeApiKey(this.model.provider)) return false;
13345
- const activeApiKey = await this.#modelRegistry.getApiKey(this.model, this.sessionId);
13346
- const rotated = await authStorage.markUsageLimitReached(this.model.provider, this.sessionId, {
14340
+ const activeApiKey = await this.#modelRegistry.getApiKey(this.model, providerAffinitySessionId);
14341
+ const rotated = await authStorage.markUsageLimitReached(this.model.provider, providerAffinitySessionId, {
13347
14342
  retryAfterMs: trigger.retryAfterMs,
13348
14343
  });
13349
- return rotated && (await this.#modelRegistry.getApiKey(this.model, this.sessionId)) !== activeApiKey;
14344
+ return rotated && (await this.#modelRegistry.getApiKey(this.model, providerAffinitySessionId)) !== activeApiKey;
13350
14345
  }
13351
14346
 
13352
14347
  /** Handle retryable errors with exponential backoff. */
@@ -13367,16 +14362,16 @@ export class AgentSession {
13367
14362
  // user opt-out.
13368
14363
  if (!managedFallback && !retrySettings.enabled) return false;
13369
14364
  const classification = managedFallback ? undefined : this.#classifyErrorForRetry(message);
13370
- // Bare defaults admit only clean, side-effect-free canonical stream watchdog failures.
13371
- if (!managedFallback && !legacyRetryConfigured) {
13372
- if (
13373
- hasBareDefaultRetryDisqualifyingFacts(message) ||
13374
- (classification !== "transient" && classification !== "first_event_timeout") ||
13375
- !BARE_DEFAULT_WATCHDOG_ERROR.test(message.errorMessage ?? "") ||
13376
- !this.#hasCleanRetryReplaySafety
13377
- ) {
13378
- return false;
13379
- }
14365
+ if (classification === "first_event_timeout") {
14366
+ this.#firstEventTimeoutRetryStartedAt ??= Date.now();
14367
+ }
14368
+ // First-event timeouts are replay-safe only before any model progress,
14369
+ // abort, or extension/provider lifecycle participation.
14370
+ if (
14371
+ classification === "first_event_timeout" &&
14372
+ (!this.#hasCleanRetryReplaySafety || assistantMessageHasVisibleOrToolContent(message))
14373
+ ) {
14374
+ return false;
13380
14375
  }
13381
14376
  const trigger = this.#fallbackTriggerFor(message, !managedFallback, transportFailure);
13382
14377
  if (!trigger) {
@@ -13384,6 +14379,39 @@ export class AgentSession {
13384
14379
  ? this.#managedFallbackExhaustionDecision(message, message.errorMessage || "Model fallback attempt failed")
13385
14380
  : false;
13386
14381
  }
14382
+ // Credential rotation: a content-free quota/rate-limit failure has no
14383
+ // observable state to corrupt, so it is replay-safe regardless of
14384
+ // extension lifecycle participation. Mark the failed credential and
14385
+ // retry with the next stored credential of the same provider.
14386
+ let credentialRotated =
14387
+ !managedFallback &&
14388
+ !assistantMessageHasVisibleOrToolContent(message) &&
14389
+ (trigger.class === "quota" || trigger.class === "rate_limit") &&
14390
+ (await this.#markFailedCredential(trigger));
14391
+ // A content-free credential rotation is inherently replay-safe: no partial
14392
+ // output, no tool calls, no extension-observable streaming state was produced
14393
+ // before the failure. This bypasses #hasCleanRetryReplaySafety because the
14394
+ // content-free check is the replay-safety guarantee for credential rotation.
14395
+ const canReplayRotatedCredential = credentialRotated;
14396
+ // Bare defaults admit only clean, side-effect-free canonical stream watchdog failures,
14397
+ // content-free quota/rate-limit credential rotations, and the explicit Codex
14398
+ // capacity-overload event.
14399
+ if (!managedFallback && !legacyRetryConfigured && !canReplayRotatedCredential) {
14400
+ const bareDefaultCodexOverload = isBareDefaultCodexOverload(message);
14401
+ // Content-free Codex overload is replay-safe by the same reasoning as
14402
+ // credential rotation: no partial output means no observable state.
14403
+ const canReplayCodexOverload = bareDefaultCodexOverload;
14404
+ if (
14405
+ (!canReplayCodexOverload &&
14406
+ !this.#isTypedFirstEventTimeout(message) &&
14407
+ (hasBareDefaultRetryDisqualifyingFacts(message) ||
14408
+ (classification !== "transient" && classification !== "first_event_timeout") ||
14409
+ !BARE_DEFAULT_WATCHDOG_ERROR.test(message.errorMessage ?? ""))) ||
14410
+ (!canReplayCodexOverload && !this.#hasCleanRetryReplaySafety)
14411
+ ) {
14412
+ return false;
14413
+ }
14414
+ }
13387
14415
  const legacyUnbounded = classification === "transient";
13388
14416
  const attemptsUsed = managedFallback ? controller.attemptsUsed || 1 : this.#retryAttempt + 1;
13389
14417
  const failedSelector = managedFallback ? controller.currentSelector() : undefined;
@@ -13392,12 +14420,14 @@ export class AgentSession {
13392
14420
  : legacyUnbounded || attemptsUsed <= retrySettings.maxRetries
13393
14421
  ? "retry"
13394
14422
  : "exhausted";
13395
- const credentialRotated =
13396
- managedFallback &&
13397
- outcome === "advance" &&
13398
- (trigger.class === "quota" || trigger.class === "rate_limit") &&
13399
- (await this.#markFailedManagedCredential(trigger));
13400
- if (credentialRotated && controller.restorePreviousEntryForRetry()) {
14423
+ // Credential rotation is unbounded: a fresh credential is a different
14424
+ // retry dimension from transient-error backoff, so it overrides maxRetries
14425
+ // exhaustion and forces an immediate same-model retry.
14426
+ if (managedFallback && outcome === "advance" && (trigger.class === "quota" || trigger.class === "rate_limit")) {
14427
+ credentialRotated = await this.#markFailedCredential(trigger);
14428
+ }
14429
+ if (credentialRotated) {
14430
+ if (managedFallback) controller.restorePreviousEntryForRetry();
13401
14431
  outcome = "retry";
13402
14432
  }
13403
14433
  if (outcome === "exhausted") {
@@ -13408,6 +14438,10 @@ export class AgentSession {
13408
14438
  controller.resetSticky();
13409
14439
  return managedOutcome ? this.#managedFallbackExhaustionDecision(message, errorMessage) : false;
13410
14440
  }
14441
+ if (classification === "first_event_timeout") {
14442
+ const elapsedMs = Date.now() - (this.#firstEventTimeoutRetryStartedAt ?? Date.now());
14443
+ message.errorMessage = `First-event stream timeout exhausted after ${attemptsUsed} attempts; waited ${elapsedMs}ms total: ${message.errorMessage ?? "Unknown error"}`;
14444
+ }
13411
14445
  return false;
13412
14446
  }
13413
14447
 
@@ -13429,7 +14463,7 @@ export class AgentSession {
13429
14463
  }
13430
14464
 
13431
14465
  const retry = async (ownership?: ManagedAttemptContinuationOwnership): Promise<void> => {
13432
- if (managedFallback && !credentialRotated) await this.#markFailedManagedCredential(trigger);
14466
+ if (managedFallback && !credentialRotated) await this.#markFailedCredential(trigger);
13433
14467
  let advanced = outcome !== "advance";
13434
14468
  let resolutionError: unknown;
13435
14469
  if (outcome === "advance") {
@@ -13471,7 +14505,12 @@ export class AgentSession {
13471
14505
  await this.#emitSessionEvent({
13472
14506
  type: "auto_retry_start",
13473
14507
  attempt: this.#retryAttempt,
13474
- maxAttempts: managedFallback ? controller.maxAttempts : retrySettings.maxRetries,
14508
+ maxAttempts:
14509
+ classification === "first_event_timeout"
14510
+ ? retrySettings.maxRetries + 1
14511
+ : managedFallback
14512
+ ? controller.maxAttempts
14513
+ : retrySettings.maxRetries,
13475
14514
  delayMs,
13476
14515
  errorMessage,
13477
14516
  unbounded: managedFallback ? false : legacyUnbounded,
@@ -13479,9 +14518,19 @@ export class AgentSession {
13479
14518
 
13480
14519
  const messages = this.agent.state.messages;
13481
14520
  if (messages.length > 0 && messages[messages.length - 1].role === "assistant") {
13482
- this.agent.replaceMessages(messages.slice(0, -1));
14521
+ let end = messages.length - 1;
14522
+ // A wedged turn can leave more than one assistant tail behind (e.g. the
14523
+ // agent loop's invalid_prompt repair resend commits its own rejected
14524
+ // message). agent.continue() refuses ANY assistant tail, so strip the
14525
+ // whole trailing run of failed attempts, not just the last one.
14526
+ while (end > 0) {
14527
+ const previous = messages[end - 1];
14528
+ if (previous.role !== "assistant") break;
14529
+ if (previous.stopReason !== "error" && previous.stopReason !== "aborted") break;
14530
+ end--;
14531
+ }
14532
+ this.agent.replaceMessages(messages.slice(0, end));
13483
14533
  }
13484
-
13485
14534
  try {
13486
14535
  await scheduler.wait(delayMs, { signal: retryAbortController.signal });
13487
14536
  } catch {
@@ -13721,12 +14770,8 @@ export class AgentSession {
13721
14770
  // Bash Execution
13722
14771
  // =========================================================================
13723
14772
 
13724
- async #saveBashOriginalArtifact(originalText: string): Promise<string | undefined> {
13725
- try {
13726
- return await this.sessionManager.saveArtifact(originalText, "bash-original");
13727
- } catch {
13728
- return undefined;
13729
- }
14773
+ async #saveBashOriginalArtifact(originalText: string): Promise<BashArtifactSaveResult> {
14774
+ return saveAgentBashOriginalArtifact(this.sessionManager, originalText);
13730
14775
  }
13731
14776
 
13732
14777
  /**
@@ -14656,7 +15701,10 @@ export class AgentSession {
14656
15701
  * Listeners are preserved and will continue receiving events.
14657
15702
  * @returns true if switch completed, false if cancelled by hook
14658
15703
  */
14659
- async switchSession(sessionPath: string): Promise<boolean> {
15704
+ async switchSession(
15705
+ sessionPath: string,
15706
+ options?: { transition?: SessionSwitchEvent["transition"] },
15707
+ ): Promise<boolean> {
14660
15708
  this.#beginSessionTransition("switch-session");
14661
15709
  try {
14662
15710
  const previousSessionFile = this.sessionManager.getSessionFile();
@@ -14717,7 +15765,7 @@ export class AgentSession {
14717
15765
  // workflow-gate emitter, and hooks cannot resolve against an ungated
14718
15766
  // root. The manager-rotation window itself is tracked in #3138
14719
15767
  // (#2797 / #2925).
14720
- if (switchingToDifferentSession) await initializeLocalRoot(this.#localProtocolOptions());
15768
+ if (switchingToDifferentSession) await this.#initializeLocalRootForLoadedSession();
14721
15769
  this.#syncAgentSessionId();
14722
15770
  this.#rekeyHindsightMemoryForCurrentSessionId();
14723
15771
 
@@ -14739,9 +15787,13 @@ export class AgentSession {
14739
15787
  this.#rebindProviderSessionState(new Map());
14740
15788
  }
14741
15789
 
15790
+ const resumeModelBehavior = this.settings.get("session.resumeModelBehavior");
14742
15791
  const configuredDefaultChain = sessionContext.configuredModelChains.default?.entries;
15792
+ const settingsDefaultEntries = normalizeModelSelectorValue(this.settings.getModelRole("default"));
14743
15793
  const defaultEntries =
14744
- configuredDefaultChain ?? (sessionContext.models.default ? [sessionContext.models.default] : []);
15794
+ resumeModelBehavior === "useCurrentDefault"
15795
+ ? settingsDefaultEntries
15796
+ : (configuredDefaultChain ?? (sessionContext.models.default ? [sessionContext.models.default] : []));
14745
15797
  this.#defaultFallbackController = undefined;
14746
15798
  if (defaultEntries.length > 0) {
14747
15799
  const resolution = await resolveModelChainWithAuth(
@@ -14782,6 +15834,12 @@ export class AgentSession {
14782
15834
  ? this.#getInheritedThinkingLevel()
14783
15835
  : persistedThinkingLevel,
14784
15836
  );
15837
+ this.#thinkingLevelMutationRevision++;
15838
+ this.#thinkingLevelLiveMutationRevision++;
15839
+ this.#pendingThinkingLevelControlSuccess = undefined;
15840
+ this.#pendingThinkingLevelControlFailure = undefined;
15841
+ this.#pendingThinkingVisibilityControlSuccess = undefined;
15842
+ this.#pendingThinkingVisibilityControlFailure = undefined;
14785
15843
  this.#thinkingLevel = nextThinkingLevel;
14786
15844
  this.agent.setThinkingLevel(toReasoningEffort(nextThinkingLevel));
14787
15845
  this.agent.serviceTier = hasServiceTierEntry
@@ -14792,6 +15850,7 @@ export class AgentSession {
14792
15850
  // Establish the successor's durable session identity only after every
14793
15851
  // restored state facet is live. Identity-bound extension hooks run below.
14794
15852
  await this.sessionManager.ensureOnDisk();
15853
+ if (!switchingToDifferentSession) await this.#initializeLocalRootForLoadedSession();
14795
15854
 
14796
15855
  if (switchingToDifferentSession) {
14797
15856
  // The local:// migration gate for this successor already ran above,
@@ -14814,6 +15873,7 @@ export class AgentSession {
14814
15873
  type: "session_switch",
14815
15874
  reason: "resume",
14816
15875
  previousSessionFile,
15876
+ ...(options?.transition ? { transition: options.transition } : {}),
14817
15877
  });
14818
15878
  }
14819
15879
  return true;
@@ -14849,8 +15909,14 @@ export class AgentSession {
14849
15909
  this.agent.restoreSteering(previousAgentSteeringQueue);
14850
15910
  this.agent.restoreFollowUp(previousAgentFollowUpQueue);
14851
15911
  if (previousModel) {
14852
- this.agent.setModel(previousModel);
15912
+ this.#setAgentModelWithReasoningContext(previousModel);
14853
15913
  }
15914
+ this.#thinkingLevelMutationRevision++;
15915
+ this.#thinkingLevelLiveMutationRevision++;
15916
+ this.#pendingThinkingLevelControlSuccess = undefined;
15917
+ this.#pendingThinkingLevelControlFailure = undefined;
15918
+ this.#pendingThinkingVisibilityControlSuccess = undefined;
15919
+ this.#pendingThinkingVisibilityControlFailure = undefined;
14854
15920
  this.#thinkingLevel = previousThinkingLevel;
14855
15921
  this.agent.setThinkingLevel(toReasoningEffort(previousThinkingLevel));
14856
15922
  this.agent.serviceTier = previousServiceTier;