@gajae-code/coding-agent 0.17.1 → 0.17.4

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 (375) hide show
  1. package/CHANGELOG.md +266 -0
  2. package/dist/types/async/job-manager.d.ts +7 -6
  3. package/dist/types/cli/daemon-cli.d.ts +3 -1
  4. package/dist/types/cli/github-release.d.ts +17 -0
  5. package/dist/types/cli/public-command-entry.d.ts +56 -0
  6. package/dist/types/cli/public-command-errors.d.ts +126 -0
  7. package/dist/types/cli/public-command-evidence.d.ts +99 -0
  8. package/dist/types/cli/public-command-help.d.ts +65 -0
  9. package/dist/types/cli/public-command-registry.d.ts +69 -0
  10. package/dist/types/cli/session-picker.d.ts +1 -1
  11. package/dist/types/cli/setup-cli.d.ts +1 -0
  12. package/dist/types/cli/skills-cli.d.ts +5 -0
  13. package/dist/types/cli.d.ts +3 -0
  14. package/dist/types/commands/daemon.d.ts +4 -50
  15. package/dist/types/commands/sdk.d.ts +20 -37
  16. package/dist/types/commands/setup.d.ts +3 -0
  17. package/dist/types/commands/skills.d.ts +9 -0
  18. package/dist/types/config/autorouting-tier-map.d.ts +24 -0
  19. package/dist/types/config/file-lock.d.ts +10 -1
  20. package/dist/types/config/model-profile-activation.d.ts +63 -4
  21. package/dist/types/config/model-registry.d.ts +20 -2
  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/coordinator-mcp/server.d.ts +3 -0
  25. package/dist/types/daemon/operator-contract.d.ts +5 -1
  26. package/dist/types/eval/js/worker-protocol.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/runtime-skill-discovery.d.ts +18 -0
  30. package/dist/types/extensibility/shared-events.d.ts +3 -1
  31. package/dist/types/gjc-runtime/session-state-lock.d.ts +61 -0
  32. package/dist/types/internal-urls/embedded-protocol.d.ts +9 -0
  33. package/dist/types/internal-urls/index.d.ts +1 -0
  34. package/dist/types/internal-urls/parse.d.ts +9 -1
  35. package/dist/types/main.d.ts +22 -0
  36. package/dist/types/modes/acp/acp-agent.d.ts +94 -1
  37. package/dist/types/modes/acp/startup-provenance.d.ts +16 -0
  38. package/dist/types/modes/components/custom-provider-wizard.d.ts +19 -1
  39. package/dist/types/modes/components/session-selector.d.ts +5 -2
  40. package/dist/types/modes/components/thinking-selector.d.ts +2 -1
  41. package/dist/types/modes/components/user-message-selector.d.ts +5 -2
  42. package/dist/types/modes/controllers/command-controller.d.ts +1 -1
  43. package/dist/types/modes/controllers/selector-controller.d.ts +1 -1
  44. package/dist/types/modes/interactive-mode.d.ts +1 -1
  45. package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +11 -1
  46. package/dist/types/modes/shared/agent-wire/workflow-gate-types.d.ts +14 -1
  47. package/dist/types/modes/types.d.ts +2 -2
  48. package/dist/types/runtime/process-lifecycle.d.ts +2 -0
  49. package/dist/types/runtime-mcp/manager.d.ts +56 -0
  50. package/dist/types/runtime-mcp/pool.d.ts +17 -0
  51. package/dist/types/runtime-mcp/redaction.d.ts +20 -0
  52. package/dist/types/runtime-mcp/startup-policy.d.ts +6 -0
  53. package/dist/types/runtime-mcp/tool-bridge.d.ts +9 -0
  54. package/dist/types/runtime-mcp/transports/http.d.ts +11 -0
  55. package/dist/types/sdk/acp/adapter.d.ts +27 -0
  56. package/dist/types/sdk/acp/final-text.d.ts +8 -0
  57. package/dist/types/sdk/broker/broker.d.ts +13 -2
  58. package/dist/types/sdk/broker/ensure.d.ts +46 -1
  59. package/dist/types/sdk/broker/identity.d.ts +3 -1
  60. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +23 -3
  61. package/dist/types/sdk/broker/lifecycle.d.ts +15 -8
  62. package/dist/types/sdk/broker/runtime.d.ts +1 -1
  63. package/dist/types/sdk/broker/session-index.d.ts +22 -3
  64. package/dist/types/sdk/broker/spawn-authority.d.ts +4 -0
  65. package/dist/types/sdk/broker/stderr-drainer.d.ts +13 -0
  66. package/dist/types/sdk/bus/index.d.ts +19 -0
  67. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +3 -1
  68. package/dist/types/sdk/bus/telegram-daemon.d.ts +5 -0
  69. package/dist/types/sdk/cli/master-cli.d.ts +6 -10
  70. package/dist/types/sdk/cli/rows.d.ts +19 -1
  71. package/dist/types/sdk/cli/session-cli.d.ts +22 -5
  72. package/dist/types/sdk/client/client.d.ts +2 -0
  73. package/dist/types/sdk/guides/cli.d.ts +3 -4
  74. package/dist/types/sdk/guides/verify.d.ts +2 -0
  75. package/dist/types/sdk/host/host.d.ts +9 -0
  76. package/dist/types/sdk/host/session-runtime.d.ts +31 -0
  77. package/dist/types/sdk/host/types.d.ts +10 -0
  78. package/dist/types/sdk/index.d.ts +1 -0
  79. package/dist/types/sdk/prompt-deadline-flush.d.ts +83 -0
  80. package/dist/types/sdk/prompt-deadline-manager.d.ts +38 -1
  81. package/dist/types/sdk/prompt-failure.d.ts +30 -1
  82. package/dist/types/sdk/prompt-status.d.ts +8 -0
  83. package/dist/types/sdk/prompt-tool-boundary.d.ts +73 -0
  84. package/dist/types/sdk/router/replay-retention-gap-cache.d.ts +13 -0
  85. package/dist/types/sdk/router/session-router.d.ts +2 -0
  86. package/dist/types/sdk/startup-capability.d.ts +2 -0
  87. package/dist/types/sdk/transport/relay.d.ts +1 -0
  88. package/dist/types/sdk/transport/serve-cli.d.ts +73 -2
  89. package/dist/types/sdk/transport/stdio.d.ts +5 -1
  90. package/dist/types/session/agent-session.d.ts +168 -21
  91. package/dist/types/session/fallback-chain-controller.d.ts +6 -6
  92. package/dist/types/session/fold-coordinator.d.ts +17 -3
  93. package/dist/types/session/internal/managed-session-storage.d.ts +7 -1
  94. package/dist/types/session/session-manager.d.ts +64 -1
  95. package/dist/types/session/session-work-lease.d.ts +14 -0
  96. package/dist/types/setup/model-onboarding-guidance.d.ts +1 -1
  97. package/dist/types/setup/provider-onboarding.d.ts +81 -1
  98. package/dist/types/thinking.d.ts +3 -1
  99. package/dist/types/tools/bash.d.ts +37 -11
  100. package/dist/types/tools/job.d.ts +1 -1
  101. package/dist/types/tools/steer-fold.d.ts +31 -0
  102. package/dist/types/utils/git.d.ts +28 -2
  103. package/dist/types/utils/mupdf-embedded.d.ts +2 -0
  104. package/dist/types/utils/mupdf-wasm-embedded.d.ts +2 -0
  105. package/dist/types/utils/mupdf.d.ts +4 -0
  106. package/dist/types/utils/session-title-generation.d.ts +6 -0
  107. package/dist/types/utils/title-generator.d.ts +11 -2
  108. package/dist/types/web/scrapers/types.d.ts +3 -0
  109. package/package.json +18 -10
  110. package/scripts/build-binary.ts +9 -3
  111. package/scripts/compile-args.ts +0 -9
  112. package/scripts/embed-mupdf.ts +52 -0
  113. package/scripts/generate-sdk-operation-inventory.ts +31 -1
  114. package/scripts/vendor-markit.ts +196 -0
  115. package/src/async/job-manager.ts +60 -9
  116. package/src/cli/daemon-cli.ts +53 -18
  117. package/src/cli/github-release.ts +87 -5
  118. package/src/cli/mcp-cli.ts +38 -4
  119. package/src/cli/public-command-entry.ts +450 -0
  120. package/src/cli/public-command-errors.ts +581 -0
  121. package/src/cli/public-command-evidence.ts +717 -0
  122. package/src/cli/public-command-help.ts +360 -0
  123. package/src/cli/public-command-registry.ts +535 -0
  124. package/src/cli/session-picker.ts +2 -1
  125. package/src/cli/setup-cli.ts +9 -1
  126. package/src/cli/skills-cli.ts +46 -2
  127. package/src/cli.ts +61 -1
  128. package/src/commands/daemon.ts +65 -69
  129. package/src/commands/sdk.ts +232 -100
  130. package/src/commands/setup.ts +2 -0
  131. package/src/commands/skills.ts +11 -0
  132. package/src/config/autorouting-tier-map.ts +16 -0
  133. package/src/config/file-lock.ts +199 -68
  134. package/src/config/model-profile-activation.ts +406 -53
  135. package/src/config/model-registry.ts +204 -25
  136. package/src/config/settings-schema.ts +26 -2
  137. package/src/coordinator-mcp/policy.ts +10 -5
  138. package/src/coordinator-mcp/question-gate-codec.ts +17 -0
  139. package/src/coordinator-mcp/question-state.ts +175 -8
  140. package/src/coordinator-mcp/server.ts +528 -87
  141. package/src/coordinator-mcp/session-reaper.ts +6 -4
  142. package/src/crash/record-loader.ts +6 -2
  143. package/src/daemon/operator-contract.ts +26 -12
  144. package/src/debug/report-bundle.ts +114 -24
  145. package/src/defaults/gjc/skills/ultragoal/SKILL.md +1 -1
  146. package/src/eval/js/context-manager.ts +8 -8
  147. package/src/eval/js/worker-core.ts +9 -4
  148. package/src/eval/js/worker-protocol.ts +2 -0
  149. package/src/extensibility/extensions/runner.ts +52 -4
  150. package/src/extensibility/extensions/types.ts +10 -0
  151. package/src/extensibility/gjc-plugins/registry.ts +196 -37
  152. package/src/extensibility/runtime-skill-discovery.ts +61 -3
  153. package/src/extensibility/shared-events.ts +3 -1
  154. package/src/gjc-runtime/managed-owner-admission.ts +67 -50
  155. package/src/gjc-runtime/session-state-lock.ts +165 -24
  156. package/src/gjc-runtime/ultragoal-succession.ts +31 -3
  157. package/src/internal-urls/docs-index.generated.ts +21 -19
  158. package/src/internal-urls/embedded-protocol.ts +83 -0
  159. package/src/internal-urls/index.ts +1 -0
  160. package/src/internal-urls/parse.ts +21 -1
  161. package/src/internal-urls/router.ts +4 -2
  162. package/src/main.ts +123 -55
  163. package/src/modes/acp/acp-agent.ts +593 -47
  164. package/src/modes/acp/acp-event-mapper.ts +9 -0
  165. package/src/modes/acp/startup-provenance.ts +59 -0
  166. package/src/modes/components/custom-provider-wizard.ts +308 -7
  167. package/src/modes/components/model-selector.ts +31 -15
  168. package/src/modes/components/oauth-selector.ts +9 -0
  169. package/src/modes/components/session-selector.ts +47 -4
  170. package/src/modes/components/status-line/segments.ts +6 -1
  171. package/src/modes/components/thinking-selector.ts +7 -3
  172. package/src/modes/components/user-message-selector.ts +57 -15
  173. package/src/modes/controllers/command-controller.ts +58 -32
  174. package/src/modes/controllers/extension-ui-controller.ts +33 -7
  175. package/src/modes/controllers/input-controller.ts +4 -0
  176. package/src/modes/controllers/selector-controller.ts +177 -18
  177. package/src/modes/interactive-mode.ts +21 -7
  178. package/src/modes/runtime-init.ts +1 -0
  179. package/src/modes/shared/agent-wire/workflow-gate-broker.ts +124 -7
  180. package/src/modes/shared/agent-wire/workflow-gate-types.ts +16 -2
  181. package/src/modes/types.ts +2 -2
  182. package/src/prompts/agent-fragments/browser-aside.md +1 -0
  183. package/src/prompts/system/title-system.md +1 -1
  184. package/src/prompts/tools/bash.md +1 -1
  185. package/src/prompts/tools/job.md +1 -1
  186. package/src/prompts/tools/read.md +2 -0
  187. package/src/runtime/network-prewarm-service.test.ts +36 -1
  188. package/src/runtime/network-prewarm-service.ts +28 -3
  189. package/src/runtime/process-lifecycle.ts +22 -11
  190. package/src/runtime-mcp/config.ts +16 -1
  191. package/src/runtime-mcp/manager.ts +689 -55
  192. package/src/runtime-mcp/pool.test.ts +31 -0
  193. package/src/runtime-mcp/pool.ts +304 -28
  194. package/src/runtime-mcp/redaction.ts +42 -0
  195. package/src/runtime-mcp/startup-policy.ts +7 -0
  196. package/src/runtime-mcp/tool-bridge.ts +71 -0
  197. package/src/runtime-mcp/transports/http.ts +108 -25
  198. package/src/sdk/acp/adapter.ts +90 -9
  199. package/src/sdk/acp/final-text.ts +19 -2
  200. package/src/sdk/broker/broker.ts +378 -52
  201. package/src/sdk/broker/ensure.ts +97 -19
  202. package/src/sdk/broker/identity.ts +9 -1
  203. package/src/sdk/broker/lifecycle-ledger.ts +95 -31
  204. package/src/sdk/broker/lifecycle.ts +690 -141
  205. package/src/sdk/broker/runtime.ts +1 -1
  206. package/src/sdk/broker/session-index.ts +97 -4
  207. package/src/sdk/broker/spawn-authority.ts +35 -7
  208. package/src/sdk/broker/stderr-drainer.ts +59 -0
  209. package/src/sdk/bus/chat-daemon-control.ts +2 -2
  210. package/src/sdk/bus/chat-daemon-runtime.ts +1 -0
  211. package/src/sdk/bus/index.ts +721 -84
  212. package/src/sdk/bus/telegram-daemon-cli.ts +31 -2
  213. package/src/sdk/bus/telegram-daemon-contract.ts +3 -1
  214. package/src/sdk/bus/telegram-daemon.ts +10 -3
  215. package/src/sdk/cli/master-cli.ts +47 -36
  216. package/src/sdk/cli/rows.ts +30 -7
  217. package/src/sdk/cli/session-cli.ts +687 -184
  218. package/src/sdk/client/client.ts +18 -0
  219. package/src/sdk/guides/catalog.ts +2 -2
  220. package/src/sdk/guides/cli.ts +20 -62
  221. package/src/sdk/guides/verify.ts +14 -0
  222. package/src/sdk/host/host.ts +49 -5
  223. package/src/sdk/host/session-runtime.test.ts +636 -9
  224. package/src/sdk/host/session-runtime.ts +499 -118
  225. package/src/sdk/host/types.ts +12 -1
  226. package/src/sdk/host/websocket-transport.lifecycle.test.ts +84 -0
  227. package/src/sdk/host/websocket-transport.ts +40 -6
  228. package/src/sdk/index.ts +11 -0
  229. package/src/sdk/prompt-deadline-flush.ts +334 -0
  230. package/src/sdk/prompt-deadline-manager.ts +136 -4
  231. package/src/sdk/prompt-failure.ts +43 -1
  232. package/src/sdk/prompt-status.ts +8 -0
  233. package/src/sdk/prompt-tool-boundary.ts +121 -0
  234. package/src/sdk/protocol/operation-inventory.generated.json +106 -0
  235. package/src/sdk/router/replay-retention-gap-cache.ts +27 -0
  236. package/src/sdk/router/session-router.ts +31 -4
  237. package/src/sdk/session.ts +194 -16
  238. package/src/sdk/startup-capability.ts +1 -1
  239. package/src/sdk/transport/relay.ts +71 -6
  240. package/src/sdk/transport/serve-cli.ts +494 -49
  241. package/src/sdk/transport/socket.ts +12 -11
  242. package/src/sdk/transport/stdio.ts +7 -3
  243. package/src/session/agent-session.ts +2437 -400
  244. package/src/session/fallback-chain-controller.ts +6 -7
  245. package/src/session/fold-coordinator.ts +24 -5
  246. package/src/session/internal/managed-session-scope.ts +58 -18
  247. package/src/session/internal/managed-session-storage.ts +304 -11
  248. package/src/session/session-manager.ts +382 -44
  249. package/src/session/session-work-lease.ts +34 -0
  250. package/src/session-import/codex.ts +12 -3
  251. package/src/setup/model-onboarding-guidance.ts +1 -1
  252. package/src/setup/provider-onboarding.ts +397 -24
  253. package/src/slash-commands/builtin-registry.ts +258 -14
  254. package/src/task/index.ts +14 -2
  255. package/src/thinking.ts +21 -2
  256. package/src/tools/bash.ts +98 -78
  257. package/src/tools/fetch.ts +119 -20
  258. package/src/tools/job.ts +151 -25
  259. package/src/tools/output-meta.ts +15 -2
  260. package/src/tools/path-utils.ts +27 -3
  261. package/src/tools/read.ts +3 -3
  262. package/src/tools/steer-fold.ts +107 -0
  263. package/src/tools/tool-catalog.generated.ts +29 -2
  264. package/src/tools/tool-errors.ts +4 -0
  265. package/src/utils/git.ts +53 -3
  266. package/src/utils/markit.ts +55 -41
  267. package/src/utils/mupdf-assets.d.ts +4 -0
  268. package/src/utils/mupdf-embedded.ts +4 -0
  269. package/src/utils/mupdf-wasm-embedded.ts +3 -0
  270. package/src/utils/mupdf-wasm.ts +24 -2
  271. package/src/utils/mupdf.ts +129 -0
  272. package/src/utils/session-title-generation.ts +29 -0
  273. package/src/utils/title-generator.ts +46 -2
  274. package/src/web/scrapers/types.ts +24 -3
  275. package/vendor/markit-ai/LICENSE +21 -0
  276. package/vendor/markit-ai/MANIFEST.json +412 -0
  277. package/vendor/markit-ai/dist/commands/config.d.ts +4 -0
  278. package/vendor/markit-ai/dist/commands/config.js +139 -0
  279. package/vendor/markit-ai/dist/commands/convert.d.ts +6 -0
  280. package/vendor/markit-ai/dist/commands/convert.js +113 -0
  281. package/vendor/markit-ai/dist/commands/formats.d.ts +2 -0
  282. package/vendor/markit-ai/dist/commands/formats.js +92 -0
  283. package/vendor/markit-ai/dist/commands/init.d.ts +2 -0
  284. package/vendor/markit-ai/dist/commands/init.js +29 -0
  285. package/vendor/markit-ai/dist/commands/onboard.d.ts +2 -0
  286. package/vendor/markit-ai/dist/commands/onboard.js +61 -0
  287. package/vendor/markit-ai/dist/commands/plugin.d.ts +4 -0
  288. package/vendor/markit-ai/dist/commands/plugin.js +58 -0
  289. package/vendor/markit-ai/dist/config.d.ts +26 -0
  290. package/vendor/markit-ai/dist/config.js +42 -0
  291. package/vendor/markit-ai/dist/converters/audio.d.ts +7 -0
  292. package/vendor/markit-ai/dist/converters/audio.js +104 -0
  293. package/vendor/markit-ai/dist/converters/csv.d.ts +7 -0
  294. package/vendor/markit-ai/dist/converters/csv.js +83 -0
  295. package/vendor/markit-ai/dist/converters/docx.d.ts +6 -0
  296. package/vendor/markit-ai/dist/converters/docx.js +57 -0
  297. package/vendor/markit-ai/dist/converters/epub.d.ts +8 -0
  298. package/vendor/markit-ai/dist/converters/epub.js +128 -0
  299. package/vendor/markit-ai/dist/converters/github.d.ts +18 -0
  300. package/vendor/markit-ai/dist/converters/github.js +148 -0
  301. package/vendor/markit-ai/dist/converters/html.d.ts +6 -0
  302. package/vendor/markit-ai/dist/converters/html.js +30 -0
  303. package/vendor/markit-ai/dist/converters/image.d.ts +6 -0
  304. package/vendor/markit-ai/dist/converters/image.js +124 -0
  305. package/vendor/markit-ai/dist/converters/ipynb.d.ts +6 -0
  306. package/vendor/markit-ai/dist/converters/ipynb.js +75 -0
  307. package/vendor/markit-ai/dist/converters/iwork.d.ts +20 -0
  308. package/vendor/markit-ai/dist/converters/iwork.js +391 -0
  309. package/vendor/markit-ai/dist/converters/json.d.ts +6 -0
  310. package/vendor/markit-ai/dist/converters/json.js +21 -0
  311. package/vendor/markit-ai/dist/converters/pdf/columns.d.ts +35 -0
  312. package/vendor/markit-ai/dist/converters/pdf/columns.js +93 -0
  313. package/vendor/markit-ai/dist/converters/pdf/extract.d.ts +19 -0
  314. package/vendor/markit-ai/dist/converters/pdf/extract.js +514 -0
  315. package/vendor/markit-ai/dist/converters/pdf/grid.d.ts +25 -0
  316. package/vendor/markit-ai/dist/converters/pdf/grid.js +795 -0
  317. package/vendor/markit-ai/dist/converters/pdf/headers.d.ts +24 -0
  318. package/vendor/markit-ai/dist/converters/pdf/headers.js +108 -0
  319. package/vendor/markit-ai/dist/converters/pdf/index.d.ts +19 -0
  320. package/vendor/markit-ai/dist/converters/pdf/index.js +136 -0
  321. package/vendor/markit-ai/dist/converters/pdf/mupdf-loader.js +9 -0
  322. package/vendor/markit-ai/dist/converters/pdf/render.d.ts +24 -0
  323. package/vendor/markit-ai/dist/converters/pdf/render.js +513 -0
  324. package/vendor/markit-ai/dist/converters/pdf/types.d.ts +75 -0
  325. package/vendor/markit-ai/dist/converters/pdf/types.js +1 -0
  326. package/vendor/markit-ai/dist/converters/plain-text.d.ts +6 -0
  327. package/vendor/markit-ai/dist/converters/plain-text.js +87 -0
  328. package/vendor/markit-ai/dist/converters/pptx.d.ts +8 -0
  329. package/vendor/markit-ai/dist/converters/pptx.js +289 -0
  330. package/vendor/markit-ai/dist/converters/rss.d.ts +11 -0
  331. package/vendor/markit-ai/dist/converters/rss.js +138 -0
  332. package/vendor/markit-ai/dist/converters/wikipedia.d.ts +6 -0
  333. package/vendor/markit-ai/dist/converters/wikipedia.js +33 -0
  334. package/vendor/markit-ai/dist/converters/xlsx.d.ts +8 -0
  335. package/vendor/markit-ai/dist/converters/xlsx.js +147 -0
  336. package/vendor/markit-ai/dist/converters/xml.d.ts +6 -0
  337. package/vendor/markit-ai/dist/converters/xml.js +18 -0
  338. package/vendor/markit-ai/dist/converters/yaml.d.ts +6 -0
  339. package/vendor/markit-ai/dist/converters/yaml.js +17 -0
  340. package/vendor/markit-ai/dist/converters/zip.d.ts +8 -0
  341. package/vendor/markit-ai/dist/converters/zip.js +57 -0
  342. package/vendor/markit-ai/dist/index.d.ts +30 -0
  343. package/vendor/markit-ai/dist/index.js +26 -0
  344. package/vendor/markit-ai/dist/main.d.ts +2 -0
  345. package/vendor/markit-ai/dist/main.js +191 -0
  346. package/vendor/markit-ai/dist/markit.d.ts +45 -0
  347. package/vendor/markit-ai/dist/markit.js +267 -0
  348. package/vendor/markit-ai/dist/plugins/api.d.ts +7 -0
  349. package/vendor/markit-ai/dist/plugins/api.js +42 -0
  350. package/vendor/markit-ai/dist/plugins/index.d.ts +4 -0
  351. package/vendor/markit-ai/dist/plugins/index.js +3 -0
  352. package/vendor/markit-ai/dist/plugins/installer.d.ts +25 -0
  353. package/vendor/markit-ai/dist/plugins/installer.js +176 -0
  354. package/vendor/markit-ai/dist/plugins/loader.d.ts +6 -0
  355. package/vendor/markit-ai/dist/plugins/loader.js +61 -0
  356. package/vendor/markit-ai/dist/plugins/types.d.ts +25 -0
  357. package/vendor/markit-ai/dist/plugins/types.js +1 -0
  358. package/vendor/markit-ai/dist/providers/anthropic.d.ts +2 -0
  359. package/vendor/markit-ai/dist/providers/anthropic.js +47 -0
  360. package/vendor/markit-ai/dist/providers/index.d.ts +21 -0
  361. package/vendor/markit-ai/dist/providers/index.js +56 -0
  362. package/vendor/markit-ai/dist/providers/openai.d.ts +2 -0
  363. package/vendor/markit-ai/dist/providers/openai.js +76 -0
  364. package/vendor/markit-ai/dist/providers/types.d.ts +26 -0
  365. package/vendor/markit-ai/dist/providers/types.js +1 -0
  366. package/vendor/markit-ai/dist/types.d.ts +36 -0
  367. package/vendor/markit-ai/dist/types.js +1 -0
  368. package/vendor/markit-ai/dist/utils/exit-codes.d.ts +4 -0
  369. package/vendor/markit-ai/dist/utils/exit-codes.js +4 -0
  370. package/vendor/markit-ai/dist/utils/output.d.ts +22 -0
  371. package/vendor/markit-ai/dist/utils/output.js +31 -0
  372. package/vendor/markit-ai/dist/utils/turndown.d.ts +8 -0
  373. package/vendor/markit-ai/dist/utils/turndown.js +70 -0
  374. package/vendor/markit-ai/package.json +74 -0
  375. package/vendor/markit-ai.patch +49 -0
@@ -52,6 +52,7 @@ import {
52
52
  type VerifiedSessionDeleteResult,
53
53
  type VerifiedSessionDeleteTarget,
54
54
  } from "../../session/session-storage";
55
+ import type { SessionWorkLease } from "../../session/session-work-lease";
55
56
  import type { SessionLifecycleMcpServer } from "../acp/mcp";
56
57
  import { SdkClient, SdkClientError } from "../client/client";
57
58
  import { BROKER_RUNTIME_CLOSE_CAPABILITY_FIELD } from "../host/control/runtime-gate";
@@ -63,6 +64,7 @@ import {
63
64
  resolveManagedSessionScope,
64
65
  } from "../session-directory";
65
66
  import {
67
+ lifecycleKnownSecrets,
66
68
  normalizeSdkStartupFailure,
67
69
  type SdkStartupFailure,
68
70
  type SdkStartupRollbackResult,
@@ -87,6 +89,7 @@ import {
87
89
  } from "./process-incarnation";
88
90
  import { resolveSdkInternalSpawnCommand, type SdkInternalSpawnCommand } from "./runtime";
89
91
  import {
92
+ DEAD_HOST_HEARTBEAT_SILENCE_MS,
90
93
  type IndexedSession,
91
94
  isSessionAuthorityEligible,
92
95
  resolveSessionLocator,
@@ -119,15 +122,308 @@ const MAX_EFFECT_MARKER_LENGTH = 128;
119
122
  const MAX_PROCESS_INCARNATION_LENGTH = 256;
120
123
  const DEAD_LIFECYCLE_MARKER_EXPIRY_MS = 60 * 60 * 1000;
121
124
  const READY_THEN_EXIT_MESSAGE = "became ready then exited before live admission";
125
+ /** Keep detached host stderr diagnostic-only and bounded on disk. */
126
+ const CHILD_STDERR_TAIL_BYTES = 512;
127
+ const LIFECYCLE_DIAGNOSTIC_MARKER = "[lifecycle-diagnostic-v1]";
128
+
129
+ type ChildStderrCapture = {
130
+ tail(): string;
131
+ exitCode(): number | null | undefined;
132
+ signalCode(): NodeJS.Signals | null | undefined;
133
+ flush(): Promise<void>;
134
+ cleanup(): Promise<void>;
135
+ };
136
+
137
+ type ChildStderrSink = {
138
+ path: string;
139
+ drainer: ChildProcess;
140
+ drainExited: Promise<void>;
141
+ parentReleased: boolean;
142
+ };
143
+
144
+ const CHILD_STDERR_LOG_NAME = /^lifecycle-spawn\.[A-Za-z0-9-]+\.log$/u;
145
+ const CHILD_STDERR_ORPHAN_EXPIRY_MS = 60 * 60 * 1000;
146
+
147
+ /**
148
+ * Detached hosts outlive the broker, so stderr must not use a broker-owned
149
+ * pipe. A detached drainer owns the read side and writes only the bounded tail
150
+ * to disk, keeping both the host and the artifact safe across broker restarts.
151
+ */
152
+ function openChildStderrSink(agentDir: string): ChildStderrSink | undefined {
153
+ // Bun on Windows cannot hand a pipe stream without an underlying fd to a
154
+ // child's stdio ("Passing a stream.Readable without an underlying file
155
+ // descriptor as stdio[2] is not yet implemented"), which would turn every
156
+ // launch into spawn_failed. Windows keeps stdio ignored; the stage/child
157
+ // diagnostics still apply, only the stderr tail reads as `none`.
158
+ if (process.platform === "win32") return undefined;
159
+ let logPath: string | undefined;
160
+ try {
161
+ const directory = path.join(agentDir, "sdk");
162
+ fsSync.mkdirSync(directory, { recursive: true, mode: 0o700 });
163
+ logPath = path.join(directory, `lifecycle-spawn.${randomUUID()}.log`);
164
+ const logDescriptor = fsSync.openSync(logPath, "w", 0o600);
165
+ fsSync.closeSync(logDescriptor);
166
+ const command = resolveSdkInternalSpawnCommand("stderr-drain-internal");
167
+ const drainer = spawn(
168
+ command.file,
169
+ [...command.args, "--path", logPath, "--max-bytes", String(CHILD_STDERR_TAIL_BYTES)],
170
+ {
171
+ ...(command.cwd === undefined ? {} : { cwd: command.cwd }),
172
+ detached: true,
173
+ stdio: ["pipe", "ignore", "ignore"],
174
+ env: command.env,
175
+ },
176
+ );
177
+ if (!drainer.stdin) throw new Error("lifecycle stderr drainer has no stdin");
178
+ const drainExit = Promise.withResolvers<void>();
179
+ const resolveDrainExit = (): void => drainExit.resolve();
180
+ drainer.once("exit", resolveDrainExit);
181
+ drainer.once("error", resolveDrainExit);
182
+ drainer.unref();
183
+ void reapOrphanedChildStderrLogs(agentDir).catch(() => undefined);
184
+ return { path: logPath, drainer, drainExited: drainExit.promise, parentReleased: false };
185
+ } catch {
186
+ if (logPath) {
187
+ try {
188
+ fsSync.unlinkSync(logPath);
189
+ } catch {}
190
+ }
191
+ return undefined;
192
+ }
193
+ }
194
+
195
+ function closeChildStderrSink(sink: ChildStderrSink | undefined): void {
196
+ if (!sink || sink.parentReleased) return;
197
+ sink.parentReleased = true;
198
+ // `end()` closes only the broker's duplicate write end after the child has
199
+ // inherited its own descriptor. `destroy()` can close the shared descriptor
200
+ // before a fast child starts writing under Bun's stream implementation.
201
+ sink.drainer.stdin?.end();
202
+ }
203
+
204
+ async function abortChildStderrSink(sink: ChildStderrSink | undefined): Promise<void> {
205
+ if (!sink) return;
206
+ closeChildStderrSink(sink);
207
+ if (sink.drainer.exitCode === null) sink.drainer.kill();
208
+ await Promise.race([sink.drainExited, Bun.sleep(100)]);
209
+ await fs.unlink(sink.path).catch(() => undefined);
210
+ }
211
+
212
+ function readChildStderrTail(logPath: string): string {
213
+ let descriptor: number | undefined;
214
+ try {
215
+ descriptor = fsSync.openSync(logPath, fsSync.constants.O_RDONLY);
216
+ const stat = fsSync.fstatSync(descriptor);
217
+ if (!stat.isFile() || stat.size <= 0) return "";
218
+ const length = Math.min(CHILD_STDERR_TAIL_BYTES, stat.size);
219
+ const buffer = Buffer.alloc(length);
220
+ fsSync.readSync(descriptor, buffer, 0, length, stat.size - length);
221
+ return buffer.toString("utf8");
222
+ } catch {
223
+ return "";
224
+ } finally {
225
+ if (descriptor !== undefined) {
226
+ try {
227
+ fsSync.closeSync(descriptor);
228
+ } catch {
229
+ // Best effort diagnostic read.
230
+ }
231
+ }
232
+ }
233
+ }
234
+
235
+ function captureChildStderr(child: ChildProcess, sink: ChildStderrSink | undefined): ChildStderrCapture {
236
+ let observedExitCode: number | null | undefined;
237
+ let observedSignalCode: NodeJS.Signals | null | undefined;
238
+ child.once("exit", (code, signal) => {
239
+ observedExitCode = code;
240
+ observedSignalCode = signal;
241
+ });
242
+ return {
243
+ tail: () => (sink ? readChildStderrTail(sink.path) : ""),
244
+ exitCode: () => observedExitCode,
245
+ signalCode: () => observedSignalCode,
246
+ flush: async () => {
247
+ if (
248
+ child.exitCode === null &&
249
+ child.signalCode === null &&
250
+ observedExitCode === undefined &&
251
+ observedSignalCode === undefined
252
+ )
253
+ return;
254
+ if (sink) await Promise.race([sink.drainExited, Bun.sleep(250)]);
255
+ },
256
+ cleanup: async () => {
257
+ if (!sink) return;
258
+ closeChildStderrSink(sink);
259
+ // The drainer writes through a descriptor it already holds, so unlinking
260
+ // here retires the artifact for successful and failed launches alike: any
261
+ // later host output lands on an unlinked inode and dies with the drainer.
262
+ await fs.unlink(sink.path).catch(() => undefined);
263
+ },
264
+ };
265
+ }
266
+
267
+ /** Remove lifecycle stderr artifacts abandoned by a broker or detached host crash. */
268
+ export async function reapOrphanedChildStderrLogs(
269
+ agentDir: string,
270
+ limit = BROKER_DEAD_REGISTRATION_SWEEP_LIMIT,
271
+ ): Promise<number> {
272
+ let directory: fsSync.Dir;
273
+ try {
274
+ directory = await fs.opendir(path.join(agentDir, "sdk"));
275
+ } catch {
276
+ return 0;
277
+ }
278
+ const cutoff = Date.now() - CHILD_STDERR_ORPHAN_EXPIRY_MS;
279
+ let inspected = 0;
280
+ let reaped = 0;
281
+ for await (const entry of directory) {
282
+ if (inspected >= Math.max(0, limit)) break;
283
+ inspected += 1;
284
+ if (!entry.isFile() || !CHILD_STDERR_LOG_NAME.test(entry.name)) continue;
285
+ try {
286
+ const target = path.join(agentDir, "sdk", entry.name);
287
+ const stat = await fs.stat(target);
288
+ if (stat.mtimeMs >= cutoff) continue;
289
+ await fs.unlink(target);
290
+ reaped += 1;
291
+ } catch {
292
+ // A concurrent drainer or another broker may own the artifact.
293
+ }
294
+ }
295
+ return reaped;
296
+ }
297
+
298
+ async function settleChildExitObservation(child: ChildProcess, stderr?: ChildStderrCapture): Promise<void> {
299
+ const exitCode = stderr?.exitCode();
300
+ const signalCode = stderr?.signalCode();
301
+ if (
302
+ child.exitCode !== null ||
303
+ child.signalCode !== null ||
304
+ (exitCode !== null && exitCode !== undefined) ||
305
+ (signalCode !== null && signalCode !== undefined)
306
+ )
307
+ return;
308
+ const exited = Promise.withResolvers<void>();
309
+ child.once("exit", () => exited.resolve());
310
+ await Promise.race([exited.promise, Bun.sleep(100)]);
311
+ }
312
+
313
+ type ChildLifecycleStatus = {
314
+ state: "alive" | "exited" | "uncertain";
315
+ pid?: number;
316
+ exitCode: number | null;
317
+ signalCode: NodeJS.Signals | null;
318
+ };
319
+
320
+ function childLifecycleStatus(
321
+ broker: Broker,
322
+ child: ChildProcess,
323
+ authority: EffectMarker | undefined,
324
+ stderrCapture?: ChildStderrCapture,
325
+ ): ChildLifecycleStatus {
326
+ const exitCode = child.exitCode ?? stderrCapture?.exitCode() ?? null;
327
+ const signalCode = child.signalCode ?? stderrCapture?.signalCode() ?? null;
328
+ if (exitCode !== null || signalCode !== null)
329
+ return { state: "exited", ...(child.pid === undefined ? {} : { pid: child.pid }), exitCode, signalCode };
330
+ if (authority) {
331
+ const observation = observeProcess(authority.pid, authority.incarnation, value =>
332
+ processIncarnationForBroker(broker, value),
333
+ );
334
+ return {
335
+ state: observation,
336
+ pid: authority.pid,
337
+ exitCode,
338
+ signalCode,
339
+ };
340
+ }
341
+ return {
342
+ state: "uncertain",
343
+ ...(child.pid === undefined ? {} : { pid: child.pid }),
344
+ exitCode,
345
+ signalCode,
346
+ };
347
+ }
348
+
349
+ /**
350
+ * The drainer keeps only the last `CHILD_STDERR_TAIL_BYTES`, so a credential
351
+ * that straddles the cutoff (or exceeds the tail) survives as a bare suffix that
352
+ * `sanitizeSdkStartupMessage` can no longer match exactly. Any tail prefix that
353
+ * is a suffix of a known secret is therefore redacted before sanitization.
354
+ */
355
+ function redactTruncatedSecretPrefix(tail: string, knownSecrets: Iterable<unknown>): string {
356
+ if (!tail) return tail;
357
+ const leadingReplacement = tail.startsWith("\uFFFD") ? 1 : 0;
358
+ const body = tail.slice(leadingReplacement);
359
+ // A one- or two-character overlap is as likely to be coincidence as a cut
360
+ // credential and reveals nothing usable; keep the diagnostic readable there.
361
+ const minimumOverlap = 3;
362
+ let longestMatch = 0;
363
+ for (const secret of knownSecrets) {
364
+ if (typeof secret !== "string" || secret.length < minimumOverlap) continue;
365
+ const max = Math.min(secret.length, body.length);
366
+ for (let length = max; length > Math.max(longestMatch, minimumOverlap - 1); length--) {
367
+ if (body.startsWith(secret.slice(secret.length - length))) {
368
+ longestMatch = length;
369
+ break;
370
+ }
371
+ }
372
+ }
373
+ return longestMatch === 0 ? tail : `[redacted-secret]${body.slice(longestMatch)}`;
374
+ }
375
+
376
+ function lifecycleChildDiagnostic(
377
+ stage: string,
378
+ waitingFor: string,
379
+ status: ChildLifecycleStatus,
380
+ stderr: ChildStderrCapture | undefined,
381
+ cause?: string,
382
+ knownSecrets: Iterable<unknown> = lifecycleKnownSecrets(),
383
+ ): string {
384
+ const parts = [LIFECYCLE_DIAGNOSTIC_MARKER, `stage=${stage}`, `waiting_for=${waitingFor}`, `child=${status.state}`];
385
+ if (status.pid !== undefined) parts.push(`pid=${status.pid}`);
386
+ if (status.state === "exited") {
387
+ parts.push(`exit=${status.exitCode ?? "null"}`, `signal=${status.signalCode ?? "none"}`);
388
+ } else {
389
+ parts.push("exit=not_observed", "signal=not_observed");
390
+ }
391
+ const rawStderr = redactTruncatedSecretPrefix(stderr?.tail() ?? "", knownSecrets).trim();
392
+ if (rawStderr) parts.push(`stderr=${JSON.stringify(sanitizeSdkStartupMessage(rawStderr, knownSecrets))}`);
393
+ else parts.push("stderr=none");
394
+ if (cause) parts.push(`cause=${sanitizeSdkStartupMessage(cause, knownSecrets)}`);
395
+ return parts.join(" ");
396
+ }
397
+
398
+ function validateLifecycleExecutable(file: string): void {
399
+ // Bun's Node-compatible spawn can emit an ENOENT error before the returned
400
+ // ChildProcess has an opportunity to receive an `error` listener. Validate
401
+ // absolute launch paths first so a missing or non-executable runtime becomes a
402
+ // normal broker response instead of an uncaught broker exception. Bare names
403
+ // still resolve through PATH at spawn time.
404
+ if (!path.isAbsolute(file)) return;
405
+ const stat = fsSync.statSync(file);
406
+ if (!stat.isFile()) throw new Error(`Lifecycle runtime executable is not a regular file: ${file}`);
407
+ fsSync.accessSync(file, fsSync.constants.X_OK);
408
+ }
409
+
410
+ function lifecycleChildExitDiagnostic(child?: ChildProcess): string | undefined {
411
+ if (!child) return undefined;
412
+ const parts: string[] = [];
413
+ if (child.exitCode !== null && child.exitCode !== 0) parts.push(`exit=${child.exitCode}`);
414
+ if (child.signalCode) parts.push(`signal=${child.signalCode}`);
415
+ return parts.length > 0 ? parts.join(", ") : undefined;
416
+ }
417
+
418
+ export function lifecycleFailureMessage(message: string, child?: ChildProcess): string {
419
+ const details = [lifecycleChildExitDiagnostic(child)].filter((value): value is string => value !== undefined);
420
+ return details.length > 0 ? `${message} (${details.join("; ")})` : message;
421
+ }
122
422
 
123
423
  function readyThenExitedResponse(id: string, child?: ChildProcess): BrokerResponse {
124
424
  const parts = [`Session ${id} ${READY_THEN_EXIT_MESSAGE}.`];
125
- if (child && child.exitCode !== null) parts.push(`exit=${child.exitCode}`);
126
- if (child?.signalCode) parts.push(`signal=${child.signalCode}`);
127
- // Host stderr is deliberately excluded: the detached host receives launch
128
- // configuration and inherited credentials, and no pattern-based redaction can
129
- // prove arbitrary child output free of that material; its stderr is discarded
130
- // by the OS and never captured (#4712 review).
425
+ const diagnostic = lifecycleFailureMessage("", child).trim();
426
+ if (diagnostic) parts.push(diagnostic);
131
427
  return fail("ready_then_exited", parts.join(" "));
132
428
  }
133
429
 
@@ -135,20 +431,34 @@ const STARTUP_CLEANUP_UNCERTAIN_MESSAGE =
135
431
  "Lifecycle startup cleanup could not be proven; retained artifacts require reconciliation.";
136
432
 
137
433
  export function terminalUncertainStartupMessage(response: BrokerResponse): string {
138
- if (response.ok || response.error.code !== "spawn_failed") return STARTUP_CLEANUP_UNCERTAIN_MESSAGE;
434
+ if (response.ok) return STARTUP_CLEANUP_UNCERTAIN_MESSAGE;
435
+ // Ordinary spawn errors may contain executable paths; keep the historical
436
+ // path-free fallback for those. Lifecycle diagnostics are deliberately
437
+ // recognizable and already bounded/redacted, so preserve their stage/cause
438
+ // fields even when cleanup proof is the part that failed.
439
+ const original = response.error.message.includes(LIFECYCLE_DIAGNOSTIC_MARKER)
440
+ ? sanitizeSdkStartupMessage(response.error.message)
441
+ : "SDK internal process could not be started.";
139
442
  logger.warn("sdk broker retained a sanitized launch failure after uncertain startup cleanup", {
140
- message: sanitizeSdkStartupMessage(response.error.message),
443
+ message: original,
141
444
  });
142
- return `${STARTUP_CLEANUP_UNCERTAIN_MESSAGE} Original launch failure: SDK internal process could not be started.`;
445
+ return `${STARTUP_CLEANUP_UNCERTAIN_MESSAGE} Original launch failure: ${original}`;
143
446
  }
144
447
 
145
448
  export async function waitForChildSpawn(
146
- spawned: Pick<ChildProcess, "off" | "on" | "once">,
449
+ spawned: Pick<ChildProcess, "off" | "on" | "once" | "pid">,
147
450
  onPostSpawnError: (error: Error) => void = error =>
148
451
  logger.warn("sdk session child emitted an error after successful spawn", {
149
452
  message: sanitizeSdkStartupMessage(error),
150
453
  }),
151
454
  ): Promise<void> {
455
+ // Bun may publish the `spawn` event synchronously for native executables.
456
+ // A populated PID is already the same ownership fact the event conveys, so
457
+ // do not wait forever for an event that was emitted before this listener ran.
458
+ if (spawned.pid !== undefined) {
459
+ spawned.on("error", onPostSpawnError);
460
+ return;
461
+ }
152
462
  const spawnOutcome = Promise.withResolvers<void>();
153
463
  const onSpawn = () => {
154
464
  spawned.off("error", onError);
@@ -618,6 +928,33 @@ type SessionLaunch = {
618
928
  worktreePlan?: GjcLaunchWorktreePlan;
619
929
  };
620
930
 
931
+ const CREDENTIAL_NAME_PATTERN = /(?:token|secret|password|credential|api[_-]?key|auth|cookie)/iu;
932
+
933
+ function lifecycleLaunchKnownSecrets(launch: SessionLaunch): string[] {
934
+ const secrets = [
935
+ ...lifecycleKnownSecrets(),
936
+ ...(launch.coordinatorSidecarSigningKey ? [launch.coordinatorSidecarSigningKey] : []),
937
+ ...(launch.mcpServers ?? []).flatMap(server => {
938
+ if ("url" in server) {
939
+ // Only credential-bearing headers are secrets. Treating every header
940
+ // value (`X-Tenant: a`) as one would redact ordinary characters out of
941
+ // the diagnostic this path exists to expose.
942
+ return Object.entries(server.headers ?? {}).flatMap(([name, value]) => {
943
+ const scheme = /^(?:Bearer|Basic)\s+/iu;
944
+ if (!CREDENTIAL_NAME_PATTERN.test(name) && !scheme.test(value)) return [];
945
+ const bare = value.replace(scheme, "");
946
+ return bare === value ? [value] : [value, bare];
947
+ });
948
+ }
949
+ if (!server.env) return [];
950
+ return Object.entries(server.env)
951
+ .filter(([name, value]) => value && CREDENTIAL_NAME_PATTERN.test(name))
952
+ .map(([, value]) => value);
953
+ }),
954
+ ];
955
+ return [...new Set(secrets.filter(secret => typeof secret === "string" && secret.length > 0))];
956
+ }
957
+
621
958
  type CleanupEvidence = BrokerCleanupEvidence;
622
959
  type CleanupIdentity = {
623
960
  dev: bigint;
@@ -1141,7 +1478,7 @@ type ReadinessResult =
1141
1478
  | { kind: "ready_then_exited" }
1142
1479
  | { kind: "ready_probe_failed"; probe: ReadyAuthorityProbe }
1143
1480
  | { kind: "child_exited" }
1144
- | { kind: "timeout" };
1481
+ | { kind: "timeout"; stage: "readiness"; waitingFor: string };
1145
1482
  type BrokerIndex = Pick<Broker, "index">;
1146
1483
  const processIncarnationReadersForTest = new WeakMap<BrokerIndex, (pid: number) => string | undefined>();
1147
1484
 
@@ -2449,6 +2786,43 @@ async function executeUncertainRetirement(
2449
2786
  return retirementProof(retirementIdentity, receipt.indexSeq);
2450
2787
  }
2451
2788
 
2789
+ /**
2790
+ * Fence a coordinator's dead-host cleanup against the broker's current index
2791
+ * authority. This is deliberately separate from ordinary `session.close`: the
2792
+ * runtime endpoint is already known to be unavailable, so the broker must make
2793
+ * the exact generation terminal under its index lock before coordinator state is
2794
+ * removed. A fresh heartbeat, successor, ambiguous row, or terminal-uncertain
2795
+ * authority refuses the fence.
2796
+ */
2797
+ async function executeStaleSessionRetirement(broker: Broker, input: Input): Promise<BrokerResponse> {
2798
+ const id = sessionId(input);
2799
+ if (!id) return fail("invalid_input", "sessionId is required.");
2800
+ if (!isCanonicalSessionId(id)) return fail("invalid_input", "sessionId must be a canonical safe identifier.");
2801
+ const requestedAuthority = requestedCloseAuthority(input);
2802
+ if ("error" in requestedAuthority) return requestedAuthority.error;
2803
+ if (!requestedAuthority.authority)
2804
+ return fail("invalid_input", "stale session retirement requires endpoint authority.");
2805
+ const cwd = lifecycleCwd(input);
2806
+ if (!cwd) return fail("invalid_input", "stale session retirement requires cwd.");
2807
+ const result = await broker.index.retireStaleIfCurrent({
2808
+ sessionId: id,
2809
+ workspace: cwd,
2810
+ endpointGeneration: requestedAuthority.authority.endpointGeneration,
2811
+ endpointIncarnation: requestedAuthority.authority.endpointIncarnation,
2812
+ });
2813
+ if (result === "not_found") return fail("not_found", "session is not indexed");
2814
+ if (result === "stale") return fail("endpoint_stale", "session endpoint authority changed or is unresolved.");
2815
+ return {
2816
+ ok: true,
2817
+ result: {
2818
+ sessionId: id,
2819
+ retired: true,
2820
+ ...(result === "already_retired" ? { alreadyRetired: true } : {}),
2821
+ heartbeatSilenceMs: DEAD_HOST_HEARTBEAT_SILENCE_MS,
2822
+ },
2823
+ };
2824
+ }
2825
+
2452
2826
  function validateLifecycleCleanupFile(root: string, id: string, file: LifecycleCleanupFile): boolean {
2453
2827
  const directory = path.join(path.resolve(root), "sdk");
2454
2828
  const original = path.resolve(file.path);
@@ -3722,6 +4096,52 @@ async function terminateSpawnedChild(
3722
4096
  failure.artifact.rollback.brokerRegistrationReleased;
3723
4097
  if (!rollbackComplete) {
3724
4098
  if (!readyThenExitToleranceEnabled()) {
4099
+ // A child that exited with a non-zero status before publishing any
4100
+ // lifecycle receipt has no durable rollback to wait for. Prove the
4101
+ // owned artifacts are gone and release its registration, but retain
4102
+ // terminal uncertainty for timeouts, clean exits, and child-authored
4103
+ // startup receipts whose rollback is incomplete.
4104
+ if (failure || child.exitCode === null || child.exitCode === 0) return failClosed();
4105
+ if (!lifecycleProofWithinDeadline(proofBudget)) return failClosed();
4106
+ const publishedReady = (await probePublishedReadyAuthority(root, id, expected)).kind === "matched";
4107
+ if (publishedReady) return failClosed();
4108
+ const artifactsRemoved = await removeOwnedLifecycleArtifacts(
4109
+ root,
4110
+ id,
4111
+ expected,
4112
+ undefined,
4113
+ undefined,
4114
+ proofBudget,
4115
+ );
4116
+ if (!lifecycleProofWithinDeadline(proofBudget)) return failClosed();
4117
+ await broker.index.refresh();
4118
+ if (!lifecycleProofWithinDeadline(proofBudget)) return failClosed();
4119
+ const registered = broker.index
4120
+ .listSessions()
4121
+ .sessions.find(session => session.sessionId === id && session.pid === pid);
4122
+ let registrationReleased =
4123
+ !broker.index.hasHostRegistrationForLifecycle(id, pid, expected.effectMarker) ||
4124
+ registered?.terminal === true ||
4125
+ registered?.terminalUncertain === true;
4126
+ if (registered && !registrationReleased) {
4127
+ registrationReleased = await broker.index.unregisterIfCurrent(registered);
4128
+ if (!lifecycleProofWithinDeadline(proofBudget)) return failClosed();
4129
+ await broker.index.refresh();
4130
+ registrationReleased =
4131
+ registrationReleased || !broker.index.hasHostRegistrationForLifecycle(id, pid, expected.effectMarker);
4132
+ }
4133
+ const stillExited =
4134
+ observeProcess(pid, expected.incarnation, value => processIncarnationForBroker(broker, value)) ===
4135
+ "exited";
4136
+ const endpointGone = await endpointRemoved(root, id);
4137
+ if (
4138
+ stillExited &&
4139
+ artifactsRemoved &&
4140
+ endpointGone &&
4141
+ registrationReleased &&
4142
+ lifecycleProofWithinDeadline(proofBudget)
4143
+ )
4144
+ return true;
3725
4145
  return failClosed();
3726
4146
  }
3727
4147
  if (!lifecycleProofWithinDeadline(proofBudget)) return failClosed();
@@ -4216,6 +4636,7 @@ async function waitForReady(
4216
4636
  deadline: number,
4217
4637
  expected: EffectMarker,
4218
4638
  timing: LifecycleTiming,
4639
+ child?: ChildProcess,
4219
4640
  signal: "session_ready" | typeof SESSION_PREPARED_EVENT = "session_ready",
4220
4641
  ): Promise<ReadinessResult> {
4221
4642
  const classifyExitedAfterReady = async (): Promise<ReadinessResult> => {
@@ -4238,10 +4659,11 @@ async function waitForReady(
4238
4659
  }
4239
4660
  return { kind: "startup_failed", failure: startupFailure };
4240
4661
  }
4241
- if (
4242
- observeProcess(expected.pid, expected.incarnation, value => processIncarnationForBroker(broker, value)) ===
4243
- "exited"
4244
- ) {
4662
+ const childExited = child !== undefined && (child.exitCode !== null || child.signalCode !== null);
4663
+ const processObservation = observeProcess(expected.pid, expected.incarnation, value =>
4664
+ processIncarnationForBroker(broker, value),
4665
+ );
4666
+ if (childExited || processObservation === "exited") {
4245
4667
  const finalStartupFailure = await readSessionLifecycleFailure(root, id, expected);
4246
4668
  if (finalStartupFailure) {
4247
4669
  const afterReady = await classifyExitedAfterReady();
@@ -4306,7 +4728,7 @@ async function waitForReady(
4306
4728
  const remaining = deadline - timing.now();
4307
4729
  if (remaining > 0) await timing.sleep(Math.min(POLL_MS, remaining));
4308
4730
  }
4309
- return { kind: "timeout" };
4731
+ return { kind: "timeout", stage: "readiness", waitingFor: signal };
4310
4732
  }
4311
4733
 
4312
4734
  function worktreeIntent(plan: GjcLaunchWorktreePlan | undefined): LifecycleWorktreeIntent | undefined {
@@ -5091,8 +5513,20 @@ async function executeLifecycleResponse(
5091
5513
  return fail("invalid_input", "sourceSessionId must be a canonical safe identifier.");
5092
5514
  if (operation === "session.create" || operation === "session.fork" || operation === "session.resume") {
5093
5515
  await broker.index.refresh();
5094
- await broker.heartbeatSessions();
5095
- await broker.index.refresh();
5516
+ if (operation === "session.create") {
5517
+ // Creation has no existing session authority to reconcile. Keep the
5518
+ // broker's periodic/startup heartbeat checkpoint as the reaping owner,
5519
+ // but do not make a new session wait behind an O(hosts) process-table
5520
+ // pass on the shared agent directory.
5521
+ void broker.heartbeatSessions().catch(error => {
5522
+ logger.warn(`sdk broker: deferred create heartbeat checkpoint failed: ${String(error)}`);
5523
+ });
5524
+ } else {
5525
+ await broker.heartbeatSessions();
5526
+ // The heartbeat mutates the durable index; resume/fork must re-read its
5527
+ // authority before validating the source session below.
5528
+ await broker.index.refresh();
5529
+ }
5096
5530
  if (operation === "session.resume") {
5097
5531
  const requestedSessionId = sessionId(input);
5098
5532
  const authority = requestedSessionId
@@ -5420,20 +5854,26 @@ async function executeLifecycleResponse(
5420
5854
  ...(launch.coordinatorSessionBranch ? { coordinatorSessionBranch: launch.coordinatorSessionBranch } : {}),
5421
5855
  };
5422
5856
  let child: ChildProcess | undefined;
5857
+ let childStderr: ChildStderrCapture | undefined;
5423
5858
  let spawnedAuthority: EffectMarker | undefined;
5424
5859
  let childSpawned = false;
5860
+ const knownSecrets = lifecycleLaunchKnownSecrets(launch);
5861
+ const childStderrSink = openChildStderrSink(broker.settings.agentDir);
5862
+ // Runs once every diagnostic that reads the stderr tail has been composed.
5863
+ const cleanupChildStderr = async (): Promise<void> => {
5864
+ if (childStderr) await childStderr.cleanup();
5865
+ else await abortChildStderrSink(childStderrSink);
5866
+ };
5425
5867
  try {
5426
5868
  const authorizedSpawn = broker.runSynchronousEffectWithFreshPublicationAuthority(() => {
5427
5869
  const cmd = command(broker);
5870
+ validateLifecycleExecutable(cmd.file);
5428
5871
  return spawn(cmd.file, cmd.args, {
5429
5872
  cwd: launch.cwd,
5430
5873
  detached: true,
5431
- // stdio stays "ignore": `unref()` does not detach an active
5432
- // stdio handle, so a parent-owned stderr pipe would keep the
5433
- // broker process alive for the host's whole lifetime (retention
5434
- // through restart/shutdown). Host stderr is therefore discarded
5435
- // by the OS rather than captured (#4712 review).
5436
- stdio: "ignore",
5874
+ // The detached drainer owns the read side, so the host's stderr remains
5875
+ // usable after this broker exits while the artifact stays bounded.
5876
+ stdio: ["ignore", "ignore", childStderrSink?.drainer.stdin ?? "ignore"],
5437
5877
  env: {
5438
5878
  // Master capability is process-local to direct Bash children and must
5439
5879
  // never cross a broker lifecycle launch boundary.
@@ -5479,6 +5919,7 @@ async function executeLifecycleResponse(
5479
5919
  });
5480
5920
  });
5481
5921
  if (!authorizedSpawn.authorized) {
5922
+ await cleanupChildStderr();
5482
5923
  return fail(
5483
5924
  "startup_admission_refused",
5484
5925
  "SDK host startup was refused because the broker no longer owns the session root.",
@@ -5486,6 +5927,8 @@ async function executeLifecycleResponse(
5486
5927
  }
5487
5928
  const spawned = authorizedSpawn.value;
5488
5929
  child = spawned;
5930
+ childStderr = captureChildStderr(spawned, childStderrSink);
5931
+ closeChildStderrSink(childStderrSink);
5489
5932
  await waitForChildSpawn(spawned);
5490
5933
  childSpawned = true;
5491
5934
  const pid = spawned.pid;
@@ -5499,6 +5942,7 @@ async function executeLifecycleResponse(
5499
5942
  await writeEffectMarker(launch.root, launch.id, spawnedAuthority);
5500
5943
  spawned.unref();
5501
5944
  } catch (error) {
5945
+ closeChildStderrSink(childStderrSink);
5502
5946
  const terminated =
5503
5947
  child && childSpawned
5504
5948
  ? await terminateSpawnedChild(
@@ -5513,139 +5957,231 @@ async function executeLifecycleResponse(
5513
5957
  )
5514
5958
  : true;
5515
5959
 
5516
- return terminated
5517
- ? fail("spawn_failed", `Unable to spawn session: ${error instanceof Error ? error.message : String(error)}`)
5960
+ const response = terminated
5961
+ ? fail(
5962
+ "spawn_failed",
5963
+ lifecycleFailureMessage(
5964
+ `Unable to spawn session: ${sanitizeSdkStartupMessage(error, knownSecrets)}`,
5965
+ child,
5966
+ ),
5967
+ )
5518
5968
  : fail(
5519
5969
  "terminal_uncertain",
5520
- `Unable to establish spawned-session ownership and could not prove the child dead: ${error instanceof Error ? error.message : String(error)}`,
5970
+ lifecycleFailureMessage(
5971
+ `Unable to establish spawned-session ownership and could not prove the child dead: ${sanitizeSdkStartupMessage(error, knownSecrets)}`,
5972
+ child,
5973
+ ),
5521
5974
  );
5975
+ await cleanupChildStderr();
5976
+ return response;
5522
5977
  }
5523
- if (!child || !spawnedAuthority) {
5524
- return fail("spawn_failed", "Unable to retain the spawned session process identity.");
5525
- }
5526
- await broker.ledger.transition(identity, "awaiting_ready", { intendedSessionId: launch.id, effectMarker });
5527
- const readiness = await waitForReady(
5528
- broker,
5529
- launch.id,
5530
- launch.root,
5531
- readinessDeadline,
5532
- spawnedAuthority,
5533
- timing,
5534
- launch.readiness === "deferred" ? SESSION_PREPARED_EVENT : "session_ready",
5535
- );
5536
-
5537
- if (readiness.kind !== "ready") {
5538
- const terminated = await terminateSpawnedChild(
5539
- child,
5978
+ try {
5979
+ if (!child || !spawnedAuthority) {
5980
+ return fail("spawn_failed", "Unable to retain the spawned session process identity.");
5981
+ }
5982
+ await broker.ledger.transition(identity, "awaiting_ready", { intendedSessionId: launch.id, effectMarker });
5983
+ const readiness = await waitForReady(
5540
5984
  broker,
5541
5985
  launch.id,
5542
5986
  launch.root,
5543
- lifecycleDeadline,
5544
- terminationStartDeadline,
5987
+ readinessDeadline,
5545
5988
  spawnedAuthority,
5546
5989
  timing,
5990
+ child,
5991
+ launch.readiness === "deferred" ? SESSION_PREPARED_EVENT : "session_ready",
5547
5992
  );
5548
5993
 
5549
- if (!terminated)
5550
- return readiness.kind === "ready_probe_failed"
5551
- ? fail(
5552
- "endpoint_unreadable",
5553
- `Session ${launch.id} exited, and its readiness could not be determined: ${describeReadyProbeFailure(readiness.probe)}; cleanup could not be proven.`,
5554
- )
5555
- : fail(
5556
- "terminal_uncertain",
5557
- `Session ${launch.id} did not become ready and its spawned process could not be verified dead.`,
5558
- );
5559
- // Host stderr never reaches caller-visible error strings: the detached
5560
- // host handles launch configuration and inherited credentials, so its
5561
- // output cannot be proven free of secret material — it is discarded at
5562
- // the OS level (stdio "ignore"), never captured (#4712 review).
5563
- return readiness.kind === "startup_failed"
5564
- ? fail(
5565
- readiness.failure.code ?? "spawn_failed",
5566
- readiness.failure.message,
5567
- undefined,
5568
- readiness.failure.details,
5569
- )
5570
- : readiness.kind === "ready_then_exited"
5571
- ? readyThenExitedResponse(launch.id, child)
5572
- : readiness.kind === "ready_probe_failed"
5994
+ if (readiness.kind !== "ready") {
5995
+ if (readiness.kind !== "timeout") await settleChildExitObservation(child, childStderr);
5996
+ if (readiness.kind !== "timeout") await childStderr?.flush();
5997
+ const statusAtFailure = childLifecycleStatus(broker, child, spawnedAuthority, childStderr);
5998
+ const diagnostic =
5999
+ readiness.kind === "startup_failed"
6000
+ ? lifecycleChildDiagnostic(
6001
+ readiness.failure.phase,
6002
+ "startup completion",
6003
+ statusAtFailure,
6004
+ childStderr,
6005
+ readiness.failure.message,
6006
+ knownSecrets,
6007
+ )
6008
+ : readiness.kind === "timeout"
6009
+ ? lifecycleChildDiagnostic(
6010
+ readiness.stage,
6011
+ readiness.waitingFor,
6012
+ statusAtFailure,
6013
+ childStderr,
6014
+ undefined,
6015
+ knownSecrets,
6016
+ )
6017
+ : readiness.kind === "ready_probe_failed"
6018
+ ? lifecycleChildDiagnostic(
6019
+ "readiness",
6020
+ "session_ready",
6021
+ statusAtFailure,
6022
+ childStderr,
6023
+ undefined,
6024
+ knownSecrets,
6025
+ )
6026
+ : readiness.kind === "child_exited"
6027
+ ? lifecycleChildDiagnostic(
6028
+ "readiness",
6029
+ "session_ready",
6030
+ statusAtFailure,
6031
+ childStderr,
6032
+ undefined,
6033
+ knownSecrets,
6034
+ )
6035
+ : undefined;
6036
+ const terminated = await terminateSpawnedChild(
6037
+ child,
6038
+ broker,
6039
+ launch.id,
6040
+ launch.root,
6041
+ lifecycleDeadline,
6042
+ terminationStartDeadline,
6043
+ spawnedAuthority,
6044
+ timing,
6045
+ );
6046
+
6047
+ if (!terminated)
6048
+ return readiness.kind === "ready_probe_failed"
5573
6049
  ? fail(
5574
6050
  "endpoint_unreadable",
5575
- `Session ${launch.id} exited, and its readiness could not be determined: ${describeReadyProbeFailure(readiness.probe)}.`,
6051
+ `Session ${launch.id} exited, and its readiness could not be determined: ${describeReadyProbeFailure(readiness.probe)}; cleanup could not be proven. ${diagnostic ?? "stage=readiness waiting_for=session_ready child=uncertain exit=not_observed signal=not_observed stderr=none"}`,
5576
6052
  )
5577
- : readiness.kind === "child_exited"
5578
- ? fail("spawn_failed", `Session ${launch.id} exited before registering readiness.`)
5579
- : fail(
5580
- "readiness_timeout",
5581
- `Session ${launch.id} did not register an endpoint before the readiness timeout.`,
5582
- );
5583
- }
5584
- await reconcileReadyScope(broker, launch.id, launch.cwd, launch.root, spawnedAuthority);
5585
- const verified = await currentReadyAuthority(broker, launch.id, launch.root, spawnedAuthority);
5586
- if (!verified || !sameReadyAuthority(readiness.authority, verified)) {
5587
- const exited =
5588
- child.exitCode !== null ||
5589
- observeProcess(spawnedAuthority.pid, spawnedAuthority.incarnation, value =>
5590
- processIncarnationForBroker(broker, value),
5591
- ) === "exited";
5592
- const terminated = await terminateSpawnedChild(
5593
- child,
5594
- broker,
5595
- launch.id,
5596
- launch.root,
5597
- lifecycleDeadline,
5598
- terminationStartDeadline,
5599
- spawnedAuthority,
5600
- timing,
5601
- );
5602
- if (exited && readyThenExitToleranceEnabled())
6053
+ : fail(
6054
+ "terminal_uncertain",
6055
+ `Session ${launch.id} did not become ready and its spawned process could not be verified dead. ${diagnostic ?? "stage=readiness waiting_for=session_ready child=uncertain exit=not_observed signal=not_observed stderr=none"}`,
6056
+ );
6057
+ return readiness.kind === "startup_failed"
6058
+ ? fail(
6059
+ readiness.failure.code ?? "spawn_failed",
6060
+ // The child-side normalizer only knows process-scoped secrets; launch-scoped
6061
+ // ones (sidecar key, MCP headers) must be redacted here before the message
6062
+ // leaves the broker.
6063
+ `${sanitizeSdkStartupMessage(readiness.failure.message, knownSecrets)} ${diagnostic ?? "stage=startup waiting_for=startup completion child=uncertain exit=not_observed signal=not_observed stderr=none"}`,
6064
+ undefined,
6065
+ readiness.failure.details,
6066
+ )
6067
+ : readiness.kind === "ready_then_exited"
6068
+ ? readyThenExitedResponse(launch.id, child)
6069
+ : readiness.kind === "ready_probe_failed"
6070
+ ? fail(
6071
+ "endpoint_unreadable",
6072
+ `Session ${launch.id} exited, and its readiness could not be determined: ${describeReadyProbeFailure(readiness.probe)} ${diagnostic}.`,
6073
+ )
6074
+ : readiness.kind === "child_exited"
6075
+ ? fail(
6076
+ "spawn_failed",
6077
+ `Session ${launch.id} exited before registering readiness. ${diagnostic}.`,
6078
+ )
6079
+ : fail(
6080
+ "readiness_timeout",
6081
+ `Session ${launch.id} did not register an endpoint before the readiness timeout. ${diagnostic}.`,
6082
+ );
6083
+ }
6084
+ await reconcileReadyScope(broker, launch.id, launch.cwd, launch.root, spawnedAuthority);
6085
+ const verified = await currentReadyAuthority(broker, launch.id, launch.root, spawnedAuthority);
6086
+ if (!verified || !sameReadyAuthority(readiness.authority, verified)) {
6087
+ const exited =
6088
+ child.exitCode !== null ||
6089
+ observeProcess(spawnedAuthority.pid, spawnedAuthority.incarnation, value =>
6090
+ processIncarnationForBroker(broker, value),
6091
+ ) === "exited";
6092
+ const terminated = await terminateSpawnedChild(
6093
+ child,
6094
+ broker,
6095
+ launch.id,
6096
+ launch.root,
6097
+ lifecycleDeadline,
6098
+ terminationStartDeadline,
6099
+ spawnedAuthority,
6100
+ timing,
6101
+ );
6102
+ if (exited && readyThenExitToleranceEnabled())
6103
+ return terminated
6104
+ ? readyThenExitedResponse(launch.id, child)
6105
+ : fail(
6106
+ "terminal_uncertain",
6107
+ `Session ${launch.id} ${READY_THEN_EXIT_MESSAGE} and cleanup could not be proven.`,
6108
+ );
5603
6109
  return terminated
5604
- ? readyThenExitedResponse(launch.id, child)
6110
+ ? fail("endpoint_stale", "Session endpoint changed while lifecycle readiness was being verified.")
5605
6111
  : fail(
5606
6112
  "terminal_uncertain",
5607
- `Session ${launch.id} ${READY_THEN_EXIT_MESSAGE} and cleanup could not be proven.`,
6113
+ "Session readiness authority changed and its spawned process could not be verified dead.",
5608
6114
  );
6115
+ }
6116
+ if (!spawnedAuthority) {
6117
+ return fail("endpoint_stale", "Session endpoint authority is unavailable.");
6118
+ }
6119
+ const replayIncarnation = endpointIncarnation(
6120
+ {
6121
+ endpointGeneration: verified.endpointGeneration,
6122
+ endpointMtimeMs: verified.endpointMtimeMs,
6123
+ pid: spawnedAuthority.pid,
6124
+ ...(verified.endpointFileId === undefined ? {} : { endpointFileId: verified.endpointFileId }),
6125
+ },
6126
+ launch.id,
6127
+ );
6128
+ if (!replayIncarnation) {
6129
+ return fail("endpoint_stale", "Session endpoint incarnation is unavailable.");
6130
+ }
6131
+ return {
6132
+ ok: true,
6133
+ result: {
6134
+ sessionId: launch.id,
6135
+ cwd: launch.cwd,
6136
+ endpointGeneration: verified.endpointGeneration,
6137
+ endpointIncarnation: replayIncarnation,
6138
+ pid: verified.endpoint.pid,
6139
+ endpointMtimeMs: verified.endpointMtimeMs,
6140
+ endpoint: verified.endpoint,
6141
+ // Public, ledger-replayable evidence of the bootstrap authority that
6142
+ // reached this runtime. The private key never enters the response.
6143
+ ...(launch.coordinatorSidecarKeyId ? { coordinatorSidecarKeyId: launch.coordinatorSidecarKeyId } : {}),
6144
+ ...(launch.readiness === "deferred" ? { readiness: "prepared" as const } : {}),
6145
+ ...(worktreeReceipt ? { worktree: worktreeReceipt } : {}),
6146
+ },
6147
+ };
6148
+ } catch (error) {
6149
+ let terminated = false;
6150
+ if (child && childSpawned) {
6151
+ try {
6152
+ terminated = await terminateSpawnedChild(
6153
+ child,
6154
+ broker,
6155
+ launch.id,
6156
+ launch.root,
6157
+ lifecycleDeadline,
6158
+ terminationStartDeadline,
6159
+ spawnedAuthority,
6160
+ timing,
6161
+ );
6162
+ } catch {
6163
+ terminated = false;
6164
+ }
6165
+ }
5609
6166
  return terminated
5610
- ? fail("endpoint_stale", "Session endpoint changed while lifecycle readiness was being verified.")
6167
+ ? fail(
6168
+ "spawn_failed",
6169
+ `Unable to complete lifecycle startup: ${error instanceof Error ? error.message : String(error)}`,
6170
+ )
5611
6171
  : fail(
5612
6172
  "terminal_uncertain",
5613
- "Session readiness authority changed and its spawned process could not be verified dead.",
6173
+ `Unable to complete lifecycle startup and could not prove the child dead: ${error instanceof Error ? error.message : String(error)}`,
5614
6174
  );
6175
+ } finally {
6176
+ await cleanupChildStderr();
5615
6177
  }
5616
- if (!spawnedAuthority) return fail("endpoint_stale", "Session endpoint authority is unavailable.");
5617
- const replayIncarnation = endpointIncarnation(
5618
- {
5619
- endpointGeneration: verified.endpointGeneration,
5620
- endpointMtimeMs: verified.endpointMtimeMs,
5621
- pid: spawnedAuthority.pid,
5622
- ...(verified.endpointFileId === undefined ? {} : { endpointFileId: verified.endpointFileId }),
5623
- },
5624
- launch.id,
5625
- );
5626
- if (!replayIncarnation) return fail("endpoint_stale", "Session endpoint incarnation is unavailable.");
5627
- return {
5628
- ok: true,
5629
- result: {
5630
- sessionId: launch.id,
5631
- cwd: launch.cwd,
5632
- endpointGeneration: verified.endpointGeneration,
5633
- endpointIncarnation: replayIncarnation,
5634
- pid: verified.endpoint.pid,
5635
- endpointMtimeMs: verified.endpointMtimeMs,
5636
- endpoint: verified.endpoint,
5637
- // Public, ledger-replayable evidence of the bootstrap authority that
5638
- // reached this runtime. The private key never enters the response.
5639
- ...(launch.coordinatorSidecarKeyId ? { coordinatorSidecarKeyId: launch.coordinatorSidecarKeyId } : {}),
5640
- ...(launch.readiness === "deferred" ? { readiness: "prepared" as const } : {}),
5641
- ...(worktreeReceipt ? { worktree: worktreeReceipt } : {}),
5642
- },
5643
- };
5644
6178
  }
5645
6179
 
5646
6180
  const id = cleanup && operation === "session.delete" ? cleanup.sessionId : sessionId(input);
5647
6181
  if (!id) return fail("invalid_input", "sessionId is required.");
5648
6182
  if (!isCanonicalSessionId(id)) return fail("invalid_input", "sessionId must be a canonical safe identifier.");
6183
+ if (operation === "session.close" && input.forceRetireStale === true)
6184
+ return await executeStaleSessionRetirement(broker, input);
5649
6185
  await broker.index.refresh();
5650
6186
  let record = broker.index.listSessions().sessions.find(session => session.sessionId === id);
5651
6187
  if (operation === "session.close") {
@@ -6854,8 +7390,9 @@ export async function executeLifecycle(
6854
7390
  }
6855
7391
 
6856
7392
  /**
6857
- * The live client/socket subscription count of the SDK session endpoint this
6858
- * process serves, or `undefined` while this process serves no endpoint.
7393
+ * The live SDK demand count of the session endpoint this process serves, or
7394
+ * `undefined` while this process serves no endpoint. Observer-only sockets are
7395
+ * excluded by {@link SessionHostRuntimeEvidence.observerClients}.
6859
7396
  *
6860
7397
  * Only the SDK session runtime owns the real socket table, so it publishes a
6861
7398
  * reader here instead of every consumer re-deriving attachment from the OS.
@@ -6867,8 +7404,10 @@ export type SessionHostAttachmentReader = () => number;
6867
7404
  export interface SessionHostRuntimeEvidence {
6868
7405
  /** This runtime's own live SDK client/socket subscription count. */
6869
7406
  attachedClients: SessionHostAttachmentReader;
6870
- /** Whether this runtime currently has agent work in flight. */
6871
- workInFlight: () => boolean;
7407
+ /** Optional subset of attached sockets that only observes the host. */
7408
+ observerClients?: SessionHostAttachmentReader;
7409
+ /** One authoritative lease held by every admitted or continuing work state. */
7410
+ workLease: SessionWorkLease;
6872
7411
  }
6873
7412
 
6874
7413
  /** One runtime's live publication, retractable only by its owner. */
@@ -6907,14 +7446,14 @@ export function publishSessionHostRuntimeEvidence(evidence: SessionHostRuntimeEv
6907
7446
  }
6908
7447
 
6909
7448
  /**
6910
- * This process's currently attached SDK client count, summed across every
7449
+ * This process's currently demanding SDK client count, summed across every
6911
7450
  * serving runtime, or `undefined` when no runtime publishes a readable count
6912
7451
  * (before startup, after teardown, or when every reader itself fails). Never
6913
7452
  * guesses: absence of a reader is absence of evidence.
6914
7453
  */
6915
7454
  export function sessionHostAttachedClients(): number | undefined {
6916
7455
  let total: number | undefined;
6917
- for (const { attachedClients } of sessionHostRuntimes) {
7456
+ for (const { attachedClients, observerClients } of sessionHostRuntimes) {
6918
7457
  let count: number;
6919
7458
  try {
6920
7459
  count = attachedClients();
@@ -6922,21 +7461,31 @@ export function sessionHostAttachedClients(): number | undefined {
6922
7461
  continue;
6923
7462
  }
6924
7463
  if (!Number.isSafeInteger(count) || count < 0) continue;
6925
- total = (total ?? 0) + count;
7464
+ let observers = 0;
7465
+ if (observerClients) {
7466
+ try {
7467
+ const observed = observerClients();
7468
+ if (Number.isSafeInteger(observed) && observed >= 0) observers = Math.min(count, observed);
7469
+ } catch {
7470
+ // If the observer subset is unavailable, retain the full attachment
7471
+ // count. Losing positive demand evidence is fail-closed.
7472
+ }
7473
+ }
7474
+ total = (total ?? 0) + count - observers;
6926
7475
  }
6927
7476
  return total;
6928
7477
  }
6929
7478
 
6930
7479
  /**
6931
- * Whether any runtime in this process has agent work in flight right now.
7480
+ * Whether any runtime in this process still holds its authoritative work lease.
6932
7481
  *
6933
7482
  * Positive evidence only: a runtime that publishes nothing, or whose reader
6934
7483
  * fails, reports no work, so this can never keep an abandoned host alive.
6935
7484
  */
6936
- export function sessionHostWorkInFlight(): boolean {
6937
- for (const { workInFlight } of sessionHostRuntimes) {
7485
+ export function sessionHostWorkLeaseActive(): boolean {
7486
+ for (const { workLease } of sessionHostRuntimes) {
6938
7487
  try {
6939
- if (workInFlight()) return true;
7488
+ if (workLease.isHeld()) return true;
6940
7489
  } catch {}
6941
7490
  }
6942
7491
  return false;
@@ -7001,7 +7550,7 @@ export async function reapDeadSessionRegistrations(
7001
7550
  .slice(0, Math.max(0, limit));
7002
7551
  const reaped: ReapedSessionRegistration[] = [];
7003
7552
  for (const session of dead) {
7004
- if (!(await broker.index.unregisterIfCurrent(session))) continue;
7553
+ if (!(await broker.index.unregisterIfCurrent(session, "process_exited"))) continue;
7005
7554
  const record = {
7006
7555
  sessionId: session.sessionId,
7007
7556
  pid: session.pid,