@gajae-code/coding-agent 0.18.7 → 0.18.9

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 (344) hide show
  1. package/CHANGELOG.md +377 -1
  2. package/dist/types/async/endpoint-id.d.ts +16 -0
  3. package/dist/types/async/job-manager.d.ts +1 -1
  4. package/dist/types/async/support.d.ts +0 -14
  5. package/dist/types/capability/mcp.d.ts +2 -0
  6. package/dist/types/capability/skill.d.ts +2 -0
  7. package/dist/types/capability/types.d.ts +2 -0
  8. package/dist/types/cli/public-command-errors.d.ts +3 -1
  9. package/dist/types/cli/setup-cli.d.ts +1 -1
  10. package/dist/types/commands/deep-interview.d.ts +3 -0
  11. package/dist/types/commands/mcp-serve.d.ts +4 -0
  12. package/dist/types/config/autorouting-tier-map.d.ts +348 -9
  13. package/dist/types/config/file-lock.d.ts +1 -0
  14. package/dist/types/config/model-bindings-applier.d.ts +2 -0
  15. package/dist/types/config/model-profile-activation.d.ts +11 -3
  16. package/dist/types/config/model-registry.d.ts +4 -9
  17. package/dist/types/config/skill-settings-defaults.d.ts +2 -0
  18. package/dist/types/cursor-delete-path.d.ts +1 -0
  19. package/dist/types/cursor-mcp-name.d.ts +8 -0
  20. package/dist/types/customization/inventory.d.ts +2 -0
  21. package/dist/types/debug/report-bundle-parent.d.ts +2 -0
  22. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.d.ts +1 -0
  23. package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/version-manager.d.ts +31 -0
  24. package/dist/types/defaults/gjc-skills.generated.d.ts +5 -6
  25. package/dist/types/discovery/helpers.d.ts +6 -1
  26. package/dist/types/discovery/mcp-json.d.ts +1 -0
  27. package/dist/types/eval/py/kernel.d.ts +18 -0
  28. package/dist/types/export/html/css-color.d.ts +2 -0
  29. package/dist/types/export/html/html-export-destination.d.ts +2 -0
  30. package/dist/types/export/html/template.generated.d.ts +1 -1
  31. package/dist/types/extensibility/extensions/function-hooks-internal.d.ts +5 -0
  32. package/dist/types/extensibility/extensions/types.d.ts +7 -0
  33. package/dist/types/extensibility/gjc-plugins/runtime-adapters.d.ts +14 -1
  34. package/dist/types/extensibility/plugins/marketplace/cache.d.ts +2 -0
  35. package/dist/types/extensibility/plugins/plugin-link-path.d.ts +2 -0
  36. package/dist/types/extensibility/runtime-skill-discovery.d.ts +3 -1
  37. package/dist/types/extensibility/skill-management.d.ts +4 -1
  38. package/dist/types/extensibility/skills.d.ts +2 -0
  39. package/dist/types/gjc-runtime/deep-interview-crystallize.d.ts +74 -0
  40. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +11 -0
  41. package/dist/types/gjc-runtime/gc-runtime.d.ts +5 -0
  42. package/dist/types/gjc-runtime/psmux-detect.d.ts +3 -3
  43. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +0 -5
  44. package/dist/types/gjc-runtime/sidecar-bootstrap-env.d.ts +6 -0
  45. package/dist/types/gjc-runtime/state-argv.d.ts +1 -1
  46. package/dist/types/gjc-runtime/state-migrations.d.ts +8 -0
  47. package/dist/types/gjc-runtime/state-runtime.d.ts +95 -0
  48. package/dist/types/gjc-runtime/state-writer.d.ts +14 -1
  49. package/dist/types/gjc-runtime/tmux-common.d.ts +8 -5
  50. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +8 -0
  51. package/dist/types/internal-urls/skill-path.d.ts +1 -0
  52. package/dist/types/lsp/config.d.ts +8 -5
  53. package/dist/types/lsp/workspace-path.d.ts +1 -0
  54. package/dist/types/modes/acp/acp-agent.d.ts +7 -0
  55. package/dist/types/modes/components/custom-editor.d.ts +6 -0
  56. package/dist/types/modes/components/customization/customization-dashboard.d.ts +1 -0
  57. package/dist/types/modes/components/extensions/state-manager.d.ts +4 -3
  58. package/dist/types/modes/controllers/input-controller.d.ts +9 -0
  59. package/dist/types/modes/interactive-mode.d.ts +1 -0
  60. package/dist/types/modes/utils/interrupt-ownership.d.ts +3 -0
  61. package/dist/types/modes/utils/keybinding-matchers.d.ts +1 -0
  62. package/dist/types/modes/utils/macos-option-forwarding.d.ts +17 -0
  63. package/dist/types/progress/collect-project-progress.d.ts +38 -0
  64. package/dist/types/progress/progress-contract.d.ts +154 -0
  65. package/dist/types/progress/progress-snapshot.d.ts +11 -0
  66. package/dist/types/progress/project-progress.d.ts +141 -0
  67. package/dist/types/progress/render-progress.d.ts +28 -0
  68. package/dist/types/runtime-mcp/manager.d.ts +2 -1
  69. package/dist/types/runtime-mcp/mcp-redirect.d.ts +9 -0
  70. package/dist/types/runtime-mcp/oauth-public-url.d.ts +3 -0
  71. package/dist/types/runtime-mcp/plugin-network-boundary.d.ts +1 -0
  72. package/dist/types/runtime-mcp/project-host-values.d.ts +27 -0
  73. package/dist/types/runtime-mcp/smithery-registry.d.ts +24 -0
  74. package/dist/types/runtime-mcp/types.d.ts +6 -0
  75. package/dist/types/sdk/acp/adapter.d.ts +1 -1
  76. package/dist/types/sdk/broker/broker-exit.d.ts +6 -2
  77. package/dist/types/sdk/broker/broker.d.ts +24 -2
  78. package/dist/types/sdk/broker/doctor-restart.d.ts +5 -0
  79. package/dist/types/sdk/broker/ensure.d.ts +14 -1
  80. package/dist/types/sdk/broker/session-index.d.ts +12 -6
  81. package/dist/types/sdk/bus/index.d.ts +12 -1
  82. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +6 -1
  83. package/dist/types/sdk/client/client.d.ts +2 -0
  84. package/dist/types/sdk/host/query/handlers.d.ts +3 -0
  85. package/dist/types/sdk/index.d.ts +1 -0
  86. package/dist/types/sdk/router/session-router.d.ts +2 -0
  87. package/dist/types/sdk/session-list.d.ts +1 -1
  88. package/dist/types/sdk/session.d.ts +8 -0
  89. package/dist/types/session/agent-session.d.ts +38 -9
  90. package/dist/types/session/artifacts.d.ts +2 -2
  91. package/dist/types/session/internal/managed-gc-retirement-codec.d.ts +32 -0
  92. package/dist/types/session/internal/managed-session-scope.d.ts +79 -2
  93. package/dist/types/session/internal/managed-session-storage.d.ts +26 -1
  94. package/dist/types/session/internal/managed-task-owner-cleanup.d.ts +62 -0
  95. package/dist/types/session/internal/task-artifact-owner-access.d.ts +2 -0
  96. package/dist/types/session/internal/task-artifact-owner-transcript.d.ts +9 -1
  97. package/dist/types/session/session-manager.d.ts +31 -5
  98. package/dist/types/session/session-retirement.d.ts +31 -7
  99. package/dist/types/session/session-storage.d.ts +69 -2
  100. package/dist/types/session/task-artifact-owner-codec.d.ts +2 -0
  101. package/dist/types/setup/credential-auto-import.d.ts +1 -1
  102. package/dist/types/setup/credential-import.d.ts +6 -4
  103. package/dist/types/skill-state/active-state.d.ts +38 -0
  104. package/dist/types/skill-state/workflow-mutation-guard.d.ts +3 -0
  105. package/dist/types/slash-commands/helpers/progress-report.d.ts +8 -0
  106. package/dist/types/task/executor.d.ts +3 -0
  107. package/dist/types/task/isolation-dir.d.ts +26 -0
  108. package/dist/types/tools/ast-edit-write-path.d.ts +13 -0
  109. package/dist/types/tools/bash-skill-urls.d.ts +1 -1
  110. package/dist/types/tools/browser/cdp-url.d.ts +2 -0
  111. package/dist/types/tools/github-side-effect.d.ts +22 -0
  112. package/dist/types/tools/index.d.ts +3 -0
  113. package/dist/types/tools/monitor-mutation.d.ts +9 -0
  114. package/dist/types/tools/python.d.ts +9 -1
  115. package/dist/types/tools/recipe/recipe-guard.d.ts +7 -0
  116. package/dist/types/tools/restricted-cwd.d.ts +1 -0
  117. package/dist/types/tools/todo-write.d.ts +6 -0
  118. package/dist/types/utils/tools-checksum.d.ts +7 -0
  119. package/package.json +7 -7
  120. package/scripts/generate-sdk-operation-inventory.ts +21 -0
  121. package/scripts/verify-managed-empty-stop.ts +28 -0
  122. package/src/async/endpoint-id.ts +26 -0
  123. package/src/async/job-manager.ts +247 -132
  124. package/src/async/support.ts +0 -25
  125. package/src/capability/index.ts +15 -0
  126. package/src/capability/mcp.ts +2 -0
  127. package/src/capability/skill.ts +2 -0
  128. package/src/capability/types.ts +2 -0
  129. package/src/cli/auth-broker-cli.ts +1 -1
  130. package/src/cli/customize-doctor.ts +15 -2
  131. package/src/cli/doctor/repairs.ts +4 -1
  132. package/src/cli/doctor-supervisor.ts +3 -0
  133. package/src/cli/public-command-errors.ts +21 -1
  134. package/src/cli/public-command-registry.ts +28 -6
  135. package/src/cli/setup-cli.ts +4 -4
  136. package/src/cli/skills-cli.ts +5 -0
  137. package/src/commands/deep-interview.ts +47 -8
  138. package/src/commands/mcp-serve.ts +7 -0
  139. package/src/commands/sdk.ts +455 -397
  140. package/src/config/autorouting-tier-map.ts +117 -7
  141. package/src/config/file-lock.ts +316 -79
  142. package/src/config/mcp-schema.json +4 -0
  143. package/src/config/model-bindings-applier.ts +20 -0
  144. package/src/config/model-profile-activation.ts +240 -46
  145. package/src/config/model-registry.ts +47 -153
  146. package/src/config/skill-settings-defaults.ts +7 -0
  147. package/src/coordinator-mcp/policy.ts +3 -2
  148. package/src/coordinator-mcp/server.ts +2 -0
  149. package/src/cursor-delete-path.ts +10 -0
  150. package/src/cursor-mcp-name.ts +15 -0
  151. package/src/cursor.ts +13 -2
  152. package/src/customization/inventory.ts +4 -13
  153. package/src/dap/config.ts +2 -2
  154. package/src/debug/report-bundle-parent.ts +22 -0
  155. package/src/debug/report-bundle.ts +3 -0
  156. package/src/debug/system-info.ts +13 -1
  157. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.ts +10 -6
  158. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.ts +67 -4
  159. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/version-manager.ts +80 -0
  160. package/src/defaults/gjc/skills/deep-interview/SKILL.md +105 -10
  161. package/src/defaults/gjc/skills/ralplan/SKILL.md +6 -4
  162. package/src/defaults/gjc-defaults.test.ts +18 -1
  163. package/src/defaults/gjc-defaults.ts +3 -0
  164. package/src/defaults/gjc-skills.generated.ts +58 -36
  165. package/src/discovery/agents.ts +6 -1
  166. package/src/discovery/builtin.ts +13 -1
  167. package/src/discovery/helpers.ts +171 -29
  168. package/src/discovery/mcp-json.ts +26 -12
  169. package/src/discovery/ssh.ts +1 -1
  170. package/src/discovery/substitute-plugin-root.ts +3 -1
  171. package/src/edit/file-read-cache.ts +1 -1
  172. package/src/eval/py/executor.ts +796 -154
  173. package/src/eval/py/kernel.ts +79 -13
  174. package/src/eval/py/tool-bridge.ts +4 -0
  175. package/src/export/html/css-color.ts +9 -0
  176. package/src/export/html/html-export-destination.ts +16 -0
  177. package/src/export/html/index.ts +7 -10
  178. package/src/export/html/template.generated.ts +1 -1
  179. package/src/extensibility/extensions/function-hooks-internal.ts +27 -0
  180. package/src/extensibility/extensions/runner.ts +15 -1
  181. package/src/extensibility/extensions/types.ts +7 -0
  182. package/src/extensibility/gjc-plugins/mcp-policy.ts +2 -1
  183. package/src/extensibility/gjc-plugins/runtime-adapters.ts +85 -8
  184. package/src/extensibility/plugins/manager.ts +3 -2
  185. package/src/extensibility/plugins/marketplace/cache.ts +15 -0
  186. package/src/extensibility/plugins/marketplace/manager.ts +11 -3
  187. package/src/extensibility/plugins/plugin-link-path.ts +12 -0
  188. package/src/extensibility/runtime-skill-discovery.ts +20 -2
  189. package/src/extensibility/skill-management.ts +9 -2
  190. package/src/extensibility/skills.ts +6 -1
  191. package/src/gjc-runtime/deep-interview-crystallize.ts +1814 -0
  192. package/src/gjc-runtime/deep-interview-recorder.ts +20 -2
  193. package/src/gjc-runtime/deep-interview-runtime.ts +1415 -13
  194. package/src/gjc-runtime/deep-interview-stage.ts +98 -21
  195. package/src/gjc-runtime/gc-runtime.ts +206 -9
  196. package/src/gjc-runtime/launch-worktree.ts +70 -9
  197. package/src/gjc-runtime/psmux-detect.ts +28 -4
  198. package/src/gjc-runtime/ralplan-runtime.ts +447 -52
  199. package/src/gjc-runtime/session-state-sidecar.ts +3 -2
  200. package/src/gjc-runtime/sidecar-bootstrap-env.ts +19 -0
  201. package/src/gjc-runtime/state-argv.ts +2 -0
  202. package/src/gjc-runtime/state-migrations.ts +91 -29
  203. package/src/gjc-runtime/state-runtime.ts +3716 -135
  204. package/src/gjc-runtime/state-writer.ts +105 -16
  205. package/src/gjc-runtime/tmux-common.ts +8 -5
  206. package/src/gjc-runtime/ultragoal-runtime.ts +16 -4
  207. package/src/gjc-runtime/workflow-manifest.generated.json +70 -42
  208. package/src/gjc-runtime/workflow-manifest.ts +54 -21
  209. package/src/harness-control-plane/operate.ts +12 -4
  210. package/src/harness-control-plane/owner.ts +5 -1
  211. package/src/hashline/parser.ts +48 -9
  212. package/src/hashline/recovery.ts +13 -4
  213. package/src/internal-urls/docs-index.generated.ts +20 -19
  214. package/src/internal-urls/skill-path.ts +19 -0
  215. package/src/internal-urls/skill-protocol.ts +2 -0
  216. package/src/lsp/config.ts +12 -63
  217. package/src/lsp/edits.ts +6 -0
  218. package/src/lsp/workspace-path.ts +51 -0
  219. package/src/main.ts +195 -31
  220. package/src/memories/index.ts +88 -7
  221. package/src/modes/acp/acp-agent.ts +88 -4
  222. package/src/modes/components/custom-editor.ts +14 -0
  223. package/src/modes/components/customization/customization-dashboard.ts +12 -0
  224. package/src/modes/components/extensions/extension-dashboard.ts +2 -2
  225. package/src/modes/components/extensions/state-manager.ts +15 -5
  226. package/src/modes/components/hook-editor.ts +1 -1
  227. package/src/modes/components/hook-selector.ts +1 -1
  228. package/src/modes/components/model-selector.ts +1 -4
  229. package/src/modes/components/provider-onboarding-selector.ts +1 -1
  230. package/src/modes/components/status-line/segments.ts +4 -0
  231. package/src/modes/controllers/event-controller.ts +11 -4
  232. package/src/modes/controllers/input-controller.ts +76 -31
  233. package/src/modes/controllers/runtime-mcp-command-controller.ts +29 -16
  234. package/src/modes/controllers/selector-controller.ts +24 -15
  235. package/src/modes/controllers/todo-command-controller.ts +1 -1
  236. package/src/modes/interactive-mode.ts +5 -1
  237. package/src/modes/runtime-init.ts +4 -0
  238. package/src/modes/utils/abort-message.ts +3 -1
  239. package/src/modes/utils/interrupt-ownership.ts +12 -0
  240. package/src/modes/utils/keybinding-matchers.ts +4 -0
  241. package/src/modes/utils/macos-option-forwarding.ts +80 -0
  242. package/src/progress/collect-project-progress.ts +153 -0
  243. package/src/progress/progress-contract.ts +172 -0
  244. package/src/progress/progress-snapshot.ts +140 -0
  245. package/src/progress/project-progress.ts +446 -0
  246. package/src/progress/render-progress.ts +209 -0
  247. package/src/prompts/system/master-mode.md +1 -0
  248. package/src/prompts/tools/skill-discovery.md +1 -1
  249. package/src/runtime-mcp/client.ts +4 -2
  250. package/src/runtime-mcp/config-writer.ts +10 -6
  251. package/src/runtime-mcp/config.ts +1 -0
  252. package/src/runtime-mcp/json-rpc.ts +2 -1
  253. package/src/runtime-mcp/manager.ts +29 -11
  254. package/src/runtime-mcp/mcp-redirect.ts +73 -0
  255. package/src/runtime-mcp/oauth-flow.ts +14 -2
  256. package/src/runtime-mcp/oauth-public-url.ts +14 -0
  257. package/src/runtime-mcp/plugin-network-boundary.ts +4 -0
  258. package/src/runtime-mcp/pool.ts +6 -0
  259. package/src/runtime-mcp/project-host-values.ts +46 -0
  260. package/src/runtime-mcp/smithery-registry.ts +22 -3
  261. package/src/runtime-mcp/transports/http.ts +4 -3
  262. package/src/runtime-mcp/types.ts +6 -0
  263. package/src/sdk/acp/adapter.ts +95 -8
  264. package/src/sdk/broker/broker-exit.ts +52 -23
  265. package/src/sdk/broker/broker.ts +81 -6
  266. package/src/sdk/broker/doctor-restart.ts +4 -0
  267. package/src/sdk/broker/ensure.ts +31 -2
  268. package/src/sdk/broker/lifecycle.ts +711 -66
  269. package/src/sdk/broker/session-index.ts +191 -81
  270. package/src/sdk/bus/chat-daemon-cli.ts +38 -1
  271. package/src/sdk/bus/chat-daemon-control.ts +2 -2
  272. package/src/sdk/bus/index.ts +238 -43
  273. package/src/sdk/bus/telegram-daemon-contract.ts +6 -1
  274. package/src/sdk/cli/session-cli.ts +17 -3
  275. package/src/sdk/client/client.ts +28 -8
  276. package/src/sdk/host/query/handlers.ts +9 -1
  277. package/src/sdk/host/session-runtime.test.ts +2705 -303
  278. package/src/sdk/host/session-runtime.ts +490 -143
  279. package/src/sdk/host/surface-policy.ts +1 -0
  280. package/src/sdk/index.ts +8 -0
  281. package/src/sdk/lifecycle-session.ts +3 -1
  282. package/src/sdk/prompt-deadline-manager.ts +15 -0
  283. package/src/sdk/protocol/operation-inventory.generated.json +146 -0
  284. package/src/sdk/protocol/operation-registry.ts +9 -2
  285. package/src/sdk/router/session-router.ts +82 -18
  286. package/src/sdk/session-list.ts +1 -0
  287. package/src/sdk/session.ts +69 -45
  288. package/src/session/agent-session.ts +679 -238
  289. package/src/session/artifacts.ts +176 -71
  290. package/src/session/internal/managed-gc-retirement-codec.ts +319 -0
  291. package/src/session/internal/managed-session-scope.ts +3779 -462
  292. package/src/session/internal/managed-session-storage.ts +501 -78
  293. package/src/session/internal/managed-task-owner-cleanup.ts +473 -0
  294. package/src/session/internal/task-artifact-owner-access.ts +19 -3
  295. package/src/session/internal/task-artifact-owner-transcript.ts +547 -1
  296. package/src/session/session-manager.ts +951 -133
  297. package/src/session/session-retirement.ts +573 -14
  298. package/src/session/session-storage.ts +447 -15
  299. package/src/session/task-artifact-owner-codec.ts +71 -8
  300. package/src/session/task-artifact-owner-retirement.ts +21 -19
  301. package/src/setup/credential-auto-import.ts +12 -3
  302. package/src/setup/credential-import.ts +176 -5
  303. package/src/setup/paseo/announce.ts +6 -1
  304. package/src/skill-state/active-state.ts +145 -2
  305. package/src/skill-state/workflow-mutation-guard.ts +102 -10
  306. package/src/slash-commands/builtin-registry.ts +39 -9
  307. package/src/slash-commands/helpers/mcp.ts +8 -4
  308. package/src/slash-commands/helpers/progress-report.ts +23 -0
  309. package/src/stt/setup.ts +3 -5
  310. package/src/task/commands.ts +3 -1
  311. package/src/task/executor.ts +4 -0
  312. package/src/task/index.ts +6 -0
  313. package/src/task/isolation-dir.ts +143 -0
  314. package/src/task/worktree.ts +16 -7
  315. package/src/tools/ask.ts +205 -6
  316. package/src/tools/ast-edit-write-path.ts +68 -0
  317. package/src/tools/ast-edit.ts +10 -1
  318. package/src/tools/bash-allowed-prefixes.ts +15 -0
  319. package/src/tools/bash-skill-urls.ts +56 -3
  320. package/src/tools/bash.ts +48 -79
  321. package/src/tools/browser/cdp-url.ts +18 -0
  322. package/src/tools/browser.ts +8 -1
  323. package/src/tools/descriptors.ts +5 -1
  324. package/src/tools/eval.ts +56 -36
  325. package/src/tools/gh.ts +10 -0
  326. package/src/tools/github-side-effect.ts +112 -0
  327. package/src/tools/image-gen.ts +1 -1
  328. package/src/tools/index.ts +3 -0
  329. package/src/tools/monitor-mutation.ts +27 -0
  330. package/src/tools/monitor.ts +7 -0
  331. package/src/tools/python.ts +188 -66
  332. package/src/tools/recipe/index.ts +9 -0
  333. package/src/tools/recipe/recipe-guard.ts +19 -0
  334. package/src/tools/restricted-cwd.ts +10 -0
  335. package/src/tools/skill-discovery.ts +5 -0
  336. package/src/tools/skill.ts +28 -1
  337. package/src/tools/todo-write.ts +10 -1
  338. package/src/tools/tool-catalog.generated.ts +1 -1
  339. package/src/tools/write.ts +1 -0
  340. package/src/utils/external-editor.ts +3 -2
  341. package/src/utils/tools-checksum.ts +34 -0
  342. package/src/utils/tools-manager.ts +24 -6
  343. package/src/web/insane/url-guard.ts +2 -1
  344. package/src/web/search/providers/codex.ts +3 -2
@@ -10,11 +10,12 @@ import {
10
10
  type NativeDirectoryTreeSnapshot,
11
11
  snapshotDirectoryTree,
12
12
  } from "@gajae-code/natives";
13
- import { logger, postmortem } from "@gajae-code/utils";
13
+ import { getAgentDir, logger, postmortem } from "@gajae-code/utils";
14
14
  import { CliParseError, Command } from "@gajae-code/utils/cli";
15
15
  import type { Args as ParsedArgs } from "../cli/args";
16
16
  import { isSafeSdkInternalAgentDir, scanPublicCommand } from "../cli/public-command-entry";
17
17
  import { PublicCommandFailure } from "../cli/public-command-errors";
18
+ import { FileLockAcquireError } from "../config/file-lock";
18
19
  import { parseModelString } from "../config/model-resolver";
19
20
  import { Settings } from "../config/settings";
20
21
  import { usePostmortemSignalExitAuthority } from "../lsp/client";
@@ -31,8 +32,10 @@ import {
31
32
  import { readBrokerDiscovery } from "../sdk/broker/discovery";
32
33
  import { type EndpointFileRead, readEndpointFile } from "../sdk/broker/endpoint-authority";
33
34
  import {
35
+ BROKER_DISCOVERY_BUDGET,
34
36
  emitBrokerStartupTestSignal,
35
37
  reconcileBrokerGenerationForStartup,
38
+ SDK_BROKER_AUTOSTART_ENV,
36
39
  withBrokerStartupLock,
37
40
  } from "../sdk/broker/ensure";
38
41
  import {
@@ -1602,7 +1605,16 @@ export default class Sdk extends Command {
1602
1605
  const scan = scanPublicCommand("sdk", this.argv);
1603
1606
  if (scan.kind !== "operation") throw new PublicCommandFailure({ kind: "usage", proof: "pre-effect" });
1604
1607
  const { args, flags } = scan;
1608
+ // `--attach-only` is the flag form of GJC_SDK_BROKER_AUTOSTART=0 for this invocation.
1609
+ if (flags["attach-only"] === true) process.env[SDK_BROKER_AUTOSTART_ENV] = "0";
1605
1610
  const operation = scan.descriptor.command[1];
1611
+ if (operation === "broker") {
1612
+ const brokerAgentDir = path.resolve((flags["agent-dir"] as string | undefined) ?? getAgentDir());
1613
+ if (!isSafeSdkInternalAgentDir(brokerAgentDir))
1614
+ throw new PublicCommandFailure({ kind: "usage", proof: "pre-effect" });
1615
+ await runSdkInternal({ action: "broker-internal", agentDir: brokerAgentDir }, true);
1616
+ return;
1617
+ }
1606
1618
  const stringFlag = (name: string): string | undefined => flags[name] as string | undefined;
1607
1619
  const timeoutMs = flags["timeout-ms"] === undefined ? undefined : Number(flags["timeout-ms"]);
1608
1620
  if (operation === "spawn") {
@@ -1697,421 +1709,467 @@ export default class Sdk extends Command {
1697
1709
  throw new PublicCommandFailure({ kind: "usage", proof: "pre-effect" });
1698
1710
  }
1699
1711
 
1700
- const internal = parseSdkInternalArgv(this.argv);
1701
- if (internal.action === "broker-trampoline-internal") {
1702
- if (process.platform === "win32") throw new CliParseError("Broker trampoline is unavailable on Windows.");
1703
- const command = resolveSdkInternalSpawnCommand("broker-internal");
1704
- let child: ChildProcess;
1705
- try {
1706
- child = spawn(command.file, [...command.args, "--agent-dir", internal.agentDir], {
1707
- detached: true,
1708
- stdio: ["ignore", "ignore", "inherit"],
1709
- env: command.env,
1710
- ...(command.kind === "bun-source" ? { cwd: command.cwd } : {}),
1711
- });
1712
- } catch (error) {
1713
- throw new Error("Broker trampoline could not start broker.", { cause: error });
1714
- }
1715
- let spawnError: Error | undefined;
1716
- child.once("error", error => {
1717
- spawnError = error;
1712
+ await runSdkInternal(parseSdkInternalArgv(this.argv));
1713
+ }
1714
+ }
1715
+
1716
+ /**
1717
+ * Runs one parsed SDK internal entry point. `publicBrokerRun` is set only by
1718
+ * the stable `gjc sdk broker run` contract, which must report a lost ownership
1719
+ * race as a failure so its supervisor sees it, instead of the quiet exit 0 that
1720
+ * the detached autostart entry uses.
1721
+ */
1722
+ async function runSdkInternal(internal: SdkInternalArgv, publicBrokerRun = false): Promise<void> {
1723
+ if (internal.action === "broker-trampoline-internal") {
1724
+ if (process.platform === "win32") throw new CliParseError("Broker trampoline is unavailable on Windows.");
1725
+ const command = resolveSdkInternalSpawnCommand("broker-internal");
1726
+ let child: ChildProcess;
1727
+ try {
1728
+ child = spawn(command.file, [...command.args, "--agent-dir", internal.agentDir], {
1729
+ detached: true,
1730
+ stdio: ["ignore", "ignore", "inherit"],
1731
+ env: command.env,
1732
+ ...(command.kind === "bun-source" ? { cwd: command.cwd } : {}),
1718
1733
  });
1719
- if (child.pid === undefined) throw new Error("Broker trampoline could not start broker.");
1720
- const incarnation = processIncarnation(child.pid);
1721
- if (!incarnation) {
1722
- try {
1723
- child.kill("SIGKILL");
1724
- } catch {
1725
- // The child may already have exited; either way identity was not established.
1726
- }
1727
- throw new Error("Broker trampoline could not observe broker identity.");
1734
+ } catch (error) {
1735
+ throw new Error("Broker trampoline could not start broker.", { cause: error });
1736
+ }
1737
+ let spawnError: Error | undefined;
1738
+ child.once("error", error => {
1739
+ spawnError = error;
1740
+ });
1741
+ if (child.pid === undefined) throw new Error("Broker trampoline could not start broker.");
1742
+ const incarnation = processIncarnation(child.pid);
1743
+ if (!incarnation) {
1744
+ try {
1745
+ child.kill("SIGKILL");
1746
+ } catch {
1747
+ // The child may already have exited; either way identity was not established.
1728
1748
  }
1729
- if (spawnError) throw new Error("Broker trampoline could not start broker.", { cause: spawnError });
1730
- const stdoutWrite = Promise.withResolvers<void>();
1731
- process.stdout.write(`${child.pid}\t${incarnation}\n`, error => {
1732
- if (error) stdoutWrite.reject(error);
1733
- else stdoutWrite.resolve();
1734
- });
1749
+ throw new Error("Broker trampoline could not observe broker identity.");
1750
+ }
1751
+ if (spawnError) throw new Error("Broker trampoline could not start broker.", { cause: spawnError });
1752
+ const stdoutWrite = Promise.withResolvers<void>();
1753
+ process.stdout.write(`${child.pid}\t${incarnation}\n`, error => {
1754
+ if (error) stdoutWrite.reject(error);
1755
+ else stdoutWrite.resolve();
1756
+ });
1757
+ try {
1758
+ await stdoutWrite.promise;
1759
+ } catch (error) {
1735
1760
  try {
1736
- await stdoutWrite.promise;
1737
- } catch (error) {
1738
- try {
1739
- child.kill("SIGKILL");
1740
- } catch {
1741
- // The child may already have exited; reporting failure remains authoritative.
1742
- }
1743
- throw error;
1761
+ child.kill("SIGKILL");
1762
+ } catch {
1763
+ // The child may already have exited; reporting failure remains authoritative.
1744
1764
  }
1745
- child.unref();
1746
- return;
1765
+ throw error;
1747
1766
  }
1748
- if (internal.action === "broker-internal" || internal.action === "session-host-internal")
1749
- usePostmortemSignalExitAuthority();
1750
- if (internal.action === "session-host-internal") {
1751
- await runSessionHost();
1767
+ child.unref();
1768
+ return;
1769
+ }
1770
+ if (internal.action === "broker-internal" || internal.action === "session-host-internal")
1771
+ usePostmortemSignalExitAuthority();
1772
+ if (internal.action === "session-host-internal") {
1773
+ await runSessionHost();
1774
+ return;
1775
+ }
1776
+ if (internal.action === "stderr-drain-internal") {
1777
+ await runSdkStderrDrainer(internal.logPath, internal.maxBytes);
1778
+ return;
1779
+ }
1780
+ const agentDir = path.resolve(internal.agentDir);
1781
+ let broker: Broker | undefined;
1782
+ let runningBroker: Broker | undefined;
1783
+ let stopSweep: (() => void) | undefined;
1784
+ let startupExitRequested = false;
1785
+ let startupExitReason: BrokerStartupExitRecord["reason"] | undefined;
1786
+ let startupExitLogged = false;
1787
+ let pendingShutdownSignal: "SIGTERM" | "SIGINT" | undefined;
1788
+ let startupExitTask: Promise<boolean> | undefined;
1789
+ const startupStartedAt = process.hrtime.bigint();
1790
+ let startupExitWrite: Promise<BrokerStartupExitWriteStatus> | undefined;
1791
+ const startupAbortController = new AbortController();
1792
+ let candidateStartTask: Promise<unknown> | undefined;
1793
+ const waitForStartupAbortCleanup = async (): Promise<void> => {
1794
+ const task = candidateStartTask;
1795
+ if (!task) return;
1796
+ await Promise.race([
1797
+ task.then(
1798
+ () => undefined,
1799
+ () => undefined,
1800
+ ),
1801
+ Bun.sleep(BROKER_STARTUP_MARKER_WRITE_TIMEOUT_MS),
1802
+ ]);
1803
+ };
1804
+ const startupSignalExitCode = (signal: "SIGTERM" | "SIGINT"): 130 | 143 => (signal === "SIGINT" ? 130 : 143);
1805
+ const makeStartupExitRecord = (
1806
+ reason: BrokerStartupExitRecord["reason"],
1807
+ exitCode: 1 | 130 | 143,
1808
+ signal: "SIGTERM" | "SIGINT" | null,
1809
+ timeoutMs?: number,
1810
+ blockingLockPath?: string,
1811
+ ): BrokerStartupExitRecord & Record<string, unknown> => ({
1812
+ version: 1,
1813
+ reason,
1814
+ mode: "startup",
1815
+ fenceReason: null,
1816
+ fencedForMs: 0,
1817
+ uptimeMs: Math.max(0, Number((process.hrtime.bigint() - startupStartedAt) / 1_000_000n)),
1818
+ pid: process.pid,
1819
+ signal,
1820
+ exitCode,
1821
+ timeoutMs: timeoutMs ?? null,
1822
+ writtenAt: Date.now(),
1823
+ ...(blockingLockPath === undefined ? {} : { blockingLockPath }),
1824
+ });
1825
+ const beginStartupExitRecordWrite = (record: BrokerStartupExitRecord): Promise<BrokerStartupExitWriteStatus> => {
1826
+ const testDelayMs = Number(process.env.GJC_SDK_TEST_BROKER_STARTUP_EXIT_RECORD_DELAY_MS ?? 0);
1827
+ const beforeWriteForTest =
1828
+ Number.isSafeInteger(testDelayMs) && testDelayMs > 0 && testDelayMs <= 10_000
1829
+ ? async () => {
1830
+ await emitBrokerStartupTestSignal("startup-exit-record-fallback-waiting");
1831
+ await Bun.sleep(testDelayMs);
1832
+ }
1833
+ : undefined;
1834
+ startupExitWrite ??= writeBrokerStartupExitRecordBounded(
1835
+ agentDir,
1836
+ record,
1837
+ record.reason === "startup-signal"
1838
+ ? BROKER_STARTUP_SIGNAL_EXIT_RECORD_WRITE_TIMEOUT_MS
1839
+ : BROKER_STARTUP_MARKER_WRITE_TIMEOUT_MS,
1840
+ beforeWriteForTest,
1841
+ );
1842
+ return startupExitWrite;
1843
+ };
1844
+ const brokerReadyForSignal = (): Broker | undefined => runningBroker ?? (broker?.ownsDiscovery ? broker : undefined);
1845
+ const writeStartupExitLog = (record: BrokerStartupExitRecord & Record<string, unknown>, message: string): void => {
1846
+ if (startupExitLogged) return;
1847
+ startupExitLogged = true;
1848
+ const level = record.exitCode === 1 ? "error" : "warn";
1849
+ if (record.exitCode === 1) logger.error("sdk broker: startup watchdog forcing exit", record);
1850
+ else logger.warn("sdk broker: startup interrupted", record);
1851
+ process.stderr.write(`${JSON.stringify({ level, message, ...record })}\n`);
1852
+ };
1853
+ const exitDuringStartup = (
1854
+ reason: BrokerStartupExitRecord["reason"],
1855
+ exitCode: 1 | 130 | 143,
1856
+ signal: "SIGTERM" | "SIGINT" | null,
1857
+ timeoutMs?: number,
1858
+ blockingLockPath?: string,
1859
+ ): Promise<boolean> => {
1860
+ // The first startup exit cause owns both the persisted reason and the process exit code.
1861
+ if (startupExitTask) return startupExitReason === reason ? startupExitTask : Promise.resolve(false);
1862
+ if (startupExitRequested) return Promise.resolve(false);
1863
+ startupExitRequested = true;
1864
+ startupExitReason = reason;
1865
+ const exitRecord = makeStartupExitRecord(reason, exitCode, signal, timeoutMs, blockingLockPath);
1866
+ const message =
1867
+ reason === "startup-lock-blocked" && blockingLockPath
1868
+ ? `SDK broker startup blocked by retained removal transition ${blockingLockPath}.`
1869
+ : reason === "startup-deadline"
1870
+ ? `SDK broker startup exceeded its ${timeoutMs}ms fence deadline.`
1871
+ : `SDK broker startup interrupted by ${signal} before readiness.`;
1872
+ writeStartupExitLog(exitRecord, message);
1873
+ startupExitTask = (async () => {
1874
+ const exitRecordWrite = await beginStartupExitRecordWrite(exitRecord);
1875
+ const exitRecordWritten = exitRecordWrite.kind === "written";
1876
+ if (!exitRecordWritten)
1877
+ logger.error("sdk broker: startup exit record write timed out", {
1878
+ reason: "startup-exit-record-write-failed-or-timed-out",
1879
+ exitReason: reason,
1880
+ exitRecordWriteStatus: exitRecordWrite.kind,
1881
+ ...(exitRecordWrite.kind === "failed" && exitRecordWrite.code
1882
+ ? { exitRecordWriteErrorCode: exitRecordWrite.code }
1883
+ : {}),
1884
+ pid: process.pid,
1885
+ exitCode,
1886
+ });
1887
+ return true;
1888
+ })();
1889
+ return startupExitTask;
1890
+ };
1891
+ const unregisterPostmortem = postmortem.register("sdk-broker:exit", async reason => {
1892
+ const signal =
1893
+ reason === postmortem.Reason.SIGTERM
1894
+ ? "SIGTERM"
1895
+ : reason === postmortem.Reason.SIGINT
1896
+ ? "SIGINT"
1897
+ : pendingShutdownSignal;
1898
+ if (!signal) return;
1899
+ pendingShutdownSignal = signal;
1900
+ if (startupExitTask && startupExitReason !== "startup-signal") {
1901
+ await startupExitTask;
1902
+ await waitForStartupAbortCleanup();
1903
+ process.exit(1);
1752
1904
  return;
1753
1905
  }
1754
- if (internal.action === "stderr-drain-internal") {
1755
- await runSdkStderrDrainer(internal.logPath, internal.maxBytes);
1906
+ const activeBroker = brokerReadyForSignal();
1907
+ if (activeBroker) {
1908
+ runningBroker = activeBroker;
1909
+ stopSweep?.();
1910
+ await activeBroker.stop({ kind: "signal", signal });
1756
1911
  return;
1757
1912
  }
1758
- const agentDir = path.resolve(internal.agentDir);
1759
- let broker: Broker | undefined;
1760
- let runningBroker: Broker | undefined;
1761
- let stopSweep: (() => void) | undefined;
1762
- let startupExitRequested = false;
1763
- let startupExitReason: "startup-deadline" | "startup-signal" | undefined;
1764
- let startupExitLogged = false;
1765
- let pendingShutdownSignal: "SIGTERM" | "SIGINT" | undefined;
1766
- let startupExitTask: Promise<boolean> | undefined;
1767
- const startupStartedAt = process.hrtime.bigint();
1768
- let startupExitWrite: Promise<BrokerStartupExitWriteStatus> | undefined;
1769
- const startupAbortController = new AbortController();
1770
- let candidateStartTask: Promise<unknown> | undefined;
1771
- const waitForStartupAbortCleanup = async (): Promise<void> => {
1772
- const task = candidateStartTask;
1773
- if (!task) return;
1774
- await Promise.race([
1775
- task.then(
1776
- () => undefined,
1777
- () => undefined,
1778
- ),
1779
- Bun.sleep(BROKER_STARTUP_MARKER_WRITE_TIMEOUT_MS),
1780
- ]);
1781
- };
1782
- const startupSignalExitCode = (signal: "SIGTERM" | "SIGINT"): 130 | 143 => (signal === "SIGINT" ? 130 : 143);
1783
- const makeStartupExitRecord = (
1784
- reason: "startup-deadline" | "startup-signal",
1785
- exitCode: 1 | 130 | 143,
1786
- signal: "SIGTERM" | "SIGINT" | null,
1787
- timeoutMs?: number,
1788
- ): BrokerStartupExitRecord & Record<string, unknown> => ({
1789
- version: 1,
1790
- reason,
1791
- mode: "startup",
1792
- fenceReason: null,
1793
- fencedForMs: 0,
1794
- uptimeMs: Math.max(0, Number((process.hrtime.bigint() - startupStartedAt) / 1_000_000n)),
1795
- pid: process.pid,
1796
- signal,
1797
- exitCode,
1798
- timeoutMs: timeoutMs ?? null,
1799
- writtenAt: Date.now(),
1800
- });
1801
- const beginStartupExitRecordWrite = (record: BrokerStartupExitRecord): Promise<BrokerStartupExitWriteStatus> => {
1802
- const testDelayMs = Number(process.env.GJC_SDK_TEST_BROKER_STARTUP_EXIT_RECORD_DELAY_MS ?? 0);
1803
- const beforeWriteForTest =
1804
- Number.isSafeInteger(testDelayMs) && testDelayMs > 0 && testDelayMs <= 10_000
1805
- ? async () => {
1806
- await emitBrokerStartupTestSignal("startup-exit-record-fallback-waiting");
1807
- await Bun.sleep(testDelayMs);
1808
- }
1809
- : undefined;
1810
- startupExitWrite ??= writeBrokerStartupExitRecordBounded(
1811
- agentDir,
1812
- record,
1813
- record.reason === "startup-signal"
1814
- ? BROKER_STARTUP_SIGNAL_EXIT_RECORD_WRITE_TIMEOUT_MS
1815
- : BROKER_STARTUP_MARKER_WRITE_TIMEOUT_MS,
1816
- beforeWriteForTest,
1817
- );
1818
- return startupExitWrite;
1819
- };
1820
- const brokerReadyForSignal = (): Broker | undefined =>
1821
- runningBroker ?? (broker?.ownsDiscovery ? broker : undefined);
1822
- const writeStartupExitLog = (
1823
- record: BrokerStartupExitRecord & Record<string, unknown>,
1824
- message: string,
1825
- ): void => {
1826
- if (startupExitLogged) return;
1827
- startupExitLogged = true;
1828
- const level = record.exitCode === 1 ? "error" : "warn";
1829
- if (record.exitCode === 1) logger.error("sdk broker: startup watchdog forcing exit", record);
1830
- else logger.warn("sdk broker: startup interrupted", record);
1831
- process.stderr.write(`${JSON.stringify({ level, message, ...record })}\n`);
1832
- };
1833
- const exitDuringStartup = (
1834
- reason: "startup-deadline" | "startup-signal",
1835
- exitCode: 1 | 130 | 143,
1836
- signal: "SIGTERM" | "SIGINT" | null,
1837
- timeoutMs?: number,
1838
- ): Promise<boolean> => {
1839
- // The first startup exit cause owns both the persisted reason and the process exit code.
1840
- if (startupExitTask) return startupExitReason === reason ? startupExitTask : Promise.resolve(false);
1841
- if (startupExitRequested) return Promise.resolve(false);
1842
- startupExitRequested = true;
1843
- startupExitReason = reason;
1844
- const exitRecord = makeStartupExitRecord(reason, exitCode, signal, timeoutMs);
1845
- const message =
1846
- reason === "startup-deadline"
1847
- ? `SDK broker startup exceeded its ${timeoutMs}ms fence deadline.`
1848
- : `SDK broker startup interrupted by ${signal} before readiness.`;
1849
- writeStartupExitLog(exitRecord, message);
1850
- startupExitTask = (async () => {
1851
- const exitRecordWrite = await beginStartupExitRecordWrite(exitRecord);
1852
- const exitRecordWritten = exitRecordWrite.kind === "written";
1853
- if (!exitRecordWritten)
1854
- logger.error("sdk broker: startup exit record write timed out", {
1855
- reason: "startup-exit-record-write-failed-or-timed-out",
1856
- exitReason: reason,
1857
- exitRecordWriteStatus: exitRecordWrite.kind,
1858
- ...(exitRecordWrite.kind === "failed" && exitRecordWrite.code
1859
- ? { exitRecordWriteErrorCode: exitRecordWrite.code }
1860
- : {}),
1861
- pid: process.pid,
1862
- exitCode,
1863
- });
1864
- return true;
1865
- })();
1866
- return startupExitTask;
1867
- };
1868
- const unregisterPostmortem = postmortem.register("sdk-broker:exit", async reason => {
1869
- const signal =
1870
- reason === postmortem.Reason.SIGTERM
1871
- ? "SIGTERM"
1872
- : reason === postmortem.Reason.SIGINT
1873
- ? "SIGINT"
1874
- : pendingShutdownSignal;
1875
- if (!signal) return;
1876
- pendingShutdownSignal = signal;
1877
- if (startupExitTask && startupExitReason === "startup-deadline") {
1878
- await startupExitTask;
1879
- await waitForStartupAbortCleanup();
1880
- process.exit(1);
1881
- return;
1882
- }
1883
- const activeBroker = brokerReadyForSignal();
1884
- if (activeBroker) {
1885
- runningBroker = activeBroker;
1886
- stopSweep?.();
1887
- await activeBroker.stop({ kind: "signal", signal });
1888
- return;
1889
- }
1890
- startupAbortController.abort();
1891
- await exitDuringStartup("startup-signal", startupSignalExitCode(signal), signal);
1892
- // Startup may remain blocked in awaited I/O after abort. Bound best-effort
1893
- // rollback so shared postmortem can exit with the original signal status.
1894
- await waitForStartupAbortCleanup();
1895
- });
1896
- const finishPendingStartupSignal = async (): Promise<boolean> => {
1897
- const signal = pendingShutdownSignal;
1898
- if (!signal) return false;
1899
- await exitDuringStartup("startup-signal", startupSignalExitCode(signal), signal);
1900
- return true;
1901
- };
1902
- try {
1903
- const startupOperation = async (deadline: number): Promise<Broker | undefined> => {
1904
- const remainingMs = Math.max(1, deadline - Date.now());
1905
- const testWatchdogMs = Number(process.env.GJC_SDK_TEST_BROKER_STARTUP_WATCHDOG_MS ?? 0);
1906
- const watchdogMs =
1907
- Number.isSafeInteger(testWatchdogMs) && testWatchdogMs > 0 && testWatchdogMs <= remainingMs
1908
- ? testWatchdogMs
1909
- : remainingMs;
1910
- const startupWatchdog = setTimeout(() => {
1911
- startupAbortController.abort();
1912
- void exitDuringStartup("startup-deadline", 1, null, watchdogMs).then(async started => {
1913
- if (!started) {
1914
- // A prior startup signal owns the exit cause; its postmortem callback
1915
- // returns after bounded cleanup and preserves the signal status.
1916
- return;
1917
- }
1918
- await waitForStartupAbortCleanup();
1919
- process.exit(1);
1920
- });
1921
- }, watchdogMs);
1922
- try {
1923
- const existing = await reconcileBrokerGenerationForStartup({ agentDir }, deadline);
1924
- if (startupAbortController.signal.aborted) return undefined;
1925
- if (existing) {
1926
- logger.info("sdk broker: startup reused an existing owner", {
1927
- reason: "existing-owner-reused",
1928
- ownerId: existing.ownerId,
1929
- pid: existing.pid,
1930
- exitCode: 0,
1931
- });
1932
- return undefined;
1933
- }
1934
- if (process.env.GJC_SDK_TEST_BROKER_STARTUP_STALL === "1") {
1935
- const stalled = Promise.withResolvers<void>();
1936
- await stalled.promise;
1937
- }
1938
- const testStartupSignal = process.env.GJC_SDK_TEST_BROKER_STARTUP_SIGNAL;
1939
- if (testStartupSignal === "SIGTERM" || testStartupSignal === "SIGINT") {
1940
- await emitBrokerStartupTestSignal("startup-signal-ready");
1941
- process.kill(process.pid, testStartupSignal);
1942
- const waiting = Promise.withResolvers<void>();
1943
- await waiting.promise;
1944
- }
1945
- const startupGateFile = process.env.GJC_SDK_TEST_BROKER_STARTUP_GATE_FILE;
1946
- if (startupGateFile) {
1947
- await emitBrokerStartupTestSignal("startup-gate-waiting");
1948
- await waitForBrokerStartupTestGate(startupGateFile);
1949
- await emitBrokerStartupTestSignal("startup-gate-released");
1950
- }
1951
- const startupDelayMs = Number(process.env.GJC_SDK_TEST_BROKER_STARTUP_DELAY_MS ?? 0);
1952
- if (Number.isSafeInteger(startupDelayMs) && startupDelayMs > 0 && startupDelayMs <= 10_000) {
1953
- await Bun.sleep(startupDelayMs);
1913
+ startupAbortController.abort();
1914
+ await exitDuringStartup("startup-signal", startupSignalExitCode(signal), signal);
1915
+ // Startup may remain blocked in awaited I/O after abort. Bound best-effort
1916
+ // rollback so shared postmortem can exit with the original signal status.
1917
+ await waitForStartupAbortCleanup();
1918
+ });
1919
+ const finishPendingStartupSignal = async (): Promise<boolean> => {
1920
+ const signal = pendingShutdownSignal;
1921
+ if (!signal) return false;
1922
+ await exitDuringStartup("startup-signal", startupSignalExitCode(signal), signal);
1923
+ return true;
1924
+ };
1925
+ try {
1926
+ const startupOperation = async (deadline: number): Promise<Broker | undefined> => {
1927
+ const remainingMs = Math.max(1, deadline - Date.now());
1928
+ const testWatchdogMs = Number(process.env.GJC_SDK_TEST_BROKER_STARTUP_WATCHDOG_MS ?? 0);
1929
+ const watchdogMs =
1930
+ Number.isSafeInteger(testWatchdogMs) && testWatchdogMs > 0 && testWatchdogMs <= remainingMs
1931
+ ? testWatchdogMs
1932
+ : remainingMs;
1933
+ const startupCheckpointDeadline = performance.now() + watchdogMs - 1_000;
1934
+ const startupWatchdog = setTimeout(() => {
1935
+ startupAbortController.abort();
1936
+ void exitDuringStartup("startup-deadline", 1, null, watchdogMs).then(async started => {
1937
+ if (!started) {
1938
+ // A prior startup signal owns the exit cause; its postmortem callback
1939
+ // returns after bounded cleanup and preserves the signal status.
1940
+ return;
1954
1941
  }
1955
- if (startupAbortController.signal.aborted) return undefined;
1956
- // The real broker-internal entry point is the only place that reads this
1957
- // launcher-supplied environment variable; it is validated here and handed
1958
- // to Broker as a typed setting, never read a second time inside broker.ts
1959
- // from process.env directly.
1960
- const restartRequestEnv = process.env.GJC_BROKER_RESTART_REQUEST;
1961
- const restartRequestId =
1962
- typeof restartRequestEnv === "string" && /^[A-Za-z0-9_-]{1,256}$/.test(restartRequestEnv)
1963
- ? restartRequestEnv
1964
- : undefined;
1965
- const testPostPublicationDelayMs = Number(
1966
- process.env.GJC_SDK_TEST_BROKER_POST_PUBLICATION_DELAY_MS ?? 0,
1967
- );
1968
- const startupPostPublicationDelayMs =
1969
- Number.isSafeInteger(testPostPublicationDelayMs) &&
1970
- testPostPublicationDelayMs > 0 &&
1971
- testPostPublicationDelayMs <= 10_000
1972
- ? testPostPublicationDelayMs
1973
- : undefined;
1974
- const testPrePublicationDelayMs = Number(process.env.GJC_SDK_TEST_BROKER_PRE_PUBLICATION_DELAY_MS ?? 0);
1975
- const startupPrePublicationDelayMs =
1976
- Number.isSafeInteger(testPrePublicationDelayMs) &&
1977
- testPrePublicationDelayMs > 0 &&
1978
- testPrePublicationDelayMs <= 10_000
1979
- ? testPrePublicationDelayMs
1980
- : undefined;
1981
- const testAfterDiscoveryWriteDelayMs = Number(
1982
- process.env.GJC_SDK_TEST_BROKER_AFTER_DISCOVERY_WRITE_DELAY_MS ?? 0,
1983
- );
1984
- const startupAfterDiscoveryWriteTestHook =
1985
- Number.isSafeInteger(testAfterDiscoveryWriteDelayMs) &&
1986
- testAfterDiscoveryWriteDelayMs > 0 &&
1987
- testAfterDiscoveryWriteDelayMs <= 10_000
1988
- ? async () => {
1989
- await emitBrokerStartupTestSignal("discovery-written-before-retain");
1990
- const signal = startupAbortController.signal;
1991
- if (signal.aborted) return;
1992
- const wait = Promise.withResolvers<void>();
1993
- const timer = setTimeout(wait.resolve, testAfterDiscoveryWriteDelayMs);
1994
- const onAbort = (): void => wait.resolve();
1995
- signal.addEventListener("abort", onAbort, { once: true });
1996
- try {
1997
- await wait.promise;
1998
- } finally {
1999
- clearTimeout(timer);
2000
- signal.removeEventListener("abort", onAbort);
2001
- }
2002
- }
2003
- : undefined;
2004
- const candidate = new Broker({
2005
- agentDir,
2006
- startupAbortSignal: startupAbortController.signal,
2007
- onStartupReady: () => clearTimeout(startupWatchdog),
2008
- masterOrphanGraceMs: (await Settings.loadForScope({ cwd: process.cwd(), agentDir })).get(
2009
- "sdk.masterOrphanGraceMs",
2010
- ),
2011
- resolveDirectoryMigration: async cwd => {
2012
- const settings = await Settings.loadForScope({ cwd, agentDir });
2013
- try {
2014
- const policy = settings.get("session.directoryMigration");
2015
- return policy === "disabled" ? "disabled" : "copy-retain";
2016
- } finally {
2017
- await settings.close();
2018
- }
2019
- },
2020
- ...(startupPrePublicationDelayMs === undefined
2021
- ? {}
2022
- : {
2023
- startupPrePublicationDelayMs,
2024
- startupPrePublicationTestHook: async () => {
2025
- await emitBrokerStartupTestSignal("pre-publication-ready");
2026
- },
2027
- }),
2028
- ...(startupAfterDiscoveryWriteTestHook === undefined ? {} : { startupAfterDiscoveryWriteTestHook }),
2029
- ...(startupPostPublicationDelayMs === undefined ? {} : { startupPostPublicationDelayMs }),
2030
- ...(restartRequestId === undefined ? {} : { restartRequestId }),
1942
+ await waitForStartupAbortCleanup();
1943
+ process.exit(1);
1944
+ });
1945
+ }, watchdogMs);
1946
+ try {
1947
+ // `gjc sdk broker run` never retires another owner, even one this generation cannot
1948
+ // reuse: any live incumbent is a refusal. Autostart repairs unusable generations.
1949
+ const existing = publicBrokerRun
1950
+ ? ((await readBrokerDiscovery(agentDir)) ?? undefined)
1951
+ : await reconcileBrokerGenerationForStartup({ agentDir }, deadline);
1952
+ if (startupAbortController.signal.aborted) return undefined;
1953
+ if (existing) {
1954
+ logger.info("sdk broker: startup reused an existing owner", {
1955
+ reason: "existing-owner-reused",
1956
+ ownerId: existing.ownerId,
1957
+ pid: existing.pid,
1958
+ exitCode: 0,
2031
1959
  });
2032
- broker = candidate;
2033
- candidateStartTask = candidate.start();
2034
- await candidateStartTask;
2035
- if (candidate.ownsDiscovery) runningBroker = candidate;
2036
- return candidate;
2037
- } finally {
2038
- clearTimeout(startupWatchdog);
1960
+ return undefined;
2039
1961
  }
2040
- };
2041
- broker = await withBrokerStartupLock(agentDir, startupOperation, {
2042
- onAcquired: () => void emitBrokerStartupTestSignal("fence-acquired"),
2043
- onContended: () => void emitBrokerStartupTestSignal("fence-contended"),
2044
- });
2045
- } catch (error) {
2046
- if (pendingShutdownSignal) {
2047
- await finishPendingStartupSignal();
2048
- return;
2049
- }
2050
- if (broker) {
2051
- try {
2052
- await broker.stop({ kind: "startup-failure" });
2053
- } catch {
2054
- // Preserve the startup/fence failure that made this bootstrap unusable.
1962
+ if (process.env.GJC_SDK_TEST_BROKER_STARTUP_STALL === "1") {
1963
+ const stalled = Promise.withResolvers<void>();
1964
+ await stalled.promise;
2055
1965
  }
1966
+ const testStartupSignal = process.env.GJC_SDK_TEST_BROKER_STARTUP_SIGNAL;
1967
+ if (testStartupSignal === "SIGTERM" || testStartupSignal === "SIGINT") {
1968
+ await emitBrokerStartupTestSignal("startup-signal-ready");
1969
+ process.kill(process.pid, testStartupSignal);
1970
+ const waiting = Promise.withResolvers<void>();
1971
+ await waiting.promise;
1972
+ }
1973
+ const startupGateFile = process.env.GJC_SDK_TEST_BROKER_STARTUP_GATE_FILE;
1974
+ if (startupGateFile) {
1975
+ await emitBrokerStartupTestSignal("startup-gate-waiting");
1976
+ await waitForBrokerStartupTestGate(startupGateFile);
1977
+ await emitBrokerStartupTestSignal("startup-gate-released");
1978
+ }
1979
+ const startupDelayMs = Number(process.env.GJC_SDK_TEST_BROKER_STARTUP_DELAY_MS ?? 0);
1980
+ if (Number.isSafeInteger(startupDelayMs) && startupDelayMs > 0 && startupDelayMs <= 10_000) {
1981
+ await Bun.sleep(startupDelayMs);
1982
+ }
1983
+ if (startupAbortController.signal.aborted) return undefined;
1984
+ // The real broker-internal entry point is the only place that reads this
1985
+ // launcher-supplied environment variable; it is validated here and handed
1986
+ // to Broker as a typed setting, never read a second time inside broker.ts
1987
+ // from process.env directly.
1988
+ const restartRequestEnv = process.env.GJC_BROKER_RESTART_REQUEST;
1989
+ const restartRequestId =
1990
+ typeof restartRequestEnv === "string" && /^[A-Za-z0-9_-]{1,256}$/.test(restartRequestEnv)
1991
+ ? restartRequestEnv
1992
+ : undefined;
1993
+ const testPostPublicationDelayMs = Number(process.env.GJC_SDK_TEST_BROKER_POST_PUBLICATION_DELAY_MS ?? 0);
1994
+ const startupPostPublicationDelayMs =
1995
+ Number.isSafeInteger(testPostPublicationDelayMs) &&
1996
+ testPostPublicationDelayMs > 0 &&
1997
+ testPostPublicationDelayMs <= 10_000
1998
+ ? testPostPublicationDelayMs
1999
+ : undefined;
2000
+ const testPrePublicationDelayMs = Number(process.env.GJC_SDK_TEST_BROKER_PRE_PUBLICATION_DELAY_MS ?? 0);
2001
+ const startupPrePublicationDelayMs =
2002
+ Number.isSafeInteger(testPrePublicationDelayMs) &&
2003
+ testPrePublicationDelayMs > 0 &&
2004
+ testPrePublicationDelayMs <= 10_000
2005
+ ? testPrePublicationDelayMs
2006
+ : undefined;
2007
+ const testAfterDiscoveryWriteDelayMs = Number(
2008
+ process.env.GJC_SDK_TEST_BROKER_AFTER_DISCOVERY_WRITE_DELAY_MS ?? 0,
2009
+ );
2010
+ const startupAfterDiscoveryWriteTestHook =
2011
+ Number.isSafeInteger(testAfterDiscoveryWriteDelayMs) &&
2012
+ testAfterDiscoveryWriteDelayMs > 0 &&
2013
+ testAfterDiscoveryWriteDelayMs <= 10_000
2014
+ ? async () => {
2015
+ await emitBrokerStartupTestSignal("discovery-written-before-retain");
2016
+ const signal = startupAbortController.signal;
2017
+ if (signal.aborted) return;
2018
+ const wait = Promise.withResolvers<void>();
2019
+ const timer = setTimeout(wait.resolve, testAfterDiscoveryWriteDelayMs);
2020
+ const onAbort = (): void => wait.resolve();
2021
+ signal.addEventListener("abort", onAbort, { once: true });
2022
+ try {
2023
+ await wait.promise;
2024
+ } finally {
2025
+ clearTimeout(timer);
2026
+ signal.removeEventListener("abort", onAbort);
2027
+ }
2028
+ }
2029
+ : undefined;
2030
+ const candidate = new Broker({
2031
+ agentDir,
2032
+ startupAbortSignal: startupAbortController.signal,
2033
+ startupCheckpointDeadline,
2034
+ onStartupReady: () => clearTimeout(startupWatchdog),
2035
+ masterOrphanGraceMs: (await Settings.loadForScope({ cwd: process.cwd(), agentDir })).get(
2036
+ "sdk.masterOrphanGraceMs",
2037
+ ),
2038
+ resolveDirectoryMigration: async cwd => {
2039
+ const settings = await Settings.loadForScope({ cwd, agentDir });
2040
+ try {
2041
+ const policy = settings.get("session.directoryMigration");
2042
+ return policy === "disabled" ? "disabled" : "copy-retain";
2043
+ } finally {
2044
+ await settings.close();
2045
+ }
2046
+ },
2047
+ ...(startupPrePublicationDelayMs === undefined
2048
+ ? {}
2049
+ : {
2050
+ startupPrePublicationDelayMs,
2051
+ startupPrePublicationTestHook: async () => {
2052
+ await emitBrokerStartupTestSignal("pre-publication-ready");
2053
+ },
2054
+ }),
2055
+ ...(startupAfterDiscoveryWriteTestHook === undefined ? {} : { startupAfterDiscoveryWriteTestHook }),
2056
+ ...(startupPostPublicationDelayMs === undefined ? {} : { startupPostPublicationDelayMs }),
2057
+ ...(restartRequestId === undefined ? {} : { restartRequestId }),
2058
+ });
2059
+ broker = candidate;
2060
+ candidateStartTask = candidate.start();
2061
+ await candidateStartTask;
2062
+ if (candidate.ownsDiscovery) runningBroker = candidate;
2063
+ return candidate;
2064
+ } finally {
2065
+ clearTimeout(startupWatchdog);
2056
2066
  }
2057
- // This process spawns detached with stdio ignored (see ensure.ts), so the
2058
- // durable marker is the only channel the caller has to see why start()
2059
- // failed instead of a bare exit code (#3963).
2060
- const startupFailureIncarnation = processIncarnation(process.pid);
2061
- const markerWritten = await writeBrokerStartupFailureMarkerBounded(agentDir, {
2062
- reason: error instanceof Error ? error.message : String(error),
2063
- exitCode: 1,
2064
- signal: null,
2065
- pid: process.pid,
2066
- ...(startupFailureIncarnation === undefined ? {} : { incarnation: startupFailureIncarnation }),
2067
- });
2068
- logger.error("sdk broker: startup failed", {
2069
- reason: "startup-failure",
2070
- pid: process.pid,
2071
- exitCode: 1,
2072
- markerWritten,
2073
- });
2074
- unregisterPostmortem();
2075
- throw error;
2076
- }
2077
- if (!broker) {
2078
- if (pendingShutdownSignal) {
2079
- await finishPendingStartupSignal();
2080
- return;
2081
- }
2082
- unregisterPostmortem();
2067
+ };
2068
+ broker = await withBrokerStartupLock(agentDir, startupOperation, {
2069
+ onAcquired: () => void emitBrokerStartupTestSignal("fence-acquired"),
2070
+ onContended: () => void emitBrokerStartupTestSignal("fence-contended"),
2071
+ });
2072
+ } catch (error) {
2073
+ if (pendingShutdownSignal) {
2074
+ await finishPendingStartupSignal();
2083
2075
  return;
2084
2076
  }
2085
- if (!broker.ownsDiscovery) {
2086
- if (pendingShutdownSignal) {
2087
- if (runningBroker) await runningBroker.completion;
2088
- else await finishPendingStartupSignal();
2089
- return;
2077
+ // A retained removal transition is an explicit acquire failure, not only
2078
+ // an exhausted ordinary contention timeout.
2079
+ if (error instanceof FileLockAcquireError && error.orphanPath) {
2080
+ await exitDuringStartup(
2081
+ "startup-lock-blocked",
2082
+ 1,
2083
+ null,
2084
+ BROKER_DISCOVERY_BUDGET.startupLockWaitMs,
2085
+ error.orphanPath,
2086
+ );
2087
+ }
2088
+ if (broker) {
2089
+ try {
2090
+ await broker.stop({ kind: "startup-failure" });
2091
+ } catch {
2092
+ // Preserve the startup/fence failure that made this bootstrap unusable.
2090
2093
  }
2091
- // Another broker owns discovery; this process exits cleanly (code 0) as
2092
- // the race loser. Record why so a caller polling for a winner that never
2093
- // appears can diagnose the loss instead of seeing only a bare exit 0.
2094
- const losingIncarnation = processIncarnation(process.pid);
2095
- await writeBrokerStartupFailureMarkerBounded(agentDir, {
2096
- reason: "Another broker owns the lock/discovery; this broker exited as the race loser.",
2097
- exitCode: 0,
2098
- signal: null,
2099
- pid: process.pid,
2100
- ...(losingIncarnation === undefined ? {} : { incarnation: losingIncarnation }),
2101
- });
2102
- unregisterPostmortem();
2094
+ }
2095
+ // This process spawns detached with stdio ignored (see ensure.ts), so the
2096
+ // durable marker is the only channel the caller has to see why start()
2097
+ // failed instead of a bare exit code (#3963).
2098
+ const startupFailureIncarnation = processIncarnation(process.pid);
2099
+ const markerWritten = await writeBrokerStartupFailureMarkerBounded(agentDir, {
2100
+ reason: error instanceof Error ? error.message : String(error),
2101
+ exitCode: 1,
2102
+ signal: null,
2103
+ pid: process.pid,
2104
+ ...(startupFailureIncarnation === undefined ? {} : { incarnation: startupFailureIncarnation }),
2105
+ });
2106
+ logger.error("sdk broker: startup failed", {
2107
+ reason: "startup-failure",
2108
+ pid: process.pid,
2109
+ exitCode: 1,
2110
+ markerWritten,
2111
+ });
2112
+ unregisterPostmortem();
2113
+ throw error;
2114
+ }
2115
+ if (!broker) {
2116
+ if (pendingShutdownSignal) {
2117
+ await finishPendingStartupSignal();
2103
2118
  return;
2104
2119
  }
2105
- runningBroker = broker;
2106
- // A live broker must not keep advertising sessions whose host process is
2107
- // gone; the sweep is the broker-side half of the host reaping bound.
2108
- stopSweep = startBrokerDeadRegistrationSweep(runningBroker);
2109
- try {
2110
- await runningBroker.completion;
2111
- } finally {
2112
- stopSweep?.();
2113
- if (!pendingShutdownSignal) unregisterPostmortem();
2120
+ unregisterPostmortem();
2121
+ if (publicBrokerRun) failPublicBrokerRun(agentDir);
2122
+ return;
2123
+ }
2124
+ if (!broker.ownsDiscovery) {
2125
+ if (pendingShutdownSignal) {
2126
+ if (runningBroker) await runningBroker.completion;
2127
+ else await finishPendingStartupSignal();
2128
+ return;
2114
2129
  }
2115
- if (!pendingShutdownSignal) process.exit(0);
2130
+ // Another broker owns discovery. The detached autostart entry exits cleanly
2131
+ // (code 0) as the race loser; `gjc sdk broker run` fails instead, because
2132
+ // its supervisor must not mistake a competing owner for its own broker.
2133
+ // Record why so a caller polling for a winner that never appears can
2134
+ // diagnose the loss instead of seeing only a bare exit code.
2135
+ const losingIncarnation = processIncarnation(process.pid);
2136
+ await writeBrokerStartupFailureMarkerBounded(agentDir, {
2137
+ reason: "Another broker owns the lock/discovery; this broker exited as the race loser.",
2138
+ exitCode: publicBrokerRun ? 1 : 0,
2139
+ signal: null,
2140
+ pid: process.pid,
2141
+ ...(losingIncarnation === undefined ? {} : { incarnation: losingIncarnation }),
2142
+ });
2143
+ unregisterPostmortem();
2144
+ if (publicBrokerRun) failPublicBrokerRun(agentDir);
2145
+ return;
2146
+ }
2147
+ runningBroker = broker;
2148
+ // A live broker must not keep advertising sessions whose host process is
2149
+ // gone; the sweep is the broker-side half of the host reaping bound.
2150
+ stopSweep = startBrokerDeadRegistrationSweep(runningBroker);
2151
+ try {
2152
+ await runningBroker.completion;
2153
+ } finally {
2154
+ stopSweep?.();
2155
+ if (!pendingShutdownSignal) unregisterPostmortem();
2116
2156
  }
2157
+ if (pendingShutdownSignal) return;
2158
+ // A supervised broker that lost its root or could not renew its heartbeat
2159
+ // must exit as a failure, so the supervisor restarts it. A requested
2160
+ // shutdown or a committed restart handoff is a clean stop.
2161
+ const terminal = runningBroker.terminalExit;
2162
+ if (publicBrokerRun && (terminal?.mode !== "owned-root" || terminal.reason === "heartbeat-renewal-blocked")) {
2163
+ process.stderr.write(
2164
+ `gjc sdk broker run: SDK broker stopped abnormally (${terminal?.mode ?? "unknown"}: ${terminal?.reason ?? "unknown"}).\n`,
2165
+ );
2166
+ process.exit(1);
2167
+ }
2168
+ process.exit(0);
2169
+ }
2170
+
2171
+ /** `gjc sdk broker run` did not become the owner: another broker already serves this agent dir. */
2172
+ function failPublicBrokerRun(agentDir: string): void {
2173
+ process.stderr.write(`gjc sdk broker run: another SDK broker already owns ${agentDir}.\n`);
2174
+ process.exitCode = 1;
2117
2175
  }