@sayknow-cli/coding-agent 0.4.7 → 0.5.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 (428) hide show
  1. package/CHANGELOG.md +122 -0
  2. package/dist/types/capability/index.d.ts +10 -20
  3. package/dist/types/capability/types.d.ts +3 -0
  4. package/dist/types/cli/args.d.ts +0 -2
  5. package/dist/types/cli/read-cli.d.ts +1 -0
  6. package/dist/types/cli.d.ts +9 -1
  7. package/dist/types/commands/daemon.d.ts +2 -2
  8. package/dist/types/commands/deep-interview.d.ts +9 -0
  9. package/dist/types/commands/harness.d.ts +22 -0
  10. package/dist/types/commands/read.d.ts +6 -0
  11. package/dist/types/commands/sdk.d.ts +20 -2
  12. package/dist/types/config/keybindings.d.ts +34 -4
  13. package/dist/types/config/model-profile-contract.d.ts +40 -0
  14. package/dist/types/config/model-profiles.d.ts +2 -3
  15. package/dist/types/config/model-registry.d.ts +5 -0
  16. package/dist/types/config/provider-auth-health.d.ts +14 -0
  17. package/dist/types/config/provider-ranking.d.ts +59 -0
  18. package/dist/types/config/settings-schema.d.ts +156 -10
  19. package/dist/types/config/settings.d.ts +3 -0
  20. package/dist/types/edit/modes/patch.d.ts +13 -0
  21. package/dist/types/edit/path-mutation-lock.d.ts +15 -0
  22. package/dist/types/eval/py/executor.d.ts +1 -1
  23. package/dist/types/eval/py/runner-artifact.d.ts +8 -0
  24. package/dist/types/eval/py/tool-bridge.d.ts +1 -2
  25. package/dist/types/extensibility/extensions/types.d.ts +43 -2
  26. package/dist/types/extensibility/shared-events.d.ts +6 -0
  27. package/dist/types/internal-urls/local-root-gc.d.ts +2 -0
  28. package/dist/types/main.d.ts +33 -2
  29. package/dist/types/memories/index.d.ts +2 -0
  30. package/dist/types/modes/acp/acp-agent.d.ts +22 -9
  31. package/dist/types/modes/components/assistant-message.d.ts +3 -1
  32. package/dist/types/modes/components/btw-panel.d.ts +1 -0
  33. package/dist/types/modes/components/irc-sidebar.d.ts +41 -0
  34. package/dist/types/modes/components/queue-pane.d.ts +8 -0
  35. package/dist/types/modes/components/queued-message-selector.d.ts +6 -0
  36. package/dist/types/modes/components/read-tool-group.d.ts +4 -4
  37. package/dist/types/modes/components/runtime-mcp-add-wizard.d.ts +2 -1
  38. package/dist/types/modes/components/session-observer-overlay.d.ts +4 -0
  39. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  40. package/dist/types/modes/components/tool-execution.d.ts +8 -0
  41. package/dist/types/modes/components/tool-status-header.d.ts +4 -2
  42. package/dist/types/modes/components/welcome.d.ts +5 -0
  43. package/dist/types/modes/controllers/btw-controller.d.ts +5 -0
  44. package/dist/types/modes/controllers/command-controller.d.ts +2 -0
  45. package/dist/types/modes/controllers/event-controller.d.ts +3 -0
  46. package/dist/types/modes/interactive-mode.d.ts +13 -2
  47. package/dist/types/modes/irc-observation-ledger.d.ts +17 -1
  48. package/dist/types/modes/prompt-action-autocomplete.d.ts +6 -1
  49. package/dist/types/modes/prompt-suggestion-controller.d.ts +35 -0
  50. package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +2 -2
  51. package/dist/types/modes/types.d.ts +6 -0
  52. package/dist/types/modes/utils/hotkeys-markdown.d.ts +2 -1
  53. package/dist/types/modes/utils/ui-helpers.d.ts +16 -0
  54. package/dist/types/runtime/memory-domain.d.ts +9 -0
  55. package/dist/types/runtime/memory-guard-contract.d.ts +71 -0
  56. package/dist/types/runtime/memory-guard.d.ts +39 -0
  57. package/dist/types/runtime/memory-limit.d.ts +11 -0
  58. package/dist/types/runtime-mcp/content-limits.d.ts +7 -0
  59. package/dist/types/runtime-mcp/discoverable-tool-metadata.d.ts +1 -1
  60. package/dist/types/runtime-mcp/manager.d.ts +9 -1
  61. package/dist/types/runtime-mcp/oauth-flow.d.ts +0 -5
  62. package/dist/types/runtime-mcp/plugin-network-boundary.d.ts +14 -0
  63. package/dist/types/runtime-mcp/redaction.d.ts +2 -0
  64. package/dist/types/runtime-mcp/smithery-auth.d.ts +5 -0
  65. package/dist/types/sdk/acp/final-text.d.ts +19 -0
  66. package/dist/types/sdk/acp/mcp.d.ts +23 -0
  67. package/dist/types/sdk/broker/broker.d.ts +2 -0
  68. package/dist/types/sdk/broker/lifecycle.d.ts +13 -0
  69. package/dist/types/sdk/bus/chat-daemon-control.d.ts +9 -1
  70. package/dist/types/sdk/bus/config-commands.d.ts +2 -2
  71. package/dist/types/sdk/bus/config.d.ts +2 -0
  72. package/dist/types/sdk/bus/control-drain-lease.d.ts +52 -0
  73. package/dist/types/sdk/bus/conversation-store.d.ts +1 -0
  74. package/dist/types/sdk/bus/daemon-paths.d.ts +1 -0
  75. package/dist/types/sdk/bus/index.d.ts +52 -4
  76. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +37 -0
  77. package/dist/types/sdk/bus/lifecycle-commands.d.ts +5 -4
  78. package/dist/types/sdk/bus/notification-orchestration.d.ts +1 -0
  79. package/dist/types/sdk/bus/notification-service.d.ts +34 -0
  80. package/dist/types/sdk/bus/operator-runtime.d.ts +1 -0
  81. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +106 -0
  82. package/dist/types/sdk/bus/rate-limit-pool.d.ts +2 -0
  83. package/dist/types/sdk/bus/reconciliation-store.d.ts +65 -0
  84. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +33 -9
  85. package/dist/types/sdk/bus/telegram-daemon-control.d.ts +5 -1
  86. package/dist/types/sdk/bus/telegram-daemon.d.ts +286 -12
  87. package/dist/types/sdk/bus/telegram-reference.d.ts +2 -0
  88. package/dist/types/sdk/bus/topic-registry.d.ts +88 -2
  89. package/dist/types/sdk/client/discovery.d.ts +2 -0
  90. package/dist/types/sdk/client/liveness.d.ts +7 -0
  91. package/dist/types/sdk/host/control/operations.d.ts +3 -2
  92. package/dist/types/sdk/host/host.d.ts +3 -1
  93. package/dist/types/sdk/host/query/handlers.d.ts +14 -0
  94. package/dist/types/sdk/index.d.ts +2 -0
  95. package/dist/types/sdk/lifecycle-session.d.ts +10 -1
  96. package/dist/types/sdk/prompt-status.d.ts +88 -0
  97. package/dist/types/sdk/session-directory.d.ts +1 -1
  98. package/dist/types/sdk/session.d.ts +7 -0
  99. package/dist/types/sdk/startup-capability.d.ts +9 -0
  100. package/dist/types/sdk/transport/auth-preface.d.ts +9 -0
  101. package/dist/types/sdk/transport/index.d.ts +12 -0
  102. package/dist/types/sdk/transport/relay.d.ts +43 -0
  103. package/dist/types/sdk/transport/serve-cli.d.ts +4 -0
  104. package/dist/types/sdk/transport/socket.d.ts +7 -0
  105. package/dist/types/sdk/transport/stdio.d.ts +3 -0
  106. package/dist/types/session/agent-session.d.ts +83 -24
  107. package/dist/types/session/btw-contract.d.ts +16 -0
  108. package/dist/types/session/fallback-chain-controller.d.ts +18 -2
  109. package/dist/types/session/internal/managed-session-scope.d.ts +70 -10
  110. package/dist/types/session/internal/managed-session-storage.d.ts +34 -12
  111. package/dist/types/session/internal/native-publish-outcome.d.ts +48 -0
  112. package/dist/types/session/memory-guard-checkpoint-participant.d.ts +69 -0
  113. package/dist/types/session/session-manager.d.ts +89 -18
  114. package/dist/types/session/session-storage.d.ts +31 -0
  115. package/dist/types/session/streaming-output.d.ts +60 -5
  116. package/dist/types/setup/credential-import.d.ts +5 -1
  117. package/dist/types/setup/provider-onboarding.d.ts +2 -0
  118. package/dist/types/skc-runtime/deep-interview-runtime.d.ts +1 -1
  119. package/dist/types/skc-runtime/deep-interview-stage.d.ts +37 -0
  120. package/dist/types/skc-runtime/gc-runtime.d.ts +1 -1
  121. package/dist/types/skc-runtime/linux-proc.d.ts +22 -14
  122. package/dist/types/skc-runtime/memory-guard-owner-claims.d.ts +47 -0
  123. package/dist/types/skc-runtime/ralplan-runtime.d.ts +112 -1
  124. package/dist/types/skc-runtime/repository-binding.d.ts +63 -0
  125. package/dist/types/skc-runtime/state-runtime.d.ts +3 -1
  126. package/dist/types/skc-runtime/state-writer.d.ts +5 -1
  127. package/dist/types/skc-runtime/team-launch.d.ts +1 -1
  128. package/dist/types/skc-runtime/team-runtime.d.ts +9 -2
  129. package/dist/types/skc-runtime/team-store.d.ts +12 -0
  130. package/dist/types/skc-runtime/team-worker-memory-guard.d.ts +92 -0
  131. package/dist/types/skc-runtime/tmux-owner-isolation.d.ts +7 -0
  132. package/dist/types/skc-runtime/ultragoal-guard.d.ts +6 -5
  133. package/dist/types/skc-runtime/ultragoal-receipt-freshness.d.ts +28 -0
  134. package/dist/types/skc-runtime/ultragoal-runtime.d.ts +43 -5
  135. package/dist/types/skill-state/workflow-hud.d.ts +3 -0
  136. package/dist/types/ssh/utils.d.ts +1 -0
  137. package/dist/types/task/discovery.d.ts +3 -1
  138. package/dist/types/task/executor.d.ts +6 -0
  139. package/dist/types/task/index.d.ts +3 -1
  140. package/dist/types/task/provider-retry-status.d.ts +13 -0
  141. package/dist/types/task/receipt.d.ts +2 -0
  142. package/dist/types/task/types.d.ts +133 -0
  143. package/dist/types/task/ultragoal-redteam-activation.d.ts +33 -0
  144. package/dist/types/tools/browser/launch.d.ts +7 -0
  145. package/dist/types/tools/fetch.d.ts +22 -9
  146. package/dist/types/tools/image-gen.d.ts +63 -1
  147. package/dist/types/tools/index.d.ts +7 -1
  148. package/dist/types/tools/output-meta.d.ts +21 -1
  149. package/dist/types/tools/read-internals.d.ts +12 -0
  150. package/dist/types/tools/read.d.ts +45 -1
  151. package/dist/types/tools/resource-gc.d.ts +23 -0
  152. package/dist/types/tools/skill-discovery.d.ts +7 -0
  153. package/dist/types/tools/sqlite-reader.d.ts +7 -0
  154. package/dist/types/tools/tool-result.d.ts +7 -1
  155. package/dist/types/utils/prompt-suggestion.d.ts +33 -0
  156. package/dist/types/utils/shell-snapshot.d.ts +12 -0
  157. package/dist/types/web/insane/bridge.d.ts +1 -0
  158. package/dist/types/web/insane/url-guard.d.ts +17 -0
  159. package/package.json +8 -12
  160. package/scripts/benchmark-sticky-viewport-pr1.ts +239 -0
  161. package/scripts/capture-platform-shortcut-labels-showcase.ts +342 -0
  162. package/scripts/capture-sticky-viewport-showcase.ts +292 -0
  163. package/scripts/compile-args.ts +0 -1
  164. package/scripts/dogfood-repository-binding.ts +209 -0
  165. package/scripts/generate-docs-index.ts +4 -1
  166. package/scripts/generate-sdk-operation-inventory.ts +9 -0
  167. package/scripts/run-test-manifest.ts +6 -4
  168. package/scripts/verify-skc-sdk-canonicalization.ts +122 -18
  169. package/scripts/verify-sticky-viewport-showcase.ts +611 -0
  170. package/src/capability/index.ts +58 -42
  171. package/src/capability/types.ts +3 -0
  172. package/src/cli/args.ts +16 -100
  173. package/src/cli/fast-help.ts +1 -1
  174. package/src/cli/mcp-cli.ts +4 -21
  175. package/src/cli/notify-cli.ts +6 -38
  176. package/src/cli/read-cli.ts +2 -1
  177. package/src/cli/setup-cli.ts +10 -4
  178. package/src/cli.ts +133 -11
  179. package/src/commands/daemon.ts +22 -5
  180. package/src/commands/deep-interview.ts +26 -1
  181. package/src/commands/harness.ts +35 -12
  182. package/src/commands/read.ts +11 -2
  183. package/src/commands/sdk.ts +134 -17
  184. package/src/commands/team.ts +1 -1
  185. package/src/commit/agentic/index.ts +3 -3
  186. package/src/commit/map-reduce/index.ts +2 -2
  187. package/src/config/keybindings.ts +158 -25
  188. package/src/config/model-profile-contract.ts +174 -0
  189. package/src/config/model-profiles.ts +51 -10
  190. package/src/config/model-registry.ts +24 -4
  191. package/src/config/model-resolver.ts +2 -2
  192. package/src/config/provider-auth-health.ts +42 -0
  193. package/src/config/provider-ranking.ts +119 -0
  194. package/src/config/settings-schema.ts +157 -10
  195. package/src/config/settings.ts +254 -108
  196. package/src/coordinator-mcp/model-preset.ts +21 -74
  197. package/src/defaults/skc/skills/deep-interview/SKILL.md +147 -24
  198. package/src/defaults/skc/skills/ralplan/SKILL.md +91 -34
  199. package/src/defaults/skc/skills/team/SKILL.md +1 -1
  200. package/src/defaults/skc/skills/ultragoal/SKILL.md +68 -17
  201. package/src/discovery/ssh.ts +12 -1
  202. package/src/edit/index.ts +3 -6
  203. package/src/edit/modes/patch.ts +38 -1
  204. package/src/edit/modes/replace.ts +42 -0
  205. package/src/edit/path-mutation-lock.ts +112 -0
  206. package/src/eval/js/tool-bridge.ts +1 -1
  207. package/src/eval/py/executor.ts +37 -13
  208. package/src/eval/py/kernel.ts +1 -21
  209. package/src/eval/py/prelude.py +2 -2
  210. package/src/eval/py/runner-artifact.ts +84 -0
  211. package/src/eval/py/tool-bridge.ts +34 -18
  212. package/src/extensibility/extensions/runner.ts +4 -0
  213. package/src/extensibility/extensions/types.ts +42 -4
  214. package/src/extensibility/plugins/marketplace/fetcher.ts +191 -7
  215. package/src/extensibility/shared-events.ts +5 -0
  216. package/src/extensibility/skc-plugins/runtime-adapters.ts +2 -1
  217. package/src/hooks/native-skill-hook.ts +17 -4
  218. package/src/internal-urls/docs-index.generated.ts +17 -16
  219. package/src/internal-urls/local-protocol.ts +87 -25
  220. package/src/internal-urls/local-root-gc.ts +127 -0
  221. package/src/internal-urls/mcp-protocol.ts +73 -11
  222. package/src/main.ts +120 -42
  223. package/src/memories/index.ts +10 -0
  224. package/src/modes/DESIGN.md +52 -0
  225. package/src/modes/acp/acp-agent.ts +647 -83
  226. package/src/modes/acp/acp-event-mapper.ts +106 -12
  227. package/src/modes/acp/acp-mode.ts +39 -3
  228. package/src/modes/action-registry.ts +1 -1
  229. package/src/modes/components/assistant-message.ts +123 -22
  230. package/src/modes/components/btw-panel.ts +82 -16
  231. package/src/modes/components/irc-sidebar.ts +176 -62
  232. package/src/modes/components/model-selector.ts +61 -4
  233. package/src/modes/components/notifications-settings-editor.ts +1 -1
  234. package/src/modes/components/oauth-selector.ts +42 -12
  235. package/src/modes/components/plan-preview-overlay.ts +1 -1
  236. package/src/modes/components/queue-pane.ts +55 -9
  237. package/src/modes/components/queued-message-selector.ts +53 -10
  238. package/src/modes/components/read-tool-group.ts +82 -14
  239. package/src/modes/components/runtime-mcp-add-wizard.ts +5 -0
  240. package/src/modes/components/sayknow-pet-widget.ts +20 -8
  241. package/src/modes/components/session-observer-overlay.ts +187 -32
  242. package/src/modes/components/settings-selector.ts +60 -18
  243. package/src/modes/components/tool-execution.ts +138 -27
  244. package/src/modes/components/tool-status-header.ts +26 -8
  245. package/src/modes/components/welcome.ts +50 -23
  246. package/src/modes/controllers/btw-controller.ts +119 -36
  247. package/src/modes/controllers/command-controller.ts +101 -54
  248. package/src/modes/controllers/event-controller.ts +119 -27
  249. package/src/modes/controllers/extension-ui-controller.ts +23 -12
  250. package/src/modes/controllers/input-controller.ts +100 -8
  251. package/src/modes/controllers/runtime-mcp-command-controller.ts +29 -6
  252. package/src/modes/controllers/selector-controller.ts +4 -1
  253. package/src/modes/interactive-mode.ts +201 -60
  254. package/src/modes/irc-observation-ledger.ts +71 -15
  255. package/src/modes/prompt-action-autocomplete.ts +32 -13
  256. package/src/modes/prompt-suggestion-controller.ts +96 -0
  257. package/src/modes/runtime-init.ts +20 -2
  258. package/src/modes/shared/agent-wire/command-dispatch.ts +3 -3
  259. package/src/modes/shared/agent-wire/workflow-gate-broker.ts +2 -2
  260. package/src/modes/types.ts +6 -0
  261. package/src/modes/utils/hotkeys-markdown.ts +64 -36
  262. package/src/modes/utils/ui-helpers.ts +99 -4
  263. package/src/prompts/agent-fragments/restricted-bash.md +1 -1
  264. package/src/prompts/agents/architect.md +17 -3
  265. package/src/prompts/agents/critic.md +17 -2
  266. package/src/prompts/agents/executor.md +1 -1
  267. package/src/prompts/agents/planner.md +8 -1
  268. package/src/prompts/system/btw-user.md +3 -8
  269. package/src/prompts/system/prompt-suggestion-system.md +32 -0
  270. package/src/prompts/system/system-prompt.md +6 -3
  271. package/src/prompts/tools/cron.md +3 -1
  272. package/src/prompts/tools/read.md +4 -2
  273. package/src/prompts/tools/skill-discovery.md +1 -0
  274. package/src/runtime/memory-domain.ts +66 -0
  275. package/src/runtime/memory-guard-contract.ts +73 -0
  276. package/src/runtime/memory-guard.ts +292 -0
  277. package/src/runtime/memory-limit.ts +50 -0
  278. package/src/runtime-mcp/client.ts +41 -55
  279. package/src/runtime-mcp/content-limits.ts +71 -0
  280. package/src/runtime-mcp/discoverable-tool-metadata.ts +1 -1
  281. package/src/runtime-mcp/json-rpc.ts +28 -11
  282. package/src/runtime-mcp/manager.ts +77 -59
  283. package/src/runtime-mcp/oauth-flow.ts +0 -72
  284. package/src/runtime-mcp/plugin-network-boundary.ts +101 -0
  285. package/src/runtime-mcp/redaction.ts +30 -0
  286. package/src/runtime-mcp/smithery-auth.ts +17 -2
  287. package/src/runtime-mcp/smithery-connect.ts +11 -1
  288. package/src/runtime-mcp/transports/http.ts +63 -22
  289. package/src/sdk/acp/adapter.ts +8 -1
  290. package/src/sdk/acp/final-text.ts +40 -0
  291. package/src/sdk/acp/mcp.ts +26 -0
  292. package/src/sdk/broker/broker.ts +34 -20
  293. package/src/sdk/broker/identity.ts +22 -12
  294. package/src/sdk/broker/lifecycle.ts +293 -43
  295. package/src/sdk/broker/session-index.ts +11 -1
  296. package/src/sdk/bus/chat-daemon-cli.ts +2 -2
  297. package/src/sdk/bus/chat-daemon-control.ts +22 -5
  298. package/src/sdk/bus/config-commands.ts +2 -2
  299. package/src/sdk/bus/config.ts +10 -2
  300. package/src/sdk/bus/control-drain-lease.ts +104 -0
  301. package/src/sdk/bus/conversation-store.ts +40 -6
  302. package/src/sdk/bus/daemon-paths.ts +2 -0
  303. package/src/sdk/bus/index.ts +793 -121
  304. package/src/sdk/bus/kind-aware-reconciliation.ts +242 -0
  305. package/src/sdk/bus/lifecycle-commands.ts +63 -10
  306. package/src/sdk/bus/lifecycle-control-runtime.ts +11 -2
  307. package/src/sdk/bus/lifecycle-orchestrator.ts +32 -1
  308. package/src/sdk/bus/notification-orchestration.ts +12 -0
  309. package/src/sdk/bus/notification-service.ts +87 -25
  310. package/src/sdk/bus/operator-runtime.ts +31 -5
  311. package/src/sdk/bus/prompt-reconciliation.ts +240 -0
  312. package/src/sdk/bus/rate-limit-pool.ts +8 -0
  313. package/src/sdk/bus/recent-activity.ts +90 -26
  314. package/src/sdk/bus/reconciliation-store.ts +224 -0
  315. package/src/sdk/bus/telegram-cli.ts +6 -13
  316. package/src/sdk/bus/telegram-daemon-cli.ts +8 -3
  317. package/src/sdk/bus/telegram-daemon-contract.ts +34 -9
  318. package/src/sdk/bus/telegram-daemon-control.ts +161 -40
  319. package/src/sdk/bus/telegram-daemon.ts +3649 -593
  320. package/src/sdk/bus/telegram-reference.ts +20 -2
  321. package/src/sdk/bus/topic-registry.ts +451 -12
  322. package/src/sdk/cli/session-cli.ts +15 -2
  323. package/src/sdk/client/discovery.ts +19 -3
  324. package/src/sdk/client/liveness.ts +49 -0
  325. package/src/sdk/host/control/dispatch.ts +8 -2
  326. package/src/sdk/host/control/operations.ts +3 -2
  327. package/src/sdk/host/host.ts +27 -9
  328. package/src/sdk/host/query/handlers.ts +125 -5
  329. package/src/sdk/index.ts +12 -0
  330. package/src/sdk/lifecycle-session.ts +19 -3
  331. package/src/sdk/mcp/server.ts +46 -18
  332. package/src/sdk/prompt-status.ts +97 -0
  333. package/src/sdk/protocol/operation-inventory.generated.json +145 -0
  334. package/src/sdk/protocol/operation-registry.ts +24 -4
  335. package/src/sdk/session-directory.ts +5 -1
  336. package/src/sdk/session.ts +69 -8
  337. package/src/sdk/startup-capability.ts +15 -1
  338. package/src/sdk/transport/auth-preface.ts +82 -0
  339. package/src/sdk/transport/index.ts +18 -0
  340. package/src/sdk/transport/relay.ts +252 -0
  341. package/src/sdk/transport/serve-cli.ts +105 -0
  342. package/src/sdk/transport/socket.ts +197 -0
  343. package/src/sdk/transport/stdio.ts +17 -0
  344. package/src/session/agent-session.ts +1112 -314
  345. package/src/session/blob-store.ts +25 -7
  346. package/src/session/btw-contract.ts +59 -0
  347. package/src/session/fallback-chain-controller.ts +47 -8
  348. package/src/session/internal/managed-session-scope.ts +1426 -259
  349. package/src/session/internal/managed-session-storage.ts +414 -238
  350. package/src/session/internal/native-publish-outcome.ts +271 -0
  351. package/src/session/memory-guard-checkpoint-participant.ts +131 -0
  352. package/src/session/session-manager.ts +2079 -469
  353. package/src/session/session-storage.ts +151 -11
  354. package/src/session/streaming-output.ts +262 -43
  355. package/src/setup/credential-import.ts +28 -3
  356. package/src/setup/provider-onboarding.ts +49 -5
  357. package/src/setup/provider-presets.json +2 -1
  358. package/src/skc-runtime/deep-interview-runtime.ts +303 -36
  359. package/src/skc-runtime/deep-interview-stage.ts +1016 -0
  360. package/src/skc-runtime/deep-interview-state.ts +8 -5
  361. package/src/skc-runtime/gc-render.ts +1 -0
  362. package/src/skc-runtime/gc-runtime.ts +11 -1
  363. package/src/skc-runtime/linux-proc.ts +70 -36
  364. package/src/skc-runtime/managed-owner-supervisor.ts +83 -4
  365. package/src/skc-runtime/memory-guard-owner-claims.ts +389 -0
  366. package/src/skc-runtime/ralplan-runtime.ts +1057 -75
  367. package/src/skc-runtime/repository-binding.ts +267 -0
  368. package/src/skc-runtime/session-state-sidecar.ts +25 -9
  369. package/src/skc-runtime/state-runtime.ts +50 -12
  370. package/src/skc-runtime/state-writer.ts +68 -56
  371. package/src/skc-runtime/team-launch.ts +34 -5
  372. package/src/skc-runtime/team-runtime.ts +41 -1
  373. package/src/skc-runtime/team-store.ts +32 -0
  374. package/src/skc-runtime/team-worker-memory-guard.ts +363 -0
  375. package/src/skc-runtime/team-workers.ts +1 -0
  376. package/src/skc-runtime/tmux-common.ts +13 -2
  377. package/src/skc-runtime/tmux-owner-isolation.ts +20 -1
  378. package/src/skc-runtime/tmux-sessions.ts +45 -6
  379. package/src/skc-runtime/ultragoal-guard.ts +93 -15
  380. package/src/skc-runtime/ultragoal-receipt-freshness.ts +123 -0
  381. package/src/skc-runtime/ultragoal-runtime.ts +469 -38
  382. package/src/skc-runtime/workflow-manifest.generated.json +195 -5
  383. package/src/skc-runtime/workflow-manifest.ts +67 -3
  384. package/src/skill-state/workflow-hud.ts +25 -2
  385. package/src/skill-state/workflow-mutation-guard.ts +104 -12
  386. package/src/slash-commands/builtin-registry.ts +1 -1
  387. package/src/ssh/utils.ts +14 -0
  388. package/src/task/discovery.ts +8 -2
  389. package/src/task/executor.ts +165 -47
  390. package/src/task/index.ts +123 -15
  391. package/src/task/provider-retry-status.ts +88 -0
  392. package/src/task/receipt.ts +3 -0
  393. package/src/task/render.ts +45 -26
  394. package/src/task/skc-command.ts +2 -2
  395. package/src/task/types.ts +55 -10
  396. package/src/task/ultragoal-redteam-activation.ts +75 -0
  397. package/src/tools/ask.ts +1 -1
  398. package/src/tools/ast-edit.ts +2 -2
  399. package/src/tools/bash-allowed-prefixes.ts +45 -1
  400. package/src/tools/bash-pty-selection.ts +2 -2
  401. package/src/tools/browser/launch.ts +41 -6
  402. package/src/tools/browser/screenshot-format.ts +6 -1
  403. package/src/tools/cron.ts +1 -1
  404. package/src/tools/fetch.ts +127 -154
  405. package/src/tools/image-gen.ts +476 -23
  406. package/src/tools/index.ts +7 -1
  407. package/src/tools/output-meta.ts +176 -9
  408. package/src/tools/path-utils.ts +5 -1
  409. package/src/tools/read-internals.ts +44 -0
  410. package/src/tools/read.ts +1544 -310
  411. package/src/tools/resource-gc.ts +665 -125
  412. package/src/tools/skill-discovery.ts +39 -3
  413. package/src/tools/sqlite-reader.ts +63 -5
  414. package/src/tools/subagent-render.ts +74 -16
  415. package/src/tools/subagent.ts +2 -0
  416. package/src/tools/tool-result.ts +14 -1
  417. package/src/utils/edit-mode.ts +2 -2
  418. package/src/utils/pasted-image-path.ts +15 -6
  419. package/src/utils/prompt-suggestion.ts +265 -0
  420. package/src/utils/shell-snapshot.ts +173 -63
  421. package/src/utils/sixel.ts +3 -3
  422. package/src/web/insane/bridge.ts +5 -1
  423. package/src/web/insane/url-guard.ts +140 -27
  424. package/src/web/scrapers/docs-rs.ts +30 -5
  425. package/src/web/scrapers/types.ts +19 -49
  426. package/src/web/scrapers/utils.ts +12 -26
  427. package/src/web/search/providers/codex.ts +1 -1
  428. package/src/workflow/workflow-intent-diff.ts +4 -1
@@ -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,
@@ -105,6 +106,16 @@ import {
105
106
  type FallbackAttemptToken,
106
107
  type FallbackTriggerClass,
107
108
  } from "@sayknow-cli/ai/utils/fallback-transport";
109
+ import {
110
+ BTW_MAX_ANSWER_UTF8_BYTES,
111
+ BTW_MAX_QUESTION_UTF8_BYTES,
112
+ BTW_STREAM_IDLE_TIMEOUT_MS,
113
+ BTW_STREAM_TOTAL_TIMEOUT_MS,
114
+ type BtwTextExchange,
115
+ boundBtwExchanges,
116
+ truncateUtf8,
117
+ utf8ByteLength,
118
+ } from "./btw-contract";
108
119
 
109
120
  export interface ForkContextSeedMetadata {
110
121
  sourceSessionId: string;
@@ -162,6 +173,7 @@ import { type AsyncJob, type AsyncJobDeliveryState, AsyncJobManager } from "../a
162
173
  import { reset as resetCapabilities } from "../capability";
163
174
  import type { Rule } from "../capability/rule";
164
175
  import type { CasReceipt } from "../config/atomic-yaml-patch";
176
+ import { activateModelProfile } from "../config/model-profile-activation";
165
177
  import {
166
178
  isAuthenticated,
167
179
  kNoAuth,
@@ -223,9 +235,10 @@ import type {
223
235
  TurnEndEvent,
224
236
  TurnStartEvent,
225
237
  } from "../extensibility/extensions";
226
- import type { CompactOptions, ContextUsage } from "../extensibility/extensions/types";
238
+ import type { CompactOptions, ContextUsage, ExtensionTranscriptEntry } from "../extensibility/extensions/types";
227
239
  import { ExtensionToolWrapper } from "../extensibility/extensions/wrapper";
228
240
  import type { HookCommandContext } from "../extensibility/hooks/types";
241
+ import type { SessionSwitchEvent } from "../extensibility/shared-events";
229
242
  import {
230
243
  type LoadedSubskillActivation,
231
244
  resolveSubskillActivationForSkillInvocation,
@@ -233,13 +246,19 @@ import {
233
246
  import { resolveCurrentPhaseForParent } from "../extensibility/skc-plugins/injection";
234
247
  import { readActiveSubskillsForParent, toActiveSubskillEntry } from "../extensibility/skc-plugins/state";
235
248
  import { loadActiveSubskillTools } from "../extensibility/skc-plugins/tools";
236
- import { buildSkillPromptMessage, type Skill, type SkillWarning } from "../extensibility/skills";
249
+ import {
250
+ buildSkillPromptMessage,
251
+ getSkillSlashCommandName,
252
+ parseSkillInvocations,
253
+ type Skill,
254
+ type SkillWarning,
255
+ } from "../extensibility/skills";
237
256
  import { expandSlashCommand, type FileSlashCommand } from "../extensibility/slash-commands";
238
257
  import { GoalRuntime } from "../goals/runtime";
239
258
  import type { Goal, GoalModeState } from "../goals/state";
240
259
  import type { HindsightSessionState } from "../hindsight/state";
241
260
  import { buildSkillStopOutput, ensureWorkflowSkillActivationState } from "../hooks/skill-state";
242
- import { type LocalProtocolOptions, resolveLocalUrlToPath } from "../internal-urls";
261
+ import { initializeLocalRoot, type LocalProtocolOptions, resolveLocalUrlToPath } from "../internal-urls";
243
262
  import { shutdownAll as shutdownAllLspClients } from "../lsp/client";
244
263
  import { resolveMemoryBackend } from "../memory-backend";
245
264
  import {
@@ -266,13 +285,20 @@ import { MCPManager } from "../runtime-mcp/manager";
266
285
  import type { NotificationSessionController } from "../sdk/bus/session-control";
267
286
  import { deobfuscateSessionContext, type SecretObfuscator } from "../secrets/obfuscator";
268
287
  import { formatNoCredentialOnboardingError, formatNoModelOnboardingError } from "../setup/model-onboarding-guidance";
288
+ import { assertDeepInterviewIntentManifest } from "../skc-runtime/deep-interview-state";
269
289
  import { buildSkcRuntimeSessionEnv, consumePendingGoalModeRequest } from "../skc-runtime/goal-mode-request";
290
+ import {
291
+ isMemoryGuardClaimsLease,
292
+ isMemoryGuardClaimsLeaseForStateDir,
293
+ type MemoryGuardClaimsLease,
294
+ } from "../skc-runtime/memory-guard-owner-claims";
270
295
  import {
271
296
  assertNonEmptySkcSessionId,
272
297
  modeStatePath as sessionModeStatePath,
273
298
  sessionStateDir,
274
299
  } from "../skc-runtime/session-layout";
275
300
  import {
301
+ ownerTerminalContextFromEnvironment,
276
302
  persistCoordinatorRuntimeStateFromEvent,
277
303
  registerCoordinatorRuntimeStateFinalizer,
278
304
  } from "../skc-runtime/session-state-sidecar";
@@ -329,6 +355,7 @@ import {
329
355
  import {
330
356
  type ConfiguredFallbackChain,
331
357
  cappedExponentialWithFullJitter,
358
+ compactionRetryDelay,
332
359
  effectiveFallbackDelay,
333
360
  FallbackChainController,
334
361
  } from "./fallback-chain-controller";
@@ -348,6 +375,7 @@ import {
348
375
  prepareContributionPrep,
349
376
  } from "./contribution-prep";
350
377
  import { pruneStaleFileMentions } from "./file-mention-pruning";
378
+ import type { MemoryGuardRestoreResult } from "./memory-guard-checkpoint-participant";
351
379
  import {
352
380
  type BashExecutionMessage,
353
381
  type CompactionSummaryMessage,
@@ -367,6 +395,7 @@ import type {
367
395
  CompactionEntry,
368
396
  DefaultModelSelectionStage,
369
397
  NewSessionOptions,
398
+ PreparedNewSession,
370
399
  RecoveryHydrationContext,
371
400
  RecoveryHydrationPromotionFence,
372
401
  SessionContext,
@@ -592,6 +621,24 @@ export interface AgentSessionConfig {
592
621
  providerCacheSessionId?: string;
593
622
  }
594
623
 
624
+ export interface AgentSessionMemoryGuardRestoreInput
625
+ extends Omit<AgentSessionConfig, "sessionManager" | "recoveryHydrationContext"> {
626
+ staged: Extract<MemoryGuardRestoreResult, { kind: "staged" }>;
627
+ claimsLease: MemoryGuardClaimsLease;
628
+ claimsStateDir: string;
629
+ }
630
+
631
+ export interface AgentMemoryGuardPromotionFence extends RecoveryHydrationPromotionFence {
632
+ readonly claimsLease: MemoryGuardClaimsLease;
633
+ }
634
+
635
+ export type AgentMemoryGuardRestoreResult =
636
+ | { kind: "staged"; session: AgentSession; promotionFence: AgentMemoryGuardPromotionFence }
637
+ | {
638
+ kind: "blocked";
639
+ reason: "transcript-mismatch" | "hydration-context-mismatch" | "claim-mismatch" | "agent-messages-mismatch";
640
+ };
641
+
595
642
  type MidRunMaintenanceLifecycle = Parameters<NonNullable<AgentLoopConfig["maintainContext"]>>[1];
596
643
 
597
644
  type AutoCompactionTerminalStatus =
@@ -621,8 +668,16 @@ export interface PromptOptions {
621
668
  /**
622
669
  * Invoked after all prompt preflight checks pass and immediately before agent execution begins.
623
670
  * Cancellation before this callback rejects the prompt.
671
+ * Prefer `onPreflightAcceptCommit` for async durable acceptance (#3031/#3032).
624
672
  */
625
673
  onPreflightAccepted?: () => void;
674
+ /**
675
+ * Awaitable durable-accept fence. Called after preflight and before queue mutation
676
+ * or `#promptAgentWithIdleRetry`. SDK bus installs a closure that fsyncs acceptance.
677
+ */
678
+ onPreflightAcceptCommit?: () => void | Promise<void>;
679
+ /** Skill-only: prepared metadata before the durable fence (path/lineCount/cleanedArgs). */
680
+ onSkillPrepared?: (meta: { name: string; path: string; lineCount?: number; cleanedArgs?: string }) => void;
626
681
  }
627
682
 
628
683
  function promptPreflightCancelledError(): Error {
@@ -773,6 +828,28 @@ type RetryErrorClassification =
773
828
 
774
829
  const BARE_DEFAULT_WATCHDOG_ERROR =
775
830
  /^(?:[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)$/;
831
+ const KIMI_CODE_FIRST_EVENT_TIMEOUT_MESSAGES = {
832
+ "anthropic-messages": new Set([
833
+ "Provider stream timed out while waiting for the first event",
834
+ "Anthropic stream timed out while waiting for the first event",
835
+ ]),
836
+ "openai-completions": new Set([
837
+ "Provider stream timed out while waiting for the first event",
838
+ "OpenAI completions stream timed out while waiting for the first event",
839
+ ]),
840
+ } as const;
841
+
842
+ const ALIBABA_TOKEN_PLAN_PROVIDER = "alibaba-token-plan";
843
+ const ALIBABA_TOKEN_PLAN_FIRST_EVENT_TIMEOUT_MESSAGES = {
844
+ "openai-responses": new Set([
845
+ "Provider stream timed out while waiting for the first event",
846
+ "OpenAI responses stream timed out while waiting for the first event",
847
+ ]),
848
+ "openai-completions": new Set([
849
+ "Provider stream timed out while waiting for the first event",
850
+ "OpenAI completions stream timed out while waiting for the first event",
851
+ ]),
852
+ } as const;
776
853
 
777
854
  function hasBareDefaultRetryDisqualifyingFacts(message: AssistantMessage): boolean {
778
855
  if (message.errorKind !== undefined || message.errorStatus !== undefined) return true;
@@ -827,7 +904,6 @@ function isLocalModelEndpoint(model: Model | undefined): boolean {
827
904
  }
828
905
 
829
906
  const IRC_REPLY_MAX_BYTES = 4096;
830
- const BTW_ESTABLISHMENT_TIMEOUT_MS = 15_000;
831
907
 
832
908
  export type EphemeralTurnPurpose = "btw" | "background";
833
909
 
@@ -837,8 +913,34 @@ interface EphemeralTurnBaseArgs {
837
913
  signal?: AbortSignal;
838
914
  }
839
915
 
916
+ export interface BtwRoleTextMessage {
917
+ role: "user" | "assistant";
918
+ text: string;
919
+ }
920
+
921
+ export interface BtwConversationScope {
922
+ model: Model;
923
+ systemPrompt: string[];
924
+ messages: BtwRoleTextMessage[];
925
+ thinkingLevel: ThinkingLevel;
926
+ hideThinkingSummary: boolean;
927
+ serviceTier: ServiceTier | undefined;
928
+ credentialSessionId: string;
929
+ providerAffinitySessionId: string;
930
+ sideSessionId: string;
931
+ }
932
+
933
+ export interface BtwTurnCapture {
934
+ question: string;
935
+ scope: BtwConversationScope | undefined;
936
+ }
937
+
840
938
  export type EphemeralTurnArgs =
841
- | (EphemeralTurnBaseArgs & { purpose: "btw" })
939
+ | (Omit<EphemeralTurnBaseArgs, "promptText"> & {
940
+ purpose: "btw";
941
+ turn: BtwTurnCapture;
942
+ contextExchanges?: readonly BtwTextExchange[];
943
+ })
842
944
  | (EphemeralTurnBaseArgs & {
843
945
  purpose?: "background";
844
946
  /** Internal caller-supplied, non-persistent context such as the IRC roster. */
@@ -990,17 +1092,37 @@ function createHandoffContext(document: string): string {
990
1092
  return `<handoff-context>\n${document}\n</handoff-context>\n\nThe above is a handoff document from a previous session. Use this context to continue the work seamlessly.`;
991
1093
  }
992
1094
 
1095
+ async function discardPreparedNewSessionAfterFailure(
1096
+ sessionManager: SessionManager,
1097
+ prepared: PreparedNewSession,
1098
+ transitionError: unknown,
1099
+ ): Promise<unknown> {
1100
+ try {
1101
+ await sessionManager.discardPreparedNewSession(prepared);
1102
+ } catch (cleanupError) {
1103
+ throw new AggregateError(
1104
+ [transitionError, cleanupError],
1105
+ "Session transition and staged successor cleanup both failed.",
1106
+ );
1107
+ }
1108
+ return transitionError;
1109
+ }
1110
+
993
1111
  // ============================================================================
994
1112
  // ACP Permission Gate
995
1113
  // ============================================================================
996
1114
 
997
1115
  /** Tools that require user permission before execution when an ACP client is connected. */
998
- const PERMISSION_REQUIRED_TOOLS = new Set(["bash", "monitor", "edit", "delete", "move"]);
1116
+ const PERMISSION_REQUIRED_TOOLS = new Set(["bash", "monitor", "eval", "edit", "delete", "move"]);
999
1117
 
1000
1118
  function isShellExecutionPermissionTool(toolName: string): boolean {
1001
1119
  return toolName === "bash" || toolName === "monitor";
1002
1120
  }
1003
1121
 
1122
+ function isExecutionPermissionTool(toolName: string): boolean {
1123
+ return isShellExecutionPermissionTool(toolName) || toolName === "eval";
1124
+ }
1125
+
1004
1126
  /** Permission options presented to the client on each gated tool call. */
1005
1127
  const PERMISSION_OPTIONS: ClientBridgePermissionOption[] = [
1006
1128
  { optionId: "allow_once", name: "Allow once", kind: "allow_once" },
@@ -1068,6 +1190,18 @@ function getPermissionIntent(
1068
1190
  const cmd = getStringProperty(a, "command")?.slice(0, 80);
1069
1191
  return { toolName, title: cmd || toolName, cacheKey: toolName };
1070
1192
  }
1193
+ if (toolName === "eval") {
1194
+ const cells = Array.isArray(a.cells) ? a.cells : [];
1195
+ const firstCell = cells.find(cell => cell && typeof cell === "object" && !Array.isArray(cell));
1196
+ const cell = firstCell as Record<string, unknown> | undefined;
1197
+ const title = cell ? getStringProperty(cell, "title") : undefined;
1198
+ const language = cell ? getStringProperty(cell, "language") : undefined;
1199
+ return {
1200
+ toolName,
1201
+ title: title ?? (language ? `Eval ${language}` : "eval"),
1202
+ cacheKey: toolName,
1203
+ };
1204
+ }
1071
1205
  if (toolName === "delete") {
1072
1206
  const p = getStringProperty(a, "path");
1073
1207
  return { toolName, title: p ? `Delete ${p}` : toolName, paths: p ? [p] : undefined, cacheKey: toolName };
@@ -1438,6 +1572,47 @@ export class AgentSession {
1438
1572
  #sessionAdmissionClosed = false;
1439
1573
  #sessionAdmissionContext = new AsyncLocalStorage<SessionAdmissionEntry>();
1440
1574
  #defaultModelSelectionMutationRevision = 0;
1575
+ #thinkingLevelMutationRevision = 0;
1576
+ #thinkingVisibilityMutationRevision = 0;
1577
+ #thinkingLevelLiveMutationRevision = 0;
1578
+ #thinkingVisibilityLiveMutationRevision = 0;
1579
+ #reasoningControlContextGeneration = 0;
1580
+ #pendingThinkingLevelControlSuccess:
1581
+ | {
1582
+ level: ThinkingLevel;
1583
+ mutationRevision: number;
1584
+ sessionId: string;
1585
+ model: Model | undefined;
1586
+ contextGeneration: number;
1587
+ }
1588
+ | undefined;
1589
+ #pendingThinkingVisibilityControlSuccess:
1590
+ | {
1591
+ visibility: "visible" | "hidden";
1592
+ mutationRevision: number;
1593
+ sessionId: string;
1594
+ model: Model | undefined;
1595
+ contextGeneration: number;
1596
+ }
1597
+ | undefined;
1598
+ #pendingThinkingLevelControlFailure:
1599
+ | {
1600
+ mutationRevision: number;
1601
+ liveMutationRevision: number;
1602
+ sessionId: string;
1603
+ model: Model | undefined;
1604
+ contextGeneration: number;
1605
+ }
1606
+ | undefined;
1607
+ #pendingThinkingVisibilityControlFailure:
1608
+ | {
1609
+ mutationRevision: number;
1610
+ liveMutationRevision: number;
1611
+ sessionId: string;
1612
+ model: Model | undefined;
1613
+ contextGeneration: number;
1614
+ }
1615
+ | undefined;
1441
1616
  #promptTemplates: PromptTemplate[];
1442
1617
  #slashCommands: FileSlashCommand[];
1443
1618
 
@@ -1633,6 +1808,7 @@ export class AgentSession {
1633
1808
 
1634
1809
  #skillsSettings: SkillsSettings | undefined;
1635
1810
  #activeSkillState: { skill: string; sessionId?: string } | undefined;
1811
+ #restoredWorkflowSkillState: { skill: string; sessionId: string } | undefined;
1636
1812
 
1637
1813
  // Model registry for API key resolution
1638
1814
  #modelRegistry: ModelRegistry;
@@ -1688,6 +1864,7 @@ export class AgentSession {
1688
1864
  #constructorMCPToolSelection: string[] | undefined;
1689
1865
  #constructorDiscoveredBuiltinToolSelection: string[] | undefined;
1690
1866
  #recoveryHydrationContext: RecoveryHydrationContext | undefined;
1867
+ #memoryGuardClaimsLease: MemoryGuardClaimsLease | undefined;
1691
1868
 
1692
1869
  // TTSR manager for time-traveling stream rules
1693
1870
  #ttsrManager: TtsrManager | undefined = undefined;
@@ -2126,7 +2303,7 @@ export class AgentSession {
2126
2303
  this.#workerIntegrationScheduler = new WorkerIntegrationRequestScheduler(
2127
2304
  config.workerIntegrationRequest ??
2128
2305
  (async signal => {
2129
- await requestSkcWorkerIntegrationAttempt(this.sessionManager.getCwd(), process.env).catch(
2306
+ await requestSkcWorkerIntegrationAttempt(this.sessionManager.getCwd(), process.env, { signal }).catch(
2130
2307
  error => {
2131
2308
  logger.warn("SKC team worker integration request failed", { error: String(error) });
2132
2309
  },
@@ -2143,6 +2320,7 @@ export class AgentSession {
2143
2320
  this.#unregisterResourceGc = registerResourceGcSession({
2144
2321
  sessionId: resourceGcSessionId,
2145
2322
  settings: this.settings,
2323
+ cwd: () => this.sessionManager.getCwd(),
2146
2324
  });
2147
2325
  }
2148
2326
  this.#unregisterRuntimeStateFinalizer = registerCoordinatorRuntimeStateFinalizer({
@@ -2452,6 +2630,34 @@ export class AgentSession {
2452
2630
  return undefined;
2453
2631
  }
2454
2632
  }
2633
+ /** Provider-facing ask metadata must expose only the active deep-interview phase. */
2634
+ getDeepInterviewAskStage(): "topology" | "post-topology" | undefined {
2635
+ const currentSessionId = this.sessionManager.getSessionId();
2636
+ const inMemory = this.#activeSkillState;
2637
+ const active =
2638
+ inMemory && (!inMemory.sessionId || inMemory.sessionId === currentSessionId)
2639
+ ? inMemory
2640
+ : this.#restoredWorkflowSkillState?.sessionId === currentSessionId
2641
+ ? this.#restoredWorkflowSkillState
2642
+ : undefined;
2643
+ if (active?.skill !== "deep-interview") return undefined;
2644
+ try {
2645
+ assertNonEmptySkcSessionId(currentSessionId, "AgentSession.getDeepInterviewAskStage");
2646
+ const stateDir = sessionStateDir(this.sessionManager.getCwd(), currentSessionId);
2647
+ const filePath = path.join(
2648
+ stateDir,
2649
+ path.basename(sessionModeStatePath(this.sessionManager.getCwd(), currentSessionId, active.skill)),
2650
+ );
2651
+ const raw = fs.readFileSync(filePath, "utf-8");
2652
+ const parsed = JSON.parse(raw) as { state?: { intent_contract?: unknown } };
2653
+ const intentContract = parsed.state?.intent_contract;
2654
+ if (intentContract === undefined) return "topology";
2655
+ assertDeepInterviewIntentManifest(intentContract);
2656
+ return "post-topology";
2657
+ } catch {
2658
+ return undefined;
2659
+ }
2660
+ }
2455
2661
 
2456
2662
  /** Peek the in-flight directive's invocation handler for use by the resolve tool. */
2457
2663
  peekQueueInvoker(): ((input: unknown) => Promise<unknown> | unknown) | undefined {
@@ -2542,9 +2748,15 @@ export class AgentSession {
2542
2748
  this.#defaultFallbackController = scope.fallbackController;
2543
2749
  const previousEditMode = this.#resolveActiveEditMode();
2544
2750
  if (scope.model) {
2545
- this.agent.setModel(scope.model);
2751
+ this.#setAgentModelWithReasoningContext(scope.model);
2546
2752
  this.#syncAppendOnlyContext(scope.model);
2547
2753
  }
2754
+ this.#thinkingLevelMutationRevision++;
2755
+ this.#thinkingLevelLiveMutationRevision++;
2756
+ this.#pendingThinkingLevelControlSuccess = undefined;
2757
+ this.#pendingThinkingLevelControlFailure = undefined;
2758
+ this.#pendingThinkingVisibilityControlSuccess = undefined;
2759
+ this.#pendingThinkingVisibilityControlFailure = undefined;
2548
2760
  this.#thinkingLevel = scope.thinkingLevel;
2549
2761
  this.agent.setThinkingLevel(toReasoningEffort(scope.thinkingLevel));
2550
2762
  void this.#syncEditToolModeAfterModelChange(previousEditMode);
@@ -3910,8 +4122,21 @@ export class AgentSession {
3910
4122
  return;
3911
4123
  }
3912
4124
  const predecessorAgentEnd = this.#claimDeferredAgentEndForContinuation(predecessorAgentEndHold);
4125
+ const startsQueuedSuccessor =
4126
+ this.agent.state.messages.at(-1)?.role === "assistant" && this.agent.hasQueuedMessages();
3913
4127
  try {
3914
- await this.agent.continue(this.#managedFallbackPromptOptions());
4128
+ await this.agent.continue({
4129
+ ...this.#managedFallbackPromptOptions(),
4130
+ // Reset only after continue() has claimed the queued turn. Skipped or stale
4131
+ // continuations retain predecessor accounting, and resetAttemptBudget keeps
4132
+ // the sticky fallback cursor unchanged.
4133
+ onRunAccepted: startsQueuedSuccessor
4134
+ ? () => {
4135
+ this.#defaultFallbackChain().resetAttemptBudget();
4136
+ this.#overflowMaintenanceAttempts = 0;
4137
+ }
4138
+ : undefined,
4139
+ });
3915
4140
  } catch (error) {
3916
4141
  this.#restoreDeferredAgentEndAfterContinuationFailure(predecessorAgentEnd);
3917
4142
  throw error;
@@ -4514,11 +4739,13 @@ export class AgentSession {
4514
4739
  return resolveToCwd(normalized, this.sessionManager.getCwd());
4515
4740
  }
4516
4741
 
4517
- #localProtocolOptions(): LocalProtocolOptions {
4742
+ #localProtocolOptions(prepared?: PreparedNewSession): LocalProtocolOptions {
4518
4743
  return {
4519
- getArtifactsDir: () => this.sessionManager.getArtifactsDir(),
4744
+ getArtifactsDir: () => prepared?.artifactsDir ?? this.sessionManager.getArtifactsDir(),
4520
4745
  isManagedDestination: () => this.sessionManager.isManagedDestination(),
4521
- getSessionId: () => this.sessionManager.getSessionId(),
4746
+ getManagedLegacyLocalMigrationSource: () =>
4747
+ prepared?.managedLegacyLocalMigrationSource ?? this.sessionManager.getManagedLegacyLocalMigrationSource(),
4748
+ getSessionId: () => prepared?.sessionId ?? this.sessionManager.getSessionId(),
4522
4749
  };
4523
4750
  }
4524
4751
 
@@ -4973,6 +5200,7 @@ export class AgentSession {
4973
5200
  * such event.
4974
5201
  */
4975
5202
  #syncAgentSessionId(sessionId?: string): void {
5203
+ this.#reasoningControlContextGeneration++;
4976
5204
  const sid = this.#providerSessionId ?? sessionId ?? this.sessionManager.getSessionId();
4977
5205
  this.agent.sessionId = sid;
4978
5206
  this.agent.providerSessionId = this.#providerCacheSessionId ?? sid;
@@ -5092,7 +5320,7 @@ export class AgentSession {
5092
5320
  // No-op when this session opened no tabs. Failure is logged, not thrown.
5093
5321
  this.#unregisterResourceGc?.();
5094
5322
  this.#unregisterResourceGc = undefined;
5095
- this.#unregisterRuntimeStateFinalizer?.();
5323
+ if (ownerTerminalContextFromEnvironment() === null) this.#unregisterRuntimeStateFinalizer?.();
5096
5324
  this.#unregisterRuntimeStateFinalizer = undefined;
5097
5325
  await releaseTabsForOwner(this.sessionManager.getSessionId()).catch((error: unknown) =>
5098
5326
  logger.warn("session dispose: releaseTabsForOwner failed", { error }),
@@ -5418,6 +5646,12 @@ export class AgentSession {
5418
5646
  return undefined;
5419
5647
  }
5420
5648
 
5649
+ /** Get a registered tool with the same guards used for model-facing execution. */
5650
+ getToolForExecution(name: string): AgentTool | undefined {
5651
+ const tool = this.getToolByName(name);
5652
+ return tool ? this.#prepareToolForExecution(tool) : undefined;
5653
+ }
5654
+
5421
5655
  /**
5422
5656
  * Register a UI/control-plane request handler for a currently foregrounded
5423
5657
  * managed bash execution. This is intentionally narrower than generic
@@ -5682,6 +5916,7 @@ export class AgentSession {
5682
5916
  return await target.execute(toolCallId, args as never, signal, onUpdate, ctx);
5683
5917
  }
5684
5918
  const isShellExecutionTool = isShellExecutionPermissionTool(target.name);
5919
+ const isExecutionTool = isExecutionPermissionTool(target.name);
5685
5920
  const command =
5686
5921
  isShellExecutionTool && args && typeof args === "object" && !Array.isArray(args)
5687
5922
  ? getStringProperty(args as Record<string, unknown>, "command")
@@ -5724,7 +5959,7 @@ export class AgentSession {
5724
5959
  toolCallId,
5725
5960
  toolName: target.name,
5726
5961
  title: permissionIntent.title,
5727
- ...(isShellExecutionTool ? { kind: "execute" } : {}),
5962
+ ...(isExecutionTool ? { kind: "execute" } : {}),
5728
5963
  status: "pending",
5729
5964
  rawInput: args,
5730
5965
  ...(commandContent ? { content: commandContent } : {}),
@@ -6290,17 +6525,65 @@ export class AgentSession {
6290
6525
  return this.#promptGeneration;
6291
6526
  }
6292
6527
 
6528
+ static async restoreFromMemoryGuardCheckpoint(
6529
+ input: AgentSessionMemoryGuardRestoreInput,
6530
+ ): Promise<AgentMemoryGuardRestoreResult> {
6531
+ const { staged, claimsLease, claimsStateDir, ...config } = input;
6532
+ if (
6533
+ staged.manager.getSessionId() !== staged.transcriptIdentity.sessionId ||
6534
+ staged.hydrationContext.identity.sessionId !== staged.transcriptIdentity.sessionId
6535
+ ) {
6536
+ await staged.cleanup();
6537
+ return { kind: "blocked", reason: "transcript-mismatch" };
6538
+ }
6539
+ if (
6540
+ !isMemoryGuardClaimsLeaseForStateDir(claimsLease, claimsStateDir) ||
6541
+ claimsLease.owner.sessionId !== staged.transcriptIdentity.sessionId
6542
+ ) {
6543
+ await staged.cleanup();
6544
+ return { kind: "blocked", reason: "claim-mismatch" };
6545
+ }
6546
+ const checkpointMessages = staged.manager.buildSessionContext().messages;
6547
+ if (!util.isDeepStrictEqual(config.agent.state.messages, checkpointMessages)) {
6548
+ await staged.cleanup();
6549
+ return { kind: "blocked", reason: "agent-messages-mismatch" };
6550
+ }
6551
+ const session = new AgentSession({
6552
+ ...config,
6553
+ sessionManager: staged.manager,
6554
+ recoveryHydrationContext: staged.hydrationContext,
6555
+ });
6556
+ session.#memoryGuardClaimsLease = claimsLease;
6557
+ if (session.recoveryHydrationContext !== staged.hydrationContext) {
6558
+ await session.dispose();
6559
+ await staged.cleanup();
6560
+ return { kind: "blocked", reason: "hydration-context-mismatch" };
6561
+ }
6562
+ return {
6563
+ kind: "staged",
6564
+ session,
6565
+ promotionFence: Object.freeze({ ownershipReady: true, claimsLease }),
6566
+ };
6567
+ }
6568
+
6293
6569
  /** The immutable recovery authority, present only before external ownership promotion. */
6294
6570
  get recoveryHydrationContext(): RecoveryHydrationContext | undefined {
6295
6571
  return this.#recoveryHydrationContext;
6296
6572
  }
6297
6573
 
6298
6574
  /** Enables normal session mutations after the owner has published its durable fence and writer lease. */
6299
- async promoteRecoveryHydrationAfterOwnershipReadyFence(fence: RecoveryHydrationPromotionFence): Promise<void> {
6575
+ async promoteRecoveryHydrationAfterOwnershipReadyFence(fence: AgentMemoryGuardPromotionFence): Promise<void> {
6300
6576
  const context = this.#recoveryHydrationContext;
6301
6577
  if (!context) throw new Error("Agent session is not awaiting recovery hydration promotion.");
6578
+ if (!isMemoryGuardClaimsLease(fence.claimsLease)) {
6579
+ throw new Error("Recovery hydration promotion requires a live memory-guard claims lease.");
6580
+ }
6581
+ if (this.#memoryGuardClaimsLease !== fence.claimsLease) {
6582
+ throw new Error("Recovery hydration promotion requires the acquired memory-guard claims lease.");
6583
+ }
6302
6584
  await this.sessionManager.promoteRecoveryHydrationAfterOwnershipReadyFence(context, fence);
6303
6585
  this.#recoveryHydrationContext = undefined;
6586
+ this.#memoryGuardClaimsLease = undefined;
6304
6587
  }
6305
6588
 
6306
6589
  /** Recovery hydration must not start a continuation before ownership promotion. */
@@ -6372,6 +6655,15 @@ export class AgentSession {
6372
6655
  return { ...context, messages: this.#withoutEphemeralCustomMessages(context.messages) };
6373
6656
  }
6374
6657
 
6658
+ /** Build display context from an unpublished successor without changing active session state. @internal */
6659
+ buildPreparedDisplaySessionContext(prepared: PreparedNewSession): SessionContext {
6660
+ const context = deobfuscateSessionContext(
6661
+ this.sessionManager.buildPreparedNewSessionContext(prepared),
6662
+ this.#obfuscator,
6663
+ );
6664
+ return { ...context, messages: this.#withoutEphemeralCustomMessages(context.messages) };
6665
+ }
6666
+
6375
6667
  /** Convert session messages using the same pre-LLM pipeline as the active session. */
6376
6668
  async convertMessagesToLlm(messages: AgentMessage[], signal?: AbortSignal): Promise<Message[]> {
6377
6669
  const transformedMessages = await this.#transformContext(messages, signal);
@@ -6479,9 +6771,11 @@ export class AgentSession {
6479
6771
  /** Live SDK configuration values exposed through the session query surface. */
6480
6772
  getSdkConfigItems(): Record<string, string> {
6481
6773
  const model = this.model;
6774
+ const modelPreset = this.getActiveModelProfile() ?? this.settings.get("modelProfile.default");
6482
6775
  return {
6483
6776
  mode: this.#planModeState?.enabled ? "plan" : "default",
6484
6777
  ...(model ? { model: `${model.provider}/${model.id}` } : {}),
6778
+ ...(modelPreset ? { modelPreset } : {}),
6485
6779
  thinking: this.#thinkingLevel ?? "off",
6486
6780
  steeringMode: this.steeringMode,
6487
6781
  followUpMode: this.followUpMode,
@@ -6500,6 +6794,7 @@ export class AgentSession {
6500
6794
  async invokeSkill(
6501
6795
  name: string,
6502
6796
  args = "",
6797
+ options?: Pick<PromptOptions, "onPreflightAccepted" | "onPreflightAcceptCommit" | "onSkillPrepared">,
6503
6798
  ): Promise<{ name: string; path: string; args?: string; lineCount?: number }> {
6504
6799
  const skillName = name.trim();
6505
6800
  if (!skillName) throw Object.assign(new Error("skill.invoke requires a skill name."), { code: "invalid_input" });
@@ -6532,7 +6827,13 @@ export class AgentSession {
6532
6827
  attribution: "user" as const,
6533
6828
  };
6534
6829
  this.#deepInterviewPreclaimedCustomInputEpochs.set(skillPromptMessage, deepInterviewUserIntentEpoch);
6535
- await this.promptCustomMessage(skillPromptMessage);
6830
+ options?.onSkillPrepared?.({
6831
+ name: skill.name,
6832
+ path: skill.filePath,
6833
+ lineCount: built.details.lineCount,
6834
+ cleanedArgs: activation.cleanedArgs || undefined,
6835
+ });
6836
+ await this.promptCustomMessage(skillPromptMessage, options);
6536
6837
  return {
6537
6838
  name: skill.name,
6538
6839
  path: skill.filePath,
@@ -6578,22 +6879,49 @@ export class AgentSession {
6578
6879
  }
6579
6880
  }
6580
6881
 
6581
- getTranscript(): Array<{ id: string; role: string; textSummary: string; ts: string; body: string }> {
6882
+ getTranscript(): ExtensionTranscriptEntry[] {
6582
6883
  return getEntriesForInternalRead(this.sessionManager).flatMap(entry => {
6583
6884
  if (entry.type !== "message") return [];
6584
- const message = entry.message as unknown as { role?: unknown; content?: unknown };
6885
+ const message = entry.message as unknown as {
6886
+ role?: unknown;
6887
+ content?: unknown;
6888
+ toolCallId?: unknown;
6889
+ toolName?: unknown;
6890
+ isError?: unknown;
6891
+ };
6892
+ const content: NonNullable<ExtensionTranscriptEntry["content"]> | undefined = Array.isArray(message.content)
6893
+ ? message.content.reduce<NonNullable<ExtensionTranscriptEntry["content"]>>((blocks, part) => {
6894
+ if (typeof part !== "object" || part === null || !("type" in part)) return blocks;
6895
+ if (part.type === "text" && "text" in part && typeof part.text === "string") {
6896
+ blocks.push({ type: "text", text: part.text });
6897
+ return blocks;
6898
+ }
6899
+ if (part.type === "thinking" && "thinking" in part && typeof part.thinking === "string") {
6900
+ blocks.push({ type: "thinking", thinking: part.thinking });
6901
+ return blocks;
6902
+ }
6903
+ if (
6904
+ part.type === "toolCall" &&
6905
+ "id" in part &&
6906
+ typeof part.id === "string" &&
6907
+ "name" in part &&
6908
+ typeof part.name === "string"
6909
+ ) {
6910
+ blocks.push({
6911
+ type: "toolCall",
6912
+ id: part.id,
6913
+ name: part.name,
6914
+ arguments: "arguments" in part ? part.arguments : {},
6915
+ });
6916
+ }
6917
+ return blocks;
6918
+ }, [])
6919
+ : undefined;
6585
6920
  const body =
6586
6921
  typeof message.content === "string"
6587
6922
  ? message.content
6588
- : Array.isArray(message.content)
6589
- ? message.content
6590
- .map(part =>
6591
- typeof part === "object" && part !== null && "text" in part && typeof part.text === "string"
6592
- ? part.text
6593
- : "",
6594
- )
6595
- .filter(Boolean)
6596
- .join("\n")
6923
+ : content
6924
+ ? content.flatMap(part => (part.type === "text" ? [part.text] : [])).join("\n")
6597
6925
  : "";
6598
6926
  return [
6599
6927
  {
@@ -6602,6 +6930,10 @@ export class AgentSession {
6602
6930
  textSummary: body.slice(0, 500),
6603
6931
  ts: entry.timestamp,
6604
6932
  body,
6933
+ ...(content ? { content } : {}),
6934
+ ...(typeof message.toolCallId === "string" ? { toolCallId: message.toolCallId } : {}),
6935
+ ...(typeof message.toolName === "string" ? { toolName: message.toolName } : {}),
6936
+ ...(typeof message.isError === "boolean" ? { isError: message.isError } : {}),
6605
6937
  },
6606
6938
  ];
6607
6939
  });
@@ -6639,8 +6971,7 @@ export class AgentSession {
6639
6971
  return getAskAnswerSourceFromRegistry(this.sessionId);
6640
6972
  }
6641
6973
 
6642
- #constructWorkflowGateEmitter(): WorkflowGateEmitter {
6643
- const sessionId = this.sessionManager.getSessionId();
6974
+ #constructWorkflowGateEmitter(sessionId = this.sessionManager.getSessionId()): WorkflowGateEmitter {
6644
6975
  assertNonEmptySkcSessionId(sessionId, "AgentSession workflow-gate session");
6645
6976
  const gateStore = this.sessionManager.isPersisted()
6646
6977
  ? new FileGateStore(path.join(sessionStateDir(this.sessionManager.getCwd(), sessionId), "workflow-gates.json"))
@@ -6745,8 +7076,9 @@ export class AgentSession {
6745
7076
  if (!activeSkill) {
6746
7077
  try {
6747
7078
  const activeState = await readVisibleSkillActiveState(this.sessionManager.getCwd(), sessionId);
6748
- activeSkill =
6749
- activeState?.skill ?? activeState?.active_skills?.find(entry => entry.active !== false)?.skill;
7079
+ activeSkill = activeState?.active_skills?.find(
7080
+ entry => entry.active !== false && entry.session_id === sessionId,
7081
+ )?.skill;
6750
7082
  } catch (error) {
6751
7083
  logger.warn("Failed to read durable workflow skill state while restoring ask tool", {
6752
7084
  error: error instanceof Error ? error.message : String(error),
@@ -6758,7 +7090,12 @@ export class AgentSession {
6758
7090
  // a predecessor session's workflow state to the current identity.
6759
7091
  if (this.#isDisposed || this.sessionManager.getSessionId() !== sessionId) return;
6760
7092
  }
6761
- if (activeSkill && isCanonicalSkcWorkflowSkill(activeSkill.trim())) this.#attachAskTool();
7093
+ if (activeSkill && isCanonicalSkcWorkflowSkill(activeSkill.trim())) {
7094
+ if (!inMemoryActiveSkill) this.#restoredWorkflowSkillState = { skill: activeSkill.trim(), sessionId };
7095
+ this.#attachAskTool();
7096
+ } else if (this.#restoredWorkflowSkillState?.sessionId === sessionId) {
7097
+ this.#restoredWorkflowSkillState = undefined;
7098
+ }
6762
7099
  }
6763
7100
 
6764
7101
  #attachAskTool(): void {
@@ -7151,8 +7488,32 @@ export class AgentSession {
7151
7488
  * @throws Error if no model selected or no API key available (when not streaming)
7152
7489
  */
7153
7490
  async prompt(text: string, options?: PromptOptions): Promise<void> {
7491
+ this.#assertRecoveryHydrationPromoted();
7154
7492
  const expandPromptTemplates = options?.expandPromptTemplates ?? true;
7155
7493
 
7494
+ if (expandPromptTemplates && text.startsWith("/skill:") && !options?.images?.length) {
7495
+ const skillCommands = new Map(this.skills.map(skill => [getSkillSlashCommandName(skill), skill]));
7496
+ const invocations = parseSkillInvocations(text, skillCommands);
7497
+ if (invocations.length === 1) {
7498
+ const invocation = invocations[0];
7499
+ if (invocation) {
7500
+ await this.invokeSkill(
7501
+ invocation.skill.name,
7502
+ invocation.args,
7503
+ options?.onPreflightAccepted || options?.onPreflightAcceptCommit
7504
+ ? {
7505
+ ...(options.onPreflightAccepted ? { onPreflightAccepted: options.onPreflightAccepted } : {}),
7506
+ ...(options.onPreflightAcceptCommit
7507
+ ? { onPreflightAcceptCommit: options.onPreflightAcceptCommit }
7508
+ : {}),
7509
+ }
7510
+ : undefined,
7511
+ );
7512
+ return;
7513
+ }
7514
+ }
7515
+ }
7516
+
7156
7517
  // Handle extension commands first (execute immediately, even during streaming)
7157
7518
  if (expandPromptTemplates && text.startsWith("/")) {
7158
7519
  const handled = await this.#tryExecuteExtensionCommand(text);
@@ -7200,7 +7561,8 @@ export class AgentSession {
7200
7561
  if (workflowIntentDiff) {
7201
7562
  this.sessionManager.appendCustomEntry(WORKFLOW_INTENT_DIFF_CUSTOM_TYPE, workflowIntentDiff);
7202
7563
  }
7203
- options?.onPreflightAccepted?.();
7564
+ if (options?.onPreflightAcceptCommit) await options.onPreflightAcceptCommit();
7565
+ else options?.onPreflightAccepted?.();
7204
7566
  return;
7205
7567
  }
7206
7568
 
@@ -7310,6 +7672,7 @@ export class AgentSession {
7310
7672
  }
7311
7673
  }
7312
7674
  // In-memory tracking keeps `getActiveSkillState` accurate for the chain guard.
7675
+ this.#restoredWorkflowSkillState = undefined;
7313
7676
  this.#activeSkillState = active ? { skill, sessionId } : undefined;
7314
7677
  if (active) {
7315
7678
  await this.refreshSkcSubskillTools();
@@ -7331,7 +7694,10 @@ export class AgentSession {
7331
7694
 
7332
7695
  async promptCustomMessage<T = unknown>(
7333
7696
  message: Pick<CustomMessage<T>, "customType" | "content" | "display" | "details" | "attribution">,
7334
- options?: Pick<PromptOptions, "streamingBehavior" | "toolChoice" | "followUpQueuePolicy">,
7697
+ options?: Pick<
7698
+ PromptOptions,
7699
+ "streamingBehavior" | "toolChoice" | "followUpQueuePolicy" | "onPreflightAccepted" | "onPreflightAcceptCommit"
7700
+ >,
7335
7701
  ): Promise<void> {
7336
7702
  const textContent =
7337
7703
  typeof message.content === "string"
@@ -7393,7 +7759,10 @@ export class AgentSession {
7393
7759
  async #promptWithMessage(
7394
7760
  message: AgentMessage,
7395
7761
  expandedText: string,
7396
- options?: Pick<PromptOptions, "toolChoice" | "images" | "skipCompactionCheck" | "onPreflightAccepted"> & {
7762
+ options?: Pick<
7763
+ PromptOptions,
7764
+ "toolChoice" | "images" | "skipCompactionCheck" | "onPreflightAccepted" | "onPreflightAcceptCommit"
7765
+ > & {
7397
7766
  prependMessages?: AgentMessage[];
7398
7767
  skipPostPromptRecoveryWait?: boolean;
7399
7768
  predecessorAgentEndHold?: symbol;
@@ -7439,7 +7808,11 @@ export class AgentSession {
7439
7808
  }
7440
7809
 
7441
7810
  // Validate API key
7442
- const apiKey = await this.#modelRegistry.getApiKey(this.model, this.sessionId);
7811
+ const apiKey = await this.#awaitPromptPreflight(
7812
+ generation,
7813
+ preflightSignal,
7814
+ this.#modelRegistry.getApiKey(this.model, this.sessionId, { signal: preflightSignal }),
7815
+ );
7443
7816
  if (!apiKey) {
7444
7817
  throw new Error(formatNoCredentialOnboardingError(this.model.provider));
7445
7818
  }
@@ -7496,7 +7869,7 @@ export class AgentSession {
7496
7869
  // A newer abort/prompt cycle superseded this preflight. Callers awaiting
7497
7870
  // acceptance (onPreflightAccepted) must be told it never ran; direct
7498
7871
  // callers (e.g. prompt() aborted during a TTSR wait) resolve gracefully.
7499
- if (options?.onPreflightAccepted) throw promptPreflightCancelledError();
7872
+ if (options?.onPreflightAccepted || options?.onPreflightAcceptCommit) throw promptPreflightCancelledError();
7500
7873
  return;
7501
7874
  }
7502
7875
 
@@ -7610,7 +7983,7 @@ export class AgentSession {
7610
7983
  this.#resetInjectedContextSignatures();
7611
7984
  // Ack-waiting callers are told the preflight never ran; direct callers
7612
7985
  // (aborted after setup) resolve gracefully as before f24f46ff5.
7613
- if (options?.onPreflightAccepted) throw promptPreflightCancelledError();
7986
+ if (options?.onPreflightAccepted || options?.onPreflightAcceptCommit) throw promptPreflightCancelledError();
7614
7987
  return;
7615
7988
  }
7616
7989
 
@@ -7623,7 +7996,8 @@ export class AgentSession {
7623
7996
  if (hindsightRecall) this.getHindsightSessionState()?.markRecallSnippetInjected(hindsightRecall);
7624
7997
  },
7625
7998
  };
7626
- options?.onPreflightAccepted?.();
7999
+ if (options?.onPreflightAcceptCommit) await options.onPreflightAcceptCommit();
8000
+ else options?.onPreflightAccepted?.();
7627
8001
  this.#throwIfPromptPreflightCancelled(generation, preflightSignal);
7628
8002
  await this.#promptAgentWithIdleRetry(messages, agentPromptOptions, predecessorAgentEndHold);
7629
8003
  const terminalAssistant = this.#findLastAssistantMessage();
@@ -7642,7 +8016,12 @@ export class AgentSession {
7642
8016
  // Session identity changes historically cancel local setup silently. Only SDK
7643
8017
  // submissions provide an acceptance callback and require an explicit terminal
7644
8018
  // preflight failure for their remote request authority.
7645
- if (isPromptPreflightCancelledError(error) && !options?.onPreflightAccepted) return;
8019
+ if (
8020
+ isPromptPreflightCancelledError(error) &&
8021
+ !options?.onPreflightAccepted &&
8022
+ !options?.onPreflightAcceptCommit
8023
+ )
8024
+ return;
7646
8025
  throw error;
7647
8026
  } finally {
7648
8027
  this.#removeEphemeralCustomMessages();
@@ -7726,6 +8105,7 @@ export class AgentSession {
7726
8105
  return tool ? { safeSummary: tool.safeSummary, safeSummaryFields: tool.safeSummaryFields } : undefined;
7727
8106
  },
7728
8107
  cycleModel: () => this.cycleModel(),
8108
+ setModelProfile: name => this.activateModelProfileForControl(name),
7729
8109
  cycleThinkingLevel: () => this.cycleThinkingLevel(),
7730
8110
  setQueueMode: (kind, mode) => {
7731
8111
  if (kind === "steering" && (mode === "all" || mode === "one-at-a-time")) {
@@ -7742,7 +8122,7 @@ export class AgentSession {
7742
8122
  }
7743
8123
  return false;
7744
8124
  },
7745
- invokeSkill: (name, args) => this.invokeSkill(name, args),
8125
+ invokeSkill: (name, args, options) => this.invokeSkill(name, args, options),
7746
8126
  setPlanMode: on => this.setSdkPlanMode(on),
7747
8127
  operateGoal: (op, objective) => this.operateGoal(op, objective),
7748
8128
  getSkillState: () => this.skills.map(skill => ({ name: skill.name, description: skill.description })),
@@ -7753,6 +8133,7 @@ export class AgentSession {
7753
8133
  getJobs: () => undefined,
7754
8134
  sdkBindings: () => [
7755
8135
  "cycleModel",
8136
+ "setModelProfile",
7756
8137
  "cycleThinkingLevel",
7757
8138
  "setQueueMode",
7758
8139
  "getSkillState",
@@ -7856,6 +8237,7 @@ export class AgentSession {
7856
8237
  * Queue a steering message to interrupt the agent mid-run.
7857
8238
  */
7858
8239
  async steer(text: string, images?: ImageContent[]): Promise<void> {
8240
+ this.#assertRecoveryHydrationPromoted();
7859
8241
  if (text.startsWith("/")) {
7860
8242
  this.#throwIfExtensionCommand(text);
7861
8243
  }
@@ -7873,6 +8255,7 @@ export class AgentSession {
7873
8255
  images?: ImageContent[],
7874
8256
  options?: Pick<PromptOptions, "followUpQueuePolicy">,
7875
8257
  ): Promise<void> {
8258
+ this.#assertRecoveryHydrationPromoted();
7876
8259
  if (text.startsWith("/")) {
7877
8260
  this.#throwIfExtensionCommand(text);
7878
8261
  }
@@ -8107,6 +8490,7 @@ export class AgentSession {
8107
8490
  followUpQueuePolicy?: "respect-mode" | "sequential";
8108
8491
  },
8109
8492
  ): Promise<void> {
8493
+ this.#assertRecoveryHydrationPromoted();
8110
8494
  const appMessage: CustomMessage<T> = {
8111
8495
  role: "custom",
8112
8496
  customType: message.customType,
@@ -8248,8 +8632,13 @@ export class AgentSession {
8248
8632
  */
8249
8633
  async sendUserMessage(
8250
8634
  content: string | (TextContent | ImageContent)[],
8251
- options?: { deliverAs?: "steer" | "followUp"; onPreflightAccepted?: () => void },
8635
+ options?: {
8636
+ deliverAs?: "steer" | "followUp";
8637
+ onPreflightAccepted?: () => void;
8638
+ onPreflightAcceptCommit?: () => void | Promise<void>;
8639
+ },
8252
8640
  ): Promise<void> {
8641
+ this.#assertRecoveryHydrationPromoted();
8253
8642
  // Normalize content to text string + optional images
8254
8643
  let text: string;
8255
8644
  let images: ImageContent[] | undefined;
@@ -8271,11 +8660,13 @@ export class AgentSession {
8271
8660
  }
8272
8661
 
8273
8662
  if (options?.deliverAs === "followUp") {
8663
+ if (options.onPreflightAcceptCommit) await options.onPreflightAcceptCommit();
8274
8664
  await this.#queueFollowUp(text, images, { claimsGenuineUserIntent: true });
8275
8665
  options.onPreflightAccepted?.();
8276
8666
  return;
8277
8667
  }
8278
8668
  if (options?.deliverAs === "steer") {
8669
+ if (options.onPreflightAcceptCommit) await options.onPreflightAcceptCommit();
8279
8670
  await this.#queueSteer(text, images, { claimsGenuineUserIntent: true });
8280
8671
  options.onPreflightAccepted?.();
8281
8672
  return;
@@ -8287,6 +8678,7 @@ export class AgentSession {
8287
8678
  // in-flight compaction internally, and #queueSteer would otherwise park
8288
8679
  // the message in the steering queue with no turn to consume it.
8289
8680
  if (this.isStreaming) {
8681
+ if (options?.onPreflightAcceptCommit) await options.onPreflightAcceptCommit();
8290
8682
  await this.#queueSteer(text, images, { claimsGenuineUserIntent: true });
8291
8683
  options?.onPreflightAccepted?.();
8292
8684
  return;
@@ -8297,6 +8689,7 @@ export class AgentSession {
8297
8689
  expandPromptTemplates: false,
8298
8690
  images,
8299
8691
  onPreflightAccepted: options?.onPreflightAccepted,
8692
+ onPreflightAcceptCommit: options?.onPreflightAcceptCommit,
8300
8693
  });
8301
8694
  }
8302
8695
 
@@ -8921,7 +9314,14 @@ export class AgentSession {
8921
9314
  this.#rebindProviderSessionState(new Map());
8922
9315
  this.agent.reset();
8923
9316
  if (!options?.drop) await this.sessionManager.flush();
8924
- await this.sessionManager.newSession(options);
9317
+ const prepared = await this.sessionManager.prepareNewSession(options);
9318
+ try {
9319
+ // Last fallible gate while public getters still show the predecessor (#3138).
9320
+ await initializeLocalRoot(this.#localProtocolOptions(prepared));
9321
+ this.sessionManager.commitPreparedNewSession(prepared);
9322
+ } catch (error) {
9323
+ throw await discardPreparedNewSessionAfterFailure(this.sessionManager, prepared, error);
9324
+ }
8925
9325
  this.setTodoPhases([]);
8926
9326
  this.#syncAgentSessionId();
8927
9327
  this.#bindWorkflowGateEmitter(previousWorkflowGateSessionId);
@@ -8983,12 +9383,18 @@ export class AgentSession {
8983
9383
  if (!(await manager.cancelAndSettleOwnerJobs(ownerId))) {
8984
9384
  throw new Error("Owned async jobs did not settle before session replacement.");
8985
9385
  }
8986
-
9386
+ const prepared = await this.sessionManager.prepareNewSession(options);
9387
+ try {
9388
+ // Last fallible gate while public getters still show the predecessor (#3138).
9389
+ await initializeLocalRoot(this.#localProtocolOptions(prepared));
9390
+ this.sessionManager.commitPreparedNewSession(prepared);
9391
+ } catch (error) {
9392
+ throw await discardPreparedNewSessionAfterFailure(this.sessionManager, prepared, error);
9393
+ }
8987
9394
  this.#disconnectFromAgent();
8988
9395
  this.#closeAllProviderSessions("new session");
8989
9396
  this.#rebindProviderSessionState(new Map());
8990
9397
  this.agent.reset();
8991
- await this.sessionManager.newSession(options);
8992
9398
  this.setTodoPhases([]);
8993
9399
  this.#syncAgentSessionId();
8994
9400
  this.#bindWorkflowGateEmitter(previousWorkflowGateSessionId);
@@ -9023,6 +9429,12 @@ export class AgentSession {
9023
9429
  ): Promise<void> {
9024
9430
  this.#clearConstructorToolSelectionAuthority();
9025
9431
  const inheritedThinkingLevel = resolveThinkingLevelForModel(this.model, this.#getInheritedThinkingLevel());
9432
+ this.#thinkingLevelMutationRevision++;
9433
+ this.#thinkingLevelLiveMutationRevision++;
9434
+ this.#pendingThinkingLevelControlSuccess = undefined;
9435
+ this.#pendingThinkingLevelControlFailure = undefined;
9436
+ this.#pendingThinkingVisibilityControlSuccess = undefined;
9437
+ this.#pendingThinkingVisibilityControlFailure = undefined;
9026
9438
  this.#thinkingLevel = inheritedThinkingLevel;
9027
9439
  this.agent.setThinkingLevel(toReasoningEffort(inheritedThinkingLevel));
9028
9440
  this.sessionManager.appendThinkingLevelChange(ThinkingLevel.Inherit);
@@ -9031,10 +9443,17 @@ export class AgentSession {
9031
9443
  }
9032
9444
  this.sessionManager.appendServiceTierChange(this.serviceTier ?? null);
9033
9445
  if (nextDiscoverySessionToolNames) {
9034
- await this.#applyActiveToolsByName(nextDiscoverySessionToolNames, {
9035
- persistMCPSelection: false,
9036
- nextSelectedDiscoveredBuiltinToolNames: [],
9037
- });
9446
+ // Avoid a full system-prompt rebuild when the active set is unchanged; rebuild
9447
+ // can block on shared workspace-tree work across repeated /new transitions.
9448
+ const current = this.getActiveToolNames().slice().sort().join("\0");
9449
+ const next = [...new Set(nextDiscoverySessionToolNames.map(n => n.toLowerCase()))].sort().join("\0");
9450
+ if (current !== next) {
9451
+ await this.#applyActiveToolsByName(nextDiscoverySessionToolNames, {
9452
+ persistMCPSelection: false,
9453
+ nextSelectedDiscoveredBuiltinToolNames: [],
9454
+ });
9455
+ } else {
9456
+ }
9038
9457
  }
9039
9458
  this.#todoReminderCount = 0;
9040
9459
  this.#planReferenceSent = false;
@@ -9047,6 +9466,7 @@ export class AgentSession {
9047
9466
  reason: "new",
9048
9467
  previousSessionFile,
9049
9468
  });
9469
+ } else {
9050
9470
  }
9051
9471
  }
9052
9472
 
@@ -9126,13 +9546,18 @@ export class AgentSession {
9126
9546
  // Flush current session to ensure all entries are written
9127
9547
  await this.sessionManager.flush();
9128
9548
 
9129
- // Fork the session (creates new session file with same entries)
9130
- const forkResult = await this.sessionManager.fork();
9131
- if (!forkResult) {
9549
+ // Prepare the copied successor and complete local:// readiness while all
9550
+ // public manager getters remain bound to the predecessor.
9551
+ const prepared = await this.sessionManager.prepareFork();
9552
+ if (!prepared) {
9132
9553
  return false;
9133
9554
  }
9134
-
9135
- // Update agent session ID
9555
+ try {
9556
+ await initializeLocalRoot(this.#localProtocolOptions(prepared));
9557
+ this.sessionManager.commitPreparedNewSession(prepared);
9558
+ } catch (error) {
9559
+ throw await discardPreparedNewSessionAfterFailure(this.sessionManager, prepared, error);
9560
+ }
9136
9561
  this.#syncAgentSessionId();
9137
9562
  this.#bindWorkflowGateEmitter(previousWorkflowGateSessionId);
9138
9563
  this.#rekeyHindsightMemoryForCurrentSessionId();
@@ -9221,6 +9646,17 @@ export class AgentSession {
9221
9646
  return this.#activeModelProfile;
9222
9647
  }
9223
9648
 
9649
+ /** Activate a complete model profile through a nonvisual session control. */
9650
+ async activateModelProfileForControl(profileName: string): Promise<boolean> {
9651
+ await activateModelProfile({
9652
+ session: this,
9653
+ modelRegistry: this.#modelRegistry,
9654
+ settings: this.settings,
9655
+ profileName,
9656
+ });
9657
+ return this.getActiveModelProfile() === profileName;
9658
+ }
9659
+
9224
9660
  /** Return the persisted configured fallback selectors for a model role. */
9225
9661
  getConfiguredModelChain(role: string): readonly string[] | undefined {
9226
9662
  return getSessionContextForInternalRead(this.sessionManager).configuredModelChains[role]?.entries;
@@ -9348,7 +9784,7 @@ export class AgentSession {
9348
9784
  const ownsScope = scope !== undefined && !suppliedScope;
9349
9785
  try {
9350
9786
  if (isTemporaryOperation) {
9351
- this.agent.setModel(model);
9787
+ this.#setAgentModelWithReasoningContext(model);
9352
9788
  this.#syncAppendOnlyContext(model);
9353
9789
  } else {
9354
9790
  this.#setModelAuthoritatively(model, options?.cause ?? "temporary-operation");
@@ -9433,6 +9869,12 @@ export class AgentSession {
9433
9869
  this.#clearActiveRetryFallback();
9434
9870
  this.#setModelWithProviderSessionReset(model);
9435
9871
  const thinkingLevelChanged = this.#thinkingLevel !== thinkingLevel;
9872
+ this.#thinkingLevelMutationRevision++;
9873
+ this.#thinkingLevelLiveMutationRevision++;
9874
+ this.#pendingThinkingLevelControlSuccess = undefined;
9875
+ this.#pendingThinkingLevelControlFailure = undefined;
9876
+ this.#pendingThinkingVisibilityControlSuccess = undefined;
9877
+ this.#pendingThinkingVisibilityControlFailure = undefined;
9436
9878
  this.#thinkingLevel = thinkingLevel;
9437
9879
  this.agent.setThinkingLevel(toReasoningEffort(thinkingLevel));
9438
9880
  if (thinkingLevelChanged) {
@@ -9741,7 +10183,19 @@ export class AgentSession {
9741
10183
  * Set thinking level.
9742
10184
  * Saves the effective metadata-clamped level to the session, and to settings when requested.
9743
10185
  */
10186
+ #assertDurableSettingsWritable(): void {
10187
+ if (this.settings.canWriteDurableConfig()) return;
10188
+ throw new Error(
10189
+ "Cannot change settings while config.yml has invalid YAML syntax. Repair config.yml and reload settings.",
10190
+ );
10191
+ }
10192
+
9744
10193
  setThinkingLevel(level: ThinkingLevel | undefined, persist: boolean = false): void {
10194
+ if (persist) this.#assertDurableSettingsWritable();
10195
+ this.#thinkingLevelMutationRevision++;
10196
+ this.#thinkingLevelLiveMutationRevision++;
10197
+ this.#pendingThinkingLevelControlSuccess = undefined;
10198
+ this.#pendingThinkingLevelControlFailure = undefined;
9745
10199
  const effectiveLevel = resolveThinkingLevelForModel(this.model, level);
9746
10200
  const isChanging = effectiveLevel !== this.#thinkingLevel;
9747
10201
 
@@ -9761,11 +10215,10 @@ export class AgentSession {
9761
10215
  }
9762
10216
 
9763
10217
  /**
9764
- * Set thinking level from a control surface. Global changes are durable or rolled back.
10218
+ * Set thinking level from a control surface. Global changes commit before affecting live state.
9765
10219
  */
9766
10220
  async setThinkingLevelForControl(level: ThinkingLevel, persist: boolean): Promise<void> {
9767
10221
  const previousThinkingLevel = this.thinkingLevel;
9768
- const previousScope = this.getThinkingScopeForControl();
9769
10222
  if (!persist) {
9770
10223
  this.setThinkingLevel(level === ThinkingLevel.Inherit ? this.#getInheritedThinkingLevel() : level);
9771
10224
  if (level === ThinkingLevel.Inherit || this.thinkingLevel === previousThinkingLevel) {
@@ -9774,26 +10227,99 @@ export class AgentSession {
9774
10227
  return;
9775
10228
  }
9776
10229
 
9777
- const previousDefaultThinkingLevel = this.settings.getGlobal("defaultThinkingLevel");
9778
- if (level === ThinkingLevel.Inherit) {
9779
- const defaultLevel = getDefault("defaultThinkingLevel");
9780
- this.settings.set("defaultThinkingLevel", defaultLevel);
9781
- this.setThinkingLevel(this.#getInheritedThinkingLevel());
9782
- } else {
9783
- this.setThinkingLevel(level, true);
9784
- }
10230
+ this.#assertDurableSettingsWritable();
10231
+ const effectiveLevel = resolveThinkingLevelForModel(
10232
+ this.model,
10233
+ level === ThinkingLevel.Inherit ? this.#getInheritedThinkingLevel() : level,
10234
+ );
10235
+ const persistedLevel = level === ThinkingLevel.Inherit ? getDefault("defaultThinkingLevel") : effectiveLevel;
10236
+ const mutationRevision = ++this.#thinkingLevelMutationRevision;
10237
+ const expectedLiveMutationRevision = this.#thinkingLevelLiveMutationRevision;
10238
+ const expectedSessionId = this.sessionManager.getSessionId();
10239
+ const expectedModel = this.model;
10240
+ const expectedContextGeneration = this.#reasoningControlContextGeneration;
9785
10241
  try {
9786
- await this.settings.flushOrThrow();
9787
- this.sessionManager.appendThinkingLevelChange(ThinkingLevel.Inherit);
10242
+ await this.settings.commitAtomicBatch([{ path: "defaultThinkingLevel", op: "set", value: persistedLevel }]);
9788
10243
  } catch {
9789
- this.settings.set("defaultThinkingLevel", previousDefaultThinkingLevel ?? getDefault("defaultThinkingLevel"));
9790
- this.setThinkingLevel(previousThinkingLevel);
9791
- this.sessionManager.appendThinkingLevelChange(
9792
- previousScope === "global config" ? ThinkingLevel.Inherit : previousThinkingLevel,
9793
- );
9794
- await this.settings.flushOrThrow().catch(() => {});
10244
+ if (
10245
+ mutationRevision === this.#thinkingLevelMutationRevision &&
10246
+ this.#reasoningControlContextGeneration === expectedContextGeneration &&
10247
+ this.sessionManager.getSessionId() === expectedSessionId &&
10248
+ this.model === expectedModel
10249
+ ) {
10250
+ const pending = this.#pendingThinkingLevelControlSuccess;
10251
+ this.#pendingThinkingLevelControlSuccess = undefined;
10252
+ if (
10253
+ pending &&
10254
+ pending.contextGeneration === expectedContextGeneration &&
10255
+ this.sessionManager.getSessionId() === pending.sessionId &&
10256
+ this.model === pending.model
10257
+ ) {
10258
+ this.setThinkingLevel(
10259
+ pending.level === ThinkingLevel.Inherit ? this.#getInheritedThinkingLevel() : pending.level,
10260
+ );
10261
+ this.sessionManager.appendThinkingLevelChange(ThinkingLevel.Inherit);
10262
+ } else {
10263
+ this.#pendingThinkingLevelControlFailure = {
10264
+ mutationRevision,
10265
+ liveMutationRevision: expectedLiveMutationRevision,
10266
+ sessionId: expectedSessionId,
10267
+ model: expectedModel,
10268
+ contextGeneration: expectedContextGeneration,
10269
+ };
10270
+ }
10271
+ }
9795
10272
  throw new Error("Unable to persist reasoning settings.");
9796
10273
  }
10274
+
10275
+ if (
10276
+ mutationRevision === this.#thinkingLevelMutationRevision &&
10277
+ this.#thinkingLevelLiveMutationRevision === expectedLiveMutationRevision
10278
+ ) {
10279
+ this.setThinkingLevel(level === ThinkingLevel.Inherit ? this.#getInheritedThinkingLevel() : level);
10280
+ this.sessionManager.appendThinkingLevelChange(ThinkingLevel.Inherit);
10281
+ return;
10282
+ }
10283
+ if (
10284
+ mutationRevision !== this.#thinkingLevelMutationRevision ||
10285
+ this.#reasoningControlContextGeneration !== expectedContextGeneration ||
10286
+ this.sessionManager.getSessionId() !== expectedSessionId ||
10287
+ this.model !== expectedModel
10288
+ ) {
10289
+ if (
10290
+ this.#thinkingLevelLiveMutationRevision === expectedLiveMutationRevision &&
10291
+ this.#reasoningControlContextGeneration === expectedContextGeneration &&
10292
+ this.sessionManager.getSessionId() === expectedSessionId &&
10293
+ this.model === expectedModel &&
10294
+ (this.#pendingThinkingLevelControlSuccess?.mutationRevision ?? -1) < mutationRevision
10295
+ ) {
10296
+ this.#pendingThinkingLevelControlSuccess = {
10297
+ level,
10298
+ mutationRevision,
10299
+ sessionId: expectedSessionId,
10300
+ model: expectedModel,
10301
+ contextGeneration: expectedContextGeneration,
10302
+ };
10303
+ const failure = this.#pendingThinkingLevelControlFailure;
10304
+ if (
10305
+ failure &&
10306
+ failure.mutationRevision === this.#thinkingLevelMutationRevision &&
10307
+ failure.liveMutationRevision === expectedLiveMutationRevision &&
10308
+ failure.sessionId === expectedSessionId &&
10309
+ failure.model === expectedModel &&
10310
+ failure.contextGeneration === expectedContextGeneration
10311
+ ) {
10312
+ this.#pendingThinkingLevelControlFailure = undefined;
10313
+ this.setThinkingLevel(
10314
+ level === ThinkingLevel.Inherit ? this.#getInheritedThinkingLevel() : effectiveLevel,
10315
+ );
10316
+ this.sessionManager.appendThinkingLevelChange(ThinkingLevel.Inherit);
10317
+ }
10318
+ }
10319
+ return;
10320
+ }
10321
+ this.setThinkingLevel(level === ThinkingLevel.Inherit ? this.#getInheritedThinkingLevel() : effectiveLevel);
10322
+ this.sessionManager.appendThinkingLevelChange(ThinkingLevel.Inherit);
9797
10323
  }
9798
10324
 
9799
10325
  getThinkingScopeForControl(): "session" | "global config" {
@@ -9809,6 +10335,11 @@ export class AgentSession {
9809
10335
  }
9810
10336
 
9811
10337
  setThinkingVisibility(visibility: "visible" | "hidden", persist: boolean = false): void {
10338
+ if (persist) this.#assertDurableSettingsWritable();
10339
+ this.#thinkingVisibilityMutationRevision++;
10340
+ this.#thinkingVisibilityLiveMutationRevision++;
10341
+ this.#pendingThinkingVisibilityControlSuccess = undefined;
10342
+ this.#pendingThinkingVisibilityControlFailure = undefined;
9812
10343
  this.agent.hideThinkingSummary = visibility === "hidden";
9813
10344
  if (persist) {
9814
10345
  this.settings.set("hideThinkingBlock", visibility === "hidden");
@@ -9816,7 +10347,7 @@ export class AgentSession {
9816
10347
  }
9817
10348
 
9818
10349
  /**
9819
- * Set thinking visibility from a control surface. Global changes are durable or rolled back.
10350
+ * Set thinking visibility from a control surface. Global changes commit before affecting live state.
9820
10351
  */
9821
10352
  async setThinkingVisibilityForControl(visibility: "visible" | "hidden", persist: boolean): Promise<void> {
9822
10353
  if (!persist) {
@@ -9824,17 +10355,88 @@ export class AgentSession {
9824
10355
  return;
9825
10356
  }
9826
10357
 
9827
- const previousVisibility = this.getThinkingVisibility();
9828
- const previousHideThinkingBlock = this.settings.getGlobal("hideThinkingBlock");
9829
- this.setThinkingVisibility(visibility, true);
10358
+ this.#assertDurableSettingsWritable();
10359
+ const mutationRevision = ++this.#thinkingVisibilityMutationRevision;
10360
+ const expectedLiveMutationRevision = this.#thinkingVisibilityLiveMutationRevision;
10361
+ const expectedSessionId = this.sessionManager.getSessionId();
10362
+ const expectedModel = this.model;
10363
+ const expectedContextGeneration = this.#reasoningControlContextGeneration;
9830
10364
  try {
9831
- await this.settings.flushOrThrow();
10365
+ await this.settings.commitAtomicBatch([
10366
+ { path: "hideThinkingBlock", op: "set", value: visibility === "hidden" },
10367
+ ]);
9832
10368
  } catch {
9833
- this.settings.set("hideThinkingBlock", previousHideThinkingBlock ?? getDefault("hideThinkingBlock"));
9834
- this.setThinkingVisibility(previousVisibility);
9835
- await this.settings.flushOrThrow().catch(() => {});
10369
+ if (
10370
+ mutationRevision === this.#thinkingVisibilityMutationRevision &&
10371
+ this.#reasoningControlContextGeneration === expectedContextGeneration &&
10372
+ this.sessionManager.getSessionId() === expectedSessionId &&
10373
+ this.model === expectedModel
10374
+ ) {
10375
+ const pending = this.#pendingThinkingVisibilityControlSuccess;
10376
+ this.#pendingThinkingVisibilityControlSuccess = undefined;
10377
+ if (
10378
+ pending &&
10379
+ pending.contextGeneration === expectedContextGeneration &&
10380
+ this.sessionManager.getSessionId() === pending.sessionId &&
10381
+ this.model === pending.model
10382
+ ) {
10383
+ this.setThinkingVisibility(pending.visibility);
10384
+ } else {
10385
+ this.#pendingThinkingVisibilityControlFailure = {
10386
+ mutationRevision,
10387
+ liveMutationRevision: expectedLiveMutationRevision,
10388
+ sessionId: expectedSessionId,
10389
+ model: expectedModel,
10390
+ contextGeneration: expectedContextGeneration,
10391
+ };
10392
+ }
10393
+ }
9836
10394
  throw new Error("Unable to persist reasoning settings.");
9837
10395
  }
10396
+ if (
10397
+ mutationRevision === this.#thinkingVisibilityMutationRevision &&
10398
+ this.#thinkingVisibilityLiveMutationRevision === expectedLiveMutationRevision
10399
+ ) {
10400
+ this.setThinkingVisibility(visibility);
10401
+ return;
10402
+ }
10403
+
10404
+ if (
10405
+ mutationRevision !== this.#thinkingVisibilityMutationRevision ||
10406
+ this.#reasoningControlContextGeneration !== expectedContextGeneration ||
10407
+ this.sessionManager.getSessionId() !== expectedSessionId ||
10408
+ this.model !== expectedModel
10409
+ ) {
10410
+ if (
10411
+ this.#thinkingVisibilityLiveMutationRevision === expectedLiveMutationRevision &&
10412
+ this.#reasoningControlContextGeneration === expectedContextGeneration &&
10413
+ this.sessionManager.getSessionId() === expectedSessionId &&
10414
+ this.model === expectedModel &&
10415
+ (this.#pendingThinkingVisibilityControlSuccess?.mutationRevision ?? -1) < mutationRevision
10416
+ ) {
10417
+ this.#pendingThinkingVisibilityControlSuccess = {
10418
+ visibility,
10419
+ mutationRevision,
10420
+ sessionId: expectedSessionId,
10421
+ model: expectedModel,
10422
+ contextGeneration: expectedContextGeneration,
10423
+ };
10424
+ const failure = this.#pendingThinkingVisibilityControlFailure;
10425
+ if (
10426
+ failure &&
10427
+ failure.mutationRevision === this.#thinkingVisibilityMutationRevision &&
10428
+ failure.liveMutationRevision === expectedLiveMutationRevision &&
10429
+ failure.sessionId === expectedSessionId &&
10430
+ failure.model === expectedModel &&
10431
+ failure.contextGeneration === expectedContextGeneration
10432
+ ) {
10433
+ this.#pendingThinkingVisibilityControlFailure = undefined;
10434
+ this.setThinkingVisibility(visibility);
10435
+ }
10436
+ }
10437
+ return;
10438
+ }
10439
+ this.setThinkingVisibility(visibility);
9838
10440
  }
9839
10441
 
9840
10442
  /**
@@ -10011,6 +10613,7 @@ export class AgentSession {
10011
10613
  * Saves to settings.
10012
10614
  */
10013
10615
  setSteeringMode(mode: "all" | "one-at-a-time"): void {
10616
+ this.#assertDurableSettingsWritable();
10014
10617
  this.agent.setSteeringMode(mode);
10015
10618
  this.settings.set("steeringMode", mode);
10016
10619
  }
@@ -10020,6 +10623,7 @@ export class AgentSession {
10020
10623
  * Saves to settings.
10021
10624
  */
10022
10625
  setFollowUpMode(mode: "all" | "one-at-a-time"): void {
10626
+ this.#assertDurableSettingsWritable();
10023
10627
  this.agent.setFollowUpMode(mode);
10024
10628
  this.settings.set("followUpMode", mode);
10025
10629
  }
@@ -10029,6 +10633,7 @@ export class AgentSession {
10029
10633
  * Saves to settings.
10030
10634
  */
10031
10635
  setInterruptMode(mode: "immediate" | "wait"): void {
10636
+ this.#assertDurableSettingsWritable();
10032
10637
  this.agent.setInterruptMode(mode);
10033
10638
  this.settings.set("interruptMode", mode);
10034
10639
  }
@@ -10523,22 +11128,36 @@ export class AgentSession {
10523
11128
  // user work that agent.reset() would otherwise clear.
10524
11129
  const rollbackAgentSteeringQueue = this.agent.snapshotSteering();
10525
11130
  const rollbackAgentFollowUpQueue = this.agent.snapshotFollowUp();
10526
- let successorSessionFile: string | undefined;
10527
11131
  let savedPath: string | undefined;
10528
11132
  let committed = false;
10529
- // Suspend (do not destroy) the workflow-gate emitter so it can be fenced on
10530
- // commit or resumed on rollback. Suspension runs inside the transaction so a
10531
- // listener fault during suspension is rolled back like any other prepare step.
10532
- let suspendedWorkflowGateEmitter: WorkflowGateEmitter | undefined;
11133
+ let prepared: PreparedNewSession | undefined;
10533
11134
  try {
10534
- suspendedWorkflowGateEmitter = this.#suspendWorkflowGateEmitter(rollbackSessionState.sessionId);
10535
- // --- Prepare (reversible): build and persist the successor session
10536
- // without touching predecessor authority. No irreversible predecessor
10537
- // teardown or identity publication happens until the commit boundary.
10538
- await this.sessionManager.newSession(
11135
+ // Prepare successor entries, persistence, display state, and gate
11136
+ // construction without publishing manager identity. Managed local-root
11137
+ // readiness is the last fallible step before the sole commit (#3138).
11138
+ prepared = await this.sessionManager.prepareNewSession(
10539
11139
  previousSessionFile ? { parentSession: previousSessionFile } : undefined,
10540
11140
  );
10541
- successorSessionFile = this.sessionFile;
11141
+ if (model) this.sessionManager.appendPreparedModelChange(prepared, `${model.provider}/${model.id}`);
11142
+ this.sessionManager.appendPreparedThinkingLevelChange(prepared, this.thinkingLevel);
11143
+ this.sessionManager.appendPreparedServiceTierChange(prepared, this.serviceTier ?? null);
11144
+ this.sessionManager.appendPreparedCustomMessageEntry(
11145
+ prepared,
11146
+ "handoff",
11147
+ createHandoffContext(handoffText),
11148
+ true,
11149
+ undefined,
11150
+ "agent",
11151
+ );
11152
+ await this.sessionManager.ensurePreparedNewSessionOnDisk(prepared);
11153
+ const sessionContext = this.buildPreparedDisplaySessionContext(prepared);
11154
+ const successorGateEmitter = this.#constructWorkflowGateEmitter(prepared.sessionId);
11155
+ // Last fallible action: verified local:// readiness from immutable staged options.
11156
+ await initializeLocalRoot(this.#localProtocolOptions(prepared));
11157
+
11158
+ // --- Commit boundary: synchronous adoption is the sole identity publication.
11159
+ this.sessionManager.commitPreparedNewSession(prepared);
11160
+ committed = true;
10542
11161
  this.agent.reset();
10543
11162
  this.#syncAgentSessionId();
10544
11163
  this.#rekeyHindsightMemoryForCurrentSessionId();
@@ -10547,16 +11166,8 @@ export class AgentSession {
10547
11166
  this.#pendingNextTurnMessages = [];
10548
11167
  this.#scheduledHiddenNextTurnGeneration = undefined;
10549
11168
  this.#todoReminderCount = 0;
10550
- if (model) {
10551
- this.sessionManager.appendModelChange(`${model.provider}/${model.id}`);
10552
- }
10553
- this.sessionManager.appendThinkingLevelChange(this.thinkingLevel);
10554
- this.sessionManager.appendServiceTierChange(this.serviceTier ?? null);
10555
-
10556
- // Inject the handoff document as a custom message
10557
- const handoffContent = createHandoffContext(handoffText);
10558
- this.sessionManager.appendCustomMessageEntry("handoff", handoffContent, true, undefined, "agent");
10559
- await this.sessionManager.ensureOnDisk();
11169
+ this.agent.replaceMessages(sessionContext.messages);
11170
+ this.#syncTodoPhasesFromBranch();
10560
11171
  if (options?.autoTriggered && this.settings.get("compaction.handoffSaveToDisk")) {
10561
11172
  try {
10562
11173
  const artifactId = await this.sessionManager.saveArtifact(`${handoffText}\n`, "handoff");
@@ -10568,30 +11179,9 @@ export class AgentSession {
10568
11179
  }
10569
11180
  }
10570
11181
 
10571
- // Rebuild agent messages from session
10572
- const sessionContext = this.buildDisplaySessionContext();
10573
- this.agent.replaceMessages(sessionContext.messages);
10574
- this.#syncTodoPhasesFromBranch();
10575
-
10576
- // Construct the successor workflow-gate emitter in the reversible window:
10577
- // FileGateStore load / broker init can throw, and a failure here must roll
10578
- // back rather than corrupt an already-committed switch. Publication is
10579
- // deferred to the (no-throw) commit step below.
10580
- const successorGateEmitter = this.#constructWorkflowGateEmitter();
10581
-
10582
- // --- Commit boundary: the successor is durably persisted and every
10583
- // reversible, fallible step above has succeeded. Cross the irreversible
10584
- // commit BEFORE any identity rotation. Fencing the predecessor gate
10585
- // emitter and rotating provider sessions cannot be undone, so once we
10586
- // begin them a failure must RETAIN the committed successor rather than
10587
- // attempt a (now-impossible) rollback that would resume a fenced emitter.
10588
- committed = true;
11182
+ // The forward-only suffix starts only after the committed successor is live.
10589
11183
  this.#resetInjectedContextSignatures();
10590
- this.#publishWorkflowGateEmitter(
10591
- successorGateEmitter,
10592
- rollbackSessionState.sessionId,
10593
- suspendedWorkflowGateEmitter,
10594
- );
11184
+ this.#publishWorkflowGateEmitter(successorGateEmitter, rollbackSessionState.sessionId);
10595
11185
  this.#closeAllProviderSessions("session handoff");
10596
11186
  this.#rebindProviderSessionState(new Map());
10597
11187
  this.#resetHindsightConversationTrackingIfHindsight();
@@ -10627,12 +11217,12 @@ export class AgentSession {
10627
11217
  return { document: handoffText, savedPath };
10628
11218
  } catch (switchError) {
10629
11219
  if (committed) {
10630
- // The switch already committed; a post-commit step failed. Do not
10631
- // tear down the new session — surface success (the handoff exists).
10632
- logger.warn("Handoff post-commit step failed after the session switch committed", {
10633
- error: switchError instanceof Error ? switchError.message : String(switchError),
11220
+ throw Object.assign(new Error("Handoff committed but successor initialization was degraded."), {
11221
+ code: "handoff_committed_degraded",
11222
+ cause: switchError,
11223
+ handoffDocument: handoffText,
11224
+ savedPath,
10634
11225
  });
10635
- return { document: handoffText, savedPath };
10636
11226
  }
10637
11227
  // Reversible window: roll back to the pre-handoff session so the
10638
11228
  // failure is non-destructive. Predecessor gate emitter, provider
@@ -10640,7 +11230,6 @@ export class AgentSession {
10640
11230
  // were never mutated before commit, so they survive intact.
10641
11231
  this.sessionManager.restoreState(rollbackSessionState);
10642
11232
  this.#syncAgentSessionId(rollbackSessionState.sessionId);
10643
- this.#restoreWorkflowGateEmitter(suspendedWorkflowGateEmitter);
10644
11233
  this.#rekeyHindsightMemoryForCurrentSessionId();
10645
11234
  this.agent.replaceMessages(rollbackAgentMessages);
10646
11235
  this.agent.clearAllQueues();
@@ -10652,25 +11241,17 @@ export class AgentSession {
10652
11241
  this.#scheduledHiddenNextTurnGeneration = rollbackScheduledHiddenNextTurnGeneration;
10653
11242
  this.#todoReminderCount = rollbackTodoReminderCount;
10654
11243
  this.#syncTodoPhasesFromBranch();
10655
- // Remove the orphaned successor transcript/artifacts written before the
10656
- // failure so a rolled-back handoff leaves nothing behind.
10657
- if (successorSessionFile && successorSessionFile !== previousSessionFile) {
10658
- try {
10659
- await this.sessionManager.discardUncommittedSession(successorSessionFile);
10660
- } catch (cleanupError) {
10661
- logger.warn("Failed to remove orphaned handoff session after rollback", {
10662
- path: successorSessionFile,
10663
- error: cleanupError instanceof Error ? cleanupError.message : String(cleanupError),
10664
- });
10665
- }
10666
- }
11244
+ // Exact-discard only the staged successor; predecessor state was never adopted.
11245
+ const rollbackError = prepared
11246
+ ? await discardPreparedNewSessionAfterFailure(this.sessionManager, prepared, switchError)
11247
+ : switchError;
10667
11248
  // Map to cancellation only for a genuine handoff-signal abort; a
10668
11249
  // downstream error keeps its cause and the generated document.
10669
11250
  if (handoffSignal.aborted) {
10670
11251
  throw new Error("Handoff cancelled");
10671
11252
  }
10672
11253
  // Preserve the generated handoff document for copy/retry.
10673
- throw Object.assign(switchError instanceof Error ? switchError : new Error(String(switchError)), {
11254
+ throw Object.assign(rollbackError instanceof Error ? rollbackError : new Error(String(rollbackError)), {
10674
11255
  handoffDocument: handoffText,
10675
11256
  });
10676
11257
  }
@@ -10678,7 +11259,7 @@ export class AgentSession {
10678
11259
  // Genuine handoff-signal cancellation maps to a cancellation error.
10679
11260
  // Errors surfaced by the inner transaction (which already rolled back
10680
11261
  // and, for non-aborts, attached the generated document) pass through.
10681
- if (handoffSignal.aborted) {
11262
+ if (handoffSignal.aborted && !(error instanceof AggregateError)) {
10682
11263
  throw new Error("Handoff cancelled");
10683
11264
  }
10684
11265
  throw error;
@@ -10720,7 +11301,11 @@ export class AgentSession {
10720
11301
  * @param assistantMessage The assistant message to check
10721
11302
  * @param skipAbortedCheck If false, include aborted messages (for pre-prompt check). Default: true
10722
11303
  */
10723
- async #checkCompaction(assistantMessage: AssistantMessage, skipAbortedCheck = true): Promise<boolean> {
11304
+ async #checkCompaction(
11305
+ assistantMessage: AssistantMessage,
11306
+ skipAbortedCheck = true,
11307
+ onTerminalOverflowNoop?: () => void,
11308
+ ): Promise<boolean> {
10724
11309
  // Safety stops are terminal and must not trigger context maintenance.
10725
11310
  if (
10726
11311
  assistantMessage.errorKind === "provider_safety_stop" ||
@@ -10756,8 +11341,10 @@ export class AgentSession {
10756
11341
  // Remove the error message from agent state (it IS saved to session for history,
10757
11342
  // but we don't want it in context for the retry)
10758
11343
  const messages = this.agent.state.messages;
11344
+ let removedOverflowAssistant = false;
10759
11345
  if (messages.length > 0 && messages[messages.length - 1].role === "assistant") {
10760
11346
  this.agent.replaceMessages(messages.slice(0, -1));
11347
+ removedOverflowAssistant = true;
10761
11348
  }
10762
11349
 
10763
11350
  // Try context promotion first - switch to a larger model and retry without compacting
@@ -10772,7 +11359,15 @@ export class AgentSession {
10772
11359
  // No promotion target available fall through to compaction
10773
11360
  const compactionSettings = this.settings.getGroup("compaction");
10774
11361
  if (compactionSettings.enabled && compactionSettings.strategy !== "off") {
10775
- const status = await this.#runAutoCompaction("overflow", true);
11362
+ const status = await this.#runAutoCompaction("overflow", true, false, {
11363
+ beforeTerminalOverflowNoop: () => {
11364
+ if (onTerminalOverflowNoop) {
11365
+ onTerminalOverflowNoop();
11366
+ } else if (removedOverflowAssistant) {
11367
+ this.agent.appendMessage(assistantMessage);
11368
+ }
11369
+ },
11370
+ });
10776
11371
  return "continuationScheduled" in status && status.continuationScheduled === true;
10777
11372
  }
10778
11373
  return this.#scheduleOverflowRetryContinuation(generation);
@@ -11540,13 +12135,18 @@ export class AgentSession {
11540
12135
  this.#defaultFallbackExhaustedLastTurn = false;
11541
12136
  }
11542
12137
 
12138
+ #setAgentModelWithReasoningContext(model: Model | undefined): void {
12139
+ this.#reasoningControlContextGeneration++;
12140
+ this.agent.setModel(model);
12141
+ }
12142
+
11543
12143
  #setModelWithProviderSessionReset(model: Model | undefined): void {
11544
12144
  this.#defaultModelSelectionMutationRevision++;
11545
12145
  const currentModel = this.model;
11546
12146
  if (currentModel) {
11547
12147
  this.#closeProviderSessionsForModelSwitch(currentModel, model);
11548
12148
  }
11549
- this.agent.setModel(model);
12149
+ this.#setAgentModelWithReasoningContext(model);
11550
12150
  }
11551
12151
 
11552
12152
  #setModelAuthoritatively(model: Model, cause: ModelChangeCause): void {
@@ -11555,7 +12155,7 @@ export class AgentSession {
11555
12155
  if (cause !== "temporary-operation") this.#commitAllTemporaryProviderSessionScopes();
11556
12156
  const currentModel = this.model;
11557
12157
  if (currentModel) this.#closeProviderSessionsForModelSwitch(currentModel, model);
11558
- this.agent.setModel(model);
12158
+ this.#setAgentModelWithReasoningContext(model);
11559
12159
  this.#syncAppendOnlyContext(model);
11560
12160
  }
11561
12161
 
@@ -12081,6 +12681,7 @@ export class AgentSession {
12081
12681
  deferHandoffMaintenance?: boolean;
12082
12682
  force?: boolean;
12083
12683
  signal?: AbortSignal;
12684
+ beforeTerminalOverflowNoop?: () => void;
12084
12685
  },
12085
12686
  ): Promise<AutoCompactionTerminalStatus> {
12086
12687
  const compactionSettings = this.settings.getGroup("compaction");
@@ -12218,16 +12819,42 @@ export class AgentSession {
12218
12819
  if (autoCompactionSignal.aborted) return await emitAborted();
12219
12820
 
12220
12821
  if (!preparation) {
12221
- const continuationSkipReason = willRetry ? this.#detectOverflowRetryContinuationSkip() : undefined;
12822
+ // #checkCompaction removes the failed overflow assistant before entering
12823
+ // this path. A resumable tail would therefore resend the same oversized
12824
+ // request even though maintenance made no change. Non-resumable tails
12825
+ // retain the existing synthetic auto-continue recovery, which changes
12826
+ // the request and is covered by the continuation contract.
12827
+ const overflowNoopWouldReplay = reason === "overflow" && willRetry && this.#isResumableAgentTail();
12828
+ const continuationSkipReason =
12829
+ !overflowNoopWouldReplay && willRetry ? this.#detectOverflowRetryContinuationSkip() : undefined;
12830
+ if (overflowNoopWouldReplay) {
12831
+ options?.beforeTerminalOverflowNoop?.();
12832
+ }
12222
12833
  await this.#emitSessionEvent({
12223
12834
  type: "auto_compaction_end",
12224
12835
  action,
12225
12836
  result: undefined,
12226
12837
  aborted: false,
12227
- willRetry: willRetry && !continuationSkipReason,
12838
+ willRetry: overflowNoopWouldReplay ? false : willRetry && !continuationSkipReason,
12839
+ errorMessage: overflowNoopWouldReplay
12840
+ ? "Context overflow recovery skipped: nothing eligible to compact. Run /clear to preserve this session ID, or switch to a larger-context model before retrying."
12841
+ : undefined,
12228
12842
  skipped: true,
12229
12843
  continuationSkipReason,
12230
12844
  });
12845
+ if (overflowNoopWouldReplay) {
12846
+ if (continueAfterMaintenance && this.agent.hasQueuedMessages()) {
12847
+ this.#scheduleAgentContinue({
12848
+ delayMs: 100,
12849
+ generation,
12850
+ shouldContinue: () => this.agent.hasQueuedMessages(),
12851
+ onSkip: skipReason => this.#logCompactionContinuationSkipped("queued_continue", skipReason),
12852
+ onError: error => this.#logCompactionContinuationError("queued_continue", error),
12853
+ });
12854
+ return { kind: "skipped", continuationScheduled: true };
12855
+ }
12856
+ return { kind: "skipped" };
12857
+ }
12231
12858
  if (willRetry) {
12232
12859
  return { kind: "skipped", continuationScheduled: this.#scheduleOverflowRetryContinuation(generation) };
12233
12860
  }
@@ -12357,6 +12984,7 @@ export class AgentSession {
12357
12984
  }
12358
12985
  const retryAfterMs = this.#parseRetryAfterMsFromError(message);
12359
12986
  const shouldRetry =
12987
+ !this.#isTerminalProviderCompactionTimeout(candidate, message) &&
12360
12988
  retrySettings.enabled &&
12361
12989
  attempt < retrySettings.maxRetries &&
12362
12990
  (retryAfterMs !== undefined ||
@@ -12367,8 +12995,14 @@ export class AgentSession {
12367
12995
  break;
12368
12996
  }
12369
12997
 
12370
- const baseDelayMs = retrySettings.baseDelayMs * 2 ** attempt;
12371
- const delayMs = retryAfterMs !== undefined ? Math.max(baseDelayMs, retryAfterMs) : baseDelayMs;
12998
+ // Legacy parsed Retry-After is capped at retry.maxDelayMs (see
12999
+ // compactionRetryDelay); only managed fallback is uncapped.
13000
+ const delayMs = compactionRetryDelay(
13001
+ retrySettings.baseDelayMs,
13002
+ retrySettings.maxDelayMs,
13003
+ attempt,
13004
+ retryAfterMs,
13005
+ );
12372
13006
 
12373
13007
  // If retry delay is too long (>30s), try next candidate instead of waiting
12374
13008
  const maxAcceptableDelayMs = 30_000;
@@ -12542,7 +13176,66 @@ export class AgentSession {
12542
13176
  * unknown/no-code errors are retryable.
12543
13177
  */
12544
13178
 
13179
+ #isKimiCodeFirstEventTimeout(message: AssistantMessage): boolean {
13180
+ if (message.stopReason !== "error" || message.provider !== "kimi-code") return false;
13181
+ const errorMessage = message.errorMessage ?? "";
13182
+ if (message.api === "anthropic-messages") {
13183
+ return KIMI_CODE_FIRST_EVENT_TIMEOUT_MESSAGES["anthropic-messages"].has(errorMessage);
13184
+ }
13185
+ if (message.api === "openai-completions") {
13186
+ return KIMI_CODE_FIRST_EVENT_TIMEOUT_MESSAGES["openai-completions"].has(errorMessage);
13187
+ }
13188
+ return false;
13189
+ }
13190
+
13191
+ #isKimiCodeCompactionTimeout(candidate: Model, errorMessage: string): boolean {
13192
+ if (candidate.provider !== "kimi-code") return false;
13193
+ return /^(?:Summarization failed|Turn prefix summarization failed): (?:Provider|Anthropic|OpenAI completions) stream timed out while waiting for the first event$/.test(
13194
+ errorMessage,
13195
+ );
13196
+ }
13197
+
13198
+ #alibabaTokenPlanTimeoutMessagesForApi(api: string): ReadonlySet<string> | undefined {
13199
+ if (api === "openai-responses" || api === "openai-completions") {
13200
+ return ALIBABA_TOKEN_PLAN_FIRST_EVENT_TIMEOUT_MESSAGES[api];
13201
+ }
13202
+ return undefined;
13203
+ }
13204
+
13205
+ #isAlibabaTokenPlanFirstEventTimeout(message: AssistantMessage): boolean {
13206
+ if (message.stopReason !== "error" || message.provider !== ALIBABA_TOKEN_PLAN_PROVIDER) return false;
13207
+ const timeoutMessages = this.#alibabaTokenPlanTimeoutMessagesForApi(message.api);
13208
+ return timeoutMessages?.has(message.errorMessage ?? "") ?? false;
13209
+ }
13210
+
13211
+ #isAlibabaTokenPlanCompactionTimeout(candidate: Model, errorMessage: string): boolean {
13212
+ if (candidate.provider !== ALIBABA_TOKEN_PLAN_PROVIDER) return false;
13213
+ const timeoutMessages = this.#alibabaTokenPlanTimeoutMessagesForApi(candidate.api);
13214
+ if (!timeoutMessages) return false;
13215
+ for (const timeoutMessage of timeoutMessages) {
13216
+ if (
13217
+ errorMessage === `Summarization failed: ${timeoutMessage}` ||
13218
+ errorMessage === `Turn prefix summarization failed: ${timeoutMessage}`
13219
+ ) {
13220
+ return true;
13221
+ }
13222
+ }
13223
+ return false;
13224
+ }
13225
+
13226
+ #isTerminalProviderFirstEventTimeout(message: AssistantMessage): boolean {
13227
+ return this.#isKimiCodeFirstEventTimeout(message) || this.#isAlibabaTokenPlanFirstEventTimeout(message);
13228
+ }
13229
+
13230
+ #isTerminalProviderCompactionTimeout(candidate: Model, errorMessage: string): boolean {
13231
+ return (
13232
+ this.#isKimiCodeCompactionTimeout(candidate, errorMessage) ||
13233
+ this.#isAlibabaTokenPlanCompactionTimeout(candidate, errorMessage)
13234
+ );
13235
+ }
13236
+
12545
13237
  #isRetryableError(message: AssistantMessage): boolean {
13238
+ if (this.#isTerminalProviderFirstEventTimeout(message)) return false;
12546
13239
  if (message.errorMessage?.startsWith("Model fallback chain exhausted;")) return false;
12547
13240
  const transportFailure = message.transportFailure;
12548
13241
  const contextWindow = this.model?.contextWindow ?? 0;
@@ -12692,6 +13385,9 @@ export class AgentSession {
12692
13385
  if (isTerminalHttp4xx && (explicitStatus !== undefined || !/rate.?limit|too many requests/i.test(err))) {
12693
13386
  return "terminal";
12694
13387
  }
13388
+ if (this.#isTerminalProviderFirstEventTimeout(message)) {
13389
+ return "terminal";
13390
+ }
12695
13391
  // A first-event timeout on ollama-cloud (the ollama-chat API) must not
12696
13392
  // join the unbounded transient class: each continuation retry re-issues
12697
13393
  // the full request to a remote, billable backend, so an unbounded loop
@@ -12866,8 +13562,15 @@ export class AgentSession {
12866
13562
  type: "maintenance",
12867
13563
  continuation: async ownership => {
12868
13564
  if (!ownership.isCurrent()) return;
12869
- const successorScheduled = await this.#checkCompaction(outcome.message);
12870
- if (successorScheduled || !ownership.isCurrent()) return;
13565
+ let terminalized = false;
13566
+ const successorScheduled = await this.#checkCompaction(outcome.message, true, () => {
13567
+ terminalized = true;
13568
+ this.agent.requestRunTerminal(ownership.logicalRunId, {
13569
+ stopReason: "error",
13570
+ messages: [outcome.message],
13571
+ });
13572
+ });
13573
+ if (terminalized || successorScheduled || !ownership.isCurrent()) return;
12871
13574
  this.agent.requestRunTerminal(ownership.logicalRunId, {
12872
13575
  stopReason: "error",
12873
13576
  messages: [outcome.message],
@@ -12875,6 +13578,18 @@ export class AgentSession {
12875
13578
  },
12876
13579
  };
12877
13580
  }
13581
+ if (this.#isTerminalProviderFirstEventTimeout(outcome.failure.message)) {
13582
+ // The managed transport discarded this attempt before session policy saw it.
13583
+ // Remove its provisional charge without changing sticky fallback selection.
13584
+ this.#defaultFallbackChain().discardStartedAttempt();
13585
+ return {
13586
+ type: "terminal",
13587
+ terminal: {
13588
+ stopReason: "error",
13589
+ messages: [outcome.failure.message],
13590
+ },
13591
+ };
13592
+ }
12878
13593
  return this.#handleRetryableError(
12879
13594
  outcome.failure.message,
12880
13595
  true,
@@ -13994,167 +14709,127 @@ export class AgentSession {
13994
14709
  this.#ircRosterClaim = null;
13995
14710
  }
13996
14711
 
14712
+ createBtwConversationScope(instruction: string): BtwConversationScope {
14713
+ const model = this.model;
14714
+ if (!model) throw new Error("No active model on session");
14715
+ const providerAffinitySessionId = this.agent.providerSessionId ?? this.agent.sessionId ?? this.sessionId;
14716
+ return {
14717
+ model,
14718
+ systemPrompt: [...this.systemPrompt, instruction],
14719
+ messages: this.#projectBtwVisibleText(this.buildDisplaySessionContext().messages),
14720
+ thinkingLevel: this.thinkingLevel ?? ThinkingLevel.Off,
14721
+ hideThinkingSummary: this.agent.hideThinkingSummary ?? false,
14722
+ serviceTier: this.serviceTier,
14723
+ credentialSessionId: providerAffinitySessionId,
14724
+ providerAffinitySessionId,
14725
+ sideSessionId: `${providerAffinitySessionId}:btw:${crypto.randomUUID()}`,
14726
+ };
14727
+ }
14728
+
14729
+ #projectBtwVisibleText(messages: readonly AgentMessage[]): BtwRoleTextMessage[] {
14730
+ const projected: BtwRoleTextMessage[] = [];
14731
+ for (const message of messages) {
14732
+ if (message.role !== "user" && message.role !== "assistant") continue;
14733
+ const text = (
14734
+ typeof message.content === "string"
14735
+ ? message.content
14736
+ : message.content
14737
+ .filter((block): block is TextContent => block.type === "text")
14738
+ .map(block => block.text)
14739
+ .join("")
14740
+ ).trim();
14741
+ if (!text) continue;
14742
+ projected.push({ role: message.role, text });
14743
+ }
14744
+ return projected;
14745
+ }
14746
+
14747
+ #buildBtwAssistantMessage(text: string): AssistantMessage {
14748
+ return {
14749
+ role: "assistant",
14750
+ content: [{ type: "text", text }],
14751
+ api: "btw",
14752
+ provider: "btw",
14753
+ model: "btw",
14754
+ usage: {
14755
+ input: 0,
14756
+ output: 0,
14757
+ cacheRead: 0,
14758
+ cacheWrite: 0,
14759
+ totalTokens: 0,
14760
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
14761
+ },
14762
+ stopReason: "stop",
14763
+ timestamp: 0,
14764
+ };
14765
+ }
13997
14766
  /**
13998
- * Run a single ephemeral side-channel turn against this session's current
13999
- * model + system prompt + history. No tools are used and session history is
14000
- * not modified. Background turns retain the IRC/session behavior used by
14001
- * autoreplies; `/btw` turns use an isolated committed-context policy.
14767
+ * Run a single ephemeral side-channel turn without modifying session history.
14768
+ * Background turns retain IRC/session behavior. `/btw` turns require a
14769
+ * pre-frozen, visible-text-only scope and bypass extension context transforms,
14770
+ * provider observability hooks, and session persistence surfaces.
14002
14771
  */
14003
14772
  async runEphemeralTurn(args: EphemeralTurnArgs): Promise<EphemeralTurnResult> {
14004
14773
  args.signal?.throwIfAborted();
14005
- const isBtw = args.purpose === "btw";
14006
- // `/btw` captures every provider-facing parent facet synchronously so an
14007
- // in-flight foreground mutation cannot create a mixed-generation request.
14008
- const btwSnapshot = isBtw
14009
- ? this.#buildBtwEphemeralSnapshot(
14010
- cloneJsonValueForForkSeed(this.buildDisplaySessionContext().messages),
14011
- args.promptText,
14012
- )
14013
- : undefined;
14014
- const model = this.model;
14015
- const systemPrompt = isBtw ? [...this.systemPrompt] : undefined;
14016
- const thinkingLevel = this.thinkingLevel;
14017
- const hideThinkingSummary = this.agent.hideThinkingSummary;
14018
- const serviceTier = this.serviceTier;
14019
- const providerAffinitySessionId = this.agent.providerSessionId ?? this.agent.sessionId ?? this.sessionId;
14020
- const sideSessionId = isBtw ? `${providerAffinitySessionId}:btw:${crypto.randomUUID()}` : undefined;
14021
- const backgroundArgs = isBtw ? undefined : args;
14022
- const callerOwnsRosterClaim = backgroundArgs?.ircRosterClaim !== undefined;
14023
- const rosterClaim = isBtw
14024
- ? null
14025
- : callerOwnsRosterClaim
14026
- ? backgroundArgs?.ircRosterClaim
14027
- : this.#claimIrcRosterCandidate();
14774
+ if (args.purpose === "btw") return await this.#runBtwTurn(args);
14775
+
14776
+ const callerOwnsRosterClaim = args.ircRosterClaim !== undefined;
14777
+ const rosterClaim = callerOwnsRosterClaim ? args.ircRosterClaim : this.#claimIrcRosterCandidate();
14028
14778
  const rosterClaimIsCurrent = () =>
14029
14779
  !rosterClaim || this.#isCurrentIrcRosterClaim(rosterClaim.token, rosterClaim.epoch);
14030
- const prependMessagesValid = () => rosterClaimIsCurrent() && backgroundArgs?.prependMessagesValid?.() !== false;
14780
+ const prependMessagesValid = () => rosterClaimIsCurrent() && args.prependMessagesValid?.() !== false;
14031
14781
  const rosterMessage = !callerOwnsRosterClaim && rosterClaimIsCurrent() ? rosterClaim?.message : undefined;
14032
14782
  try {
14783
+ const model = this.model;
14033
14784
  if (!model) throw new Error("No active model on session");
14034
- const credentialSessionId = isBtw ? providerAffinitySessionId : this.sessionId;
14035
- const apiKey = await awaitEphemeralAbort(
14036
- this.#modelRegistry.getApiKey(model, credentialSessionId),
14037
- args.signal,
14038
- );
14785
+ const apiKey = await awaitEphemeralAbort(this.#modelRegistry.getApiKey(model, this.sessionId), args.signal);
14039
14786
  if (!apiKey) throw new Error(`No API key for ${model.provider}/${model.id}`);
14040
-
14041
14787
  const prependMessages = prependMessagesValid()
14042
- ? [...(rosterMessage ? [rosterMessage] : []), ...(backgroundArgs?.prependMessages ?? [])]
14788
+ ? [...(rosterMessage ? [rosterMessage] : []), ...(args.prependMessages ?? [])]
14043
14789
  : undefined;
14044
- let snapshot = btwSnapshot ?? this.#buildEphemeralSnapshot(args.promptText, prependMessages);
14790
+ let snapshot = this.#buildEphemeralSnapshot(args.promptText, prependMessages);
14045
14791
  let llmMessages = await awaitEphemeralAbort(this.convertMessagesToLlm(snapshot, args.signal), args.signal);
14046
- if (!isBtw && prependMessages && !prependMessagesValid()) {
14792
+ if (prependMessages && !prependMessagesValid()) {
14047
14793
  snapshot = this.#buildEphemeralSnapshot(args.promptText);
14048
14794
  llmMessages = await awaitEphemeralAbort(this.convertMessagesToLlm(snapshot, args.signal), args.signal);
14049
14795
  }
14050
- const context: Context = {
14051
- systemPrompt: systemPrompt ?? this.systemPrompt,
14052
- messages: llmMessages,
14053
- tools: [],
14054
- };
14055
-
14796
+ const context: Context = { systemPrompt: this.systemPrompt, messages: llmMessages, tools: [] };
14797
+ const ephemeralSessionId = crypto.randomUUID();
14798
+ const options = this.prepareSimpleStreamOptions(
14799
+ {
14800
+ apiKey,
14801
+ sessionId: ephemeralSessionId,
14802
+ metadata: buildSessionMetadata(
14803
+ ephemeralSessionId,
14804
+ model.provider,
14805
+ this.#modelRegistry.authStorage,
14806
+ this.sessionId,
14807
+ ),
14808
+ reasoning: toReasoningEffort(this.thinkingLevel),
14809
+ hideThinkingSummary: this.agent.hideThinkingSummary,
14810
+ serviceTier: this.serviceTier,
14811
+ signal: args.signal,
14812
+ toolChoice: "none",
14813
+ },
14814
+ model.provider,
14815
+ );
14816
+ args.signal?.throwIfAborted();
14056
14817
  let replyText = "";
14057
14818
  let assistantMessage: AssistantMessage | undefined;
14058
- if (isBtw) {
14059
- const establishmentAbort = new AbortController();
14060
- const requestSignal = args.signal
14061
- ? AbortSignal.any([args.signal, establishmentAbort.signal])
14062
- : establishmentAbort.signal;
14063
- const timeoutError = new Error(
14064
- `/btw provider did not produce an event within ${BTW_ESTABLISHMENT_TIMEOUT_MS / 1000} seconds`,
14065
- );
14066
- timeoutError.name = "TimeoutError";
14067
- let establishmentTimer: NodeJS.Timeout | undefined;
14068
- try {
14069
- requestSignal.throwIfAborted();
14070
- const options: SimpleStreamOptions = {
14071
- apiKey,
14072
- sessionId: sideSessionId!,
14073
- metadata: buildSessionMetadata(
14074
- sideSessionId!,
14075
- model.provider,
14076
- this.#modelRegistry.authStorage,
14077
- providerAffinitySessionId,
14078
- ),
14079
- reasoning: toReasoningEffort(thinkingLevel),
14080
- hideThinkingSummary,
14081
- serviceTier,
14082
- onPayload: this.#onPayload,
14083
- signal: requestSignal,
14084
- toolChoice: "none",
14085
- requestMaxRetries: 0,
14086
- streamMaxRetries: 0,
14087
- streamFirstEventTimeoutMs: 0,
14088
- };
14089
- establishmentTimer = setTimeout(
14090
- () => establishmentAbort.abort(timeoutError),
14091
- BTW_ESTABLISHMENT_TIMEOUT_MS,
14092
- );
14093
- establishmentTimer.unref?.();
14094
- const stream = streamSimple(model, context, options);
14095
- let receivedProviderEvent = false;
14096
- for await (const event of stream) {
14097
- if (!receivedProviderEvent && event.type !== "start") {
14098
- receivedProviderEvent = true;
14099
- clearTimeout(establishmentTimer);
14100
- establishmentTimer = undefined;
14101
- }
14102
- if (event.type === "text_delta") {
14103
- replyText += event.delta;
14104
- args.onTextDelta?.(event.delta);
14105
- } else if (event.type === "done") {
14106
- assistantMessage = event.message;
14107
- break;
14108
- } else if (event.type === "error") {
14109
- throw new Error(event.error.errorMessage || "Ephemeral turn failed");
14110
- }
14111
- }
14112
- requestSignal.throwIfAborted();
14113
- } catch (error) {
14114
- if (establishmentAbort.signal.aborted && !args.signal?.aborted) throw timeoutError;
14115
- throw error;
14116
- } finally {
14117
- if (establishmentTimer) clearTimeout(establishmentTimer);
14118
- }
14119
- } else {
14120
- const ephemeralSessionId = crypto.randomUUID();
14121
- const options = this.prepareSimpleStreamOptions(
14122
- {
14123
- apiKey,
14124
- sessionId: ephemeralSessionId,
14125
- metadata: buildSessionMetadata(
14126
- ephemeralSessionId,
14127
- model.provider,
14128
- this.#modelRegistry.authStorage,
14129
- this.sessionId,
14130
- ),
14131
- reasoning: toReasoningEffort(thinkingLevel),
14132
- hideThinkingSummary,
14133
- serviceTier,
14134
- signal: args.signal,
14135
- toolChoice: "none",
14136
- },
14137
- model.provider,
14138
- );
14139
- args.signal?.throwIfAborted();
14140
- const stream = streamSimple(model, context, options);
14141
- for await (const event of stream) {
14142
- if (event.type === "text_delta") {
14143
- replyText += event.delta;
14144
- args.onTextDelta?.(event.delta);
14145
- } else if (event.type === "done") {
14146
- assistantMessage = event.message;
14147
- break;
14148
- } else if (event.type === "error") {
14149
- throw new Error(event.error.errorMessage || "Ephemeral turn failed");
14150
- }
14819
+ for await (const event of streamSimple(model, context, options)) {
14820
+ if (event.type === "text_delta") {
14821
+ replyText += event.delta;
14822
+ args.onTextDelta?.(event.delta);
14823
+ } else if (event.type === "done") {
14824
+ assistantMessage = event.message;
14825
+ break;
14826
+ } else if (event.type === "error") {
14827
+ throw new Error(event.error.errorMessage || "Ephemeral turn failed");
14151
14828
  }
14152
14829
  }
14153
-
14154
14830
  if (!assistantMessage) throw new Error("Ephemeral turn ended without a final message");
14155
14831
  args.signal?.throwIfAborted();
14156
14832
  if (
14157
- !isBtw &&
14158
14833
  !callerOwnsRosterClaim &&
14159
14834
  rosterClaim &&
14160
14835
  assistantMessage.stopReason !== "error" &&
@@ -14162,21 +14837,117 @@ export class AgentSession {
14162
14837
  ) {
14163
14838
  this.#commitIrcRosterClaim(rosterClaim.token, rosterClaim.epoch);
14164
14839
  }
14165
- args.signal?.throwIfAborted();
14166
14840
  return { replyText: replyText.trim(), assistantMessage };
14167
14841
  } finally {
14168
- if (!isBtw && !callerOwnsRosterClaim && rosterClaim) {
14169
- this.#releaseIrcRosterClaim(rosterClaim.token, rosterClaim.epoch);
14842
+ if (!callerOwnsRosterClaim && rosterClaim) this.#releaseIrcRosterClaim(rosterClaim.token, rosterClaim.epoch);
14843
+ }
14844
+ }
14845
+
14846
+ async #runBtwTurn(args: Extract<EphemeralTurnArgs, { purpose: "btw" }>): Promise<EphemeralTurnResult> {
14847
+ const model = args.turn.scope?.model;
14848
+ const credentialSessionId = args.turn.scope?.credentialSessionId;
14849
+ if (!model || !credentialSessionId) throw new Error("The /btw conversation scope was scrubbed.");
14850
+ if (utf8ByteLength(args.turn.question) > BTW_MAX_QUESTION_UTF8_BYTES) {
14851
+ throw new RangeError(`/btw questions are limited to ${BTW_MAX_QUESTION_UTF8_BYTES} UTF-8 bytes.`);
14852
+ }
14853
+ const apiKey = await awaitEphemeralAbort(this.#modelRegistry.getApiKey(model, credentialSessionId), args.signal);
14854
+ if (!apiKey) throw new Error(`No API key for ${model.provider}/${model.id}`);
14855
+ const scope = args.turn.scope;
14856
+ if (!scope) throw new Error("The /btw conversation scope was scrubbed.");
14857
+ const messages = scope.messages.map(message => ({
14858
+ role: message.role,
14859
+ content: [{ type: "text" as const, text: message.text }],
14860
+ })) as Message[];
14861
+ for (const exchange of boundBtwExchanges(args.contextExchanges ?? [])) {
14862
+ messages.push({
14863
+ role: "user",
14864
+ content: [{ type: "text", text: exchange.question }],
14865
+ } as Message);
14866
+ messages.push({
14867
+ role: "assistant",
14868
+ content: [{ type: "text", text: exchange.answer }],
14869
+ } as Message);
14870
+ }
14871
+ messages.push({
14872
+ role: "user",
14873
+ content: [{ type: "text", text: args.turn.question }],
14874
+ } as Message);
14875
+ const context: Context = { systemPrompt: scope.systemPrompt, messages, tools: [] };
14876
+ const timeoutAbort = new AbortController();
14877
+ const requestSignal = args.signal ? AbortSignal.any([args.signal, timeoutAbort.signal]) : timeoutAbort.signal;
14878
+ const options: SimpleStreamOptions = {
14879
+ apiKey,
14880
+ sessionId: scope.sideSessionId,
14881
+ reasoning: toReasoningEffort(scope.thinkingLevel),
14882
+ hideThinkingSummary: scope.hideThinkingSummary,
14883
+ serviceTier: scope.serviceTier,
14884
+ signal: requestSignal,
14885
+ toolChoice: "none",
14886
+ requestMaxRetries: 0,
14887
+ streamMaxRetries: 0,
14888
+ streamFirstEventTimeoutMs: 0,
14889
+ };
14890
+ const iterator = streamSimple(scope.model, context, options)[Symbol.asyncIterator]();
14891
+ let replyText = "";
14892
+ let completed = false;
14893
+ let active = true;
14894
+ let onTextDelta = args.onTextDelta;
14895
+ let idleTimer: NodeJS.Timeout | undefined;
14896
+ const timeout = (message: string) => {
14897
+ const error = new Error(message);
14898
+ error.name = "TimeoutError";
14899
+ timeoutAbort.abort(error);
14900
+ };
14901
+ const resetIdleTimer = () => {
14902
+ if (idleTimer) clearTimeout(idleTimer);
14903
+ idleTimer = setTimeout(
14904
+ () => timeout(`/btw provider was idle for ${BTW_STREAM_IDLE_TIMEOUT_MS / 1000} seconds`),
14905
+ BTW_STREAM_IDLE_TIMEOUT_MS,
14906
+ );
14907
+ idleTimer.unref?.();
14908
+ };
14909
+ const totalTimer = setTimeout(
14910
+ () => timeout(`/btw provider exceeded ${BTW_STREAM_TOTAL_TIMEOUT_MS / 1000} seconds`),
14911
+ BTW_STREAM_TOTAL_TIMEOUT_MS,
14912
+ );
14913
+ totalTimer.unref?.();
14914
+ resetIdleTimer();
14915
+ const consume = async () => {
14916
+ while (active) {
14917
+ const result = await iterator.next();
14918
+ if (result.done || !active) break;
14919
+ resetIdleTimer();
14920
+ const event = result.value;
14921
+ if (event.type === "text_delta") {
14922
+ const bounded = truncateUtf8(replyText + event.delta, BTW_MAX_ANSWER_UTF8_BYTES);
14923
+ const delta = bounded.slice(replyText.length);
14924
+ replyText = bounded;
14925
+ if (delta) onTextDelta?.(delta);
14926
+ } else if (event.type === "done") {
14927
+ completed = true;
14928
+ break;
14929
+ } else if (event.type === "error") {
14930
+ throw new Error(event.error.errorMessage || "Ephemeral turn failed");
14931
+ }
14170
14932
  }
14933
+ };
14934
+ try {
14935
+ await awaitEphemeralAbort(consume(), requestSignal);
14936
+ requestSignal.throwIfAborted();
14937
+ if (!completed) throw new Error("Ephemeral turn ended without a final message");
14938
+ const finalText = replyText.trim();
14939
+ return { replyText: finalText, assistantMessage: this.#buildBtwAssistantMessage(finalText) };
14940
+ } finally {
14941
+ active = false;
14942
+ onTextDelta = undefined;
14943
+ replyText = "";
14944
+ context.messages = [];
14945
+ if (idleTimer) clearTimeout(idleTimer);
14946
+ clearTimeout(totalTimer);
14947
+ void iterator.return?.().catch(() => undefined);
14171
14948
  }
14172
14949
  }
14173
14950
 
14174
- /** Build a `/btw` snapshot from an already-cloned committed context. */
14175
- #buildBtwEphemeralSnapshot(messages: AgentMessage[], promptText: string): AgentMessage[] {
14176
- messages.push(this.#buildEphemeralPromptMessage(promptText));
14177
- return messages;
14178
- }
14179
-
14180
14951
  /** Build a background snapshot with in-flight assistant and optional context. */
14181
14952
  #buildEphemeralSnapshot(promptText: string, prependMessages?: AgentMessage[]): AgentMessage[] {
14182
14953
  const messages = [...this.messages];
@@ -14285,7 +15056,10 @@ export class AgentSession {
14285
15056
  * Listeners are preserved and will continue receiving events.
14286
15057
  * @returns true if switch completed, false if cancelled by hook
14287
15058
  */
14288
- async switchSession(sessionPath: string): Promise<boolean> {
15059
+ async switchSession(
15060
+ sessionPath: string,
15061
+ options?: { transition?: SessionSwitchEvent["transition"] },
15062
+ ): Promise<boolean> {
14289
15063
  this.#beginSessionTransition("switch-session");
14290
15064
  try {
14291
15065
  const previousSessionFile = this.sessionManager.getSessionFile();
@@ -14341,6 +15115,12 @@ export class AgentSession {
14341
15115
 
14342
15116
  try {
14343
15117
  await this.sessionManager.setSessionFile(sessionPath);
15118
+ // The successor identity is already rotated in the manager but not yet
15119
+ // published; gate its local:// root before publication so the agent,
15120
+ // workflow-gate emitter, and hooks cannot resolve against an ungated
15121
+ // root. The manager-rotation window itself is tracked in #3138
15122
+ // (#2797 / #2925).
15123
+ if (switchingToDifferentSession) await initializeLocalRoot(this.#localProtocolOptions());
14344
15124
  this.#syncAgentSessionId();
14345
15125
  this.#rekeyHindsightMemoryForCurrentSessionId();
14346
15126
 
@@ -14405,6 +15185,12 @@ export class AgentSession {
14405
15185
  ? this.#getInheritedThinkingLevel()
14406
15186
  : persistedThinkingLevel,
14407
15187
  );
15188
+ this.#thinkingLevelMutationRevision++;
15189
+ this.#thinkingLevelLiveMutationRevision++;
15190
+ this.#pendingThinkingLevelControlSuccess = undefined;
15191
+ this.#pendingThinkingLevelControlFailure = undefined;
15192
+ this.#pendingThinkingVisibilityControlSuccess = undefined;
15193
+ this.#pendingThinkingVisibilityControlFailure = undefined;
14408
15194
  this.#thinkingLevel = nextThinkingLevel;
14409
15195
  this.agent.setThinkingLevel(toReasoningEffort(nextThinkingLevel));
14410
15196
  this.agent.serviceTier = hasServiceTierEntry
@@ -14415,8 +15201,11 @@ export class AgentSession {
14415
15201
  // Establish the successor's durable session identity only after every
14416
15202
  // restored state facet is live. Identity-bound extension hooks run below.
14417
15203
  await this.sessionManager.ensureOnDisk();
15204
+ if (!switchingToDifferentSession) await initializeLocalRoot(this.#localProtocolOptions());
14418
15205
 
14419
15206
  if (switchingToDifferentSession) {
15207
+ // The local:// migration gate for this successor already ran above,
15208
+ // before the identity was published (#2797 / #2925).
14420
15209
  this.#resetHindsightConversationTrackingIfHindsight();
14421
15210
  this.#resetIrcRosterDeliveryState();
14422
15211
  }
@@ -14435,6 +15224,7 @@ export class AgentSession {
14435
15224
  type: "session_switch",
14436
15225
  reason: "resume",
14437
15226
  previousSessionFile,
15227
+ ...(options?.transition ? { transition: options.transition } : {}),
14438
15228
  });
14439
15229
  }
14440
15230
  return true;
@@ -14470,8 +15260,14 @@ export class AgentSession {
14470
15260
  this.agent.restoreSteering(previousAgentSteeringQueue);
14471
15261
  this.agent.restoreFollowUp(previousAgentFollowUpQueue);
14472
15262
  if (previousModel) {
14473
- this.agent.setModel(previousModel);
14474
- }
15263
+ this.#setAgentModelWithReasoningContext(previousModel);
15264
+ }
15265
+ this.#thinkingLevelMutationRevision++;
15266
+ this.#thinkingLevelLiveMutationRevision++;
15267
+ this.#pendingThinkingLevelControlSuccess = undefined;
15268
+ this.#pendingThinkingLevelControlFailure = undefined;
15269
+ this.#pendingThinkingVisibilityControlSuccess = undefined;
15270
+ this.#pendingThinkingVisibilityControlFailure = undefined;
14475
15271
  this.#thinkingLevel = previousThinkingLevel;
14476
15272
  this.agent.setThinkingLevel(toReasoningEffort(previousThinkingLevel));
14477
15273
  this.agent.serviceTier = previousServiceTier;
@@ -14535,19 +15331,21 @@ export class AgentSession {
14535
15331
  skipConversationRestore = result?.skipConversationRestore ?? false;
14536
15332
  }
14537
15333
 
14538
- // Clear pending messages (bound to old session state)
15334
+ // Flush pending writes before preparing the successor.
15335
+ await this.sessionManager.flush();
15336
+ const prepared = selectedEntry.parentId
15337
+ ? await this.sessionManager.prepareBranchedSession(selectedEntry.parentId)
15338
+ : await this.sessionManager.prepareNewSession({ parentSession: previousSessionFile });
15339
+ try {
15340
+ await initializeLocalRoot(this.#localProtocolOptions(prepared));
15341
+ this.sessionManager.commitPreparedNewSession(prepared);
15342
+ } catch (error) {
15343
+ throw await discardPreparedNewSessionAfterFailure(this.sessionManager, prepared, error);
15344
+ }
14539
15345
  this.#pendingNextTurnMessages = [];
14540
15346
  this.#scheduledHiddenNextTurnGeneration = undefined;
14541
-
14542
- // Flush pending writes before branching
14543
- await this.sessionManager.flush();
14544
15347
  this.#cancelOwnAsyncJobs();
14545
15348
 
14546
- if (!selectedEntry.parentId) {
14547
- await this.sessionManager.newSession({ parentSession: previousSessionFile });
14548
- } else {
14549
- this.sessionManager.createBranchedSession(selectedEntry.parentId);
14550
- }
14551
15349
  this.#syncTodoPhasesFromBranch();
14552
15350
  this.#syncAgentSessionId();
14553
15351
  this.#bindWorkflowGateEmitter(previousWorkflowGateSessionId);