@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
@@ -8,6 +8,7 @@ import { logger, resolveEquivalentPath } from "@gajae-code/utils";
8
8
  import packageJson from "../../../package.json" with { type: "json" };
9
9
  import type { ModelProfileErrorDetails } from "../../config/model-profile-contract";
10
10
  import { planLaunchWorktree } from "../../gjc-runtime/launch-worktree";
11
+ import { readExistingStateForMutation, withWorkflowStateLock } from "../../gjc-runtime/state-writer";
11
12
  import { SdkClient, SdkClientError } from "../client";
12
13
  import {
13
14
  BROKER_RUNTIME_ABORT_CAPABILITY_FIELD,
@@ -37,7 +38,13 @@ import {
37
38
  redactBrokerDiscovery,
38
39
  } from "./discovery";
39
40
  import { endpointIncarnation, matchesIndexedEndpointFile, readEndpointFile } from "./endpoint-authority";
40
- import { deriveIdempotencyIdentity, getBrokerIdentityKey } from "./identity";
41
+ import {
42
+ deriveIdempotencyIdentity,
43
+ deriveLegacyIdentity,
44
+ deriveLegacyTargetIdentity,
45
+ deriveScopedIdempotencyIdentity,
46
+ getBrokerIdentityKey,
47
+ } from "./identity";
41
48
  import {
42
49
  canonicalDeleteLocatorPath,
43
50
  executeLifecycle,
@@ -52,7 +59,38 @@ import {
52
59
  LifecycleLedger,
53
60
  type LifecycleStartupFailureReceipt,
54
61
  type LifecycleState,
62
+ type TerminalReadBack,
55
63
  } from "./lifecycle-ledger";
64
+ import {
65
+ cancelManagedTasks,
66
+ createManagedDomainBinding,
67
+ currentManagedRevision,
68
+ defineManagedTaskGraph,
69
+ inspectManagedAttemptByNativeIdentity,
70
+ loadManagedDomainBinding,
71
+ loadManagedEnrollmentRecord,
72
+ lookupManagedAttemptByNativeIdentity,
73
+ type ManagedAttemptRef,
74
+ type ManagedDomainBinding,
75
+ type ManagedEnrollmentRecord,
76
+ type ManagedTaskAttempt,
77
+ managedAttemptRefMatches,
78
+ managedIdentity,
79
+ managedNativeVector,
80
+ managedTaskDomainPath,
81
+ markManagedEnrollmentEstablished,
82
+ markManagedEnrollmentPending,
83
+ markManagedEnrollmentPublishing,
84
+ observeOrAdmitManagedTask,
85
+ recordManagedEnrollment,
86
+ recordManagedNativeObservation,
87
+ restoreManagedAttemptRefs,
88
+ reviseManagedTaskGraph,
89
+ transactManagedTaskDomain,
90
+ validateManagedTaskDomain,
91
+ withManagedNativeEffectAuthorized,
92
+ } from "./managed-task-dag";
93
+ import { reconcileRunningManagedVerification, verifyManagedTaskAttempt } from "./managed-task-verification";
56
94
  import { createMasterCapabilityVerifier, readEndpoint } from "./master-capability";
57
95
  import { sdkInternalRuntimeImage } from "./runtime";
58
96
  import { type IndexedSession, isSessionAuthorityEligible, SessionIndex, type SessionList } from "./session-index";
@@ -209,7 +247,7 @@ export type BrokerCleanupEvidence = {
209
247
  artifactsIdentity?: BrokerCleanupIdentity;
210
248
  transcriptIdentity?: BrokerCleanupIdentity;
211
249
  transcriptParentIdentity?: { dev: string; ino: string };
212
- /** Identity-bound lifecycle metadata marker retained when exact cleanup is deferred. */
250
+ /** Identity-bound lifecycle metadataker retained when exact cleanup is deferred. */
213
251
  metadataIdentity?: BrokerCleanupIdentity;
214
252
  metadataPath?: string;
215
253
  /** Monotonic append-only cleanup attempt. */
@@ -241,7 +279,7 @@ export type BrokerCleanupEvidence = {
241
279
  /** Fully identity-bound startup-failure cleanup plan, persisted before any detach. */
242
280
  lifecycleFiles?: BrokerLifecycleCleanupFile[];
243
281
  lifecycleParentIdentity?: { dev: string; ino: string };
244
- /** Delete metadata receipts authorize only the canonical marker/ready sibling pair. */
282
+ /** Delete metadata receipts authorize only the canonicalker/ready sibling pair. */
245
283
  lifecycleDeleteMetadata?: true;
246
284
  };
247
285
  export type BrokerResponse =
@@ -259,6 +297,17 @@ export type BrokerResponse =
259
297
  durableEffects?: LifecycleDurableEffectsReceipt;
260
298
  startupFailure?: LifecycleStartupFailureReceipt;
261
299
  };
300
+ type VerifiedManagedOwner =
301
+ | {
302
+ kind: "verified";
303
+ ownerSessionId: string;
304
+ attestationEpoch: string;
305
+ controlRoot: string;
306
+ enrollmentId: string;
307
+ worktrees: string[];
308
+ aliases?: string[];
309
+ }
310
+ | { kind: "denied"; response: BrokerResponse };
262
311
  const error = (code: BrokerErrorCode, message: string): BrokerResponse => ({ ok: false, error: { code, message } });
263
312
  const spawnFailureError = (failure: SpawnSubstrateFailure): BrokerResponse => ({
264
313
  ok: false,
@@ -334,7 +383,7 @@ function lifecycleResponseState(response: BrokerResponse): LifecycleState {
334
383
  : "terminal_error";
335
384
  }
336
385
 
337
- type InputNormalization = { input: Record<string, unknown> } | BrokerResponse;
386
+ export type InputNormalization = { input: Record<string, unknown> } | BrokerResponse;
338
387
 
339
388
  type SessionListCursor = {
340
389
  sessions: IndexedSession[];
@@ -356,6 +405,7 @@ type SpawnInFlight = {
356
405
  resolve: (response: BrokerResponse) => void;
357
406
  claimId?: string;
358
407
  phase?: SpawnClaimV2["state"];
408
+ managedRequestHash?: string;
359
409
  };
360
410
 
361
411
  /** Complete five-leg child endpoint pin captured at registration. */
@@ -411,6 +461,9 @@ type SpawnAdmissionInput = {
411
461
  cwd: string;
412
462
  modelId?: string;
413
463
  modelPreset?: string;
464
+ managedAttempt?: ManagedAttemptRef;
465
+ managedBinding?: ManagedDomainBinding;
466
+ managedNativeVector?: string;
414
467
  };
415
468
 
416
469
  function parseSpawnInput(
@@ -505,6 +558,23 @@ const SPAWN_HOST_REGISTRATION_TIMEOUT_MS = 10_000;
505
558
  const SPAWN_HOST_REGISTRATION_POLL_MS = 50;
506
559
  const SPAWN_PROMPT_EXCHANGE_TIMEOUT_MS = 10_000;
507
560
  const MASTER_ORPHAN_GRACE_DEFAULT_MS = 120_000;
561
+ const MANAGED_CLOSE_WAIT_MS = 10_000;
562
+
563
+ function settlesWithin(promise: Promise<unknown>, timeoutMs: number): Promise<boolean> {
564
+ const settled = Promise.withResolvers<boolean>();
565
+ const timer = setTimeout(() => settled.resolve(false), timeoutMs);
566
+ void promise.then(
567
+ () => {
568
+ clearTimeout(timer);
569
+ settled.resolve(true);
570
+ },
571
+ () => {
572
+ clearTimeout(timer);
573
+ settled.resolve(true);
574
+ },
575
+ );
576
+ return settled.promise;
577
+ }
508
578
 
509
579
  /** The five legs every usable pin must carry; a partial pin is missing authority. */
510
580
  type CompleteSpawnPinAuthority = SpawnAuthorityV1 & {
@@ -563,6 +633,26 @@ function matchesSpawnPin(candidate: IndexedSession, pinned: SpawnHostRegistratio
563
633
  );
564
634
  }
565
635
 
636
+ /** Exact admission predicate for the child host row published after launch. */
637
+ export function spawnRegistrationMatches(
638
+ candidate: IndexedSession,
639
+ expected: { childId: string; cwd: string; stateRoot: string },
640
+ ): boolean {
641
+ return (
642
+ candidate.sessionId === expected.childId &&
643
+ candidate.endpointGeneration > 0 &&
644
+ candidate.live &&
645
+ !candidate.terminal &&
646
+ !candidate.terminalUncertain &&
647
+ // The child publishes a realpath-canonicalized locator while the launch
648
+ // spec carries the caller's lexical spelling. Compare path identity so a
649
+ // symlinked workspace (macOS /var vs /private/var) cannot strand a valid
650
+ // registration behind different spellings.
651
+ resolveEquivalentPath(candidate.locator.cwd) === resolveEquivalentPath(expected.cwd) &&
652
+ resolveEquivalentPath(candidate.locator.stateRoot) === resolveEquivalentPath(expected.stateRoot)
653
+ );
654
+ }
655
+
566
656
  /** Rebuilds the provider proof from durable authority facts only. */
567
657
  function spawnProofFromAuthority(authority: SpawnAuthorityV1): SpawnSubstrateProof {
568
658
  return {
@@ -635,7 +725,7 @@ function normalizeAliasedString(
635
725
  return { value: values[0] };
636
726
  }
637
727
 
638
- function normalizeBrokerInput(operation: string, input: Record<string, unknown>): InputNormalization {
728
+ export function normalizeBrokerInput(operation: string, input: Record<string, unknown>): InputNormalization {
639
729
  const normalized: Record<string, unknown> = { ...input };
640
730
  const session = normalizeAliasedString(input, "sessionId", ["id"]);
641
731
  if (session.error) return error("invalid_input", session.error);
@@ -725,11 +815,47 @@ function canonicalJson(value: unknown): string {
725
815
  if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
726
816
  const record = value as Record<string, unknown>;
727
817
  return `{${Object.keys(record)
818
+ .filter(key => record[key] !== undefined)
728
819
  .sort()
729
820
  .map(key => `${JSON.stringify(key)}:${canonicalJson(record[key])}`)
730
821
  .join(",")}}`;
731
822
  }
732
823
 
824
+ type TerminalPersistenceVerification =
825
+ | { kind: "unverified" }
826
+ | { kind: "verified" }
827
+ | { kind: "uncertain"; mismatches: readonly string[] };
828
+
829
+ function verifyTerminalPersistence(
830
+ readBack: TerminalReadBack,
831
+ storedResponse: BrokerResponse,
832
+ durableEffects: LifecycleDurableEffectsReceipt | undefined,
833
+ startupFailure: LifecycleStartupFailureReceipt | undefined,
834
+ ): TerminalPersistenceVerification {
835
+ // An absent read-back is a verification failure, not evidence that the
836
+ // operation itself is ambiguous. The terminal transition was already synced
837
+ // before this read, so preserve its settled outcome and let a later reader
838
+ // reconcile the durable row.
839
+ if (readBack.kind === "absent") return { kind: "unverified" };
840
+ // A *rejected* row is the opposite: the ledger holds something for this request
841
+ // and refused it. That is damaged durable state, which previously fenced the
842
+ // session and must keep fencing it — treating it as "not yet written" would let
843
+ // a corrupt row clear the fence it exists to raise.
844
+ if (readBack.kind === "rejected") return { kind: "uncertain", mismatches: [`readBack:${readBack.reason}`] };
845
+ const persisted = readBack.entry;
846
+ const optionalReceiptJson = (value: unknown): string | undefined =>
847
+ value === undefined || value === null ? undefined : canonicalJson(value);
848
+ // The field name is always present; only the two compared receipt renderings are
849
+ // optional. Naming that shape keeps the mismatch list a plain string list.
850
+ const comparisons: readonly (readonly [string, string | undefined, string | undefined])[] = [
851
+ ["response", canonicalJson(persisted.response), canonicalJson(storedResponse)],
852
+ ["durableEffects", optionalReceiptJson(persisted.durableEffects), optionalReceiptJson(durableEffects)],
853
+ ["startupFailure", optionalReceiptJson(persisted.startupFailure), optionalReceiptJson(startupFailure)],
854
+ ];
855
+ const mismatches = comparisons.filter(([, actual, expected]) => actual !== expected).map(([field]) => field);
856
+ return mismatches.length === 0 ? { kind: "verified" } : { kind: "uncertain", mismatches };
857
+ }
858
+
733
859
  function credentialFreeLifecycleResponse(value: unknown): unknown {
734
860
  if (Array.isArray(value)) return value.map(credentialFreeLifecycleResponse);
735
861
  if (value === null || typeof value !== "object") return value;
@@ -903,15 +1029,31 @@ function lifecycleTarget(operation: string, input: Record<string, unknown>): unk
903
1029
  sourceSessionPath: string(input.sourceSessionPath, input.sourcePath, input.sessionPath),
904
1030
  };
905
1031
  case "session.resume":
906
- case "session.close":
907
1032
  case "session.delete":
908
1033
  case "session.reconcile_uncertain":
909
1034
  return { sessionId: id };
1035
+ case "session.close":
1036
+ return { sessionId: id, ...closeTargetAuthority(input) };
910
1037
  default:
911
1038
  return { operation, root, sessionId: id };
912
1039
  }
913
1040
  }
914
1041
 
1042
+ function closeTargetAuthority(input: Record<string, unknown>): {
1043
+ endpointGeneration?: number;
1044
+ endpointIncarnation?: string;
1045
+ } {
1046
+ if (
1047
+ typeof input.endpointGeneration !== "number" ||
1048
+ !Number.isSafeInteger(input.endpointGeneration) ||
1049
+ input.endpointGeneration <= 0 ||
1050
+ typeof input.endpointIncarnation !== "string" ||
1051
+ !/^[a-f0-9]{64}$/u.test(input.endpointIncarnation)
1052
+ )
1053
+ return {};
1054
+ return { endpointGeneration: input.endpointGeneration, endpointIncarnation: input.endpointIncarnation };
1055
+ }
1056
+
915
1057
  /**
916
1058
  * A lifecycle request without a worktree remains serialized by its state root.
917
1059
  * Worktree launches instead serialize on the source repository plus the
@@ -990,10 +1132,11 @@ function isBrokerLockArtifactName(name: string): boolean {
990
1132
  * Decide whether one candidate directory is provably abandoned.
991
1133
  *
992
1134
  * Fail-closed by construction: every branch that cannot prove abandonment
993
- * returns a retention reason. A tombstone is only abandoned when its owner
994
- * record parses and names a dead PID — an unreadable, permission-denied, or
995
- * absent record keeps it forever. Backup directories carry no owner contract,
996
- * so an absent record there is not ambiguity and age alone governs.
1135
+ * returns a retention reason. A tombstone with an owner record is only
1136
+ * abandoned when that record names a dead PID. A missing record is abandoned
1137
+ * only when the aged tombstone is empty; non-empty tombstones remain retained
1138
+ * as ambiguous evidence. Backup directories carry no owner contract, so an
1139
+ * absent record there is not ambiguity and age alone governs.
997
1140
  */
998
1141
  async function classifyBrokerLockArtifact(
999
1142
  directory: string,
@@ -1014,7 +1157,14 @@ async function classifyBrokerLockArtifact(
1014
1157
  } catch (error) {
1015
1158
  const code = (error as NodeJS.ErrnoException).code;
1016
1159
  if (code !== "ENOENT" && code !== "ENOTDIR") return "owner-record-unreadable";
1017
- return name.startsWith(BROKER_LOCK_TOMBSTONE_PREFIX) ? "owner-record-missing" : "abandoned";
1160
+ if (!name.startsWith(BROKER_LOCK_TOMBSTONE_PREFIX)) return "abandoned";
1161
+ try {
1162
+ const entries = await fs.readdir(target);
1163
+ return entries.length === 0 ? "abandoned" : "owner-record-missing";
1164
+ } catch (readError) {
1165
+ if ((readError as NodeJS.ErrnoException).code === "ENOENT") throw readError;
1166
+ return "owner-record-unreadable";
1167
+ }
1018
1168
  }
1019
1169
  let pid: unknown;
1020
1170
  try {
@@ -1034,7 +1184,8 @@ async function classifyBrokerLockArtifact(
1034
1184
  * of the lock's dev+ino, so a machine accrues one directory per dead owner and
1035
1185
  * nothing ever removed them (54 on the install in #3963). Reaping is
1036
1186
  * best-effort and fail-closed: anything live, unreadable, permission-denied, or
1037
- * otherwise ambiguous is kept and the reason is logged.
1187
+ * otherwise ambiguous is kept. Retained paths are logged at debug level and
1188
+ * summarized in one warning per reap pass.
1038
1189
  */
1039
1190
  export async function reapStaleBrokerLockArtifacts(input: {
1040
1191
  agentDir: string;
@@ -1048,6 +1199,7 @@ export async function reapStaleBrokerLockArtifacts(input: {
1048
1199
  const pidAlive = input.pidAlive ?? isPidAlive;
1049
1200
  const removed: string[] = [];
1050
1201
  const retained: BrokerLockArtifactRetention[] = [];
1202
+ const retainedByReason = new Map<BrokerLockArtifactRetentionReason, number>();
1051
1203
  let names: string[];
1052
1204
  try {
1053
1205
  names = await fs.readdir(directory);
@@ -1069,7 +1221,10 @@ export async function reapStaleBrokerLockArtifacts(input: {
1069
1221
  }
1070
1222
  if (verdict !== "abandoned") {
1071
1223
  retained.push({ path: target, reason: verdict });
1072
- if (verdict !== "within-grace") logger.warn(`sdk broker: retained stale lock artifact ${name} (${verdict})`);
1224
+ if (verdict !== "within-grace") {
1225
+ logger.debug(`sdk broker: retained stale lock artifact ${name} (${verdict})`);
1226
+ retainedByReason.set(verdict, (retainedByReason.get(verdict) ?? 0) + 1);
1227
+ }
1073
1228
  continue;
1074
1229
  }
1075
1230
  try {
@@ -1078,9 +1233,18 @@ export async function reapStaleBrokerLockArtifacts(input: {
1078
1233
  } catch (error) {
1079
1234
  if ((error as NodeJS.ErrnoException).code === "ENOENT") continue;
1080
1235
  retained.push({ path: target, reason: "removal-failed" });
1081
- logger.warn(`sdk broker: retained stale lock artifact ${name} (removal-failed)`);
1236
+ logger.debug(`sdk broker: retained stale lock artifact ${name} (removal-failed)`);
1237
+ retainedByReason.set("removal-failed", (retainedByReason.get("removal-failed") ?? 0) + 1);
1082
1238
  }
1083
1239
  }
1240
+ if (retainedByReason.size > 0) {
1241
+ const details = [...retainedByReason.entries()]
1242
+ .sort(([left], [right]) => left.localeCompare(right))
1243
+ .map(([reason, count]) => `${reason}: ${count}`)
1244
+ .join(", ");
1245
+ const retainedCount = [...retainedByReason.values()].reduce((total, count) => total + count, 0);
1246
+ logger.warn(`sdk broker: retained ${retainedCount} stale lock artifact(s) (${details})`);
1247
+ }
1084
1248
  if (removed.length > 0) logger.info(`sdk broker: reaped ${removed.length} stale lock artifact(s)`);
1085
1249
  return { removed, retained };
1086
1250
  }
@@ -1283,6 +1447,7 @@ const publicationObservationOverridesForTest = new WeakMap<Broker, BrokerPublica
1283
1447
  const lockArtifactGraceOverridesForTest = new WeakMap<Broker, number>();
1284
1448
  const livenessGraceOverridesForTest = new WeakMap<Broker, number>();
1285
1449
  const heartbeatStallOverridesForTest = new WeakMap<Broker, PromiseWithResolvers<void>>();
1450
+ const managedCloseWaitOverridesForTest = new WeakMap<Broker, number>();
1286
1451
 
1287
1452
  export class Broker {
1288
1453
  readonly settings: ResolvedBrokerSettings;
@@ -1295,6 +1460,9 @@ export class Broker {
1295
1460
  #chains = new Map<string, Promise<void>>();
1296
1461
  #spawnInFlight = new Map<string, SpawnInFlight>();
1297
1462
  #spawnTasks = new WeakMap<SpawnInFlight, string>();
1463
+ #managedAttempts = new Map<string, ManagedAttemptRef>();
1464
+ #managedFailedRoots = new Set<string>();
1465
+ #managedEnrollmentFailed = false;
1298
1466
  #spawnAuthority: SpawnAuthorityStore | null = null;
1299
1467
  #spawnPromptLayer: SpawnPromptLayer;
1300
1468
  #spawnReapInFlight = false;
@@ -1369,11 +1537,17 @@ export class Broker {
1369
1537
  async #handleSpawn(input: Record<string, unknown>, idempotencyKey: string | undefined): Promise<BrokerResponse> {
1370
1538
  const admission = parseSpawnInput(input, idempotencyKey);
1371
1539
  if (isBrokerResponse(admission)) return admission;
1372
- const lifecycleIdentity = await deriveIdempotencyIdentity(
1540
+ // session.spawn predates target-bound lifecycle identities and its durable
1541
+ // authority store already binds the raw request. Keep the v3 identity for
1542
+ // new claims, while recognizing the v4 identity written by the short-lived
1543
+ // target-bound implementation so an upgrade cannot admit a second child.
1544
+ const targetBoundIdentity = await deriveIdempotencyIdentity(
1373
1545
  this.settings.agentDir,
1374
1546
  "session.spawn",
1375
1547
  idempotencyKey!,
1376
1548
  );
1549
+ const legacyIdentity = await deriveLegacyIdentity(this.settings.agentDir, "session.spawn", idempotencyKey!);
1550
+ const lifecycleIdentity = this.#spawnAuthority?.claim(targetBoundIdentity) ? targetBoundIdentity : legacyIdentity;
1377
1551
  const active = this.#spawnInFlight.get(lifecycleIdentity);
1378
1552
  if (active) {
1379
1553
  if (this.#spawnTasks.get(active) !== admission.task)
@@ -1404,119 +1578,12 @@ export class Broker {
1404
1578
  admission.masterCapability = "";
1405
1579
  }
1406
1580
  if (!verified.allowed) return finish(error("spawn_failed", "master capability verification was denied"));
1407
- const key = await getBrokerIdentityKey(this.settings.agentDir);
1408
- const authority = this.#spawnAuthority;
1409
- if (!authority) return finish(error("unavailable", "spawn authority is unavailable"));
1410
- const requestBindingMac = spawnBindingMac(
1411
- key,
1412
- admission,
1413
- admission.modelId ?? null,
1414
- admission.modelPreset ?? null,
1415
- );
1416
- const existing = authority.claim(lifecycleIdentity);
1417
- const resolveModels = async (): Promise<BrokerResponse | undefined> => {
1418
- // Validate the optional profile BEFORE any new durable claim or substrate
1419
- // effect. Generic lifecycle create validates it; spawn must do the same.
1420
- if (admission.modelPreset !== undefined) {
1421
- const validated = validateBrokerModelPresetSync(this.settings.agentDir, admission.modelPreset);
1422
- if (isBrokerResponse(validated)) return validated;
1423
- admission.modelPreset = validated;
1424
- }
1425
- // Resolve the explicit model pin before a new durable claim. Raw
1426
- // selectors must never reach a newly launched child.
1427
- if (admission.modelId !== undefined) {
1428
- const resolved = await this.#resolveModelPin(admission.modelId, { cwd: admission.cwd });
1429
- if (!resolved.ok) return error("unknown_model", resolved.error);
1430
- if (resolved.model === null)
1431
- return error("unknown_model", "session.spawn modelId could not be resolved.");
1432
- admission.modelId = resolved.model;
1433
- }
1434
- return undefined;
1435
- };
1436
- let decision: SpawnClaimDecision;
1437
- if (existing !== undefined) {
1438
- // An existing claim carries the raw-selector binding, so terminal and
1439
- // replay responses do not depend on today's model/profile catalog.
1440
- let existingBindingMac: string | undefined;
1441
- if (existing.requestBindingMac === undefined) {
1442
- // Claims written before raw-selector evidence was introduced only have
1443
- // the canonical binding. Re-resolve that historical selector before
1444
- // comparison; if the catalog no longer proves it, fail closed rather
1445
- // than guessing or weakening idempotency conflict protection.
1446
- const modelError = await resolveModels();
1447
- if (modelError) return finish(modelError);
1448
- existingBindingMac = spawnBindingMac(
1449
- key,
1450
- admission,
1451
- admission.modelId ?? null,
1452
- admission.modelPreset ?? null,
1453
- );
1454
- }
1455
- decision = await authority.claimOrJoin(
1456
- lifecycleIdentity,
1457
- existingBindingMac,
1458
- existingBindingMac ?? requestBindingMac,
1459
- );
1460
- if (decision.kind === "owner") {
1461
- becameOwner = true;
1462
- // A recovered pre-send claim may still need to launch. Preserve the
1463
- // fail-closed model validation for that effectful path and reject a
1464
- // catalog remap rather than launching a different model.
1465
- const modelError = await resolveModels();
1466
- if (modelError) return finish(modelError);
1467
- const recoveredBindingMac = spawnBindingMac(
1468
- key,
1469
- admission,
1470
- admission.modelId ?? null,
1471
- admission.modelPreset ?? null,
1472
- );
1473
- if (recoveredBindingMac !== decision.claim.bindingMac)
1474
- return finish(
1475
- error("idempotency_conflict", "session.spawn model selection differs from its durable claim"),
1476
- );
1477
- }
1478
- } else {
1479
- const modelError = await resolveModels();
1480
- if (modelError) {
1481
- // A concurrent claimant may have durably established this exact raw
1482
- // request while catalog loading was in flight. Re-check its opaque
1483
- // binding before returning the validation failure.
1484
- const raced = authority.claim(lifecycleIdentity);
1485
- if (raced === undefined) return finish(modelError);
1486
- decision = await authority.claimOrJoin(lifecycleIdentity, undefined, requestBindingMac);
1487
- if (decision.kind === "owner") {
1488
- becameOwner = true;
1489
- return finish(modelError);
1490
- }
1491
- } else {
1492
- const bindingMac = spawnBindingMac(
1493
- key,
1494
- admission,
1495
- admission.modelId ?? null,
1496
- admission.modelPreset ?? null,
1497
- );
1498
- decision = await authority.claimOrJoin(lifecycleIdentity, bindingMac, requestBindingMac);
1499
- }
1500
- }
1501
- if (decision.kind === "idempotency_conflict")
1502
- return finish(
1503
- error("idempotency_conflict", "idempotency key conflicts with an existing session.spawn claim"),
1504
- );
1505
- if (decision.kind === "in_progress")
1506
- return finish(error("spawn_in_progress", `session.spawn is ${decision.claim.state}`));
1507
- if (decision.kind === "terminal_uncertain")
1508
- return finish(error("terminal_uncertain", "session.spawn outcome is uncertain"));
1509
- if (decision.kind === "terminal") return finish(this.#spawnTerminalResponse(decision.claim));
1510
- if (decision.kind === "replay")
1511
- return finish(await this.#reconcileSpawnReplay(lifecycleIdentity, decision.claim));
1512
- becameOwner = true;
1513
- inFlight.claimId = decision.claim.claimId;
1514
- inFlight.phase = decision.claim.state;
1515
- // Claim fsync precedes this audit mirror. Substrate work may start
1516
- // only after the mirror succeeds; a startup reconciler rebuilds it claim-first.
1517
- await this.ledger.begin(lifecycleIdentity, decision.claim.bindingMac);
1581
+ const managedDenied = await this.#denyOrdinaryManagedSpawn(lifecycleIdentity, idempotencyKey!);
1582
+ if (managedDenied) return finish(managedDenied);
1518
1583
  return finish(
1519
- await this.#driveSpawn(lifecycleIdentity, decision.claim, admission, inFlight, decision.recovery),
1584
+ await this.#driveVerifiedSpawn(lifecycleIdentity, admission, inFlight, owner => {
1585
+ becameOwner = owner;
1586
+ }),
1520
1587
  );
1521
1588
  } catch {
1522
1589
  return finish(error("spawn_failed", "session.spawn admission could not be durably established"));
@@ -1527,6 +1594,641 @@ export class Broker {
1527
1594
  if (becameOwner) await this.#spawnAuthority?.releaseOwner(lifecycleIdentity);
1528
1595
  }
1529
1596
  }
1597
+ async #denyOrdinaryManagedSpawn(lifecycleIdentity: string, callerKey: string): Promise<BrokerResponse | undefined> {
1598
+ if (this.#managedAttempts.has(lifecycleIdentity))
1599
+ return error("spawn_failed", "managed native identity cannot re-enter ordinary session.spawn");
1600
+ if (this.#managedEnrollmentFailed) return error("spawn_failed", "managed enrollment membership is unprovable");
1601
+ let enrolled: ManagedEnrollmentRecord;
1602
+ try {
1603
+ enrolled = await loadManagedEnrollmentRecord(this.settings.agentDir);
1604
+ } catch {
1605
+ return error("spawn_failed", "managed enrollment membership is unprovable");
1606
+ }
1607
+ if (enrolled.nativeIdentities.includes(lifecycleIdentity))
1608
+ return error("spawn_failed", "managed native identity cannot re-enter ordinary session.spawn");
1609
+ for (const root of enrolled.controlRoots) {
1610
+ if (this.#managedFailedRoots.has(root)) continue;
1611
+ try {
1612
+ const scopedIdentity = await deriveScopedIdempotencyIdentity(
1613
+ this.settings.agentDir,
1614
+ "session.spawn",
1615
+ callerKey,
1616
+ root,
1617
+ );
1618
+ if (this.#managedAttempts.has(scopedIdentity) || enrolled.nativeIdentities.includes(scopedIdentity))
1619
+ return error("spawn_failed", "managed native identity cannot re-enter ordinary session.spawn");
1620
+ const read = await readExistingStateForMutation(managedTaskDomainPath(root));
1621
+ if (read.kind !== "valid") continue;
1622
+ const state = validateManagedTaskDomain(read.value);
1623
+ const attempt =
1624
+ lookupManagedAttemptByNativeIdentity(state, lifecycleIdentity) ??
1625
+ lookupManagedAttemptByNativeIdentity(state, scopedIdentity) ??
1626
+ state.graphs.flatMap(graph => graph.attempts).find(item => item.native.key === callerKey);
1627
+ if (attempt) return error("spawn_failed", "managed native identity cannot re-enter ordinary session.spawn");
1628
+ } catch {}
1629
+ }
1630
+ return undefined;
1631
+ }
1632
+
1633
+ async #driveVerifiedSpawn(
1634
+ lifecycleIdentity: string,
1635
+ admission: SpawnAdmissionInput,
1636
+ inFlight: SpawnInFlight,
1637
+ setOwner: (owned: boolean) => void,
1638
+ ): Promise<BrokerResponse> {
1639
+ const key = await getBrokerIdentityKey(this.settings.agentDir);
1640
+ const authority = this.#spawnAuthority;
1641
+ if (!authority) return error("unavailable", "spawn authority is unavailable");
1642
+ const requestBindingMac = spawnBindingMac(
1643
+ key,
1644
+ admission,
1645
+ admission.modelId ?? null,
1646
+ admission.modelPreset ?? null,
1647
+ );
1648
+ const existing = authority.claim(lifecycleIdentity);
1649
+ const resolveModels = async (): Promise<BrokerResponse | undefined> => {
1650
+ if (admission.modelPreset !== undefined) {
1651
+ const validated = validateBrokerModelPresetSync(this.settings.agentDir, admission.modelPreset);
1652
+ if (isBrokerResponse(validated)) return validated;
1653
+ admission.modelPreset = validated;
1654
+ }
1655
+ if (admission.modelId !== undefined) {
1656
+ const resolved = await this.#resolveModelPin(admission.modelId, { cwd: admission.cwd });
1657
+ if (!resolved.ok) return error("unknown_model", resolved.error);
1658
+ if (resolved.model === null) return error("unknown_model", "session.spawn modelId could not be resolved.");
1659
+ admission.modelId = resolved.model;
1660
+ }
1661
+ return undefined;
1662
+ };
1663
+ let decision: SpawnClaimDecision | undefined;
1664
+ if (existing !== undefined) {
1665
+ let existingBindingMac: string | undefined;
1666
+ if (existing.requestBindingMac === undefined) {
1667
+ const modelError = await resolveModels();
1668
+ if (modelError) return modelError;
1669
+ existingBindingMac = spawnBindingMac(
1670
+ key,
1671
+ admission,
1672
+ admission.modelId ?? null,
1673
+ admission.modelPreset ?? null,
1674
+ );
1675
+ }
1676
+ decision = await authority.claimOrJoin(
1677
+ lifecycleIdentity,
1678
+ existingBindingMac,
1679
+ existingBindingMac ?? requestBindingMac,
1680
+ );
1681
+ if (decision.kind === "owner") {
1682
+ setOwner(true);
1683
+ const modelError = await resolveModels();
1684
+ if (modelError) return modelError;
1685
+ const recoveredBindingMac = spawnBindingMac(
1686
+ key,
1687
+ admission,
1688
+ admission.modelId ?? null,
1689
+ admission.modelPreset ?? null,
1690
+ );
1691
+ if (recoveredBindingMac !== decision.claim.bindingMac)
1692
+ return error("idempotency_conflict", "session.spawn model selection differs from its durable claim");
1693
+ }
1694
+ } else {
1695
+ const modelError = await resolveModels();
1696
+ if (modelError) {
1697
+ const raced = authority.claim(lifecycleIdentity);
1698
+ if (raced === undefined) return modelError;
1699
+ decision = await authority.claimOrJoin(lifecycleIdentity, undefined, requestBindingMac);
1700
+ if (decision.kind === "owner") {
1701
+ setOwner(true);
1702
+ return modelError;
1703
+ }
1704
+ } else {
1705
+ const bindingMac = spawnBindingMac(
1706
+ key,
1707
+ admission,
1708
+ admission.modelId ?? null,
1709
+ admission.modelPreset ?? null,
1710
+ );
1711
+ decision = await authority.claimOrJoin(lifecycleIdentity, bindingMac, requestBindingMac);
1712
+ }
1713
+ }
1714
+ if (!decision) return error("spawn_failed", "session.spawn admission could not be durably established");
1715
+ if (decision.kind === "managed_key_conflict")
1716
+ return error("idempotency_conflict", "idempotency key is reserved by a managed task.dag attempt");
1717
+ if (decision.kind === "idempotency_conflict")
1718
+ return error("idempotency_conflict", "idempotency key conflicts with an existing session.spawn claim");
1719
+ if (decision.kind === "in_progress")
1720
+ return error("spawn_in_progress", `session.spawn is ${decision.claim.state}`);
1721
+ if (decision.kind === "terminal_uncertain") {
1722
+ if (decision.claim.substrateProof !== undefined && decision.claim.authorityRef === undefined)
1723
+ return await this.#reconcileUncertainRegistration(lifecycleIdentity, decision.claim);
1724
+ return error("terminal_uncertain", "session.spawn outcome is uncertain");
1725
+ }
1726
+ if (decision.kind === "terminal") return this.#spawnTerminalResponse(decision.claim);
1727
+ if (decision.kind === "replay") return await this.#reconcileSpawnReplay(lifecycleIdentity, decision.claim);
1728
+ setOwner(true);
1729
+ inFlight.claimId = decision.claim.claimId;
1730
+ inFlight.phase = decision.claim.state;
1731
+ await this.ledger.begin(lifecycleIdentity, decision.claim.bindingMac);
1732
+ return await this.#driveSpawn(lifecycleIdentity, decision.claim, admission, inFlight, decision.recovery);
1733
+ }
1734
+
1735
+ async #handleManagedTaskDag(
1736
+ input: Record<string, unknown>,
1737
+ idempotencyKey: string | undefined,
1738
+ ): Promise<BrokerResponse> {
1739
+ if (input.action === "define") return this.#managedDefine(input);
1740
+ if (input.action === "advance") return this.#managedAdvance(input, idempotencyKey);
1741
+ if (input.action === "status") return this.#managedStatus(input);
1742
+ if (input.action === "revise") return this.#managedRevise(input);
1743
+ if (input.action === "cancel") return this.#managedCancel(input);
1744
+ if (input.action === "verify") return this.#managedVerify(input);
1745
+ return error("invalid_input", "unknown task.dag action");
1746
+ }
1747
+
1748
+ async #verifiedManagedOwner(input: Record<string, unknown>): Promise<VerifiedManagedOwner> {
1749
+ const ownerSessionId = input.ownerSessionId;
1750
+ const attestationEpoch = input.attestationEpoch;
1751
+ const masterCapability = input.masterCapability;
1752
+ const controlRoot = input.controlRoot;
1753
+ const enrollmentId = input.enrollmentId;
1754
+ if (
1755
+ typeof ownerSessionId !== "string" ||
1756
+ !isCanonicalSessionId(ownerSessionId) ||
1757
+ typeof attestationEpoch !== "string" ||
1758
+ attestationEpoch.length === 0 ||
1759
+ attestationEpoch.length > 512 ||
1760
+ typeof masterCapability !== "string" ||
1761
+ masterCapability.length === 0 ||
1762
+ masterCapability.length > 16_384 ||
1763
+ typeof controlRoot !== "string" ||
1764
+ !path.isAbsolute(controlRoot) ||
1765
+ path.normalize(controlRoot) !== controlRoot ||
1766
+ typeof enrollmentId !== "string" ||
1767
+ enrollmentId.trim().length === 0 ||
1768
+ enrollmentId.length > 256
1769
+ )
1770
+ return { kind: "denied", response: error("invalid_input", "task.dag authorization is invalid") };
1771
+ if ("accepted" in input || "pass" in input || "receipt" in input)
1772
+ return { kind: "denied", response: error("invalid_input", "public verification material is not authority") };
1773
+ if (input.worktrees !== undefined) {
1774
+ if (!Array.isArray(input.worktrees) || input.worktrees.length === 0)
1775
+ return { kind: "denied", response: error("invalid_input", "worktrees are invalid") };
1776
+ if (
1777
+ input.worktrees.some(
1778
+ value => typeof value !== "string" || !path.isAbsolute(value) || path.normalize(value) !== value,
1779
+ )
1780
+ )
1781
+ return { kind: "denied", response: error("invalid_input", "worktrees are invalid") };
1782
+ }
1783
+ if (input.aliases !== undefined) {
1784
+ if (!Array.isArray(input.aliases))
1785
+ return { kind: "denied", response: error("invalid_input", "aliases are invalid") };
1786
+ if (
1787
+ input.aliases.some(
1788
+ value =>
1789
+ typeof value !== "string" || value.length === 0 || path.normalize(value) !== path.resolve(value),
1790
+ )
1791
+ )
1792
+ return { kind: "denied", response: error("invalid_input", "aliases are invalid") };
1793
+ }
1794
+ let allowed = false;
1795
+ try {
1796
+ allowed = (await this.verifyMasterCapability(ownerSessionId, masterCapability, attestationEpoch)).allowed;
1797
+ } finally {
1798
+ if (typeof input.masterCapability === "string") input.masterCapability = "";
1799
+ }
1800
+ if (!allowed)
1801
+ return { kind: "denied", response: error("spawn_failed", "master capability verification was denied") };
1802
+ const worktrees = Array.isArray(input.worktrees)
1803
+ ? input.worktrees.filter((value): value is string => typeof value === "string" && value.length > 0)
1804
+ : [controlRoot];
1805
+ const aliases = Array.isArray(input.aliases)
1806
+ ? input.aliases.filter((value): value is string => typeof value === "string" && value.length > 0)
1807
+ : undefined;
1808
+ return {
1809
+ kind: "verified",
1810
+ ownerSessionId,
1811
+ attestationEpoch,
1812
+ controlRoot,
1813
+ enrollmentId,
1814
+ worktrees: worktrees.length > 0 ? worktrees : [controlRoot],
1815
+ ...(aliases && aliases.length > 0 ? { aliases } : {}),
1816
+ };
1817
+ }
1818
+
1819
+ #managedPublic(
1820
+ stateRevision: number,
1821
+ graphs: Array<{ id: string; revision: number; attempts: ManagedTaskAttempt[] }>,
1822
+ ): Record<string, unknown> {
1823
+ return {
1824
+ stateRevision,
1825
+ graphs: graphs.map(graph => ({
1826
+ id: graph.id,
1827
+ revision: graph.revision,
1828
+ attempts: graph.attempts.length,
1829
+ nativeRefs: graph.attempts.map(attempt => attempt.native.identity),
1830
+ fences: graph.attempts.map(attempt => attempt.fence),
1831
+ validations: graph.attempts.map(attempt => attempt.validation),
1832
+ accepted: graph.attempts.map(attempt => attempt.accepted !== null),
1833
+ })),
1834
+ };
1835
+ }
1836
+
1837
+ async #managedDefine(input: Record<string, unknown>): Promise<BrokerResponse> {
1838
+ if (
1839
+ typeof input.expectedRevision !== "number" ||
1840
+ !Number.isSafeInteger(input.expectedRevision) ||
1841
+ input.expectedRevision < 0
1842
+ )
1843
+ return error("invalid_input", "expectedRevision is required");
1844
+ if (typeof input.graphId !== "string" || input.graphId.trim().length === 0)
1845
+ return error("invalid_input", "graphId is required");
1846
+ const auth = await this.#verifiedManagedOwner(input);
1847
+ if (auth.kind === "denied") return auth.response;
1848
+ try {
1849
+ const binding = await createManagedDomainBinding({
1850
+ controlRoot: auth.controlRoot,
1851
+ agentDir: this.settings.agentDir,
1852
+ enrollmentId: auth.enrollmentId,
1853
+ worktrees: auth.worktrees,
1854
+ ...(auth.aliases ? { aliases: auth.aliases } : {}),
1855
+ });
1856
+ let rootWasEstablished = false;
1857
+ try {
1858
+ rootWasEstablished = (await loadManagedEnrollmentRecord(this.settings.agentDir)).establishedRoots.includes(
1859
+ binding.controlRoot,
1860
+ );
1861
+ } catch {
1862
+ throw new Error("native managed evidence exists");
1863
+ }
1864
+ // Publish the enrollment before the first domain snapshot. The index is
1865
+ // the recovery root list, so this ordering leaves only a recoverable
1866
+ // stale index entry if the process dies before the state publication.
1867
+ await recordManagedEnrollment(this.settings.agentDir, binding.controlRoot);
1868
+ const result = await transactManagedTaskDomain(
1869
+ {
1870
+ binding,
1871
+ expectedRevision: input.expectedRevision,
1872
+ ...(input.expectedRevision === 0
1873
+ ? {
1874
+ assertNoManagedEvidence: async () => {
1875
+ let enrolled: ManagedEnrollmentRecord;
1876
+ try {
1877
+ enrolled = await loadManagedEnrollmentRecord(this.settings.agentDir);
1878
+ } catch {
1879
+ throw new Error("native managed evidence exists");
1880
+ }
1881
+ if (
1882
+ rootWasEstablished ||
1883
+ enrolled.establishedRoots.includes(binding.controlRoot) ||
1884
+ enrolled.publishingRoots.includes(binding.controlRoot) ||
1885
+ (enrolled.byRoot[binding.controlRoot] ?? []).length > 0
1886
+ )
1887
+ throw new Error("native managed evidence exists");
1888
+ await recordManagedEnrollment(this.settings.agentDir, binding.controlRoot);
1889
+ },
1890
+ beforeFirstPublication: async () => {
1891
+ await markManagedEnrollmentPublishing(this.settings.agentDir, binding.controlRoot);
1892
+ },
1893
+ onFirstPublication: async () => {
1894
+ await markManagedEnrollmentEstablished(this.settings.agentDir, binding.controlRoot);
1895
+ },
1896
+ onFirstPublicationAborted: async () => {
1897
+ await markManagedEnrollmentPending(this.settings.agentDir, binding.controlRoot);
1898
+ },
1899
+ }
1900
+ : {}),
1901
+ },
1902
+ async state =>
1903
+ defineManagedTaskGraph(state, {
1904
+ id: input.graphId as string,
1905
+ owner: auth.ownerSessionId,
1906
+ nodes: input.nodes,
1907
+ }),
1908
+ );
1909
+ await markManagedEnrollmentEstablished(this.settings.agentDir, binding.controlRoot);
1910
+ return {
1911
+ ok: true,
1912
+ result: {
1913
+ ...this.#managedPublic(result.state.state_revision, result.state.graphs),
1914
+ graphId: result.result.id,
1915
+ },
1916
+ };
1917
+ } catch (caught) {
1918
+ return error("spawn_failed", caught instanceof Error ? caught.message : "managed define failed");
1919
+ }
1920
+ }
1921
+
1922
+ async #managedAdvance(input: Record<string, unknown>, idempotencyKey: string | undefined): Promise<BrokerResponse> {
1923
+ if (!idempotencyKey || idempotencyKey.length > 512)
1924
+ return error("invalid_input", "idempotencyKey is required for task.dag advance");
1925
+ if (
1926
+ typeof input.expectedRevision !== "number" ||
1927
+ !Number.isSafeInteger(input.expectedRevision) ||
1928
+ input.expectedRevision < 0
1929
+ )
1930
+ return error("invalid_input", "expectedRevision is required");
1931
+ if (typeof input.graphId !== "string" || typeof input.nodeId !== "string")
1932
+ return error("invalid_input", "graphId and nodeId are required");
1933
+ const maxAdmissions = input.maxAdmissions === undefined ? 1 : input.maxAdmissions;
1934
+ if (typeof maxAdmissions !== "number" || !Number.isSafeInteger(maxAdmissions) || maxAdmissions < 1)
1935
+ return error("invalid_input", "maxAdmissions is invalid");
1936
+ const auth = await this.#verifiedManagedOwner(input);
1937
+ if (auth.kind === "denied") return auth.response;
1938
+ try {
1939
+ const binding = await createManagedDomainBinding({
1940
+ controlRoot: auth.controlRoot,
1941
+ agentDir: this.settings.agentDir,
1942
+ enrollmentId: auth.enrollmentId,
1943
+ worktrees: auth.worktrees,
1944
+ ...(auth.aliases ? { aliases: auth.aliases } : {}),
1945
+ });
1946
+ const scopedIdentity = await deriveScopedIdempotencyIdentity(
1947
+ this.settings.agentDir,
1948
+ "session.spawn",
1949
+ idempotencyKey,
1950
+ binding.controlRoot,
1951
+ );
1952
+ const ordinaryAliases = await Promise.all([
1953
+ deriveIdempotencyIdentity(this.settings.agentDir, "session.spawn", idempotencyKey),
1954
+ deriveLegacyIdentity(this.settings.agentDir, "session.spawn", idempotencyKey),
1955
+ ]);
1956
+ const authority = this.#spawnAuthority;
1957
+ if (!authority) return error("unavailable", "spawn authority is unavailable");
1958
+ const requestHash = managedIdentity({
1959
+ graphId: input.graphId,
1960
+ nodeId: input.nodeId,
1961
+ ownerSessionId: auth.ownerSessionId,
1962
+ controlRoot: binding.controlRoot,
1963
+ });
1964
+ const active = this.#spawnInFlight.get(scopedIdentity);
1965
+ if (active) {
1966
+ if (active.managedRequestHash !== requestHash)
1967
+ return error("idempotency_conflict", "idempotency key conflicts with a live managed task.dag advance");
1968
+ return await active.completion;
1969
+ }
1970
+ const reserved = await transactManagedTaskDomain(
1971
+ { binding, expectedRevision: input.expectedRevision },
1972
+ async state => {
1973
+ const graph = state.graphs.find(item => item.id === input.graphId);
1974
+ if (!graph) throw new Error("graph owner mismatch");
1975
+ const node = graph.nodes.find(item => item.definition.id === input.nodeId);
1976
+ if (!node) throw new Error("node not ready");
1977
+ const workspace = path.resolve(node.definition.workspace);
1978
+ if (typeof input.cwd === "string" && input.cwd.length > 0 && path.resolve(input.cwd) !== workspace)
1979
+ throw new Error("cwd must match admitted workspace");
1980
+ const prior = state.graphs
1981
+ .flatMap(item => item.attempts)
1982
+ .find(
1983
+ attempt =>
1984
+ !attempt.retired &&
1985
+ attempt.native.key === idempotencyKey &&
1986
+ attempt.native.requestHash === requestHash &&
1987
+ attempt.nodeId === input.nodeId &&
1988
+ attempt.id === `attempt-${idempotencyKey}`,
1989
+ );
1990
+ const admitted = await observeOrAdmitManagedTask(state, {
1991
+ graphId: graph.id,
1992
+ owner: auth.ownerSessionId,
1993
+ nodeId: input.nodeId as string,
1994
+ attemptId: `attempt-${idempotencyKey}`,
1995
+ native: {
1996
+ key: idempotencyKey,
1997
+ identity: prior?.native.identity ?? scopedIdentity,
1998
+ requestHash,
1999
+ },
2000
+ });
2001
+ if (
2002
+ !(await authority.reserveManagedKeyAliases(
2003
+ ordinaryAliases,
2004
+ admitted.nativeIdentity,
2005
+ binding.controlRoot,
2006
+ ))
2007
+ )
2008
+ throw new Error("idempotency key conflicts with an existing ordinary session.spawn claim");
2009
+ return admitted;
2010
+ },
2011
+ );
2012
+ const attemptRef = reserved.result;
2013
+ if (
2014
+ !managedAttemptRefMatches(
2015
+ attemptRef,
2016
+ reserved.state.graphs.flatMap(g => g.attempts).find(a => a.id === attemptRef.attemptId)!,
2017
+ binding,
2018
+ )
2019
+ )
2020
+ return error("spawn_failed", "managed attempt ref mismatch");
2021
+ const activeAfterAdmission = this.#spawnInFlight.get(attemptRef.nativeIdentity);
2022
+ if (activeAfterAdmission) {
2023
+ if (activeAfterAdmission.managedRequestHash !== requestHash)
2024
+ return error("idempotency_conflict", "idempotency key conflicts with a live managed task.dag advance");
2025
+ return await activeAfterAdmission.completion;
2026
+ }
2027
+ const node = reserved.state.graphs
2028
+ .find(graph => graph.id === attemptRef.graphId)
2029
+ ?.nodes.find(item => item.definition.id === attemptRef.nodeId);
2030
+ const persisted = reserved.state.graphs
2031
+ .flatMap(graph => graph.attempts)
2032
+ ?.find(item => item.id === attemptRef.attemptId);
2033
+ if (!node || !persisted) return error("spawn_failed", "admitted node missing");
2034
+ this.#managedAttempts.set(attemptRef.nativeIdentity, attemptRef);
2035
+ const spawnAdmission: SpawnAdmissionInput = {
2036
+ task: node.definition.task,
2037
+ masterCapability: "",
2038
+ ownerSessionId: auth.ownerSessionId,
2039
+ attestationEpoch: auth.attestationEpoch,
2040
+ cwd: path.resolve(node.definition.workspace),
2041
+ managedAttempt: attemptRef,
2042
+ managedBinding: binding,
2043
+ managedNativeVector: managedNativeVector(persisted),
2044
+ };
2045
+ const completion = Promise.withResolvers<BrokerResponse>();
2046
+ const inFlight: SpawnInFlight = {
2047
+ completion: completion.promise,
2048
+ resolve: completion.resolve,
2049
+ managedRequestHash: requestHash,
2050
+ };
2051
+ this.#spawnInFlight.set(attemptRef.nativeIdentity, inFlight);
2052
+ this.#spawnTasks.set(inFlight, spawnAdmission.task);
2053
+ let becameOwner = false;
2054
+ let completionResult: BrokerResponse = error("spawn_failed", "managed advance did not complete");
2055
+ try {
2056
+ await recordManagedEnrollment(this.settings.agentDir, binding.controlRoot, attemptRef.nativeIdentity);
2057
+ const driven = await this.#driveVerifiedSpawn(
2058
+ attemptRef.nativeIdentity,
2059
+ spawnAdmission,
2060
+ inFlight,
2061
+ owner => {
2062
+ becameOwner = owner;
2063
+ },
2064
+ );
2065
+ completionResult = driven;
2066
+ if (!driven.ok) return driven;
2067
+ const liveRevision = await currentManagedRevision(binding);
2068
+ completionResult = {
2069
+ ok: true,
2070
+ result: {
2071
+ ...this.#managedPublic(liveRevision, reserved.state.graphs),
2072
+ attemptId: attemptRef.attemptId,
2073
+ nativeIdentity: attemptRef.nativeIdentity,
2074
+ spawn: driven.result,
2075
+ },
2076
+ };
2077
+ return completionResult;
2078
+ } catch (caught) {
2079
+ completionResult = error(
2080
+ "spawn_failed",
2081
+ caught instanceof Error ? caught.message : "managed advance failed",
2082
+ );
2083
+ throw caught;
2084
+ } finally {
2085
+ completion.resolve(completionResult);
2086
+ this.#spawnTasks.delete(inFlight);
2087
+ if (this.#spawnInFlight.get(attemptRef.nativeIdentity) === inFlight)
2088
+ this.#spawnInFlight.delete(attemptRef.nativeIdentity);
2089
+ if (becameOwner) await this.#spawnAuthority?.releaseOwner(attemptRef.nativeIdentity);
2090
+ }
2091
+ } catch (caught) {
2092
+ return error("spawn_failed", caught instanceof Error ? caught.message : "managed advance failed");
2093
+ }
2094
+ }
2095
+
2096
+ async #managedStatus(input: Record<string, unknown>): Promise<BrokerResponse> {
2097
+ const auth = await this.#verifiedManagedOwner(input);
2098
+ if (auth.kind === "denied") return auth.response;
2099
+ try {
2100
+ const binding = await createManagedDomainBinding({
2101
+ controlRoot: auth.controlRoot,
2102
+ agentDir: this.settings.agentDir,
2103
+ enrollmentId: auth.enrollmentId,
2104
+ worktrees: auth.worktrees,
2105
+ ...(auth.aliases ? { aliases: auth.aliases } : {}),
2106
+ });
2107
+ const target = managedTaskDomainPath(binding.controlRoot);
2108
+ const state = await withWorkflowStateLock(
2109
+ target,
2110
+ async () => {
2111
+ const read = await readExistingStateForMutation(target);
2112
+ if (read.kind !== "valid") throw new Error("established authority missing");
2113
+ const parsed = validateManagedTaskDomain(read.value);
2114
+ if (managedIdentity(parsed.binding) !== managedIdentity(binding))
2115
+ throw new Error("domain binding mismatch");
2116
+ return parsed;
2117
+ },
2118
+ { cwd: binding.controlRoot, privateDurable: { directory: path.dirname(target) } },
2119
+ );
2120
+ return { ok: true, result: this.#managedPublic(state.state_revision, state.graphs) };
2121
+ } catch (caught) {
2122
+ return error("spawn_failed", caught instanceof Error ? caught.message : "managed status failed");
2123
+ }
2124
+ }
2125
+
2126
+ async #managedRevise(input: Record<string, unknown>): Promise<BrokerResponse> {
2127
+ if (typeof input.expectedRevision !== "number" || !Number.isSafeInteger(input.expectedRevision))
2128
+ return error("invalid_input", "expectedRevision is required");
2129
+ if (typeof input.graphId !== "string") return error("invalid_input", "graphId is required");
2130
+ const auth = await this.#verifiedManagedOwner(input);
2131
+ if (auth.kind === "denied") return auth.response;
2132
+ if (this.#managedEnrollmentFailed || this.#managedFailedRoots.has(auth.controlRoot))
2133
+ return error("terminal_uncertain", "managed enrollment membership is unprovable; revision was not applied");
2134
+ try {
2135
+ const binding = await createManagedDomainBinding({
2136
+ controlRoot: auth.controlRoot,
2137
+ agentDir: this.settings.agentDir,
2138
+ enrollmentId: auth.enrollmentId,
2139
+ worktrees: auth.worktrees,
2140
+ ...(auth.aliases ? { aliases: auth.aliases } : {}),
2141
+ });
2142
+ const result = await transactManagedTaskDomain(
2143
+ { binding, expectedRevision: input.expectedRevision },
2144
+ async state => {
2145
+ const graph = state.graphs.find(item => item.id === input.graphId);
2146
+ if (!graph || graph.owner !== auth.ownerSessionId) throw new Error("graph owner mismatch");
2147
+ return reviseManagedTaskGraph(state, graph, input.nodes);
2148
+ },
2149
+ );
2150
+ if (!(await this.#closeManagedAttempts(binding.controlRoot, input.graphId as string, result.result)))
2151
+ return error("terminal_uncertain", "managed child close remains unresolved after graph revision");
2152
+ return {
2153
+ ok: true,
2154
+ result: {
2155
+ ...this.#managedPublic(result.state.state_revision, result.state.graphs),
2156
+ affected: result.result,
2157
+ },
2158
+ };
2159
+ } catch (caught) {
2160
+ return error("spawn_failed", caught instanceof Error ? caught.message : "managed revise failed");
2161
+ }
2162
+ }
2163
+
2164
+ async #managedCancel(input: Record<string, unknown>): Promise<BrokerResponse> {
2165
+ if (typeof input.expectedRevision !== "number" || !Number.isSafeInteger(input.expectedRevision))
2166
+ return error("invalid_input", "expectedRevision is required");
2167
+ if (typeof input.graphId !== "string") return error("invalid_input", "graphId is required");
2168
+ const ids = Array.isArray(input.nodeIds)
2169
+ ? input.nodeIds.filter((value): value is string => typeof value === "string")
2170
+ : [];
2171
+ if (ids.length === 0) return error("invalid_input", "nodeIds are required");
2172
+ const auth = await this.#verifiedManagedOwner(input);
2173
+ if (auth.kind === "denied") return auth.response;
2174
+ if (this.#managedEnrollmentFailed || this.#managedFailedRoots.has(auth.controlRoot))
2175
+ return error(
2176
+ "terminal_uncertain",
2177
+ "managed enrollment membership is unprovable; cancellation was not applied",
2178
+ );
2179
+ try {
2180
+ const binding = await createManagedDomainBinding({
2181
+ controlRoot: auth.controlRoot,
2182
+ agentDir: this.settings.agentDir,
2183
+ enrollmentId: auth.enrollmentId,
2184
+ worktrees: auth.worktrees,
2185
+ ...(auth.aliases ? { aliases: auth.aliases } : {}),
2186
+ });
2187
+ const result = await transactManagedTaskDomain(
2188
+ { binding, expectedRevision: input.expectedRevision },
2189
+ async state => {
2190
+ const graph = state.graphs.find(item => item.id === input.graphId);
2191
+ if (!graph || graph.owner !== auth.ownerSessionId) throw new Error("graph owner mismatch");
2192
+ return cancelManagedTasks(graph, ids);
2193
+ },
2194
+ );
2195
+ if (!(await this.#closeManagedAttempts(binding.controlRoot, input.graphId as string, result.result)))
2196
+ return error("terminal_uncertain", "managed child close remains unresolved after cancellation");
2197
+ return {
2198
+ ok: true,
2199
+ result: {
2200
+ ...this.#managedPublic(result.state.state_revision, result.state.graphs),
2201
+ affected: result.result,
2202
+ },
2203
+ };
2204
+ } catch (caught) {
2205
+ return error("spawn_failed", caught instanceof Error ? caught.message : "managed cancel failed");
2206
+ }
2207
+ }
2208
+ async #managedVerify(input: Record<string, unknown>): Promise<BrokerResponse> {
2209
+ if (typeof input.graphId !== "string" || typeof input.nodeId !== "string")
2210
+ return error("invalid_input", "graphId and nodeId are required");
2211
+ const auth = await this.#verifiedManagedOwner(input);
2212
+ if (auth.kind === "denied") return auth.response;
2213
+ try {
2214
+ const binding = await createManagedDomainBinding({
2215
+ controlRoot: auth.controlRoot,
2216
+ agentDir: this.settings.agentDir,
2217
+ enrollmentId: auth.enrollmentId,
2218
+ worktrees: auth.worktrees,
2219
+ ...(auth.aliases ? { aliases: auth.aliases } : {}),
2220
+ });
2221
+ const verified = await verifyManagedTaskAttempt({
2222
+ binding,
2223
+ graphId: input.graphId,
2224
+ nodeId: input.nodeId,
2225
+ owner: auth.ownerSessionId,
2226
+ });
2227
+ return { ok: true, result: { ...verified, commandsStarted: verified.started } };
2228
+ } catch (caught) {
2229
+ return error("spawn_failed", caught instanceof Error ? caught.message : "managed verify failed");
2230
+ }
2231
+ }
1530
2232
 
1531
2233
  /** Safe, allowlisted spawn result projection; never carries request material. */
1532
2234
  #spawnResult(code: "spawn_accepted" | "spawn_replayed", claim: SpawnClaimV2): Record<string, unknown> {
@@ -1560,6 +2262,53 @@ export class Broker {
1560
2262
  return error("resource_gone", "session.spawn claim is closed");
1561
2263
  }
1562
2264
 
2265
+ /** Re-prove an unbound launch on an idempotent retry instead of fencing it forever. */
2266
+ async #reconcileUncertainRegistration(lifecycleIdentity: string, claim: SpawnClaimV2): Promise<BrokerResponse> {
2267
+ const proof = claim.substrateProof;
2268
+ if (claim.state !== "uncertain" || claim.authorityRef !== undefined || !claim.childId || proof === undefined)
2269
+ return error("terminal_uncertain", "session.spawn registration authority is unavailable for reconciliation");
2270
+ const provider = this.#spawnSubstrateProvider();
2271
+ let verdict: "verified" | "mismatch" | "gone";
2272
+ try {
2273
+ verdict = await provider.verify(proof);
2274
+ } catch {
2275
+ return error("terminal_uncertain", "session.spawn registration proof could not be re-verified");
2276
+ }
2277
+ if (verdict === "mismatch")
2278
+ return error("terminal_uncertain", "session.spawn registration proof no longer matches the launch");
2279
+ if (verdict === "verified") {
2280
+ try {
2281
+ const closed = await provider.close(proof);
2282
+ if (!closed.ok)
2283
+ return error(
2284
+ "terminal_uncertain",
2285
+ "session.spawn child registration remains unresolved after the bounded close attempt",
2286
+ );
2287
+ } catch {
2288
+ return error("terminal_uncertain", "session.spawn child registration close could not be verified");
2289
+ }
2290
+ }
2291
+ const failure: SpawnSubstrateFailure = {
2292
+ substrateKind: proof.substrateKind,
2293
+ code: "child_registration_reconciled_failed",
2294
+ message:
2295
+ verdict === "gone"
2296
+ ? "session.spawn child host exited before registration"
2297
+ : "session.spawn child host was closed before registration",
2298
+ };
2299
+ try {
2300
+ await this.#spawnAuthority?.persistTransition(lifecycleIdentity, {
2301
+ claimId: claim.claimId,
2302
+ from: "uncertain",
2303
+ to: "pre_send_rejected",
2304
+ failure,
2305
+ });
2306
+ } catch {
2307
+ return error("terminal_uncertain", "session.spawn registration reconciliation could not be persisted");
2308
+ }
2309
+ return spawnFailureError(failure);
2310
+ }
2311
+
1563
2312
  #spawnSubstrateProvider(): SpawnSubstrateProvider {
1564
2313
  if (this.settings.spawnSubstrateProvider) return this.settings.spawnSubstrateProvider;
1565
2314
  this.settings.spawnSubstrateProvider = createSpawnSubstrateProvider();
@@ -1595,14 +2344,39 @@ export class Broker {
1595
2344
  ...(admission.modelId === undefined ? {} : { modelId: admission.modelId }),
1596
2345
  ...(admission.modelPreset === undefined ? {} : { modelPreset: admission.modelPreset }),
1597
2346
  });
1598
- current = (
1599
- await store.persistTransition(lifecycleIdentity, {
1600
- claimId: current.claimId,
1601
- from: "prepared",
1602
- to: "substrate_starting",
1603
- childId: prep.childId,
1604
- })
1605
- ).claim;
2347
+ if (admission.managedAttempt && admission.managedBinding && admission.managedNativeVector) {
2348
+ try {
2349
+ current = await withManagedNativeEffectAuthorized(
2350
+ admission.managedBinding,
2351
+ admission.managedAttempt.nativeIdentity,
2352
+ admission.managedNativeVector,
2353
+ "launch",
2354
+ async () =>
2355
+ (
2356
+ await store.persistTransition(lifecycleIdentity, {
2357
+ claimId: current.claimId,
2358
+ from: "prepared",
2359
+ to: "substrate_starting",
2360
+ childId: prep.childId,
2361
+ })
2362
+ ).claim,
2363
+ );
2364
+ } catch (caught) {
2365
+ return error(
2366
+ "spawn_failed",
2367
+ caught instanceof Error ? caught.message : "managed native fence denied",
2368
+ );
2369
+ }
2370
+ } else {
2371
+ current = (
2372
+ await store.persistTransition(lifecycleIdentity, {
2373
+ claimId: current.claimId,
2374
+ from: "prepared",
2375
+ to: "substrate_starting",
2376
+ childId: prep.childId,
2377
+ })
2378
+ ).claim;
2379
+ }
1606
2380
  inFlight.phase = current.state;
1607
2381
  const launched = await provider.launch({
1608
2382
  childSessionId: prep.childId,
@@ -1635,8 +2409,26 @@ export class Broker {
1635
2409
  code: "substrate_proof_failed",
1636
2410
  message: "session.spawn substrate lacks lifecycle process authority",
1637
2411
  };
1638
- await this.#releaseUnownedSubstrate(provider, launchedProof);
1639
- launchedProof = undefined;
2412
+ const release = await this.#releaseUnownedSubstrate(provider, launchedProof);
2413
+ if (release === "unresolved") {
2414
+ current = (
2415
+ await store.persistTransition(lifecycleIdentity, {
2416
+ claimId: current.claimId,
2417
+ from: "substrate_starting",
2418
+ to: "uncertain",
2419
+ failure: {
2420
+ substrateKind: launched.proof.substrateKind,
2421
+ code: "child_registration_release_unproven",
2422
+ message: "session.spawn child registration could not prove substrate release",
2423
+ },
2424
+ })
2425
+ ).claim;
2426
+ launchedProof = undefined;
2427
+ return error(
2428
+ "terminal_uncertain",
2429
+ "session.spawn substrate release could not be proven after the launch proof failed",
2430
+ );
2431
+ }
1640
2432
  current = (
1641
2433
  await store.persistTransition(lifecycleIdentity, {
1642
2434
  claimId: current.claimId,
@@ -1645,8 +2437,68 @@ export class Broker {
1645
2437
  failure,
1646
2438
  })
1647
2439
  ).claim;
2440
+ launchedProof = undefined;
1648
2441
  return spawnFailureError(failure);
1649
2442
  }
2443
+ current = (
2444
+ await store.persistTransition(lifecycleIdentity, {
2445
+ claimId: current.claimId,
2446
+ from: "substrate_starting",
2447
+ to: "substrate_starting",
2448
+ childId: prep.childId,
2449
+ substrateProof: launched.proof,
2450
+ })
2451
+ ).claim;
2452
+ inFlight.phase = current.state;
2453
+ if (admission.managedAttempt && admission.managedBinding) {
2454
+ let attempt: ManagedTaskAttempt | undefined;
2455
+ try {
2456
+ attempt = await inspectManagedAttemptByNativeIdentity(
2457
+ admission.managedBinding,
2458
+ admission.managedAttempt.nativeIdentity,
2459
+ );
2460
+ } catch {
2461
+ // A failed fence read cannot prove the launch is still authorized.
2462
+ }
2463
+ if (attempt?.fence !== "current" || attempt?.retired) {
2464
+ const release = await this.#releaseUnownedSubstrate(provider, launched.proof);
2465
+ let gone = release === "gone";
2466
+ if (release === "closed") {
2467
+ try {
2468
+ gone = (await provider.verify(launched.proof)) === "gone";
2469
+ } catch {
2470
+ gone = false;
2471
+ }
2472
+ }
2473
+ if (gone) {
2474
+ current = (
2475
+ await store.persistTransition(lifecycleIdentity, {
2476
+ claimId: current.claimId,
2477
+ from: "substrate_starting",
2478
+ to: "closed",
2479
+ })
2480
+ ).claim;
2481
+ inFlight.phase = current.state;
2482
+ launchedProof = undefined;
2483
+ return error("spawn_failed", "managed attempt was fenced while its child was launching");
2484
+ }
2485
+ current = (
2486
+ await store.persistTransition(lifecycleIdentity, {
2487
+ claimId: current.claimId,
2488
+ from: "substrate_starting",
2489
+ to: "uncertain",
2490
+ failure: {
2491
+ substrateKind: launched.proof.substrateKind,
2492
+ code: "managed_child_close_unresolved",
2493
+ message: "managed child close could not be proven after its attempt was fenced",
2494
+ },
2495
+ })
2496
+ ).claim;
2497
+ inFlight.phase = current.state;
2498
+ launchedProof = undefined;
2499
+ return error("terminal_uncertain", "managed child close is unresolved after its attempt was fenced");
2500
+ }
2501
+ }
1650
2502
  const marker = { pid, incarnation, effectMarker: prep.effectMarker };
1651
2503
  await writeEffectMarker(prep.stateRoot, prep.childId, marker);
1652
2504
  const registration = await this.#spawnPromptLayer.awaitRegistration({
@@ -1656,18 +2508,44 @@ export class Broker {
1656
2508
  });
1657
2509
  if (!registration.ok) {
1658
2510
  const startupFailure = await readSessionLifecycleFailure(prep.stateRoot, prep.childId, marker);
1659
- await this.#releaseUnownedSubstrate(provider, launchedProof);
1660
- launchedProof = undefined;
2511
+ const release = await this.#releaseUnownedSubstrate(provider, launchedProof);
2512
+ if (release === "closed" || release === "gone") {
2513
+ const failure: SpawnSubstrateFailure = {
2514
+ substrateKind: launched.proof.substrateKind,
2515
+ code: startupFailure === undefined ? "child_registration_timeout" : "child_registration_failed",
2516
+ message:
2517
+ startupFailure === undefined
2518
+ ? "session.spawn child host did not register before the bounded admission deadline"
2519
+ : `session.spawn child host failed during ${startupFailure.phase}/${startupFailure.reason}`,
2520
+ };
2521
+ current = (
2522
+ await store.persistTransition(lifecycleIdentity, {
2523
+ claimId: current.claimId,
2524
+ from: "substrate_starting",
2525
+ to: "pre_send_rejected",
2526
+ failure,
2527
+ })
2528
+ ).claim;
2529
+ launchedProof = undefined;
2530
+ return spawnFailureError(failure);
2531
+ }
2532
+ const unresolvedRelease: SpawnSubstrateFailure = {
2533
+ substrateKind: launched.proof.substrateKind,
2534
+ code: "child_registration_release_unproven",
2535
+ message: "session.spawn child registration could not prove substrate release",
2536
+ };
1661
2537
  current = (
1662
2538
  await store.persistTransition(lifecycleIdentity, {
1663
2539
  claimId: current.claimId,
1664
2540
  from: "substrate_starting",
1665
2541
  to: "uncertain",
2542
+ failure: unresolvedRelease,
1666
2543
  })
1667
2544
  ).claim;
2545
+ launchedProof = undefined;
1668
2546
  return error(
1669
2547
  "terminal_uncertain",
1670
- "session.spawn child registration is uncertain" +
2548
+ "session.spawn child registration is uncertain (substrate release could not be proven)" +
1671
2549
  (startupFailure ? ` (${startupFailure.phase}/${startupFailure.reason})` : ""),
1672
2550
  );
1673
2551
  }
@@ -1766,26 +2644,59 @@ export class Broker {
1766
2644
  }
1767
2645
  const childId = current.childId;
1768
2646
  const preparedSeed = current.seed;
2647
+ const preSendLeaseEpoch = current.preSendLease.epoch;
2648
+ const exactPinnedRegistration = pinnedRegistration;
1769
2649
  // Dispatch without a complete pin must never consume the pre-send lease.
1770
- if (pinnedRegistration === undefined)
2650
+ if (exactPinnedRegistration === undefined)
1771
2651
  return error("terminal_uncertain", "session.spawn endpoint pin is unavailable before dispatch");
1772
- current = (
1773
- await store.persistTransition(lifecycleIdentity, {
1774
- claimId: current.claimId,
1775
- from: "seed_prepared",
1776
- to: "dispatching",
1777
- leaseEpoch: current.preSendLease.epoch,
1778
- seed: { ...preparedSeed, phase: "dispatching" },
1779
- })
1780
- ).claim;
1781
- inFlight.phase = current.state;
1782
- handedOff = true;
1783
- const dispatched = await this.#spawnPromptLayer.dispatch({
1784
- sessionId: childId,
1785
- task: admission.task,
1786
- clientRef: preparedSeed.clientRef,
1787
- pinned: pinnedRegistration,
1788
- });
2652
+ const dispatchSeed = async (): Promise<SpawnPromptDispatch> =>
2653
+ await this.#spawnPromptLayer.dispatch({
2654
+ sessionId: childId,
2655
+ task: admission.task,
2656
+ clientRef: preparedSeed.clientRef,
2657
+ pinned: exactPinnedRegistration,
2658
+ });
2659
+ let dispatched: SpawnPromptDispatch;
2660
+ if (admission.managedAttempt && admission.managedBinding && admission.managedNativeVector) {
2661
+ try {
2662
+ dispatched = await withManagedNativeEffectAuthorized(
2663
+ admission.managedBinding,
2664
+ admission.managedAttempt.nativeIdentity,
2665
+ admission.managedNativeVector,
2666
+ "seed",
2667
+ async () => {
2668
+ current = (
2669
+ await store.persistTransition(lifecycleIdentity, {
2670
+ claimId: current.claimId,
2671
+ from: "seed_prepared",
2672
+ to: "dispatching",
2673
+ leaseEpoch: preSendLeaseEpoch,
2674
+ seed: { ...preparedSeed, phase: "dispatching" },
2675
+ })
2676
+ ).claim;
2677
+ inFlight.phase = current.state;
2678
+ handedOff = true;
2679
+ return await dispatchSeed();
2680
+ },
2681
+ );
2682
+ } catch (caught) {
2683
+ if (handedOff) throw caught;
2684
+ return error("spawn_failed", caught instanceof Error ? caught.message : "managed native fence denied");
2685
+ }
2686
+ } else {
2687
+ current = (
2688
+ await store.persistTransition(lifecycleIdentity, {
2689
+ claimId: current.claimId,
2690
+ from: "seed_prepared",
2691
+ to: "dispatching",
2692
+ leaseEpoch: current.preSendLease.epoch,
2693
+ seed: { ...preparedSeed, phase: "dispatching" },
2694
+ })
2695
+ ).claim;
2696
+ inFlight.phase = current.state;
2697
+ handedOff = true;
2698
+ dispatched = await dispatchSeed();
2699
+ }
1789
2700
  if (dispatched.kind === "accepted") {
1790
2701
  current = (
1791
2702
  await store.persistTransition(lifecycleIdentity, {
@@ -1833,15 +2744,76 @@ export class Broker {
1833
2744
  } catch {
1834
2745
  // Once a substrate exists the outcome is ambiguous even before handoff:
1835
2746
  // reporting an ordinary failure would downgrade retained uncertainty.
1836
- const ambiguous = handedOff || launchedProof !== undefined;
2747
+ let ambiguous = handedOff || launchedProof !== undefined;
1837
2748
  // A launched substrate with no persisted authority is invisible to the
1838
2749
  // reaper, so in-process cleanup is its only chance. This must run on
1839
2750
  // EVERY post-launch failure exit, not just a returned registration
1840
2751
  // failure: a throw from awaitRegistration, verify, close, or a durable
1841
2752
  // transition all land here.
1842
- if (!handedOff) await this.#releaseUnownedSubstrate(provider, launchedProof);
2753
+ if (!handedOff) {
2754
+ const release = await this.#releaseUnownedSubstrate(provider, launchedProof);
2755
+ if ((release === "closed" || release === "gone") && current.state === "substrate_starting") {
2756
+ const failure: SpawnSubstrateFailure = {
2757
+ substrateKind: launchedProof?.substrateKind ?? "headless",
2758
+ code: "child_registration_failed",
2759
+ message: "session.spawn child host failed before registration",
2760
+ };
2761
+ try {
2762
+ await store.persistTransition(lifecycleIdentity, {
2763
+ claimId: current.claimId,
2764
+ from: "substrate_starting",
2765
+ to: "pre_send_rejected",
2766
+ failure,
2767
+ });
2768
+ launchedProof = undefined;
2769
+ return spawnFailureError(failure);
2770
+ } catch {
2771
+ // The substrate is closed, but a failed terminal write still leaves
2772
+ // the durable claim unresolved for the next broker recovery pass.
2773
+ }
2774
+ } else if (release === "unresolved" && current.state === "substrate_starting") {
2775
+ try {
2776
+ current = (
2777
+ await store.persistTransition(lifecycleIdentity, {
2778
+ claimId: current.claimId,
2779
+ from: "substrate_starting",
2780
+ to: "uncertain",
2781
+ failure: {
2782
+ substrateKind: launchedProof?.substrateKind ?? "headless",
2783
+ code: "child_registration_release_unproven",
2784
+ message: "session.spawn child registration could not prove substrate release",
2785
+ },
2786
+ })
2787
+ ).claim;
2788
+ launchedProof = undefined;
2789
+ } catch {
2790
+ // Preserve the generic durable-state uncertainty if the reason itself
2791
+ // cannot be appended.
2792
+ }
2793
+ } else if (release === "absent" && current.state === "substrate_starting") {
2794
+ try {
2795
+ current = (
2796
+ await store.persistTransition(lifecycleIdentity, {
2797
+ claimId: current.claimId,
2798
+ from: "substrate_starting",
2799
+ to: "uncertain",
2800
+ })
2801
+ ).claim;
2802
+ ambiguous = true;
2803
+ } catch {
2804
+ // Keep the generic failure when even the uncertainty transition cannot be written.
2805
+ }
2806
+ }
2807
+ }
2808
+ if (ambiguous && admission.managedBinding)
2809
+ await recordManagedNativeObservation(admission.managedBinding, lifecycleIdentity, "unknown").catch(
2810
+ () => undefined,
2811
+ );
1843
2812
  return ambiguous
1844
- ? error("terminal_uncertain", "session.spawn state could not be advanced durably")
2813
+ ? error(
2814
+ "terminal_uncertain",
2815
+ current.failure?.message ?? "session.spawn state could not be advanced durably",
2816
+ )
1845
2817
  : error("spawn_failed", "session.spawn could not be advanced durably");
1846
2818
  }
1847
2819
  }
@@ -1855,22 +2827,26 @@ export class Broker {
1855
2827
  async #releaseUnownedSubstrate(
1856
2828
  provider: SpawnSubstrateProvider,
1857
2829
  proof: SpawnSubstrateProof | undefined,
1858
- ): Promise<void> {
1859
- if (!proof) return;
2830
+ ): Promise<"closed" | "gone" | "absent" | "unresolved"> {
2831
+ // A launch that throws before returning proof has no substrate identity to
2832
+ // classify as gone; keep the claim uncertain instead of fabricating a kind.
2833
+ if (!proof) return "absent";
1860
2834
  let verdict: "verified" | "mismatch" | "gone";
1861
2835
  try {
1862
2836
  verdict = await provider.verify(proof);
1863
2837
  } catch {
1864
2838
  // An unprovable substrate is never mutated; uncertainty is retained by
1865
2839
  // the caller's durable transition instead.
1866
- return;
2840
+ return "unresolved";
1867
2841
  }
1868
- if (verdict !== "verified") return;
2842
+ if (verdict === "gone") return "gone";
2843
+ if (verdict !== "verified") return "unresolved";
1869
2844
  try {
1870
- await provider.close(proof);
2845
+ return (await provider.close(proof)).ok ? "closed" : "unresolved";
1871
2846
  } catch {
1872
2847
  // The substrate may survive. The caller still records uncertainty, which
1873
2848
  // is the honest durable outcome for an unclosed unowned substrate.
2849
+ return "unresolved";
1874
2850
  }
1875
2851
  }
1876
2852
 
@@ -2032,6 +3008,148 @@ export class Broker {
2032
3008
  }
2033
3009
  }
2034
3010
 
3011
+ async #restoreManagedAttempts(): Promise<void> {
3012
+ this.#managedAttempts.clear();
3013
+ this.#managedFailedRoots.clear();
3014
+ this.#managedEnrollmentFailed = false;
3015
+ try {
3016
+ const restored = await restoreManagedAttemptRefs(this.settings.agentDir);
3017
+ for (const ref of restored.refs) this.#managedAttempts.set(ref.nativeIdentity, ref);
3018
+ for (const root of restored.failedRoots) this.#managedFailedRoots.add(root);
3019
+ } catch {
3020
+ this.#managedEnrollmentFailed = true;
3021
+ }
3022
+ }
3023
+
3024
+ async #observeManagedNative(
3025
+ lifecycleIdentity: string,
3026
+ worker: "no-effect" | "authorized" | "unknown" | "closed",
3027
+ ): Promise<void> {
3028
+ const ref = this.#managedAttempts.get(lifecycleIdentity);
3029
+ if (!ref) return;
3030
+ const binding = await loadManagedDomainBinding(ref.controlRoot);
3031
+ if (!binding) return;
3032
+ await recordManagedNativeObservation(binding, lifecycleIdentity, worker);
3033
+ }
3034
+
3035
+ async #reconcileManagedNativeLifetimes(): Promise<void> {
3036
+ const store = this.#spawnAuthority;
3037
+ if (!store || this.#managedEnrollmentFailed) return;
3038
+ for (const ref of this.#managedAttempts.values()) {
3039
+ if (this.#managedFailedRoots.has(ref.controlRoot)) continue;
3040
+ const binding = await loadManagedDomainBinding(ref.controlRoot);
3041
+ if (!binding) throw new Error("managed attempt domain is unavailable during startup reconciliation");
3042
+ const attempt = await inspectManagedAttemptByNativeIdentity(binding, ref.nativeIdentity);
3043
+ if (!attempt) throw new Error("managed attempt authority is missing during startup reconciliation");
3044
+ const claim = store.claim(ref.nativeIdentity);
3045
+ if (attempt.fence !== "current" || attempt.retired) {
3046
+ if (!claim) {
3047
+ await this.#observeManagedNative(ref.nativeIdentity, "no-effect");
3048
+ continue;
3049
+ }
3050
+ if (claim.state === "prepared") {
3051
+ await store.persistTransition(ref.nativeIdentity, {
3052
+ claimId: claim.claimId,
3053
+ from: "prepared",
3054
+ to: "closed",
3055
+ });
3056
+ await this.#observeManagedNative(ref.nativeIdentity, "no-effect");
3057
+ continue;
3058
+ }
3059
+ if (claim.state === "closed" && !store.authority(ref.nativeIdentity) && !claim.substrateProof) {
3060
+ await this.#observeManagedNative(ref.nativeIdentity, "no-effect");
3061
+ continue;
3062
+ }
3063
+ if (claim.state === "pre_send_rejected" && !store.authority(ref.nativeIdentity) && !claim.substrateProof) {
3064
+ await this.#observeManagedNative(ref.nativeIdentity, "no-effect");
3065
+ continue;
3066
+ }
3067
+ const outcome = await this.#closeSpawnAuthority(ref.nativeIdentity);
3068
+ if (outcome !== "closed") {
3069
+ await this.#observeManagedNative(ref.nativeIdentity, "unknown").catch(() => undefined);
3070
+ throw new Error("fenced managed child could not be exactly closed during startup reconciliation");
3071
+ }
3072
+ await this.#observeManagedNative(ref.nativeIdentity, "closed");
3073
+ continue;
3074
+ }
3075
+ if (!claim) {
3076
+ await this.#observeManagedNative(ref.nativeIdentity, "no-effect");
3077
+ continue;
3078
+ }
3079
+ if (claim.state === "uncertain" || claim.state === "dispatching" || claim.state === "substrate_starting") {
3080
+ await this.#observeManagedNative(ref.nativeIdentity, "unknown");
3081
+ continue;
3082
+ }
3083
+ if (claim.state === "closed") {
3084
+ const authority = store.authority(ref.nativeIdentity);
3085
+ if (authority?.closeState === "closed" || (!authority && claim.substrateProof))
3086
+ await this.#observeManagedNative(ref.nativeIdentity, "closed");
3087
+ else if (!authority) await this.#observeManagedNative(ref.nativeIdentity, "no-effect");
3088
+ else await this.#observeManagedNative(ref.nativeIdentity, "unknown");
3089
+ continue;
3090
+ }
3091
+ if (claim.state === "pre_send_rejected") {
3092
+ const authority = store.authority(ref.nativeIdentity);
3093
+ if (!authority && claim.substrateProof) {
3094
+ const outcome = await this.#closeSpawnAuthority(ref.nativeIdentity);
3095
+ await this.#observeManagedNative(ref.nativeIdentity, outcome === "closed" ? "closed" : "unknown");
3096
+ } else if (!authority) await this.#observeManagedNative(ref.nativeIdentity, "no-effect");
3097
+ else await this.#observeManagedNative(ref.nativeIdentity, "unknown");
3098
+ continue;
3099
+ }
3100
+ if (claim.state === "prepared") continue;
3101
+ await this.#observeManagedNative(ref.nativeIdentity, "authorized");
3102
+ }
3103
+ }
3104
+
3105
+ async #closeManagedAttempts(controlRoot: string, graphId: string, nodeIds: string[]): Promise<boolean> {
3106
+ const affected = [...this.#managedAttempts.values()].filter(
3107
+ ref => ref.controlRoot === controlRoot && ref.graphId === graphId && nodeIds.includes(ref.nodeId),
3108
+ );
3109
+ let allClosed = true;
3110
+ for (const ref of affected) {
3111
+ const inFlight = this.#spawnInFlight.get(ref.nativeIdentity);
3112
+ const waitMs = managedCloseWaitOverridesForTest.get(this) ?? MANAGED_CLOSE_WAIT_MS;
3113
+ if (inFlight && !(await settlesWithin(inFlight.completion, waitMs))) {
3114
+ this.#scheduleManagedCloseAfterFlight(ref, inFlight);
3115
+ await this.#observeManagedNative(ref.nativeIdentity, "unknown").catch(() => undefined);
3116
+ allClosed = false;
3117
+ continue;
3118
+ }
3119
+ const claim = this.#spawnAuthority?.claim(ref.nativeIdentity);
3120
+ const authority = this.#spawnAuthority?.authority(ref.nativeIdentity);
3121
+ if (!claim || (claim.state === "pre_send_rejected" && !authority && !claim.substrateProof)) {
3122
+ try {
3123
+ await this.#observeManagedNative(ref.nativeIdentity, "no-effect");
3124
+ } catch {
3125
+ await this.#observeManagedNative(ref.nativeIdentity, "unknown").catch(() => undefined);
3126
+ allClosed = false;
3127
+ }
3128
+ continue;
3129
+ }
3130
+ const outcome = await this.#closeSpawnAuthority(ref.nativeIdentity);
3131
+ if (outcome === "closed") await this.#observeManagedNative(ref.nativeIdentity, "closed");
3132
+ else {
3133
+ await this.#observeManagedNative(ref.nativeIdentity, "unknown").catch(() => undefined);
3134
+ allClosed = false;
3135
+ }
3136
+ }
3137
+ return allClosed;
3138
+ }
3139
+
3140
+ #scheduleManagedCloseAfterFlight(ref: ManagedAttemptRef, inFlight: SpawnInFlight): void {
3141
+ void inFlight.completion
3142
+ .then(async () => {
3143
+ const outcome = await this.#closeSpawnAuthority(ref.nativeIdentity);
3144
+ if (outcome === "closed") await this.#observeManagedNative(ref.nativeIdentity, "closed");
3145
+ else await this.#observeManagedNative(ref.nativeIdentity, "unknown");
3146
+ })
3147
+ .catch(async caught => {
3148
+ logger.warn(`sdk broker: deferred managed child close failed for ${ref.nativeIdentity}: ${String(caught)}`);
3149
+ await this.#observeManagedNative(ref.nativeIdentity, "unknown").catch(() => undefined);
3150
+ });
3151
+ }
3152
+
2035
3153
  async #awaitSpawnHostRegistration(input: {
2036
3154
  childId: string;
2037
3155
  cwd: string;
@@ -2043,21 +3161,9 @@ export class Broker {
2043
3161
  await this.index.refresh();
2044
3162
  // The launch locator is authority: a same-id row registered by an
2045
3163
  // unrelated workspace must never be adopted as this spawn's child.
2046
- const row = this.index.listSessionIdentities().find(
2047
- candidate =>
2048
- candidate.sessionId === input.childId &&
2049
- candidate.endpointGeneration > 0 &&
2050
- candidate.live &&
2051
- !candidate.terminal &&
2052
- !candidate.terminalUncertain &&
2053
- // Path IDENTITY, not spelling. The child publishes a
2054
- // realpath-canonicalized locator while the launch spec carries the
2055
- // caller's lexical path, so `path.resolve` never reconciles a
2056
- // symlinked workspace (macOS /var vs /private/var) and a
2057
- // legitimate child would never match.
2058
- resolveEquivalentPath(candidate.locator.cwd) === resolveEquivalentPath(input.cwd) &&
2059
- resolveEquivalentPath(candidate.locator.stateRoot) === resolveEquivalentPath(input.stateRoot),
2060
- );
3164
+ const row = this.index
3165
+ .listSessionIdentities()
3166
+ .find(candidate => spawnRegistrationMatches(candidate, input));
2061
3167
  if (row) {
2062
3168
  const incarnation = row.hostIncarnation ?? row.processIncarnation;
2063
3169
  // An incarnation-less row is incomplete endpoint evidence; a partial pin
@@ -2235,7 +3341,10 @@ export class Broker {
2235
3341
  const claim = store.claims().find(candidate => candidate.childId === sessionId);
2236
3342
  if (!claim) return undefined;
2237
3343
  const outcome = await this.#closeSpawnAuthority(claim.lifecycleIdentity);
2238
- if (outcome === "closed") return { ok: true, result: { code: "spawn_child_closed", sessionId } };
3344
+ if (outcome === "closed") {
3345
+ await this.#observeManagedNative(claim.lifecycleIdentity, "closed").catch(() => undefined);
3346
+ return { ok: true, result: { code: "spawn_child_closed", sessionId } };
3347
+ }
2239
3348
  if (outcome === "uncertain")
2240
3349
  return error("terminal_uncertain", "session.close substrate identity could not be re-proven");
2241
3350
  return error("close_refused", "session.close could not complete for the spawned child");
@@ -2252,8 +3361,47 @@ export class Broker {
2252
3361
  try {
2253
3362
  const claim = store.claim(lifecycleIdentity);
2254
3363
  let authority = store.authority(lifecycleIdentity);
2255
- if (!claim || !authority) return "retained";
2256
- if (claim.state === "closed" && authority.closeState === "closed") return "closed";
3364
+ if (!claim) return "retained";
3365
+ if (claim.state === "closed" && (!authority || authority.closeState === "closed")) return "closed";
3366
+ if (!authority) {
3367
+ if (!claim.substrateProof || !claim.childId) return "retained";
3368
+ const provider = this.#spawnSubstrateProvider();
3369
+ const proof = claim.substrateProof;
3370
+ let verdict: "verified" | "mismatch" | "gone";
3371
+ try {
3372
+ verdict = await provider.verify(proof);
3373
+ } catch {
3374
+ verdict = "mismatch";
3375
+ }
3376
+ let gone = verdict === "gone";
3377
+ if (verdict === "verified") {
3378
+ try {
3379
+ gone = (await provider.close(proof)).ok && (await provider.verify(proof)) === "gone";
3380
+ } catch {
3381
+ gone = false;
3382
+ }
3383
+ }
3384
+ if (!gone) {
3385
+ if (claim.state !== "uncertain")
3386
+ await store.persistTransition(lifecycleIdentity, {
3387
+ claimId: claim.claimId,
3388
+ from: claim.state,
3389
+ to: "uncertain",
3390
+ failure: {
3391
+ substrateKind: proof.substrateKind,
3392
+ code: "child_close_unresolved",
3393
+ message: "session.spawn child close could not be proven",
3394
+ },
3395
+ });
3396
+ return verdict === "mismatch" ? "uncertain" : "retained";
3397
+ }
3398
+ await store.persistTransition(lifecycleIdentity, {
3399
+ claimId: claim.claimId,
3400
+ from: claim.state,
3401
+ to: "closed",
3402
+ });
3403
+ return "closed";
3404
+ }
2257
3405
  if (authority.closeState === "closed") return "closed";
2258
3406
  const provider = this.#spawnSubstrateProvider();
2259
3407
  const proof = spawnProofFromAuthority(authority);
@@ -2280,13 +3428,11 @@ export class Broker {
2280
3428
  })
2281
3429
  ).authority!;
2282
3430
  }
2283
- if (verdict === "verified") {
3431
+ if (verdict === "verified" || verdict === "gone") {
2284
3432
  const closedSubstrate = await provider.close(proof);
2285
3433
  if (!closedSubstrate.ok) return "retained";
2286
- // Signal delivery is not terminal evidence: only a second exact proof
2287
- // that observes this substrate gone permits the durable close.
2288
3434
  if ((await provider.verify(proof)) !== "gone") return "retained";
2289
- }
3435
+ } else return "retained";
2290
3436
  const at = Math.max(Date.now(), authority.updatedAt + 1);
2291
3437
  const closedAuthority: SpawnAuthorityV1 = { ...authority, closeState: "closed", closedAt: at, updatedAt: at };
2292
3438
  const currentClaim = store.claim(lifecycleIdentity);
@@ -2617,7 +3763,20 @@ export class Broker {
2617
3763
  else if (mirror.requestHash !== claim.bindingMac)
2618
3764
  throw new Error("Spawn claim lifecycle mirror binding differs from durable authority.");
2619
3765
  }
3766
+ await this.#restoreManagedAttempts();
3767
+ if (this.#managedEnrollmentFailed || this.#managedFailedRoots.size > 0)
3768
+ throw new Error("Broker cannot establish complete managed enrollment membership.");
2620
3769
  await this.#recoverSpawnClaims();
3770
+ await this.#reconcileManagedNativeLifetimes();
3771
+ const reconciledRoots = new Set<string>();
3772
+ for (const ref of this.#managedAttempts.values()) {
3773
+ if (this.#managedFailedRoots.has(ref.controlRoot) || reconciledRoots.has(ref.controlRoot)) continue;
3774
+ reconciledRoots.add(ref.controlRoot);
3775
+ try {
3776
+ const binding = await loadManagedDomainBinding(ref.controlRoot);
3777
+ if (binding) await reconcileRunningManagedVerification(binding);
3778
+ } catch {}
3779
+ }
2621
3780
  const now = Date.now();
2622
3781
  const incarnation = brokerProcessIncarnation(process.pid);
2623
3782
  if (!incarnation) throw new Error("Broker process incarnation is unavailable.");
@@ -3390,7 +4549,9 @@ export class Broker {
3390
4549
  return error("invalid_input", "session.spawn does not support lifecycle lookup");
3391
4550
  if (!LIFECYCLE_OPERATIONS.has(requestedOperation)) return error("not_found", "lifecycle operation was not found");
3392
4551
  const identity = await deriveIdempotencyIdentity(this.settings.agentDir, requestedOperation, idempotencyKey);
3393
- const entry = this.ledger.get(identity);
4552
+ const entry =
4553
+ this.ledger.get(identity) ??
4554
+ this.ledger.findByOperationKey(`${requestedOperation}\0${idempotencyKey}`, requestedFingerprint);
3394
4555
  if (!entry) return error("not_found", "lifecycle operation was not found");
3395
4556
  if (entry.fingerprint !== requestedFingerprint)
3396
4557
  return error("idempotency_conflict", "lifecycle request fingerprint differs");
@@ -3436,6 +4597,7 @@ export class Broker {
3436
4597
  // Spawn bypasses generic raw-input transforms. They must never observe task
3437
4598
  // or capability fields, even transiently.
3438
4599
  if (operation === "session.spawn") return this.#handleSpawn(input, idempotencyKey);
4600
+ if (operation === "task.dag") return this.#handleManagedTaskDag(input, idempotencyKey);
3439
4601
  if (operation === "session.close") {
3440
4602
  const spawnClose = await this.#maybeCloseSpawnChild(input);
3441
4603
  if (spawnClose) return spawnClose;
@@ -3450,10 +4612,10 @@ export class Broker {
3450
4612
  lifecycleFingerprint(lookup.operation, lookup.target),
3451
4613
  );
3452
4614
  }
3453
- const fingerprint = lifecycleFingerprint(operation, input);
3454
4615
  const normalization = normalizeBrokerInput(operation, input);
3455
4616
  if (isBrokerResponse(normalization)) return normalization;
3456
4617
  input = normalization.input;
4618
+ const fingerprint = lifecycleFingerprint(operation, input);
3457
4619
  if (operation === "session.list") {
3458
4620
  if (input.cursor === undefined) await this.index.refresh();
3459
4621
  const page = await this.#sessionListPage(input, this.index.listSessions());
@@ -3523,8 +4685,64 @@ export class Broker {
3523
4685
  const target = createHash("sha256")
3524
4686
  .update(canonicalJson(lifecycleTarget(operation, input)))
3525
4687
  .digest("hex");
3526
- const identity = await deriveIdempotencyIdentity(this.settings.agentDir, operation, idempotencyKey, target);
4688
+ const identity = await deriveIdempotencyIdentity(
4689
+ this.settings.agentDir,
4690
+ operation,
4691
+ idempotencyKey,
4692
+ operation === "session.create" ? undefined : target,
4693
+ );
3527
4694
  const operationKey = `${operation}\0${idempotencyKey}`;
4695
+ const requestedRequestHash = createHash("sha256")
4696
+ .update(canonicalJson({ operation, input: lifecycleRequestIdentity(input) }))
4697
+ .digest("hex");
4698
+ const legacyIdentity = await deriveLegacyIdentity(this.settings.agentDir, operation, idempotencyKey);
4699
+ const legacyTargetIdentity = await deriveLegacyTargetIdentity(
4700
+ this.settings.agentDir,
4701
+ operation,
4702
+ idempotencyKey,
4703
+ target,
4704
+ );
4705
+ const metadata = {
4706
+ operationKey,
4707
+ fingerprint,
4708
+ ...(operation === "session.close" && typeof input.sessionId === "string"
4709
+ ? { intendedSessionId: input.sessionId }
4710
+ : {}),
4711
+ };
4712
+ if (!this.ledger.get(identity)) {
4713
+ const matchingOperation = this.ledger.findAnyByOperationKey(operationKey);
4714
+ const sameCloseSession =
4715
+ operation === "session.close" &&
4716
+ (matchingOperation?.intendedSessionId === input.sessionId ||
4717
+ matchingOperation?.resultSessionId === input.sessionId);
4718
+ if (
4719
+ matchingOperation &&
4720
+ !sameCloseSession &&
4721
+ (matchingOperation.fingerprint !== fingerprint || matchingOperation.requestHash !== requestedRequestHash)
4722
+ )
4723
+ return error("idempotency_conflict", "idempotency key was used with a different request");
4724
+ const matchingLegacy = matchingOperation;
4725
+ if (matchingLegacy) {
4726
+ if (matchingLegacy.identity === legacyIdentity || matchingLegacy.identity === legacyTargetIdentity)
4727
+ await this.ledger.migrateIdentity(matchingLegacy.identity, identity, metadata);
4728
+ } else if (this.ledger.get(legacyIdentity)) {
4729
+ return error("idempotency_conflict", "idempotency key was used with a different request");
4730
+ } else {
4731
+ const legacyTargetEntry = this.ledger.get(legacyTargetIdentity);
4732
+ if (legacyTargetEntry) {
4733
+ if (legacyTargetEntry.requestHash !== requestedRequestHash)
4734
+ return error("idempotency_conflict", "idempotency key was used with a different request");
4735
+ await this.ledger.migrateIdentity(legacyTargetIdentity, identity, metadata);
4736
+ // Exact target-derived legacy identity migrated without granting new authority.
4737
+ } else if (
4738
+ this.ledger.hasLegacyIdentity(
4739
+ new Set(this.#spawnAuthority?.claims().map(claim => claim.lifecycleIdentity) ?? []),
4740
+ )
4741
+ ) {
4742
+ return error("idempotency_conflict", "legacy lifecycle request has an ambiguous target");
4743
+ }
4744
+ }
4745
+ }
3528
4746
 
3529
4747
  let reconstructedDeleteCleanup: BrokerCleanupEvidence | undefined;
3530
4748
  if (operation === "session.delete" && input.cwd === undefined && input.sessionPath === undefined) {
@@ -3594,7 +4812,7 @@ export class Broker {
3594
4812
  await prev;
3595
4813
  try {
3596
4814
  const beforeBegin = this.ledger.get(identity);
3597
- const begun = await this.ledger.begin(identity, requestHash, { operationKey, fingerprint });
4815
+ const begun = await this.ledger.begin(identity, requestHash, metadata);
3598
4816
  if (begun.kind === "replay") {
3599
4817
  const replay = begun.entry.response as BrokerResponse;
3600
4818
  const cleanup = cleanupFromResponse(replay) ?? reconstructedDeleteCleanup;
@@ -3710,12 +4928,21 @@ export class Broker {
3710
4928
  });
3711
4929
  if (isCleanupPending(response)) return response;
3712
4930
  const persisted = await this.ledger.readTerminal(identity, requestHash);
3713
- const persistenceVerified =
3714
- persisted !== undefined &&
3715
- canonicalJson(persisted.response) === canonicalJson(storedResponse) &&
3716
- canonicalJson(persisted.durableEffects) === canonicalJson(outcome.durableEffects) &&
3717
- canonicalJson(persisted.startupFailure) === canonicalJson(outcome.startupFailure);
3718
- if (!persistenceVerified) {
4931
+ const persistenceVerification = verifyTerminalPersistence(
4932
+ persisted,
4933
+ storedResponse,
4934
+ outcome.durableEffects,
4935
+ outcome.startupFailure,
4936
+ );
4937
+ if (persistenceVerification.kind === "unverified") {
4938
+ logger.warn("sdk broker terminal persistence read-back was unavailable; preserving settled outcome", {
4939
+ identity,
4940
+ });
4941
+ } else if (persistenceVerification.kind === "uncertain") {
4942
+ logger.warn("sdk broker terminal persistence verification found conflicting evidence", {
4943
+ identity,
4944
+ mismatches: persistenceVerification.mismatches,
4945
+ });
3719
4946
  const uncertain = error(
3720
4947
  "terminal_uncertain",
3721
4948
  "Lifecycle terminal evidence could not be verified after persistence; retained artifacts require reconciliation.",
@@ -3762,6 +4989,12 @@ export function setLivenessGraceForTest(broker: Broker, graceMs: number | undefi
3762
4989
  else livenessGraceOverridesForTest.set(broker, graceMs);
3763
4990
  }
3764
4991
 
4992
+ /** Test-only hook for shortening the bounded wait before a deferred managed close is queued. */
4993
+ export function setManagedCloseWaitForTest(broker: Broker, waitMs: number | undefined): void {
4994
+ if (waitMs === undefined) managedCloseWaitOverridesForTest.delete(broker);
4995
+ else managedCloseWaitOverridesForTest.set(broker, Math.max(0, waitMs));
4996
+ }
4997
+
3765
4998
  /**
3766
4999
  * Test-only hook for stalling the heartbeat write, reproducing a publication tick
3767
5000
  * whose awaited IO does not settle. Clearing it releases the stalled tick the way