@gajae-code/coding-agent 0.17.2 → 0.17.5

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 (390) hide show
  1. package/CHANGELOG.md +267 -0
  2. package/dist/types/cli/daemon-cli.d.ts +3 -1
  3. package/dist/types/cli/github-release.d.ts +17 -0
  4. package/dist/types/cli/public-command-entry.d.ts +56 -0
  5. package/dist/types/cli/public-command-errors.d.ts +126 -0
  6. package/dist/types/cli/public-command-evidence.d.ts +99 -0
  7. package/dist/types/cli/public-command-help.d.ts +65 -0
  8. package/dist/types/cli/public-command-registry.d.ts +69 -0
  9. package/dist/types/cli/session-picker.d.ts +1 -1
  10. package/dist/types/cli/setup-cli.d.ts +1 -0
  11. package/dist/types/cli/skills-cli.d.ts +5 -0
  12. package/dist/types/cli.d.ts +3 -0
  13. package/dist/types/commands/daemon.d.ts +4 -50
  14. package/dist/types/commands/sdk.d.ts +20 -37
  15. package/dist/types/commands/setup.d.ts +3 -0
  16. package/dist/types/commands/skills.d.ts +9 -0
  17. package/dist/types/config/autorouting-tier-map.d.ts +1049 -3
  18. package/dist/types/config/file-lock.d.ts +13 -3
  19. package/dist/types/config/model-profile-activation.d.ts +45 -1
  20. package/dist/types/config/model-registry.d.ts +14 -3
  21. package/dist/types/config/model-resolver.d.ts +1 -1
  22. package/dist/types/config/settings-schema.d.ts +11 -0
  23. package/dist/types/coordinator-mcp/question-state.d.ts +6 -1
  24. package/dist/types/daemon/operator-contract.d.ts +5 -1
  25. package/dist/types/eval/js/worker-protocol.d.ts +2 -0
  26. package/dist/types/exec/bash-executor.d.ts +2 -0
  27. package/dist/types/extensibility/extensions/types.d.ts +12 -0
  28. package/dist/types/extensibility/gjc-plugins/registry.d.ts +4 -0
  29. package/dist/types/extensibility/gjc-plugins/runtime-adapters.d.ts +11 -2
  30. package/dist/types/extensibility/runtime-skill-discovery.d.ts +18 -0
  31. package/dist/types/extensibility/shared-events.d.ts +3 -1
  32. package/dist/types/gjc-runtime/session-state-lock.d.ts +61 -0
  33. package/dist/types/gjc-runtime/state-writer.d.ts +14 -1
  34. package/dist/types/harness-control-plane/control-endpoint.d.ts +4 -1
  35. package/dist/types/harness-control-plane/finalize.d.ts +14 -3
  36. package/dist/types/internal-urls/embedded-protocol.d.ts +9 -0
  37. package/dist/types/internal-urls/index.d.ts +1 -0
  38. package/dist/types/internal-urls/parse.d.ts +9 -1
  39. package/dist/types/main.d.ts +23 -1
  40. package/dist/types/modes/acp/acp-agent.d.ts +55 -1
  41. package/dist/types/modes/acp/startup-provenance.d.ts +16 -0
  42. package/dist/types/modes/components/custom-provider-wizard.d.ts +19 -1
  43. package/dist/types/modes/components/session-selector.d.ts +5 -2
  44. package/dist/types/modes/components/user-message-selector.d.ts +5 -2
  45. package/dist/types/modes/controllers/command-controller.d.ts +1 -1
  46. package/dist/types/modes/controllers/selector-controller.d.ts +1 -1
  47. package/dist/types/modes/interactive-mode.d.ts +1 -1
  48. package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +11 -1
  49. package/dist/types/modes/shared/agent-wire/workflow-gate-types.d.ts +14 -1
  50. package/dist/types/modes/types.d.ts +2 -2
  51. package/dist/types/runtime/process-lifecycle.d.ts +2 -0
  52. package/dist/types/runtime-mcp/loader.d.ts +2 -0
  53. package/dist/types/runtime-mcp/manager.d.ts +69 -6
  54. package/dist/types/runtime-mcp/pool.d.ts +17 -0
  55. package/dist/types/runtime-mcp/redaction.d.ts +20 -0
  56. package/dist/types/runtime-mcp/startup-policy.d.ts +6 -0
  57. package/dist/types/runtime-mcp/tool-bridge.d.ts +9 -0
  58. package/dist/types/runtime-mcp/transports/http.d.ts +11 -0
  59. package/dist/types/sdk/broker/broker.d.ts +17 -4
  60. package/dist/types/sdk/broker/ensure.d.ts +3 -0
  61. package/dist/types/sdk/broker/identity.d.ts +5 -1
  62. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +23 -3
  63. package/dist/types/sdk/broker/lifecycle.d.ts +15 -8
  64. package/dist/types/sdk/broker/managed-task-dag.d.ts +1335 -0
  65. package/dist/types/sdk/broker/managed-task-verification.d.ts +37 -0
  66. package/dist/types/sdk/broker/runtime.d.ts +1 -1
  67. package/dist/types/sdk/broker/session-index.d.ts +22 -3
  68. package/dist/types/sdk/broker/spawn-authority.d.ts +12 -0
  69. package/dist/types/sdk/broker/startup-failure.d.ts +4 -0
  70. package/dist/types/sdk/broker/stderr-drainer.d.ts +13 -0
  71. package/dist/types/sdk/bus/index.d.ts +19 -0
  72. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +7 -1
  73. package/dist/types/sdk/bus/telegram-daemon.d.ts +5 -0
  74. package/dist/types/sdk/cli/master-cli.d.ts +6 -10
  75. package/dist/types/sdk/cli/rows.d.ts +19 -1
  76. package/dist/types/sdk/cli/session-cli.d.ts +22 -5
  77. package/dist/types/sdk/client/client.d.ts +2 -0
  78. package/dist/types/sdk/guides/cli.d.ts +3 -4
  79. package/dist/types/sdk/guides/verify.d.ts +2 -0
  80. package/dist/types/sdk/host/host.d.ts +9 -0
  81. package/dist/types/sdk/host/session-runtime.d.ts +31 -0
  82. package/dist/types/sdk/host/types.d.ts +10 -0
  83. package/dist/types/sdk/index.d.ts +1 -0
  84. package/dist/types/sdk/prompt-deadline-flush.d.ts +85 -0
  85. package/dist/types/sdk/prompt-deadline-manager.d.ts +37 -0
  86. package/dist/types/sdk/prompt-tool-boundary.d.ts +73 -0
  87. package/dist/types/sdk/router/session-router.d.ts +2 -0
  88. package/dist/types/sdk/session.d.ts +3 -3
  89. package/dist/types/sdk/startup-capability.d.ts +2 -0
  90. package/dist/types/sdk/transport/relay.d.ts +1 -0
  91. package/dist/types/sdk/transport/serve-cli.d.ts +48 -4
  92. package/dist/types/sdk/transport/stdio.d.ts +5 -1
  93. package/dist/types/session/agent-session.d.ts +175 -23
  94. package/dist/types/session/fallback-chain-controller.d.ts +6 -6
  95. package/dist/types/session/fold-coordinator.d.ts +17 -3
  96. package/dist/types/session/internal/managed-session-storage.d.ts +7 -1
  97. package/dist/types/session/session-manager.d.ts +64 -1
  98. package/dist/types/session/session-work-lease.d.ts +14 -0
  99. package/dist/types/setup/model-onboarding-guidance.d.ts +1 -1
  100. package/dist/types/setup/provider-onboarding.d.ts +81 -1
  101. package/dist/types/task/types.d.ts +2 -0
  102. package/dist/types/tools/bash-interactive.d.ts +1 -0
  103. package/dist/types/tools/bash.d.ts +37 -11
  104. package/dist/types/tools/fetch.d.ts +10 -1
  105. package/dist/types/tools/job.d.ts +1 -1
  106. package/dist/types/tools/steer-fold.d.ts +31 -0
  107. package/dist/types/tools/subagent.d.ts +8 -1
  108. package/dist/types/utils/git.d.ts +28 -2
  109. package/dist/types/utils/mupdf-embedded.d.ts +2 -0
  110. package/dist/types/utils/mupdf.d.ts +4 -0
  111. package/dist/types/utils/session-title-generation.d.ts +6 -0
  112. package/dist/types/utils/title-generator.d.ts +11 -2
  113. package/dist/types/web/scrapers/types.d.ts +3 -0
  114. package/package.json +18 -10
  115. package/scripts/build-binary.ts +9 -3
  116. package/scripts/compile-args.ts +0 -9
  117. package/scripts/embed-mupdf.ts +52 -0
  118. package/scripts/generate-sdk-operation-inventory.ts +31 -1
  119. package/scripts/vendor-markit.ts +196 -0
  120. package/src/async/job-manager.ts +22 -1
  121. package/src/cli/daemon-cli.ts +53 -18
  122. package/src/cli/github-release.ts +87 -5
  123. package/src/cli/mcp-cli.ts +38 -4
  124. package/src/cli/public-command-entry.ts +450 -0
  125. package/src/cli/public-command-errors.ts +581 -0
  126. package/src/cli/public-command-evidence.ts +717 -0
  127. package/src/cli/public-command-help.ts +360 -0
  128. package/src/cli/public-command-registry.ts +541 -0
  129. package/src/cli/session-picker.ts +2 -1
  130. package/src/cli/setup-cli.ts +10 -1
  131. package/src/cli/skills-cli.ts +46 -2
  132. package/src/cli/stats-cli.ts +11 -1
  133. package/src/cli.ts +61 -1
  134. package/src/commands/daemon.ts +65 -69
  135. package/src/commands/sdk.ts +175 -117
  136. package/src/commands/setup.ts +2 -0
  137. package/src/commands/skills.ts +11 -0
  138. package/src/config/autorouting-tier-map.ts +543 -3
  139. package/src/config/file-lock.ts +206 -65
  140. package/src/config/model-discovery-manager.ts +4 -4
  141. package/src/config/model-profile-activation.ts +417 -66
  142. package/src/config/model-registry.ts +254 -36
  143. package/src/config/model-resolver.ts +6 -1
  144. package/src/config/settings-schema.ts +26 -2
  145. package/src/coordinator-mcp/policy.ts +10 -5
  146. package/src/coordinator-mcp/question-gate-codec.ts +17 -0
  147. package/src/coordinator-mcp/question-state.ts +179 -9
  148. package/src/coordinator-mcp/server.ts +521 -87
  149. package/src/coordinator-mcp/session-reaper.ts +6 -4
  150. package/src/crash/record-loader.ts +6 -2
  151. package/src/daemon/operator-contract.ts +26 -12
  152. package/src/defaults/gjc/skills/ultragoal/SKILL.md +1 -1
  153. package/src/eval/js/context-manager.ts +8 -8
  154. package/src/eval/js/worker-core.ts +9 -4
  155. package/src/eval/js/worker-protocol.ts +2 -0
  156. package/src/exec/bash-executor.ts +3 -0
  157. package/src/extensibility/extensions/runner.ts +52 -4
  158. package/src/extensibility/extensions/types.ts +10 -0
  159. package/src/extensibility/gjc-plugins/registry.ts +196 -37
  160. package/src/extensibility/gjc-plugins/runtime-adapters.ts +46 -8
  161. package/src/extensibility/runtime-skill-discovery.ts +61 -3
  162. package/src/extensibility/shared-events.ts +3 -1
  163. package/src/gjc-runtime/session-state-lock.ts +165 -24
  164. package/src/gjc-runtime/state-writer.ts +317 -6
  165. package/src/gjc-runtime/ultragoal-succession.ts +31 -3
  166. package/src/harness-control-plane/control-endpoint.ts +24 -6
  167. package/src/harness-control-plane/finalize.ts +312 -25
  168. package/src/harness-control-plane/owner.ts +323 -89
  169. package/src/internal-urls/docs-index.generated.ts +23 -20
  170. package/src/internal-urls/embedded-protocol.ts +83 -0
  171. package/src/internal-urls/index.ts +1 -0
  172. package/src/internal-urls/parse.ts +21 -1
  173. package/src/internal-urls/router.ts +4 -2
  174. package/src/main.ts +192 -74
  175. package/src/modes/acp/acp-agent.ts +973 -56
  176. package/src/modes/acp/startup-provenance.ts +59 -0
  177. package/src/modes/components/custom-provider-wizard.ts +308 -7
  178. package/src/modes/components/gjc-bundle-settings.ts +21 -1
  179. package/src/modes/components/oauth-selector.ts +9 -0
  180. package/src/modes/components/session-selector.ts +47 -4
  181. package/src/modes/components/tool-status-header.ts +72 -6
  182. package/src/modes/components/user-message-selector.ts +57 -15
  183. package/src/modes/controllers/command-controller.ts +58 -32
  184. package/src/modes/controllers/extension-ui-controller.ts +33 -7
  185. package/src/modes/controllers/input-controller.ts +4 -0
  186. package/src/modes/controllers/runtime-mcp-command-controller.ts +1 -1
  187. package/src/modes/controllers/selector-controller.ts +161 -15
  188. package/src/modes/interactive-mode.ts +21 -7
  189. package/src/modes/runtime-init.ts +1 -0
  190. package/src/modes/shared/agent-wire/workflow-gate-broker.ts +124 -7
  191. package/src/modes/shared/agent-wire/workflow-gate-types.ts +16 -2
  192. package/src/modes/types.ts +2 -2
  193. package/src/prompts/agent-fragments/browser-aside.md +1 -0
  194. package/src/prompts/system/title-system.md +1 -1
  195. package/src/prompts/tools/bash.md +1 -1
  196. package/src/prompts/tools/job.md +1 -1
  197. package/src/prompts/tools/read.md +2 -0
  198. package/src/runtime/network-prewarm-service.test.ts +36 -1
  199. package/src/runtime/network-prewarm-service.ts +28 -3
  200. package/src/runtime/process-lifecycle.ts +22 -11
  201. package/src/runtime-mcp/config.ts +16 -1
  202. package/src/runtime-mcp/loader.ts +6 -3
  203. package/src/runtime-mcp/manager.ts +845 -109
  204. package/src/runtime-mcp/pool.test.ts +31 -0
  205. package/src/runtime-mcp/pool.ts +304 -28
  206. package/src/runtime-mcp/redaction.ts +42 -0
  207. package/src/runtime-mcp/startup-policy.ts +7 -0
  208. package/src/runtime-mcp/tool-bridge.ts +71 -0
  209. package/src/runtime-mcp/transports/http.ts +108 -25
  210. package/src/sdk/acp/adapter.ts +27 -8
  211. package/src/sdk/broker/broker.ts +1431 -198
  212. package/src/sdk/broker/ensure.ts +21 -3
  213. package/src/sdk/broker/identity.ts +22 -1
  214. package/src/sdk/broker/lifecycle-ledger.ts +95 -31
  215. package/src/sdk/broker/lifecycle.ts +690 -141
  216. package/src/sdk/broker/managed-task-dag.ts +1927 -0
  217. package/src/sdk/broker/managed-task-verification.ts +642 -0
  218. package/src/sdk/broker/runtime.ts +1 -1
  219. package/src/sdk/broker/session-index.ts +97 -4
  220. package/src/sdk/broker/spawn-authority.ts +175 -14
  221. package/src/sdk/broker/startup-failure.ts +80 -1
  222. package/src/sdk/broker/stderr-drainer.ts +59 -0
  223. package/src/sdk/broker/transport.ts +1 -0
  224. package/src/sdk/bus/chat-daemon-control.ts +2 -2
  225. package/src/sdk/bus/chat-daemon-runtime.ts +1 -0
  226. package/src/sdk/bus/index.ts +707 -81
  227. package/src/sdk/bus/telegram-daemon-cli.ts +31 -2
  228. package/src/sdk/bus/telegram-daemon-contract.ts +7 -1
  229. package/src/sdk/bus/telegram-daemon.ts +10 -3
  230. package/src/sdk/cli/master-cli.ts +47 -36
  231. package/src/sdk/cli/rows.ts +30 -7
  232. package/src/sdk/cli/session-cli.ts +705 -184
  233. package/src/sdk/client/client.ts +18 -0
  234. package/src/sdk/guides/catalog.ts +2 -2
  235. package/src/sdk/guides/cli.ts +20 -62
  236. package/src/sdk/guides/verify.ts +14 -0
  237. package/src/sdk/host/host.ts +49 -5
  238. package/src/sdk/host/session-runtime.test.ts +184 -2
  239. package/src/sdk/host/session-runtime.ts +322 -101
  240. package/src/sdk/host/types.ts +12 -1
  241. package/src/sdk/host/websocket-transport.lifecycle.test.ts +84 -0
  242. package/src/sdk/host/websocket-transport.ts +40 -6
  243. package/src/sdk/index.ts +11 -0
  244. package/src/sdk/lifecycle/service.ts +2 -0
  245. package/src/sdk/prompt-deadline-flush.ts +356 -0
  246. package/src/sdk/prompt-deadline-manager.ts +133 -1
  247. package/src/sdk/prompt-tool-boundary.ts +121 -0
  248. package/src/sdk/protocol/operation-inventory.generated.json +106 -0
  249. package/src/sdk/router/session-router.ts +9 -1
  250. package/src/sdk/session.ts +763 -116
  251. package/src/sdk/startup-capability.ts +1 -1
  252. package/src/sdk/transport/relay.ts +71 -6
  253. package/src/sdk/transport/serve-cli.ts +397 -42
  254. package/src/sdk/transport/socket.ts +12 -11
  255. package/src/sdk/transport/stdio.ts +7 -3
  256. package/src/session/agent-session.ts +2472 -407
  257. package/src/session/fallback-chain-controller.ts +6 -7
  258. package/src/session/fold-coordinator.ts +24 -5
  259. package/src/session/internal/managed-session-scope.ts +68 -18
  260. package/src/session/internal/managed-session-storage.ts +334 -12
  261. package/src/session/session-manager.ts +382 -44
  262. package/src/session/session-work-lease.ts +34 -0
  263. package/src/setup/model-onboarding-guidance.ts +1 -1
  264. package/src/setup/provider-onboarding.ts +397 -24
  265. package/src/setup/provider-presets.json +2 -1
  266. package/src/slash-commands/builtin-registry.ts +259 -15
  267. package/src/task/executor.ts +46 -17
  268. package/src/task/index.ts +14 -2
  269. package/src/task/types.ts +2 -0
  270. package/src/tools/bash-interactive.ts +2 -0
  271. package/src/tools/bash.ts +118 -78
  272. package/src/tools/fetch.ts +140 -25
  273. package/src/tools/job.ts +151 -25
  274. package/src/tools/output-meta.ts +15 -2
  275. package/src/tools/path-utils.ts +27 -3
  276. package/src/tools/read.ts +3 -3
  277. package/src/tools/steer-fold.ts +107 -0
  278. package/src/tools/subagent-render.ts +24 -1
  279. package/src/tools/subagent.ts +27 -1
  280. package/src/tools/tool-catalog.generated.ts +29 -2
  281. package/src/tools/tool-errors.ts +4 -0
  282. package/src/utils/git.ts +53 -3
  283. package/src/utils/markit.ts +55 -41
  284. package/src/utils/mupdf-assets.d.ts +4 -0
  285. package/src/utils/mupdf-embedded.ts +4 -0
  286. package/src/utils/mupdf.ts +136 -0
  287. package/src/utils/session-title-generation.ts +29 -0
  288. package/src/utils/title-generator.ts +46 -2
  289. package/src/web/scrapers/types.ts +24 -3
  290. package/vendor/markit-ai/LICENSE +21 -0
  291. package/vendor/markit-ai/MANIFEST.json +412 -0
  292. package/vendor/markit-ai/dist/commands/config.d.ts +4 -0
  293. package/vendor/markit-ai/dist/commands/config.js +139 -0
  294. package/vendor/markit-ai/dist/commands/convert.d.ts +6 -0
  295. package/vendor/markit-ai/dist/commands/convert.js +113 -0
  296. package/vendor/markit-ai/dist/commands/formats.d.ts +2 -0
  297. package/vendor/markit-ai/dist/commands/formats.js +92 -0
  298. package/vendor/markit-ai/dist/commands/init.d.ts +2 -0
  299. package/vendor/markit-ai/dist/commands/init.js +29 -0
  300. package/vendor/markit-ai/dist/commands/onboard.d.ts +2 -0
  301. package/vendor/markit-ai/dist/commands/onboard.js +61 -0
  302. package/vendor/markit-ai/dist/commands/plugin.d.ts +4 -0
  303. package/vendor/markit-ai/dist/commands/plugin.js +58 -0
  304. package/vendor/markit-ai/dist/config.d.ts +26 -0
  305. package/vendor/markit-ai/dist/config.js +42 -0
  306. package/vendor/markit-ai/dist/converters/audio.d.ts +7 -0
  307. package/vendor/markit-ai/dist/converters/audio.js +104 -0
  308. package/vendor/markit-ai/dist/converters/csv.d.ts +7 -0
  309. package/vendor/markit-ai/dist/converters/csv.js +83 -0
  310. package/vendor/markit-ai/dist/converters/docx.d.ts +6 -0
  311. package/vendor/markit-ai/dist/converters/docx.js +57 -0
  312. package/vendor/markit-ai/dist/converters/epub.d.ts +8 -0
  313. package/vendor/markit-ai/dist/converters/epub.js +128 -0
  314. package/vendor/markit-ai/dist/converters/github.d.ts +18 -0
  315. package/vendor/markit-ai/dist/converters/github.js +148 -0
  316. package/vendor/markit-ai/dist/converters/html.d.ts +6 -0
  317. package/vendor/markit-ai/dist/converters/html.js +30 -0
  318. package/vendor/markit-ai/dist/converters/image.d.ts +6 -0
  319. package/vendor/markit-ai/dist/converters/image.js +124 -0
  320. package/vendor/markit-ai/dist/converters/ipynb.d.ts +6 -0
  321. package/vendor/markit-ai/dist/converters/ipynb.js +75 -0
  322. package/vendor/markit-ai/dist/converters/iwork.d.ts +20 -0
  323. package/vendor/markit-ai/dist/converters/iwork.js +391 -0
  324. package/vendor/markit-ai/dist/converters/json.d.ts +6 -0
  325. package/vendor/markit-ai/dist/converters/json.js +21 -0
  326. package/vendor/markit-ai/dist/converters/pdf/columns.d.ts +35 -0
  327. package/vendor/markit-ai/dist/converters/pdf/columns.js +93 -0
  328. package/vendor/markit-ai/dist/converters/pdf/extract.d.ts +19 -0
  329. package/vendor/markit-ai/dist/converters/pdf/extract.js +514 -0
  330. package/vendor/markit-ai/dist/converters/pdf/grid.d.ts +25 -0
  331. package/vendor/markit-ai/dist/converters/pdf/grid.js +795 -0
  332. package/vendor/markit-ai/dist/converters/pdf/headers.d.ts +24 -0
  333. package/vendor/markit-ai/dist/converters/pdf/headers.js +108 -0
  334. package/vendor/markit-ai/dist/converters/pdf/index.d.ts +19 -0
  335. package/vendor/markit-ai/dist/converters/pdf/index.js +136 -0
  336. package/vendor/markit-ai/dist/converters/pdf/mupdf-loader.js +9 -0
  337. package/vendor/markit-ai/dist/converters/pdf/render.d.ts +24 -0
  338. package/vendor/markit-ai/dist/converters/pdf/render.js +513 -0
  339. package/vendor/markit-ai/dist/converters/pdf/types.d.ts +75 -0
  340. package/vendor/markit-ai/dist/converters/pdf/types.js +1 -0
  341. package/vendor/markit-ai/dist/converters/plain-text.d.ts +6 -0
  342. package/vendor/markit-ai/dist/converters/plain-text.js +87 -0
  343. package/vendor/markit-ai/dist/converters/pptx.d.ts +8 -0
  344. package/vendor/markit-ai/dist/converters/pptx.js +289 -0
  345. package/vendor/markit-ai/dist/converters/rss.d.ts +11 -0
  346. package/vendor/markit-ai/dist/converters/rss.js +138 -0
  347. package/vendor/markit-ai/dist/converters/wikipedia.d.ts +6 -0
  348. package/vendor/markit-ai/dist/converters/wikipedia.js +33 -0
  349. package/vendor/markit-ai/dist/converters/xlsx.d.ts +8 -0
  350. package/vendor/markit-ai/dist/converters/xlsx.js +147 -0
  351. package/vendor/markit-ai/dist/converters/xml.d.ts +6 -0
  352. package/vendor/markit-ai/dist/converters/xml.js +18 -0
  353. package/vendor/markit-ai/dist/converters/yaml.d.ts +6 -0
  354. package/vendor/markit-ai/dist/converters/yaml.js +17 -0
  355. package/vendor/markit-ai/dist/converters/zip.d.ts +8 -0
  356. package/vendor/markit-ai/dist/converters/zip.js +57 -0
  357. package/vendor/markit-ai/dist/index.d.ts +30 -0
  358. package/vendor/markit-ai/dist/index.js +26 -0
  359. package/vendor/markit-ai/dist/main.d.ts +2 -0
  360. package/vendor/markit-ai/dist/main.js +191 -0
  361. package/vendor/markit-ai/dist/markit.d.ts +45 -0
  362. package/vendor/markit-ai/dist/markit.js +267 -0
  363. package/vendor/markit-ai/dist/plugins/api.d.ts +7 -0
  364. package/vendor/markit-ai/dist/plugins/api.js +42 -0
  365. package/vendor/markit-ai/dist/plugins/index.d.ts +4 -0
  366. package/vendor/markit-ai/dist/plugins/index.js +3 -0
  367. package/vendor/markit-ai/dist/plugins/installer.d.ts +25 -0
  368. package/vendor/markit-ai/dist/plugins/installer.js +176 -0
  369. package/vendor/markit-ai/dist/plugins/loader.d.ts +6 -0
  370. package/vendor/markit-ai/dist/plugins/loader.js +61 -0
  371. package/vendor/markit-ai/dist/plugins/types.d.ts +25 -0
  372. package/vendor/markit-ai/dist/plugins/types.js +1 -0
  373. package/vendor/markit-ai/dist/providers/anthropic.d.ts +2 -0
  374. package/vendor/markit-ai/dist/providers/anthropic.js +47 -0
  375. package/vendor/markit-ai/dist/providers/index.d.ts +21 -0
  376. package/vendor/markit-ai/dist/providers/index.js +56 -0
  377. package/vendor/markit-ai/dist/providers/openai.d.ts +2 -0
  378. package/vendor/markit-ai/dist/providers/openai.js +76 -0
  379. package/vendor/markit-ai/dist/providers/types.d.ts +26 -0
  380. package/vendor/markit-ai/dist/providers/types.js +1 -0
  381. package/vendor/markit-ai/dist/types.d.ts +36 -0
  382. package/vendor/markit-ai/dist/types.js +1 -0
  383. package/vendor/markit-ai/dist/utils/exit-codes.d.ts +4 -0
  384. package/vendor/markit-ai/dist/utils/exit-codes.js +4 -0
  385. package/vendor/markit-ai/dist/utils/output.d.ts +22 -0
  386. package/vendor/markit-ai/dist/utils/output.js +31 -0
  387. package/vendor/markit-ai/dist/utils/turndown.d.ts +8 -0
  388. package/vendor/markit-ai/dist/utils/turndown.js +70 -0
  389. package/vendor/markit-ai/package.json +74 -0
  390. package/vendor/markit-ai.patch +49 -0
@@ -50,7 +50,12 @@ type NotificationServer = NativeNotificationServer;
50
50
  import { $credentialEnv, logger, postmortem, VERSION } from "@gajae-code/utils";
51
51
 
52
52
  import { AsyncJobManager } from "../../async";
53
- import { Settings, validateSettingPatch } from "../../config/settings";
53
+ import {
54
+ resolveSdkPromptDeadlineMs,
55
+ resolveSdkPromptMaxRuntimeMs,
56
+ Settings,
57
+ validateSettingPatch,
58
+ } from "../../config/settings";
54
59
  import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "../../extensibility/extensions";
55
60
  import { INTERACTIVE_SELECTOR_RESUME_ORIGIN } from "../../extensibility/shared-events";
56
61
  import { toAgentWireEventPayload } from "../../modes/shared/agent-wire/event-envelope";
@@ -62,6 +67,11 @@ import {
62
67
  } from "../../modes/shared/agent-wire/workflow-gate-broker";
63
68
  import type { AgentSessionEvent } from "../../session/agent-session";
64
69
  import type { ClientBridge } from "../../session/client-bridge";
70
+ import {
71
+ createSessionWorkLease,
72
+ type SessionWorkLease,
73
+ type SessionWorkLeaseHandle,
74
+ } from "../../session/session-work-lease";
65
75
  import {
66
76
  boundTerminalRetentionState,
67
77
  findOwnedRegistrationsForTurn,
@@ -92,14 +102,14 @@ import { publishSessionHostRuntimeEvidence, type SessionHostRuntimePublication }
92
102
  import { processIncarnation } from "../broker/process-incarnation";
93
103
  import { resolveSessionLocator, SessionIndex, type SessionIndexEvent } from "../broker/session-index";
94
104
  import {
95
- CAP_GATED_FRAME_KINDS,
105
+ canDeliverSdkEvent,
96
106
  createSdkSurfaceFactory,
97
107
  masterAttestationForEffectiveHost,
98
108
  reattestMasterSessionIdentity,
109
+ SESSION_HOST_OBSERVER_CAPABILITY,
99
110
  type SessionSdkHost,
100
111
  SessionSdkSessionRuntime,
101
112
  shouldHostSdk,
102
- TOOL_ACTIVITY_CAPABILITY,
103
113
  verifyMasterCapabilityFrame,
104
114
  } from "../host";
105
115
  import { type AbortScope, type ControlSurface, dispatchControl, TypedControlError } from "../host/control";
@@ -114,6 +124,7 @@ import {
114
124
  syntheticModelInputError,
115
125
  syntheticNamespaceCollision,
116
126
  } from "../model-profile-model";
127
+ import { flushWorktreeOnPromptDeadline } from "../prompt-deadline-flush";
117
128
  import {
118
129
  createPromptDeadlineLease,
119
130
  isAttributableProgressEventType,
@@ -121,13 +132,21 @@ import {
121
132
  promptDeadlineAt,
122
133
  recordAttributableProgress,
123
134
  } from "../prompt-deadline-lease";
135
+ import { runBoundedDeadlineFlush } from "../prompt-deadline-manager";
124
136
  import {
125
137
  assistantFailureCode,
126
138
  failedPromptOutcome,
127
139
  formatPromptFailureForLocalLog,
140
+ PROMPT_FAILURE_MESSAGE_DEADLINE,
128
141
  sanitizePromptFailure,
129
142
  } from "../prompt-failure";
130
143
  import { PROMPT_CLIENT_REF_MAX_LENGTH, type SdkPromptTerminalOutcome } from "../prompt-status";
144
+ import {
145
+ IDLE_TOOL_BOUNDARY_RESULT,
146
+ TOOL_CALL_BOUNDARY_GRACE_MS,
147
+ type ToolBoundaryWaitResult,
148
+ waitForToolCallBoundary,
149
+ } from "../prompt-tool-boundary";
131
150
  import { OPERATIONS } from "../protocol/operation-registry";
132
151
  import {
133
152
  lifecycleStartupCapabilityForApi,
@@ -274,6 +293,20 @@ type PromptTerminalExtra = {
274
293
  diagnosticAlreadyLogged?: boolean;
275
294
  };
276
295
 
296
+ /**
297
+ * Diagnostic reason for a deadline terminal. The pre-#5637 string is preserved
298
+ * byte-identically whenever the abort landed at a tool boundary (or with no tool
299
+ * running); only a force-termination with tool calls still executing gets its
300
+ * own reason, so the "we killed a tool mid-flight" case stops being
301
+ * indistinguishable from a clean expiry. The failure code, provenance, category
302
+ * and message are untouched either way — downstream retry classification must
303
+ * not move.
304
+ */
305
+ function promptDeadlineDiagnosticReason(boundary: ToolBoundaryWaitResult): string {
306
+ if (boundary.outcome !== "forced") return PROMPT_FAILURE_MESSAGE_DEADLINE;
307
+ return `${PROMPT_FAILURE_MESSAGE_DEADLINE} Forced after ${TOOL_CALL_BOUNDARY_GRACE_MS}ms with ${boundary.pendingToolCallIds.length} tool call(s) still executing.`;
308
+ }
309
+
277
310
  function formatPromptTerminalFailureReason(reason: unknown): string {
278
311
  let rawReason: string;
279
312
  if (reason instanceof Error) rawReason = reason.message;
@@ -1266,6 +1299,17 @@ interface SessionRuntime {
1266
1299
  serverStopped: boolean;
1267
1300
  /** This runtime's own host-liveness publication; only its teardown may retract it. */
1268
1301
  evidencePublication?: SessionHostRuntimePublication;
1302
+ /** Authoritative lease held by admitted input, execution, and recovery work. */
1303
+ workLease: SessionWorkLease;
1304
+ /** Lease retained while an uncorrelated agent loop is active. */
1305
+ activeWorkLease?: SessionWorkLeaseHandle;
1306
+ /** Lease retained while an agent loop is paused awaiting resumption. */
1307
+ pausedWorkLease?: SessionWorkLeaseHandle;
1308
+ /**
1309
+ * Handles of queued submissions promoted to their own run, handed over at
1310
+ * the promotion boundary and retired by that run's agent_start.
1311
+ */
1312
+ promotedWorkLeases: SessionWorkLeaseHandle[];
1269
1313
  brokerRegistrationReleased: boolean;
1270
1314
  verbosity: "lean" | "verbose";
1271
1315
  /** Whether the agent loop is currently running (drives the typing indicator). */
@@ -2474,6 +2518,82 @@ function deepStructuralEqual(left: unknown, right: unknown): boolean {
2474
2518
  );
2475
2519
  }
2476
2520
 
2521
+ /**
2522
+ * Holds one session work lease handle for a dispatched submission from dispatch
2523
+ * until the submission's own settlement boundary:
2524
+ *
2525
+ * - a queued message (steer, follow-up, or a prompt diverted into steering)
2526
+ * releases when it leaves the session queue: consumed inside the running turn
2527
+ * (`startsOwnRun: false`), removed (`removed: true`), or promoted to its own
2528
+ * run (`startsOwnRun: true`), where the handle is handed to the runtime and
2529
+ * released by that run's `agent_start`;
2530
+ * - an inline prompt releases when its dispatch resolves, which is the end of
2531
+ * its run; the runtime's active-run handle and the accepted submission's
2532
+ * own handle cover the execution in between;
2533
+ * - a failed or never-admitted dispatch releases immediately.
2534
+ *
2535
+ * Every transfer is bound to THIS message's hooks, never to queue position, so
2536
+ * batched or reordered promotions cannot release a different submission's
2537
+ * handle. A message that stays queued keeps its handle: it is admitted work
2538
+ * the host must outlive, and only the session's own removal hook ends it.
2539
+ */
2540
+ interface AdmissionWorkLease {
2541
+ hooks: {
2542
+ onQueuedPromoted: (promotion: { startsOwnRun?: boolean; removed?: boolean }) => void;
2543
+ onDispatchDisposition: (promotion: { startsOwnRun: boolean }) => void;
2544
+ };
2545
+ /** The session admitted the message (a preflight hook fired). */
2546
+ accepted(): void;
2547
+ /** The dispatch resolves at queue time; the message waits for its promotion hook. */
2548
+ queued(): void;
2549
+ /** The dispatch resolved; releases unless the message is still queued. */
2550
+ dispatched(): void;
2551
+ /** The dispatch failed or was never admitted; nothing is queued. */
2552
+ abandon(): void;
2553
+ }
2554
+
2555
+ function createAdmissionWorkLease(
2556
+ lease: SessionWorkLease | undefined,
2557
+ queuedAtDispatch: boolean,
2558
+ promote: ((handle: SessionWorkLeaseHandle) => void) | undefined,
2559
+ ): AdmissionWorkLease {
2560
+ let handle = lease?.acquire();
2561
+ let accepted = false;
2562
+ let queued = queuedAtDispatch;
2563
+ const release = (): void => {
2564
+ handle?.release();
2565
+ handle = undefined;
2566
+ };
2567
+ return {
2568
+ hooks: {
2569
+ onQueuedPromoted: promotion => {
2570
+ const current = handle;
2571
+ if (!current) return;
2572
+ handle = undefined;
2573
+ if (promotion.removed || promotion.startsOwnRun !== true || !promote) current.release();
2574
+ else promote(current);
2575
+ },
2576
+ onDispatchDisposition: promotion => {
2577
+ // The dispatch resolved at queue time, not at run end: the message sits
2578
+ // in the steering queue until its promotion hook fires.
2579
+ if (promotion.startsOwnRun === false) queued = true;
2580
+ },
2581
+ },
2582
+ accepted: () => {
2583
+ accepted = true;
2584
+ },
2585
+ queued: () => {
2586
+ queued = true;
2587
+ },
2588
+ dispatched: () => {
2589
+ // The session send contract invokes a preflight hook before resolving;
2590
+ // an adapter that resolves without admitting anything queued no work.
2591
+ if (!accepted || !queued) release();
2592
+ },
2593
+ abandon: release,
2594
+ };
2595
+ }
2596
+
2477
2597
  function sdkControlSurface(
2478
2598
  ctx: ExtensionContext,
2479
2599
  pendingInteractive: Map<string, PendingInteractiveAsk>,
@@ -2567,6 +2687,10 @@ function sdkControlSurface(
2567
2687
  // executions and post-response publications) so session teardown can join
2568
2688
  // them before reporting durable quiescence.
2569
2689
  trackReconciliationProducer?: (producer: Promise<unknown>) => void,
2690
+ sessionWorkLease?: SessionWorkLease,
2691
+ // Hands a queued submission's lease handle to the runtime when that
2692
+ // submission is promoted to its own run; the run's agent_start adopts it.
2693
+ promoteWorkLease?: (handle: SessionWorkLeaseHandle) => void,
2570
2694
  ): ControlSurface & {
2571
2695
  cancelPendingPreflights(): Promise<void>;
2572
2696
  cancelPendingPreflightsForConnection(connectionId: string): Promise<void>;
@@ -2574,6 +2698,7 @@ function sdkControlSurface(
2574
2698
  const unavailable = (operation: string, reason: string) => () => {
2575
2699
  throw Object.assign(new Error(`${operation} is unavailable: ${reason}`), { code: "unavailable" });
2576
2700
  };
2701
+ const hostWorkLease = sessionWorkLease ?? ctx.getSessionWorkLease?.();
2577
2702
  const bindings = new Set(ctx.sdkBindings?.() ?? []);
2578
2703
  const surfacePolicy = createSdkSurfaceFactory({
2579
2704
  ctx,
@@ -2611,23 +2736,58 @@ function sdkControlSurface(
2611
2736
  return "unknown";
2612
2737
  };
2613
2738
  const sendSteer = async (text: string, clientRef?: string) => {
2739
+ // A steer is always queued: its handle is held from dispatch until the
2740
+ // message leaves the session queue (consumed in-run, promoted to its own
2741
+ // run, or removed), which is the only boundary bound to THIS message.
2742
+ const admission = createAdmissionWorkLease(hostWorkLease, true, promoteWorkLease);
2743
+ const sendOptions = {
2744
+ deliverAs: "steer" as const,
2745
+ onPreflightAcceptCommit: admission.accepted,
2746
+ onPreflightAccepted: admission.accepted,
2747
+ ...admission.hooks,
2748
+ };
2614
2749
  if (clientRef === undefined) {
2615
2750
  const correlation = { commandId: crypto.randomUUID(), turnId: crypto.randomUUID() };
2616
- await api.sendUserMessage(text, { deliverAs: "steer" });
2751
+ try {
2752
+ await api.sendUserMessage(text, sendOptions);
2753
+ admission.dispatched();
2754
+ } catch (error) {
2755
+ admission.abandon();
2756
+ throw error;
2757
+ }
2617
2758
  return { ...correlation, accepted: true };
2618
2759
  }
2619
2760
  const normalizedClientRef = clientRef.trim();
2620
- if (!skillRecon?.reserveSteer || !skillRecon.settleSteer)
2761
+ if (!skillRecon?.reserveSteer || !skillRecon.settleSteer) {
2762
+ admission.abandon();
2621
2763
  throw Object.assign(new Error("Steer reconciliation is unavailable."), { code: "unavailable" });
2622
- const reservation = await skillRecon.reserveSteer(normalizedClientRef, text);
2623
- if (reservation.replay) return { sessionId: ctx.sessionManager.getSessionId(), ...reservation.result };
2764
+ }
2765
+ let reservation: Awaited<ReturnType<NonNullable<typeof skillRecon.reserveSteer>>>;
2624
2766
  try {
2625
- await api.sendUserMessage(text, { deliverAs: "steer" });
2767
+ reservation = await skillRecon.reserveSteer(normalizedClientRef, text);
2768
+ } catch (error) {
2769
+ admission.abandon();
2770
+ throw error;
2771
+ }
2772
+ if (reservation.replay) {
2773
+ admission.abandon();
2774
+ return { sessionId: ctx.sessionManager.getSessionId(), ...reservation.result };
2775
+ }
2776
+ let dispatched = false;
2777
+ try {
2778
+ await api.sendUserMessage(text, sendOptions);
2779
+ dispatched = true;
2780
+ admission.dispatched();
2626
2781
  return {
2627
2782
  sessionId: ctx.sessionManager.getSessionId(),
2628
2783
  ...(await skillRecon.settleSteer(normalizedClientRef, "accepted")),
2629
2784
  };
2630
2785
  } catch (error) {
2786
+ // Only a failed dispatch leaves no queued message behind. When the
2787
+ // dispatch resolved and the durable settlement failed afterwards, the
2788
+ // steer is still queued in the session, so its handle stays held until
2789
+ // the promotion or removal hook fires.
2790
+ if (!dispatched) admission.abandon();
2631
2791
  return {
2632
2792
  sessionId: ctx.sessionManager.getSessionId(),
2633
2793
  ...(await skillRecon.settleSteer(normalizedClientRef, "rejected", error)),
@@ -2735,17 +2895,28 @@ function sdkControlSurface(
2735
2895
  throw Object.assign(new Error("clientRef must be a non-empty string of at most 128 characters."), {
2736
2896
  code: "invalid_input",
2737
2897
  });
2898
+ // An explicit steer/follow-up is queued and settles at its promotion
2899
+ // boundary; a fresh prompt settles when its run resolves. A plain prompt
2900
+ // that finds the loop busy at dispatch is queued as steering (below), and
2901
+ // one the session itself diverts reports that through onDispatchDisposition.
2902
+ const admission = createAdmissionWorkLease(hostWorkLease, deliverAs !== undefined, promoteWorkLease);
2738
2903
  if (trackReconciliation) {
2739
2904
  // Restart recovery must be committed before a tracked prompt reserves capacity,
2740
2905
  // otherwise it can admit against pre-hydration state or a lost clientRef.
2741
2906
  try {
2742
2907
  await awaitReconciliationReady();
2743
2908
  } catch {
2909
+ admission.abandon();
2744
2910
  throw Object.assign(new Error("Prompt reconciliation state is unavailable; retry after restart."), {
2745
2911
  code: "unavailable",
2746
2912
  });
2747
2913
  }
2748
- admitPrompt(trimmedClientRef);
2914
+ try {
2915
+ admitPrompt(trimmedClientRef);
2916
+ } catch (error) {
2917
+ admission.abandon();
2918
+ throw error;
2919
+ }
2749
2920
  }
2750
2921
  try {
2751
2922
  if (forceFresh && isSessionBusy()) {
@@ -2764,6 +2935,7 @@ function sdkControlSurface(
2764
2935
  },
2765
2936
  );
2766
2937
  } catch (error) {
2938
+ admission.abandon();
2767
2939
  if (trackReconciliation) releasePromptAdmission(trimmedClientRef);
2768
2940
  throw error;
2769
2941
  }
@@ -2833,12 +3005,14 @@ function sdkControlSurface(
2833
3005
  );
2834
3006
  } catch (error) {
2835
3007
  accepting = false;
3008
+ admission.abandon();
2836
3009
  // Durable acceptance failed, so the prompt was never accepted: reject the
2837
3010
  // control preflight and rethrow so the awaiting session does not execute it.
2838
3011
  onPromptAcceptFailed(correlation);
2839
3012
  settlePreflight({ status: "rejected", error });
2840
3013
  throw error;
2841
3014
  }
3015
+ admission.accepted();
2842
3016
  accepting = false;
2843
3017
  accepted = true;
2844
3018
  // #4743: an accepted run owns a durable terminal publication when it
@@ -2858,18 +3032,22 @@ function sdkControlSurface(
2858
3032
  };
2859
3033
  // Do not acknowledge the prompt until AgentSession's async preflight
2860
3034
  // succeeds. The terminal result records correlation before agent_start can fire.
3035
+ const effectiveDeliverAs = deliverAs ?? (!forceFresh && isBusy() ? ("steer" as const) : undefined);
3036
+ if (effectiveDeliverAs !== undefined) admission.queued();
2861
3037
  try {
2862
3038
  submission = Promise.resolve(
2863
3039
  api.sendUserMessage(content, {
2864
- ...(deliverAs ? { deliverAs } : !forceFresh && isBusy() ? { deliverAs: "steer" as const } : {}),
3040
+ ...(effectiveDeliverAs ? { deliverAs: effectiveDeliverAs } : {}),
2865
3041
  onPreflightAcceptCommit,
2866
3042
  onPreflightAccepted,
3043
+ ...admission.hooks,
2867
3044
  preflightSignal: preflightController.signal,
2868
3045
  ...(sdkRunToken ? { sdkRunToken } : {}),
2869
3046
  }),
2870
3047
  );
2871
3048
  } catch (error) {
2872
3049
  submissionSettled.resolve();
3050
+ admission.abandon();
2873
3051
  if (accepted && !preflightController.signal.aborted)
2874
3052
  trackReconciliationProducer?.(Promise.resolve(onPromptFailed(correlation, error)));
2875
3053
  else settlePreflight({ status: "rejected", error });
@@ -2880,6 +3058,7 @@ function sdkControlSurface(
2880
3058
  // joined — only its reconciliation publications are tracked below.
2881
3059
  void submission.then(
2882
3060
  () => {
3061
+ admission.dispatched();
2883
3062
  if (!accepted)
2884
3063
  settlePreflight({
2885
3064
  status: "rejected",
@@ -2890,6 +3069,7 @@ function sdkControlSurface(
2890
3069
  submissionSettled.resolve();
2891
3070
  },
2892
3071
  error => {
3072
+ admission.abandon();
2893
3073
  if (accepted && !preflightController.signal.aborted)
2894
3074
  trackReconciliationProducer?.(Promise.resolve(onPromptFailed(correlation, error)));
2895
3075
  else settlePreflight({ status: "rejected", error });
@@ -2902,6 +3082,7 @@ function sdkControlSurface(
2902
3082
  if (result.status === "rejected") throw result.error;
2903
3083
  return { commandId, turnId, accepted: true, ...(trimmedClientRef ? { clientRef: trimmedClientRef } : {}) };
2904
3084
  } catch (error) {
3085
+ admission.abandon();
2905
3086
  if (trackReconciliation) releasePromptAdmission(trimmedClientRef);
2906
3087
  throw error;
2907
3088
  } finally {
@@ -3425,7 +3606,15 @@ function sdkControlSurface(
3425
3606
  ...(typeof result === "object" && result ? (result as object) : {}),
3426
3607
  ...(trimmedClientRef ? { clientRef: trimmedClientRef } : {}),
3427
3608
  });
3428
- } else if (durableSkillAccepted && skillRecon) {
3609
+ } else if (durableSkillAccepted && skillRecon && !promptOwned) {
3610
+ // A lifecycle-owned skill (requester connection present, submission
3611
+ // registered by onPromptAccepted) is terminalized solely by the
3612
+ // correlated agent_end handler (terminalizePrompt -> claim ->
3613
+ // finalize with the final text). This promise-settlement transition
3614
+ // carries no text and would pre-empt that finalization, dropping the
3615
+ // durable content; it remains only for ownerless acceptance, where
3616
+ // no lifecycle handler will ever fire — and there it is the sole
3617
+ // terminal publication, so it must carry the returned text itself.
3429
3618
  trackReconciliationProducer?.(
3430
3619
  skillRecon.noteTransition(correlation, {
3431
3620
  type: "agent_end",
@@ -4096,6 +4285,13 @@ export function createNotificationsExtension(
4096
4285
  handle: string,
4097
4286
  options: { graceMs: number; terminal?: { scope: "turn" | "owned"; expectedEpoch?: number } },
4098
4287
  ) => Promise<RunSettlementProof>;
4288
+ /**
4289
+ * Live, SYNCHRONOUS view of the dispatched tool calls the run resource
4290
+ * ledger holds for an execution handle (#5637). Strictly read-only: it
4291
+ * never claims, seals or otherwise mutates ledger state. Optional so an
4292
+ * older host that does not thread it degrades to the event-derived set.
4293
+ */
4294
+ pendingToolExecutions?: (handle: string) => readonly string[];
4099
4295
  };
4100
4296
  } = {},
4101
4297
  ): void {
@@ -4323,6 +4519,37 @@ export function createNotificationsExtension(
4323
4519
  "session.branch",
4324
4520
  ]);
4325
4521
  const sessionId = (ctx: ExtensionContext): string => ctx.sessionManager.getSessionId();
4522
+ const releaseActiveWorkLease = (rt: SessionRuntime): void => {
4523
+ rt.activeWorkLease?.release();
4524
+ rt.activeWorkLease = undefined;
4525
+ };
4526
+ const releasePausedWorkLease = (rt: SessionRuntime): void => {
4527
+ rt.pausedWorkLease?.release();
4528
+ rt.pausedWorkLease = undefined;
4529
+ };
4530
+ const releasePromotedWorkLeases = (rt: SessionRuntime): void => {
4531
+ for (const handle of rt.promotedWorkLeases.splice(0)) handle.release();
4532
+ };
4533
+ const beginAgentWork = (rt: SessionRuntime): void => {
4534
+ releasePausedWorkLease(rt);
4535
+ // Acquire the run's handle before retiring the promoted ones so the lease
4536
+ // never reads as released between a queued submission and its run.
4537
+ if (!rt.activeWorkLease) rt.activeWorkLease = rt.workLease.acquire();
4538
+ // Every submission promoted into this run handed its handle over at the
4539
+ // promotion boundary; the run's own handle covers them from here.
4540
+ releasePromotedWorkLeases(rt);
4541
+ };
4542
+ const finishAgentWork = (rt: SessionRuntime, paused: boolean): void => {
4543
+ if (paused) {
4544
+ if (!rt.pausedWorkLease) rt.pausedWorkLease = rt.activeWorkLease ?? rt.workLease.acquire();
4545
+ rt.activeWorkLease = undefined;
4546
+ return;
4547
+ }
4548
+ // A run that failed before its agent_start still reaches agent_end; the
4549
+ // handles promoted into it have no later agent_start to retire them.
4550
+ if (!rt.activeWorkLease) releasePromotedWorkLeases(rt);
4551
+ releaseActiveWorkLease(rt);
4552
+ };
4326
4553
 
4327
4554
  async function stopSession(
4328
4555
  id: string,
@@ -4466,6 +4693,9 @@ export function createNotificationsExtension(
4466
4693
  // runs while an identity successor is already serving, and clearing
4467
4694
  // that live runtime's reader would manufacture "no evidence" for a
4468
4695
  // host whose clients are still attached.
4696
+ releaseActiveWorkLease(rt);
4697
+ releasePausedWorkLease(rt);
4698
+ releasePromotedWorkLeases(rt);
4469
4699
  rt.evidencePublication?.retract();
4470
4700
  rt.evidencePublication = undefined;
4471
4701
  } catch (e) {
@@ -4603,7 +4833,25 @@ export function createNotificationsExtension(
4603
4833
  const pendingInteractive = new Map<string, PendingInteractiveAsk>();
4604
4834
  const pendingPromptCorrelations: Array<{ commandId: string; turnId: string }> = [];
4605
4835
  const pendingPromptCorrelationsBySdkRunToken = new Map<string, { commandId: string; turnId: string }>();
4836
+ const workLease = ctx.getSessionWorkLease?.() ?? createSessionWorkLease();
4606
4837
  let runtime: SessionRuntime | undefined;
4838
+ // A promoted submission's handle must either enter the runtime-owned list
4839
+ // before teardown fences it, or be released immediately: stopSession drains
4840
+ // the list after fencing, and a predecessor runtime that already stopped
4841
+ // (mid session_switch) would otherwise hold the handle forever.
4842
+ const promoteWorkLease = (handle: SessionWorkLeaseHandle): void => {
4843
+ const currentRuntime = runtime;
4844
+ if (
4845
+ !currentRuntime ||
4846
+ currentRuntime.stopping ||
4847
+ currentRuntime.serverStopped ||
4848
+ runtimes.get(id) !== currentRuntime
4849
+ ) {
4850
+ handle.release();
4851
+ return;
4852
+ }
4853
+ currentRuntime.promotedWorkLeases.push(handle);
4854
+ };
4607
4855
 
4608
4856
  // The SDK can always answer now (interactive via the answer source, or the
4609
4857
  // workflow gate), so the endpoint advertises a resolver. Validate the native
@@ -4626,6 +4874,8 @@ export function createNotificationsExtension(
4626
4874
  const gatePresentations = new PresentationArbiter(server, () => runtime?.redact ?? true);
4627
4875
  gatePresentations.setPublicationSuspended(true);
4628
4876
  let inboundSdkFrame: ((connectionId: string, frame: Record<string, unknown>) => void) | undefined;
4877
+ let negotiatedCapabilitiesHandler: ((connectionId: string, capabilities: readonly string[]) => void) | undefined;
4878
+ let connectionCloseHandler: ((connectionId: string) => void) | undefined;
4629
4879
  const inFlightGateResolutions = new Set<Promise<void>>();
4630
4880
  const trackGateResolution = <T>(resolution: Promise<T>): Promise<T> => {
4631
4881
  const quiesced = resolution.then(
@@ -4750,12 +5000,55 @@ export function createNotificationsExtension(
4750
5000
  }
4751
5001
  };
4752
5002
 
4753
- const hostCapCache = new Map<string, ReadonlySet<string>>();
5003
+ type HostConnectionIncarnation = { generation: number; closed: boolean };
5004
+ type HostCapabilityCacheEntry = { generation: number; capabilities: ReadonlySet<string> };
5005
+ const hostCapCache = new Map<string, HostCapabilityCacheEntry>();
5006
+ const hostConnectionIncarnations = new Map<string, HostConnectionIncarnation>();
5007
+ const hostAttachedConnections = new Set<string>();
5008
+ let nextHostConnectionGeneration = 0;
5009
+ const liveHostConnection = (connectionId: string): HostConnectionIncarnation | undefined => {
5010
+ if (!connectionId) return undefined;
5011
+ const current = hostConnectionIncarnations.get(connectionId);
5012
+ if (current?.closed) return undefined;
5013
+ if (current) return current;
5014
+ const incarnation = { generation: ++nextHostConnectionGeneration, closed: false };
5015
+ hostConnectionIncarnations.set(connectionId, incarnation);
5016
+ return incarnation;
5017
+ };
5018
+ const rememberHostCapabilities = (connectionId: string, capabilities: readonly string[]): void => {
5019
+ const incarnation = liveHostConnection(connectionId);
5020
+ if (!incarnation) return;
5021
+ hostCapCache.set(connectionId, {
5022
+ generation: incarnation.generation,
5023
+ capabilities: new Set(capabilities),
5024
+ });
5025
+ };
5026
+ const liveHostCapabilities = (connectionId: string): ReadonlySet<string> | undefined => {
5027
+ const incarnation = hostConnectionIncarnations.get(connectionId);
5028
+ const entry = hostCapCache.get(connectionId);
5029
+ return incarnation && !incarnation.closed && entry?.generation === incarnation.generation
5030
+ ? entry.capabilities
5031
+ : undefined;
5032
+ };
5033
+ const closeHostConnection = (connectionId: string): void => {
5034
+ const current = hostConnectionIncarnations.get(connectionId);
5035
+ if (current) current.closed = true;
5036
+ else {
5037
+ hostConnectionIncarnations.set(connectionId, {
5038
+ generation: ++nextHostConnectionGeneration,
5039
+ closed: true,
5040
+ });
5041
+ }
5042
+ hostCapCache.delete(connectionId);
5043
+ hostAttachedConnections.delete(connectionId);
5044
+ };
4754
5045
 
4755
5046
  const configOverrides = new Map<string, unknown>();
4756
5047
  const configRevision = { current: 0 };
4757
5048
  const PROMPT_SUBMISSION_CAPACITY = 128;
4758
5049
  const PROMPT_SUBMISSION_TTL_MS = 5 * 60_000;
5050
+ /** Bound on remembered `tool_execution_end`s that outran their own start. */
5051
+ const UNMATCHED_TOOL_END_CAPACITY = 64;
4759
5052
  const PROMPT_TERMINAL_TOMBSTONE_CAPACITY = 256;
4760
5053
  const PROMPT_TERMINAL_TOMBSTONE_TTL_MS = 15 * 60_000;
4761
5054
  // SDK-owned terminalization grace; injectable in tests, never a user setting.
@@ -4816,14 +5109,40 @@ export function createNotificationsExtension(
4816
5109
  */
4817
5110
  selfAbortStarted?: true;
4818
5111
  };
5112
+ /**
5113
+ * A due deadline expiry is already awaiting the tool-call boundary for
5114
+ * this submission. Progress that cannot renew past an already-due hard
5115
+ * cap re-arms a zero-delay timer on every delivery; without this fence
5116
+ * each one would open ANOTHER grace wait and they would pile up through
5117
+ * the whole grace (review thread P2).
5118
+ */
5119
+ deadlineExpiryInFlight?: boolean;
4819
5120
  phase: "active" | "outcome_claimed" | "terminalizing" | "publication_closed" | "delivered";
4820
5121
  outcome?: SdkPromptTerminalOutcome;
4821
5122
  /** Agent-owned resource run captured at acceptance; cleanup targets only this handle. */
4822
5123
  executionHandle?: string;
4823
5124
  /** Cancels accepted skill preparation before an execution handle exists. */
4824
5125
  preflightAbort?: () => void | Promise<void>;
5126
+ /** Keeps the host alive from durable admission through terminal publication. */
5127
+ workLease?: SessionWorkLeaseHandle;
4825
5128
  reconciliationKind: ReconciliationKind;
4826
5129
  bufferedFrames: Array<PromptLifecycleFrame | Record<string, unknown>>;
5130
+ /**
5131
+ * Dispatched tool calls currently executing for this correlation. A SET of
5132
+ * ids, not a counter: a duplicate or unmatched `tool_execution_end` must
5133
+ * not drive it negative and falsely report the prompt idle (#5637).
5134
+ */
5135
+ runningToolCallIds: Set<string>;
5136
+ /**
5137
+ * Ids whose `tool_execution_end` was delivered with no matching start yet.
5138
+ * The fanout is asynchronous and unordered across listeners, so the end of
5139
+ * a finished call can land first; without this the late start would add a
5140
+ * call that is already over and leave it running forever. Bounded, and
5141
+ * only ever written on that out-of-order path.
5142
+ */
5143
+ endedToolCallIds: Set<string>;
5144
+ /** Drained when `runningToolCallIds` becomes empty; one entry per waiting expiry attempt. */
5145
+ toolIdleWaiters: Array<() => void>;
4827
5146
  };
4828
5147
  const promptSubmissions = new Map<string, PromptSubmission>();
4829
5148
  /** Connections fenced by a fatal prompt closure; their later frames are refused. */
@@ -4841,15 +5160,17 @@ export function createNotificationsExtension(
4841
5160
  * ordinary direct SDK subscribers retain both public surfaces from #4570.
4842
5161
  */
4843
5162
  const broadcastEventFrame = (event: SdkFrame): string[] => {
4844
- const gated = CAP_GATED_FRAME_KINDS.has(String(event.kind));
4845
5163
  const json = JSON.stringify(event);
4846
5164
  const recipients: string[] = [];
4847
- for (const [connectionId, capabilities] of hostCapCache) {
5165
+ for (const connectionId of hostAttachedConnections) {
5166
+ const incarnation = hostConnectionIncarnations.get(connectionId);
5167
+ if (!incarnation || incarnation.closed) continue;
5168
+ const capabilities = liveHostCapabilities(connectionId);
4848
5169
  if (fencedConnections.has(connectionId)) continue;
4849
- if (gated && !capabilities.has(TOOL_ACTIVITY_CAPABILITY)) continue;
5170
+ if (!canDeliverSdkEvent(String(event.kind), capabilities)) continue;
4850
5171
  try {
4851
5172
  server.sendTo(connectionId, json);
4852
- if (capabilities.has(POSITIONED_NOTIFICATION_EFFECTS_CAPABILITY)) recipients.push(connectionId);
5173
+ if (capabilities?.has(POSITIONED_NOTIFICATION_EFFECTS_CAPABILITY)) recipients.push(connectionId);
4853
5174
  } catch {
4854
5175
  // Broadcasts are best effort; directed responses surface send failures.
4855
5176
  }
@@ -4857,15 +5178,17 @@ export function createNotificationsExtension(
4857
5178
  return recipients;
4858
5179
  };
4859
5180
  const broadcastEventFrameWithReceipts = (event: SdkFrame): string[] => {
4860
- const gated = CAP_GATED_FRAME_KINDS.has(String(event.kind));
4861
5181
  const json = JSON.stringify(event);
4862
5182
  const receipts: string[] = [];
4863
- for (const [connectionId, capabilities] of hostCapCache) {
5183
+ for (const connectionId of hostAttachedConnections) {
5184
+ const incarnation = hostConnectionIncarnations.get(connectionId);
5185
+ if (!incarnation || incarnation.closed) continue;
5186
+ const capabilities = liveHostCapabilities(connectionId);
4864
5187
  if (fencedConnections.has(connectionId)) continue;
4865
- if (gated && !capabilities.has(TOOL_ACTIVITY_CAPABILITY)) continue;
5188
+ if (!canDeliverSdkEvent(String(event.kind), capabilities)) continue;
4866
5189
  try {
4867
5190
  const receipt = server.sendToWithReceipt(connectionId, json);
4868
- if (capabilities.has(POSITIONED_NOTIFICATION_EFFECTS_CAPABILITY)) receipts.push(receipt);
5191
+ if (capabilities?.has(POSITIONED_NOTIFICATION_EFFECTS_CAPABILITY)) receipts.push(receipt);
4869
5192
  } catch {
4870
5193
  // Rejected positioned sends remain eligible for the atomic raw fallback.
4871
5194
  }
@@ -4949,6 +5272,7 @@ export function createNotificationsExtension(
4949
5272
  pendingPromptCorrelationsBySdkRunToken.delete(sdkRunToken);
4950
5273
  }
4951
5274
  };
5275
+
4952
5276
  const addTerminalTombstone = (key: string, connectionId: string, now = Date.now()) => {
4953
5277
  promptTerminalTombstones.delete(key);
4954
5278
  promptTerminalTombstones.set(key, { connectionId, expiresAt: now + PROMPT_TERMINAL_TOMBSTONE_TTL_MS });
@@ -4958,6 +5282,10 @@ export function createNotificationsExtension(
4958
5282
  const finalizePrompt = (key: string, correlation: { commandId: string; turnId: string }) => {
4959
5283
  const submission = promptSubmissions.get(key);
4960
5284
  if (submission?.deadlineTimer) clearTimeout(submission.deadlineTimer);
5285
+ if (submission?.workLease) {
5286
+ submission.workLease.release();
5287
+ submission.workLease = undefined;
5288
+ }
4961
5289
  promptSubmissions.delete(key);
4962
5290
  removePendingPromptCorrelation(correlation);
4963
5291
  if (submission) addTerminalTombstone(key, submission.connectionId);
@@ -4965,6 +5293,10 @@ export function createNotificationsExtension(
4965
5293
  const expirePromptDelivery = (key: string, submission: PromptSubmission) => {
4966
5294
  if (!submission.terminal) return;
4967
5295
  if (submission.deadlineTimer) clearTimeout(submission.deadlineTimer);
5296
+ if (submission.workLease) {
5297
+ submission.workLease.release();
5298
+ submission.workLease = undefined;
5299
+ }
4968
5300
  promptSubmissions.delete(key);
4969
5301
  // A fatal closure is transport-level, not a committed semantic terminal: the
4970
5302
  // durable record stays authoritative, so it must never leave a tombstone.
@@ -5030,10 +5362,6 @@ export function createNotificationsExtension(
5030
5362
  finalizePrompt(key, correlation);
5031
5363
  }
5032
5364
  };
5033
- const readFiniteSetting = (key: "sdk.promptDeadlineMs" | "sdk.promptMaxRuntimeMs", fallback: number): number => {
5034
- const value = settings?.get(key);
5035
- return typeof value === "number" && Number.isFinite(value) ? value : fallback;
5036
- };
5037
5365
  /**
5038
5366
  * (Re)arm the accepted prompt's terminal deadline from its current lease.
5039
5367
  * `terminalizePrompt` stays the single authoritative terminal owner; this
@@ -5046,33 +5374,104 @@ export function createNotificationsExtension(
5046
5374
  if (submission.deadlineTimer) clearTimeout(submission.deadlineTimer);
5047
5375
  submission.deadlineTimer = setTimeout(
5048
5376
  () => {
5049
- const current = promptSubmissions.get(key);
5050
- // Identity fencing: a discarded, evicted or re-accepted submission is
5051
- // never terminalized by a predecessor's timer.
5052
- if (!current || current.deadlineLease !== lease) return;
5053
- // Progress delivered while this timer was pending renews the lease, so
5054
- // re-arm instead of terminalizing a demonstrably live prompt.
5055
- if (Date.now() < promptDeadlineAt(lease)) {
5056
- armPromptDeadline(key, correlation);
5057
- return;
5058
- }
5059
- // Register the expiry attempt BEFORE the awaited durable claim:
5060
- // progress arriving while the claim is in flight must supersede
5061
- // this attempt (see the post-claim fence in terminalizePrompt).
5062
- current.deadlineAttempt = { lease, generation: lease.generation };
5063
- void terminalizePrompt(
5064
- correlation,
5065
- {
5066
- kind: "failed",
5067
- code: "prompt_deadline_exceeded",
5068
- message: "Prompt deadline exceeded.",
5069
- provenance: "deadline",
5070
- phase: "submission",
5071
- category: "deadline",
5072
- },
5073
- { fence: true },
5074
- { diagnostic: { reason: "Prompt deadline exceeded." } },
5075
- );
5377
+ void (async () => {
5378
+ const current = promptSubmissions.get(key);
5379
+ // Identity fencing: a discarded, evicted or re-accepted submission is
5380
+ // never terminalized by a predecessor's timer.
5381
+ if (!current || current.deadlineLease !== lease) return;
5382
+ // Progress delivered while this timer was pending renews the lease, so
5383
+ // re-arm instead of terminalizing a demonstrably live prompt.
5384
+ if (Date.now() < promptDeadlineAt(lease)) {
5385
+ armPromptDeadline(key, correlation);
5386
+ return;
5387
+ }
5388
+ // Nothing to terminalize behind the terminal owner — and nothing to
5389
+ // wait for on its behalf either. `terminalizePrompt` refuses this
5390
+ // state anyway; checking it here keeps a superseded timer from
5391
+ // opening a boundary wait it can never use.
5392
+ if (current.terminal || current.phase !== "active") return;
5393
+ // One boundary wait per submission (review thread P2). At an already
5394
+ // due HARD CAP `promptDeadlineAt` is pinned to `acceptedAt + maxMs`,
5395
+ // so every streamed `tool_execution_update` — attributable progress
5396
+ // by design, so a multi-minute compile does not trip the inactivity
5397
+ // lease — re-arms a ZERO-delay timer that walks straight past the
5398
+ // re-arm check above. `deadlineAttempt` cannot serve as the fence: it
5399
+ // is registered only AFTER the wait, precisely so the boundary event
5400
+ // that ends the wait is not read as superseding it.
5401
+ if (current.deadlineExpiryInFlight) return;
5402
+ current.deadlineExpiryInFlight = true;
5403
+ try {
5404
+ // Let an in-flight dispatched tool call reach its boundary first
5405
+ // (#5637): terminal fencing aborts the run before it waits for
5406
+ // settlement, so a tool aborted mid-write leaves a torn artifact.
5407
+ // The wait sits BEFORE the attempt is registered on purpose — inside
5408
+ // `terminalizePrompt` the very `tool_execution_end` that ends the
5409
+ // wait would bump the lease generation and the post-claim fence
5410
+ // would read this attempt as "superseded", so the deadline would
5411
+ // effectively never fire while tools run. Here the existing
5412
+ // renewal/re-arm logic handles that case unchanged. Nothing has been
5413
+ // aborted yet, so this wait is composed with NO abort signal: an
5414
+ // already-aborted one would make the path unreachable.
5415
+ const boundary: ToolBoundaryWaitResult =
5416
+ pendingToolCallIdsFor(current).length === 0
5417
+ ? IDLE_TOOL_BOUNDARY_RESULT
5418
+ : await waitForToolCallBoundary({
5419
+ pending: () => pendingToolCallIdsFor(current),
5420
+ whenIdle: () => whenPromptToolsIdle(current),
5421
+ graceMs: TOOL_CALL_BOUNDARY_GRACE_MS,
5422
+ });
5423
+ // Every pre-await fence is re-validated after the await, in order.
5424
+ const after = promptSubmissions.get(key);
5425
+ if (!after || after.deadlineLease !== lease) return;
5426
+ if (after.terminal || after.phase !== "active") return;
5427
+ if (Date.now() < promptDeadlineAt(lease)) {
5428
+ // The boundary event renewed the lease: the prompt is demonstrably
5429
+ // alive, so re-arm instead of terminalizing it.
5430
+ armPromptDeadline(key, correlation);
5431
+ return;
5432
+ }
5433
+ if (boundary.outcome === "forced") {
5434
+ // Ids only: never tool arguments, output, paths or anything
5435
+ // credential-shaped.
5436
+ logger.warn("sdk_prompt_deadline_forced_mid_tool", {
5437
+ commandId: correlation.commandId,
5438
+ turnId: correlation.turnId,
5439
+ graceMs: TOOL_CALL_BOUNDARY_GRACE_MS,
5440
+ pendingToolCallIds: boundary.pendingToolCallIds,
5441
+ });
5442
+ }
5443
+ // Register the expiry attempt BEFORE the awaited durable claim:
5444
+ // progress arriving while the claim is in flight must supersede
5445
+ // this attempt (see the post-claim fence in terminalizePrompt).
5446
+ // The generation is read AFTER the boundary wait because the
5447
+ // boundary event may have bumped it and this attempt is
5448
+ // deliberately the current one.
5449
+ after.deadlineAttempt = { lease, generation: lease.generation };
5450
+ void terminalizePrompt(
5451
+ correlation,
5452
+ {
5453
+ kind: "failed",
5454
+ code: "prompt_deadline_exceeded",
5455
+ message: "Prompt deadline exceeded.",
5456
+ provenance: "deadline",
5457
+ phase: "submission",
5458
+ category: "deadline",
5459
+ },
5460
+ { fence: true },
5461
+ { diagnostic: { reason: promptDeadlineDiagnosticReason(boundary) } },
5462
+ );
5463
+ } finally {
5464
+ // `finally` is safe here, and a bare clear before each `return` is
5465
+ // not: a leaked `true` disables the deadline for the rest of the
5466
+ // submission's life, which is strictly worse than the pile-up it
5467
+ // guards. It can only ever clear the flag THIS attempt set on THIS
5468
+ // object — a competing attempt would have returned at the guard
5469
+ // above without setting it — and an attempt that threw is no longer
5470
+ // waiting, so re-admitting the next one is correct. The wait itself
5471
+ // is bounded by the grace, so this always runs.
5472
+ current.deadlineExpiryInFlight = false;
5473
+ }
5474
+ })();
5076
5475
  },
5077
5476
  Math.max(0, promptDeadlineAt(lease) - Date.now()),
5078
5477
  );
@@ -5083,6 +5482,7 @@ export function createNotificationsExtension(
5083
5482
  const correlation = runtime.activePromptCorrelation;
5084
5483
  // Renewal is attributed exactly like the correlated delivery below.
5085
5484
  renewPromptDeadline(correlation, event);
5485
+ noteToolDispatchBoundary(correlation, event);
5086
5486
  const submission = promptSubmissions.get(promptSubmissionKey(correlation));
5087
5487
  if (!submission || submission.abandoned) return;
5088
5488
  const frame = {
@@ -5169,6 +5569,99 @@ export function createNotificationsExtension(
5169
5569
  if (submission.deadlineLease.generation === generation) return;
5170
5570
  armPromptDeadline(key, correlation);
5171
5571
  };
5572
+ /**
5573
+ * Track which dispatched tool calls are executing for the accepted prompt,
5574
+ * so an expired deadline can abort at a tool boundary instead of mid-write
5575
+ * (#5637). Attribution is deliberately identical to `renewPromptDeadline`'s
5576
+ * — same funnel, same allowlist, same pairing-only exclusion — because a
5577
+ * call that cannot prove progress cannot prove it is running either.
5578
+ * `tool_execution_update` proves neither start nor end, so it never changes
5579
+ * membership.
5580
+ */
5581
+ const noteToolDispatchBoundary = (
5582
+ correlation: { commandId: string; turnId: string },
5583
+ event: { type: string; toolCallId?: unknown },
5584
+ ) => {
5585
+ if (event.type !== "tool_execution_start" && event.type !== "tool_execution_end") return;
5586
+ if (isNonDispatchedToolEvent(event) || typeof event.toolCallId !== "string") return;
5587
+ const submission = promptSubmissions.get(promptSubmissionKey(correlation));
5588
+ if (!submission) return;
5589
+ if (event.type === "tool_execution_start") {
5590
+ // A start whose own end already arrived describes a call that is over:
5591
+ // re-adding it would strand an id that can never be removed again, and
5592
+ // the deadline would then burn the whole grace and report a forced
5593
+ // mid-tool kill for a tool that is not running (review thread P1,
5594
+ // mirror case). The id is consumed, so the pair closes exactly once.
5595
+ if (submission.endedToolCallIds.delete(event.toolCallId)) return;
5596
+ submission.runningToolCallIds.add(event.toolCallId);
5597
+ return;
5598
+ }
5599
+ if (!submission.runningToolCallIds.delete(event.toolCallId)) {
5600
+ // End with no known start: remember it only for this out-of-order case.
5601
+ submission.endedToolCallIds.add(event.toolCallId);
5602
+ // Oldest-first eviction; a Set iterates in insertion order.
5603
+ while (submission.endedToolCallIds.size > UNMATCHED_TOOL_END_CAPACITY) {
5604
+ const oldest = submission.endedToolCallIds.values().next().value;
5605
+ if (oldest === undefined) break;
5606
+ submission.endedToolCallIds.delete(oldest);
5607
+ }
5608
+ return;
5609
+ }
5610
+ if (submission.runningToolCallIds.size > 0) return;
5611
+ for (const resolve of submission.toolIdleWaiters.splice(0)) resolve();
5612
+ };
5613
+ /**
5614
+ * Authoritative set of tool calls executing for this submission (#5637).
5615
+ *
5616
+ * The run resource ledger is the source of truth, not the extension fanout.
5617
+ * AgentLoop reserves a `kind: "tool"` lease SYNCHRONOUSLY inside its
5618
+ * dispatch loop, before the tool's `execute` is invoked, and the lease
5619
+ * settles at the call's real end. `tool_execution_start` instead travels an
5620
+ * ASYNCHRONOUS path — enqueued on the agent event stream, drained a turn
5621
+ * later, then handed to listeners whose returned promises are deliberately
5622
+ * not awaited, behind any user extension registered ahead of this one. So
5623
+ * `runningToolCallIds` can still be EMPTY while a file-mutating tool is
5624
+ * already writing, which is exactly the mid-`apply_patch` kill this fix
5625
+ * exists to prevent.
5626
+ *
5627
+ * So whenever the ledger can be read it is the SOLE authority, and a read
5628
+ * that comes back empty means empty. The fanout is wrong in both
5629
+ * directions, not one: ledger-running/event-idle must not report idle (the
5630
+ * mid-`apply_patch` kill above), and ledger-empty/event-running must not
5631
+ * report pending — the same asynchrony that delays a start also delays an
5632
+ * end, so a `tool_execution_end` stuck behind another extension would
5633
+ * otherwise burn the whole grace and record a forced mid-tool kill for a
5634
+ * call that had already returned.
5635
+ *
5636
+ * The fanout stays for observability and still wakes `toolIdleWaiters`, but
5637
+ * it is the pending authority only when the ledger cannot be consulted: an
5638
+ * older host without the seam, no execution handle bound yet, or a read
5639
+ * that threw. That fallback is exactly the pre-ledger behaviour and never
5640
+ * worse.
5641
+ */
5642
+ const pendingToolCallIdsFor = (submission: PromptSubmission): string[] => {
5643
+ const handle = submission.executionHandle;
5644
+ const readLedger = terminalAbortSeams?.pendingToolExecutions;
5645
+ if (handle && readLedger)
5646
+ try {
5647
+ // Materialized inside the `try` so only a COMPLETED read wins; a read
5648
+ // that succeeds with no entries still answers "nothing is running",
5649
+ // which is the whole point of preferring it over the stale fanout.
5650
+ return [...readLedger(handle)];
5651
+ } catch (error) {
5652
+ // A seam failure must never disable the deadline or extend it
5653
+ // unboundedly; fall back to the event-derived set for this attempt.
5654
+ logger.warn(`sdk: pending tool execution seam failed: ${String(error)}`);
5655
+ }
5656
+ return [...submission.runningToolCallIds];
5657
+ };
5658
+ /** Resolves once no dispatched tool call is executing for this submission. */
5659
+ const whenPromptToolsIdle = (submission: PromptSubmission): Promise<void> => {
5660
+ if (submission.runningToolCallIds.size === 0) return Promise.resolve();
5661
+ const { promise, resolve } = Promise.withResolvers<void>();
5662
+ submission.toolIdleWaiters.push(resolve);
5663
+ return promise;
5664
+ };
5172
5665
  const flushPromptLifecycle = (key: string, submission: PromptSubmission) => {
5173
5666
  for (const frame of submission.bufferedFrames.splice(0)) {
5174
5667
  try {
@@ -5200,6 +5693,7 @@ export function createNotificationsExtension(
5200
5693
  if (trackReconciliation) releasePromptAdmission(clientRef);
5201
5694
  return;
5202
5695
  }
5696
+ const workLeaseHandle = runtime?.workLease.acquire();
5203
5697
  // Register delivery ownership synchronously before any await so post-accept
5204
5698
  // failures that race the durable write still emit correlated terminals.
5205
5699
  cleanupPromptRecords();
@@ -5211,11 +5705,13 @@ export function createNotificationsExtension(
5211
5705
  ([, submission]) =>
5212
5706
  submission.terminal && (submission.phase === "delivered" || submission.fatal === true),
5213
5707
  );
5214
- if (!oldestTerminal)
5708
+ if (!oldestTerminal) {
5709
+ workLeaseHandle?.release();
5215
5710
  throw Object.assign(
5216
5711
  new Error("Too many active prompt submissions; reconcile or await terminal state."),
5217
5712
  { code: "reconciliation_capacity" },
5218
5713
  );
5714
+ }
5219
5715
  expirePromptDelivery(oldestTerminal[0], oldestTerminal[1]);
5220
5716
  }
5221
5717
  if (sdkRunToken) pendingPromptCorrelationsBySdkRunToken.set(sdkRunToken, correlation);
@@ -5230,15 +5726,19 @@ export function createNotificationsExtension(
5230
5726
  createdAt: Date.now(),
5231
5727
  deadlineLease: createPromptDeadlineLease({
5232
5728
  now: Date.now(),
5233
- leaseMs: readFiniteSetting("sdk.promptDeadlineMs", 1_800_000),
5234
- maxMs: readFiniteSetting("sdk.promptMaxRuntimeMs", 21_600_000),
5729
+ leaseMs: resolveSdkPromptDeadlineMs(settings?.get("sdk.promptDeadlineMs")),
5730
+ maxMs: resolveSdkPromptMaxRuntimeMs(settings?.get("sdk.promptMaxRuntimeMs")),
5235
5731
  }),
5236
5732
  phase: "active",
5733
+ ...(workLeaseHandle ? { workLease: workLeaseHandle } : {}),
5237
5734
  // Bound to the Agent run at `agent_start`; acceptance precedes execution.
5238
5735
  executionHandle: undefined,
5239
5736
  ...(preflightAbort ? { preflightAbort } : {}),
5240
5737
  reconciliationKind,
5241
5738
  bufferedFrames: [],
5739
+ runningToolCallIds: new Set<string>(),
5740
+ endedToolCallIds: new Set<string>(),
5741
+ toolIdleWaiters: [],
5242
5742
  };
5243
5743
  const key = promptSubmissionKey(correlation);
5244
5744
  promptSubmissions.set(key, submission);
@@ -5249,9 +5749,14 @@ export function createNotificationsExtension(
5249
5749
  };
5250
5750
  /** Roll back process-local registration when durable acceptance failed. */
5251
5751
  const discardPromptAcceptance = (correlation: { commandId: string; turnId: string }) => {
5252
- const submission = promptSubmissions.get(promptSubmissionKey(correlation));
5752
+ const key = promptSubmissionKey(correlation);
5753
+ const submission = promptSubmissions.get(key);
5253
5754
  if (submission?.deadlineTimer) clearTimeout(submission.deadlineTimer);
5254
- promptSubmissions.delete(promptSubmissionKey(correlation));
5755
+ if (submission?.workLease) {
5756
+ submission.workLease.release();
5757
+ submission.workLease = undefined;
5758
+ }
5759
+ promptSubmissions.delete(key);
5255
5760
  removePendingPromptCorrelation(correlation);
5256
5761
  };
5257
5762
  const recordPromptTerminal = (correlation: { commandId: string; turnId: string } | undefined) => {
@@ -5275,6 +5780,10 @@ export function createNotificationsExtension(
5275
5780
  ) => {
5276
5781
  submission.deadlineAttempt = undefined;
5277
5782
  if (submission.deadlineTimer) clearTimeout(submission.deadlineTimer);
5783
+ if (submission.workLease) {
5784
+ submission.workLease.release();
5785
+ submission.workLease = undefined;
5786
+ }
5278
5787
  if (!submission.terminal) {
5279
5788
  submission.terminal = true;
5280
5789
  submission.fatal = true;
@@ -5466,10 +5975,57 @@ export function createNotificationsExtension(
5466
5975
  }
5467
5976
  if (capture) capture.proof = proof;
5468
5977
  }
5469
- // Second deadline-attempt fence across the awaited fencing transition:
5470
- // progress recorded while fencing was in flight supersedes exactly
5471
- // like progress during the claim. Non-deadline attempts skip this —
5472
- // their attempt identity was never registered.
5978
+ if (deadlineAttempt) {
5979
+ // Durability BEFORE the durable terminal (#5583, #5623 review round 4).
5980
+ // The bus owns an independent deadline timer and terminalization path,
5981
+ // so the host wiring in `session-runtime` never sees this expiry —
5982
+ // without this call an active notification-bus session loses its dirty
5983
+ // edits on a deadline, which is the exact work-loss this change exists
5984
+ // to stop. It runs between the durable pending claim above and the
5985
+ // finalize below on purpose: a process death anywhere in this window
5986
+ // leaves the durable record PENDING, so restart recovery retries the
5987
+ // prompt instead of treating work that was never autosaved as finished.
5988
+ // Ordering it after the finalize made the autosave the one step a crash
5989
+ // could silently skip. The cost is that the durable terminal is delayed
5990
+ // by at most the flush's 10s bound.
5991
+ //
5992
+ // Deadline path only: reaching here inside `if (deadlineAttempt)` proves
5993
+ // the attempt was registered, and `winner` is the authoritative claimed
5994
+ // outcome, so a cancel, an `agent_failed`, or a normal `agent_end` never
5995
+ // autosaves. Bounded by the shared race and fully swallowed: the
5996
+ // terminal recorded and published below is identical whether the flush
5997
+ // succeeds, fails, or is abandoned.
5998
+ if (
5999
+ winner.kind === "failed" &&
6000
+ winner.code === "prompt_deadline_exceeded" &&
6001
+ settings?.get("sdk.flushWorktreeOnDeadline") !== false
6002
+ ) {
6003
+ // `has` is true only for a value the user actually wrote, so this
6004
+ // separates an explicit opt-in from the schema default. The flush only
6005
+ // honours the default inside a linked worktree the session owns.
6006
+ const configured = settings?.get("sdk.flushWorktreeOnDeadline");
6007
+ const hasExplicitSetting =
6008
+ typeof settings?.has === "function"
6009
+ ? settings.has("sdk.flushWorktreeOnDeadline")
6010
+ : configured === true;
6011
+ const explicitOptIn = hasExplicitSetting === true && configured === true;
6012
+ await runBoundedDeadlineFlush(signal =>
6013
+ flushWorktreeOnPromptDeadline(ctx.cwd, {
6014
+ agentDir: settings?.getAgentDir(),
6015
+ explicitOptIn,
6016
+ isCurrent: () =>
6017
+ deadlineAttemptStatus(promptSubmissionKey(correlation), submission, deadlineAttempt) ===
6018
+ "current",
6019
+ signal,
6020
+ }),
6021
+ );
6022
+ }
6023
+ }
6024
+ // Second deadline-attempt fence, across both the awaited fencing
6025
+ // transition and the autosave above: progress recorded while either was in
6026
+ // flight supersedes exactly like progress during the claim, and the
6027
+ // autosave alone can hold this path for ten seconds. Non-deadline attempts
6028
+ // skip this — their attempt identity was never registered.
5473
6029
  if (deadlineAttempt) {
5474
6030
  const key = promptSubmissionKey(correlation);
5475
6031
  const status = deadlineAttemptStatus(key, submission, deadlineAttempt);
@@ -6720,6 +7276,8 @@ export function createNotificationsExtension(
6720
7276
  terminalAbortSeams,
6721
7277
  // #4743: join fire-and-forget reconciliation producers at teardown.
6722
7278
  trackReconciliationProducer,
7279
+ workLease,
7280
+ promoteWorkLease,
6723
7281
  );
6724
7282
  cancelPreflightsForConnection = controlSurface.cancelPendingPreflightsForConnection;
6725
7283
  const abandonPromptResponse = (connectionId: string, frame: Record<string, unknown>) => {
@@ -6847,6 +7405,18 @@ export function createNotificationsExtension(
6847
7405
  start: async () => await server.start(),
6848
7406
  stop: async () => await server.stopAndWait(),
6849
7407
  broadcastFrame: frame => broadcastEventFrame(frame),
7408
+ onConnectionClose(handler) {
7409
+ connectionCloseHandler = handler;
7410
+ return () => {
7411
+ if (connectionCloseHandler === handler) connectionCloseHandler = undefined;
7412
+ };
7413
+ },
7414
+ onNegotiatedCapabilities(handler) {
7415
+ negotiatedCapabilitiesHandler = handler;
7416
+ return () => {
7417
+ if (negotiatedCapabilitiesHandler === handler) negotiatedCapabilitiesHandler = undefined;
7418
+ };
7419
+ },
6850
7420
  },
6851
7421
  ...(preparesExistingThread ? { readiness: "deferred" as const } : {}),
6852
7422
  ...(activationGate ? { activationGate } : {}),
@@ -6859,7 +7429,7 @@ export function createNotificationsExtension(
6859
7429
  expectedEpoch: options.masterAttestationEpoch,
6860
7430
  replay: consumedMasterNonces,
6861
7431
  }),
6862
- connectionCapabilities: connectionId => hostCapCache.get(connectionId),
7432
+ connectionCapabilities: liveHostCapabilities,
6863
7433
  installProviderDefinitions,
6864
7434
  onProviderDefinitionsRemoved: removeProviderDefinitions,
6865
7435
  onRequest: options.onSdkRequest,
@@ -7242,6 +7812,8 @@ export function createNotificationsExtension(
7242
7812
  policyGeneration: 0,
7243
7813
  workflowGatePublicationEpoch: 0,
7244
7814
  busy: false,
7815
+ workLease,
7816
+ promotedWorkLeases: [],
7245
7817
  pendingPromptCorrelations,
7246
7818
  pendingPromptCorrelationsBySdkRunToken,
7247
7819
  activePromptCorrelation: undefined,
@@ -7385,14 +7957,10 @@ export function createNotificationsExtension(
7385
7957
  sendEndpointStale(inbound.connectionId, typedFrame);
7386
7958
  return;
7387
7959
  }
7388
- if (typedFrame.type === "ephemeral_turn" || typedFrame.type === "ephemeral_turn_cancel") return;
7389
7960
  if (typedFrame.type === "event_replay") {
7390
- const capabilities = Array.isArray(typedFrame.capabilities) ? typedFrame.capabilities : [];
7391
- hostCapCache.set(
7392
- inbound.connectionId,
7393
- new Set(capabilities.filter((capability): capability is string => typeof capability === "string")),
7394
- );
7961
+ if (liveHostConnection(inbound.connectionId)) hostAttachedConnections.add(inbound.connectionId);
7395
7962
  }
7963
+ if (typedFrame.type === "ephemeral_turn" || typedFrame.type === "ephemeral_turn_cancel") return;
7396
7964
  inboundSdkFrame?.(inbound.connectionId, typedFrame);
7397
7965
  } catch (error) {
7398
7966
  sendMalformed(
@@ -7425,15 +7993,27 @@ export function createNotificationsExtension(
7425
7993
  );
7426
7994
  }
7427
7995
  server.onNegotiatedCapabilities((_err, connectionId, capabilities) => {
7428
- if (connectionId) hostCapCache.set(connectionId, new Set(capabilities));
7996
+ // ThreadsafeFunction currently delivers its tuple payload as the second
7997
+ // callback argument at runtime, while older generated declarations expose
7998
+ // the tuple members as separate parameters. Accept both shapes without
7999
+ // allowing an unvalidated payload to become authority.
8000
+ const tuple = Array.isArray(connectionId) ? (connectionId as unknown[]) : undefined;
8001
+ const id = tuple?.[0] ?? connectionId;
8002
+ const negotiated = tuple?.[1] ?? capabilities;
8003
+ if (typeof id !== "string" || !Array.isArray(negotiated)) return;
8004
+ const normalized = negotiated.filter((capability): capability is string => typeof capability === "string");
8005
+ rememberHostCapabilities(id, normalized);
8006
+ if (liveHostCapabilities(id) === undefined) return;
8007
+ hostAttachedConnections.add(id);
8008
+ negotiatedCapabilitiesHandler?.(id, normalized);
7429
8009
  });
7430
8010
  server.onConnectionClose((_err, connectionId) => {
7431
8011
  if (!connectionId) return;
8012
+ closeHostConnection(connectionId);
8013
+ connectionCloseHandler?.(connectionId);
7432
8014
  void controlSurface
7433
8015
  .cancelPendingPreflightsForConnection(connectionId)
7434
8016
  .catch(error => logger.warn(`sdk: failed to cancel disconnected preflight: ${String(error)}`));
7435
- host.handleDisconnect(connectionId);
7436
- hostCapCache.delete(connectionId);
7437
8017
  // The socket is gone, so its fence has nothing left to refuse. Dropping the
7438
8018
  // entry keeps the set bounded by live connections instead of growing forever.
7439
8019
  fencedConnections.delete(connectionId);
@@ -7699,7 +8279,7 @@ export function createNotificationsExtension(
7699
8279
  // thread (forwarded by the daemon over the WS, fail-closed at the daemon).
7700
8280
  server.onInbound(async (err, inbound) => {
7701
8281
  if (err || !inbound) return;
7702
- const notificationOrigin = hostCapCache.get(inbound.connectionId)?.has(ASK_SELECTED_ACK_CAPABILITY);
8282
+ const notificationOrigin = liveHostCapabilities(inbound.connectionId)?.has(ASK_SELECTED_ACK_CAPABILITY);
7703
8283
  const admission = notificationInboundAdmission({
7704
8284
  inboundFenced: initializedRuntime.inboundFenced,
7705
8285
  policySuspended: runtime?.policySuspended ?? false,
@@ -7804,6 +8384,8 @@ export function createNotificationsExtension(
7804
8384
  ]
7805
8385
  : text;
7806
8386
  let acceptedSent = false;
8387
+ const deliverAsSteer = runtime?.busy === true;
8388
+ const admission = createAdmissionWorkLease(workLease, deliverAsSteer, promoteWorkLease);
7807
8389
  const acceptAdmission = (): void => {
7808
8390
  // Fired by AgentSession's preflight acceptance: after admission has
7809
8391
  // committed but before the turn starts. Registering the update id and
@@ -7811,6 +8393,7 @@ export function createNotificationsExtension(
7811
8393
  // pendingInbound registration it consumes.
7812
8394
  if (acceptedSent) return;
7813
8395
  acceptedSent = true;
8396
+ admission.accepted();
7814
8397
  if (runtime && typeof inbound.updateId === "number") runtime.pendingInbound.add(inbound.updateId);
7815
8398
  sendInboundAck(authenticatedInbound.connectionId, authenticatedInbound, "accepted");
7816
8399
  };
@@ -7820,12 +8403,15 @@ export function createNotificationsExtension(
7820
8403
  // of after the await; a rejection (preflight, admission, or later)
7821
8404
  // still reaches the catch and maps to a rejected ack.
7822
8405
  await api.sendUserMessage(content, {
7823
- ...(runtime?.busy ? { deliverAs: "steer" as const } : {}),
8406
+ ...(deliverAsSteer ? { deliverAs: "steer" as const } : {}),
7824
8407
  onPreflightAcceptCommit: acceptAdmission,
7825
8408
  onPreflightAccepted: acceptAdmission,
8409
+ ...admission.hooks,
7826
8410
  });
7827
8411
  acceptAdmission();
8412
+ admission.dispatched();
7828
8413
  } catch (e) {
8414
+ admission.abandon();
7829
8415
  sendInboundAck(
7830
8416
  authenticatedInbound.connectionId,
7831
8417
  authenticatedInbound,
@@ -7950,15 +8536,21 @@ export function createNotificationsExtension(
7950
8536
  }
7951
8537
 
7952
8538
  // The native server owns the only authoritative view of this host's live
7953
- // SDK client sockets; publish it so a detached session host can bound its
7954
- // own lifetime without probing the OS (#4010). The handle is this
7955
- // runtime's alone, so only this runtime's teardown can retract it.
8539
+ // SDK client sockets; publish it with observer-only sockets separated so a
8540
+ // detached session host can bound its own lifetime without probing the OS
8541
+ // (#4010). Work is represented by the session's single lease, not by
8542
+ // reconstructed busy/correlation fields. The handle is this runtime's alone,
8543
+ // so only this runtime's teardown can retract it.
7956
8544
  initializedRuntime.evidencePublication = publishSessionHostRuntimeEvidence({
7957
8545
  attachedClients: () => server.clientCount(),
7958
- workInFlight: () =>
7959
- initializedRuntime.busy ||
7960
- initializedRuntime.pendingPromptCorrelations.length > 0 ||
7961
- initializedRuntime.pendingPromptCorrelationsBySdkRunToken.size > 0,
8546
+ observerClients: () => {
8547
+ if (workLease.isHeld()) return 0;
8548
+ const observers = [...hostCapCache.keys()].filter(connectionId =>
8549
+ liveHostCapabilities(connectionId)?.has(SESSION_HOST_OBSERVER_CAPABILITY),
8550
+ ).length;
8551
+ return Math.min(server.clientCount(), observers);
8552
+ },
8553
+ workLease,
7962
8554
  });
7963
8555
  ephemeralTurns.configureAuthority({
7964
8556
  sessionId: id,
@@ -8060,6 +8652,32 @@ export function createNotificationsExtension(
8060
8652
  ...(lifecycleRequestId ? { lifecycleRequestId } : {}),
8061
8653
  });
8062
8654
  },
8655
+ heartbeat: async input => {
8656
+ const expected = registration;
8657
+ if (
8658
+ !expected ||
8659
+ expected.sessionId !== input.sessionId ||
8660
+ expected.endpointGeneration !== input.endpointGeneration ||
8661
+ path.resolve(expected.locator.stateRoot) !== path.resolve(input.stateRoot)
8662
+ )
8663
+ return;
8664
+ await index.append({
8665
+ type: "host_heartbeat",
8666
+ sessionId: expected.sessionId,
8667
+ locator: expected.locator,
8668
+ endpointGeneration: expected.endpointGeneration,
8669
+ pid: expected.pid,
8670
+ ...(expected.processIncarnation === undefined
8671
+ ? {}
8672
+ : { processIncarnation: expected.processIncarnation }),
8673
+ ...(expected.hostIncarnation === undefined
8674
+ ? {}
8675
+ : { hostIncarnation: expected.hostIncarnation }),
8676
+ ...(expected.masterRole === undefined ? {} : { masterRole: expected.masterRole }),
8677
+ activity: input.activity,
8678
+ ts: input.activity.at,
8679
+ });
8680
+ },
8063
8681
  unregister: async input => {
8064
8682
  const expected = registration;
8065
8683
  if (
@@ -8075,8 +8693,8 @@ export function createNotificationsExtension(
8075
8693
  });
8076
8694
  throwIfLifecycleStopped();
8077
8695
  initializedRuntime.brokerRegistrationActive = true;
8078
- // Host liveness is derived from alive(pid) when the index is read; heartbeats
8079
- // are deliberately not appended to the durable session index.
8696
+ // Host liveness is derived from alive(pid) and the coalesced heartbeats;
8697
+ // activity transitions are appended by the host through this registration.
8080
8698
  } catch (brokerError) {
8081
8699
  if (lifecycleRequired) throw brokerError;
8082
8700
  logger.warn(`sdk broker registration skipped: ${String(brokerError)}`);
@@ -8896,6 +9514,9 @@ export function createNotificationsExtension(
8896
9514
  const id = sessionId(ctx);
8897
9515
  const rt = runtimes.get(id);
8898
9516
  if (!rt) return;
9517
+ void rt.host
9518
+ .reportActivity("active")
9519
+ .catch(error => logger.warn(`notifications: active activity checkpoint failed: ${String(error)}`));
8899
9520
  // Streaming state is SDK-visible session truth (context.get isStreaming);
8900
9521
  // it is tracked regardless of whether notifications are active.
8901
9522
  rt.busy = true;
@@ -8918,6 +9539,7 @@ export function createNotificationsExtension(
8918
9539
  if (sdkRunToken && correlation) rt.pendingPromptCorrelationsBySdkRunToken.delete(sdkRunToken);
8919
9540
  const continuation = rt.activePromptCorrelation !== undefined;
8920
9541
  rt.activePromptCorrelation = correlation;
9542
+ beginAgentWork(rt);
8921
9543
  if (correlation && !continuation) rt.bindPromptExecutionHandle(correlation, ctx.getActivePromptHandle());
8922
9544
  if (continuation) return;
8923
9545
  await rt.notePromptReconciliation(correlation, { type: "agent_start" });
@@ -8983,6 +9605,9 @@ export function createNotificationsExtension(
8983
9605
  const id = sessionId(ctx);
8984
9606
  const rt = runtimes.get(id);
8985
9607
  if (!rt) return;
9608
+ void rt.host
9609
+ .reportActivity("idle")
9610
+ .catch(error => logger.warn(`notifications: idle activity checkpoint failed: ${String(error)}`));
8986
9611
  // Clear the streaming flag for SDK consumers even when notifications are off.
8987
9612
  rt.busy = false;
8988
9613
  const correlation = rt.activePromptCorrelation;
@@ -9070,6 +9695,7 @@ export function createNotificationsExtension(
9070
9695
  } else {
9071
9696
  rt.emitPromptLifecycle(undefined, { type: "agent_end", sessionId: id });
9072
9697
  }
9698
+ finishAgentWork(rt, event.stopReason === "paused");
9073
9699
  rt.activePromptCorrelation = undefined;
9074
9700
  terminalizeInFlightTools(rt, id, event.stopReason === "cancelled" ? "cancelled" : "failed");
9075
9701
  try {