@gajae-code/coding-agent 0.14.2 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (345) hide show
  1. package/CHANGELOG.md +120 -1
  2. package/dist/types/async/job-manager.d.ts +4 -1
  3. package/dist/types/autoresearch/dashboard.d.ts +29 -4
  4. package/dist/types/autoresearch/data-context.d.ts +25 -0
  5. package/dist/types/autoresearch/git.d.ts +65 -7
  6. package/dist/types/autoresearch/harness.d.ts +58 -0
  7. package/dist/types/autoresearch/prompts.d.ts +51 -0
  8. package/dist/types/autoresearch/runs.d.ts +173 -0
  9. package/dist/types/autoresearch/types.d.ts +9 -139
  10. package/dist/types/capability/types.d.ts +20 -0
  11. package/dist/types/cli/accounts-cli.d.ts +1 -0
  12. package/dist/types/commands/{rlm.d.ts → autoresearch.d.ts} +2 -4
  13. package/dist/types/config/autorouting-contract.d.ts +146 -0
  14. package/dist/types/config/autorouting-generator.d.ts +40 -0
  15. package/dist/types/config/autorouting-tier-map.d.ts +16844 -0
  16. package/dist/types/config/autorouting.d.ts +35 -0
  17. package/dist/types/config/model-discovery-manager.d.ts +8 -1
  18. package/dist/types/config/model-registry.d.ts +30 -1
  19. package/dist/types/config/provider-selection-policy.d.ts +23 -0
  20. package/dist/types/config/settings-schema.d.ts +61 -3
  21. package/dist/types/config/settings.d.ts +3 -0
  22. package/dist/types/coordinator/contract.d.ts +1 -1
  23. package/dist/types/coordinator-mcp/durability.d.ts +29 -0
  24. package/dist/types/coordinator-mcp/event-webhook.d.ts +50 -0
  25. package/dist/types/coordinator-mcp/question-state.d.ts +1 -1
  26. package/dist/types/coordinator-mcp/server.d.ts +14 -0
  27. package/dist/types/crash/index-store.d.ts +3 -0
  28. package/dist/types/crash/record-loader.d.ts +2 -0
  29. package/dist/types/crash/upstream/relay.d.ts +5 -3
  30. package/dist/types/cursor.d.ts +12 -0
  31. package/dist/types/defaults/gjc-defaults.d.ts +35 -1
  32. package/dist/types/defaults/gjc-skills.generated.d.ts +1 -1
  33. package/dist/types/discovery/helpers.d.ts +1 -1
  34. package/dist/types/edit/index.d.ts +2 -1
  35. package/dist/types/eval/py/executor.d.ts +2 -0
  36. package/dist/types/extensibility/extensions/loader.d.ts +32 -0
  37. package/dist/types/extensibility/gjc-plugins/types.d.ts +1 -1
  38. package/dist/types/extensibility/skills.d.ts +2 -0
  39. package/dist/types/gjc-runtime/autoresearch-runtime.d.ts +194 -0
  40. package/dist/types/gjc-runtime/gc-runtime.d.ts +1 -1
  41. package/dist/types/gjc-runtime/psmux-detect.d.ts +1 -1
  42. package/dist/types/gjc-runtime/python-transcript.d.ts +19 -0
  43. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +2 -8
  44. package/dist/types/gjc-runtime/session-layout.d.ts +6 -3
  45. package/dist/types/gjc-runtime/state-renderer.d.ts +0 -22
  46. package/dist/types/gjc-runtime/state-schema.d.ts +7 -7
  47. package/dist/types/gjc-runtime/tmux-common.d.ts +2 -2
  48. package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +38 -1
  49. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +2 -24
  50. package/dist/types/gjc-runtime/ultragoal-validation-policy.d.ts +73 -0
  51. package/dist/types/gjc-runtime/workflow-recovery-projection.d.ts +100 -0
  52. package/dist/types/hooks/skill-keywords.d.ts +1 -1
  53. package/dist/types/main.d.ts +0 -10
  54. package/dist/types/modes/acp/acp-agent.d.ts +13 -6
  55. package/dist/types/modes/components/model-selector.d.ts +14 -0
  56. package/dist/types/modes/components/skill-hud/render.d.ts +6 -1
  57. package/dist/types/modes/components/smart-routing-panel.d.ts +82 -0
  58. package/dist/types/modes/controllers/selector-controller.d.ts +14 -0
  59. package/dist/types/modes/interactive-mode.d.ts +3 -0
  60. package/dist/types/modes/types.d.ts +9 -0
  61. package/dist/types/rlm/artifacts.d.ts +4 -4
  62. package/dist/types/runtime/optional-runtime-services.d.ts +2 -1
  63. package/dist/types/runtime/workspace-tree-service.d.ts +5 -1
  64. package/dist/types/runtime-mcp/config.d.ts +6 -0
  65. package/dist/types/runtime-mcp/manager.d.ts +10 -0
  66. package/dist/types/sdk/acp/adapter.d.ts +0 -4
  67. package/dist/types/sdk/broker/broker.d.ts +1 -1
  68. package/dist/types/sdk/broker/lifecycle.d.ts +57 -2
  69. package/dist/types/sdk/broker/session-index.d.ts +15 -0
  70. package/dist/types/sdk/bus/chat-daemon-control.d.ts +6 -0
  71. package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +4 -0
  72. package/dist/types/sdk/bus/index.d.ts +8 -0
  73. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +2 -0
  74. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +12 -2
  75. package/dist/types/sdk/client/client.d.ts +39 -4
  76. package/dist/types/sdk/host/host.d.ts +1 -0
  77. package/dist/types/sdk/host/internal-autorouting-state.d.ts +3 -0
  78. package/dist/types/sdk/host/model-pin.d.ts +29 -0
  79. package/dist/types/sdk/host/session-runtime.d.ts +2 -1
  80. package/dist/types/sdk/lifecycle-session.d.ts +13 -0
  81. package/dist/types/sdk/router/session-router.d.ts +27 -1
  82. package/dist/types/sdk/session.d.ts +11 -1
  83. package/dist/types/session/agent-session.d.ts +36 -6
  84. package/dist/types/session/artifacts.d.ts +16 -1
  85. package/dist/types/session/internal/managed-session-storage.d.ts +7 -0
  86. package/dist/types/session/session-manager.d.ts +84 -2
  87. package/dist/types/session/session-staging-paths.d.ts +3 -0
  88. package/dist/types/skill-state/canonical-skills.d.ts +1 -1
  89. package/dist/types/skill-state/workflow-hud.d.ts +12 -21
  90. package/dist/types/skill-state/workflow-mutation-guard.d.ts +1 -0
  91. package/dist/types/slash-commands/helpers/autorouting-status.d.ts +8 -0
  92. package/dist/types/system-prompt.d.ts +23 -0
  93. package/dist/types/task/executor.d.ts +35 -4
  94. package/dist/types/task/index.d.ts +17 -3
  95. package/dist/types/task/receipt.d.ts +3 -1
  96. package/dist/types/task/types.d.ts +144 -0
  97. package/dist/types/tools/atomic-file-write.d.ts +72 -0
  98. package/dist/types/tools/descriptors.d.ts +1 -0
  99. package/dist/types/tools/index.d.ts +36 -1
  100. package/dist/types/tools/move-session.d.ts +40 -0
  101. package/dist/types/tools/python.d.ts +12 -0
  102. package/dist/types/tools/resource-gc.d.ts +0 -3
  103. package/dist/types/tools/search.d.ts +8 -1
  104. package/dist/types/tools/skill.d.ts +9 -4
  105. package/dist/types/tools/subagent.d.ts +33 -3
  106. package/dist/types/utils/xml-escape.d.ts +2 -0
  107. package/dist/types/workflow/workflow-intent-diff.d.ts +2 -2
  108. package/dist/types/workflow/workflow-intent-report-consumer.d.ts +1 -1
  109. package/package.json +11 -12
  110. package/scripts/build-sdk-package-smoke.ts +7 -5
  111. package/scripts/check-autorouting-tier-map.ts +183 -0
  112. package/scripts/generate-sdk-operation-inventory.ts +12 -0
  113. package/scripts/generate-tool-catalog.ts +40 -5
  114. package/src/async/job-manager.ts +5 -2
  115. package/src/autoresearch/dashboard.ts +134 -314
  116. package/src/autoresearch/data-context.ts +35 -0
  117. package/src/autoresearch/git.ts +186 -26
  118. package/src/autoresearch/{helpers.ts → harness.ts} +65 -40
  119. package/src/autoresearch/{prompt-setup.md → prompts/prompt-setup.md} +7 -7
  120. package/src/autoresearch/prompts/prompt.md +103 -0
  121. package/src/autoresearch/prompts.ts +126 -0
  122. package/src/autoresearch/runs.ts +708 -0
  123. package/src/autoresearch/types.ts +9 -150
  124. package/src/capability/index.ts +4 -4
  125. package/src/capability/types.ts +20 -0
  126. package/src/cli/accounts-cli.ts +21 -5
  127. package/src/cli/config-cli.ts +25 -1
  128. package/src/cli/fast-help.ts +1 -2
  129. package/src/cli/list-models.ts +5 -8
  130. package/src/cli/setup-cli.ts +8 -0
  131. package/src/cli.ts +2 -21
  132. package/src/commands/autoresearch.ts +33 -0
  133. package/src/commands/coordinator.ts +15 -0
  134. package/src/commands/launch.ts +2 -4
  135. package/src/commands/quick-lane.ts +1 -1
  136. package/src/commands/sdk.ts +24 -0
  137. package/src/commands/state.ts +1 -1
  138. package/src/config/autorouting-contract.ts +474 -0
  139. package/src/config/autorouting-generator.ts +238 -0
  140. package/src/config/autorouting-tier-map.ts +6108 -0
  141. package/src/config/autorouting.ts +120 -0
  142. package/src/config/model-discovery-manager.ts +42 -11
  143. package/src/config/model-registry.ts +348 -68
  144. package/src/config/provider-selection-policy.ts +60 -9
  145. package/src/config/settings-schema.ts +195 -6
  146. package/src/config/settings.ts +62 -1
  147. package/src/config.ts +2 -3
  148. package/src/coordinator/contract.ts +0 -1
  149. package/src/coordinator-mcp/codex-handoff.ts +68 -42
  150. package/src/coordinator-mcp/durability.ts +200 -0
  151. package/src/coordinator-mcp/event-webhook.ts +278 -0
  152. package/src/coordinator-mcp/question-state.ts +109 -46
  153. package/src/coordinator-mcp/server.ts +932 -504
  154. package/src/crash/index-store.ts +126 -22
  155. package/src/crash/record-loader.ts +7 -0
  156. package/src/crash/upstream/relay.ts +82 -34
  157. package/src/cursor.ts +84 -1
  158. package/src/debug/index.ts +26 -0
  159. package/src/defaults/gjc/skills/autoresearch/SKILL.md +116 -0
  160. package/src/defaults/gjc/skills/autoresearch/auto-critic.md +46 -0
  161. package/src/defaults/gjc/skills/autoresearch/auto-iterate.md +42 -0
  162. package/src/defaults/gjc/skills/deep-interview/SKILL.md +17 -37
  163. package/src/defaults/gjc/skills/ralplan/SKILL.md +21 -18
  164. package/src/defaults/gjc/skills/ultragoal/SKILL.md +6 -17
  165. package/src/defaults/gjc/skills/ultragoal/ai-slop-cleaner.md +2 -2
  166. package/src/defaults/gjc-defaults.ts +89 -2
  167. package/src/defaults/gjc-skills.generated.ts +24 -17
  168. package/src/discovery/builtin.ts +18 -5
  169. package/src/discovery/helpers.ts +6 -6
  170. package/src/edit/index.ts +5 -2
  171. package/src/eval/py/executor.ts +8 -0
  172. package/src/extensibility/extensions/loader.ts +135 -13
  173. package/src/extensibility/extensions/wrapper.ts +1 -2
  174. package/src/extensibility/gjc-plugins/schema.ts +1 -1
  175. package/src/extensibility/runtime-skill-discovery.ts +2 -2
  176. package/src/extensibility/skill-management.ts +2 -3
  177. package/src/extensibility/skills.ts +16 -8
  178. package/src/gjc-runtime/autoresearch-runtime.ts +1509 -0
  179. package/src/gjc-runtime/gc-render.ts +0 -1
  180. package/src/gjc-runtime/gc-runtime.ts +1 -11
  181. package/src/gjc-runtime/launch-tmux.ts +3 -3
  182. package/src/gjc-runtime/psmux-detect.ts +2 -2
  183. package/src/gjc-runtime/python-transcript.ts +58 -0
  184. package/src/gjc-runtime/ralplan-runtime.ts +6 -18
  185. package/src/gjc-runtime/session-layout.ts +18 -7
  186. package/src/gjc-runtime/session-state-sidecar.ts +58 -9
  187. package/src/gjc-runtime/state-renderer.ts +0 -38
  188. package/src/gjc-runtime/state-runtime.ts +43 -22
  189. package/src/gjc-runtime/state-schema.ts +1 -1
  190. package/src/gjc-runtime/tmux-common.ts +7 -7
  191. package/src/gjc-runtime/ultragoal-change-set.ts +232 -11
  192. package/src/gjc-runtime/ultragoal-runtime.ts +309 -114
  193. package/src/gjc-runtime/ultragoal-validation-policy.ts +247 -0
  194. package/src/gjc-runtime/workflow-command-ref.ts +15 -11
  195. package/src/gjc-runtime/workflow-manifest.generated.json +413 -532
  196. package/src/gjc-runtime/workflow-manifest.ts +43 -83
  197. package/src/gjc-runtime/workflow-recovery-projection.ts +616 -0
  198. package/src/hooks/skill-keywords.ts +3 -3
  199. package/src/hooks/skill-state.ts +2 -2
  200. package/src/internal-urls/docs-index.generated.ts +26 -24
  201. package/src/lsp/index.ts +25 -5
  202. package/src/main.ts +2 -27
  203. package/src/modes/acp/acp-agent.ts +200 -75
  204. package/src/modes/components/model-selector.ts +216 -13
  205. package/src/modes/components/oauth-selector.ts +5 -5
  206. package/src/modes/components/skill-hud/render.ts +173 -35
  207. package/src/modes/components/smart-routing-panel.ts +531 -0
  208. package/src/modes/components/tool-status-header.ts +6 -3
  209. package/src/modes/controllers/command-controller.ts +17 -7
  210. package/src/modes/controllers/input-controller.ts +44 -9
  211. package/src/modes/controllers/runtime-mcp-command-controller.ts +4 -0
  212. package/src/modes/controllers/selector-controller.ts +298 -65
  213. package/src/modes/interactive-mode.ts +8 -1
  214. package/src/modes/shared/agent-wire/workflow-gate-types.ts +1 -1
  215. package/src/modes/types.ts +9 -1
  216. package/src/prompts/agents/planner.md +1 -1
  217. package/src/prompts/system/system-prompt.md +1 -1
  218. package/src/prompts/system/volatile-project-context.md +2 -1
  219. package/src/prompts/tools/move-session.md +15 -0
  220. package/src/prompts/tools/python.md +12 -0
  221. package/src/prompts/tools/skill-discovery.md +1 -1
  222. package/src/prompts/tools/skill.md +2 -2
  223. package/src/prompts/tools/task-summary.md +1 -0
  224. package/src/prompts/tools/task.md +6 -0
  225. package/src/rlm/artifacts.ts +12 -10
  226. package/src/runtime/optional-runtime-services.ts +3 -2
  227. package/src/runtime/workspace-tree-service.ts +11 -3
  228. package/src/runtime-mcp/config.ts +8 -1
  229. package/src/runtime-mcp/manager.ts +33 -0
  230. package/src/sdk/acp/adapter.ts +48 -1
  231. package/src/sdk/broker/broker.ts +30 -2
  232. package/src/sdk/broker/discovery.ts +217 -2
  233. package/src/sdk/broker/lifecycle.ts +385 -59
  234. package/src/sdk/broker/session-index.ts +243 -48
  235. package/src/sdk/broker/transport.ts +1 -0
  236. package/src/sdk/bus/chat-daemon-control.ts +8 -2
  237. package/src/sdk/bus/chat-daemon-runtime.ts +5 -0
  238. package/src/sdk/bus/index.ts +208 -11
  239. package/src/sdk/bus/kind-aware-reconciliation.ts +16 -0
  240. package/src/sdk/bus/reconciliation-store.ts +27 -5
  241. package/src/sdk/bus/telegram-daemon-contract.ts +12 -2
  242. package/src/sdk/client/client.ts +204 -22
  243. package/src/sdk/host/host.ts +16 -0
  244. package/src/sdk/host/internal-autorouting-state.ts +13 -0
  245. package/src/sdk/host/model-pin.ts +101 -0
  246. package/src/sdk/host/reverse-leases.ts +2 -1
  247. package/src/sdk/host/session-runtime.test.ts +224 -1
  248. package/src/sdk/host/session-runtime.ts +125 -37
  249. package/src/sdk/lifecycle/service.ts +3 -1
  250. package/src/sdk/lifecycle-session.ts +42 -1
  251. package/src/sdk/protocol/operation-inventory.generated.json +66 -0
  252. package/src/sdk/router/session-router.ts +375 -36
  253. package/src/sdk/session.ts +547 -47
  254. package/src/session/account-inventory.ts +1 -1
  255. package/src/session/agent-session.ts +765 -189
  256. package/src/session/artifacts.ts +234 -4
  257. package/src/session/internal/managed-session-storage.ts +91 -4
  258. package/src/session/provider-safety-stop-hint.ts +3 -2
  259. package/src/session/session-manager.ts +775 -16
  260. package/src/session/session-staging-paths.ts +8 -0
  261. package/src/setup/hermes/templates/operator-instructions.v1.md +0 -1
  262. package/src/setup/host-plugin-setup.ts +2 -2
  263. package/src/skill-state/active-state.ts +2 -2
  264. package/src/skill-state/canonical-skills.ts +1 -1
  265. package/src/skill-state/initial-phase.ts +1 -1
  266. package/src/skill-state/workflow-hud.ts +41 -41
  267. package/src/skill-state/workflow-mutation-guard.ts +26 -7
  268. package/src/slash-commands/builtin-registry.ts +77 -0
  269. package/src/slash-commands/helpers/autorouting-status.ts +55 -0
  270. package/src/system-prompt.ts +102 -5
  271. package/src/task/discovery.ts +2 -3
  272. package/src/task/executor.ts +617 -43
  273. package/src/task/index.ts +262 -15
  274. package/src/task/receipt.ts +30 -0
  275. package/src/task/render.ts +11 -1
  276. package/src/task/types.ts +316 -0
  277. package/src/tools/atomic-file-write.ts +594 -0
  278. package/src/tools/bisect.ts +3 -2
  279. package/src/tools/descriptors.test.ts +3 -0
  280. package/src/tools/descriptors.ts +24 -0
  281. package/src/tools/image-gen.ts +5 -1
  282. package/src/tools/index.ts +62 -5
  283. package/src/tools/move-session.ts +107 -0
  284. package/src/tools/python.ts +205 -0
  285. package/src/tools/read.ts +88 -15
  286. package/src/tools/renderers.ts +2 -0
  287. package/src/tools/resource-gc.ts +8 -177
  288. package/src/tools/search.ts +30 -6
  289. package/src/tools/skill-discovery.ts +1 -0
  290. package/src/tools/skill.ts +37 -7
  291. package/src/tools/subagent-render.ts +119 -13
  292. package/src/tools/subagent.ts +91 -74
  293. package/src/tools/tool-catalog.generated.ts +64 -2
  294. package/src/tools/write.ts +16 -12
  295. package/src/utils/xml-escape.ts +14 -0
  296. package/src/web/search/providers/codex.ts +2 -2
  297. package/src/workflow/workflow-intent-diff.ts +13 -12
  298. package/src/workflow/workflow-intent-report-consumer.ts +6 -6
  299. package/dist/types/autoresearch/helpers.d.ts +0 -24
  300. package/dist/types/autoresearch/index.d.ts +0 -2
  301. package/dist/types/autoresearch/state.d.ts +0 -17
  302. package/dist/types/autoresearch/storage.d.ts +0 -142
  303. package/dist/types/autoresearch/tools/init-experiment.d.ts +0 -31
  304. package/dist/types/autoresearch/tools/log-experiment.d.ts +0 -23
  305. package/dist/types/autoresearch/tools/run-experiment.d.ts +0 -8
  306. package/dist/types/autoresearch/tools/update-notes.d.ts +0 -12
  307. package/dist/types/commands/team.d.ts +0 -24
  308. package/dist/types/gjc-runtime/team-gc.d.ts +0 -7
  309. package/dist/types/gjc-runtime/team-launch.d.ts +0 -61
  310. package/dist/types/gjc-runtime/team-notify.d.ts +0 -45
  311. package/dist/types/gjc-runtime/team-runtime.d.ts +0 -336
  312. package/dist/types/gjc-runtime/team-store.d.ts +0 -168
  313. package/dist/types/gjc-runtime/team-worker-heartbeat.d.ts +0 -43
  314. package/dist/types/gjc-runtime/team-worker-memory-guard.d.ts +0 -92
  315. package/dist/types/gjc-runtime/team-workers.d.ts +0 -83
  316. package/dist/types/rlm/index.d.ts +0 -47
  317. package/dist/types/rlm/preset.d.ts +0 -23
  318. package/dist/types/rlm/python-tool.d.ts +0 -16
  319. package/src/autoresearch/command-resume.md +0 -14
  320. package/src/autoresearch/index.ts +0 -536
  321. package/src/autoresearch/prompt.md +0 -103
  322. package/src/autoresearch/resume-message.md +0 -10
  323. package/src/autoresearch/state.ts +0 -273
  324. package/src/autoresearch/storage.ts +0 -699
  325. package/src/autoresearch/tools/init-experiment.ts +0 -272
  326. package/src/autoresearch/tools/log-experiment.ts +0 -524
  327. package/src/autoresearch/tools/run-experiment.ts +0 -475
  328. package/src/autoresearch/tools/update-notes.ts +0 -109
  329. package/src/commands/rlm.ts +0 -19
  330. package/src/commands/team.ts +0 -238
  331. package/src/defaults/gjc/skills/deep-interview/auto-research-greenfield.md +0 -42
  332. package/src/defaults/gjc/skills/team/SKILL.md +0 -467
  333. package/src/gjc-runtime/team-gc.ts +0 -49
  334. package/src/gjc-runtime/team-launch.ts +0 -404
  335. package/src/gjc-runtime/team-notify.ts +0 -326
  336. package/src/gjc-runtime/team-runtime.ts +0 -5923
  337. package/src/gjc-runtime/team-store.ts +0 -878
  338. package/src/gjc-runtime/team-worker-heartbeat.ts +0 -149
  339. package/src/gjc-runtime/team-worker-memory-guard.ts +0 -363
  340. package/src/gjc-runtime/team-workers.ts +0 -1123
  341. package/src/prompts/system/rlm-report-command.md +0 -1
  342. package/src/prompts/system/rlm-research.md +0 -23
  343. package/src/rlm/index.ts +0 -365
  344. package/src/rlm/preset.ts +0 -76
  345. package/src/rlm/python-tool.ts +0 -68
@@ -96,6 +96,18 @@ const MAX_RECEIVED_AT_SKEW_MS = 5_000;
96
96
  const MAX_LIFECYCLE_METADATA_BYTES = 4096;
97
97
  const MAX_EFFECT_MARKER_LENGTH = 128;
98
98
  const MAX_PROCESS_INCARNATION_LENGTH = 256;
99
+ const READY_THEN_EXIT_MESSAGE = "became ready then exited before live admission";
100
+
101
+ function readyThenExitedResponse(id: string, child?: ChildProcess): BrokerResponse {
102
+ const parts = [`Session ${id} ${READY_THEN_EXIT_MESSAGE}.`];
103
+ if (child && child.exitCode !== null) parts.push(`exit=${child.exitCode}`);
104
+ if (child?.signalCode) parts.push(`signal=${child.signalCode}`);
105
+ // Host stderr is deliberately excluded: the detached host receives launch
106
+ // configuration and inherited credentials, and no pattern-based redaction can
107
+ // prove arbitrary child output free of that material; its stderr is discarded
108
+ // by the OS and never captured (#4712 review).
109
+ return fail("ready_then_exited", parts.join(" "));
110
+ }
99
111
 
100
112
  export interface LifecycleDeadlines {
101
113
  receivedAt: number;
@@ -161,6 +173,25 @@ export function setLifecycleTimingForTest(broker: Broker, timing: LifecycleTimin
161
173
  else lifecycleTimingsForTest.delete(broker);
162
174
  }
163
175
 
176
+ let lifecycleHostPlatformForTest: NodeJS.Platform | undefined;
177
+
178
+ /** Deterministic platform seam for Windows-only ready-then-exit tolerance. */
179
+ export function setLifecycleHostPlatformForTest(platform: NodeJS.Platform | undefined): void {
180
+ lifecycleHostPlatformForTest = platform;
181
+ }
182
+
183
+ function lifecycleHostPlatform(): NodeJS.Platform {
184
+ return lifecycleHostPlatformForTest ?? process.platform;
185
+ }
186
+
187
+ function readyThenExitToleranceEnabled(): boolean {
188
+ return lifecycleHostPlatform() === "win32";
189
+ }
190
+
191
+ export function readyThenExitToleranceEnabledForTest(): boolean {
192
+ return readyThenExitToleranceEnabled();
193
+ }
194
+
164
195
  function lifecycleTiming(broker: Broker): LifecycleTiming {
165
196
  return lifecycleTimingsForTest.get(broker) ?? defaultLifecycleTiming;
166
197
  }
@@ -244,6 +275,14 @@ export interface SessionLifecycleLaunchRequest {
244
275
  sessionIdentity?: SessionLifecycleTranscriptIdentity;
245
276
  /** Broker-issued effect marker which the child echoes only after host readiness. */
246
277
  effectMarker?: string;
278
+ /**
279
+ * Explicit `provider/model` pin with `gjc --model` grammar (#4707). The
280
+ * coordinator resolves it before the broker; the session host applies it
281
+ * exactly like a CLI `--model` selection, so it wins over `modelPreset`
282
+ * (mirroring CLI precedence where an explicit `--model` overrides
283
+ * activated profiles).
284
+ */
285
+ modelId?: string;
247
286
  modelPreset?: string;
248
287
  mcpServers?: SessionLifecycleMcpServer[];
249
288
  worktree?: SessionLifecycleWorktreeTarget;
@@ -385,6 +424,7 @@ export function readSessionLifecycleLaunchRequest(
385
424
  (request.sessionIdentity !== undefined && !isSessionLifecycleTranscriptIdentity(request.sessionIdentity)) ||
386
425
  (request.effectMarker !== undefined &&
387
426
  (typeof request.effectMarker !== "string" || !/^[A-Za-z0-9._-]{1,128}$/.test(request.effectMarker))) ||
427
+ (request.modelId !== undefined && (typeof request.modelId !== "string" || !request.modelId.trim())) ||
388
428
  (request.modelPreset !== undefined && (typeof request.modelPreset !== "string" || !request.modelPreset)) ||
389
429
  (request.mcpServers !== undefined && !isSessionLifecycleMcpServers(request.mcpServers)) ||
390
430
  !hasValidLifecycleDeadlines(
@@ -427,6 +467,13 @@ type SessionLaunch = {
427
467
  sourceCwd?: string;
428
468
  sessionPath?: string;
429
469
  sessionIdentity?: SessionLifecycleTranscriptIdentity;
470
+ /**
471
+ * Explicit `provider/model` pin with `gjc --model` grammar (#4707). Applied
472
+ * by the session host exactly like a CLI `--model` selection, so it wins
473
+ * over `modelPreset` (mirroring CLI precedence where an explicit `--model`
474
+ * overrides activated profiles).
475
+ */
476
+ modelId?: string;
430
477
  modelPreset?: string;
431
478
  mcpServers?: SessionLifecycleMcpServer[];
432
479
  /** Coordinator namespace dir; broker computes the state file path from launch.id (#2549). */
@@ -798,6 +845,8 @@ type ReadyAuthority = {
798
845
  type ReadinessResult =
799
846
  | { kind: "ready"; authority: ReadyAuthority }
800
847
  | { kind: "startup_failed"; failure: SdkStartupFailure }
848
+ | { kind: "ready_then_exited" }
849
+ | { kind: "ready_probe_failed"; probe: ReadyAuthorityProbe }
801
850
  | { kind: "child_exited" }
802
851
  | { kind: "timeout" };
803
852
  type BrokerIndex = Pick<Broker, "index">;
@@ -974,10 +1023,26 @@ async function writeEffectMarker(root: string, id: string, marker: EffectMarker)
974
1023
  export async function writeSessionLifecycleReady(root: string, id: string, effectMarker: string): Promise<void> {
975
1024
  const incarnation = processIncarnation(process.pid);
976
1025
  if (!incarnation) throw new Error("Lifecycle child has no readable OS incarnation.");
977
- await fs.mkdir(path.join(root, "sdk"), { recursive: true, mode: 0o700 });
978
- await fs.writeFile(lifecycleReadyPath(root, id), canonicalJson({ pid: process.pid, effectMarker, incarnation }), {
979
- mode: 0o600,
980
- });
1026
+ const directory = path.join(root, "sdk");
1027
+ await fs.mkdir(directory, { recursive: true, mode: 0o700 });
1028
+ const temporary = path.join(directory, `.${id}.lifecycle.ready.${randomUUID()}.tmp`);
1029
+ const handle = await fs.open(
1030
+ temporary,
1031
+ fsSync.constants.O_CREAT | fsSync.constants.O_EXCL | fsSync.constants.O_WRONLY,
1032
+ 0o600,
1033
+ );
1034
+ try {
1035
+ await handle.writeFile(canonicalJson({ pid: process.pid, effectMarker, incarnation }));
1036
+ await handle.sync();
1037
+ } finally {
1038
+ await handle.close();
1039
+ }
1040
+ try {
1041
+ await fs.rename(temporary, lifecycleReadyPath(root, id));
1042
+ await syncDirectory(directory);
1043
+ } finally {
1044
+ await fs.rm(temporary, { force: true });
1045
+ }
981
1046
  }
982
1047
 
983
1048
  export interface LifecycleTranscriptEvidence {
@@ -1074,6 +1139,10 @@ function isLifecycleFailureArtifact(value: unknown): value is LifecycleFailureAr
1074
1139
  }
1075
1140
 
1076
1141
  async function syncDirectory(directory: string): Promise<void> {
1142
+ // Windows does not support fsync on directory handles. File contents and
1143
+ // lifecycle markers are still written through the normal atomic path; the
1144
+ // directory durability barrier is a POSIX-only operation.
1145
+ if (process.platform === "win32") return;
1077
1146
  const handle = await fs.open(directory, fsSync.constants.O_RDONLY);
1078
1147
  try {
1079
1148
  await handle.sync();
@@ -1082,7 +1151,7 @@ async function syncDirectory(directory: string): Promise<void> {
1082
1151
  }
1083
1152
  }
1084
1153
 
1085
- /** The child writes bounded startup diagnostics before exiting without readiness. */
1154
+ /** Writes bounded startup diagnostics. The child stamps its own pid; the broker may stamp a proven child identity. */
1086
1155
  export async function writeSessionLifecycleFailure(
1087
1156
  root: string,
1088
1157
  id: string,
@@ -1091,6 +1160,7 @@ export async function writeSessionLifecycleFailure(
1091
1160
  rollback: SdkStartupRollbackResult,
1092
1161
  transcript?: LifecycleTranscriptEvidence,
1093
1162
  ownerIncarnation?: string,
1163
+ ownerPid?: number,
1094
1164
  ): Promise<void> {
1095
1165
  if (!isSdkStartupFailure(failure))
1096
1166
  throw new Error("Lifecycle startup failure does not satisfy the canonical failure contract.");
@@ -1105,7 +1175,7 @@ export async function writeSessionLifecycleFailure(
1105
1175
  await fs.mkdir(directory, { recursive: true, mode: 0o700 });
1106
1176
  await fs.chmod(directory, 0o700);
1107
1177
  const artifact: LifecycleFailureArtifact = {
1108
- pid: process.pid,
1178
+ pid: ownerPid ?? process.pid,
1109
1179
  effectMarker,
1110
1180
  incarnation,
1111
1181
  ...failure,
@@ -2074,6 +2144,89 @@ async function hasOwnedReadinessEvidence(
2074
2144
  );
2075
2145
  }
2076
2146
 
2147
+ /**
2148
+ * Typed ready-then-exit probe for a dead child (#4712 review: the earlier
2149
+ * boolean collapsed EACCES/EIO/malformed JSON into "absent", routing the
2150
+ * teardown decision through the secondary authority and finally surfacing as
2151
+ * a false `spawn_failed` claim that the child "exited before registering
2152
+ * readiness" when the real failure was reading the endpoint).
2153
+ *
2154
+ * A host that died hard leaves its endpoint file on disk; a host that tore
2155
+ * down gracefully removed it first. `matched` requires the owned marker +
2156
+ * ready marker chain to still name exactly this child and the endpoint file
2157
+ * to still name the same pid and session. `absent_indexed` means the endpoint
2158
+ * file is gone (ENOENT only) and the broker's own session index recorded a
2159
+ * host registration for exactly this incarnation — the sole permitted
2160
+ * fallback authority. `malformed` and `io_error` are their own fail-closed
2161
+ * outcomes: they feed no teardown decision and assert nothing about the
2162
+ * child process.
2163
+ */
2164
+ type ReadyAuthorityProbe =
2165
+ | { kind: "matched" }
2166
+ | { kind: "absent_indexed" }
2167
+ | { kind: "absent_unindexed" }
2168
+ | { kind: "not_published" }
2169
+ | { kind: "malformed" }
2170
+ | { kind: "io_error"; code: string };
2171
+
2172
+ async function probePublishedReadyAuthority(
2173
+ root: string,
2174
+ id: string,
2175
+ expected: EffectMarker,
2176
+ index?: BrokerIndex,
2177
+ ): Promise<ReadyAuthorityProbe> {
2178
+ const [effect, ready] = await Promise.all([
2179
+ readEffectMarker(lifecycleMarkerPath(root, id)),
2180
+ readEffectMarker(lifecycleReadyPath(root, id)),
2181
+ ]);
2182
+ if (!effect || !ready || !sameEffectMarker(effect, expected) || !sameEffectMarker(ready, expected))
2183
+ return { kind: "not_published" };
2184
+ let endpointText: string;
2185
+ try {
2186
+ endpointText = await fs.readFile(path.join(root, "sdk", `${id}.json`), "utf8");
2187
+ } catch (error) {
2188
+ const code = (error as NodeJS.ErrnoException).code;
2189
+ // Only a missing endpoint means "gracefully removed"; anything else
2190
+ // (EACCES, EIO, ...) is its own condition and must not silently route the
2191
+ // teardown decision to the secondary authority.
2192
+ if (code !== "ENOENT") return { kind: "io_error", code: code ?? "unknown" };
2193
+ if (!index) return { kind: "absent_unindexed" };
2194
+ const sessionIndex = index.index;
2195
+ await sessionIndex.refresh();
2196
+ return sessionIndex
2197
+ .listSessions()
2198
+ .sessions.some(
2199
+ session =>
2200
+ session.sessionId === id &&
2201
+ session.pid === expected.pid &&
2202
+ (session.hostIncarnation ?? session.processIncarnation) === expected.incarnation,
2203
+ )
2204
+ ? { kind: "absent_indexed" }
2205
+ : { kind: "absent_unindexed" };
2206
+ }
2207
+ let endpoint: { sessionId?: unknown; pid?: unknown };
2208
+ try {
2209
+ endpoint = JSON.parse(endpointText) as { sessionId?: unknown; pid?: unknown };
2210
+ } catch {
2211
+ return { kind: "malformed" };
2212
+ }
2213
+ return endpoint.pid === expected.pid && endpoint.sessionId === id ? { kind: "matched" } : { kind: "not_published" };
2214
+ }
2215
+
2216
+ /** Test seam for the typed ready-authority probe boundary (#4712 review). */
2217
+ export async function probePublishedReadyAuthorityForTest(
2218
+ root: string,
2219
+ id: string,
2220
+ expected: EffectMarker,
2221
+ ): Promise<ReadyAuthorityProbe> {
2222
+ return probePublishedReadyAuthority(root, id, expected);
2223
+ }
2224
+
2225
+ /** Human-readable, honest description of a fail-closed probe outcome. */
2226
+ function describeReadyProbeFailure(probe: ReadyAuthorityProbe): string {
2227
+ return probe.kind === "io_error" ? `endpoint read failed (${probe.code})` : "endpoint file is malformed";
2228
+ }
2229
+
2077
2230
  type LifecycleFileCapture = {
2078
2231
  bytes: Buffer;
2079
2232
  identity: { dev: bigint; ino: bigint; size: bigint; mtimeNs: bigint; sha256: string; nlink: bigint };
@@ -2176,6 +2329,15 @@ async function removeOwnedLifecycleArtifacts(
2176
2329
  }
2177
2330
  if (parsed.pid !== expected.pid || observeProcess(expected.pid, expected.incarnation) !== "exited") return false;
2178
2331
  if (createHash("sha256").update(endpoint.bytes).digest("hex") !== endpoint.digest) return false;
2332
+ // Bind the deletion to the expected process incarnation and endpoint
2333
+ // identity, rechecking immediately before unlink: a successor launch for
2334
+ // this session rewrites the lifecycle marker through the broker before its
2335
+ // child can publish anything, so a marker that no longer matches `expected`
2336
+ // means this endpoint file is no longer provably the dead child's — it may
2337
+ // belong to a PID-reusing successor and must not be unlinked (#4712
2338
+ // review: PID-reuse race must not delete a successor endpoint).
2339
+ const preUnlinkMarker = await readEffectMarker(lifecycleMarkerPath(root, id));
2340
+ if (!preUnlinkMarker || !sameEffectMarker(preUnlinkMarker, expected)) return false;
2179
2341
  const endpointRemoval = exactUnlinkLifecycleFile(
2180
2342
  endpointSource,
2181
2343
  endpoint.identity,
@@ -2239,6 +2401,19 @@ async function removeOwnedLifecycleArtifacts(
2239
2401
  return true;
2240
2402
  }
2241
2403
 
2404
+ /**
2405
+ * Test seam for the PID-reuse deletion boundary: a successor launch rewriting
2406
+ * the lifecycle marker must stop the unlink of an endpoint that only matches
2407
+ * by a reused pid (#4712 review).
2408
+ */
2409
+ export async function removeOwnedLifecycleArtifactsForTest(
2410
+ root: string,
2411
+ id: string,
2412
+ expected: EffectMarker,
2413
+ ): Promise<boolean> {
2414
+ return removeOwnedLifecycleArtifacts(root, id, expected);
2415
+ }
2416
+
2242
2417
  async function recordTerminalUncertain(broker: Broker, id: string, root: string, pid: number): Promise<void> {
2243
2418
  await broker.index.refresh();
2244
2419
  const registered = broker.index.listSessions().sessions.find(session => session.sessionId === id);
@@ -2344,12 +2519,61 @@ async function terminateSpawnedChild(
2344
2519
  lifecycleFailurePath(root, id, expected.effectMarker),
2345
2520
  expected,
2346
2521
  );
2347
- if (
2348
- !failure?.artifact.rollback.fenced ||
2349
- !failure.artifact.rollback.runtimeRemoved ||
2350
- !failure.artifact.rollback.hostStopped ||
2351
- !failure.artifact.rollback.brokerRegistrationReleased
2352
- ) {
2522
+ const rollbackComplete =
2523
+ failure?.artifact.rollback.fenced === true &&
2524
+ failure.artifact.rollback.runtimeRemoved &&
2525
+ failure.artifact.rollback.hostStopped &&
2526
+ failure.artifact.rollback.brokerRegistrationReleased;
2527
+ if (!rollbackComplete) {
2528
+ if (!readyThenExitToleranceEnabled()) {
2529
+ await recordTerminalUncertain(broker, id, root, pid);
2530
+ return false;
2531
+ }
2532
+ const publishedReady = (await probePublishedReadyAuthority(root, id, expected)).kind === "matched";
2533
+ const artifactsRemoved = await removeOwnedLifecycleArtifacts(root, id, expected);
2534
+ await broker.index.refresh();
2535
+ const registered = broker.index
2536
+ .listSessions()
2537
+ .sessions.find(session => session.sessionId === id && session.pid === pid);
2538
+ const registeredRowTerminal = registered?.terminal === true || registered?.terminalUncertain === true;
2539
+ let registrationReleased =
2540
+ !broker.index.hasHostRegistrationForLifecycle(id, pid, expected.effectMarker) || registeredRowTerminal;
2541
+ if (registered && !registrationReleased) {
2542
+ registrationReleased = await broker.index.unregisterIfCurrent(registered);
2543
+ await broker.index.refresh();
2544
+ registrationReleased =
2545
+ registrationReleased || !broker.index.hasHostRegistrationForLifecycle(id, pid, expected.effectMarker);
2546
+ }
2547
+ const stillExited =
2548
+ observeProcess(pid, expected.incarnation, value => processIncarnationForBroker(broker, value)) === "exited";
2549
+ const endpointGone = await endpointRemoved(root, id);
2550
+ try {
2551
+ if (publishedReady && !failure) {
2552
+ await writeSessionLifecycleFailure(
2553
+ root,
2554
+ id,
2555
+ expected.effectMarker,
2556
+ {
2557
+ phase: "startup",
2558
+ reason: "failed",
2559
+ message: `Session ${id} ${READY_THEN_EXIT_MESSAGE}.`,
2560
+ },
2561
+ {
2562
+ endpointGeneration: registered?.endpointGeneration ?? null,
2563
+ fenced: stillExited && registrationReleased,
2564
+ runtimeRemoved: artifactsRemoved && endpointGone,
2565
+ hostStopped: stillExited,
2566
+ brokerRegistrationReleased: registrationReleased,
2567
+ },
2568
+ undefined,
2569
+ expected.incarnation,
2570
+ expected.pid,
2571
+ );
2572
+ }
2573
+ } catch {
2574
+ // A missing broker-authored receipt must not hide a proven dead child.
2575
+ }
2576
+ if (stillExited && artifactsRemoved && endpointGone && registrationReleased) return true;
2353
2577
  await recordTerminalUncertain(broker, id, root, pid);
2354
2578
  return false;
2355
2579
  }
@@ -2668,14 +2892,17 @@ async function currentReadyAuthority(
2668
2892
  token?: unknown;
2669
2893
  pid?: unknown;
2670
2894
  };
2671
- // A new lifecycle registration has no heartbeat until the broker checkpoints it.
2672
- // Publish that positive liveness evidence through the broker-owned checkpoint before
2673
- // enforcing ready authority; endpoint, marker, incarnation, and generation checks below remain unchanged.
2895
+ // Native-alive owned readiness is the admission authority. `record.live`
2896
+ // also requires a fresh index heartbeat projection, which can lag a just-
2897
+ // registered detached Windows host. Never admit a terminal/uncertain or
2898
+ // native-dead child; do not refuse a native-alive ready host for a stale live bit.
2674
2899
  await broker.heartbeatSessions();
2675
2900
  await broker.index.refresh();
2676
2901
  const record = broker.index.listSessions().sessions.find(session => session.sessionId === id);
2677
2902
  if (
2678
- !record?.live ||
2903
+ !record ||
2904
+ record.terminal ||
2905
+ record.terminalUncertain ||
2679
2906
  record.pid !== expected.pid ||
2680
2907
  resolveEquivalentPath(record.locator.stateRoot) !== resolveEquivalentPath(root) ||
2681
2908
  record.endpointMtimeMs !== endpointMetadata.mtimeMs ||
@@ -2685,6 +2912,11 @@ async function currentReadyAuthority(
2685
2912
  typeof endpoint.token !== "string"
2686
2913
  )
2687
2914
  return undefined;
2915
+ if (
2916
+ observeProcess(expected.pid, expected.incarnation, value => processIncarnationForBroker(broker, value)) !==
2917
+ "alive"
2918
+ )
2919
+ return undefined;
2688
2920
  return {
2689
2921
  endpoint: endpoint as Record<string, unknown>,
2690
2922
  endpointSource,
@@ -2723,17 +2955,40 @@ async function waitForReady(
2723
2955
  timing: LifecycleTiming,
2724
2956
  signal: "session_ready" | typeof SESSION_PREPARED_EVENT = "session_ready",
2725
2957
  ): Promise<ReadinessResult> {
2958
+ const classifyExitedAfterReady = async (): Promise<ReadinessResult> => {
2959
+ const probe = await probePublishedReadyAuthority(root, id, expected, broker);
2960
+ if (probe.kind === "malformed" || probe.kind === "io_error") return { kind: "ready_probe_failed", probe };
2961
+ const publishedReady = probe.kind === "matched" || probe.kind === "absent_indexed";
2962
+ return publishedReady && readyThenExitToleranceEnabled()
2963
+ ? { kind: "ready_then_exited" }
2964
+ : { kind: "child_exited" };
2965
+ };
2726
2966
  while (timing.now() < deadline) {
2727
2967
  const startupFailure = await readSessionLifecycleFailure(root, id, expected);
2728
- if (startupFailure) return { kind: "startup_failed", failure: startupFailure };
2968
+ if (startupFailure) {
2969
+ if (
2970
+ observeProcess(expected.pid, expected.incarnation, value => processIncarnationForBroker(broker, value)) ===
2971
+ "exited"
2972
+ ) {
2973
+ const afterReady = await classifyExitedAfterReady();
2974
+ if (afterReady.kind !== "child_exited") return afterReady;
2975
+ }
2976
+ return { kind: "startup_failed", failure: startupFailure };
2977
+ }
2729
2978
  if (
2730
2979
  observeProcess(expected.pid, expected.incarnation, value => processIncarnationForBroker(broker, value)) ===
2731
2980
  "exited"
2732
2981
  ) {
2733
2982
  const finalStartupFailure = await readSessionLifecycleFailure(root, id, expected);
2734
- return finalStartupFailure
2735
- ? { kind: "startup_failed", failure: finalStartupFailure }
2736
- : { kind: "child_exited" };
2983
+ if (finalStartupFailure) {
2984
+ const afterReady = await classifyExitedAfterReady();
2985
+ if (afterReady.kind !== "child_exited") return afterReady;
2986
+ return { kind: "startup_failed", failure: finalStartupFailure };
2987
+ }
2988
+ // A malformed or unreadable endpoint is its own fail-closed outcome: it
2989
+ // never feeds the teardown decision and never claims the child "exited
2990
+ // before registering readiness" (#4712 review).
2991
+ return classifyExitedAfterReady();
2737
2992
  }
2738
2993
  try {
2739
2994
  const authority = await currentReadyAuthority(broker, id, root, expected);
@@ -2840,6 +3095,14 @@ async function launchInput(
2840
3095
  if (typeof validatedModelPreset !== "string") return validatedModelPreset;
2841
3096
  modelPreset = validatedModelPreset;
2842
3097
  }
3098
+ // An explicit model pin (#4707) is coordinator-resolved before it reaches
3099
+ // the broker; the broker only guards shape here. It deliberately does NOT
3100
+ // shadow modelPreset: both are threaded to the child, whose startup applies
3101
+ // the profile first and then overrides it with the explicit model exactly
3102
+ // like `gjc --mpreset p --model m` behaves on the CLI.
3103
+ const modelId = text(input.modelId)?.trim();
3104
+ if (input.modelId !== undefined && !modelId)
3105
+ return fail("invalid_input", "modelId must be a non-empty explicit provider/model selector.");
2843
3106
  const worktree = lifecycleWorktreeTarget(input);
2844
3107
  if (worktree === null || (worktree !== undefined && requestedCwd === undefined))
2845
3108
  return fail("invalid_input", "Lifecycle worktree target is invalid.");
@@ -2896,6 +3159,7 @@ async function launchInput(
2896
3159
  id: randomUUID(),
2897
3160
  cwd,
2898
3161
  root: resolvedRoot,
3162
+ modelId,
2899
3163
  modelPreset,
2900
3164
  mcpServers,
2901
3165
  worktree,
@@ -2916,6 +3180,7 @@ async function launchInput(
2916
3180
  cwd,
2917
3181
  root: resolvedRoot,
2918
3182
  sessionPath: saved.path,
3183
+ modelId,
2919
3184
  sessionIdentity: saved.identity,
2920
3185
  modelPreset,
2921
3186
  mcpServers,
@@ -2942,6 +3207,7 @@ async function launchInput(
2942
3207
  sourceSessionPath: source.path,
2943
3208
  sourceSessionIdentity: source.identity,
2944
3209
  sourceCwd,
3210
+ modelId,
2945
3211
  modelPreset,
2946
3212
  mcpServers,
2947
3213
  worktree,
@@ -3645,6 +3911,7 @@ async function executeLifecycleResponse(
3645
3911
  ...(launch.sessionPath ? { sessionPath: launch.sessionPath } : {}),
3646
3912
  ...(launch.sessionIdentity ? { sessionIdentity: launch.sessionIdentity } : {}),
3647
3913
  ...(launch.modelPreset ? { modelPreset: launch.modelPreset } : {}),
3914
+ ...(launch.modelId ? { modelId: launch.modelId } : {}),
3648
3915
  ...(launch.mcpServers ? { mcpServers: launch.mcpServers } : {}),
3649
3916
  ...(launch.worktree ? { worktree: launch.worktree } : {}),
3650
3917
  ...(launch.readiness ? { readiness: launch.readiness } : {}),
@@ -3660,6 +3927,11 @@ async function executeLifecycleResponse(
3660
3927
  return spawn(cmd.file, cmd.args, {
3661
3928
  cwd: launch.cwd,
3662
3929
  detached: true,
3930
+ // stdio stays "ignore": `unref()` does not detach an active
3931
+ // stdio handle, so a parent-owned stderr pipe would keep the
3932
+ // broker process alive for the host's whole lifetime (retention
3933
+ // through restart/shutdown). Host stderr is therefore discarded
3934
+ // by the OS rather than captured (#4712 review).
3663
3935
  stdio: "ignore",
3664
3936
  env: {
3665
3937
  ...("kind" in cmd ? cmd.env : process.env),
@@ -3692,11 +3964,12 @@ async function executeLifecycleResponse(
3692
3964
  },
3693
3965
  });
3694
3966
  });
3695
- if (!authorizedSpawn.authorized)
3967
+ if (!authorizedSpawn.authorized) {
3696
3968
  return fail(
3697
3969
  "startup_admission_refused",
3698
3970
  "SDK host startup was refused because the broker no longer owns the session root.",
3699
3971
  );
3972
+ }
3700
3973
  const spawned = authorizedSpawn.value;
3701
3974
  child = spawned;
3702
3975
  const pid = spawned.pid;
@@ -3730,8 +4003,9 @@ async function executeLifecycleResponse(
3730
4003
  `Unable to establish spawned-session ownership and could not prove the child dead: ${error instanceof Error ? error.message : String(error)}`,
3731
4004
  );
3732
4005
  }
3733
- if (!child || !spawnedAuthority)
4006
+ if (!child || !spawnedAuthority) {
3734
4007
  return fail("spawn_failed", "Unable to retain the spawned session process identity.");
4008
+ }
3735
4009
  await broker.ledger.transition(identity, "awaiting_ready", { intendedSessionId: launch.id, effectMarker });
3736
4010
  const readiness = await waitForReady(
3737
4011
  broker,
@@ -3756,10 +4030,19 @@ async function executeLifecycleResponse(
3756
4030
  );
3757
4031
 
3758
4032
  if (!terminated)
3759
- return fail(
3760
- "terminal_uncertain",
3761
- `Session ${launch.id} did not become ready and its spawned process could not be verified dead.`,
3762
- );
4033
+ return readiness.kind === "ready_probe_failed"
4034
+ ? fail(
4035
+ "endpoint_unreadable",
4036
+ `Session ${launch.id} exited, and its readiness could not be determined: ${describeReadyProbeFailure(readiness.probe)}; cleanup could not be proven.`,
4037
+ )
4038
+ : fail(
4039
+ "terminal_uncertain",
4040
+ `Session ${launch.id} did not become ready and its spawned process could not be verified dead.`,
4041
+ );
4042
+ // Host stderr never reaches caller-visible error strings: the detached
4043
+ // host handles launch configuration and inherited credentials, so its
4044
+ // output cannot be proven free of secret material — it is discarded at
4045
+ // the OS level (stdio "ignore"), never captured (#4712 review).
3763
4046
  return readiness.kind === "startup_failed"
3764
4047
  ? fail(
3765
4048
  readiness.failure.code ?? "spawn_failed",
@@ -3767,16 +4050,28 @@ async function executeLifecycleResponse(
3767
4050
  undefined,
3768
4051
  readiness.failure.details,
3769
4052
  )
3770
- : readiness.kind === "child_exited"
3771
- ? fail("spawn_failed", `Session ${launch.id} exited before registering readiness.`)
3772
- : fail(
3773
- "readiness_timeout",
3774
- `Session ${launch.id} did not register an endpoint before the readiness timeout.`,
3775
- );
4053
+ : readiness.kind === "ready_then_exited"
4054
+ ? readyThenExitedResponse(launch.id, child)
4055
+ : readiness.kind === "ready_probe_failed"
4056
+ ? fail(
4057
+ "endpoint_unreadable",
4058
+ `Session ${launch.id} exited, and its readiness could not be determined: ${describeReadyProbeFailure(readiness.probe)}.`,
4059
+ )
4060
+ : readiness.kind === "child_exited"
4061
+ ? fail("spawn_failed", `Session ${launch.id} exited before registering readiness.`)
4062
+ : fail(
4063
+ "readiness_timeout",
4064
+ `Session ${launch.id} did not register an endpoint before the readiness timeout.`,
4065
+ );
3776
4066
  }
3777
4067
  await reconcileReadyScope(broker, launch.id, launch.cwd);
3778
4068
  const verified = await currentReadyAuthority(broker, launch.id, launch.root, spawnedAuthority);
3779
4069
  if (!verified || !sameReadyAuthority(readiness.authority, verified)) {
4070
+ const exited =
4071
+ child.exitCode !== null ||
4072
+ observeProcess(spawnedAuthority.pid, spawnedAuthority.incarnation, value =>
4073
+ processIncarnationForBroker(broker, value),
4074
+ ) === "exited";
3780
4075
  const terminated = await terminateSpawnedChild(
3781
4076
  child,
3782
4077
  broker,
@@ -3787,6 +4082,13 @@ async function executeLifecycleResponse(
3787
4082
  spawnedAuthority,
3788
4083
  timing,
3789
4084
  );
4085
+ if (exited && readyThenExitToleranceEnabled())
4086
+ return terminated
4087
+ ? readyThenExitedResponse(launch.id, child)
4088
+ : fail(
4089
+ "terminal_uncertain",
4090
+ `Session ${launch.id} ${READY_THEN_EXIT_MESSAGE} and cleanup could not be proven.`,
4091
+ );
3790
4092
  return terminated
3791
4093
  ? fail("endpoint_stale", "Session endpoint changed while lifecycle readiness was being verified.")
3792
4094
  : fail(
@@ -4870,40 +5172,64 @@ export async function executeLifecycle(
4870
5172
  ...(durableEffects ? { durableEffects } : {}),
4871
5173
  ...(startupFailure ? { startupFailure } : {}),
4872
5174
  };
5175
+ const provenExpected = expected;
5176
+ const provenRoot = root;
5177
+ const provenId = entry?.intendedSessionId;
5178
+ const provenDeadCleanup =
5179
+ Boolean(provenExpected) &&
5180
+ Boolean(provenRoot) &&
5181
+ Boolean(provenId) &&
5182
+ observeProcess(provenExpected!.pid, provenExpected!.incarnation, value =>
5183
+ processIncarnationForBroker(broker, value),
5184
+ ) === "exited" &&
5185
+ (await endpointRemoved(provenRoot!, provenId!));
4873
5186
  const terminalResponse: BrokerResponse =
4874
5187
  !response.ok &&
4875
5188
  entry?.effectMarker &&
4876
5189
  (operation === "session.create" || operation === "session.fork" || operation === "session.resume")
4877
5190
  ? entry.effectIntent?.childOwnershipEstablished === false
4878
5191
  ? response
4879
- : startupFailure && cleanupProof
4880
- ? {
4881
- ok: false,
4882
- error: startupFailure.code
4883
- ? {
4884
- code: startupFailure.code,
4885
- message: startupFailure.message,
4886
- details: startupFailure.details!,
4887
- endpoint: "unavailable" as const,
4888
- }
5192
+ : response.error.code === "endpoint_unreadable"
5193
+ ? response
5194
+ : // An unreadable/corrupt endpoint is already the fail-closed honest
5195
+ // terminal classification (it names the artifact, not the
5196
+ // child); the reconciliation wrapper must not erase it.
5197
+ response.error.code === "ready_then_exited" && (cleanupProof || provenDeadCleanup)
5198
+ ? {
5199
+ ...response,
5200
+ ...(durableEffects ? { durableEffects } : {}),
5201
+ ...(startupFailure ? { startupFailure } : {}),
5202
+ }
5203
+ : startupFailure && cleanupProof
5204
+ ? {
5205
+ ok: false,
5206
+ error: startupFailure.code
5207
+ ? {
5208
+ code: startupFailure.code,
5209
+ message: startupFailure.message,
5210
+ details: startupFailure.details!,
5211
+ endpoint: "unavailable" as const,
5212
+ }
5213
+ : {
5214
+ code: "spawn_failed",
5215
+ message: startupFailure.message,
5216
+ endpoint: "unavailable" as const,
5217
+ },
5218
+ ...(durableEffects ? { durableEffects } : {}),
5219
+ startupFailure,
5220
+ }
5221
+ : provenDeadCleanup && response.error.code !== "terminal_uncertain"
5222
+ ? response
4889
5223
  : {
4890
- code: "spawn_failed",
4891
- message: startupFailure.message,
4892
- endpoint: "unavailable" as const,
4893
- },
4894
- ...(durableEffects ? { durableEffects } : {}),
4895
- startupFailure,
4896
- }
4897
- : {
4898
- ok: false,
4899
- error: {
4900
- code: "terminal_uncertain",
4901
- message:
4902
- "Lifecycle startup cleanup could not be proven; retained artifacts require reconciliation.",
4903
- },
4904
- ...(durableEffects ? { durableEffects } : {}),
4905
- ...(startupFailure ? { startupFailure } : {}),
4906
- }
5224
+ ok: false,
5225
+ error: {
5226
+ code: "terminal_uncertain",
5227
+ message:
5228
+ "Lifecycle startup cleanup could not be proven; retained artifacts require reconciliation.",
5229
+ },
5230
+ ...(durableEffects ? { durableEffects } : {}),
5231
+ ...(startupFailure ? { startupFailure } : {}),
5232
+ }
4907
5233
  : response;
4908
5234
  return {
4909
5235
  response: terminalResponse,