@gajae-code/coding-agent 0.11.1 → 0.11.2

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 (404) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/dist/types/cli/config-cli.d.ts +11 -1
  3. package/dist/types/cli/notify-cli.d.ts +1 -0
  4. package/dist/types/cli/session-picker.d.ts +2 -1
  5. package/dist/types/cli/setup-cli.d.ts +1 -1
  6. package/dist/types/commands/mcp-serve.d.ts +33 -0
  7. package/dist/types/commands/mcp.d.ts +1 -1
  8. package/dist/types/commands/notify.d.ts +0 -2
  9. package/dist/types/commands/sdk.d.ts +1 -1
  10. package/dist/types/config/atomic-yaml-patch.d.ts +18 -0
  11. package/dist/types/config/file-lock.d.ts +14 -4
  12. package/dist/types/config/keybindings.d.ts +42 -1
  13. package/dist/types/config/model-auth.d.ts +3 -0
  14. package/dist/types/config/model-bindings-applier.d.ts +13 -0
  15. package/dist/types/config/model-discovery-manager.d.ts +49 -0
  16. package/dist/types/config/model-equivalence.d.ts +20 -0
  17. package/dist/types/config/model-registry.d.ts +16 -15
  18. package/dist/types/config/model-resolver.d.ts +28 -18
  19. package/dist/types/config/models-config-schema.d.ts +11 -0
  20. package/dist/types/config/settings-schema.d.ts +56 -1
  21. package/dist/types/config/settings.d.ts +14 -11
  22. package/dist/types/coordinator-mcp/policy.d.ts +5 -0
  23. package/dist/types/coordinator-mcp/question-gate-codec.d.ts +129 -0
  24. package/dist/types/coordinator-mcp/question-state.d.ts +356 -0
  25. package/dist/types/coordinator-mcp/server.d.ts +4 -0
  26. package/dist/types/discovery/helpers.d.ts +0 -6
  27. package/dist/types/edit/index.d.ts +3 -0
  28. package/dist/types/eval/py/env.d.ts +9 -0
  29. package/dist/types/extensibility/extensions/loader.d.ts +2 -1
  30. package/dist/types/extensibility/extensions/runner.d.ts +2 -2
  31. package/dist/types/extensibility/extensions/types.d.ts +34 -3
  32. package/dist/types/extensibility/hooks/runner.d.ts +1 -1
  33. package/dist/types/extensibility/shared-events.d.ts +2 -0
  34. package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +10 -1
  35. package/dist/types/gjc-runtime/deep-interview-state.d.ts +44 -0
  36. package/dist/types/gjc-runtime/goal-mode-request.d.ts +11 -1
  37. package/dist/types/gjc-runtime/launch-worktree.d.ts +4 -2
  38. package/dist/types/gjc-runtime/linux-proc.d.ts +29 -0
  39. package/dist/types/gjc-runtime/session-resolution.d.ts +2 -2
  40. package/dist/types/gjc-runtime/state-migrations.d.ts +1 -1
  41. package/dist/types/gjc-runtime/state-writer.d.ts +2 -1
  42. package/dist/types/gjc-runtime/team-launch.d.ts +59 -0
  43. package/dist/types/gjc-runtime/team-notify.d.ts +45 -0
  44. package/dist/types/gjc-runtime/team-runtime.d.ts +29 -121
  45. package/dist/types/gjc-runtime/team-store.d.ts +160 -0
  46. package/dist/types/gjc-runtime/team-workers.d.ts +78 -0
  47. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +8 -0
  48. package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +13 -0
  49. package/dist/types/gjc-runtime/ultragoal-evidence.d.ts +38 -0
  50. package/dist/types/gjc-runtime/ultragoal-guard.d.ts +7 -0
  51. package/dist/types/gjc-runtime/ultragoal-pipeline.d.ts +23 -0
  52. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +55 -33
  53. package/dist/types/gjc-runtime/workflow-cli-common.d.ts +8 -0
  54. package/dist/types/gjc-runtime/workflow-manifest.d.ts +3 -0
  55. package/dist/types/goals/continuation-timeout-guard.d.ts +22 -0
  56. package/dist/types/goals/runtime.d.ts +1 -0
  57. package/dist/types/goals/state.d.ts +8 -0
  58. package/dist/types/harness-control-plane/lifecycle-reconciliation.d.ts +17 -0
  59. package/dist/types/harness-control-plane/phase-rollup.d.ts +4 -7
  60. package/dist/types/harness-control-plane/receipt-ingest.d.ts +5 -0
  61. package/dist/types/harness-control-plane/receipt-spool.d.ts +4 -0
  62. package/dist/types/hindsight/state.d.ts +11 -4
  63. package/dist/types/lsp/lspmux.d.ts +5 -3
  64. package/dist/types/lsp/path-trust.d.ts +1 -0
  65. package/dist/types/main.d.ts +3 -2
  66. package/dist/types/memories/storage.d.ts +1 -0
  67. package/dist/types/modes/action-registry.d.ts +29 -0
  68. package/dist/types/modes/components/chrome.d.ts +14 -0
  69. package/dist/types/modes/components/command-palette.d.ts +27 -0
  70. package/dist/types/modes/components/custom-provider-wizard.d.ts +1 -0
  71. package/dist/types/modes/components/index.d.ts +2 -1
  72. package/dist/types/modes/components/pet-selector.d.ts +1 -1
  73. package/dist/types/modes/components/plan-preview-overlay.d.ts +43 -0
  74. package/dist/types/modes/components/queue-mode-selector.d.ts +1 -1
  75. package/dist/types/modes/components/queue-pane.d.ts +12 -0
  76. package/dist/types/modes/components/read-tool-group.d.ts +6 -0
  77. package/dist/types/modes/components/session-observer-overlay.d.ts +3 -4
  78. package/dist/types/modes/components/sessions-dashboard.d.ts +32 -0
  79. package/dist/types/modes/components/settings-selector.d.ts +1 -1
  80. package/dist/types/modes/components/show-images-selector.d.ts +1 -1
  81. package/dist/types/modes/components/status-line/types.d.ts +1 -1
  82. package/dist/types/modes/components/status-line.d.ts +1 -94
  83. package/dist/types/modes/components/tasks-pane.d.ts +15 -0
  84. package/dist/types/modes/components/theme-selector.d.ts +1 -1
  85. package/dist/types/modes/components/tool-execution.d.ts +15 -0
  86. package/dist/types/modes/components/tool-status-header.d.ts +111 -0
  87. package/dist/types/modes/components/transcript-viewer-overlay.d.ts +42 -0
  88. package/dist/types/modes/controllers/goal-mode-controller.d.ts +62 -0
  89. package/dist/types/modes/controllers/input-controller.d.ts +8 -1
  90. package/dist/types/modes/controllers/mode-gate.d.ts +8 -0
  91. package/dist/types/modes/controllers/plan-mode-controller.d.ts +65 -0
  92. package/dist/types/modes/controllers/selector-controller.d.ts +15 -3
  93. package/dist/types/modes/controllers/stt-controller.d.ts +26 -0
  94. package/dist/types/modes/interactive-mode.d.ts +19 -17
  95. package/dist/types/modes/jobs-observer.d.ts +2 -0
  96. package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +4 -11
  97. package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +20 -2
  98. package/dist/types/modes/shared/agent-wire/workflow-gate-types.d.ts +31 -0
  99. package/dist/types/modes/tasks-aggregator.d.ts +39 -0
  100. package/dist/types/modes/transcript-item-registry.d.ts +60 -0
  101. package/dist/types/modes/types.d.ts +20 -13
  102. package/dist/types/modes/utils/ui-helpers.d.ts +2 -2
  103. package/dist/types/runtime-mcp/manager.d.ts +1 -1
  104. package/dist/types/sdk/broker/broker.d.ts +78 -12
  105. package/dist/types/sdk/broker/lifecycle-codec.d.ts +4 -0
  106. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +15 -1
  107. package/dist/types/sdk/broker/lifecycle.d.ts +6 -2
  108. package/dist/types/sdk/broker/process-incarnation.d.ts +2 -0
  109. package/dist/types/sdk/broker/session-index.d.ts +0 -1
  110. package/dist/types/sdk/broker/state-version.d.ts +2 -0
  111. package/dist/types/sdk/bus/chat-effect-journal.d.ts +1 -0
  112. package/dist/types/sdk/bus/config-commands.d.ts +1 -1
  113. package/dist/types/sdk/bus/config.d.ts +11 -0
  114. package/dist/types/sdk/bus/daemon-paths.d.ts +1 -0
  115. package/dist/types/sdk/bus/html-format.d.ts +3 -2
  116. package/dist/types/sdk/bus/index.d.ts +27 -0
  117. package/dist/types/sdk/bus/lifecycle-control-runtime.d.ts +4 -0
  118. package/dist/types/sdk/bus/notification-service.d.ts +20 -0
  119. package/dist/types/sdk/bus/rate-limit-pool.d.ts +13 -2
  120. package/dist/types/sdk/bus/recent-activity.d.ts +21 -12
  121. package/dist/types/sdk/bus/rich-document.d.ts +7 -0
  122. package/dist/types/sdk/bus/rich-render.d.ts +18 -41
  123. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +9 -4
  124. package/dist/types/sdk/bus/telegram-daemon.d.ts +46 -3
  125. package/dist/types/sdk/bus/threaded-inbound.d.ts +1 -1
  126. package/dist/types/sdk/bus/threaded-render.d.ts +13 -0
  127. package/dist/types/sdk/bus/topic-registry.d.ts +24 -2
  128. package/dist/types/sdk/host/control/operations.d.ts +1 -1
  129. package/dist/types/sdk/host/host.d.ts +2 -0
  130. package/dist/types/sdk/index.d.ts +1 -0
  131. package/dist/types/sdk/session-directory.d.ts +56 -0
  132. package/dist/types/session/agent-session.d.ts +82 -47
  133. package/dist/types/session/agent-storage.d.ts +7 -0
  134. package/dist/types/session/fallback-chain-controller.d.ts +10 -2
  135. package/dist/types/session/internal/managed-session-scope.d.ts +102 -0
  136. package/dist/types/session/internal/managed-session-storage.d.ts +38 -0
  137. package/dist/types/session/messages.d.ts +16 -0
  138. package/dist/types/session/session-manager-internal.d.ts +14 -0
  139. package/dist/types/session/session-manager.d.ts +82 -12
  140. package/dist/types/session/session-storage.d.ts +43 -3
  141. package/dist/types/session/volatile-context-pruning.d.ts +15 -0
  142. package/dist/types/skill-state/{deep-interview-mutation-guard.d.ts → workflow-mutation-guard.d.ts} +24 -8
  143. package/dist/types/skill-state/workflow-state-contract.d.ts +1 -3
  144. package/dist/types/slash-commands/builtin-registry.d.ts +4 -1
  145. package/dist/types/system-prompt.d.ts +2 -11
  146. package/dist/types/task/executor.d.ts +3 -1
  147. package/dist/types/thinking.d.ts +7 -0
  148. package/dist/types/tool-discovery/tool-index.d.ts +1 -50
  149. package/dist/types/tools/ask.d.ts +64 -2
  150. package/dist/types/tools/bash.d.ts +3 -0
  151. package/dist/types/tools/browser/dead-tab-recovery.d.ts +24 -0
  152. package/dist/types/tools/browser/tab-supervisor.d.ts +21 -0
  153. package/dist/types/tools/cron.d.ts +2 -0
  154. package/dist/types/tools/fetch.d.ts +1 -0
  155. package/dist/types/tools/index.d.ts +30 -15
  156. package/dist/types/tools/output-meta.d.ts +1 -0
  157. package/dist/types/tools/read.d.ts +3 -0
  158. package/dist/types/tools/search-tool-bm25.d.ts +3 -4
  159. package/dist/types/utils/event-bus.d.ts +1 -1
  160. package/dist/types/web/search/index.d.ts +3 -1
  161. package/package.json +21 -9
  162. package/scripts/build-sdk-package-smoke.ts +34 -1
  163. package/scripts/generate-hotkeys-docs.ts +74 -0
  164. package/scripts/generate-sdk-operation-inventory.ts +64 -20
  165. package/scripts/verify-gjc-sdk-canonicalization.ts +182 -2
  166. package/src/async/job-manager.ts +49 -6
  167. package/src/cli/config-cli.ts +75 -2
  168. package/src/cli/notify-cli.ts +92 -26
  169. package/src/cli/session-picker.ts +20 -3
  170. package/src/cli/setup-cli.ts +1 -1
  171. package/src/cli.ts +1 -1
  172. package/src/commands/config.ts +1 -1
  173. package/src/commands/harness.ts +34 -106
  174. package/src/commands/launch.ts +6 -4
  175. package/src/commands/mcp-serve.ts +85 -8
  176. package/src/commands/mcp.ts +2 -2
  177. package/src/commands/notify.ts +8 -4
  178. package/src/commands/sdk.ts +61 -40
  179. package/src/commands/ultragoal.ts +4 -1
  180. package/src/config/atomic-yaml-patch.ts +80 -13
  181. package/src/config/file-lock.ts +105 -43
  182. package/src/config/keybindings.ts +64 -20
  183. package/src/config/model-auth.ts +9 -0
  184. package/src/config/model-bindings-applier.ts +139 -0
  185. package/src/config/model-discovery-manager.ts +224 -0
  186. package/src/config/model-equivalence.ts +66 -0
  187. package/src/config/model-registry.ts +169 -302
  188. package/src/config/model-resolver.ts +361 -369
  189. package/src/config/models-config-schema.ts +5 -4
  190. package/src/config/settings-schema.ts +159 -2
  191. package/src/config/settings.ts +458 -482
  192. package/src/coordinator-mcp/policy.ts +76 -11
  193. package/src/coordinator-mcp/question-gate-codec.ts +274 -0
  194. package/src/coordinator-mcp/question-state.ts +778 -0
  195. package/src/coordinator-mcp/server.ts +1702 -238
  196. package/src/debug/index.ts +1 -1
  197. package/src/defaults/gjc/skills/deep-interview/SKILL.md +13 -1
  198. package/src/defaults/gjc/skills/team/SKILL.md +16 -0
  199. package/src/discovery/helpers.ts +1 -50
  200. package/src/edit/index.ts +64 -0
  201. package/src/eval/py/env.ts +26 -0
  202. package/src/eval/py/kernel.ts +5 -3
  203. package/src/extensibility/extensions/loader.ts +9 -1
  204. package/src/extensibility/extensions/runner.ts +12 -3
  205. package/src/extensibility/extensions/types.ts +39 -1
  206. package/src/extensibility/gjc-plugins/installer.ts +16 -16
  207. package/src/extensibility/hooks/runner.ts +2 -2
  208. package/src/extensibility/runtime-skill-discovery.ts +33 -13
  209. package/src/extensibility/shared-events.ts +2 -0
  210. package/src/gjc-runtime/deep-interview-recorder.ts +63 -3
  211. package/src/gjc-runtime/deep-interview-runtime.ts +76 -27
  212. package/src/gjc-runtime/deep-interview-state.ts +317 -5
  213. package/src/gjc-runtime/goal-mode-request.ts +55 -8
  214. package/src/gjc-runtime/launch-tmux.ts +2 -5
  215. package/src/gjc-runtime/launch-worktree.ts +5 -5
  216. package/src/gjc-runtime/linux-proc.ts +71 -0
  217. package/src/gjc-runtime/ralplan-runtime.ts +74 -90
  218. package/src/gjc-runtime/session-resolution.ts +18 -3
  219. package/src/gjc-runtime/session-state-sidecar.ts +6 -107
  220. package/src/gjc-runtime/state-migrations.ts +1 -1
  221. package/src/gjc-runtime/state-runtime.ts +320 -249
  222. package/src/gjc-runtime/state-writer.ts +36 -40
  223. package/src/gjc-runtime/team-launch.ts +319 -0
  224. package/src/gjc-runtime/team-notify.ts +326 -0
  225. package/src/gjc-runtime/team-runtime.ts +1099 -1482
  226. package/src/gjc-runtime/team-store.ts +828 -0
  227. package/src/gjc-runtime/team-workers.ts +962 -0
  228. package/src/gjc-runtime/tmux-owner-isolation-cli.ts +2 -11
  229. package/src/gjc-runtime/tmux-owner-isolation.ts +14 -9
  230. package/src/gjc-runtime/tmux-sessions.ts +4 -21
  231. package/src/gjc-runtime/ultragoal-change-set.ts +172 -0
  232. package/src/gjc-runtime/ultragoal-evidence.ts +874 -0
  233. package/src/gjc-runtime/ultragoal-guard.ts +29 -5
  234. package/src/gjc-runtime/ultragoal-pipeline.ts +306 -0
  235. package/src/gjc-runtime/ultragoal-runtime.ts +84 -1311
  236. package/src/gjc-runtime/workflow-cli-common.ts +30 -0
  237. package/src/gjc-runtime/workflow-manifest.generated.json +58 -0
  238. package/src/gjc-runtime/workflow-manifest.ts +18 -0
  239. package/src/goals/continuation-timeout-guard.ts +79 -0
  240. package/src/goals/runtime.ts +7 -5
  241. package/src/goals/state.ts +18 -0
  242. package/src/harness-control-plane/lifecycle-reconciliation.ts +64 -0
  243. package/src/harness-control-plane/phase-rollup.ts +4 -7
  244. package/src/harness-control-plane/receipt-ingest.ts +5 -0
  245. package/src/harness-control-plane/receipt-spool.ts +51 -7
  246. package/src/harness-control-plane/session-lease.ts +12 -130
  247. package/src/hindsight/backend.ts +26 -27
  248. package/src/hindsight/state.ts +101 -32
  249. package/src/hooks/skill-state.ts +17 -11
  250. package/src/internal-urls/docs-index.generated.ts +21 -18
  251. package/src/lsp/client.ts +1 -1
  252. package/src/lsp/config.ts +145 -26
  253. package/src/lsp/index.ts +1 -1
  254. package/src/lsp/lspmux.ts +39 -10
  255. package/src/lsp/path-trust.ts +81 -0
  256. package/src/main.ts +28 -8
  257. package/src/memories/index.ts +1 -0
  258. package/src/memories/storage.ts +6 -2
  259. package/src/modes/action-registry.ts +165 -0
  260. package/src/modes/components/chrome.ts +40 -0
  261. package/src/modes/components/command-palette.ts +168 -0
  262. package/src/modes/components/custom-editor.ts +1 -1
  263. package/src/modes/components/custom-provider-wizard.ts +51 -14
  264. package/src/modes/components/index.ts +2 -1
  265. package/src/modes/components/pet-selector.ts +11 -17
  266. package/src/modes/components/plan-preview-overlay.ts +295 -0
  267. package/src/modes/components/queue-mode-selector.ts +10 -27
  268. package/src/modes/components/queue-pane.ts +79 -0
  269. package/src/modes/components/read-tool-group.ts +13 -0
  270. package/src/modes/components/session-observer-overlay.ts +207 -772
  271. package/src/modes/components/sessions-dashboard.ts +181 -0
  272. package/src/modes/components/settings-selector.ts +1 -1
  273. package/src/modes/components/show-images-selector.ts +10 -24
  274. package/src/modes/components/status-line/types.ts +1 -1
  275. package/src/modes/components/status-line.ts +1 -969
  276. package/src/modes/components/tasks-pane.ts +73 -0
  277. package/src/modes/components/theme-selector.ts +11 -31
  278. package/src/modes/components/tool-execution.ts +22 -0
  279. package/src/modes/components/tool-status-header.ts +1056 -0
  280. package/src/modes/components/transcript-viewer-overlay.ts +384 -0
  281. package/src/modes/controllers/btw-controller.ts +1 -0
  282. package/src/modes/controllers/event-controller.ts +3 -2
  283. package/src/modes/controllers/extension-ui-controller.ts +24 -1
  284. package/src/modes/controllers/goal-mode-controller.ts +474 -0
  285. package/src/modes/controllers/input-controller.ts +764 -229
  286. package/src/modes/controllers/mode-gate.ts +24 -0
  287. package/src/modes/controllers/plan-mode-controller.ts +517 -0
  288. package/src/modes/controllers/selector-controller.ts +225 -10
  289. package/src/modes/controllers/stt-controller.ts +125 -0
  290. package/src/modes/interactive-mode.ts +289 -1150
  291. package/src/modes/jobs-observer.ts +3 -0
  292. package/src/modes/runtime-init.ts +8 -0
  293. package/src/modes/shared/agent-wire/deep-interview-gate.ts +31 -106
  294. package/src/modes/shared/agent-wire/workflow-gate-broker.ts +71 -16
  295. package/src/modes/shared/agent-wire/workflow-gate-types.ts +177 -0
  296. package/src/modes/tasks-aggregator.ts +199 -0
  297. package/src/modes/transcript-item-registry.ts +229 -0
  298. package/src/modes/types.ts +45 -18
  299. package/src/modes/utils/ui-helpers.ts +17 -9
  300. package/src/prompts/agent-fragments/ralplan-persistence.md +6 -0
  301. package/src/prompts/agent-fragments/restricted-bash.md +1 -0
  302. package/src/prompts/agents/architect.md +2 -8
  303. package/src/prompts/agents/critic.md +2 -8
  304. package/src/prompts/agents/executor.md +3 -0
  305. package/src/prompts/agents/planner.md +4 -12
  306. package/src/prompts/system/custom-system-prompt.md +1 -23
  307. package/src/prompts/system/plan-mode-approved.md +8 -0
  308. package/src/prompts/system/system-prompt.md +17 -96
  309. package/src/prompts/tools/bash.md +4 -4
  310. package/src/prompts/tools/browser.md +15 -52
  311. package/src/prompts/tools/find.md +2 -2
  312. package/src/prompts/tools/hashline.md +13 -55
  313. package/src/prompts/tools/read.md +3 -6
  314. package/src/prompts/tools/search-tool-bm25.md +0 -2
  315. package/src/prompts/tools/search.md +2 -4
  316. package/src/prompts/tools/skill-discovery.md +1 -2
  317. package/src/prompts/tools/task.md +1 -1
  318. package/src/runtime-mcp/manager.ts +1 -1
  319. package/src/sdk/broker/broker.ts +206 -78
  320. package/src/sdk/broker/discovery.ts +1 -1
  321. package/src/sdk/broker/ensure.ts +3 -1
  322. package/src/sdk/broker/lifecycle-codec.ts +11 -0
  323. package/src/sdk/broker/lifecycle-ledger.ts +371 -74
  324. package/src/sdk/broker/lifecycle.ts +1399 -142
  325. package/src/sdk/broker/process-incarnation.ts +5 -13
  326. package/src/sdk/broker/session-index.ts +145 -56
  327. package/src/sdk/broker/state-version.ts +21 -0
  328. package/src/sdk/broker/transport.ts +1 -1
  329. package/src/sdk/bus/chat-daemon-cli.ts +2 -0
  330. package/src/sdk/bus/chat-daemon-control.ts +8 -28
  331. package/src/sdk/bus/chat-effect-journal.ts +10 -2
  332. package/src/sdk/bus/config-commands.ts +5 -4
  333. package/src/sdk/bus/config.ts +98 -0
  334. package/src/sdk/bus/conversation-store.ts +5 -29
  335. package/src/sdk/bus/daemon-paths.ts +2 -0
  336. package/src/sdk/bus/html-format.ts +20 -3
  337. package/src/sdk/bus/index.ts +778 -93
  338. package/src/sdk/bus/lifecycle-control-runtime.ts +91 -33
  339. package/src/sdk/bus/natives-augment.d.ts +26 -1
  340. package/src/sdk/bus/notification-service.ts +168 -20
  341. package/src/sdk/bus/rate-limit-pool.ts +58 -14
  342. package/src/sdk/bus/recent-activity.ts +188 -74
  343. package/src/sdk/bus/rich-document.ts +258 -0
  344. package/src/sdk/bus/rich-render.ts +62 -70
  345. package/src/sdk/bus/telegram-daemon-cli.ts +15 -59
  346. package/src/sdk/bus/telegram-daemon-contract.ts +9 -4
  347. package/src/sdk/bus/telegram-daemon.ts +1389 -237
  348. package/src/sdk/bus/threaded-inbound.ts +8 -3
  349. package/src/sdk/bus/threaded-render.ts +57 -1
  350. package/src/sdk/bus/topic-registry.ts +120 -13
  351. package/src/sdk/host/control/dispatch.ts +14 -2
  352. package/src/sdk/host/control/operations.ts +6 -1
  353. package/src/sdk/host/host.ts +31 -1
  354. package/src/sdk/host/query/handlers.ts +20 -22
  355. package/src/sdk/host/query/revision-store.ts +80 -57
  356. package/src/sdk/index.ts +1 -0
  357. package/src/sdk/protocol/operation-inventory.generated.json +943 -1907
  358. package/src/sdk/session-directory.ts +141 -0
  359. package/src/sdk/session.ts +458 -479
  360. package/src/session/agent-session.ts +1700 -583
  361. package/src/session/agent-storage.ts +18 -0
  362. package/src/session/cache-economics.ts +25 -8
  363. package/src/session/fallback-chain-controller.ts +53 -3
  364. package/src/session/internal/managed-session-scope.ts +2226 -0
  365. package/src/session/internal/managed-session-storage.ts +453 -0
  366. package/src/session/messages.ts +91 -2
  367. package/src/session/session-manager-internal.ts +33 -0
  368. package/src/session/session-manager.ts +1112 -333
  369. package/src/session/session-storage.ts +444 -39
  370. package/src/session/volatile-context-pruning.ts +78 -0
  371. package/src/setup/credential-auto-import.ts +2 -1
  372. package/src/setup/hermes/templates/operator-instructions.v1.md +1 -1
  373. package/src/setup/provider-onboarding.ts +36 -61
  374. package/src/setup/provider-presets.json +5 -0
  375. package/src/skill-state/active-state.ts +3 -13
  376. package/src/skill-state/{deep-interview-mutation-guard.ts → workflow-mutation-guard.ts} +47 -29
  377. package/src/skill-state/workflow-state-contract.ts +6 -41
  378. package/src/slash-commands/builtin-registry.ts +46 -11
  379. package/src/system-prompt.ts +37 -44
  380. package/src/task/agents.ts +19 -1
  381. package/src/task/executor.ts +90 -6
  382. package/src/task/index.ts +36 -21
  383. package/src/thinking.ts +21 -6
  384. package/src/tool-discovery/tool-index.ts +2 -134
  385. package/src/tools/ask.ts +157 -21
  386. package/src/tools/ast-edit.ts +1 -1
  387. package/src/tools/bash.ts +38 -0
  388. package/src/tools/browser/dead-tab-recovery.ts +78 -0
  389. package/src/tools/browser/tab-supervisor.ts +282 -36
  390. package/src/tools/computer.ts +10 -3
  391. package/src/tools/cron.ts +30 -16
  392. package/src/tools/eval.ts +1 -2
  393. package/src/tools/fetch.ts +11 -4
  394. package/src/tools/index.ts +76 -24
  395. package/src/tools/output-meta.ts +13 -5
  396. package/src/tools/read.ts +37 -1
  397. package/src/tools/resource-gc.ts +2 -2
  398. package/src/tools/search-tool-bm25.ts +11 -74
  399. package/src/utils/event-bus.ts +9 -4
  400. package/src/web/search/index.ts +3 -3
  401. package/dist/types/discovery/plugin-dir-roots.d.ts +0 -15
  402. package/dist/types/runtime-mcp/discoverable-tool-metadata.d.ts +0 -7
  403. package/src/discovery/plugin-dir-roots.ts +0 -28
  404. package/src/runtime-mcp/discoverable-tool-metadata.ts +0 -25
@@ -28,9 +28,9 @@ import * as os from "node:os";
28
28
  import * as path from "node:path";
29
29
  import { promisify } from "node:util";
30
30
  import { ThinkingLevel } from "@gajae-code/agent-core";
31
- import type { ImageContent, TextContent } from "@gajae-code/ai";
31
+ import type { ImageContent, TextContent, Tool } from "@gajae-code/ai";
32
32
  import { NotificationServer, nativeBuildInfo } from "@gajae-code/natives";
33
- import { logger, postmortem, VERSION } from "@gajae-code/utils";
33
+ import { logger, postmortem, prompt, VERSION } from "@gajae-code/utils";
34
34
  import { Settings } from "../../config/settings";
35
35
  import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "../../extensibility/extensions";
36
36
  import type {
@@ -38,6 +38,7 @@ import type {
38
38
  WorkflowGateTerminalController,
39
39
  WorkflowGateTerminalProof,
40
40
  } from "../../modes/shared/agent-wire/workflow-gate-broker";
41
+ import btwUserPrompt from "../../prompts/system/btw-user.md" with { type: "text" };
41
42
  import { parseThinkingLevel } from "../../thinking";
42
43
  import type {
43
44
  AskAnswerRequest,
@@ -76,10 +77,14 @@ import {
76
77
  sessionTag,
77
78
  } from "./config";
78
79
  import { telegramControlCommandUsage } from "./config-commands";
79
- import { imageAttachmentsFromMessage, notificationActionPayload, summaryFromMessage } from "./helpers";
80
+ import { imageAttachmentsFromMessage, notificationActionPayload, summaryFromMessage, truncate } from "./helpers";
80
81
  import { assertNativeRuntimeCompatibility } from "./native-runtime-compatibility";
81
82
  import { NotificationSessionController, type NotificationSessionRuntime } from "./session-control";
82
- import { type EnsureDaemonResult, ensureTelegramDaemonRunningDetailed } from "./telegram-daemon";
83
+ import {
84
+ type EnsureDaemonResult,
85
+ endpointAuthorityDigest,
86
+ ensureTelegramDaemonRunningDetailed,
87
+ } from "./telegram-daemon";
83
88
 
84
89
  // ===========================================================================
85
90
  // Session lifecycle control protocol (TypeScript mirror of the Rust wire
@@ -868,7 +873,10 @@ interface SessionRuntime {
868
873
  disposeGateTerminalController: () => void;
869
874
  disposeAckRecoveryParticipant: () => void;
870
875
  disposeGateEmitterListener: () => void;
876
+ /** Aborts and fences side turns while notification delivery is disabled. */
877
+ disableEphemeralTurns: () => void;
871
878
  waitForGateResolutionQuiescence: () => Promise<void>;
879
+ trackGateResolution: <T>(resolution: Promise<T>) => Promise<T>;
872
880
  workflowGate?: WorkflowGateEmitter;
873
881
  gatePresentations?: PresentationArbiter;
874
882
  redact: boolean;
@@ -918,10 +926,52 @@ interface SessionRuntime {
918
926
  /** True between turn_end and the next turn_start: drops late async message_update
919
927
  * frames so a stale live edit can never be emitted after the finalized turn. */
920
928
  turnClosed?: boolean;
929
+ /** Started tool calls awaiting a terminal activity frame, keyed by tool call id. */
930
+ inFlightTools: Map<string, { toolName: string; args: unknown }>;
921
931
  /** Cancels the postmortem cleanup that emits `session_closed` on process teardown. */
922
932
  cancelPostmortemCleanup: () => void;
923
- /** Stops optional broker presence heartbeats. */
924
- stopBrokerHeartbeat: () => void;
933
+ /** Disposes side-turn resources when their owning logical session becomes unavailable. */
934
+ abortEphemeralTurns: () => void;
935
+ }
936
+
937
+ const SENSITIVE_MODEL_LABEL =
938
+ /(?:\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;
939
+ const TOOL_SUMMARY_MAX = 280;
940
+ const EMPTY_CAPABILITIES: ReadonlySet<string> = new Set();
941
+
942
+ /** Stable projection of the tool-owned safe-display seam (never the full Tool surface). */
943
+ type SafeSummaryTool = Pick<Tool, "safeSummary" | "safeSummaryFields">;
944
+
945
+ export function projectToolSummary(
946
+ tool: SafeSummaryTool | undefined,
947
+ kind: "args" | "result",
948
+ value: unknown,
949
+ ): string | undefined {
950
+ let summary: string | undefined;
951
+ try {
952
+ if (tool?.safeSummary) {
953
+ summary = tool.safeSummary(kind, value);
954
+ } else {
955
+ const fields = tool?.safeSummaryFields?.[kind];
956
+ if (fields) {
957
+ const source =
958
+ value && typeof value === "object" && !Array.isArray(value)
959
+ ? (value as Record<string, unknown>)
960
+ : undefined;
961
+ if (source) {
962
+ const projected: Record<string, unknown> = {};
963
+ for (const field of fields) if (Object.hasOwn(source, field)) projected[field] = source[field];
964
+ summary = JSON.stringify(projected);
965
+ }
966
+ }
967
+ }
968
+ } catch {
969
+ return undefined;
970
+ }
971
+ if (typeof summary !== "string") return undefined;
972
+ const normalized = summary.replace(/[\u0000-\u001F\u007F-\u009F\u202A-\u202E\u2066-\u2069]/g, " ").trim();
973
+ if (!normalized || SENSITIVE_MODEL_LABEL.test(normalized)) return undefined;
974
+ return truncate(normalized, TOOL_SUMMARY_MAX);
925
975
  }
926
976
 
927
977
  /** Request-local requester authority for stable ControlSurface dispatches. */
@@ -938,9 +988,28 @@ function pushSessionFrame(
938
988
  frame: { type: string; [key: string]: unknown },
939
989
  ): void {
940
990
  runtime.host.emitEvent({ kind: frame.type, payload: frame });
991
+ if (frame.type === "turn_stream") {
992
+ runtime.server.pushTurnStreamUnchecked(
993
+ String(frame.sessionId),
994
+ frame.phase === "live" ? "live" : "finalized",
995
+ String(frame.text),
996
+ typeof frame.finalAnswer === "boolean" ? frame.finalAnswer : undefined,
997
+ typeof frame.messageRef === "string" ? frame.messageRef : undefined,
998
+ );
999
+ return;
1000
+ }
941
1001
  runtime.server.pushFrame(JSON.stringify(frame));
942
1002
  }
943
1003
 
1004
+ function pushFileAttachment(
1005
+ runtime: Pick<SessionRuntime, "server" | "host">,
1006
+ frame: { type: "file_attachment"; sessionId: string; name: string; mime?: string; caption?: string },
1007
+ data: Buffer,
1008
+ ): void {
1009
+ runtime.host.emitEvent({ kind: frame.type, payload: { ...frame, data: data.toString("base64") } });
1010
+ runtime.server.pushFileAttachmentUnchecked(frame.sessionId, frame.name, frame.mime, data, frame.caption);
1011
+ }
1012
+
944
1013
  /** Agent lifecycle is SDK session truth, independent of optional chat delivery. */
945
1014
  function emitAgentLifecycle(
946
1015
  runtime: Pick<SessionRuntime, "server" | "host">,
@@ -981,6 +1050,7 @@ const defaultConfig: NotificationConfig = {
981
1050
  rich: { enabled: true },
982
1051
  richDraft: { enabled: false },
983
1052
  topics: {},
1053
+ btw: { enabled: true },
984
1054
  };
985
1055
 
986
1056
  export function notificationsEnabled(): boolean {
@@ -1628,11 +1698,18 @@ function hasTerminalArbitrationCapability(
1628
1698
  Required<
1629
1699
  Pick<
1630
1700
  WorkflowGateEmitter,
1631
- "resolveGate" | "prepareTerminalization" | "clearPreparedTerminalization" | "registerGateTerminalController"
1701
+ | "resolveGate"
1702
+ | "recoverAcceptedGates"
1703
+ | "lookupCompletedResolution"
1704
+ | "prepareTerminalization"
1705
+ | "clearPreparedTerminalization"
1706
+ | "registerGateTerminalController"
1632
1707
  >
1633
1708
  > {
1634
1709
  return (
1635
1710
  typeof workflowGate?.resolveGate === "function" &&
1711
+ typeof workflowGate.recoverAcceptedGates === "function" &&
1712
+ typeof workflowGate.lookupCompletedResolution === "function" &&
1636
1713
  typeof workflowGate.prepareTerminalization === "function" &&
1637
1714
  typeof workflowGate.clearPreparedTerminalization === "function" &&
1638
1715
  typeof workflowGate.registerGateTerminalController === "function"
@@ -1986,7 +2063,7 @@ function sdkControlSurface(
1986
2063
  pending.completeDirect();
1987
2064
  return { resolved: true };
1988
2065
  },
1989
- answerGate: async (id, response, expectedSessionId) => {
2066
+ answerGate: async (id, response, expectedSessionId, idempotencyKey) => {
1990
2067
  if (!acceptGateResolution())
1991
2068
  throw Object.assign(new Error("Workflow gate is no longer answerable."), { code: "resource_gone" });
1992
2069
  if (expectedSessionId === undefined) auditMissingExpectedSessionId("workflow.gate_answer");
@@ -1999,6 +2076,26 @@ function sdkControlSurface(
1999
2076
  throw Object.assign(new Error("Workflow gates are unavailable for this session."), {
2000
2077
  code: "resource_gone",
2001
2078
  });
2079
+ const workflowGate = ctx.workflowGate;
2080
+ if (!hasTerminalArbitrationCapability(workflowGate))
2081
+ throw Object.assign(new Error("Workflow gates are unavailable for this session."), {
2082
+ code: "resource_gone",
2083
+ });
2084
+ const gateResponse = {
2085
+ gate_id: id,
2086
+ answer: response,
2087
+ idempotency_key: idempotencyKey ?? id,
2088
+ };
2089
+ const completed = workflowGate.lookupCompletedResolution(gateResponse);
2090
+ if (completed.kind === "completed") return completed.resolution;
2091
+ if (completed.kind === "accepted_incomplete") {
2092
+ await trackGateResolution(workflowGate.recoverAcceptedGates());
2093
+ const recovered = workflowGate.lookupCompletedResolution(gateResponse);
2094
+ if (recovered.kind === "completed") return recovered.resolution;
2095
+ throw Object.assign(new Error("Workflow gate resolution outcome is uncertain."), {
2096
+ code: "terminal_uncertain",
2097
+ });
2098
+ }
2002
2099
  const prepared = presentations.prepareDirectControl(id);
2003
2100
  if (!prepared || prepared.status === "stale")
2004
2101
  throw Object.assign(new Error("Workflow gate is no longer answerable."), { code: "resource_gone" });
@@ -2007,31 +2104,22 @@ function sdkControlSurface(
2007
2104
  if (prepared.status !== "queued" && prepared.status !== "retired")
2008
2105
  throw new Error(`Unexpected direct control preparation: ${prepared.status}`);
2009
2106
  if (
2010
- ctx.workflowGate?.prepareTerminalization?.(
2011
- id,
2012
- prepared.status === "queued" ? "not_published" : "retired",
2013
- ) !== true
2107
+ workflowGate.prepareTerminalization(id, prepared.status === "queued" ? "not_published" : "retired") !== true
2014
2108
  ) {
2015
2109
  presentations.finishDirectControl(id, prepared, "rejected");
2016
2110
  throw Object.assign(new Error("Workflow gate lacks a terminalization proof."), { code: "resource_gone" });
2017
2111
  }
2018
2112
  try {
2019
- const resolution = await trackGateResolution(
2020
- ctx.workflowGate?.resolveGate?.({
2021
- gate_id: id,
2022
- answer: response,
2023
- idempotency_key: id,
2024
- }) ?? unavailable("workflow.gate_answer", "workflow gates are unavailable for this session")(),
2025
- );
2113
+ const resolution = await trackGateResolution(workflowGate.resolveGate(gateResponse));
2026
2114
  const status = (resolution as { status?: unknown }).status;
2027
2115
  if (status === "accepted" || status === "rejected") {
2028
- if (status === "rejected") ctx.workflowGate?.clearPreparedTerminalization?.(id);
2116
+ if (status === "rejected") workflowGate.clearPreparedTerminalization(id);
2029
2117
  presentations.finishDirectControl(id, prepared, status);
2030
2118
  return resolution;
2031
2119
  }
2032
2120
  } catch (error) {
2033
2121
  const outcome = reconcileDirectControlFailure(id);
2034
- if (outcome === "rejected") ctx.workflowGate?.clearPreparedTerminalization?.(id);
2122
+ if (outcome === "rejected") workflowGate.clearPreparedTerminalization(id);
2035
2123
  presentations.finishDirectControl(id, prepared, outcome);
2036
2124
  if (outcome === "unknown")
2037
2125
  throw Object.assign(new Error("Workflow gate resolution outcome is uncertain."), {
@@ -2040,7 +2128,7 @@ function sdkControlSurface(
2040
2128
  throw error;
2041
2129
  }
2042
2130
  const outcome = reconcileDirectControlFailure(id);
2043
- if (outcome === "rejected") ctx.workflowGate?.clearPreparedTerminalization?.(id);
2131
+ if (outcome === "rejected") workflowGate.clearPreparedTerminalization(id);
2044
2132
  presentations.finishDirectControl(id, prepared, outcome);
2045
2133
  logger.warn("workflow_gate_direct_control_uncertain_outcome", {
2046
2134
  operation: "workflow.gate_answer",
@@ -2064,6 +2152,22 @@ function sdkControlSurface(
2064
2152
  throw Object.assign(new Error("Workflow gates are unavailable for this session."), {
2065
2153
  code: "resource_gone",
2066
2154
  });
2155
+ const workflowGate = ctx.workflowGate;
2156
+ if (!hasTerminalArbitrationCapability(workflowGate))
2157
+ throw Object.assign(new Error("Workflow gates are unavailable for this session."), {
2158
+ code: "resource_gone",
2159
+ });
2160
+ const gateResponse = { gate_id: id, answer: choice, idempotency_key: id };
2161
+ const completed = workflowGate.lookupCompletedResolution(gateResponse);
2162
+ if (completed.kind === "completed") return completed.resolution;
2163
+ if (completed.kind === "accepted_incomplete") {
2164
+ await trackGateResolution(workflowGate.recoverAcceptedGates());
2165
+ const recovered = workflowGate.lookupCompletedResolution(gateResponse);
2166
+ if (recovered.kind === "completed") return recovered.resolution;
2167
+ throw Object.assign(new Error("Workflow plan resolution outcome is uncertain."), {
2168
+ code: "terminal_uncertain",
2169
+ });
2170
+ }
2067
2171
  const prepared = presentations.prepareDirectControl(id);
2068
2172
  if (!prepared || prepared.status === "stale")
2069
2173
  throw Object.assign(new Error("Workflow plan is no longer answerable."), { code: "resource_gone" });
@@ -2072,31 +2176,22 @@ function sdkControlSurface(
2072
2176
  if (prepared.status !== "queued" && prepared.status !== "retired")
2073
2177
  throw new Error(`Unexpected direct control preparation: ${prepared.status}`);
2074
2178
  if (
2075
- ctx.workflowGate?.prepareTerminalization?.(
2076
- id,
2077
- prepared.status === "queued" ? "not_published" : "retired",
2078
- ) !== true
2179
+ workflowGate.prepareTerminalization(id, prepared.status === "queued" ? "not_published" : "retired") !== true
2079
2180
  ) {
2080
2181
  presentations.finishDirectControl(id, prepared, "rejected");
2081
2182
  throw Object.assign(new Error("Workflow plan lacks a terminalization proof."), { code: "resource_gone" });
2082
2183
  }
2083
2184
  try {
2084
- const resolution = await trackGateResolution(
2085
- ctx.workflowGate?.resolveGate?.({
2086
- gate_id: id,
2087
- answer: choice,
2088
- idempotency_key: id,
2089
- }) ?? unavailable("workflow.plan_approve", "workflow gates are unavailable for this session")(),
2090
- );
2185
+ const resolution = await trackGateResolution(workflowGate.resolveGate(gateResponse));
2091
2186
  const status = (resolution as { status?: unknown }).status;
2092
2187
  if (status === "accepted" || status === "rejected") {
2093
- if (status === "rejected") ctx.workflowGate?.clearPreparedTerminalization?.(id);
2188
+ if (status === "rejected") workflowGate.clearPreparedTerminalization(id);
2094
2189
  presentations.finishDirectControl(id, prepared, status);
2095
2190
  return resolution;
2096
2191
  }
2097
2192
  } catch (error) {
2098
2193
  const outcome = reconcileDirectControlFailure(id);
2099
- if (outcome === "rejected") ctx.workflowGate?.clearPreparedTerminalization?.(id);
2194
+ if (outcome === "rejected") workflowGate.clearPreparedTerminalization(id);
2100
2195
  presentations.finishDirectControl(id, prepared, outcome);
2101
2196
  if (outcome === "unknown")
2102
2197
  throw Object.assign(new Error("Workflow plan resolution outcome is uncertain."), {
@@ -2105,7 +2200,7 @@ function sdkControlSurface(
2105
2200
  throw error;
2106
2201
  }
2107
2202
  const outcome = reconcileDirectControlFailure(id);
2108
- if (outcome === "rejected") ctx.workflowGate?.clearPreparedTerminalization?.(id);
2203
+ if (outcome === "rejected") workflowGate.clearPreparedTerminalization(id);
2109
2204
  presentations.finishDirectControl(id, prepared, outcome);
2110
2205
  logger.warn("workflow_gate_direct_control_uncertain_outcome", {
2111
2206
  operation: "workflow.plan_approve",
@@ -2124,14 +2219,14 @@ function sdkControlSurface(
2124
2219
  throw Object.assign(new Error("skill.invoke args must be a string."), { code: "invalid_input" });
2125
2220
  return ctx.invokeSkill(name, args);
2126
2221
  },
2127
- setPlanMode: on => {
2222
+ setPlanMode: async on => {
2128
2223
  if (!bindings.has("setPlanMode") || !ctx.setPlanMode)
2129
2224
  return unavailable("mode.plan.set", "no plan-mode seam is installed")();
2130
2225
 
2131
2226
  if (typeof on !== "boolean")
2132
2227
  throw Object.assign(new Error("mode.plan.set requires a boolean on value."), { code: "invalid_input" });
2133
2228
 
2134
- return { state: ctx.setPlanMode(on) };
2229
+ return { state: await ctx.setPlanMode(on) };
2135
2230
  },
2136
2231
  operateGoal: (op, objective) => {
2137
2232
  if (!bindings.has("operateGoal") || !ctx.operateGoal)
@@ -2252,6 +2347,367 @@ function sdkControlSurface(
2252
2347
  return surface;
2253
2348
  }
2254
2349
 
2350
+ const EPHEMERAL_TURN_DEADLINE_MS = 120_000;
2351
+ const EPHEMERAL_TURN_TTL_MS = 300_000;
2352
+ const EPHEMERAL_TURN_MAX_RECORDS = 256;
2353
+ const EPHEMERAL_TURN_MAX_ACTIVE_PER_SESSION = 2;
2354
+ const EPHEMERAL_TURN_MAX_RESULT_BYTES = 262_144;
2355
+
2356
+ interface EphemeralTurnTuple {
2357
+ sessionId: string;
2358
+ requestId: string;
2359
+ updateId: number;
2360
+ messageId: number;
2361
+ threadId: string;
2362
+ }
2363
+
2364
+ type EphemeralTurnStatus = "ok" | "busy" | "timeout" | "cancelled" | "session_unavailable" | "failed";
2365
+
2366
+ interface EphemeralTurnAuthority {
2367
+ sessionId: string;
2368
+ endpointDigest: string;
2369
+ eventGeneration: number;
2370
+ }
2371
+
2372
+ interface EphemeralTurnEvent {
2373
+ tuple: EphemeralTurnTuple;
2374
+ authority: EphemeralTurnAuthority;
2375
+ status: EphemeralTurnStatus;
2376
+ text?: string;
2377
+ completedAt: number;
2378
+ expiresAt: number;
2379
+ }
2380
+
2381
+ interface EphemeralTurnTombstone {
2382
+ tuple: EphemeralTurnTuple;
2383
+ authority: EphemeralTurnAuthority;
2384
+ status: EphemeralTurnStatus;
2385
+ completedAt: number;
2386
+ expiresAt: number;
2387
+ }
2388
+
2389
+ interface ActiveEphemeralTurn {
2390
+ tuple: EphemeralTurnTuple;
2391
+ authority: EphemeralTurnAuthority;
2392
+ connectionId: string;
2393
+ staleConnectionIds: Set<string>;
2394
+ controller: AbortController;
2395
+ subscribers: Set<string>;
2396
+ deadline: NodeJS.Timeout;
2397
+ abortListener: () => void;
2398
+ }
2399
+
2400
+ function ephemeralTuple(frame: Record<string, unknown>): EphemeralTurnTuple | undefined {
2401
+ const { sessionId, requestId, updateId, messageId, threadId } = frame;
2402
+ return typeof sessionId === "string" &&
2403
+ typeof requestId === "string" &&
2404
+ typeof updateId === "number" &&
2405
+ Number.isSafeInteger(updateId) &&
2406
+ typeof messageId === "number" &&
2407
+ Number.isSafeInteger(messageId) &&
2408
+ messageId > 0 &&
2409
+ typeof threadId === "string"
2410
+ ? { sessionId, requestId, updateId, messageId, threadId }
2411
+ : undefined;
2412
+ }
2413
+
2414
+ function sameEphemeralTuple(left: EphemeralTurnTuple, right: EphemeralTurnTuple): boolean {
2415
+ return (
2416
+ left.sessionId === right.sessionId &&
2417
+ left.requestId === right.requestId &&
2418
+ left.updateId === right.updateId &&
2419
+ left.messageId === right.messageId &&
2420
+ left.threadId === right.threadId
2421
+ );
2422
+ }
2423
+
2424
+ function ephemeralTupleKey(tuple: EphemeralTurnTuple): string {
2425
+ return JSON.stringify([tuple.sessionId, tuple.requestId, tuple.updateId, tuple.messageId, tuple.threadId]);
2426
+ }
2427
+
2428
+ /** Host-owned, bounded idempotency and cancellation lifecycle for v3 side turns. */
2429
+ export class EphemeralTurnHost {
2430
+ #active = new Map<string, ActiveEphemeralTurn>();
2431
+ #terminalEvents = new Map<string, EphemeralTurnEvent>();
2432
+ #tombstones = new Map<string, EphemeralTurnTombstone>();
2433
+ #expiryTimer: NodeJS.Timeout | undefined;
2434
+ #disposed = false;
2435
+ #enabled = true;
2436
+ #now: () => number;
2437
+ #sendTo: (connectionId: string, frame: Record<string, unknown>) => void;
2438
+ #execute: (question: string, signal: AbortSignal) => Promise<{ replyText: string }>;
2439
+ #authority: EphemeralTurnAuthority | undefined;
2440
+
2441
+ constructor(
2442
+ sendTo: (connectionId: string, frame: Record<string, unknown>) => void,
2443
+ execute: (question: string, signal: AbortSignal) => Promise<{ replyText: string }>,
2444
+ now: () => number = Date.now,
2445
+ ) {
2446
+ this.#sendTo = sendTo;
2447
+ this.#execute = execute;
2448
+ this.#now = now;
2449
+ }
2450
+
2451
+ configureAuthority(authority: EphemeralTurnAuthority): void {
2452
+ if (this.#authority && !this.#sameAuthority(this.#authority, authority))
2453
+ for (const active of [...this.#active.values()]) active.controller.abort("session_unavailable");
2454
+ this.#authority = { ...authority };
2455
+ }
2456
+
2457
+ disable(): void {
2458
+ if (this.#disposed) return;
2459
+ this.#enabled = false;
2460
+ for (const active of this.#active.values()) active.controller.abort("session_unavailable");
2461
+ this.#terminalEvents.clear();
2462
+ this.#tombstones.clear();
2463
+ if (this.#expiryTimer) clearTimeout(this.#expiryTimer);
2464
+ this.#expiryTimer = undefined;
2465
+ }
2466
+
2467
+ enable(): void {
2468
+ if (!this.#disposed) this.#enabled = true;
2469
+ }
2470
+
2471
+ dispose(): void {
2472
+ this.#disposed = true;
2473
+ for (const active of this.#active.values()) active.controller.abort("session_unavailable");
2474
+ if (this.#expiryTimer) clearTimeout(this.#expiryTimer);
2475
+ this.#expiryTimer = undefined;
2476
+ this.#terminalEvents.clear();
2477
+ this.#tombstones.clear();
2478
+ }
2479
+
2480
+ handle(connectionId: string, frame: Record<string, unknown>): boolean {
2481
+ if (!this.#enabled) return frame.type === "ephemeral_turn" || frame.type === "ephemeral_turn_cancel";
2482
+ if (frame.type === "ephemeral_turn") return this.#start(connectionId, frame);
2483
+ if (frame.type === "ephemeral_turn_cancel") return this.#cancel(connectionId, frame);
2484
+ return false;
2485
+ }
2486
+
2487
+ sessionUnavailable(sessionId: string): void {
2488
+ for (const active of [...this.#active.values()])
2489
+ if (active.tuple.sessionId === sessionId) active.controller.abort("session_unavailable");
2490
+ }
2491
+
2492
+ /** Testable event-ring eviction boundary; tombstones remain idempotency authority. */
2493
+ evictTerminalEvents(): void {
2494
+ this.#terminalEvents.clear();
2495
+ }
2496
+
2497
+ #start(connectionId: string, frame: Record<string, unknown>): boolean {
2498
+ const tuple = ephemeralTuple(frame);
2499
+ const question = typeof frame.question === "string" ? frame.question.trim() : "";
2500
+ const authority = this.#authority;
2501
+ if (!tuple || !question || !authority || tuple.sessionId !== authority.sessionId) return true;
2502
+ this.#purge();
2503
+ const key = ephemeralTupleKey(tuple);
2504
+ const active = this.#active.get(key);
2505
+ if (active) {
2506
+ if (!this.#sameAuthority(active.authority, authority)) {
2507
+ active.controller.abort("session_unavailable");
2508
+ return true;
2509
+ }
2510
+ if (active.connectionId === connectionId || active.staleConnectionIds.has(connectionId)) return true;
2511
+ active.staleConnectionIds.add(active.connectionId);
2512
+ active.connectionId = connectionId;
2513
+ active.subscribers = new Set([connectionId]);
2514
+ return true;
2515
+ }
2516
+ const event = this.#terminalEvents.get(key);
2517
+ if (event) {
2518
+ if (this.#sameAuthority(event.authority, authority))
2519
+ this.#send(connectionId, event.tuple, event.status, event.text);
2520
+ return true;
2521
+ }
2522
+ const tombstone = this.#tombstones.get(key);
2523
+ if (tombstone) {
2524
+ if (this.#sameAuthority(tombstone.authority, authority)) this.#send(connectionId, tombstone.tuple, "failed");
2525
+ return true;
2526
+ }
2527
+ for (const candidate of this.#tombstones.values()) {
2528
+ if (candidate.tuple.sessionId === tuple.sessionId && candidate.tuple.requestId === tuple.requestId) {
2529
+ logger.warn("notifications: ephemeral request id conflict", {
2530
+ sessionId: tuple.sessionId,
2531
+ requestId: tuple.requestId,
2532
+ });
2533
+ return true;
2534
+ }
2535
+ }
2536
+ for (const candidate of this.#active.values()) {
2537
+ if (candidate.tuple.sessionId === tuple.sessionId && candidate.tuple.requestId === tuple.requestId) {
2538
+ logger.warn("notifications: ephemeral request id conflict", {
2539
+ sessionId: tuple.sessionId,
2540
+ requestId: tuple.requestId,
2541
+ });
2542
+ return true;
2543
+ }
2544
+ }
2545
+ const activeForSession = [...this.#active.values()].filter(
2546
+ candidate => candidate.tuple.sessionId === tuple.sessionId,
2547
+ ).length;
2548
+ if (activeForSession >= EPHEMERAL_TURN_MAX_ACTIVE_PER_SESSION) {
2549
+ const completedAt = this.#now();
2550
+ this.#finish(key, {
2551
+ tuple,
2552
+ authority: { ...authority },
2553
+ status: "busy",
2554
+ completedAt,
2555
+ expiresAt: completedAt + EPHEMERAL_TURN_TTL_MS,
2556
+ });
2557
+ this.#send(connectionId, tuple, "busy");
2558
+ return true;
2559
+ }
2560
+ const controller = new AbortController();
2561
+ const abortListener = () => this.#complete(key, this.#abortStatus(controller.signal));
2562
+ const record: ActiveEphemeralTurn = {
2563
+ tuple,
2564
+ authority: { ...authority },
2565
+ connectionId,
2566
+ controller,
2567
+ subscribers: new Set([connectionId]),
2568
+ staleConnectionIds: new Set(),
2569
+ deadline: setTimeout(() => controller.abort("timeout"), EPHEMERAL_TURN_DEADLINE_MS),
2570
+ abortListener,
2571
+ };
2572
+ this.#active.set(key, record);
2573
+ controller.signal.addEventListener("abort", abortListener, { once: true });
2574
+ void this.#execute(question, controller.signal).then(
2575
+ result =>
2576
+ this.#complete(
2577
+ key,
2578
+ controller.signal.aborted ? this.#abortStatus(controller.signal) : "ok",
2579
+ result.replyText,
2580
+ ),
2581
+ () => this.#complete(key, controller.signal.aborted ? this.#abortStatus(controller.signal) : "failed"),
2582
+ );
2583
+ return true;
2584
+ }
2585
+
2586
+ #cancel(connectionId: string, frame: Record<string, unknown>): boolean {
2587
+ const tuple = ephemeralTuple(frame);
2588
+ const authority = this.#authority;
2589
+ if (!tuple || frame.reason !== "daemon_shutdown" || !authority || tuple.sessionId !== authority.sessionId)
2590
+ return true;
2591
+ const active = this.#active.get(ephemeralTupleKey(tuple));
2592
+ if (
2593
+ !active ||
2594
+ !sameEphemeralTuple(active.tuple, tuple) ||
2595
+ active.connectionId !== connectionId ||
2596
+ !this.#sameAuthority(active.authority, authority)
2597
+ )
2598
+ return true;
2599
+ active.controller.abort("cancelled");
2600
+ return true;
2601
+ }
2602
+
2603
+ #abortStatus(signal: AbortSignal): EphemeralTurnStatus {
2604
+ return signal.reason === "timeout"
2605
+ ? "timeout"
2606
+ : signal.reason === "session_unavailable"
2607
+ ? "session_unavailable"
2608
+ : "cancelled";
2609
+ }
2610
+
2611
+ #sameAuthority(left: EphemeralTurnAuthority, right: EphemeralTurnAuthority): boolean {
2612
+ return (
2613
+ left.sessionId === right.sessionId &&
2614
+ left.endpointDigest === right.endpointDigest &&
2615
+ left.eventGeneration === right.eventGeneration
2616
+ );
2617
+ }
2618
+
2619
+ #complete(key: string, status: EphemeralTurnStatus, text?: string): void {
2620
+ const active = this.#active.get(key);
2621
+ if (!active) return;
2622
+ clearTimeout(active.deadline);
2623
+ active.controller.signal.removeEventListener("abort", active.abortListener);
2624
+ this.#active.delete(key);
2625
+ if (this.#disposed || !this.#enabled) return;
2626
+ const terminalTextIsValid =
2627
+ typeof text === "string" &&
2628
+ text.trim().length > 0 &&
2629
+ Buffer.byteLength(text, "utf8") <= EPHEMERAL_TURN_MAX_RESULT_BYTES;
2630
+ const terminalStatus = status === "ok" && !terminalTextIsValid ? "failed" : status;
2631
+ const completedAt = this.#now();
2632
+ const terminal: EphemeralTurnEvent = {
2633
+ tuple: active.tuple,
2634
+ authority: active.authority,
2635
+ status: terminalStatus,
2636
+ ...(terminalStatus === "ok" ? { text: text ?? "" } : {}),
2637
+ completedAt,
2638
+ expiresAt: completedAt + EPHEMERAL_TURN_TTL_MS,
2639
+ };
2640
+ this.#finish(key, terminal);
2641
+ for (const connectionId of active.subscribers) {
2642
+ try {
2643
+ this.#send(connectionId, terminal.tuple, terminal.status, terminal.text);
2644
+ } catch {
2645
+ // Directed SDK delivery has already logged the disconnected route.
2646
+ }
2647
+ }
2648
+ }
2649
+
2650
+ #finish(key: string, terminal: EphemeralTurnEvent): void {
2651
+ this.#terminalEvents.set(key, terminal);
2652
+ this.#tombstones.set(key, {
2653
+ tuple: terminal.tuple,
2654
+ authority: terminal.authority,
2655
+ status: terminal.status,
2656
+ completedAt: terminal.completedAt,
2657
+ expiresAt: terminal.expiresAt,
2658
+ });
2659
+ this.#purge();
2660
+ while (this.#terminalEvents.size > EPHEMERAL_TURN_MAX_RECORDS)
2661
+ this.#terminalEvents.delete(this.#terminalEvents.keys().next().value!);
2662
+ while (this.#tombstones.size > EPHEMERAL_TURN_MAX_RECORDS) {
2663
+ const oldestKey = this.#tombstones.keys().next().value!;
2664
+ this.#tombstones.delete(oldestKey);
2665
+ this.#terminalEvents.delete(oldestKey);
2666
+ }
2667
+ this.#scheduleExpiry();
2668
+ }
2669
+
2670
+ #send(connectionId: string, tuple: EphemeralTurnTuple, status: EphemeralTurnStatus, text?: string): void {
2671
+ this.#sendTo(connectionId, {
2672
+ type: "ephemeral_turn_result",
2673
+ ...tuple,
2674
+ status,
2675
+ ...(status === "ok" ? { text: text ?? "" } : {}),
2676
+ });
2677
+ }
2678
+
2679
+ #purge(): void {
2680
+ const now = this.#now();
2681
+ for (const [key, tombstone] of this.#tombstones) {
2682
+ if (tombstone.expiresAt > now) continue;
2683
+ this.#tombstones.delete(key);
2684
+ this.#terminalEvents.delete(key);
2685
+ }
2686
+ }
2687
+
2688
+ #scheduleExpiry(): void {
2689
+ if (this.#disposed) return;
2690
+ if (this.#expiryTimer) clearTimeout(this.#expiryTimer);
2691
+ const nextExpiry = [...this.#tombstones.values()].reduce(
2692
+ (earliest, tombstone) => Math.min(earliest, tombstone.expiresAt),
2693
+ Number.POSITIVE_INFINITY,
2694
+ );
2695
+ if (!Number.isFinite(nextExpiry)) {
2696
+ this.#expiryTimer = undefined;
2697
+ return;
2698
+ }
2699
+ this.#expiryTimer = setTimeout(
2700
+ () => {
2701
+ this.#expiryTimer = undefined;
2702
+ if (this.#disposed) return;
2703
+ this.#purge();
2704
+ this.#scheduleExpiry();
2705
+ },
2706
+ Math.max(0, nextExpiry - this.#now()),
2707
+ );
2708
+ this.#expiryTimer.unref();
2709
+ }
2710
+ }
2255
2711
  /** Parse only v3 frames carried through the existing control-command seam. */
2256
2712
  function sdkInboundFrame(commandJson: string | undefined): Record<string, unknown> | undefined {
2257
2713
  if (!commandJson) return undefined;
@@ -2287,6 +2743,7 @@ export function createNotificationsExtension(
2287
2743
  controller?: NotificationSessionController;
2288
2744
 
2289
2745
  onSdkRequest?: (kind: "control" | "query", connectionId: string, frame: Record<string, unknown>) => void;
2746
+ runEphemeralTurn?: (promptText: string, signal: AbortSignal) => Promise<{ replyText: string }>;
2290
2747
  } = {},
2291
2748
  ): void {
2292
2749
  const lifecycleStartupCapability = lifecycleStartupCapabilityForApi(api);
@@ -2305,6 +2762,7 @@ export function createNotificationsExtension(
2305
2762
  let activeRuntimeId: string | undefined;
2306
2763
  let identityControlInFlight = false;
2307
2764
  let deferredIdentityRotation: { event: { previousSessionFile?: string }; ctx: ExtensionContext } | undefined;
2765
+ let extensionShuttingDown = false;
2308
2766
 
2309
2767
  async function ensureTelegramOwner(
2310
2768
  settings: Settings,
@@ -2352,7 +2810,10 @@ export function createNotificationsExtension(
2352
2810
  const activeRuntime = runtimes.get(id);
2353
2811
  const requestedRuntime = retryRuntime ?? activeRuntime;
2354
2812
  if (expectedRuntime && requestedRuntime !== expectedRuntime) return false;
2355
- if (reason === "session" && requestedRuntime) requestedRuntime.stopping = true;
2813
+ if (reason === "session" && requestedRuntime) {
2814
+ requestedRuntime.stopping = true;
2815
+ requestedRuntime.abortEphemeralTurns();
2816
+ }
2356
2817
  if (reason === "session" && requestedRuntime) {
2357
2818
  // Fence the exact runtime before awaiting its startup promise: a late start
2358
2819
  // must observe removal and clean itself up rather than becoming reachable.
@@ -2365,7 +2826,12 @@ export function createNotificationsExtension(
2365
2826
  .catch(() => {})
2366
2827
  .then(() => {
2367
2828
  if (runtimes.get(id) === requestedRuntime || cleanupRetries.get(id) === requestedRuntime)
2368
- void stopSession(id, reason, requestedRuntime);
2829
+ void stopSession(id, reason, requestedRuntime).catch(error =>
2830
+ // A retained owner-release failure keeps the exact runtime in
2831
+ // cleanupRetries for a later retry; log it rather than letting a
2832
+ // fire-and-forget rejection become a fatal unhandled rejection.
2833
+ logger.error(`notifications: SDK notification runtime cleanup failed: ${String(error)}`),
2834
+ );
2369
2835
  });
2370
2836
  const rt = requestedRuntime;
2371
2837
 
@@ -2377,6 +2843,7 @@ export function createNotificationsExtension(
2377
2843
  }
2378
2844
  if (reason === "notifications" && rt.host.started) {
2379
2845
  rt.notificationsActive = false;
2846
+ rt.disableEphemeralTurns();
2380
2847
  try {
2381
2848
  rt.disposeAnswerSource();
2382
2849
  } catch {}
@@ -2395,9 +2862,6 @@ export function createNotificationsExtension(
2395
2862
  try {
2396
2863
  rt.cancelPostmortemCleanup();
2397
2864
  } catch {}
2398
- try {
2399
- rt.stopBrokerHeartbeat();
2400
- } catch {}
2401
2865
  try {
2402
2866
  rt.disposeAnswerSource();
2403
2867
  } catch {}
@@ -2407,6 +2871,9 @@ export function createNotificationsExtension(
2407
2871
  try {
2408
2872
  rt.disposeGateListener();
2409
2873
  } catch {}
2874
+ try {
2875
+ rt.workflowGate?.setRuntimeTurnProvider?.(null);
2876
+ } catch {}
2410
2877
  await rt.waitForGateResolutionQuiescence();
2411
2878
  try {
2412
2879
  rt.disposeAckRecoveryParticipant();
@@ -2559,7 +3026,6 @@ export function createNotificationsExtension(
2559
3026
  }
2560
3027
  const gatePresentations = new PresentationArbiter(server, () => runtime?.redact ?? redact, tag);
2561
3028
  let inboundSdkFrame: ((connectionId: string, frame: Record<string, unknown>) => void) | undefined;
2562
- let stopBrokerHeartbeat = () => {};
2563
3029
  const inFlightGateResolutions = new Set<Promise<void>>();
2564
3030
  const trackGateResolution = <T>(resolution: Promise<T>): Promise<T> => {
2565
3031
  const quiesced = resolution.then(
@@ -2601,6 +3067,8 @@ export function createNotificationsExtension(
2601
3067
  if (capability === "permission") ctx.setSdkPermissionProvider?.(undefined);
2602
3068
  };
2603
3069
 
3070
+ const hostCapCache = new Map<string, ReadonlySet<string>>();
3071
+
2604
3072
  const configOverrides = new Map<string, unknown>();
2605
3073
  const configRevision = { current: 0 };
2606
3074
  const PROMPT_SUBMISSION_CAPACITY = 128;
@@ -2759,6 +3227,11 @@ export function createNotificationsExtension(
2759
3227
  submission.failed = true;
2760
3228
  submission.error = error;
2761
3229
  removePendingPromptCorrelation(correlation);
3230
+ if (
3231
+ runtime?.activePromptCorrelation?.commandId === correlation.commandId &&
3232
+ runtime.activePromptCorrelation.turnId === correlation.turnId
3233
+ )
3234
+ runtime.activePromptCorrelation = undefined;
2762
3235
  emitPromptFailure(correlation, error);
2763
3236
  };
2764
3237
  const acknowledgePrompt = (connectionId: string, correlation: { commandId: string; turnId: string }) => {
@@ -2826,6 +3299,10 @@ export function createNotificationsExtension(
2826
3299
  };
2827
3300
 
2828
3301
  const sendSdkFrame = (connectionId: string, frame: Record<string, unknown>) => {
3302
+ if (extensionShuttingDown || runtime?.stopping || runtimes.get(id) !== runtime) {
3303
+ abandonPromptResponse(connectionId, frame);
3304
+ return;
3305
+ }
2829
3306
  const json = JSON.stringify(frame);
2830
3307
  if (connectionId.startsWith("seam:")) {
2831
3308
  try {
@@ -2857,6 +3334,7 @@ export function createNotificationsExtension(
2857
3334
  stateRoot,
2858
3335
  token,
2859
3336
  sendFrame: (connectionId, frame) => sendSdkFrame(connectionId, frame),
3337
+ connectionCapabilities: connectionId => hostCapCache.get(connectionId) ?? EMPTY_CAPABILITIES,
2860
3338
  installProviderDefinitions,
2861
3339
  onProviderDefinitionsRemoved: removeProviderDefinitions,
2862
3340
  onFrame: handler => {
@@ -2974,14 +3452,16 @@ export function createNotificationsExtension(
2974
3452
  disposeGateTerminalController: () => {},
2975
3453
  disposeAckRecoveryParticipant: () => {},
2976
3454
  disposeGateEmitterListener: () => {},
3455
+ trackGateResolution,
2977
3456
  waitForGateResolutionQuiescence: async () => {
2978
3457
  await Promise.allSettled(inFlightGateResolutions);
2979
3458
  },
2980
3459
  workflowGate: undefined,
2981
3460
  gatePresentations,
2982
3461
  stopping: false,
3462
+ abortEphemeralTurns: () => {},
3463
+ disableEphemeralTurns: () => {},
2983
3464
  cancelPostmortemCleanup: () => {},
2984
- stopBrokerHeartbeat,
2985
3465
 
2986
3466
  redact,
2987
3467
  verbosity,
@@ -2993,8 +3473,10 @@ export function createNotificationsExtension(
2993
3473
  recordPromptTerminal,
2994
3474
  emitPromptLifecycle,
2995
3475
  pendingInbound: new Set<number>(),
3476
+ inFlightTools: new Map<string, { toolName: string; args: unknown }>(),
2996
3477
  };
2997
- runtimes.set(id, runtime);
3478
+ const initializedRuntime = runtime;
3479
+ runtimes.set(id, initializedRuntime);
2998
3480
  activeRuntimeId = id;
2999
3481
  const startSettled = Promise.withResolvers<SessionStartResult>();
3000
3482
  sessionStartPromises.set(id, startSettled.promise);
@@ -3028,16 +3510,40 @@ export function createNotificationsExtension(
3028
3510
  } catch {}
3029
3511
  };
3030
3512
 
3513
+ const ephemeralTurns = new EphemeralTurnHost(sendSdkFrame, async (question, signal) => {
3514
+ if (!options.runEphemeralTurn) throw new Error("Ephemeral turns are unavailable.");
3515
+ return await options.runEphemeralTurn(prompt.render(btwUserPrompt, { question }), signal);
3516
+ });
3517
+ initializedRuntime.abortEphemeralTurns = () => ephemeralTurns.dispose();
3518
+ initializedRuntime.disableEphemeralTurns = () => ephemeralTurns.disable();
3031
3519
  try {
3032
3520
  server.onSdkFrame((err, inbound) => {
3033
3521
  if (err || !inbound) return;
3034
3522
  try {
3035
- inboundSdkFrame?.(inbound.connectionId, JSON.parse(inbound.json) as Record<string, unknown>);
3523
+ const frame = JSON.parse(inbound.json) as unknown;
3524
+ if (!frame || typeof frame !== "object") return;
3525
+ const typedFrame = frame as Record<string, unknown>;
3526
+ if (typedFrame.type === "ephemeral_turn" || typedFrame.type === "ephemeral_turn_cancel") return;
3527
+ inboundSdkFrame?.(inbound.connectionId, typedFrame);
3036
3528
  } catch {}
3037
3529
  });
3530
+ // Required: the negotiated-capability callback is how the TS host learns
3531
+ // each connection's caps for replay-frame gating. If the linked
3532
+ // @gajae-code/natives binary predates it (linked/deduped installs where the
3533
+ // version did not change), fail loudly with an actionable message instead of
3534
+ // silently shipping a half-wired capability bridge.
3535
+ if (typeof server.onNegotiatedCapabilities !== "function") {
3536
+ throw new Error(
3537
+ "@gajae-code/natives is out of date: missing onNegotiatedCapabilities. Rebuild the native addon (bun --cwd=packages/natives run build).",
3538
+ );
3539
+ }
3540
+ server.onNegotiatedCapabilities((_err, connectionId, capabilities) => {
3541
+ if (connectionId) hostCapCache.set(connectionId, new Set(capabilities));
3542
+ });
3038
3543
  server.onConnectionClose((_err, connectionId) => {
3039
3544
  if (!connectionId) return;
3040
3545
  host.handleDisconnect(connectionId);
3546
+ hostCapCache.delete(connectionId);
3041
3547
  for (const submission of promptSubmissions.values())
3042
3548
  if (submission.connectionId === connectionId) abandonPrompt(submission);
3043
3549
  });
@@ -3259,6 +3765,11 @@ export function createNotificationsExtension(
3259
3765
  // thread (forwarded by the daemon over the WS, fail-closed at the daemon).
3260
3766
  server.onInbound((err, inbound) => {
3261
3767
  if (err || !inbound) return;
3768
+ const authenticatedInbound = inbound as typeof inbound & {
3769
+ connectionId: string;
3770
+ messageId?: number;
3771
+ reason?: string;
3772
+ };
3262
3773
  if (inbound.kind === "control_command") {
3263
3774
  const frame = sdkInboundFrame(inbound.commandJson);
3264
3775
  if (frame) {
@@ -3266,6 +3777,25 @@ export function createNotificationsExtension(
3266
3777
  return;
3267
3778
  }
3268
3779
  }
3780
+ if (
3781
+ (inbound.kind === "ephemeral_turn" || inbound.kind === "ephemeral_turn_cancel") &&
3782
+ !runtime?.notificationsActive
3783
+ )
3784
+ return;
3785
+ if (inbound.kind === "ephemeral_turn" || inbound.kind === "ephemeral_turn_cancel") {
3786
+ ephemeralTurns.handle(authenticatedInbound.connectionId, {
3787
+ type: authenticatedInbound.kind,
3788
+ sessionId: authenticatedInbound.sessionId,
3789
+ requestId: authenticatedInbound.requestId,
3790
+ updateId: authenticatedInbound.updateId,
3791
+ messageId: authenticatedInbound.messageId,
3792
+ threadId: authenticatedInbound.threadId,
3793
+ ...(authenticatedInbound.kind === "ephemeral_turn"
3794
+ ? { question: authenticatedInbound.text }
3795
+ : { reason: authenticatedInbound.reason }),
3796
+ });
3797
+ return;
3798
+ }
3269
3799
 
3270
3800
  if (inbound.kind === "user_message") {
3271
3801
  // Inject as a user turn (steers/continues the agent; the resulting
@@ -3313,6 +3843,12 @@ export function createNotificationsExtension(
3313
3843
  update.verbosity = inbound.verbosity;
3314
3844
  }
3315
3845
  if (typeof inbound.redact === "boolean") {
3846
+ // Redact turning ON: terminalize any already-visible in-flight tool
3847
+ // bubbles (while redact is still off so the helper emits) so they never
3848
+ // strand permanently in "started"; subsequent detail is then suppressed.
3849
+ if (inbound.redact && !runtime.redact) {
3850
+ terminalizeInFlightTools(runtime, runtime.id, "unknown");
3851
+ }
3316
3852
  runtime.redact = inbound.redact;
3317
3853
  update.redact = inbound.redact;
3318
3854
  }
@@ -3394,6 +3930,11 @@ export function createNotificationsExtension(
3394
3930
  };
3395
3931
  host.emitEvent({ kind: identityHeader.type, payload: identityHeader });
3396
3932
  const endpoint = await server.start();
3933
+ ephemeralTurns.configureAuthority({
3934
+ sessionId: id,
3935
+ endpointDigest: endpointAuthorityDigest(endpoint.url, token),
3936
+ eventGeneration: host.generation,
3937
+ });
3397
3938
  throwIfLifecycleStopped();
3398
3939
  if (runtimes.get(id) !== runtime) {
3399
3940
  finishStartup({ status: "failed" });
@@ -3437,30 +3978,20 @@ export function createNotificationsExtension(
3437
3978
  },
3438
3979
  });
3439
3980
  throwIfLifecycleStopped();
3440
- runtime.brokerRegistrationActive = true;
3441
- const timer = setInterval(() => {
3442
- void index
3443
- .append({
3444
- type: "host_heartbeat",
3445
- sessionId: id,
3446
- locator,
3447
- endpointGeneration: host.generation,
3448
- pid: process.pid,
3449
- })
3450
- .catch(error => logger.warn(`sdk broker heartbeat failed: ${String(error)}`));
3451
- }, 5_000);
3452
- stopBrokerHeartbeat = () => clearInterval(timer);
3981
+ initializedRuntime.brokerRegistrationActive = true;
3982
+ // Host liveness is derived from alive(pid) when the index is read; heartbeats
3983
+ // are deliberately not appended to the durable session index.
3453
3984
  } catch (brokerError) {
3454
3985
  if (lifecycleRequired) throw brokerError;
3455
3986
  logger.warn(`sdk broker registration skipped: ${String(brokerError)}`);
3456
3987
  }
3457
3988
  }
3458
3989
 
3459
- runtime.stopBrokerHeartbeat = stopBrokerHeartbeat;
3460
- const startedRuntime = runtime;
3461
- runtime.enableNotifications = () => {
3990
+ const startedRuntime = initializedRuntime;
3991
+ initializedRuntime.enableNotifications = () => {
3462
3992
  const runtime = startedRuntime;
3463
3993
  if (runtime.notificationsActive) return;
3994
+ ephemeralTurns.enable();
3464
3995
  runtime.notificationsActive = true;
3465
3996
  runtime.disposeAnswerSource = registerInteractiveAnswerSource(
3466
3997
  runtime.id,
@@ -3471,35 +4002,42 @@ export function createNotificationsExtension(
3471
4002
  if (runtime.redact) return { ok: false, error: TELEGRAM_FILE_REDACTION_ERROR };
3472
4003
  try {
3473
4004
  const data = await fs.promises.readFile(file.path);
3474
- pushSessionFrame(runtime, {
3475
- type: "file_attachment",
3476
- sessionId: runtime.id,
3477
- name: path.basename(file.path),
3478
- data: data.toString("base64"),
3479
- caption: file.caption,
3480
- });
4005
+ pushFileAttachment(
4006
+ runtime,
4007
+ {
4008
+ type: "file_attachment",
4009
+ sessionId: runtime.id,
4010
+ name: path.basename(file.path),
4011
+ caption: file.caption,
4012
+ },
4013
+ data,
4014
+ );
3481
4015
  return { ok: true };
3482
4016
  } catch (e) {
3483
4017
  return { ok: false, error: e instanceof Error ? e.message : String(e) };
3484
4018
  }
3485
4019
  });
3486
4020
  };
3487
- const activeRuntime = runtime;
4021
+ const activeRuntime = initializedRuntime;
3488
4022
  // A native terminal close (SIGHUP), SIGTERM, Ctrl+C exit, or fatal error
3489
4023
  // skips AgentSession.dispose(), so the `session_shutdown` extension event
3490
4024
  // never fires and the daemon-side topic would be orphaned. postmortem
3491
4025
  // awaits registered cleanups on those paths, so send the graceful
3492
4026
  // `session_closed` frame from there too. stopSession() cancels this
3493
4027
  // registration on every other teardown path, so it never double-fires.
3494
- runtime.cancelPostmortemCleanup = postmortem.register(`notifications-session-closed:${id}`, async () => {
3495
- await stopSession(runtime!.id);
3496
- });
4028
+ initializedRuntime.cancelPostmortemCleanup = postmortem.register(
4029
+ `notifications-session-closed:${id}`,
4030
+ async () => {
4031
+ await stopSession(initializedRuntime.id);
4032
+ },
4033
+ );
3497
4034
  logger.info(`notifications: serving session ${id} at ${endpoint.url}`);
3498
4035
  // A workflow-gate emitter can be installed after session startup.
3499
4036
  // Attach dynamically so the SDK bus presents every durable gate.
3500
4037
  const attachWorkflowGate = (gate: WorkflowGateEmitter | undefined): void => {
3501
4038
  if (activeRuntime.workflowGate === gate) return;
3502
4039
  activeRuntime.disposeGateListener();
4040
+ activeRuntime.workflowGate?.setRuntimeTurnProvider?.(null);
3503
4041
  activeRuntime.disposeAckRecoveryParticipant();
3504
4042
  gatePresentations.dispose();
3505
4043
  activeRuntime.disposeGateTerminalController();
@@ -3512,6 +4050,7 @@ export function createNotificationsExtension(
3512
4050
  return;
3513
4051
  }
3514
4052
  activeRuntime.workflowGate = gate;
4053
+ gate.setRuntimeTurnProvider?.(() => activeRuntime.activePromptCorrelation?.turnId);
3515
4054
  if (hasTerminalArbitrationCapability(gate)) {
3516
4055
  const controller: WorkflowGateTerminalController = {
3517
4056
  completeGateInteractions: gateId => gatePresentations.complete(gateId),
@@ -3565,18 +4104,30 @@ export function createNotificationsExtension(
3565
4104
  });
3566
4105
  activeRuntime.disposeAckRecoveryParticipant = () => gate.setAckRecoveryParticipant?.(null);
3567
4106
  }
3568
- void gate.recoverAcceptedGates?.();
4107
+ void (typeof gate.recoverAcceptedGates === "function"
4108
+ ? trackGateResolution(gate.recoverAcceptedGates()).catch(() => {})
4109
+ : Promise.resolve());
3569
4110
  };
3570
4111
  activeRuntime.disposeGateEmitterListener = registerWorkflowGateEmitterListener(id, attachWorkflowGate);
3571
4112
  if (ctx.workflowGate) attachWorkflowGate(ctx.workflowGate);
3572
- if (notificationsEnabledForSession) runtime.enableNotifications();
3573
- finishStartup({ status: "started", runtime });
3574
- return { status: "started", runtime };
4113
+ if (notificationsEnabledForSession) initializedRuntime.enableNotifications();
4114
+ finishStartup({ status: "started", runtime: initializedRuntime });
4115
+ return { status: "started", runtime: initializedRuntime };
3575
4116
  } catch (e) {
3576
4117
  logger.warn(`notifications: failed to start server: ${String(e)}`);
3577
4118
  const result = failLifecycleStartup("failed", e);
3578
4119
  finishStartup(result);
3579
- if (!(await stopSession(id, "session", runtime))) await cleanupAbandonedStartup();
4120
+ let stopped = false;
4121
+ try {
4122
+ stopped = await stopSession(id, "session", runtime);
4123
+ } catch (error) {
4124
+ // A secondary owner-release failure during abandoned-startup cleanup is
4125
+ // retained for an explicit later retry via cleanupRetries; log it rather
4126
+ // than letting it escape startSession and surface a red extension error
4127
+ // through session_start / session_switch / session_branch.
4128
+ logger.error(`notifications: SDK notification runtime cleanup failed: ${String(error)}`);
4129
+ }
4130
+ if (!stopped) await cleanupAbandonedStartup();
3580
4131
  return { ...result, runtime };
3581
4132
  }
3582
4133
  }
@@ -3701,13 +4252,27 @@ export function createNotificationsExtension(
3701
4252
  event: { previousSessionFile?: string },
3702
4253
  ctx: ExtensionContext,
3703
4254
  ): Promise<void> => {
4255
+ if (extensionShuttingDown) return;
3704
4256
  const newId = sessionId(ctx);
3705
4257
  const prevId = activeRuntimeId ?? sessionIdFromFile(event.previousSessionFile);
3706
4258
  if (prevId && prevId !== newId) {
3707
4259
  controller.rekeySession(prevId, newId);
3708
- await stopSession(prevId);
4260
+ try {
4261
+ await stopSession(prevId);
4262
+ } catch (error) {
4263
+ // A retained owner-release failure keeps the exact runtime in
4264
+ // cleanupRetries for an explicit later retry; log it rather than
4265
+ // surfacing a red extension error
4266
+ // while rotating session authority (/new, fork, resume, branch).
4267
+ logger.error(`notifications: SDK notification runtime cleanup failed: ${String(error)}`);
4268
+ }
3709
4269
  }
4270
+ if (extensionShuttingDown) return;
3710
4271
  await startSession(ctx);
4272
+ if (extensionShuttingDown) {
4273
+ await stopSession(newId);
4274
+ return;
4275
+ }
3711
4276
  await controller.reconcileCurrentSession(ctx);
3712
4277
  };
3713
4278
  api.on("session_switch", async (event, ctx) => {
@@ -3725,6 +4290,19 @@ export function createNotificationsExtension(
3725
4290
  await rotateSessionAuthority(event, ctx);
3726
4291
  });
3727
4292
 
4293
+ const terminalizeInFlightTools = (rt: SessionRuntime, id: string, phase: "cancelled" | "unknown"): void => {
4294
+ if (rt.notificationsActive && !rt.redact) {
4295
+ for (const [toolCallId, { toolName }] of rt.inFlightTools) {
4296
+ try {
4297
+ pushSessionFrame(rt, { type: "tool_activity", sessionId: id, toolCallId, toolName, phase });
4298
+ } catch (e) {
4299
+ logger.warn(`notifications: synthetic tool_activity failed: ${String(e)}`);
4300
+ }
4301
+ }
4302
+ }
4303
+ rt.inFlightTools.clear();
4304
+ };
4305
+
3728
4306
  // Drive the live typing indicator: mark busy when the agent loop starts so
3729
4307
  // the daemon shows "typing…" in the thread while the agent is thinking,
3730
4308
  // before any turn output exists. Cleared on `agent_end` below.
@@ -3753,7 +4331,9 @@ export function createNotificationsExtension(
3753
4331
  api.on("turn_start", (_event, ctx) => {
3754
4332
  const id = sessionId(ctx);
3755
4333
  const rt = runtimes.get(id);
3756
- if (!rt?.notificationsActive) return;
4334
+ if (!rt) return;
4335
+ rt.turnSeq = (rt.turnSeq ?? 0) + 1;
4336
+ if (!rt.notificationsActive) return;
3757
4337
  // A new turn is live: re-open the live-stream window (see turnClosed).
3758
4338
  rt.turnClosed = false;
3759
4339
  if (rt.pendingInbound.size === 0) return;
@@ -3771,7 +4351,7 @@ export function createNotificationsExtension(
3771
4351
  // per `turn_end`. turn_end fires once per turn iteration, so a single
3772
4352
  // user-visible idle previously produced many idle pings (the flood); agent_end
3773
4353
  // fires exactly once per settle, yielding exactly one idle notification.
3774
- api.on("agent_end", (_event, ctx) => {
4354
+ api.on("agent_end", (event, ctx) => {
3775
4355
  const id = sessionId(ctx);
3776
4356
  const rt = runtimes.get(id);
3777
4357
  if (!rt) return;
@@ -3784,13 +4364,17 @@ export function createNotificationsExtension(
3784
4364
  } else {
3785
4365
  rt.emitPromptLifecycle(undefined, { type: "agent_end", sessionId: id });
3786
4366
  }
4367
+ rt.activePromptCorrelation = undefined;
4368
+ terminalizeInFlightTools(rt, id, event.stopReason === "cancelled" ? "cancelled" : "unknown");
3787
4369
  try {
3788
4370
  pushSessionFrame(rt, { type: "activity", sessionId: id, state: "idle" });
3789
4371
  } catch (e) {
3790
4372
  logger.warn(`notifications: activity (idle) failed: ${String(e)}`);
3791
4373
  }
3792
4374
  if (!rt.notificationsActive) return;
3793
- void rt.workflowGate?.recoverAcceptedGates?.();
4375
+ void (typeof rt.workflowGate?.recoverAcceptedGates === "function"
4376
+ ? rt.trackGateResolution(rt.workflowGate.recoverAcceptedGates()).catch(() => {})
4377
+ : Promise.resolve());
3794
4378
  const seq = rt.idleSeq++;
3795
4379
  // Re-assert the identity header so the daemon renames the topic once the
3796
4380
  // session title has been auto-generated ("{repo}/{branch} - {title}"). The
@@ -3862,11 +4446,10 @@ export function createNotificationsExtension(
3862
4446
  rt.preAskFlushedText = text;
3863
4447
  // Decision A: a stream-enabled turn must finalize as an in-place edit of ONE
3864
4448
  // live message, never a fresh (rich-promotable) send. If live frames were
3865
- // async-queued and none landed before this flush, allocate the per-turn ref
3866
- // now so the finalized frame always carries a messageRef → the daemon keeps it
3867
- // editable (HTML edit) and never rich-promotes a streamed final.
3868
- if (finalAnswer && rt.stream && rt.liveRef === undefined) {
3869
- rt.turnSeq = (rt.turnSeq ?? 0) + 1;
4449
+ // async-queued and none landed before this flush, reuse the per-turn ref
4450
+ // assigned at turn_start so the finalized frame remains editable (HTML edit)
4451
+ // and never rich-promotes a streamed final.
4452
+ if (finalAnswer && rt.stream && rt.liveRef === undefined && rt.turnSeq !== undefined) {
3870
4453
  rt.liveRef = String(rt.turnSeq);
3871
4454
  }
3872
4455
  try {
@@ -3890,11 +4473,99 @@ export function createNotificationsExtension(
3890
4473
  // path and ordered before it — unlike message_update, which is queued async),
3891
4474
  // then flushed here before the ask tool's execute calls registerAsk.
3892
4475
  api.on("tool_execution_start", (event, ctx) => {
3893
- if (event.toolName !== "ask") return;
4476
+ if (event.toolName === "ask") {
4477
+ const id = sessionId(ctx);
4478
+ const rt = runtimes.get(id);
4479
+ if (!rt?.notificationsActive || rt.redact) return;
4480
+ flushTurnText(rt, id, rt.currentTurnText, false);
4481
+ }
3894
4482
  const id = sessionId(ctx);
3895
4483
  const rt = runtimes.get(id);
3896
4484
  if (!rt?.notificationsActive || rt.redact) return;
3897
- flushTurnText(rt, id, rt.currentTurnText, false);
4485
+ rt.inFlightTools.set(event.toolCallId, { toolName: event.toolName, args: event.args });
4486
+ try {
4487
+ pushSessionFrame(rt, {
4488
+ type: "tool_activity",
4489
+ sessionId: id,
4490
+ toolCallId: event.toolCallId,
4491
+ toolName: event.toolName,
4492
+ phase: "started",
4493
+ });
4494
+ } catch (e) {
4495
+ logger.warn(`notifications: tool_activity start failed: ${String(e)}`);
4496
+ }
4497
+ });
4498
+
4499
+ api.on("tool_execution_end", (event, ctx) => {
4500
+ const id = sessionId(ctx);
4501
+ const rt = runtimes.get(id);
4502
+ if (!rt) return;
4503
+ const inFlight = rt.inFlightTools.get(event.toolCallId);
4504
+ if (!rt.notificationsActive || rt.redact) {
4505
+ rt.inFlightTools.delete(event.toolCallId);
4506
+ return;
4507
+ }
4508
+ try {
4509
+ const frame: {
4510
+ type: "tool_activity";
4511
+ sessionId: string;
4512
+ toolCallId: string;
4513
+ toolName: string;
4514
+ phase: "completed" | "failed";
4515
+ isError: boolean;
4516
+ argsSummary?: string;
4517
+ resultSummary?: string;
4518
+ } = {
4519
+ type: "tool_activity",
4520
+ sessionId: id,
4521
+ toolCallId: event.toolCallId,
4522
+ toolName: event.toolName,
4523
+ phase: event.isError ? "failed" : "completed",
4524
+ isError: event.isError,
4525
+ };
4526
+ if (rt.verbosity === "verbose") {
4527
+ const tool = ctx.resolveTool(event.toolName);
4528
+ const argsSummary = projectToolSummary(tool, "args", inFlight?.args);
4529
+ const resultSummary = projectToolSummary(tool, "result", event.result);
4530
+ if (argsSummary !== undefined) frame.argsSummary = argsSummary;
4531
+ if (resultSummary !== undefined) frame.resultSummary = resultSummary;
4532
+ }
4533
+ pushSessionFrame(rt, frame);
4534
+ } catch (e) {
4535
+ logger.warn(`notifications: tool_activity end failed: ${String(e)}`);
4536
+ } finally {
4537
+ rt.inFlightTools.delete(event.toolCallId);
4538
+ }
4539
+ });
4540
+
4541
+ api.on("reasoning_summary_end", (event, ctx) => {
4542
+ const id = sessionId(ctx);
4543
+ const rt = runtimes.get(id);
4544
+ if (!rt?.notificationsActive || rt.redact || rt.verbosity !== "verbose") return;
4545
+ if (!event.message || typeof event.message !== "object" || !("content" in event.message)) return;
4546
+ const content = event.message.content;
4547
+ if (!Array.isArray(content)) return;
4548
+ const block = content[event.contentIndex];
4549
+ if (block?.type !== "thinking" || (block.provenance !== "summary" && block.provenance !== "mixed")) return;
4550
+ // CoT boundary: emit ONLY the canonical provider-marked summaryText. Never
4551
+ // fall back to the event payload, which could carry inconsistent/mutated text.
4552
+ const text = block.summaryText;
4553
+ if (typeof text !== "string" || text === "") return;
4554
+ try {
4555
+ pushSessionFrame(rt, {
4556
+ type: "reasoning_summary",
4557
+ sessionId: id,
4558
+ text,
4559
+ // Coalesce on the reasoning block's stable itemId carried on the event, NOT
4560
+ // the mutable rt.turnSeq: a streamed reasoning_summary_end is queued async and
4561
+ // turn_start for the next iteration advances turnSeq synchronously first, so
4562
+ // reading turnSeq here could bind turn N's summary to turn N+1. Absent an
4563
+ // itemId, omit turnRef (threaded-render sends a fresh non-editable message).
4564
+ ...((block as { itemId?: string }).itemId ? { turnRef: (block as { itemId?: string }).itemId } : {}),
4565
+ });
4566
+ } catch (e) {
4567
+ logger.warn(`notifications: reasoning_summary failed: ${String(e)}`);
4568
+ }
3898
4569
  });
3899
4570
 
3900
4571
  api.on("turn_end", (event, ctx) => {
@@ -3924,8 +4595,7 @@ export function createNotificationsExtension(
3924
4595
  const rt = runtimes.get(id);
3925
4596
  if (!rt?.notificationsActive || !rt.stream || rt.redact || rt.turnClosed) return;
3926
4597
  if ((event.message as { role?: unknown }).role !== "assistant") return;
3927
- if (rt.liveRef === undefined) {
3928
- rt.turnSeq = (rt.turnSeq ?? 0) + 1;
4598
+ if (rt.liveRef === undefined && rt.turnSeq !== undefined) {
3929
4599
  rt.liveRef = String(rt.turnSeq);
3930
4600
  }
3931
4601
  const now = Date.now();
@@ -3971,9 +4641,24 @@ export function createNotificationsExtension(
3971
4641
  });
3972
4642
 
3973
4643
  api.on("session_shutdown", async (_event, ctx) => {
4644
+ extensionShuttingDown = true;
4645
+ identityControlInFlight = false;
4646
+ deferredIdentityRotation = undefined;
4647
+ const id = sessionId(ctx);
4648
+ const rt = runtimes.get(id);
4649
+ if (rt) terminalizeInFlightTools(rt, id, "unknown");
3974
4650
  const controllerStop =
3975
4651
  typeof ctx.sessionManager.getCwd === "function" ? controller.stopCurrentSession(ctx) : Promise.resolve(false);
3976
4652
  void controllerStop.catch(error => logger.warn(`notifications: controller shutdown failed: ${String(error)}`));
3977
- await stopSession(sessionId(ctx));
4653
+ try {
4654
+ await stopSession(id);
4655
+ } catch (error) {
4656
+ // A retained owner-release failure keeps the exact runtime in
4657
+ // cleanupRetries for an explicit later retry; log it rather than
4658
+ // surfacing a red extension error at
4659
+ // shutdown. On terminal quit there is no later retry cycle, so log at
4660
+ // error severity (matching the postmortem cleanup precedent).
4661
+ logger.error(`notifications: SDK notification runtime cleanup failed: ${String(error)}`);
4662
+ }
3978
4663
  });
3979
4664
  }