@gajae-code/coding-agent 0.16.7 → 0.17.1

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 (321) hide show
  1. package/CHANGELOG.md +180 -1
  2. package/dist/types/capability/fs.d.ts +37 -4
  3. package/dist/types/capability/index.d.ts +19 -0
  4. package/dist/types/capability/types.d.ts +9 -0
  5. package/dist/types/cli/doctor/args.d.ts +36 -0
  6. package/dist/types/cli/doctor/checks.d.ts +18 -0
  7. package/dist/types/cli/doctor/config-repairs.d.ts +38 -0
  8. package/dist/types/cli/doctor/context.d.ts +89 -0
  9. package/dist/types/cli/doctor/files.d.ts +27 -0
  10. package/dist/types/cli/doctor/ids.d.ts +20 -0
  11. package/dist/types/cli/doctor/install-repairs.d.ts +94 -0
  12. package/dist/types/cli/doctor/journal.d.ts +43 -0
  13. package/dist/types/cli/doctor/managed-link.d.ts +39 -0
  14. package/dist/types/cli/doctor/native-probe.d.ts +2 -0
  15. package/dist/types/cli/doctor/permission-repairs.d.ts +25 -0
  16. package/dist/types/cli/doctor/plugin-quarantine.d.ts +77 -0
  17. package/dist/types/cli/doctor/plugin-restore.d.ts +40 -0
  18. package/dist/types/cli/doctor/plugin-targets.d.ts +11 -0
  19. package/dist/types/cli/doctor/probe-client.d.ts +9 -0
  20. package/dist/types/cli/doctor/probe-types.d.ts +18 -0
  21. package/dist/types/cli/doctor/probe.d.ts +3 -0
  22. package/dist/types/cli/doctor/projection-probe.d.ts +2 -0
  23. package/dist/types/cli/doctor/repairs.d.ts +28 -0
  24. package/dist/types/cli/doctor/report.d.ts +27 -0
  25. package/dist/types/cli/doctor/runner.d.ts +4 -0
  26. package/dist/types/cli/doctor/safety.d.ts +10 -0
  27. package/dist/types/cli/doctor/service-repairs.d.ts +8 -0
  28. package/dist/types/cli/doctor/service-targets.d.ts +41 -0
  29. package/dist/types/cli/doctor/types.d.ts +145 -0
  30. package/dist/types/cli/doctor-cli.d.ts +7 -0
  31. package/dist/types/cli/doctor-supervisor.d.ts +121 -0
  32. package/dist/types/cli/doctor-worker.d.ts +29 -0
  33. package/dist/types/cli/install-activation.d.ts +158 -0
  34. package/dist/types/cli/macos-community-app.d.ts +41 -0
  35. package/dist/types/cli/mcp-cli.d.ts +0 -11
  36. package/dist/types/cli/update-cli.d.ts +41 -4
  37. package/dist/types/cli-main.d.ts +174 -0
  38. package/dist/types/cli.d.ts +1 -171
  39. package/dist/types/commands/doctor.d.ts +7 -0
  40. package/dist/types/commands/sdk.d.ts +17 -8
  41. package/dist/types/config/autorouting-tier-map.d.ts +19 -0
  42. package/dist/types/config/file-lock.d.ts +27 -3
  43. package/dist/types/config/model-preset-registry.d.ts +1 -0
  44. package/dist/types/config/model-registry.d.ts +12 -5
  45. package/dist/types/config/model-resolver.d.ts +16 -3
  46. package/dist/types/config/models-config-schema.d.ts +2 -0
  47. package/dist/types/config/settings-schema.d.ts +1 -1
  48. package/dist/types/coordinator-mcp/question-gate-codec.d.ts +1 -1
  49. package/dist/types/coordinator-mcp/session-reaper.d.ts +15 -5
  50. package/dist/types/cursor.d.ts +9 -9
  51. package/dist/types/discovery/agents-md.d.ts +6 -1
  52. package/dist/types/discovery/helpers.d.ts +36 -7
  53. package/dist/types/discovery/index.d.ts +1 -1
  54. package/dist/types/discovery/mcp-json.d.ts +2 -0
  55. package/dist/types/extensibility/extensions/loader.d.ts +22 -1
  56. package/dist/types/extensibility/gjc-plugins/lifecycle.d.ts +14 -1
  57. package/dist/types/extensibility/gjc-plugins/paths.d.ts +2 -1
  58. package/dist/types/extensibility/gjc-plugins/types.d.ts +25 -0
  59. package/dist/types/extensibility/plugins/manager.d.ts +33 -4
  60. package/dist/types/extensibility/plugins/marketplace/cache.d.ts +6 -0
  61. package/dist/types/extensibility/plugins/marketplace/manager.d.ts +186 -0
  62. package/dist/types/extensibility/plugins/marketplace/registry.d.ts +27 -0
  63. package/dist/types/extensibility/plugins/marketplace/source-resolver.d.ts +16 -1
  64. package/dist/types/extensibility/runtime-skill-discovery.d.ts +16 -3
  65. package/dist/types/gjc-runtime/autoresearch-runtime.d.ts +6 -0
  66. package/dist/types/gjc-runtime/launch-worktree.d.ts +53 -0
  67. package/dist/types/gjc-runtime/session-layout.d.ts +8 -0
  68. package/dist/types/gjc-runtime/session-state-lock.d.ts +15 -0
  69. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +13 -0
  70. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +6 -0
  71. package/dist/types/main.d.ts +19 -1
  72. package/dist/types/modes/acp/acp-agent.d.ts +40 -0
  73. package/dist/types/modes/components/agent-dashboard.d.ts +1 -0
  74. package/dist/types/modes/components/model-selector.d.ts +2 -0
  75. package/dist/types/modes/interactive-mode.d.ts +1 -1
  76. package/dist/types/runtime-mcp/config-writer.d.ts +47 -0
  77. package/dist/types/runtime-mcp/policy-json-edit.d.ts +60 -0
  78. package/dist/types/runtime-mcp/startup-policy.d.ts +4 -0
  79. package/dist/types/runtime-mcp/types.d.ts +2 -2
  80. package/dist/types/sdk/acp/adapter.d.ts +5 -1
  81. package/dist/types/sdk/broker/broker.d.ts +51 -0
  82. package/dist/types/sdk/broker/daemon-entry.d.ts +39 -0
  83. package/dist/types/sdk/broker/discovery.d.ts +38 -0
  84. package/dist/types/sdk/broker/doctor-restart.d.ts +79 -0
  85. package/dist/types/sdk/broker/endpoint-authority.d.ts +7 -0
  86. package/dist/types/sdk/broker/ensure.d.ts +14 -0
  87. package/dist/types/sdk/broker/lifecycle.d.ts +2 -0
  88. package/dist/types/sdk/broker/process-incarnation.d.ts +50 -0
  89. package/dist/types/sdk/broker/runtime.d.ts +26 -0
  90. package/dist/types/sdk/broker/session-index.d.ts +13 -4
  91. package/dist/types/sdk/broker/worktree-occupancy.d.ts +14 -0
  92. package/dist/types/sdk/bus/chat-daemon-cli.d.ts +16 -0
  93. package/dist/types/sdk/bus/chat-daemon-control.d.ts +101 -6
  94. package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +16 -0
  95. package/dist/types/sdk/bus/daemon-startup-exclusion.d.ts +43 -0
  96. package/dist/types/sdk/bus/doctor-daemon-restart-client.d.ts +51 -0
  97. package/dist/types/sdk/bus/doctor-daemon-restart.d.ts +39 -0
  98. package/dist/types/sdk/bus/index.d.ts +2 -0
  99. package/dist/types/sdk/bus/notification-service.d.ts +3 -0
  100. package/dist/types/sdk/bus/telegram-daemon-cli.d.ts +11 -0
  101. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
  102. package/dist/types/sdk/bus/telegram-daemon-control.d.ts +5 -0
  103. package/dist/types/sdk/bus/telegram-daemon-owner-registry.d.ts +1 -0
  104. package/dist/types/sdk/bus/telegram-daemon.d.ts +36 -5
  105. package/dist/types/sdk/cli/session-cli.d.ts +1 -0
  106. package/dist/types/sdk/host/session-runtime.d.ts +4 -0
  107. package/dist/types/sdk/host/surface-policy.d.ts +3 -1
  108. package/dist/types/sdk/lifecycle/service.d.ts +44 -10
  109. package/dist/types/sdk/prompt-deadline-lease.d.ts +6 -3
  110. package/dist/types/sdk/prompt-deadline-manager.d.ts +2 -2
  111. package/dist/types/sdk/prompt-failure.d.ts +47 -0
  112. package/dist/types/sdk/prompt-status.d.ts +19 -0
  113. package/dist/types/sdk/router/session-router.d.ts +3 -4
  114. package/dist/types/sdk/service-artifact-paths.d.ts +16 -0
  115. package/dist/types/sdk/session-authority.d.ts +9 -0
  116. package/dist/types/sdk/session.d.ts +3 -1
  117. package/dist/types/session/agent-session.d.ts +1 -1
  118. package/dist/types/session/blob-store.d.ts +1 -1
  119. package/dist/types/session/fallback-chain-controller.d.ts +3 -6
  120. package/dist/types/session/internal/managed-session-scope.d.ts +44 -0
  121. package/dist/types/session/primary-control-surface.d.ts +4 -0
  122. package/dist/types/session/session-manager.d.ts +6 -0
  123. package/dist/types/system-prompt.d.ts +6 -0
  124. package/dist/types/tools/ask.d.ts +25 -0
  125. package/dist/types/tools/image-gen.d.ts +0 -4
  126. package/dist/types/tools/skill-discovery.d.ts +7 -5
  127. package/dist/types/tools/ultragoal-ask-guard.d.ts +7 -4
  128. package/dist/types/utils/changelog.d.ts +0 -9
  129. package/dist/types/utils/mupdf-wasm.d.ts +1 -0
  130. package/package.json +9 -8
  131. package/scripts/compile-args.ts +8 -4
  132. package/src/capability/fs.ts +395 -17
  133. package/src/capability/index.ts +273 -10
  134. package/src/capability/types.ts +9 -0
  135. package/src/cli/customize-doctor.ts +18 -12
  136. package/src/cli/doctor/args.ts +236 -0
  137. package/src/cli/doctor/checks.ts +431 -0
  138. package/src/cli/doctor/config-repairs.ts +517 -0
  139. package/src/cli/doctor/context.ts +149 -0
  140. package/src/cli/doctor/files.ts +149 -0
  141. package/src/cli/doctor/ids.ts +115 -0
  142. package/src/cli/doctor/install-repairs.ts +335 -0
  143. package/src/cli/doctor/journal.ts +423 -0
  144. package/src/cli/doctor/managed-link.ts +514 -0
  145. package/src/cli/doctor/native-probe.ts +13 -0
  146. package/src/cli/doctor/permission-repairs.ts +242 -0
  147. package/src/cli/doctor/plugin-quarantine.ts +377 -0
  148. package/src/cli/doctor/plugin-restore.ts +59 -0
  149. package/src/cli/doctor/plugin-targets.ts +373 -0
  150. package/src/cli/doctor/probe-client.ts +195 -0
  151. package/src/cli/doctor/probe-types.ts +21 -0
  152. package/src/cli/doctor/probe.ts +42 -0
  153. package/src/cli/doctor/projection-probe.ts +249 -0
  154. package/src/cli/doctor/repairs.ts +1157 -0
  155. package/src/cli/doctor/report.ts +176 -0
  156. package/src/cli/doctor/runner.ts +342 -0
  157. package/src/cli/doctor/safety.ts +40 -0
  158. package/src/cli/doctor/service-repairs.ts +452 -0
  159. package/src/cli/doctor/service-targets.ts +274 -0
  160. package/src/cli/doctor/types.ts +164 -0
  161. package/src/cli/doctor-cli.ts +62 -0
  162. package/src/cli/doctor-supervisor.ts +571 -0
  163. package/src/cli/doctor-worker.ts +181 -0
  164. package/src/cli/install-activation.ts +666 -0
  165. package/src/cli/macos-community-app.ts +1449 -0
  166. package/src/cli/mcp-cli.ts +11 -21
  167. package/src/cli/nofile-limit.ts +5 -1
  168. package/src/cli/plugin-cli.ts +6 -1
  169. package/src/cli/update-cli.ts +362 -264
  170. package/src/cli-main.ts +517 -0
  171. package/src/cli.ts +64 -433
  172. package/src/commands/autoresearch.ts +11 -5
  173. package/src/commands/completion.ts +1 -1
  174. package/src/commands/doctor.ts +13 -0
  175. package/src/commands/sdk.ts +58 -10
  176. package/src/commands/ultragoal.ts +11 -5
  177. package/src/config/autorouting-tier-map.ts +7 -0
  178. package/src/config/file-lock.ts +736 -71
  179. package/src/config/mcp-schema.json +3 -3
  180. package/src/config/model-preset-registry.ts +1 -0
  181. package/src/config/model-registry.ts +80 -27
  182. package/src/config/model-resolver.ts +33 -4
  183. package/src/config/models-config-schema.ts +2 -0
  184. package/src/config/settings-schema.ts +6 -2
  185. package/src/coordinator-mcp/question-gate-codec.ts +1 -0
  186. package/src/coordinator-mcp/server.ts +534 -51
  187. package/src/coordinator-mcp/session-reaper.ts +64 -8
  188. package/src/cursor.ts +223 -53
  189. package/src/discovery/agents-md.ts +74 -8
  190. package/src/discovery/agents.ts +32 -7
  191. package/src/discovery/builtin.ts +184 -75
  192. package/src/discovery/claude-plugins.ts +85 -17
  193. package/src/discovery/cline.ts +38 -10
  194. package/src/discovery/cursor.ts +19 -6
  195. package/src/discovery/gemini.ts +62 -22
  196. package/src/discovery/github.ts +13 -3
  197. package/src/discovery/helpers.ts +483 -89
  198. package/src/discovery/index.ts +1 -0
  199. package/src/discovery/mcp-json.ts +36 -10
  200. package/src/discovery/opencode.ts +36 -19
  201. package/src/discovery/ssh.ts +11 -8
  202. package/src/discovery/windsurf.ts +12 -3
  203. package/src/exec/bash-executor.ts +10 -3
  204. package/src/exec/isolated-shell.ts +15 -2
  205. package/src/extensibility/extensions/loader.ts +88 -26
  206. package/src/extensibility/gjc-plugins/lifecycle.ts +189 -0
  207. package/src/extensibility/gjc-plugins/paths.ts +4 -1
  208. package/src/extensibility/gjc-plugins/types.ts +27 -0
  209. package/src/extensibility/plugins/manager.ts +200 -22
  210. package/src/extensibility/plugins/marketplace/cache.ts +55 -2
  211. package/src/extensibility/plugins/marketplace/manager.ts +1262 -162
  212. package/src/extensibility/plugins/marketplace/registry.ts +153 -0
  213. package/src/extensibility/plugins/marketplace/source-resolver.ts +116 -10
  214. package/src/extensibility/runtime-skill-discovery.ts +61 -9
  215. package/src/extensibility/skill-management.ts +7 -1
  216. package/src/gjc-runtime/autoresearch-runtime.ts +10 -1
  217. package/src/gjc-runtime/launch-worktree.test.ts +248 -0
  218. package/src/gjc-runtime/launch-worktree.ts +275 -38
  219. package/src/gjc-runtime/repository-binding.ts +26 -1
  220. package/src/gjc-runtime/session-layout.ts +10 -0
  221. package/src/gjc-runtime/session-state-lock.ts +330 -23
  222. package/src/gjc-runtime/session-state-sidecar.ts +328 -80
  223. package/src/gjc-runtime/ultragoal-runtime.ts +10 -1
  224. package/src/harness-control-plane/session-lease.ts +2 -2
  225. package/src/internal-urls/docs-index.generated.ts +28 -26
  226. package/src/main.ts +150 -29
  227. package/src/memories/index.ts +24 -2
  228. package/src/modes/acp/acp-agent.ts +522 -68
  229. package/src/modes/components/agent-dashboard.ts +3 -1
  230. package/src/modes/components/model-selector.ts +28 -19
  231. package/src/modes/components/plugin-settings.ts +2 -1
  232. package/src/modes/controllers/selector-controller.ts +15 -9
  233. package/src/modes/interactive-mode.ts +21 -3
  234. package/src/prompts/system/system-prompt.md +5 -1
  235. package/src/prompts/tools/browser.md +1 -1
  236. package/src/prompts/tools/move-session.md +4 -0
  237. package/src/prompts/tools/read.md +2 -1
  238. package/src/prompts/tools/skill-discovery.md +6 -5
  239. package/src/prompts/tools/task.md +1 -0
  240. package/src/runtime-mcp/config-writer.ts +228 -0
  241. package/src/runtime-mcp/oauth-discovery.ts +114 -14
  242. package/src/runtime-mcp/policy-json-edit.ts +129 -0
  243. package/src/runtime-mcp/startup-policy.ts +14 -0
  244. package/src/runtime-mcp/types.ts +2 -2
  245. package/src/sdk/acp/adapter.ts +27 -12
  246. package/src/sdk/broker/broker.ts +273 -44
  247. package/src/sdk/broker/daemon-entry.ts +119 -0
  248. package/src/sdk/broker/discovery.ts +173 -20
  249. package/src/sdk/broker/doctor-restart.ts +206 -0
  250. package/src/sdk/broker/endpoint-authority.ts +36 -0
  251. package/src/sdk/broker/ensure.ts +49 -14
  252. package/src/sdk/broker/lifecycle.ts +174 -42
  253. package/src/sdk/broker/process-incarnation.ts +86 -2
  254. package/src/sdk/broker/runtime.ts +145 -21
  255. package/src/sdk/broker/session-index.ts +136 -22
  256. package/src/sdk/broker/spawn-substrate.ts +4 -1
  257. package/src/sdk/broker/transport.ts +5 -0
  258. package/src/sdk/broker/worktree-occupancy.ts +21 -1
  259. package/src/sdk/bus/chat-daemon-cli.ts +63 -0
  260. package/src/sdk/bus/chat-daemon-control.ts +260 -64
  261. package/src/sdk/bus/chat-daemon-runtime.ts +141 -25
  262. package/src/sdk/bus/daemon-startup-exclusion.ts +119 -0
  263. package/src/sdk/bus/doctor-daemon-restart-client.ts +291 -0
  264. package/src/sdk/bus/doctor-daemon-restart.ts +92 -0
  265. package/src/sdk/bus/index.ts +252 -51
  266. package/src/sdk/bus/kind-aware-reconciliation.ts +76 -41
  267. package/src/sdk/bus/notification-service.ts +21 -3
  268. package/src/sdk/bus/reconciliation-store.ts +17 -11
  269. package/src/sdk/bus/telegram-daemon-cli.ts +91 -5
  270. package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
  271. package/src/sdk/bus/telegram-daemon-control.ts +47 -0
  272. package/src/sdk/bus/telegram-daemon-owner-registry.ts +1 -1
  273. package/src/sdk/bus/telegram-daemon.ts +178 -13
  274. package/src/sdk/cli/session-cli.ts +100 -12
  275. package/src/sdk/host/host.ts +2 -7
  276. package/src/sdk/host/session-runtime.test.ts +329 -8
  277. package/src/sdk/host/session-runtime.ts +202 -50
  278. package/src/sdk/host/surface-policy.ts +8 -1
  279. package/src/sdk/lifecycle/service.ts +199 -22
  280. package/src/sdk/mcp/server.ts +1 -1
  281. package/src/sdk/prompt-deadline-lease.ts +12 -4
  282. package/src/sdk/prompt-deadline-manager.ts +5 -7
  283. package/src/sdk/prompt-failure.ts +148 -0
  284. package/src/sdk/prompt-status.ts +30 -1
  285. package/src/sdk/prompt-watchdog.ts +11 -3
  286. package/src/sdk/protocol/operation-inventory.generated.json +18 -0
  287. package/src/sdk/protocol/operation-registry.ts +20 -8
  288. package/src/sdk/router/session-router.ts +8 -3
  289. package/src/sdk/service-artifact-paths.ts +26 -0
  290. package/src/sdk/session-authority.ts +10 -0
  291. package/src/sdk/session.ts +361 -194
  292. package/src/session/agent-session.ts +398 -93
  293. package/src/session/blob-store.ts +9 -9
  294. package/src/session/fallback-chain-controller.ts +3 -7
  295. package/src/session/internal/managed-session-scope.ts +445 -29
  296. package/src/session/internal/managed-session-storage.ts +38 -6
  297. package/src/session/primary-control-surface.ts +12 -0
  298. package/src/session/session-manager.ts +11 -3
  299. package/src/session/session-storage.ts +1 -1
  300. package/src/session-import/codex.ts +6 -1
  301. package/src/session-import/redact.ts +7 -0
  302. package/src/skill-state/workflow-mutation-guard.ts +645 -39
  303. package/src/slash-commands/builtin-registry.ts +7 -3
  304. package/src/system-prompt.ts +8 -0
  305. package/src/task/executor.ts +36 -3
  306. package/src/task/types.ts +27 -5
  307. package/src/tools/ask.ts +114 -34
  308. package/src/tools/browser/actions.ts +2 -2
  309. package/src/tools/browser/tab-worker.ts +11 -8
  310. package/src/tools/descriptors.ts +6 -1
  311. package/src/tools/fetch.ts +7 -2
  312. package/src/tools/image-gen.ts +33 -34
  313. package/src/tools/read.ts +78 -32
  314. package/src/tools/skill-discovery.ts +17 -7
  315. package/src/tools/tool-catalog.generated.ts +8 -8
  316. package/src/tools/ultragoal-ask-guard.ts +16 -26
  317. package/src/utils/changelog.ts +7 -1
  318. package/src/utils/git.ts +3 -1
  319. package/src/utils/markit.ts +24 -2
  320. package/src/utils/mupdf-wasm.ts +26 -0
  321. package/src/web/search/providers/openai-compatible.ts +191 -14
@@ -3,7 +3,7 @@ import type { BigIntStats } from "node:fs";
3
3
  import * as fs from "node:fs/promises";
4
4
  import * as os from "node:os";
5
5
  import path from "node:path";
6
- import type { NativeDirectoryTreeSnapshot } from "@gajae-code/natives";
6
+ import type { NativeBrokerRestartIntent, NativeDirectoryTreeSnapshot } from "@gajae-code/natives";
7
7
  import { logger, resolveEquivalentPath } from "@gajae-code/utils";
8
8
  import packageJson from "../../../package.json" with { type: "json" };
9
9
  import type { ModelProfileErrorDetails } from "../../config/model-profile-contract";
@@ -14,6 +14,7 @@ import {
14
14
  BROKER_RUNTIME_CLOSE_CAPABILITY_FIELD,
15
15
  } from "../host/control/runtime-gate";
16
16
  import { createDefaultSdkHostModelResolver, type SdkHostModelResolver } from "../host/model-pin";
17
+ import { canonicalServiceRootDigest } from "../service-artifact-paths";
17
18
  import {
18
19
  type DirectoryMigrationPolicy,
19
20
  listManagedSessionCandidates,
@@ -32,9 +33,10 @@ import {
32
33
  type RedactedBrokerDiscovery,
33
34
  type RetainedBrokerDiscovery,
34
35
  readBrokerDiscovery,
36
+ readBrokerRestartIntent,
35
37
  redactBrokerDiscovery,
36
38
  } from "./discovery";
37
- import { matchesIndexedEndpointFile, readEndpointFile } from "./endpoint-authority";
39
+ import { endpointIncarnation, matchesIndexedEndpointFile, readEndpointFile } from "./endpoint-authority";
38
40
  import { deriveIdempotencyIdentity, getBrokerIdentityKey } from "./identity";
39
41
  import {
40
42
  canonicalDeleteLocatorPath,
@@ -52,6 +54,7 @@ import {
52
54
  type LifecycleState,
53
55
  } from "./lifecycle-ledger";
54
56
  import { createMasterCapabilityVerifier, readEndpoint } from "./master-capability";
57
+ import { sdkInternalRuntimeImage } from "./runtime";
55
58
  import { type IndexedSession, isSessionAuthorityEligible, SessionIndex, type SessionList } from "./session-index";
56
59
  import {
57
60
  type ResolvedScopeV1,
@@ -77,6 +80,8 @@ import { BrokerTransport } from "./transport";
77
80
  export interface BrokerSettings {
78
81
  agentDir: string;
79
82
  packageGeneration?: string;
83
+ /** Published runtime image; tests inject one to model a vanished executable. */
84
+ runtime?: string;
80
85
  port?: number;
81
86
  heartbeatTtlMs?: number;
82
87
  /** Broker-owned migration policy. Client lifecycle frames cannot select it. */
@@ -91,11 +96,20 @@ export interface BrokerSettings {
91
96
  masterOrphanGraceMs?: number;
92
97
  /** Host model resolver override for lifecycle tests and embedders. */
93
98
  resolveModelPin?: SdkHostModelResolver;
99
+ /**
100
+ * Exact restart request this process instance is the authorized successor
101
+ * for. Set only by the real `broker-internal` entry point from its own
102
+ * `GJC_BROKER_RESTART_REQUEST` environment read — never inferred here from
103
+ * `process.env` directly, so the value this broker publishes is exactly the
104
+ * value its launcher decided, with no cast/assumed tag in between.
105
+ */
106
+ restartRequestId?: string;
94
107
  }
95
108
 
96
109
  type ResolvedBrokerSettings = {
97
110
  agentDir: string;
98
111
  packageGeneration: string;
112
+ runtime?: string;
99
113
  port: number;
100
114
  heartbeatTtlMs: number;
101
115
  resolveDirectoryMigration: (_cwd: string) => Promise<DirectoryMigrationPolicy>;
@@ -103,6 +117,7 @@ type ResolvedBrokerSettings = {
103
117
  spawnSubstrateProvider?: SpawnSubstrateProvider;
104
118
  spawnPromptLayer?: SpawnPromptLayer;
105
119
  masterOrphanGraceMs: number;
120
+ restartRequestId?: string;
106
121
  };
107
122
  export function resolveBrokerPackageGeneration(): string {
108
123
  const v = (packageJson as { version?: unknown }).version;
@@ -300,6 +315,16 @@ function lifecycleFingerprint(operation: string, input: Record<string, unknown>)
300
315
  .update(JSON.stringify({ operation, input: lifecycleRequestIdentity(input) }))
301
316
  .digest("hex");
302
317
  }
318
+
319
+ function publicLifecycleLookupInput(
320
+ input: Record<string, unknown>,
321
+ ): { operation: "session.create"; target: Record<string, unknown> } | BrokerResponse {
322
+ if (input.operation !== "session.create")
323
+ return error("invalid_input", "session.lookup only supports operation session.create");
324
+ const target = objectRecord(input.target);
325
+ if (!target) return error("invalid_input", "session.lookup target must be an object");
326
+ return { operation: "session.create", target };
327
+ }
303
328
  function lifecycleResponseState(response: BrokerResponse): LifecycleState {
304
329
  if (response.ok) return "terminal_ok";
305
330
  if (isCleanupPending(response)) return "effect_started";
@@ -732,31 +757,6 @@ type LifecycleReplayEndpoint = {
732
757
  };
733
758
 
734
759
  type EndpointAuthority = { endpointGeneration?: number; endpointIncarnation?: string };
735
- function endpointIncarnation(
736
- record: Pick<IndexedSession, "endpointGeneration" | "endpointMtimeMs" | "pid">,
737
- sessionId: string,
738
- ): string | undefined {
739
- if (
740
- !Number.isSafeInteger(record.endpointGeneration) ||
741
- record.endpointGeneration <= 0 ||
742
- !Number.isSafeInteger(record.pid) ||
743
- record.pid <= 0 ||
744
- typeof record.endpointMtimeMs !== "number" ||
745
- !Number.isFinite(record.endpointMtimeMs) ||
746
- record.endpointMtimeMs <= 0
747
- )
748
- return undefined;
749
- return createHash("sha256")
750
- .update(
751
- canonicalJson({
752
- endpointGeneration: record.endpointGeneration,
753
- endpointMtimeMs: record.endpointMtimeMs,
754
- pid: record.pid,
755
- sessionId,
756
- }),
757
- )
758
- .digest("hex");
759
- }
760
760
  function expectedEndpointAuthority(input: Record<string, unknown>): EndpointAuthority | BrokerResponse {
761
761
  const endpointGeneration = input.endpointGeneration;
762
762
  const endpointIncarnation = input.endpointIncarnation;
@@ -1117,6 +1117,32 @@ export interface StartupAdmissionTiming {
1117
1117
  sleep(ms: number, signal?: AbortSignal): Promise<void>;
1118
1118
  }
1119
1119
 
1120
+ export interface BrokerRestartOwnerIdentity {
1121
+ ownerId: string;
1122
+ generation: string;
1123
+ pid: number;
1124
+ incarnation: string;
1125
+ }
1126
+ export interface BrokerRestartPrepareOptions extends BrokerRestartOwnerIdentity {
1127
+ requestId: string;
1128
+ deadlineAt: number;
1129
+ drain?: boolean;
1130
+ }
1131
+ export interface BrokerRestartPrepareResult {
1132
+ lease: string;
1133
+ occupancyEpoch: number;
1134
+ expiresAt: number;
1135
+ requestId: string;
1136
+ owner: BrokerRestartOwnerIdentity;
1137
+ }
1138
+ export interface BrokerRestartCommitOptions extends BrokerRestartPrepareOptions {
1139
+ lease: string;
1140
+ occupancyEpoch: number;
1141
+ }
1142
+ export type BrokerRestartResult =
1143
+ | { ok: true; result: BrokerRestartPrepareResult | { committed: true } | { cancelled: true } }
1144
+ | { ok: false; error: { code: string; message: string } };
1145
+
1120
1146
  export type StartupAdmissionResult<T> =
1121
1147
  | { status: "completed"; admittedAt: number; value: T }
1122
1148
  | { status: "admission_timeout"; reason: "admission_timeout" }
@@ -1289,10 +1315,20 @@ export class Broker {
1289
1315
  #rejectCompletion!: (error: unknown) => void;
1290
1316
  #resolveModelPin: SdkHostModelResolver;
1291
1317
  #ownsResolveModelPin: boolean;
1318
+ #restart:
1319
+ | {
1320
+ options: BrokerRestartPrepareOptions;
1321
+ lease: string;
1322
+ occupancyEpoch: number;
1323
+ expiresAt: number;
1324
+ timer: NodeJS.Timeout;
1325
+ }
1326
+ | undefined;
1292
1327
  constructor(settings: BrokerSettings) {
1293
1328
  this.settings = {
1294
1329
  agentDir: settings.agentDir,
1295
1330
  packageGeneration: settings.packageGeneration ?? resolveBrokerPackageGeneration(),
1331
+ runtime: settings.runtime,
1296
1332
  port: settings.port ?? 0,
1297
1333
  heartbeatTtlMs: settings.heartbeatTtlMs ?? BROKER_HEARTBEAT_TTL_MS,
1298
1334
  resolveDirectoryMigration: settings.resolveDirectoryMigration ?? (async () => "copy-retain"),
@@ -1300,6 +1336,7 @@ export class Broker {
1300
1336
  spawnSubstrateProvider: settings.spawnSubstrateProvider,
1301
1337
  spawnPromptLayer: settings.spawnPromptLayer,
1302
1338
  masterOrphanGraceMs: settings.masterOrphanGraceMs ?? MASTER_ORPHAN_GRACE_DEFAULT_MS,
1339
+ ...(settings.restartRequestId === undefined ? {} : { restartRequestId: settings.restartRequestId }),
1303
1340
  };
1304
1341
  this.index = new SessionIndex(settings.agentDir);
1305
1342
  this.ledger = new LifecycleLedger(settings.agentDir);
@@ -2402,9 +2439,23 @@ export class Broker {
2402
2439
  async #createLock(): Promise<void> {
2403
2440
  await fs.mkdir(this.#lock, { mode: 0o700 });
2404
2441
  try {
2442
+ const incarnation = brokerProcessIncarnation(process.pid);
2443
+ // A minimal, non-secret receipt delta for D9 stale-artifact detach
2444
+ // (sequenced follow-up): the OS incarnation and a digest of the exact
2445
+ // resolved root this lock authorizes, so a later positive-death check can
2446
+ // cross-bind this owner record against the discovery record it authored
2447
+ // without re-deriving anything from file content or adding new identity.
2448
+ const rootDigest = await canonicalServiceRootDigest(this.settings.agentDir);
2405
2449
  await fs.writeFile(
2406
2450
  this.#lockRecordPath(),
2407
- JSON.stringify({ version: 1, ownerId: this.#owner, pid: process.pid, acquiredAt: Date.now() }),
2451
+ JSON.stringify({
2452
+ version: 1,
2453
+ ownerId: this.#owner,
2454
+ pid: process.pid,
2455
+ acquiredAt: Date.now(),
2456
+ ...(incarnation ? { incarnation } : {}),
2457
+ rootDigest,
2458
+ }),
2408
2459
  { flag: "wx", mode: 0o600 },
2409
2460
  );
2410
2461
  } catch (e) {
@@ -2577,6 +2628,7 @@ export class Broker {
2577
2628
  version: 1,
2578
2629
  protocolVersion: 3,
2579
2630
  packageGeneration: this.settings.packageGeneration,
2631
+ runtime: this.settings.runtime ?? sdkInternalRuntimeImage(),
2580
2632
  ownerId: this.#owner,
2581
2633
  pid: process.pid,
2582
2634
  incarnation,
@@ -2586,6 +2638,9 @@ export class Broker {
2586
2638
  token,
2587
2639
  startedAt: now,
2588
2640
  heartbeatAt: now,
2641
+ ...(this.settings.restartRequestId === undefined
2642
+ ? {}
2643
+ : { restartRequestId: this.settings.restartRequestId }),
2589
2644
  };
2590
2645
  // Readiness must not be externally visible until the initial session
2591
2646
  // checkpoint settles. The bootstrap watchdog owns this pre-publication
@@ -2603,6 +2658,23 @@ export class Broker {
2603
2658
  void this.#watchPublication();
2604
2659
  void this.#reapSpawnOrphans();
2605
2660
  }, cadenceMs);
2661
+ // This process is now the independently verified successor: its own
2662
+ // discovery just published under its own retained authority. Release the
2663
+ // predecessor's reservation only now, keyed to the exact request this
2664
+ // process was launched for -- never on a startup that carries no restart
2665
+ // request, and never before this publish proved this is the real owner.
2666
+ if (this.settings.restartRequestId !== undefined && this.#publication) {
2667
+ try {
2668
+ const intent = await readBrokerRestartIntent(this.settings.agentDir);
2669
+ if (intent && intent.phase === "committed" && intent.requestId === this.settings.restartRequestId)
2670
+ await this.#publication.clearForeignRestartIntent(intent.identity);
2671
+ } catch {
2672
+ // Best-effort reservation release. The admission gate in ensure.ts keys
2673
+ // off requestId equality, which already matches this exact successor,
2674
+ // so a retained committed intent here blocks nothing further; a failed
2675
+ // clear must never fail this broker's own successful startup.
2676
+ }
2677
+ }
2606
2678
  return this.discovery;
2607
2679
  } catch (error) {
2608
2680
  await this.#transport?.stop();
@@ -2620,6 +2692,130 @@ export class Broker {
2620
2692
  get completion(): Promise<void> {
2621
2693
  return this.#completion;
2622
2694
  }
2695
+ #restartIdentity(): BrokerRestartOwnerIdentity | undefined {
2696
+ const d = this.discovery;
2697
+ if (!d?.incarnation) return undefined;
2698
+ return { ownerId: d.ownerId, generation: d.packageGeneration, pid: d.pid, incarnation: d.incarnation };
2699
+ }
2700
+ #restartBusy(): boolean {
2701
+ for (const session of this.index.listSessions().sessions) {
2702
+ if (session.live && !session.terminal) return true;
2703
+ }
2704
+ return this.#admitted.size > 1;
2705
+ }
2706
+ async prepareRestart(options: BrokerRestartPrepareOptions): Promise<BrokerRestartResult> {
2707
+ const identity = this.#restartIdentity();
2708
+ if (
2709
+ !identity ||
2710
+ options.ownerId !== identity.ownerId ||
2711
+ options.generation !== identity.generation ||
2712
+ options.pid !== identity.pid ||
2713
+ options.incarnation !== identity.incarnation ||
2714
+ !options.requestId ||
2715
+ !Number.isSafeInteger(options.deadlineAt) ||
2716
+ options.deadlineAt <= Date.now()
2717
+ )
2718
+ return { ok: false, error: { code: "restart_identity_mismatch", message: "broker owner identity mismatch" } };
2719
+ if (this.#restart) {
2720
+ if (this.#restart.options.requestId !== options.requestId)
2721
+ return {
2722
+ ok: false,
2723
+ error: { code: "restart_request_mismatch", message: "restart request is already prepared" },
2724
+ };
2725
+ return {
2726
+ ok: true,
2727
+ result: {
2728
+ lease: this.#restart.lease,
2729
+ occupancyEpoch: this.#restart.occupancyEpoch,
2730
+ expiresAt: this.#restart.expiresAt,
2731
+ requestId: this.#restart.options.requestId,
2732
+ owner: identity,
2733
+ },
2734
+ };
2735
+ }
2736
+ this.#startupAdmissions.close();
2737
+ const occupancyEpoch = this.index.listSessions().indexSeq;
2738
+ const lease = randomBytes(24).toString("base64url");
2739
+ const expiresAt = Math.min(options.deadlineAt, Date.now() + 30_000);
2740
+ const timer = setTimeout(() => void this.cancelRestart(options.requestId), Math.max(1, expiresAt - Date.now()));
2741
+ this.#restart = { options, lease, occupancyEpoch, expiresAt, timer };
2742
+ const abortPrepare = async (code: string, message: string): Promise<BrokerRestartResult> => {
2743
+ clearTimeout(timer);
2744
+ this.#restart = undefined;
2745
+ this.#startupAdmissions.reopen();
2746
+ return { ok: false, error: { code, message } };
2747
+ };
2748
+ try {
2749
+ await this.index.refresh();
2750
+ } catch {
2751
+ return abortPrepare("restart_prepare_failed", "broker occupancy could not be refreshed");
2752
+ }
2753
+ const deadline = Math.min(options.deadlineAt, Date.now() + 30_000);
2754
+ while (options.drain && this.#restartBusy() && Date.now() < deadline) await Bun.sleep(25);
2755
+ if (this.#restartBusy()) return abortPrepare("restart_busy", "broker has active or admitted work");
2756
+ const intent: NativeBrokerRestartIntent = { requestId: options.requestId, lease, expiresAt };
2757
+ const publication = this.#publication;
2758
+ if (!publication || !(await publication.prepareRestartIntent(intent)))
2759
+ return abortPrepare("restart_prepare_failed", "restart intent could not be durably prepared");
2760
+ return { ok: true, result: { lease, occupancyEpoch, expiresAt, requestId: options.requestId, owner: identity } };
2761
+ }
2762
+ async commitRestart(options: BrokerRestartCommitOptions): Promise<BrokerRestartResult> {
2763
+ const current = this.#restart;
2764
+ const identity = this.#restartIdentity();
2765
+ if (
2766
+ !current ||
2767
+ !identity ||
2768
+ current.lease !== options.lease ||
2769
+ current.occupancyEpoch !== options.occupancyEpoch ||
2770
+ current.options.requestId !== options.requestId ||
2771
+ options.ownerId !== identity.ownerId ||
2772
+ options.generation !== identity.generation ||
2773
+ options.pid !== identity.pid ||
2774
+ options.incarnation !== identity.incarnation ||
2775
+ Date.now() >= current.expiresAt ||
2776
+ this.#restartBusy()
2777
+ )
2778
+ return { ok: false, error: { code: "restart_commit_refused", message: "restart commit proof is invalid" } };
2779
+ const publication = this.#publication;
2780
+ const intent: NativeBrokerRestartIntent = {
2781
+ requestId: options.requestId,
2782
+ lease: current.lease,
2783
+ expiresAt: current.expiresAt,
2784
+ };
2785
+ if (!publication || !(await publication.commitRestartIntent(intent)))
2786
+ return {
2787
+ ok: false,
2788
+ error: { code: "restart_commit_refused", message: "restart intent could not be durably committed" },
2789
+ };
2790
+ clearTimeout(current.timer);
2791
+ // The retained restart-intent slot (independent of `sdk/broker.lock/`)
2792
+ // deliberately survives this owned-root exit: it is the durable proof a
2793
+ // successor's ordinary startup consults, and clearing it here -- before
2794
+ // this process has actually exited -- would let a racing ordinary
2795
+ // `ensureBroker` treat the still-live old owner as idle and spawn early.
2796
+ setTimeout(() => {
2797
+ void this.#complete("owned-root").finally(() => {
2798
+ if (this.#restart === current) this.#restart = undefined;
2799
+ });
2800
+ }, 0);
2801
+ return { ok: true, result: { committed: true } };
2802
+ }
2803
+ async cancelRestart(requestId: string): Promise<BrokerRestartResult> {
2804
+ const current = this.#restart;
2805
+ if (!current) return { ok: true, result: { cancelled: true } };
2806
+ if (current.options.requestId !== requestId)
2807
+ return { ok: false, error: { code: "restart_request_mismatch", message: "restart request does not match" } };
2808
+ const publication = this.#publication;
2809
+ // Best-effort: the owner's own descriptor-bound cancel removes the intent
2810
+ // it prepared. A publication that is no longer reachable (already fenced,
2811
+ // already stopping) leaves the slot for lease-expiry discovery by the
2812
+ // caller instead of retrying indefinitely inside this admission path.
2813
+ if (publication) await publication.cancelRestartIntent().catch(() => undefined);
2814
+ clearTimeout(current.timer);
2815
+ this.#restart = undefined;
2816
+ this.#startupAdmissions.reopen();
2817
+ return { ok: true, result: { cancelled: true } };
2818
+ }
2623
2819
  status(): RedactedBrokerDiscovery | null {
2624
2820
  return this.discovery ? redactBrokerDiscovery(this.discovery) : null;
2625
2821
  }
@@ -3160,7 +3356,8 @@ export class Broker {
3160
3356
  };
3161
3357
  const sessions = snapshot.sessions.slice(snapshot.offset, snapshot.offset + snapshot.limit).map(session => {
3162
3358
  const { lifecycleRequestId: _lifecycleRequestId, ...publicSession } = session;
3163
- return publicSession;
3359
+ const incarnation = endpointIncarnation(session, session.sessionId);
3360
+ return incarnation === undefined ? publicSession : { ...publicSession, endpointIncarnation: incarnation };
3164
3361
  });
3165
3362
  const offset = snapshot.offset + sessions.length;
3166
3363
  if (offset >= snapshot.sessions.length && typeof cursor === "string") this.#sessionListCursors.delete(cursor);
@@ -3183,9 +3380,45 @@ export class Broker {
3183
3380
  indexSeq: snapshot.indexSeq,
3184
3381
  };
3185
3382
  }
3383
+ async #lookupLifecycle(
3384
+ requestedOperation: string,
3385
+ idempotencyKey: string | undefined,
3386
+ requestedFingerprint: string,
3387
+ ): Promise<BrokerResponse> {
3388
+ if (!idempotencyKey) return error("invalid_input", "operation, idempotencyKey, and fingerprint are required");
3389
+ if (requestedOperation === "session.spawn")
3390
+ return error("invalid_input", "session.spawn does not support lifecycle lookup");
3391
+ if (!LIFECYCLE_OPERATIONS.has(requestedOperation)) return error("not_found", "lifecycle operation was not found");
3392
+ const identity = await deriveIdempotencyIdentity(this.settings.agentDir, requestedOperation, idempotencyKey);
3393
+ const entry = this.ledger.get(identity);
3394
+ if (!entry) return error("not_found", "lifecycle operation was not found");
3395
+ if (entry.fingerprint !== requestedFingerprint)
3396
+ return error("idempotency_conflict", "lifecycle request fingerprint differs");
3397
+ if (entry.state === "terminal_uncertain")
3398
+ return error("terminal_uncertain", "lifecycle outcome is still uncertain");
3399
+ if (entry.state !== "terminal_ok" && entry.state !== "terminal_error")
3400
+ return error("lifecycle_pending", "lifecycle outcome is still pending");
3401
+ return isBrokerResponse(entry.response)
3402
+ ? entry.response
3403
+ : error("terminal_uncertain", "lifecycle outcome has no recorded response");
3404
+ }
3186
3405
  handleRequest(operation: string, input: Record<string, unknown>, idempotencyKey?: string): Promise<BrokerResponse> {
3406
+ if (operation === "broker.status") return Promise.resolve({ ok: true, result: this.status() });
3407
+ if (operation === "broker.prepare_restart")
3408
+ return this.prepareRestart(input as unknown as BrokerRestartPrepareOptions).then(
3409
+ result => result as BrokerResponse,
3410
+ );
3411
+ if (operation === "broker.commit_restart")
3412
+ return this.commitRestart(input as unknown as BrokerRestartCommitOptions).then(
3413
+ result => result as BrokerResponse,
3414
+ );
3415
+ if (operation === "broker.cancel_restart") {
3416
+ const requestId = typeof input.requestId === "string" ? input.requestId : "";
3417
+ return this.cancelRestart(requestId).then(result => result as BrokerResponse);
3418
+ }
3187
3419
  if (this.#stopping || (this.#publication !== null && this.#publicationState !== "healthy-owned"))
3188
3420
  return Promise.resolve(error("unavailable", "broker publication is unavailable"));
3421
+ if (this.#restart) return Promise.resolve(error("broker_restarting", "broker restart is prepared"));
3189
3422
  let release!: () => void;
3190
3423
  const admission = new Promise<void>(resolve => (release = resolve));
3191
3424
  this.#admitted.add(admission);
@@ -3208,6 +3441,15 @@ export class Broker {
3208
3441
  if (spawnClose) return spawnClose;
3209
3442
  }
3210
3443
  if (operation === "session.control") return this.#sessionControl(input, idempotencyKey);
3444
+ if (operation === "session.lookup") {
3445
+ const lookup = publicLifecycleLookupInput(input);
3446
+ if (isBrokerResponse(lookup)) return lookup;
3447
+ return await this.#lookupLifecycle(
3448
+ lookup.operation,
3449
+ idempotencyKey,
3450
+ lifecycleFingerprint(lookup.operation, lookup.target),
3451
+ );
3452
+ }
3211
3453
  const fingerprint = lifecycleFingerprint(operation, input);
3212
3454
  const normalization = normalizeBrokerInput(operation, input);
3213
3455
  if (isBrokerResponse(normalization)) return normalization;
@@ -3270,22 +3512,9 @@ export class Broker {
3270
3512
  if (operation === "broker.lookup_lifecycle") {
3271
3513
  const requestedOperation = typeof input.operation === "string" ? input.operation : undefined;
3272
3514
  const requestedFingerprint = typeof input.fingerprint === "string" ? input.fingerprint : undefined;
3273
- if (!idempotencyKey || !requestedOperation || !requestedFingerprint)
3515
+ if (!requestedOperation || !requestedFingerprint)
3274
3516
  return error("invalid_input", "operation, idempotencyKey, and fingerprint are required");
3275
- if (requestedOperation === "session.spawn")
3276
- return error("invalid_input", "session.spawn does not support lifecycle lookup");
3277
- if (!LIFECYCLE_OPERATIONS.has(requestedOperation))
3278
- return error("not_found", "lifecycle operation was not found");
3279
- const identity = await deriveIdempotencyIdentity(this.settings.agentDir, requestedOperation, idempotencyKey);
3280
- const entry = this.ledger.get(identity);
3281
- if (!entry) return error("not_found", "lifecycle operation was not found");
3282
- if (entry.fingerprint !== requestedFingerprint)
3283
- return error("idempotency_conflict", "lifecycle request fingerprint differs");
3284
- if (entry.state === "terminal_uncertain")
3285
- return error("terminal_uncertain", "lifecycle outcome is still uncertain");
3286
- return isBrokerResponse(entry.response)
3287
- ? entry.response
3288
- : error("terminal_uncertain", "lifecycle outcome has no recorded response");
3517
+ return await this.#lookupLifecycle(requestedOperation, idempotencyKey, requestedFingerprint);
3289
3518
  }
3290
3519
 
3291
3520
  if (!idempotencyKey) return error("invalid_input", "idempotencyKey is required for lifecycle operations");
@@ -0,0 +1,119 @@
1
+ import { type ChildProcess, spawn } from "node:child_process";
2
+ import { type BrokerDiscovery, readBrokerDiscovery, readBrokerRestartIntent } from "./discovery";
3
+ import { withBrokerStartupLock } from "./ensure";
4
+ import { observeProcessIncarnation } from "./process-incarnation";
5
+ import { resolveSdkInternalSpawnCommand } from "./runtime";
6
+
7
+ export interface AuthorizedBrokerSuccessorOptions {
8
+ agentDir: string;
9
+ requestId: string;
10
+ deadlineAt: number;
11
+ packageGeneration: string;
12
+ }
13
+
14
+ /** Typed outcome of a successor-launch attempt; never an opaque thrown message. */
15
+ export type AuthorizedBrokerSuccessorResult =
16
+ | { kind: "adopted"; discovery: BrokerDiscovery }
17
+ | { kind: "spawned"; discovery: BrokerDiscovery }
18
+ | {
19
+ kind: "refused";
20
+ reason:
21
+ | "deadline_elapsed"
22
+ | "intent_not_committed"
23
+ | "spawn_failed"
24
+ | "spawn_exited_before_publication"
25
+ | "publication_timeout";
26
+ detail?: string;
27
+ };
28
+
29
+ const SUCCESSOR_SPAWN_POLL_MS = 25;
30
+
31
+ /**
32
+ * Launches (or adopts) exactly one authorized successor for `requestId`.
33
+ *
34
+ * Holds `withBrokerStartupLock` only around the decision to spawn and the
35
+ * detached `spawn()` call itself -- never across the successor's own
36
+ * publication wait. The successor process (`commands/sdk.ts`'s
37
+ * `broker-internal` branch) acquires that SAME lock during its own startup to
38
+ * serialize against ordinary concurrent starts; holding it here while
39
+ * blocking on that child's publication would deadlock the parent against its
40
+ * own child. The unguarded polling loop below runs strictly after the lock
41
+ * has been released.
42
+ */
43
+ export async function launchAuthorizedBrokerSuccessor(
44
+ options: AuthorizedBrokerSuccessorOptions,
45
+ ): Promise<AuthorizedBrokerSuccessorResult> {
46
+ const spawnOutcome = await withBrokerStartupLock(options.agentDir, async deadline => {
47
+ if (Date.now() >= Math.min(deadline, options.deadlineAt))
48
+ return { kind: "refused" as const, reason: "deadline_elapsed" as const };
49
+ // Only a durably COMMITTED intent for this exact requestId authorizes a
50
+ // spawn. A prepared-but-not-committed or absent intent means the owner
51
+ // side of the protocol never reached its commit boundary, so no successor
52
+ // may exist yet -- spawning here would double-launch a broker the owner
53
+ // might still be about to retire cleanly on its own.
54
+ const intent = await readBrokerRestartIntent(options.agentDir);
55
+ if (intent?.phase !== "committed" || intent.requestId !== options.requestId)
56
+ return { kind: "refused" as const, reason: "intent_not_committed" as const };
57
+ const existing = await readBrokerDiscovery(options.agentDir);
58
+ if (
59
+ existing &&
60
+ existing.packageGeneration === options.packageGeneration &&
61
+ existing.restartRequestId === options.requestId
62
+ )
63
+ return { kind: "adopted" as const, discovery: existing };
64
+ const command = resolveSdkInternalSpawnCommand("broker-internal");
65
+ let child: ChildProcess;
66
+ try {
67
+ child = spawn(command.file, [...command.args, "--agent-dir", options.agentDir], {
68
+ detached: true,
69
+ stdio: "ignore",
70
+ env: { ...command.env, GJC_BROKER_RESTART_REQUEST: options.requestId },
71
+ ...(command.kind === "bun-source" ? { cwd: command.cwd } : {}),
72
+ });
73
+ } catch (spawnError) {
74
+ return {
75
+ kind: "refused" as const,
76
+ reason: "spawn_failed" as const,
77
+ detail: spawnError instanceof Error ? spawnError.message : String(spawnError),
78
+ };
79
+ }
80
+ let spawnError: Error | undefined;
81
+ child.once("error", childError => {
82
+ spawnError = childError;
83
+ });
84
+ child.unref();
85
+ return { kind: "spawned" as const, child, spawnError: () => spawnError };
86
+ });
87
+ if (spawnOutcome.kind !== "spawned") return spawnOutcome;
88
+ const { child } = spawnOutcome;
89
+ const until = Math.min(Date.now() + Math.max(1, options.deadlineAt - Date.now()), options.deadlineAt);
90
+ for (;;) {
91
+ if (spawnOutcome.spawnError())
92
+ return {
93
+ kind: "refused",
94
+ reason: "spawn_failed",
95
+ detail: spawnOutcome.spawnError()?.message,
96
+ };
97
+ if (child.exitCode !== null || child.signalCode !== null)
98
+ return { kind: "refused", reason: "spawn_exited_before_publication" };
99
+ const discovered = await readBrokerDiscovery(options.agentDir);
100
+ if (
101
+ discovered &&
102
+ discovered.packageGeneration === options.packageGeneration &&
103
+ discovered.restartRequestId === options.requestId
104
+ )
105
+ return { kind: "spawned", discovery: discovered };
106
+ if (Date.now() >= until) return { kind: "refused", reason: "publication_timeout" };
107
+ await Bun.sleep(SUCCESSOR_SPAWN_POLL_MS);
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Positive OS-confirmed absence of the exact prior owner, distinguished from
113
+ * every merely-inconclusive outcome (permission denial, PID reuse ambiguity,
114
+ * missing native addon). Never treats a null/changed incarnation read alone as
115
+ * death proof; only `observeProcessIncarnation`'s `"absent"` status counts.
116
+ */
117
+ export function oldOwnerConfirmedExited(pid: number): boolean {
118
+ return observeProcessIncarnation(pid).status === "absent";
119
+ }