@sayknow-cli/coding-agent 0.4.7 → 0.5.0

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 +97 -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
@@ -30,9 +30,12 @@ import { promisify } from "node:util";
30
30
  import { ThinkingLevel } from "@sayknow-cli/agent-core";
31
31
  import type { ImageContent, TextContent, Tool } from "@sayknow-cli/ai";
32
32
  import { NotificationServer, nativeBuildInfo } from "@sayknow-cli/natives";
33
- import { logger, postmortem, prompt, VERSION } from "@sayknow-cli/utils";
33
+ import { logger, postmortem, VERSION } from "@sayknow-cli/utils";
34
+ import { isModelProfileProviderAvailable, projectModelProfileCatalog } from "../../config/model-profile-contract";
35
+ import { isAuthenticated, kNoAuth } from "../../config/model-registry";
34
36
  import { Settings } from "../../config/settings";
35
37
  import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "../../extensibility/extensions";
38
+ import { INTERACTIVE_SELECTOR_RESUME_ORIGIN } from "../../extensibility/shared-events";
36
39
  import { toAgentWireEventPayload } from "../../modes/shared/agent-wire/event-envelope";
37
40
  import {
38
41
  NotificationGatePolicyChangedError,
@@ -40,7 +43,6 @@ import {
40
43
  type WorkflowGateTerminalController,
41
44
  type WorkflowGateTerminalProof,
42
45
  } from "../../modes/shared/agent-wire/workflow-gate-broker";
43
- import btwUserPrompt from "../../prompts/system/btw-user.md" with { type: "text" };
44
46
  import type { AgentSessionEvent } from "../../session/agent-session";
45
47
  import { parseThinkingLevel } from "../../thinking";
46
48
  import type {
@@ -54,19 +56,20 @@ import type {
54
56
  AskSettlementResult,
55
57
  } from "../../tools";
56
58
  import { registerAskAnswerSource, registerWorkflowGateEmitterListener } from "../../tools/ask-answer-registry";
59
+ import { acpFinalTextFromMessage } from "../acp/final-text";
57
60
  import { ensureBroker } from "../broker/ensure";
58
61
  import { SessionIndex } from "../broker/session-index";
59
62
  import { SessionSdkHost, shouldHostSdk } from "../host";
60
63
  import { type ControlSurface, dispatchControl } from "../host/control";
61
64
  import { CursorRegistry, QueryHandlers, RevisionStore, type SessionSurface } from "../host/query";
62
65
  import { projectQ10Models } from "../models.js";
66
+ import { PROMPT_CLIENT_REF_MAX_LENGTH } from "../prompt-status";
63
67
  import { OPERATIONS } from "../protocol/operation-registry";
64
68
  import {
65
69
  lifecycleStartupCapabilityForApi,
66
70
  normalizeSdkStartupFailure,
67
71
  type SdkStartupFailure,
68
72
  } from "../startup-capability";
69
-
70
73
  import { registerTelegramFileSink } from "./attachment-registry";
71
74
  import { ensureDiscordDaemon, ensureSlackDaemon } from "./chat-daemon-control";
72
75
  import {
@@ -80,16 +83,37 @@ import {
80
83
  sessionTag,
81
84
  } from "./config";
82
85
  import { telegramControlCommandUsage } from "./config-commands";
86
+ import {
87
+ isNativeControlDrainAvailable,
88
+ runIdentityControlSuccessPath,
89
+ type TerminalSendOutcome,
90
+ } from "./control-drain-lease";
83
91
  import { imageAttachmentsFromMessage, notificationActionPayload, summaryFromMessage, truncate } from "./helpers";
92
+ import { createKindAwareReconciliation } from "./kind-aware-reconciliation";
84
93
  import { assertNativeRuntimeCompatibility } from "./native-runtime-compatibility";
94
+ import { createPromptReconciliation, sanitizePromptFailure } from "./prompt-reconciliation";
95
+ import { createReconciliationStore } from "./reconciliation-store";
85
96
  import { NotificationSessionController, type NotificationSessionRuntime } from "./session-control";
86
97
  import {
87
98
  ASK_SELECTED_ACK_CAPABILITY,
88
99
  type EnsureDaemonResult,
89
100
  endpointAuthorityDigest,
90
101
  ensureTelegramDaemonRunningDetailed,
102
+ type RegisterNotificationRootResult,
103
+ unregisterNotificationRoot,
91
104
  } from "./telegram-daemon";
92
105
 
106
+ export type {
107
+ IdentityControlSuccessPathInput,
108
+ IdentityControlTerminalPathInput,
109
+ TerminalSendOutcome,
110
+ } from "./control-drain-lease";
111
+ export {
112
+ isNativeControlDrainAvailable,
113
+ runIdentityControlSuccessPath,
114
+ runIdentityControlTerminalPath,
115
+ } from "./control-drain-lease";
116
+
93
117
  // ===========================================================================
94
118
  // Session lifecycle control protocol (TypeScript mirror of the Rust wire
95
119
  // contract in `crates/skc-sdk/src/lifecycle.rs`).
@@ -402,6 +426,7 @@ interface UnattendedGatePresentation {
402
426
  question: string;
403
427
  options: string[];
404
428
  controls: readonly AskRemoteControl[];
429
+ recommendedIndex?: number;
405
430
  multi: boolean;
406
431
  allowEmpty: boolean;
407
432
  navigationLabel?: "Next" | "Done";
@@ -410,6 +435,14 @@ interface UnattendedGatePresentation {
410
435
  onActivated?: (actionId: string, lease: { actionId: string; registrationEpoch: number }) => void;
411
436
  onClosed?: () => void;
412
437
  }
438
+ function recommendedIndexFromGateOptions(options: readonly unknown[]): number | undefined {
439
+ const descriptions = options.map(option => (option as { description?: unknown }).description);
440
+ const recommended = descriptions.filter(description => description === "recommended");
441
+ return recommended.length === 1 &&
442
+ descriptions.every(description => description === undefined || description === "recommended")
443
+ ? descriptions.indexOf("recommended")
444
+ : undefined;
445
+ }
413
446
 
414
447
  type RetireStatus = "retired" | "already_terminal" | "claimed" | "stale";
415
448
  type DirectControlOutcome = "accepted" | "rejected" | "unknown";
@@ -740,6 +773,9 @@ export class PresentationArbiter {
740
773
  ? `(${presentation.selectedOptions.length} selected) ${presentation.question}`
741
774
  : presentation.question,
742
775
  options: presentation.options,
776
+ ...(presentation.recommendedIndex === undefined
777
+ ? {}
778
+ : { recommendedIndex: presentation.recommendedIndex }),
743
779
  controls: presentation.multi
744
780
  ? [
745
781
  {
@@ -869,6 +905,8 @@ interface SessionRuntime {
869
905
  disposeGateListener: () => void;
870
906
  /** Whether notification-only delivery and answer resources are active. */
871
907
  notificationsActive: boolean;
908
+ /** Rejects new SDK frames while a leased terminal response drains. */
909
+ inboundFenced: boolean;
872
910
  /** Set as soon as terminal teardown is requested, before startup settles. */
873
911
  stopping: boolean;
874
912
  /** Recreates notification-only resources after `/notify on`. */
@@ -896,6 +934,8 @@ interface SessionRuntime {
896
934
  hostStopped: boolean;
897
935
  serverStopped: boolean;
898
936
  brokerRegistrationReleased: boolean;
937
+ /** Managed Telegram root registration released during terminal teardown. */
938
+ notificationRootRegistration?: { settings: Settings; cwd: string; registrationToken?: string };
899
939
  verbosity: "lean" | "verbose";
900
940
  sessionTag: string;
901
941
  /** Whether the agent loop is currently running (drives the typing indicator). */
@@ -907,11 +947,24 @@ interface SessionRuntime {
907
947
  /** Records a correlated prompt terminal boundary after agent unwind. */
908
948
  /** Atomically claims a correlated prompt terminal boundary after agent unwind. */
909
949
  recordPromptTerminal: (correlation: { commandId: string; turnId: string } | undefined) => boolean;
950
+ /** Transitions the authoritative reconciliation record at lifecycle ingress; terminal outcomes settle once. */
951
+ notePromptReconciliation: (
952
+ correlation: { commandId: string; turnId: string } | undefined,
953
+ frame: { type: "agent_start" | "agent_end" } | { type: "agent_failed"; error: unknown },
954
+ ) => void;
955
+ /** Settles and emits one sanitized correlated prompt failure. */
956
+ emitPromptFailure: (correlation: { commandId: string; turnId: string }, error: unknown) => void;
910
957
  /** Records correlated lifecycle frames for replay and delivers them only to the accepted requester after acknowledgement. */
911
958
  emitPromptLifecycle: (
912
959
  correlation: { commandId: string; turnId: string } | undefined,
913
960
  frame:
914
- | { type: "agent_start" | "agent_end"; sessionId: string; commandId?: string; turnId?: string }
961
+ | {
962
+ type: "agent_start" | "agent_end";
963
+ sessionId: string;
964
+ commandId?: string;
965
+ turnId?: string;
966
+ finalText?: string;
967
+ }
915
968
  | {
916
969
  type: "agent_failed";
917
970
  sessionId: string;
@@ -940,12 +993,21 @@ interface SessionRuntime {
940
993
  turnClosed?: boolean;
941
994
  /** Finalized while provisional policy was held; flush exactly once on stable activation. */
942
995
  pendingFinal?: { text?: string; messageRef?: string };
996
+ /**
997
+ * Lean-mode deferred final answer: latest assistant text observed at `turn_end`,
998
+ * emitted once at `agent_end` so intermediate tool-turn narration does not flood
999
+ * remote clients. Cleared after flush or when replaced by a newer turn.
1000
+ */
1001
+ pendingSettled?: { text: string; messageRef?: string };
943
1002
  /** Durable gates emitted while ownership is provisional; presented only after stable activation. */
944
1003
  deferredGatePresentations: Array<() => void>;
945
1004
  /** SDK control frames received during provisional ownership; replayed only after stable activation. */
946
1005
  deferredInboundControls: Array<() => void>;
947
1006
  /** Started tool calls awaiting a terminal activity frame, keyed by tool call id. */
948
- inFlightTools: Map<string, { toolName: string; args: unknown }>;
1007
+ inFlightTools: Map<
1008
+ string,
1009
+ { toolName: string; args?: unknown; pendingPhase?: "completed" | "failed" | "cancelled" }
1010
+ >;
949
1011
  /** Cancels the postmortem cleanup that emits `session_closed` on process teardown. */
950
1012
  cancelPostmortemCleanup: () => void;
951
1013
  /** Disposes side-turn resources when their owning logical session becomes unavailable. */
@@ -955,7 +1017,6 @@ interface SessionRuntime {
955
1017
  const SENSITIVE_MODEL_LABEL =
956
1018
  /(?:\b(?:https?|wss?):\/\/|\b[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}\b|\b(?:api[-_ ]?key|access[-_ ]?token|bearer|secret|password|account(?:\s*id)?|email|exception|stack trace)\b|\b(?:sk|pk|rk)-[A-Za-z0-9_-]{12,}\b)/i;
957
1019
  const TOOL_SUMMARY_MAX = 280;
958
- const EMPTY_CAPABILITIES: ReadonlySet<string> = new Set();
959
1020
 
960
1021
  /** Stable projection of the tool-owned safe-display seam (never the full Tool surface). */
961
1022
  type SafeSummaryTool = Pick<Tool, "safeSummary" | "safeSummaryFields">;
@@ -1020,6 +1081,14 @@ function pushSessionFrame(
1020
1081
  runtime.server.pushFrame(JSON.stringify(frame));
1021
1082
  }
1022
1083
 
1084
+ async function pushTerminalSessionFrame(
1085
+ runtime: Pick<SessionRuntime, "server" | "host">,
1086
+ frame: { type: "session_closed"; sessionId: string },
1087
+ ): Promise<boolean> {
1088
+ runtime.host.emitEvent({ kind: frame.type, payload: frame });
1089
+ return await runtime.server.pushFrameAndWait(JSON.stringify(frame), 1_000);
1090
+ }
1091
+
1023
1092
  function pushFileAttachment(
1024
1093
  runtime: Pick<SessionRuntime, "server" | "host">,
1025
1094
  frame: { type: "file_attachment"; sessionId: string; name: string; mime?: string; caption?: string },
@@ -1068,7 +1137,7 @@ const defaultConfig: NotificationConfig = {
1068
1137
  idleTimeoutMs: 60_000,
1069
1138
  rich: { enabled: true },
1070
1139
  richDraft: { enabled: false },
1071
- toolActivity: { enabled: true },
1140
+ toolActivity: { enabled: false },
1072
1141
  streaming: { enabled: true },
1073
1142
  topics: {},
1074
1143
  btw: { enabled: true },
@@ -1594,6 +1663,7 @@ function registerInteractiveAnswerSource(
1594
1663
  question: request.question,
1595
1664
  options: request.options,
1596
1665
  controls: request.controls,
1666
+ recommendedIndex: request.recommendedIndex,
1597
1667
  multi: false,
1598
1668
  allowEmpty: false,
1599
1669
  selectedOptions: [],
@@ -1660,6 +1730,7 @@ const UNINSTALLED_CONTROL_OPERATIONS = new Set(["auth.login", "host_tools.regist
1660
1730
 
1661
1731
  const CONTROL_BINDINGS: Readonly<Record<string, string | undefined>> = {
1662
1732
  "model.cycle": "cycleModel",
1733
+ "model.profile.set": "setModelProfile",
1663
1734
  "thinking.cycle": "cycleThinkingLevel",
1664
1735
  "queue.steering_mode.set": "setQueueMode",
1665
1736
  "queue.follow_up_mode.set": "setQueueMode",
@@ -1756,6 +1827,10 @@ function sdkQuerySurface(
1756
1827
  followupQueueDepth: 0,
1757
1828
  }),
1758
1829
  configOverrides: ReadonlyMap<string, unknown> = new Map(),
1830
+ promptStatusLookup: (selector: { commandId?: string; turnId?: string; clientRef?: string }) => unknown,
1831
+ skillStatusLookup: (selector: { commandId?: string; turnId?: string; clientRef?: string }) => unknown = () => ({
1832
+ status: "unknown",
1833
+ }),
1759
1834
  ): SessionSurface {
1760
1835
  const metadata = () => ({
1761
1836
  sessionId: id,
@@ -1823,6 +1898,26 @@ function sdkQuerySurface(
1823
1898
  const currentThinkingLevel = api.getThinkingLevel();
1824
1899
  return projectQ10Models({ models, currentModel, currentThinkingLevel });
1825
1900
  },
1901
+ getModelProfiles: async () => {
1902
+ const profiles = ctx.modelRegistry.getModelProfiles();
1903
+ const catalog = projectModelProfileCatalog(profiles, ctx.modelRegistry.getError());
1904
+ const providers = new Set([...profiles.values()].flatMap(profile => profile.requiredProviders));
1905
+ const authenticatedProviders = new Set<string>();
1906
+ await Promise.all(
1907
+ [...providers].map(async provider => {
1908
+ try {
1909
+ const credential = await ctx.modelRegistry.getApiKeyForProvider(provider, id);
1910
+ if (credential === kNoAuth || isAuthenticated(credential)) authenticatedProviders.add(provider);
1911
+ } catch {
1912
+ // A provider whose credential state cannot be read is not currently configurable.
1913
+ }
1914
+ }),
1915
+ );
1916
+ return catalog.map(item => ({
1917
+ ...item,
1918
+ available: isModelProfileProviderAvailable(profiles.get(item.id)!, authenticatedProviders),
1919
+ }));
1920
+ },
1826
1921
  getSkillState: () => ctx.getSkillState(),
1827
1922
  getGates: () => {
1828
1923
  const workflowGate = ctx.workflowGate;
@@ -1862,6 +1957,10 @@ function sdkQuerySurface(
1862
1957
  getExtensions: () => ctx.getExtensions(),
1863
1958
  getArtifactRange: (id, offset, length) => ctx.getArtifactRange?.(id, offset, length),
1864
1959
  getJobs: () => ctx.getJobs(),
1960
+ getPromptStatus: (selector: { commandId?: string; turnId?: string; clientRef?: string }) =>
1961
+ promptStatusLookup(selector),
1962
+ getSkillInvokeStatus: (selector: { commandId?: string; turnId?: string; clientRef?: string }) =>
1963
+ skillStatusLookup(selector),
1865
1964
  installedQueries: installedOperations(ctx, "query"),
1866
1965
  };
1867
1966
  }
@@ -1887,13 +1986,31 @@ function sdkControlSurface(
1887
1986
  onPromptAccepted: (
1888
1987
  correlation: { commandId: string; turnId: string },
1889
1988
  requesterConnectionId?: string,
1890
- ) => void = () => {},
1989
+ clientRef?: string,
1990
+ trackReconciliation?: boolean,
1991
+ ) => void | Promise<void> = () => {},
1891
1992
  onPromptFailed: (correlation: { commandId: string; turnId: string }, error: unknown) => void = () => {},
1892
1993
  acceptGateResolution: () => boolean,
1893
1994
  trackGateResolution: <T>(resolution: Promise<T>) => Promise<T>,
1995
+ admitPrompt: (clientRef?: string) => void,
1996
+ releasePromptAdmission: (clientRef?: string) => void,
1894
1997
  settings?: Settings,
1895
1998
  configOverrides: Map<string, unknown> = new Map(),
1896
1999
  configRevision: { current: number } = { current: 0 },
2000
+ skillRecon?: {
2001
+ admit: (clientRef?: string) => void;
2002
+ release: (clientRef?: string) => void;
2003
+ noteAccepted: (
2004
+ correlation: { commandId: string; turnId: string },
2005
+ clientRef?: string,
2006
+ extra?: { skillName?: string },
2007
+ ) => Promise<void>;
2008
+ noteTransition: (
2009
+ correlation: { commandId: string; turnId: string } | undefined,
2010
+ frame: { type: "agent_start" | "agent_end" } | { type: "agent_failed"; error: unknown },
2011
+ ) => Promise<void>;
2012
+ lookup: (selector: { commandId?: string; turnId?: string; clientRef?: string }) => unknown;
2013
+ },
1897
2014
  ): ControlSurface {
1898
2015
  const unavailable = (operation: string, reason: string) => () => {
1899
2016
  throw Object.assign(new Error(`${operation} is unavailable: ${reason}`), { code: "unavailable" });
@@ -1967,19 +2084,35 @@ function sdkControlSurface(
1967
2084
  deliverAs?: "steer" | "followUp",
1968
2085
  rejectWhenBusy = false,
1969
2086
  requesterConnectionId?: string,
2087
+ clientRef?: string,
2088
+ trackReconciliation = false,
1970
2089
  ) => {
1971
- if (forceFresh && isSessionBusy()) {
1972
- throw Object.assign(new Error("Previous turn did not finish aborting before replacement prompt submission."), {
1973
- code: "busy",
2090
+ const trimmedClientRef = typeof clientRef === "string" ? clientRef.trim() : undefined;
2091
+ if (clientRef !== undefined && (!trimmedClientRef || trimmedClientRef.length > PROMPT_CLIENT_REF_MAX_LENGTH))
2092
+ throw Object.assign(new Error("clientRef must be a non-empty string of at most 128 characters."), {
2093
+ code: "invalid_input",
1974
2094
  });
2095
+ if (trackReconciliation) admitPrompt(trimmedClientRef);
2096
+ try {
2097
+ if (forceFresh && isSessionBusy()) {
2098
+ throw Object.assign(
2099
+ new Error("Previous turn did not finish aborting before replacement prompt submission."),
2100
+ {
2101
+ code: "busy",
2102
+ },
2103
+ );
2104
+ }
2105
+ if (rejectWhenBusy && isSessionBusy())
2106
+ throw Object.assign(
2107
+ new Error("turn.prompt is unavailable while the agent is busy; use turn.steer explicitly."),
2108
+ {
2109
+ code: "busy",
2110
+ },
2111
+ );
2112
+ } catch (error) {
2113
+ if (trackReconciliation) releasePromptAdmission(trimmedClientRef);
2114
+ throw error;
1975
2115
  }
1976
- if (rejectWhenBusy && isSessionBusy())
1977
- throw Object.assign(
1978
- new Error("turn.prompt is unavailable while the agent is busy; use turn.steer explicitly."),
1979
- {
1980
- code: "busy",
1981
- },
1982
- );
1983
2116
  const promptImages = Array.isArray(images) ? (images as { data: string; mimeType?: string }[]) : [];
1984
2117
  const content: string | (TextContent | ImageContent)[] =
1985
2118
  promptImages.length > 0
@@ -2008,12 +2141,17 @@ function sdkControlSurface(
2008
2141
  error: Object.assign(new Error("Prompt preflight was cancelled before execution."), { code: "busy" }),
2009
2142
  });
2010
2143
  pendingPreflightCancellations.add(cancelPreflight);
2011
- const onPreflightAccepted = () => {
2144
+ const settleAccepted = async () => {
2012
2145
  if (preflightSettled) return;
2013
2146
  accepted = true;
2014
- onPromptAccepted(correlation, requesterConnectionId);
2147
+ await onPromptAccepted(correlation, requesterConnectionId, trimmedClientRef, trackReconciliation);
2015
2148
  settlePreflight({ status: "accepted" });
2016
2149
  };
2150
+ // Durable fence preferred; keep legacy onPreflightAccepted for hosts/tests that only fire the sync hook.
2151
+ const onPreflightAcceptCommit = settleAccepted;
2152
+ const onPreflightAccepted = () => {
2153
+ void settleAccepted();
2154
+ };
2017
2155
  // Do not acknowledge the prompt until AgentSession's async preflight
2018
2156
  // succeeds. The terminal result records correlation before agent_start can fire.
2019
2157
  let submission: Promise<void> | undefined;
@@ -2021,6 +2159,7 @@ function sdkControlSurface(
2021
2159
  submission = Promise.resolve(
2022
2160
  api.sendUserMessage(content, {
2023
2161
  ...(deliverAs ? { deliverAs } : !forceFresh && isBusy() ? { deliverAs: "steer" as const } : {}),
2162
+ onPreflightAcceptCommit,
2024
2163
  onPreflightAccepted,
2025
2164
  }),
2026
2165
  );
@@ -2048,13 +2187,17 @@ function sdkControlSurface(
2048
2187
  try {
2049
2188
  const result = await preflight.promise;
2050
2189
  if (result.status === "rejected") throw result.error;
2051
- return { commandId, turnId, accepted: true };
2190
+ return { commandId, turnId, accepted: true, ...(trimmedClientRef ? { clientRef: trimmedClientRef } : {}) };
2191
+ } catch (error) {
2192
+ if (trackReconciliation) releasePromptAdmission(trimmedClientRef);
2193
+ throw error;
2052
2194
  } finally {
2053
2195
  pendingPreflightCancellations.delete(cancelPreflight);
2054
2196
  }
2055
2197
  };
2056
2198
  const surface: ControlSurface & { cancelPendingPreflights(): void } = {
2057
- prompt: (text, images) => submitPrompt(text, images, false, undefined, true, controlRequesterContext.getStore()),
2199
+ prompt: (text, images, clientRef) =>
2200
+ submitPrompt(text, images, false, undefined, true, controlRequesterContext.getStore(), clientRef, true),
2058
2201
  steer: text => sendSteer(text),
2059
2202
  followUp: text => submitPrompt(text, undefined, false, "followUp", false, controlRequesterContext.getStore()),
2060
2203
  abort: () => {
@@ -2227,13 +2370,84 @@ function sdkControlSurface(
2227
2370
  code: "terminal_uncertain",
2228
2371
  });
2229
2372
  },
2230
- invokeSkill: (name, args) => {
2373
+ invokeSkill: async (name, args, clientRef) => {
2231
2374
  if (!bindings.has("invokeSkill") || !ctx.invokeSkill)
2232
2375
  return unavailable("skill.invoke", "no skill invocation seam is installed")();
2233
2376
 
2234
2377
  if (typeof args !== "undefined" && typeof args !== "string")
2235
2378
  throw Object.assign(new Error("skill.invoke args must be a string."), { code: "invalid_input" });
2236
- return ctx.invokeSkill(name, args);
2379
+ const trimmedClientRef =
2380
+ typeof clientRef === "string" ? clientRef.trim() : clientRef === undefined ? undefined : "";
2381
+ if (clientRef !== undefined && (!trimmedClientRef || trimmedClientRef.length > 128))
2382
+ throw Object.assign(new Error("clientRef must be a non-empty string of at most 128 characters."), {
2383
+ code: "invalid_input",
2384
+ });
2385
+ const commandId = crypto.randomUUID();
2386
+ const turnId = crypto.randomUUID();
2387
+ const correlation = { commandId, turnId };
2388
+ if (skillRecon) skillRecon.admit(trimmedClientRef);
2389
+ const { promise: acceptedP, resolve, reject } = Promise.withResolvers<Record<string, unknown>>();
2390
+ let phase: "pending" | "accepted" | "rejected" = "pending";
2391
+ const settleAccept = (value: Record<string, unknown>) => {
2392
+ if (phase !== "pending") return;
2393
+ phase = "accepted";
2394
+ resolve(value);
2395
+ };
2396
+ const settleReject = (error: unknown) => {
2397
+ if (phase !== "pending") return;
2398
+ phase = "rejected";
2399
+ reject(error);
2400
+ };
2401
+ let prepared: { name: string; path: string; lineCount?: number; cleanedArgs?: string } | undefined;
2402
+ const run = Promise.resolve(
2403
+ ctx.invokeSkill(name, args as string | undefined, {
2404
+ onSkillPrepared: meta => {
2405
+ prepared = meta;
2406
+ },
2407
+ onPreflightAcceptCommit: async () => {
2408
+ const meta = prepared ?? { name: String(name), path: "" };
2409
+ if (skillRecon)
2410
+ await skillRecon.noteAccepted(correlation, trimmedClientRef, { skillName: meta.name });
2411
+ settleAccept({
2412
+ accepted: true,
2413
+ commandId,
2414
+ turnId,
2415
+ name: meta.name,
2416
+ path: meta.path,
2417
+ ...(meta.lineCount !== undefined ? { lineCount: meta.lineCount } : {}),
2418
+ ...(meta.cleanedArgs !== undefined ? { args: meta.cleanedArgs } : {}),
2419
+ ...(trimmedClientRef ? { clientRef: trimmedClientRef } : {}),
2420
+ });
2421
+ },
2422
+ }),
2423
+ ).then(
2424
+ result => {
2425
+ if (phase === "pending") {
2426
+ // Completed without fence (e.g. queue path) — still surface result.
2427
+ settleAccept({
2428
+ accepted: true,
2429
+ commandId,
2430
+ turnId,
2431
+ ...(typeof result === "object" && result ? (result as object) : {}),
2432
+ ...(trimmedClientRef ? { clientRef: trimmedClientRef } : {}),
2433
+ });
2434
+ } else if (skillRecon) {
2435
+ void skillRecon.noteTransition(correlation, { type: "agent_end" });
2436
+ }
2437
+ return result;
2438
+ },
2439
+ error => {
2440
+ if (phase === "pending") {
2441
+ if (skillRecon) skillRecon.release(trimmedClientRef);
2442
+ settleReject(error);
2443
+ } else if (skillRecon) {
2444
+ void skillRecon.noteTransition(correlation, { type: "agent_failed", error });
2445
+ }
2446
+ throw error;
2447
+ },
2448
+ );
2449
+ void run.catch(() => {});
2450
+ return await acceptedP;
2237
2451
  },
2238
2452
  setPlanMode: async on => {
2239
2453
  if (!bindings.has("setPlanMode") || !ctx.setPlanMode)
@@ -2266,6 +2480,14 @@ function sdkControlSurface(
2266
2480
  );
2267
2481
  return typed("model.set", { id: `${model.provider}/${model.id}`, thinkingLevel });
2268
2482
  },
2483
+ setModelProfile: async id => {
2484
+ if (!bindings.has("setModelProfile") || !ctx.setModelProfile)
2485
+ return unavailable("model.profile.set", "no model-profile activation seam is installed")();
2486
+ if (!id) throw Object.assign(new Error("model.profile.set requires a profile id."), { code: "invalid_input" });
2487
+ if (!ctx.modelRegistry.getModelProfile(id))
2488
+ throw Object.assign(new Error(`Model profile ${id} was not found.`), { code: "invalid_input" });
2489
+ return { changed: await ctx.setModelProfile(id), id };
2490
+ },
2269
2491
  cycleModel: async () => {
2270
2492
  if (!bindings.has("cycleModel"))
2271
2493
  return unavailable("model.cycle", "no session model-cycle seam is installed")();
@@ -2761,6 +2983,18 @@ export async function ensureConfiguredProviderDaemons(
2761
2983
  if (isSlackConfigured(cfg)) await ensureProviderDaemon("slack", settings);
2762
2984
  }
2763
2985
 
2986
+ /**
2987
+ * Classify whether a session identity event must await notification endpoint startup.
2988
+ * Only an interactive selector resume may defer this ancillary startup; all other
2989
+ * events, including branches and unknown origins, fail closed to awaiting it.
2990
+ */
2991
+ export function shouldAwaitNotificationStartup(event: {
2992
+ type: "session_switch" | "session_branch";
2993
+ transition?: { origin: string };
2994
+ }): boolean {
2995
+ return event.type !== "session_switch" || event.transition?.origin !== INTERACTIVE_SELECTOR_RESUME_ORIGIN;
2996
+ }
2997
+
2764
2998
  export function createNotificationsExtension(
2765
2999
  api: ExtensionAPI,
2766
3000
  options: {
@@ -2769,6 +3003,7 @@ export function createNotificationsExtension(
2769
3003
  settings: Settings;
2770
3004
  cwd: string;
2771
3005
  sessionId: string;
3006
+ onRegistered?: (registration: RegisterNotificationRootResult) => void;
2772
3007
  }) => Promise<EnsureDaemonResult>;
2773
3008
  ensureProviderDaemon?: (provider: "discord" | "slack", settings: Settings) => Promise<unknown>;
2774
3009
  /** Suppress auto-delivery for a SKC-spawned child under `sessionScope=primary`. */
@@ -2778,7 +3013,7 @@ export function createNotificationsExtension(
2778
3013
  sdkHostModeSupported?: boolean;
2779
3014
 
2780
3015
  onSdkRequest?: (kind: "control" | "query", connectionId: string, frame: Record<string, unknown>) => void;
2781
- runEphemeralTurn?: (promptText: string, signal: AbortSignal) => Promise<{ replyText: string }>;
3016
+ runBtwTurn?: (question: string, signal: AbortSignal) => Promise<{ replyText: string }>;
2782
3017
  /** Observes settlement of optional session-branch startup after reconciliation completes. */
2783
3018
  onBranchStartupSettled?: (receipt: { sessionId: string; status: SessionStartResult["status"] }) => void;
2784
3019
  readNotificationFile?: (path: string) => Promise<Buffer>;
@@ -2799,10 +3034,15 @@ export function createNotificationsExtension(
2799
3034
  const cleanupRetries = new Map<string, SessionRuntime>();
2800
3035
  const sessionStartPromises = new Map<string, Promise<SessionStartResult>>();
2801
3036
  const branchStartupTasks = new Set<Promise<void>>();
3037
+ const sessionLifecycleTasks = new Set<Promise<void>>();
2802
3038
  let activeRuntimeId: string | undefined;
2803
3039
  let identityControlInFlight = false;
2804
3040
  let deferredIdentityRotation:
2805
- | { event: { previousSessionFile?: string }; ctx: ExtensionContext; awaitStartup: boolean }
3041
+ | {
3042
+ event: { previousSessionFile?: string; transition?: { origin: string } };
3043
+ ctx: ExtensionContext;
3044
+ awaitStartup: boolean;
3045
+ }
2806
3046
  | undefined;
2807
3047
  let extensionShuttingDown = false;
2808
3048
 
@@ -2810,13 +3050,24 @@ export function createNotificationsExtension(
2810
3050
  settings: Settings,
2811
3051
  cwd: string,
2812
3052
  id: string,
3053
+ onRegistered?: (registrationToken: string) => void,
2813
3054
  ): Promise<"ready" | "blocked_identity"> {
2814
3055
  if (options.ensureTelegramDaemon) {
2815
- return (await options.ensureTelegramDaemon({ settings, cwd, sessionId: id })) === "blocked"
3056
+ return (await options.ensureTelegramDaemon({
3057
+ settings,
3058
+ cwd,
3059
+ sessionId: id,
3060
+ onRegistered: registration => onRegistered?.(registration.token),
3061
+ })) === "blocked"
2816
3062
  ? "blocked_identity"
2817
3063
  : "ready";
2818
3064
  }
2819
- return (await ensureTelegramDaemonRunningDetailed({ settings, cwd, sessionId: id })) === "blocked_identity"
3065
+ return (await ensureTelegramDaemonRunningDetailed({
3066
+ settings,
3067
+ cwd,
3068
+ sessionId: id,
3069
+ onRegistered: registration => onRegistered?.(registration.token),
3070
+ })) === "blocked_identity"
2820
3071
  ? "blocked_identity"
2821
3072
  : "ready";
2822
3073
  }
@@ -2825,9 +3076,10 @@ export function createNotificationsExtension(
2825
3076
  cfg: NotificationConfig,
2826
3077
  cwd: string,
2827
3078
  id: string,
3079
+ onRegistered?: (registrationToken: string) => void,
2828
3080
  ): Promise<boolean> {
2829
3081
  if (isTelegramConfigured(cfg)) {
2830
- if ((await ensureTelegramOwner(settings, cwd, id)) === "blocked_identity") return false;
3082
+ if ((await ensureTelegramOwner(settings, cwd, id, onRegistered)) === "blocked_identity") return false;
2831
3083
  }
2832
3084
  await ensureConfiguredProviderDaemons(settings, cfg, options.ensureProviderDaemon);
2833
3085
  return true;
@@ -2840,7 +3092,6 @@ export function createNotificationsExtension(
2840
3092
  "session.branch",
2841
3093
  ]);
2842
3094
  const sessionId = (ctx: ExtensionContext): string => ctx.sessionManager.getSessionId();
2843
- const sleep = (ms: number): Promise<void> => new Promise(resolve => setTimeout(resolve, ms));
2844
3095
 
2845
3096
  async function stopSession(
2846
3097
  id: string,
@@ -2852,6 +3103,7 @@ export function createNotificationsExtension(
2852
3103
  const requestedRuntime = retryRuntime ?? activeRuntime;
2853
3104
  if (expectedRuntime && requestedRuntime !== expectedRuntime) return false;
2854
3105
  if (reason === "session" && requestedRuntime) {
3106
+ requestedRuntime.inboundFenced = true;
2855
3107
  requestedRuntime.stopping = true;
2856
3108
  requestedRuntime.abortEphemeralTurns();
2857
3109
  }
@@ -2915,6 +3167,12 @@ export function createNotificationsExtension(
2915
3167
  try {
2916
3168
  rt.workflowGate?.setRuntimeTurnProvider?.(null);
2917
3169
  } catch {}
3170
+ try {
3171
+ const delivered = await pushTerminalSessionFrame(rt, { type: "session_closed", sessionId: id });
3172
+ if (!delivered) logger.warn("notifications: session_closed socket delivery was not acknowledged");
3173
+ } catch (e) {
3174
+ logger.warn(`notifications: session_closed failed: ${String(e)}`);
3175
+ }
2918
3176
  await rt.waitForGateResolutionQuiescence();
2919
3177
  try {
2920
3178
  rt.disposeAckRecoveryParticipant();
@@ -2956,14 +3214,6 @@ export function createNotificationsExtension(
2956
3214
  ownerReleaseFailures.push(e);
2957
3215
  logger.warn(`sdk query snapshots: close failed: ${String(e)}`);
2958
3216
  }
2959
- let closeFrameSent = false;
2960
- try {
2961
- pushSessionFrame(rt, { type: "session_closed", sessionId: id });
2962
- closeFrameSent = true;
2963
- } catch (e) {
2964
- logger.warn(`notifications: session_closed failed: ${String(e)}`);
2965
- }
2966
- if (closeFrameSent) await sleep(100);
2967
3217
  let serverStopped = rt.serverStopped;
2968
3218
  if (!serverStopped) {
2969
3219
  try {
@@ -2982,6 +3232,20 @@ export function createNotificationsExtension(
2982
3232
  hostStopped: rt.hostStopped && rt.serverStopped,
2983
3233
  brokerRegistrationReleased: rt.brokerRegistrationReleased,
2984
3234
  });
3235
+ if (rt.notificationRootRegistration) {
3236
+ try {
3237
+ await unregisterNotificationRoot({
3238
+ settings: rt.notificationRootRegistration.settings,
3239
+ cwd: rt.notificationRootRegistration.cwd,
3240
+ sessionId: id,
3241
+ registrationToken: rt.notificationRootRegistration.registrationToken,
3242
+ });
3243
+ rt.notificationRootRegistration = undefined;
3244
+ } catch (e) {
3245
+ ownerReleaseFailures.push(e);
3246
+ logger.warn(`notifications: Telegram root unregister failed: ${String(e)}`);
3247
+ }
3248
+ }
2985
3249
  if (ownerReleaseFailures.length > 0) {
2986
3250
  cleanupRetries.set(id, rt);
2987
3251
  throw new AggregateError(ownerReleaseFailures, `SDK notification runtime ${id} owner release failed.`);
@@ -3130,7 +3394,13 @@ export function createNotificationsExtension(
3130
3394
  const promptSubmissionKey = (correlation: { commandId: string; turnId: string }) =>
3131
3395
  `${correlation.commandId}:${correlation.turnId}`;
3132
3396
  type PromptLifecycleFrame =
3133
- | { type: "agent_start" | "agent_end"; sessionId: string; commandId?: string; turnId?: string }
3397
+ | {
3398
+ type: "agent_start" | "agent_end";
3399
+ sessionId: string;
3400
+ commandId?: string;
3401
+ turnId?: string;
3402
+ finalText?: string;
3403
+ }
3134
3404
  | {
3135
3405
  type: "agent_failed";
3136
3406
  sessionId: string;
@@ -3145,11 +3415,39 @@ export function createNotificationsExtension(
3145
3415
  failed: boolean;
3146
3416
  error: unknown;
3147
3417
  terminal: boolean;
3418
+ retainCorrelation: boolean;
3148
3419
  createdAt: number;
3149
3420
  bufferedFrames: Array<PromptLifecycleFrame | Record<string, unknown>>;
3150
3421
  };
3151
3422
  const promptSubmissions = new Map<string, PromptSubmission>();
3152
3423
  const promptTerminalTombstones = new Map<string, number>();
3424
+ // Authoritative bounded reconciliation state for Q26 turn.prompt_status
3425
+ // (contract documented in ./prompt-reconciliation and ../prompt-status).
3426
+ // Active records never age into terminal; documented TTL/capacity
3427
+ // eviction is the only removal, after which lookups report `unknown`.
3428
+ const sessionFile =
3429
+ typeof ctx.sessionManager?.getSessionFile === "function" ? ctx.sessionManager.getSessionFile() : null;
3430
+ const reconciliationSessionId =
3431
+ typeof ctx.sessionManager?.getSessionId === "function" ? ctx.sessionManager.getSessionId() : "";
3432
+ const durableStore =
3433
+ sessionFile && reconciliationSessionId
3434
+ ? createReconciliationStore({ sessionFile, sessionId: String(reconciliationSessionId) })
3435
+ : null;
3436
+ const kindReconciliation = createKindAwareReconciliation({ store: durableStore });
3437
+ if (durableStore) void kindReconciliation.hydrateFromStore();
3438
+ // Backward-compatible process-local prompt reconciler kept for unit-test isolation;
3439
+ // production path uses kindReconciliation for prompt+skill.
3440
+ const reconciliation = createPromptReconciliation();
3441
+ const admitPromptSubmission = (clientRef?: string) => kindReconciliation.admit("prompt", clientRef);
3442
+ const notePromptReconciliationAccepted = async (
3443
+ correlation: { commandId: string; turnId: string },
3444
+ clientRef?: string,
3445
+ ) => {
3446
+ await kindReconciliation.noteAccepted("prompt", correlation, clientRef);
3447
+ };
3448
+ const lookupPromptStatus = (selector: { commandId?: string; turnId?: string; clientRef?: string }) =>
3449
+ kindReconciliation.lookup("prompt", selector);
3450
+ const releasePromptAdmission = (clientRef?: string) => kindReconciliation.releaseAdmission("prompt", clientRef);
3153
3451
  const removePendingPromptCorrelation = (correlation: { commandId: string; turnId: string }) => {
3154
3452
  const pendingIndex = pendingPromptCorrelations.findIndex(
3155
3453
  candidate => candidate.commandId === correlation.commandId && candidate.turnId === correlation.turnId,
@@ -3167,14 +3465,21 @@ export function createNotificationsExtension(
3167
3465
  removePendingPromptCorrelation(correlation);
3168
3466
  addTerminalTombstone(key);
3169
3467
  };
3468
+ const expirePromptDelivery = (key: string, submission: PromptSubmission) => {
3469
+ promptSubmissions.delete(key);
3470
+ if (!submission.terminal && submission.retainCorrelation) return;
3471
+ const [commandId, turnId] = key.split(":", 2);
3472
+ if (!commandId || !turnId) return;
3473
+ removePendingPromptCorrelation({ commandId, turnId });
3474
+ addTerminalTombstone(key);
3475
+ };
3170
3476
  const cleanupPromptRecords = (now = Date.now()) => {
3477
+ kindReconciliation.cleanup();
3478
+ reconciliation.cleanup();
3171
3479
  for (const [key, expiresAt] of promptTerminalTombstones)
3172
3480
  if (expiresAt <= now) promptTerminalTombstones.delete(key);
3173
3481
  for (const [key, submission] of promptSubmissions)
3174
- if (submission.createdAt + PROMPT_SUBMISSION_TTL_MS <= now) {
3175
- const [commandId, turnId] = key.split(":", 2);
3176
- if (commandId && turnId) finalizePrompt(key, { commandId, turnId });
3177
- }
3482
+ if (submission.createdAt + PROMPT_SUBMISSION_TTL_MS <= now) expirePromptDelivery(key, submission);
3178
3483
  };
3179
3484
  const abandonPrompt = (submission: PromptSubmission) => {
3180
3485
  submission.abandoned = true;
@@ -3185,6 +3490,7 @@ export function createNotificationsExtension(
3185
3490
  frame: PromptLifecycleFrame,
3186
3491
  ) => {
3187
3492
  cleanupPromptRecords();
3493
+
3188
3494
  if (!correlation || !runtime) {
3189
3495
  emitAgentLifecycle(runtime!, frame as Extract<PromptLifecycleFrame, { type: "agent_start" | "agent_end" }>);
3190
3496
  return;
@@ -3247,18 +3553,25 @@ export function createNotificationsExtension(
3247
3553
  if (commandId && turnId) finalizePrompt(key, { commandId, turnId });
3248
3554
  }
3249
3555
  };
3250
- const recordPromptAccepted = (
3556
+ const recordPromptAccepted = async (
3251
3557
  correlation: { commandId: string; turnId: string },
3252
3558
  requesterConnectionId?: string,
3559
+ clientRef?: string,
3560
+ trackReconciliation = false,
3253
3561
  ) => {
3254
- if (!requesterConnectionId) return;
3562
+ if (!requesterConnectionId) {
3563
+ // No delivery owner: tracked prompts cannot be reconciled. Release
3564
+ // their admission reservation instead of leaking the active slot.
3565
+ if (trackReconciliation) releasePromptAdmission(clientRef);
3566
+ return;
3567
+ }
3568
+ // Register delivery ownership synchronously before any await so post-accept
3569
+ // failures that race the durable write still emit correlated terminals.
3255
3570
  cleanupPromptRecords();
3256
3571
  while (promptSubmissions.size >= PROMPT_SUBMISSION_CAPACITY) {
3257
3572
  const oldest = promptSubmissions.entries().next().value as [string, PromptSubmission] | undefined;
3258
3573
  if (!oldest) break;
3259
- const [key] = oldest;
3260
- const [commandId, turnId] = key.split(":", 2);
3261
- if (commandId && turnId) finalizePrompt(key, { commandId, turnId });
3574
+ expirePromptDelivery(oldest[0], oldest[1]);
3262
3575
  }
3263
3576
  pendingPromptCorrelations.push(correlation);
3264
3577
  promptSubmissions.set(promptSubmissionKey(correlation), {
@@ -3268,9 +3581,11 @@ export function createNotificationsExtension(
3268
3581
  failed: false,
3269
3582
  error: undefined,
3270
3583
  terminal: false,
3584
+ retainCorrelation: trackReconciliation,
3271
3585
  createdAt: Date.now(),
3272
3586
  bufferedFrames: [],
3273
3587
  });
3588
+ if (trackReconciliation) await notePromptReconciliationAccepted(correlation, clientRef);
3274
3589
  };
3275
3590
  const recordPromptTerminal = (correlation: { commandId: string; turnId: string } | undefined) => {
3276
3591
  if (!correlation) return false;
@@ -3283,17 +3598,15 @@ export function createNotificationsExtension(
3283
3598
  return true;
3284
3599
  };
3285
3600
  const emitPromptFailure = (correlation: { commandId: string; turnId: string }, error: unknown) => {
3601
+ const sanitized = sanitizePromptFailure(error);
3602
+ void kindReconciliation.noteTransition("prompt", correlation, { type: "agent_failed", error: sanitized });
3286
3603
  const submission = promptSubmissions.get(promptSubmissionKey(correlation));
3287
3604
  if (!submission || !runtime || !recordPromptTerminal(correlation)) return;
3288
- const candidate = error as { code?: unknown; message?: unknown };
3289
3605
  emitPromptLifecycle(correlation, {
3290
3606
  type: "agent_failed",
3291
3607
  sessionId: runtime.id,
3292
3608
  ...correlation,
3293
- error: {
3294
- code: typeof candidate.code === "string" ? candidate.code : "internal",
3295
- message: typeof candidate.message === "string" ? candidate.message : "Prompt submission failed.",
3296
- },
3609
+ error: sanitized,
3297
3610
  });
3298
3611
  };
3299
3612
  const recordPromptFailure = (correlation: { commandId: string; turnId: string }, error: unknown) => {
@@ -3336,6 +3649,8 @@ export function createNotificationsExtension(
3336
3649
  };
3337
3650
  },
3338
3651
  configOverrides,
3652
+ lookupPromptStatus,
3653
+ selector => kindReconciliation.lookup("skill", selector),
3339
3654
  ),
3340
3655
  id,
3341
3656
  revisions,
@@ -3351,9 +3666,19 @@ export function createNotificationsExtension(
3351
3666
  recordPromptFailure,
3352
3667
  () => runtime?.stopping !== true,
3353
3668
  trackGateResolution,
3669
+ admitPromptSubmission,
3670
+ releasePromptAdmission,
3354
3671
  settings,
3355
3672
  configOverrides,
3356
3673
  configRevision,
3674
+ {
3675
+ admit: (clientRef?: string) => kindReconciliation.admit("skill", clientRef),
3676
+ release: (clientRef?: string) => kindReconciliation.releaseAdmission("skill", clientRef),
3677
+ noteAccepted: (correlation, clientRef, extra) =>
3678
+ kindReconciliation.noteAccepted("skill", correlation, clientRef, extra),
3679
+ noteTransition: (correlation, frame) => kindReconciliation.noteTransition("skill", correlation, frame),
3680
+ lookup: selector => kindReconciliation.lookup("skill", selector),
3681
+ },
3357
3682
  );
3358
3683
  const abandonPromptResponse = (connectionId: string, frame: Record<string, unknown>) => {
3359
3684
  if (
@@ -3409,7 +3734,7 @@ export function createNotificationsExtension(
3409
3734
  stateRoot,
3410
3735
  token,
3411
3736
  sendFrame: (connectionId, frame) => sendSdkFrame(connectionId, frame),
3412
- connectionCapabilities: connectionId => hostCapCache.get(connectionId) ?? EMPTY_CAPABILITIES,
3737
+ connectionCapabilities: connectionId => hostCapCache.get(connectionId),
3413
3738
  installProviderDefinitions,
3414
3739
  onProviderDefinitionsRemoved: removeProviderDefinitions,
3415
3740
  onFrame: handler => {
@@ -3419,6 +3744,75 @@ export function createNotificationsExtension(
3419
3744
  };
3420
3745
  },
3421
3746
  onRequest: options.onSdkRequest,
3747
+ beforeControlResponse: async (_connectionId, request, response, sendTerminal) => {
3748
+ if (typeof request.operation !== "string" || !identityControlOperations.has(request.operation)) return;
3749
+ const pending = deferredIdentityRotation;
3750
+ deferredIdentityRotation = undefined;
3751
+ identityControlInFlight = false;
3752
+ if (response.ok !== true || !pending) return;
3753
+
3754
+ const predecessorId = activeRuntimeId ?? sessionIdFromFile(pending.event.previousSessionFile);
3755
+ if (!predecessorId) throw new Error("notifications: identity control predecessor is unknown.");
3756
+ let terminalAttempted = false;
3757
+ let terminalOutcome: TerminalSendOutcome | undefined;
3758
+ try {
3759
+ terminalOutcome = await runIdentityControlSuccessPath({
3760
+ fence: () => {
3761
+ const predecessor = runtimes.get(predecessorId);
3762
+ if (!predecessor || predecessor.stopping || predecessor.serverStopped)
3763
+ throw new Error(`notifications: predecessor runtime ${predecessorId} cannot be fenced.`);
3764
+ predecessor.inboundFenced = true;
3765
+ },
3766
+ ensurePredecessorSendCapable: () => {
3767
+ const predecessor = runtimes.get(predecessorId);
3768
+ if (!predecessor || predecessor.stopping || predecessor.serverStopped)
3769
+ throw new Error(`notifications: predecessor runtime ${predecessorId} is not send-capable.`);
3770
+ },
3771
+ startSuccessor: async () => {
3772
+ const successorId = sessionId(pending.ctx);
3773
+ try {
3774
+ await rotateSessionAuthority(pending.event, pending.ctx, true, {
3775
+ deferPredecessorStop: true,
3776
+ });
3777
+ const successor = runtimes.get(successorId);
3778
+ if (!successor?.host.started || activeRuntimeId !== successorId)
3779
+ throw new Error(`notifications: successor runtime ${successorId} was not ready.`);
3780
+ } catch (error) {
3781
+ (response as Record<string, unknown>).ok = false;
3782
+ delete (response as Record<string, unknown>).result;
3783
+ (response as Record<string, unknown>).error = {
3784
+ code: "unavailable",
3785
+ message: error instanceof Error ? error.message : "Successor session was not ready.",
3786
+ };
3787
+ }
3788
+ },
3789
+ sendTerminal: async () => {
3790
+ terminalAttempted = true;
3791
+ try {
3792
+ await sendTerminal();
3793
+ return "written";
3794
+ } catch {
3795
+ return "write_failed";
3796
+ }
3797
+ },
3798
+ stopPredecessor: async () => {
3799
+ try {
3800
+ await stopSession(predecessorId);
3801
+ } catch (error) {
3802
+ if (!terminalAttempted) throw error;
3803
+ logger.error(`notifications: deferred predecessor cleanup failed: ${String(error)}`);
3804
+ }
3805
+ },
3806
+ requireNativeControlDrain:
3807
+ (request as { requireNativeControlDrain?: unknown }).requireNativeControlDrain === true,
3808
+ });
3809
+ } catch (error) {
3810
+ if (!terminalAttempted) throw error;
3811
+ logger.error(
3812
+ `notifications: identity terminal delivery failed (${terminalOutcome ?? "unknown"}): ${String(error)}`,
3813
+ );
3814
+ }
3815
+ },
3422
3816
  afterControlResponse: async (connectionId, request, response) => {
3423
3817
  if (
3424
3818
  (request.operation === "turn.prompt" ||
@@ -3439,13 +3833,6 @@ export function createNotificationsExtension(
3439
3833
  }
3440
3834
 
3441
3835
  if (request.operation === "session.close" && response.ok === true) ctx.shutdown();
3442
- if (typeof request.operation === "string" && identityControlOperations.has(request.operation)) {
3443
- const pending = deferredIdentityRotation;
3444
- deferredIdentityRotation = undefined;
3445
- identityControlInFlight = false;
3446
- if (response.ok === true && pending)
3447
- await rotateSessionAuthority(pending.event, pending.ctx, pending.awaitStartup);
3448
- }
3449
3836
  },
3450
3837
  control: async (connectionId, frame) => {
3451
3838
  const request = frame as {
@@ -3465,6 +3852,21 @@ export function createNotificationsExtension(
3465
3852
  ok: false,
3466
3853
  error: { code: "conflict", message: "session identity mutation is already active" },
3467
3854
  };
3855
+ const requireNativeControlDrain =
3856
+ (request as { requireNativeControlDrain?: unknown }).requireNativeControlDrain === true ||
3857
+ (!!request.input &&
3858
+ typeof request.input === "object" &&
3859
+ !Array.isArray(request.input) &&
3860
+ (request.input as { requireNativeControlDrain?: unknown }).requireNativeControlDrain === true);
3861
+ if (requireNativeControlDrain && !isNativeControlDrainAvailable())
3862
+ return {
3863
+ id: requestId,
3864
+ ok: false,
3865
+ error: {
3866
+ code: "unavailable",
3867
+ message: "SDK identity control requires the native control-drain lease.",
3868
+ },
3869
+ };
3468
3870
  if (rotatesIdentity) identityControlInFlight = true;
3469
3871
  const response = await controlRequesterContext.run(connectionId, () =>
3470
3872
  dispatchControl(
@@ -3481,6 +3883,7 @@ export function createNotificationsExtension(
3481
3883
  },
3482
3884
  ),
3483
3885
  );
3886
+
3484
3887
  if (rotatesIdentity && response.ok !== true) {
3485
3888
  identityControlInFlight = false;
3486
3889
  deferredIdentityRotation = undefined;
@@ -3535,6 +3938,7 @@ export function createNotificationsExtension(
3535
3938
  workflowGate: undefined,
3536
3939
  gatePresentations,
3537
3940
  stopping: false,
3941
+ inboundFenced: false,
3538
3942
  abortEphemeralTurns: () => {},
3539
3943
  disableEphemeralTurns: () => {},
3540
3944
  cancelPostmortemCleanup: () => {},
@@ -3550,12 +3954,20 @@ export function createNotificationsExtension(
3550
3954
  pendingPromptCorrelations,
3551
3955
  activePromptCorrelation: undefined,
3552
3956
  recordPromptTerminal,
3957
+ notePromptReconciliation: (correlation, frame) => {
3958
+ void kindReconciliation.noteTransition("prompt", correlation, frame);
3959
+ },
3960
+ emitPromptFailure,
3553
3961
  emitPromptLifecycle,
3554
3962
  emitPromptEvent,
3555
3963
  pendingInbound: new Set<number>(),
3556
- inFlightTools: new Map<string, { toolName: string; args: unknown }>(),
3964
+ inFlightTools: new Map<
3965
+ string,
3966
+ { toolName: string; args?: unknown; pendingPhase?: "completed" | "failed" | "cancelled" }
3967
+ >(),
3557
3968
  deferredGatePresentations: [],
3558
3969
  deferredInboundControls: [],
3970
+ notificationRootRegistration: undefined,
3559
3971
  };
3560
3972
  const initializedRuntime = runtime;
3561
3973
  runtimes.set(id, initializedRuntime);
@@ -3589,10 +4001,10 @@ export function createNotificationsExtension(
3589
4001
  };
3590
4002
 
3591
4003
  const ephemeralTurns = new EphemeralTurnHost(sendSdkFrame, async (question, signal) => {
3592
- if (!options.runEphemeralTurn) throw new Error("Ephemeral turns are unavailable.");
4004
+ if (!options.runBtwTurn) throw new Error("Ephemeral turns are unavailable.");
3593
4005
  const generation = initializedRuntime.policyGeneration;
3594
4006
  if (initializedRuntime.policySuspended) throw new Error("Notification policy is provisional.");
3595
- const result = await options.runEphemeralTurn(prompt.render(btwUserPrompt, { question }), signal);
4007
+ const result = await options.runBtwTurn(question, signal);
3596
4008
  if (
3597
4009
  initializedRuntime.policySuspended ||
3598
4010
  initializedRuntime.policyGeneration !== generation ||
@@ -3610,7 +4022,30 @@ export function createNotificationsExtension(
3610
4022
  const frame = JSON.parse(inbound.json) as unknown;
3611
4023
  if (!frame || typeof frame !== "object") return;
3612
4024
  const typedFrame = frame as Record<string, unknown>;
4025
+ if (initializedRuntime.inboundFenced) {
4026
+ if (typedFrame.type === "control_request" && typeof typedFrame.id === "string") {
4027
+ try {
4028
+ server.sendTo(
4029
+ inbound.connectionId,
4030
+ JSON.stringify({
4031
+ type: "control_response",
4032
+ id: typedFrame.id,
4033
+ ok: false,
4034
+ error: { code: "endpoint_stale", message: "session endpoint is draining." },
4035
+ }),
4036
+ );
4037
+ } catch {}
4038
+ }
4039
+ return;
4040
+ }
3613
4041
  if (typedFrame.type === "ephemeral_turn" || typedFrame.type === "ephemeral_turn_cancel") return;
4042
+ if (typedFrame.type === "event_replay") {
4043
+ const capabilities = Array.isArray(typedFrame.capabilities) ? typedFrame.capabilities : [];
4044
+ hostCapCache.set(
4045
+ inbound.connectionId,
4046
+ new Set(capabilities.filter((capability): capability is string => typeof capability === "string")),
4047
+ );
4048
+ }
3614
4049
  inboundSdkFrame?.(inbound.connectionId, typedFrame);
3615
4050
  } catch {}
3616
4051
  });
@@ -3637,7 +4072,12 @@ export function createNotificationsExtension(
3637
4072
 
3638
4073
  server.onReply((err, reply) => {
3639
4074
  if (err || !reply) return;
3640
- if (runtime?.stopping || runtime?.policySuspended || runtimes.get(id) !== runtime) {
4075
+ if (
4076
+ runtime?.inboundFenced ||
4077
+ runtime?.stopping ||
4078
+ runtime?.policySuspended ||
4079
+ runtimes.get(id) !== runtime
4080
+ ) {
3641
4081
  try {
3642
4082
  server.closeClaimInvalid(reply.replyReceiptId, "session_stopping");
3643
4083
  } catch {}
@@ -3751,7 +4191,7 @@ export function createNotificationsExtension(
3751
4191
  }
3752
4192
  const gate = runtime?.workflowGate;
3753
4193
  const workflowGateActive =
3754
- gate?.isUnattended?.() === true &&
4194
+ gate?.supportsRemoteGateAnswers?.() === true &&
3755
4195
  typeof gate.onGateEmitted === "function" &&
3756
4196
  typeof gate.resolveGateFromNotification === "function";
3757
4197
  const gateId = gatePresentations.routeFor(reply.id);
@@ -3879,6 +4319,7 @@ export function createNotificationsExtension(
3879
4319
  // thread (forwarded by the daemon over the WS, fail-closed at the daemon).
3880
4320
  server.onInbound((err, inbound) => {
3881
4321
  if (err || !inbound) return;
4322
+ if (initializedRuntime.inboundFenced) return;
3882
4323
  const authenticatedInbound = inbound as typeof inbound & {
3883
4324
  connectionId: string;
3884
4325
  messageId?: number;
@@ -3979,7 +4420,7 @@ export function createNotificationsExtension(
3979
4420
  }
3980
4421
  if (typeof inbound.redact === "boolean") {
3981
4422
  if (inbound.redact && !runtime.committedRedact) {
3982
- terminalizeInFlightTools(runtime, runtime.id, "unknown");
4423
+ terminalizeInFlightTools(runtime, runtime.id, "cancelled");
3983
4424
  }
3984
4425
  runtime.committedRedact = inbound.redact;
3985
4426
  runtime.redact = inbound.redact;
@@ -4038,12 +4479,19 @@ export function createNotificationsExtension(
4038
4479
  }
4039
4480
  if (notificationsEnabledForSession && settingsAvailable && settings) {
4040
4481
  try {
4041
- if (!(await ensureConfiguredDaemonOwners(settings, cfg, ctx.cwd, id))) {
4482
+ let registrationToken: string | undefined;
4483
+ if (
4484
+ !(await ensureConfiguredDaemonOwners(settings, cfg, ctx.cwd, id, token => {
4485
+ registrationToken = token;
4486
+ }))
4487
+ ) {
4042
4488
  const result = failLifecycleStartup("failed", "Telegram daemon ownership is blocked.");
4043
4489
  finishStartup(result);
4044
4490
  await cleanupAbandonedStartup();
4045
4491
  return result;
4046
4492
  }
4493
+ if (isTelegramConfigured(cfg))
4494
+ runtime.notificationRootRegistration = { settings, cwd: ctx.cwd, registrationToken };
4047
4495
  } catch (error) {
4048
4496
  const result = failLifecycleStartup("failed", error);
4049
4497
  finishStartup(result);
@@ -4204,7 +4652,9 @@ export function createNotificationsExtension(
4204
4652
  ? Gate
4205
4653
  : never,
4206
4654
  ): void => {
4207
- const options = (g.options ?? []).map(o => String((o as { label?: unknown }).label ?? ""));
4655
+ const rawGateOptions = g.options ?? [];
4656
+ const options = rawGateOptions.map(o => String((o as { label?: unknown }).label ?? ""));
4657
+ const recommendedIndex = recommendedIndexFromGateOptions(rawGateOptions);
4208
4658
  gateOptions.set(g.gate_id, options);
4209
4659
  const promptCtx = g.context as { prompt?: unknown; title?: unknown } | undefined;
4210
4660
  const question =
@@ -4221,6 +4671,7 @@ export function createNotificationsExtension(
4221
4671
  sessionId: id,
4222
4672
  question,
4223
4673
  options,
4674
+ ...(recommendedIndex === undefined ? {} : { recommendedIndex }),
4224
4675
  controls: [],
4225
4676
  multi: stageState.multi === true,
4226
4677
  allowEmpty: stageState.allow_empty === true,
@@ -4316,8 +4767,15 @@ export function createNotificationsExtension(
4316
4767
  runtime.redact = true;
4317
4768
  runtime.verbosity = "lean";
4318
4769
  runtime.stream = false;
4770
+ for (const [toolCallId, tool] of runtime.inFlightTools) {
4771
+ runtime.inFlightTools.set(toolCallId, {
4772
+ toolName: tool.toolName,
4773
+ ...(tool.pendingPhase ? { pendingPhase: tool.pendingPhase } : {}),
4774
+ });
4775
+ }
4319
4776
  return;
4320
4777
  }
4778
+ const wasPolicySuspended = runtime.policySuspended;
4321
4779
  const redactionEnabled = policy.redact && !runtime.committedRedact;
4322
4780
  runtime.policyGeneration++;
4323
4781
  runtime.committedRedact = policy.redact;
@@ -4325,7 +4783,8 @@ export function createNotificationsExtension(
4325
4783
  runtime.redact = policy.redact;
4326
4784
  runtime.verbosity = policy.verbosity;
4327
4785
  runtime.stream = policy.stream;
4328
- if (redactionEnabled) terminalizeInFlightTools(runtime, runtime.id, "unknown");
4786
+ if (redactionEnabled) terminalizeInFlightTools(runtime, runtime.id, "cancelled", true);
4787
+ else if (wasPolicySuspended && !policy.redact) settleProvisionalToolTerminals(runtime, runtime.id);
4329
4788
  },
4330
4789
  activate: binding => {
4331
4790
  const runtime = runtimes.get(binding.sessionId);
@@ -4339,7 +4798,23 @@ export function createNotificationsExtension(
4339
4798
  const { settings, settingsAvailable } = resolveSettings(options.settings);
4340
4799
  if (!settingsAvailable || !settings) return "blocked_identity";
4341
4800
  try {
4342
- return await ensureTelegramOwner(settings, binding.cwd, binding.sessionId);
4801
+ let registrationToken: string | undefined;
4802
+ const result = await ensureTelegramOwner(settings, binding.cwd, binding.sessionId, token => {
4803
+ registrationToken = token;
4804
+ });
4805
+ const runtime = runtimes.get(binding.sessionId);
4806
+ const configured = isTelegramConfigured(resolveSettings(options.settings).cfg);
4807
+ if (result === "ready" && runtime && !runtime.stopping && configured) {
4808
+ runtime.notificationRootRegistration = { settings, cwd: binding.cwd, registrationToken };
4809
+ } else if (registrationToken !== undefined) {
4810
+ await unregisterNotificationRoot({
4811
+ settings,
4812
+ cwd: binding.cwd,
4813
+ sessionId: binding.sessionId,
4814
+ registrationToken,
4815
+ });
4816
+ }
4817
+ return result;
4343
4818
  } catch {
4344
4819
  return "failed";
4345
4820
  }
@@ -4437,7 +4912,16 @@ export function createNotificationsExtension(
4437
4912
  };
4438
4913
 
4439
4914
  api.on("session_start", async (_event, ctx) => {
4440
- await startAndReconcileSession(ctx);
4915
+ const task = startAndReconcileSession(ctx);
4916
+ // Track full start+reconcile so settled startups join replacement-token
4917
+ // reconcile before owner release. Pending native startup (/notify on) stays
4918
+ // nonblocking: shutdown only awaits these tasks when sessionStartPromises is clear.
4919
+ sessionLifecycleTasks.add(task);
4920
+ try {
4921
+ await task;
4922
+ } finally {
4923
+ sessionLifecycleTasks.delete(task);
4924
+ }
4441
4925
  });
4442
4926
 
4443
4927
  // A session endpoint's token and generation are authority for exactly one
@@ -4483,10 +4967,27 @@ export function createNotificationsExtension(
4483
4967
  });
4484
4968
  };
4485
4969
 
4970
+ const preparePredecessorForTerminal = async (id: string): Promise<void> => {
4971
+ const predecessor = runtimes.get(id);
4972
+ if (!predecessor || cleanupRetries.has(id))
4973
+ throw new Error(`notifications: predecessor runtime ${id} is not safely send-capable.`);
4974
+ predecessor.inboundFenced = true;
4975
+ // Release broker/host authority while leaving the native server alive for
4976
+ // the one accepted terminal response. stopAndWait is deferred to stopSession.
4977
+ const stopped = await predecessor.host.stop();
4978
+ if (stopped !== "stopped" && predecessor.host.started)
4979
+ throw new Error(`notifications: predecessor runtime ${id} host release was not proven.`);
4980
+ predecessor.hostStopped = true;
4981
+ predecessor.brokerRegistrationReleased = !predecessor.brokerRegistrationActive || predecessor.hostStopped;
4982
+ if (predecessor.brokerRegistrationActive && predecessor.hostStopped) predecessor.brokerRegistrationActive = false;
4983
+ predecessor.host.reverse.dispose();
4984
+ };
4985
+
4486
4986
  const rotateSessionAuthority = async (
4487
4987
  event: { previousSessionFile?: string },
4488
4988
  ctx: ExtensionContext,
4489
4989
  awaitStartup: boolean,
4990
+ options: { deferPredecessorStop?: boolean } = {},
4490
4991
  ): Promise<void> => {
4491
4992
  if (extensionShuttingDown) return;
4492
4993
  const newId = sessionId(ctx);
@@ -4495,7 +4996,11 @@ export function createNotificationsExtension(
4495
4996
  const pendingStartup = sessionStartPromises.get(newId);
4496
4997
  if (pendingStartup) {
4497
4998
  if (awaitStartup) {
4498
- await pendingStartup;
4999
+ const result = await pendingStartup;
5000
+ if (!lifecycleStartupCapability && result.status === "failed")
5001
+ throw new Error(
5002
+ `notifications: SDK startup failed: ${result.failure?.message ?? "Unknown startup failure."}`,
5003
+ );
4499
5004
  if (!extensionShuttingDown && runtimes.has(newId) && activeRuntimeId === newId)
4500
5005
  await controller.reconcileCurrentSession(ctx);
4501
5006
  } else {
@@ -4510,14 +5015,14 @@ export function createNotificationsExtension(
4510
5015
  }
4511
5016
  if (prevId && prevId !== newId) {
4512
5017
  controller.rekeySession(prevId, newId);
4513
- try {
4514
- await stopSession(prevId);
4515
- } catch (error) {
4516
- // A retained owner-release failure keeps the exact runtime in
4517
- // cleanupRetries for an explicit later retry; log it rather than
4518
- // surfacing a red extension error
4519
- // while rotating session authority (/new, fork, resume, branch).
4520
- logger.error(`notifications: SDK notification runtime cleanup failed: ${String(error)}`);
5018
+ if (options.deferPredecessorStop) {
5019
+ await preparePredecessorForTerminal(prevId);
5020
+ } else {
5021
+ const stopped = await stopSession(prevId);
5022
+ if (cleanupRetries.has(prevId) || runtimes.has(prevId) || sessionStartPromises.has(prevId))
5023
+ throw new Error(`notifications: predecessor runtime ${prevId} release is uncertain.`);
5024
+ if (!stopped && activeRuntimeId === prevId)
5025
+ throw new Error(`notifications: predecessor runtime ${prevId} release was not proven.`);
4521
5026
  }
4522
5027
  }
4523
5028
  if (extensionShuttingDown) return;
@@ -4538,22 +5043,37 @@ export function createNotificationsExtension(
4538
5043
  trackBranchStartup(newId, ctx, startup);
4539
5044
  };
4540
5045
  api.on("session_switch", async (event, ctx) => {
5046
+ const awaitStartup = shouldAwaitNotificationStartup(event);
4541
5047
  if (identityControlInFlight) {
4542
- deferredIdentityRotation = { event, ctx, awaitStartup: true };
5048
+ deferredIdentityRotation = { event, ctx, awaitStartup };
4543
5049
  return;
4544
5050
  }
4545
- await rotateSessionAuthority(event, ctx, true);
5051
+ await rotateSessionAuthority(event, ctx, awaitStartup);
4546
5052
  });
4547
5053
  api.on("session_branch", async (event, ctx) => {
4548
5054
  if (identityControlInFlight) {
4549
- deferredIdentityRotation = { event, ctx, awaitStartup: false };
5055
+ deferredIdentityRotation = { event, ctx, awaitStartup: true };
4550
5056
  return;
4551
5057
  }
4552
- await rotateSessionAuthority(event, ctx, false);
5058
+ await rotateSessionAuthority(event, ctx, true);
4553
5059
  });
4554
5060
 
4555
- const terminalizeInFlightTools = (rt: SessionRuntime, id: string, phase: "cancelled" | "unknown"): void => {
4556
- if (rt.notificationsActive && !rt.redact) {
5061
+ const terminalizeInFlightTools = (
5062
+ rt: SessionRuntime,
5063
+ id: string,
5064
+ phase: "cancelled" | "failed",
5065
+ allowSafeRedactedFrame = false,
5066
+ ): void => {
5067
+ if (rt.policySuspended && !allowSafeRedactedFrame) {
5068
+ for (const [toolCallId, tool] of rt.inFlightTools) {
5069
+ rt.inFlightTools.set(toolCallId, {
5070
+ toolName: tool.toolName,
5071
+ pendingPhase: tool.pendingPhase ?? phase,
5072
+ });
5073
+ }
5074
+ return;
5075
+ }
5076
+ if (rt.notificationsActive && (!rt.redact || allowSafeRedactedFrame)) {
4557
5077
  for (const [toolCallId, { toolName }] of rt.inFlightTools) {
4558
5078
  try {
4559
5079
  pushSessionFrame(rt, { type: "tool_activity", sessionId: id, toolCallId, toolName, phase });
@@ -4565,6 +5085,27 @@ export function createNotificationsExtension(
4565
5085
  rt.inFlightTools.clear();
4566
5086
  };
4567
5087
 
5088
+ const settleProvisionalToolTerminals = (rt: SessionRuntime, id: string): void => {
5089
+ for (const [toolCallId, tool] of rt.inFlightTools) {
5090
+ if (!tool.pendingPhase) continue;
5091
+ try {
5092
+ if (rt.notificationsActive && !rt.redact) {
5093
+ pushSessionFrame(rt, {
5094
+ type: "tool_activity",
5095
+ sessionId: id,
5096
+ toolCallId,
5097
+ toolName: tool.toolName,
5098
+ phase: tool.pendingPhase,
5099
+ });
5100
+ }
5101
+ } catch (e) {
5102
+ logger.warn(`notifications: provisional tool_activity settlement failed: ${String(e)}`);
5103
+ } finally {
5104
+ rt.inFlightTools.delete(toolCallId);
5105
+ }
5106
+ }
5107
+ };
5108
+
4568
5109
  const resetTurnStreamState = (rt: SessionRuntime): void => {
4569
5110
  rt.currentTurnText = undefined;
4570
5111
  rt.preAskFlushedText = undefined;
@@ -4579,22 +5120,45 @@ export function createNotificationsExtension(
4579
5120
  if (!pending) return;
4580
5121
  rt.pendingFinal = undefined;
4581
5122
  if (pending.text && rt.notificationsActive && !rt.redact) {
4582
- try {
4583
- pushSessionFrame(rt, {
4584
- type: "turn_stream",
4585
- sessionId: id,
4586
- phase: "finalized",
4587
- finalAnswer: true,
5123
+ // Under lean, hold intermediate finals until agent_end when the agent is still
5124
+ // running so provisional-policy activation cannot reintroduce per-turn spam.
5125
+ if (rt.verbosity === "lean" && rt.busy) {
5126
+ rt.pendingSettled = {
4588
5127
  text: pending.text,
4589
5128
  ...(pending.messageRef ? { messageRef: pending.messageRef } : {}),
4590
- });
4591
- } catch (error) {
4592
- logger.warn(`notifications: pushFrame (pending turn) failed: ${String(error)}`);
5129
+ };
5130
+ } else {
5131
+ try {
5132
+ pushSessionFrame(rt, {
5133
+ type: "turn_stream",
5134
+ sessionId: id,
5135
+ phase: "finalized",
5136
+ finalAnswer: true,
5137
+ text: pending.text,
5138
+ ...(pending.messageRef ? { messageRef: pending.messageRef } : {}),
5139
+ });
5140
+ } catch (error) {
5141
+ logger.warn(`notifications: pushFrame (pending turn) failed: ${String(error)}`);
5142
+ }
4593
5143
  }
4594
5144
  }
4595
5145
  resetTurnStreamState(rt);
4596
5146
  };
4597
5147
 
5148
+ /** Emit the deferred lean settled answer exactly once (agent_end / idle). */
5149
+ const flushPendingSettled = (rt: SessionRuntime, id: string): void => {
5150
+ const settled = rt.pendingSettled;
5151
+ rt.pendingSettled = undefined;
5152
+ if (!settled?.text || !rt.notificationsActive || rt.redact || rt.policySuspended) return;
5153
+ const previousLiveRef = rt.liveRef;
5154
+ if (settled.messageRef) rt.liveRef = settled.messageRef;
5155
+ try {
5156
+ flushTurnText(rt, id, settled.text, true);
5157
+ } finally {
5158
+ if (!settled.messageRef) rt.liveRef = previousLiveRef;
5159
+ }
5160
+ };
5161
+
4598
5162
  // Drive the live typing indicator: mark busy when the agent loop starts so
4599
5163
  // the daemon shows "typing…" in the thread while the agent is thinking,
4600
5164
  // before any turn output exists. Cleared on `agent_end` below.
@@ -4607,6 +5171,7 @@ export function createNotificationsExtension(
4607
5171
  rt.busy = true;
4608
5172
  const correlation = rt.pendingPromptCorrelations.shift();
4609
5173
  rt.activePromptCorrelation = correlation;
5174
+ rt.notePromptReconciliation(correlation, { type: "agent_start" });
4610
5175
  rt.emitPromptLifecycle(correlation, { type: "agent_start", sessionId: id, ...correlation });
4611
5176
  try {
4612
5177
  // `activity` is the native live-host lifecycle surface. The separately
@@ -4651,13 +5216,54 @@ export function createNotificationsExtension(
4651
5216
  rt.busy = false;
4652
5217
  const correlation = rt.activePromptCorrelation;
4653
5218
  if (correlation) {
4654
- if (rt.recordPromptTerminal(correlation))
4655
- rt.emitPromptLifecycle(correlation, { type: "agent_end", sessionId: id, ...correlation });
5219
+ const terminalAssistant = (Array.isArray(event.messages) ? [...event.messages].reverse() : []).find(
5220
+ message =>
5221
+ message &&
5222
+ typeof message === "object" &&
5223
+ (message as { role?: unknown }).role === "assistant" &&
5224
+ ((message as { stopReason?: unknown }).stopReason === "error" ||
5225
+ (message as { stopReason?: unknown }).stopReason === "aborted"),
5226
+ ) as { stopReason?: "error" | "aborted"; errorMessage?: unknown } | undefined;
5227
+ const failureCode =
5228
+ event.stopReason === "cancelled"
5229
+ ? "cancelled"
5230
+ : terminalAssistant?.stopReason === "error"
5231
+ ? "agent_error"
5232
+ : terminalAssistant?.stopReason === "aborted"
5233
+ ? "aborted"
5234
+ : undefined;
5235
+ if (failureCode)
5236
+ rt.emitPromptFailure(correlation, {
5237
+ code: failureCode,
5238
+ message:
5239
+ typeof terminalAssistant?.errorMessage === "string"
5240
+ ? terminalAssistant.errorMessage
5241
+ : "Prompt submission failed.",
5242
+ });
5243
+ else {
5244
+ rt.notePromptReconciliation(correlation, { type: "agent_end" });
5245
+ if (rt.recordPromptTerminal(correlation)) {
5246
+ const finalAssistant = (Array.isArray(event.messages) ? [...event.messages].reverse() : []).find(
5247
+ message =>
5248
+ message &&
5249
+ typeof message === "object" &&
5250
+ (message as { role?: unknown }).role === "assistant" &&
5251
+ (message as { stopReason?: unknown }).stopReason === "stop",
5252
+ );
5253
+ const finalText = finalAssistant ? acpFinalTextFromMessage(finalAssistant).text : "";
5254
+ rt.emitPromptLifecycle(correlation, {
5255
+ type: "agent_end",
5256
+ sessionId: id,
5257
+ ...correlation,
5258
+ ...(finalText ? { finalText } : {}),
5259
+ });
5260
+ }
5261
+ }
4656
5262
  } else {
4657
5263
  rt.emitPromptLifecycle(undefined, { type: "agent_end", sessionId: id });
4658
5264
  }
4659
5265
  rt.activePromptCorrelation = undefined;
4660
- terminalizeInFlightTools(rt, id, event.stopReason === "cancelled" ? "cancelled" : "unknown");
5266
+ terminalizeInFlightTools(rt, id, event.stopReason === "cancelled" ? "cancelled" : "failed");
4661
5267
  try {
4662
5268
  pushSessionFrame(rt, { type: "activity", sessionId: id, state: "idle" });
4663
5269
  } catch (e) {
@@ -4696,9 +5302,14 @@ export function createNotificationsExtension(
4696
5302
  logger.warn(`notifications: noteIdle failed: ${String(e)}`);
4697
5303
  }
4698
5304
 
5305
+ // Lean: emit the latest deferred assistant answer exactly once at idle.
5306
+ // Intermediate tool-turn narration was held on turn_end; ask lead-ins were
5307
+ // already flushed immediately and deduped via preAskFlushedText.
5308
+ flushPendingSettled(rt, id);
5309
+
4699
5310
  // On idle, stream a context update with metadata (token/model usage +
4700
- // working-tree diff) unless redaction is on. The agent's last message is
4701
- // NOT repeated here — it is already streamed once via `turn_stream`.
5311
+ // working-tree diff) unless redaction is on. Under verbose the agent's last
5312
+ // message is already streamed per turn_end; lean flushes it just above.
4702
5313
  if (!rt.redact && rt.verbosity === "verbose") {
4703
5314
  const usage = (
4704
5315
  ctx as { getContextUsage?: () => { tokens: number | null; contextWindow: number } | undefined }
@@ -4727,17 +5338,20 @@ export function createNotificationsExtension(
4727
5338
  }
4728
5339
  });
4729
5340
 
4730
- // Stream viable agent output per turn (the live thread mirror). Unlike idle,
4731
- // turn output is expected to be multiple messages — one per turn that
4732
- // produced assistant text. Tool-only turns yield no text and are skipped.
4733
- // Redaction suppresses streamed content (only the one-time identity header
4734
- // survives redaction). The daemon coalesces/throttles these via its shared
4735
- // rate-limit pool before sending to Telegram.
5341
+ // Stream viable agent output. Verbose mirrors each turn that produced assistant
5342
+ // text. Lean defers the latest answer until agent_end (idle) so tool-heavy runs
5343
+ // do not flood remote clients with intermediate narration; ask lead-ins still
5344
+ // flush immediately. Tool-only turns yield no text and are skipped. Redaction
5345
+ // suppresses streamed content (only the one-time identity header survives).
5346
+ // The daemon coalesces/throttles these via its shared rate-limit pool.
4736
5347
  // Push the in-flight turn's assistant text as a finalized turn_stream, deduped
4737
5348
  // against what was already flushed for this turn (the pre-ask lead-in).
4738
5349
  const flushTurnText = (rt: SessionRuntime, id: string, text: string | undefined, finalAnswer: boolean): void => {
4739
5350
  if (!text || text === rt.preAskFlushedText || !rt.notificationsActive || rt.policySuspended) return;
4740
5351
  rt.preAskFlushedText = text;
5352
+ // Ask lead-ins supersede any deferred lean settled answer from earlier turns
5353
+ // so agent_end does not re-emit stale intermediate narration (#2863 review).
5354
+ if (!finalAnswer) rt.pendingSettled = undefined;
4741
5355
  // Decision A: a stream-enabled turn must finalize as an in-place edit of ONE
4742
5356
  // live message, never a fresh (rich-promotable) send. If live frames were
4743
5357
  // async-queued and none landed before this flush, reuse the per-turn ref
@@ -4767,14 +5381,13 @@ export function createNotificationsExtension(
4767
5381
  // path and ordered before it — unlike message_update, which is queued async),
4768
5382
  // then flushed here before the ask tool's execute calls registerAsk.
4769
5383
  api.on("tool_execution_start", (event, ctx) => {
5384
+ const id = sessionId(ctx);
5385
+ const rt = runtimes.get(id);
5386
+ rt?.emitPromptEvent(event);
4770
5387
  if (event.toolName === "ask") {
4771
- const id = sessionId(ctx);
4772
- const rt = runtimes.get(id);
4773
5388
  if (!rt?.notificationsActive || rt.redact) return;
4774
5389
  flushTurnText(rt, id, rt.currentTurnText, false);
4775
5390
  }
4776
- const id = sessionId(ctx);
4777
- const rt = runtimes.get(id);
4778
5391
  if (!rt?.notificationsActive || rt.redact) return;
4779
5392
  rt.inFlightTools.set(event.toolCallId, { toolName: event.toolName, args: event.args });
4780
5393
  try {
@@ -4790,12 +5403,32 @@ export function createNotificationsExtension(
4790
5403
  }
4791
5404
  });
4792
5405
 
5406
+ api.on("tool_execution_update", (event, ctx) => {
5407
+ const rt = runtimes.get(sessionId(ctx));
5408
+ rt?.emitPromptEvent(event);
5409
+ });
5410
+
4793
5411
  api.on("tool_execution_end", (event, ctx) => {
4794
5412
  const id = sessionId(ctx);
4795
5413
  const rt = runtimes.get(id);
4796
5414
  if (!rt) return;
5415
+ rt.emitPromptEvent(event);
4797
5416
  const inFlight = rt.inFlightTools.get(event.toolCallId);
4798
- if (!rt.notificationsActive || rt.redact) {
5417
+ if (!inFlight) return;
5418
+ if (!rt.notificationsActive) {
5419
+ rt.inFlightTools.delete(event.toolCallId);
5420
+ return;
5421
+ }
5422
+ if (rt.policySuspended) {
5423
+ if (!inFlight.pendingPhase) {
5424
+ rt.inFlightTools.set(event.toolCallId, {
5425
+ toolName: inFlight.toolName,
5426
+ pendingPhase: event.isError ? "failed" : "completed",
5427
+ });
5428
+ }
5429
+ return;
5430
+ }
5431
+ if (rt.redact) {
4799
5432
  rt.inFlightTools.delete(event.toolCallId);
4800
5433
  return;
4801
5434
  }
@@ -4878,19 +5511,37 @@ export function createNotificationsExtension(
4878
5511
  rt.turnClosed = true;
4879
5512
  return;
4880
5513
  }
4881
- if (text) flushTurnText(rt, id, text, true);
5514
+ if (text) {
5515
+ if (rt.verbosity === "verbose") {
5516
+ // Verbose: one finalized turn_stream per turn with assistant text.
5517
+ flushTurnText(rt, id, text, true);
5518
+ } else if (text !== rt.preAskFlushedText) {
5519
+ // Lean: hold the latest answer until agent_end. Skip when this turn
5520
+ // already flushed the same text as an ask lead-in (no duplicate at idle).
5521
+ rt.pendingSettled = {
5522
+ text,
5523
+ ...(rt.liveRef ? { messageRef: rt.liveRef } : {}),
5524
+ };
5525
+ } else {
5526
+ // Lead-in already flushed: drop any older deferred settled text so idle
5527
+ // does not re-emit intermediate narration after the ask prompt (#2863).
5528
+ rt.pendingSettled = undefined;
5529
+ }
5530
+ }
4882
5531
  resetTurnStreamState(rt);
4883
5532
  });
4884
5533
 
4885
- // Live streaming (opt-in): push throttled in-progress assistant text as
4886
- // non-finalized turn_stream frames so remote clients edit one message as the
4887
- // turn streams. The finalized frame (turn_end) carries the same messageRef and
4888
- // lands the authoritative text. Suppressed under redaction.
5534
+ // Live streaming (opt-in + verbose only): push throttled in-progress assistant
5535
+ // text as non-finalized turn_stream frames so remote clients edit one message
5536
+ // as the turn streams. Lean keeps settled-answer-only delivery; live frames
5537
+ // are suppressed even when the stream preference is on. The finalized frame
5538
+ // (turn_end / agent_end) carries the same messageRef and lands the authoritative
5539
+ // text. Suppressed under redaction.
4889
5540
  api.on("message_update", (event, ctx) => {
4890
5541
  const id = sessionId(ctx);
4891
5542
  const rt = runtimes.get(id);
4892
5543
  rt?.emitPromptEvent(event);
4893
- if (!rt?.notificationsActive || !rt.stream || rt.redact || rt.turnClosed) return;
5544
+ if (!rt?.notificationsActive || !rt.stream || rt.redact || rt.turnClosed || rt.verbosity !== "verbose") return;
4894
5545
  if ((event.message as { role?: unknown }).role !== "assistant") return;
4895
5546
  if (rt.liveRef === undefined && rt.turnSeq !== undefined) {
4896
5547
  rt.liveRef = String(rt.turnSeq);
@@ -4942,13 +5593,29 @@ export function createNotificationsExtension(
4942
5593
  extensionShuttingDown = true;
4943
5594
  identityControlInFlight = false;
4944
5595
  deferredIdentityRotation = undefined;
4945
- await Promise.allSettled([...branchStartupTasks]);
4946
5596
  const id = sessionId(ctx);
5597
+ // Join settled start+reconcile before owner release. Keep pending native
5598
+ // startup (/notify on) nonblocking — do not await sessionLifecycleTasks while
5599
+ // sessionStartPromises still has this id.
5600
+ if (!sessionStartPromises.has(id)) await Promise.allSettled([...sessionLifecycleTasks]);
5601
+ await Promise.allSettled([...branchStartupTasks]);
4947
5602
  const rt = runtimes.get(id);
4948
- if (rt) terminalizeInFlightTools(rt, id, "unknown");
5603
+ if (rt) terminalizeInFlightTools(rt, id, "cancelled");
5604
+ // Startup is only genuinely in flight when a `sessionStartPromises` entry
5605
+ // exists. Once startup has settled, the host is broker-visible and its
5606
+ // post-start `reconcileCurrentSession` may already have minted a
5607
+ // replacement notification-root token whose unregister is still awaiting
5608
+ // its file lock and atomic registry write. Returning before that settles
5609
+ // leaves a stale `sessions[id]` row that the retained older token is
5610
+ // correctly fenced from removing, so shutdown must join it.
5611
+ const startupWasPending = sessionStartPromises.has(id);
4949
5612
  const controllerStop =
4950
5613
  typeof ctx.sessionManager.getCwd === "function" ? controller.stopCurrentSession(ctx) : Promise.resolve(false);
4951
- void controllerStop.catch(error => logger.warn(`notifications: controller shutdown failed: ${String(error)}`));
5614
+ const settledControllerStop = controllerStop.catch(error => {
5615
+ logger.warn(`notifications: controller shutdown failed: ${String(error)}`);
5616
+ return false;
5617
+ });
5618
+ if (startupWasPending) void settledControllerStop;
4952
5619
  try {
4953
5620
  await stopSession(id);
4954
5621
  } catch (error) {
@@ -4959,5 +5626,10 @@ export function createNotificationsExtension(
4959
5626
  // error severity (matching the postmortem cleanup precedent).
4960
5627
  logger.error(`notifications: SDK notification runtime cleanup failed: ${String(error)}`);
4961
5628
  }
5629
+ // Keep shutdown nonblocking only while native startup is genuinely
5630
+ // pending (the `/notify on` path); otherwise await the controller queue so
5631
+ // completed-start reconciliation and its replacement-token cleanup are
5632
+ // joined before lifecycle shutdown returns.
5633
+ if (!startupWasPending) await settledControllerStop;
4962
5634
  });
4963
5635
  }