@gajae-code/coding-agent 0.18.10 → 0.18.12

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 (270) hide show
  1. package/CHANGELOG.md +295 -0
  2. package/dist/types/async/job-manager.d.ts +7 -3
  3. package/dist/types/async/result-formatting.d.ts +2 -0
  4. package/dist/types/autoresearch/git.d.ts +1 -1
  5. package/dist/types/capability/fs.d.ts +2 -2
  6. package/dist/types/cli/accounts-cli.d.ts +2 -2
  7. package/dist/types/cli/doctor/install-repairs.d.ts +4 -1
  8. package/dist/types/cli/install-activation.d.ts +10 -4
  9. package/dist/types/cli/public-command-errors.d.ts +1 -0
  10. package/dist/types/cli/update-cli.d.ts +14 -1
  11. package/dist/types/cli-ordinary.d.ts +2 -2
  12. package/dist/types/commands/accounts.d.ts +2 -1
  13. package/dist/types/commands/migrate.d.ts +1 -1
  14. package/dist/types/commands/sdk.d.ts +5 -0
  15. package/dist/types/commit/changelog/index.d.ts +1 -0
  16. package/dist/types/config/autorouting-tier-map.d.ts +30 -0
  17. package/dist/types/config/config-hot-reload-trust.d.ts +12 -0
  18. package/dist/types/config/config-hot-reload.d.ts +39 -0
  19. package/dist/types/config/file-lock.d.ts +8 -1
  20. package/dist/types/config/model-discovery-manager.d.ts +2 -0
  21. package/dist/types/config/model-profile-activation.d.ts +62 -6
  22. package/dist/types/config/model-registry.d.ts +56 -5
  23. package/dist/types/config/model-resolver.d.ts +2 -0
  24. package/dist/types/config/models-config-schema.d.ts +10 -0
  25. package/dist/types/config/settings-schema.d.ts +46 -3
  26. package/dist/types/config/settings.d.ts +33 -1
  27. package/dist/types/dap/config.d.ts +8 -4
  28. package/dist/types/debug/system-info.d.ts +6 -1
  29. package/dist/types/discovery/mcp-compat.d.ts +3 -1
  30. package/dist/types/edit/modes/patch.d.ts +5 -0
  31. package/dist/types/edit/modes/replace.d.ts +3 -0
  32. package/dist/types/exec/bash-executor.d.ts +12 -0
  33. package/dist/types/gjc-runtime/managed-owner-admission.d.ts +3 -0
  34. package/dist/types/gjc-runtime/session-state-lock.d.ts +2 -0
  35. package/dist/types/gjc-runtime/state-runtime.d.ts +0 -2
  36. package/dist/types/migrate/adapters/codex.d.ts +2 -1
  37. package/dist/types/migrate/adapters/index.d.ts +10 -1
  38. package/dist/types/migrate/executor.d.ts +3 -1
  39. package/dist/types/migrate/types.d.ts +15 -3
  40. package/dist/types/modes/components/settings-selector.d.ts +6 -0
  41. package/dist/types/modes/controllers/selector-controller.d.ts +2 -0
  42. package/dist/types/modes/theme/theme.d.ts +1 -0
  43. package/dist/types/runtime-mcp/content-limits.d.ts +4 -0
  44. package/dist/types/runtime-mcp/manager.d.ts +7 -0
  45. package/dist/types/runtime-mcp/plugin-mcp-name-filter.d.ts +22 -0
  46. package/dist/types/runtime-mcp/tool-bridge.d.ts +4 -0
  47. package/dist/types/sdk/broker/broker-exit.d.ts +2 -2
  48. package/dist/types/sdk/broker/broker.d.ts +7 -1
  49. package/dist/types/sdk/broker/daemon-entry.d.ts +11 -1
  50. package/dist/types/sdk/broker/doctor-restart.d.ts +1 -1
  51. package/dist/types/sdk/broker/ensure.d.ts +136 -7
  52. package/dist/types/sdk/broker/hop.d.ts +36 -0
  53. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +18 -10
  54. package/dist/types/sdk/broker/lock-host-identity.d.ts +5 -0
  55. package/dist/types/sdk/broker/session-index.d.ts +4 -1
  56. package/dist/types/sdk/bus/chat-command-policy.d.ts +2 -1
  57. package/dist/types/sdk/bus/index.d.ts +23 -0
  58. package/dist/types/sdk/bus/reconciliation-store.d.ts +10 -0
  59. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +3 -1
  60. package/dist/types/sdk/cli/session-cli.d.ts +0 -1
  61. package/dist/types/sdk/host/session-runtime.d.ts +32 -0
  62. package/dist/types/sdk/prompt-deadline-flush.d.ts +42 -4
  63. package/dist/types/sdk/prompt-deadline-manager.d.ts +2 -0
  64. package/dist/types/sdk/protocol/adapter-validation.d.ts +10 -4
  65. package/dist/types/sdk/reconciliation-extensions.d.ts +1 -1
  66. package/dist/types/sdk/session-list.d.ts +1 -1
  67. package/dist/types/session/agent-session.d.ts +58 -2
  68. package/dist/types/session/internal/managed-session-scope.d.ts +2 -2
  69. package/dist/types/session/internal/managed-session-storage.d.ts +1 -1
  70. package/dist/types/session/internal/native-publish-outcome.d.ts +1 -1
  71. package/dist/types/session/messages.d.ts +5 -1
  72. package/dist/types/session/session-manager.d.ts +13 -3
  73. package/dist/types/session/startup-auth-config.d.ts +24 -2
  74. package/dist/types/session/streaming-output.d.ts +11 -0
  75. package/dist/types/session/terminal-abort.d.ts +3 -1
  76. package/dist/types/session-import/service.d.ts +1 -1
  77. package/dist/types/skill-state/workflow-mutation-guard.d.ts +7 -3
  78. package/dist/types/slash-commands/helpers/provider-auth-status.d.ts +8 -0
  79. package/dist/types/task/executor.d.ts +19 -2
  80. package/dist/types/tools/ast-edit-write-path.d.ts +10 -0
  81. package/dist/types/tools/atomic-file-write.d.ts +9 -7
  82. package/dist/types/tools/bash-allowed-prefixes.d.ts +11 -0
  83. package/dist/types/tools/descriptors.d.ts +2 -0
  84. package/dist/types/tools/fetch.d.ts +1 -1
  85. package/dist/types/tools/index.d.ts +2 -0
  86. package/dist/types/tools/python.d.ts +1 -1
  87. package/dist/types/tools/report-issue.d.ts +41 -0
  88. package/dist/types/tools/subagent.d.ts +23 -0
  89. package/dist/types/web/scrapers/types.d.ts +8 -1
  90. package/dist/types/web/search/providers/insane.d.ts +7 -1
  91. package/dist/types/web/search/providers/searxng.d.ts +5 -0
  92. package/package.json +8 -8
  93. package/scripts/build-binary.ts +2 -1
  94. package/scripts/capture-subagent-compact-panel-showcase.ts +105 -0
  95. package/scripts/generate-docs-index.ts +8 -1
  96. package/scripts/generate-sdk-operation-inventory.ts +22 -4
  97. package/src/async/job-manager.ts +356 -56
  98. package/src/async/result-formatting.ts +40 -0
  99. package/src/autoresearch/git.ts +9 -2
  100. package/src/autoresearch/prompts/prompt-setup.md +1 -1
  101. package/src/capability/fs.ts +27 -19
  102. package/src/capability/index.ts +2 -2
  103. package/src/cli/accounts-cli.ts +176 -8
  104. package/src/cli/auth-broker-cli.ts +21 -11
  105. package/src/cli/doctor/install-repairs.ts +35 -5
  106. package/src/cli/fast-help.ts +1 -1
  107. package/src/cli/install-activation.ts +47 -4
  108. package/src/cli/migrate-cli.ts +7 -5
  109. package/src/cli/public-command-errors.ts +4 -0
  110. package/src/cli/root-flags.ts +2 -2
  111. package/src/cli/stats-cli.ts +9 -5
  112. package/src/cli/update-cli.ts +224 -7
  113. package/src/cli-ordinary.ts +7 -2
  114. package/src/commands/accounts.ts +10 -5
  115. package/src/commands/migrate.ts +3 -3
  116. package/src/commands/sdk.ts +199 -42
  117. package/src/commands/ultragoal.ts +16 -8
  118. package/src/commit/changelog/index.ts +39 -1
  119. package/src/commit/message.ts +10 -3
  120. package/src/config/autorouting-tier-map.ts +30 -0
  121. package/src/config/config-hot-reload-trust.ts +148 -0
  122. package/src/config/config-hot-reload.ts +734 -0
  123. package/src/config/file-lock.ts +216 -56
  124. package/src/config/model-bindings-applier.ts +85 -47
  125. package/src/config/model-discovery-manager.ts +10 -0
  126. package/src/config/model-profile-activation.ts +716 -113
  127. package/src/config/model-registry.ts +1334 -158
  128. package/src/config/model-resolver.ts +3 -1
  129. package/src/config/models-config-schema.ts +6 -0
  130. package/src/config/settings-schema.ts +60 -3
  131. package/src/config/settings.ts +458 -24
  132. package/src/coordinator-mcp/policy.ts +26 -1
  133. package/src/crash/sanitize.ts +4 -0
  134. package/src/customization/import.ts +3 -1
  135. package/src/dap/config.ts +31 -11
  136. package/src/dap/defaults.json +1 -1
  137. package/src/debug/crash-diagnostics.ts +2 -1
  138. package/src/debug/report-bundle.ts +19 -0
  139. package/src/debug/system-info.ts +8 -3
  140. package/src/defaults/gjc/extensions/grok-cli-vendor/src/shared/base-url.ts +21 -7
  141. package/src/discovery/agents-md.ts +1 -1
  142. package/src/discovery/helpers.ts +1 -2
  143. package/src/discovery/mcp-compat.ts +120 -17
  144. package/src/edit/apply-patch/index.ts +12 -1
  145. package/src/edit/index.ts +13 -17
  146. package/src/edit/modes/patch.ts +187 -8
  147. package/src/edit/modes/replace.ts +136 -0
  148. package/src/eval/js/context-manager.ts +29 -3
  149. package/src/eval/js/worker-entry.ts +51 -18
  150. package/src/eval/py/executor.ts +14 -4
  151. package/src/eval/py/runner.py +144 -5
  152. package/src/exec/bash-executor.ts +81 -4
  153. package/src/exec/bash-shell-guardian.ts +4 -1
  154. package/src/exec/bash-shell-supervisor.ts +8 -4
  155. package/src/exec/isolated-shell.ts +5 -1
  156. package/src/export/html/index.ts +46 -6
  157. package/src/extensibility/custom-tools/wrapper.ts +3 -1
  158. package/src/extensibility/extensions/loader.ts +8 -1
  159. package/src/extensibility/plugins/marketplace/fetcher.ts +2 -2
  160. package/src/gjc-runtime/gc-runtime.ts +33 -6
  161. package/src/gjc-runtime/launch-tmux.ts +7 -2
  162. package/src/gjc-runtime/launch-worktree.test.ts +148 -0
  163. package/src/gjc-runtime/launch-worktree.ts +101 -14
  164. package/src/gjc-runtime/managed-owner-admission.ts +46 -2
  165. package/src/gjc-runtime/session-resolution.ts +38 -6
  166. package/src/gjc-runtime/session-state-lock.ts +59 -14
  167. package/src/gjc-runtime/state-runtime.ts +23 -19
  168. package/src/hooks/native-skill-hook.ts +14 -11
  169. package/src/internal-urls/docs-index.generated.ts +14 -13
  170. package/src/internal-urls/local-protocol.ts +142 -8
  171. package/src/lsp/index.ts +29 -0
  172. package/src/lsp/workspace-path.ts +23 -0
  173. package/src/migrate/action-planner.ts +57 -0
  174. package/src/migrate/adapters/claude-code.ts +23 -4
  175. package/src/migrate/adapters/codex.ts +22 -4
  176. package/src/migrate/adapters/index.ts +22 -4
  177. package/src/migrate/adapters/opencode.ts +6 -1
  178. package/src/migrate/executor.ts +84 -7
  179. package/src/migrate/report.ts +19 -2
  180. package/src/migrate/types.ts +14 -4
  181. package/src/modes/DESIGN.md +51 -0
  182. package/src/modes/acp/permission-mode.ts +18 -1
  183. package/src/modes/components/assistant-message.ts +55 -1
  184. package/src/modes/components/settings-selector.ts +103 -37
  185. package/src/modes/controllers/command-controller.ts +38 -2
  186. package/src/modes/controllers/plan-mode-controller.ts +17 -7
  187. package/src/modes/controllers/selector-controller.ts +62 -6
  188. package/src/modes/interactive-mode.ts +68 -0
  189. package/src/modes/theme/theme.ts +15 -0
  190. package/src/prompts/tools/debug.md +1 -1
  191. package/src/prompts/tools/read.md +1 -1
  192. package/src/prompts/tools/report-issue.md +13 -0
  193. package/src/prompts/tools/task.md +1 -1
  194. package/src/runtime-credential-selector.ts +2 -2
  195. package/src/runtime-mcp/content-limits.ts +7 -0
  196. package/src/runtime-mcp/manager-pool.test.ts +5 -4
  197. package/src/runtime-mcp/manager.ts +32 -0
  198. package/src/runtime-mcp/oauth-flow.ts +32 -6
  199. package/src/runtime-mcp/plugin-mcp-name-filter.ts +62 -0
  200. package/src/runtime-mcp/render.ts +16 -1
  201. package/src/runtime-mcp/tool-bridge.ts +126 -17
  202. package/src/sdk/broker/broker-exit.ts +8 -5
  203. package/src/sdk/broker/broker.ts +342 -82
  204. package/src/sdk/broker/daemon-entry.ts +338 -116
  205. package/src/sdk/broker/doctor-restart.ts +2 -1
  206. package/src/sdk/broker/ensure.ts +1143 -265
  207. package/src/sdk/broker/hop.ts +274 -0
  208. package/src/sdk/broker/lifecycle-ledger.ts +35 -11
  209. package/src/sdk/broker/lock-host-identity.ts +29 -0
  210. package/src/sdk/broker/session-index.ts +62 -12
  211. package/src/sdk/bus/chat-command-policy.ts +16 -3
  212. package/src/sdk/bus/index.ts +52 -21
  213. package/src/sdk/bus/reconciliation-store.ts +201 -8
  214. package/src/sdk/bus/telegram-daemon-contract.ts +3 -1
  215. package/src/sdk/cli/session-cli.ts +21 -6
  216. package/src/sdk/host/session-runtime.test.ts +118 -23
  217. package/src/sdk/host/session-runtime.ts +352 -47
  218. package/src/sdk/lifecycle/service.ts +1 -0
  219. package/src/sdk/prompt-deadline-flush.ts +78 -5
  220. package/src/sdk/prompt-deadline-manager.ts +68 -14
  221. package/src/sdk/protocol/adapter-validation.ts +53 -11
  222. package/src/sdk/protocol/operation-inventory.generated.json +101 -2
  223. package/src/sdk/reconciliation-extensions.ts +1 -0
  224. package/src/sdk/session-list.ts +1 -0
  225. package/src/sdk/session.ts +114 -61
  226. package/src/session/agent-session.ts +1043 -81
  227. package/src/session/internal/managed-session-scope.ts +126 -39
  228. package/src/session/internal/managed-session-storage.ts +14 -11
  229. package/src/session/internal/native-publish-outcome.ts +5 -1
  230. package/src/session/messages.ts +47 -6
  231. package/src/session/session-manager.ts +236 -14
  232. package/src/session/startup-auth-config.ts +116 -8
  233. package/src/session/streaming-output.ts +262 -4
  234. package/src/session/terminal-abort.ts +14 -2
  235. package/src/session-import/service.ts +1 -0
  236. package/src/skill-state/workflow-mutation-guard.ts +491 -32
  237. package/src/slash-commands/builtin-registry.ts +15 -3
  238. package/src/slash-commands/helpers/credential-switch.ts +10 -5
  239. package/src/slash-commands/helpers/provider-auth-status.ts +456 -0
  240. package/src/task/executor.ts +103 -12
  241. package/src/task/index.ts +389 -175
  242. package/src/tools/ast-edit-write-path.ts +64 -0
  243. package/src/tools/ast-edit.ts +18 -1
  244. package/src/tools/atomic-file-write.ts +46 -15
  245. package/src/tools/auto-generated-guard.ts +3 -0
  246. package/src/tools/bash-allowed-prefixes.ts +153 -5
  247. package/src/tools/bash-interactive.ts +14 -2
  248. package/src/tools/bash.ts +14 -0
  249. package/src/tools/debug.ts +7 -6
  250. package/src/tools/descriptors.test.ts +44 -0
  251. package/src/tools/descriptors.ts +34 -2
  252. package/src/tools/fetch.ts +3 -2
  253. package/src/tools/index.ts +2 -0
  254. package/src/tools/output-meta.ts +23 -2
  255. package/src/tools/python.ts +1 -1
  256. package/src/tools/read.ts +17 -35
  257. package/src/tools/report-issue.ts +184 -0
  258. package/src/tools/subagent-render.ts +114 -12
  259. package/src/tools/subagent.ts +108 -1
  260. package/src/tools/todo-write.ts +41 -3
  261. package/src/tools/tool-catalog.generated.ts +31 -3
  262. package/src/tools/write.ts +35 -3
  263. package/src/utils/commit-message-generator.ts +9 -2
  264. package/src/utils/file-mentions.ts +29 -1
  265. package/src/utils/title-generator.ts +7 -2
  266. package/src/web/scrapers/index.ts +3 -1
  267. package/src/web/scrapers/types.ts +8 -0
  268. package/src/web/scrapers/youtube.ts +3 -2
  269. package/src/web/search/providers/insane.ts +91 -28
  270. package/src/web/search/providers/searxng.ts +62 -10
@@ -1,7 +1,22 @@
1
- import { type ChildProcess, spawn } from "node:child_process";
2
- import { type BrokerDiscovery, readBrokerDiscovery, readBrokerRestartIntent } from "./discovery";
3
- import { withBrokerStartupLock } from "./ensure";
4
- import { isProcessIncarnation, observeProcessIncarnation } from "./process-incarnation";
1
+ import type * as childProcess from "node:child_process";
2
+ import { FileLockAcquireError } from "../../config/file-lock";
3
+ import {
4
+ type BrokerDiscovery,
5
+ type BrokerRestartIntentWithIdentity,
6
+ brokerProcessIncarnation,
7
+ readBrokerDiscovery,
8
+ readBrokerRestartIntent,
9
+ } from "./discovery";
10
+ import {
11
+ type BrokerLaunchHandoff,
12
+ type BrokerLaunchResult,
13
+ launchBrokerViaHop,
14
+ launchBrokerViaPosixTrampoline,
15
+ reapFailedBrokerLaunch,
16
+ resolveBrokerLaunchMode,
17
+ withBrokerStartupLock,
18
+ } from "./ensure";
19
+ import { observeProcessIncarnation } from "./process-incarnation";
5
20
  import { resolveSdkInternalSpawnCommand } from "./runtime";
6
21
 
7
22
  export interface AuthorizedBrokerSuccessorOptions {
@@ -14,7 +29,14 @@ export interface AuthorizedBrokerSuccessorOptions {
14
29
  /** Typed outcome of a successor-launch attempt; never an opaque thrown message. */
15
30
  export type AuthorizedBrokerSuccessorResult =
16
31
  | { kind: "adopted"; discovery: BrokerDiscovery }
17
- | { kind: "spawned"; discovery: BrokerDiscovery }
32
+ | {
33
+ kind: "spawned";
34
+ child: childProcess.ChildProcess;
35
+ realBrokerPid: number | undefined;
36
+ realBrokerIncarnation: string | undefined;
37
+ handoff: BrokerLaunchHandoff | undefined;
38
+ discovery: BrokerDiscovery;
39
+ }
18
40
  | {
19
41
  kind: "refused";
20
42
  reason:
@@ -22,12 +44,74 @@ export type AuthorizedBrokerSuccessorResult =
22
44
  | "intent_not_committed"
23
45
  | "spawn_failed"
24
46
  | "spawn_exited_before_publication"
25
- | "publication_timeout";
47
+ | "publication_timeout"
48
+ | "startup_lock_unavailable";
26
49
  detail?: string;
27
50
  };
28
51
 
52
+ type BrokerSuccessorSpawnOutcome =
53
+ | Exclude<AuthorizedBrokerSuccessorResult, { kind: "spawned" }>
54
+ | { kind: "retry" }
55
+ | {
56
+ kind: "spawned";
57
+ child: childProcess.ChildProcess;
58
+ realBrokerPid: number | undefined;
59
+ realBrokerIncarnation: string | undefined;
60
+ handoff: BrokerLaunchHandoff | undefined;
61
+ };
62
+
29
63
  const SUCCESSOR_SPAWN_POLL_MS = 25;
30
64
 
65
+ class BrokerSuccessorDeadlineError extends Error {}
66
+
67
+ async function readBeforeBrokerDeadline<T>(read: Promise<T>, deadlineAt: number, message: string): Promise<T> {
68
+ const remainingMs = deadlineAt - Date.now();
69
+ if (remainingMs <= 0) throw new BrokerSuccessorDeadlineError(message);
70
+ const timeout = Promise.withResolvers<T>();
71
+ const timer = setTimeout(() => timeout.reject(new BrokerSuccessorDeadlineError(message)), remainingMs);
72
+ try {
73
+ return await Promise.race([read, timeout.promise]);
74
+ } finally {
75
+ clearTimeout(timer);
76
+ }
77
+ }
78
+
79
+ async function refuseFailedBrokerLaunch(
80
+ launched: BrokerLaunchResult,
81
+ ): Promise<Extract<AuthorizedBrokerSuccessorResult, { kind: "refused" }>> {
82
+ const child: childProcess.ChildProcess = launched.process;
83
+ if (!launched.handoff) child.unref();
84
+ let detail = launched.error?.message ?? "Broker launch failed.";
85
+ try {
86
+ await reapFailedBrokerLaunch(launched);
87
+ } catch (error) {
88
+ const cleanupDetail = error instanceof Error ? error.message : String(error);
89
+ detail = `${detail}; retry cleanup failed: ${cleanupDetail}`;
90
+ }
91
+ return { kind: "refused", reason: "spawn_failed", detail };
92
+ }
93
+
94
+ async function refuseUnpublishedBrokerSuccessor(
95
+ child: childProcess.ChildProcess,
96
+ realBrokerPid: number | undefined,
97
+ realBrokerIncarnation: string | undefined,
98
+ handoff: BrokerLaunchResult["handoff"],
99
+ reason: "spawn_exited_before_publication" | "publication_timeout",
100
+ ): Promise<Extract<AuthorizedBrokerSuccessorResult, { kind: "refused" }>> {
101
+ let detail: string | undefined;
102
+ try {
103
+ await reapFailedBrokerLaunch({
104
+ process: child,
105
+ realBrokerPid,
106
+ realBrokerIncarnation,
107
+ handoff,
108
+ });
109
+ } catch (error) {
110
+ detail = `Broker successor cleanup failed: ${error instanceof Error ? error.message : String(error)}`;
111
+ }
112
+ return { kind: "refused", reason, ...(detail === undefined ? {} : { detail }) };
113
+ }
114
+
31
115
  /**
32
116
  * Launches (or adopts) exactly one authorized successor for `requestId`.
33
117
  *
@@ -43,132 +127,270 @@ const SUCCESSOR_SPAWN_POLL_MS = 25;
43
127
  export async function launchAuthorizedBrokerSuccessor(
44
128
  options: AuthorizedBrokerSuccessorOptions,
45
129
  ): 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(
65
- process.platform === "win32" ? "broker-internal" : "broker-trampoline-internal",
66
- );
67
- let child: ChildProcess;
68
- const isTrampoline = process.platform !== "win32";
130
+ let spawnOutcome: BrokerSuccessorSpawnOutcome;
131
+ let successorDeadlineAt = options.deadlineAt;
132
+ let startupLockFailure: FileLockAcquireError | undefined;
133
+ for (;;) {
134
+ if (Date.now() >= successorDeadlineAt)
135
+ return startupLockFailure
136
+ ? { kind: "refused", reason: "startup_lock_unavailable", detail: startupLockFailure.message }
137
+ : { kind: "refused", reason: "deadline_elapsed" };
69
138
  try {
70
- child = spawn(command.file, [...command.args, "--agent-dir", options.agentDir], {
71
- detached: true,
72
- stdio: isTrampoline ? ["ignore", "pipe", "ignore"] : "ignore",
73
- env: { ...command.env, GJC_BROKER_RESTART_REQUEST: options.requestId },
74
- ...(command.kind === "bun-source" ? { cwd: command.cwd } : {}),
75
- });
76
- } catch (spawnError) {
77
- return {
78
- kind: "refused" as const,
79
- reason: "spawn_failed" as const,
80
- detail: spawnError instanceof Error ? spawnError.message : String(spawnError),
81
- };
139
+ spawnOutcome = await withBrokerStartupLock(
140
+ options.agentDir,
141
+ async deadline => {
142
+ const operationDeadlineAt = Math.min(deadline, successorDeadlineAt);
143
+ const deadlineOutcome = (): BrokerSuccessorSpawnOutcome =>
144
+ operationDeadlineAt < successorDeadlineAt && Date.now() < successorDeadlineAt
145
+ ? { kind: "retry" }
146
+ : { kind: "refused", reason: "deadline_elapsed" };
147
+ if (Date.now() >= operationDeadlineAt) return deadlineOutcome();
148
+ // A matching published owner proves this request already succeeded; the
149
+ // broker may have cleared its committed intent after publication.
150
+ let existing: BrokerDiscovery | null;
151
+ try {
152
+ existing = await readBeforeBrokerDeadline(
153
+ readBrokerDiscovery(options.agentDir),
154
+ operationDeadlineAt,
155
+ "Timed out reading broker discovery before successor expiry.",
156
+ );
157
+ } catch (error) {
158
+ if (error instanceof BrokerSuccessorDeadlineError) return deadlineOutcome();
159
+ throw error;
160
+ }
161
+ if (Date.now() >= operationDeadlineAt) return deadlineOutcome();
162
+ if (
163
+ existing &&
164
+ existing.packageGeneration === options.packageGeneration &&
165
+ existing.restartRequestId === options.requestId
166
+ )
167
+ return { kind: "adopted" as const, discovery: existing };
168
+ // Only a durably COMMITTED intent for this exact requestId authorizes a
169
+ // new spawn. Re-read it on every lock attempt so a retry cannot use stale
170
+ // authorization or launch a duplicate successor.
171
+ let intent: BrokerRestartIntentWithIdentity | null;
172
+ try {
173
+ intent = await readBeforeBrokerDeadline(
174
+ readBrokerRestartIntent(options.agentDir),
175
+ operationDeadlineAt,
176
+ "Timed out validating the successor restart intent.",
177
+ );
178
+ } catch (error) {
179
+ if (error instanceof BrokerSuccessorDeadlineError) return deadlineOutcome();
180
+ throw error;
181
+ }
182
+ if (Date.now() >= operationDeadlineAt) return deadlineOutcome();
183
+ if (intent?.phase !== "committed" || intent.requestId !== options.requestId)
184
+ return { kind: "refused" as const, reason: "intent_not_committed" as const };
185
+ if (intent.expiresAt <= Date.now())
186
+ return { kind: "refused" as const, reason: "deadline_elapsed" as const };
187
+ successorDeadlineAt = Math.min(successorDeadlineAt, intent.expiresAt);
188
+ if (Date.now() >= successorDeadlineAt)
189
+ return { kind: "refused" as const, reason: "deadline_elapsed" as const };
190
+ const command = resolveSdkInternalSpawnCommand("broker-internal");
191
+ try {
192
+ const env = {
193
+ ...command.env,
194
+ GJC_BROKER_RESTART_REQUEST: options.requestId,
195
+ GJC_BROKER_RESTART_DEADLINE: String(intent.expiresAt),
196
+ };
197
+ const launchMode = resolveBrokerLaunchMode(process.platform, "discovery");
198
+ if (launchMode === "direct") throw new Error("Broker successor launch cannot use a fixture child.");
199
+ const timeoutMs = Math.max(0, Math.min(deadline, successorDeadlineAt) - Date.now());
200
+ const launched =
201
+ launchMode === "windows-hop"
202
+ ? await launchBrokerViaHop(
203
+ {
204
+ command: {
205
+ file: command.file,
206
+ args: [...command.args, "--agent-dir", options.agentDir],
207
+ },
208
+ ...(command.kind === "bun-source" ? { cwd: command.cwd } : {}),
209
+ },
210
+ {
211
+ env,
212
+ cwd: command.kind === "bun-source" ? command.cwd : undefined,
213
+ timeoutMs,
214
+ },
215
+ )
216
+ : await launchBrokerViaPosixTrampoline(options.agentDir, {
217
+ env,
218
+ ...(command.kind === "bun-source" ? { cwd: command.cwd } : {}),
219
+ timeoutMs,
220
+ retainChildUntilPublication: launchMode === "darwin-trampoline",
221
+ });
222
+ if (launched.error) return await refuseFailedBrokerLaunch(launched);
223
+ if (launched.realBrokerPid === undefined || launched.realBrokerIncarnation === undefined)
224
+ return await refuseFailedBrokerLaunch({
225
+ ...launched,
226
+ error: new Error("Broker launcher returned no verified process identity."),
227
+ });
228
+ const child: childProcess.ChildProcess = launched.process;
229
+ if (!launched.handoff) child.unref();
230
+ return {
231
+ kind: "spawned" as const,
232
+ child,
233
+ realBrokerPid: launched.realBrokerPid,
234
+ realBrokerIncarnation: launched.realBrokerIncarnation,
235
+ handoff: launched.handoff,
236
+ };
237
+ } catch (spawnError) {
238
+ return {
239
+ kind: "refused" as const,
240
+ reason: "spawn_failed" as const,
241
+ detail: spawnError instanceof Error ? spawnError.message : String(spawnError),
242
+ };
243
+ }
244
+ },
245
+ { deadlineAt: options.deadlineAt },
246
+ );
247
+ startupLockFailure = undefined;
248
+ if (spawnOutcome.kind === "retry") {
249
+ // The validation read exhausted this short lock operation, not the
250
+ // committed lease. The lock has been released; revalidate under a fresh
251
+ // operation window while the request is still authorized.
252
+ const remainingMs = successorDeadlineAt - Date.now();
253
+ if (remainingMs > 0) await Bun.sleep(Math.min(SUCCESSOR_SPAWN_POLL_MS, remainingMs));
254
+ continue;
255
+ }
256
+ break;
257
+ } catch (lockError) {
258
+ if (!(lockError instanceof FileLockAcquireError)) throw lockError;
259
+ startupLockFailure = lockError;
260
+ if (Date.now() >= successorDeadlineAt)
261
+ return { kind: "refused", reason: "startup_lock_unavailable", detail: lockError.message };
262
+ await Bun.sleep(Math.min(50, options.deadlineAt - Date.now()));
82
263
  }
83
- let spawnError: Error | undefined;
84
- let trampolineOutput = "";
85
- let trampolineOutputEnded = !isTrampoline;
86
- if (child.stdout)
87
- child.stdout
88
- .setEncoding("utf8")
89
- .on("data", chunk => (trampolineOutput += chunk))
90
- .once("end", () => (trampolineOutputEnded = true));
91
- child.once("error", childError => {
92
- spawnError = childError;
93
- });
94
- child.unref();
95
- return {
96
- kind: "spawned" as const,
97
- child,
98
- isTrampoline,
99
- trampolineOutput: () => trampolineOutput,
100
- trampolineOutputEnded: () => trampolineOutputEnded,
101
- spawnError: () => spawnError,
102
- };
103
- });
264
+ }
104
265
  if (spawnOutcome.kind !== "spawned") return spawnOutcome;
105
266
  const { child } = spawnOutcome;
106
- let successorPid: number | undefined;
107
- let successorIncarnation: string | undefined;
108
- let trampolineExited = !spawnOutcome.isTrampoline;
109
- let trampolineExitCode: number | null = null;
110
- let trampolineSignal: NodeJS.Signals | null = null;
111
- if (spawnOutcome.isTrampoline)
112
- child.once("exit", (code, signal) => {
113
- trampolineExitCode = code;
114
- trampolineSignal = signal;
115
- trampolineExited = true;
116
- });
117
- if (spawnOutcome.isTrampoline && (child.exitCode !== null || child.signalCode !== null)) {
118
- trampolineExitCode = child.exitCode;
119
- trampolineSignal = child.signalCode;
120
- trampolineExited = true;
121
- }
122
- const until = Math.min(Date.now() + Math.max(1, options.deadlineAt - Date.now()), options.deadlineAt);
267
+ const realBrokerPid = spawnOutcome.realBrokerPid ?? child.pid;
268
+ let expectedBrokerIncarnation =
269
+ spawnOutcome.realBrokerIncarnation ??
270
+ (realBrokerPid !== undefined && realBrokerPid === child.pid
271
+ ? brokerProcessIncarnation(realBrokerPid)
272
+ : undefined);
273
+ const until = Math.min(Date.now() + Math.max(1, successorDeadlineAt - Date.now()), successorDeadlineAt);
123
274
  for (;;) {
124
- if (spawnOutcome.spawnError())
125
- return {
126
- kind: "refused",
127
- reason: "spawn_failed",
128
- detail: spawnOutcome.spawnError()?.message,
129
- };
130
- if (!spawnOutcome.isTrampoline && (child.exitCode !== null || child.signalCode !== null))
131
- return { kind: "refused", reason: "spawn_exited_before_publication" };
132
- if (spawnOutcome.isTrampoline && !successorPid) {
133
- const report = spawnOutcome.trampolineOutput().trim();
134
- const match = /^(\d+)\t([^\r\n]+)$/.exec(report);
135
- if (match) {
136
- const pid = Number(match[1]);
137
- const incarnation = match[2];
138
- if (Number.isSafeInteger(pid) && pid > 0 && isProcessIncarnation(incarnation)) {
139
- const observed = observeProcessIncarnation(pid);
140
- if (observed.status === "present" && observed.incarnation === incarnation) {
141
- successorPid = pid;
142
- successorIncarnation = incarnation;
143
- }
144
- }
145
- }
146
- }
147
- if (successorPid) {
148
- const observed = observeProcessIncarnation(successorPid);
275
+ if (Date.now() >= until)
276
+ return await refuseUnpublishedBrokerSuccessor(
277
+ child,
278
+ realBrokerPid,
279
+ expectedBrokerIncarnation,
280
+ spawnOutcome.handoff,
281
+ "publication_timeout",
282
+ );
283
+ // On Darwin the trampoline remains alive until publication; elsewhere the
284
+ // launcher exits after handoff. The broker identity remains authoritative.
285
+ const failedSpawn = child.signalCode !== null || (child.exitCode !== null && child.exitCode !== 0);
286
+ if (failedSpawn)
287
+ return await refuseUnpublishedBrokerSuccessor(
288
+ child,
289
+ realBrokerPid,
290
+ expectedBrokerIncarnation,
291
+ spawnOutcome.handoff,
292
+ "spawn_exited_before_publication",
293
+ );
294
+ if (realBrokerPid !== undefined) {
295
+ const observation = observeProcessIncarnation(realBrokerPid);
149
296
  if (
150
- observed.status === "absent" ||
151
- (observed.status === "present" && observed.incarnation !== successorIncarnation)
297
+ observation.status === "absent" ||
298
+ (observation.status === "present" &&
299
+ expectedBrokerIncarnation !== undefined &&
300
+ observation.incarnation !== expectedBrokerIncarnation)
152
301
  )
153
- return { kind: "refused", reason: "spawn_exited_before_publication" };
302
+ return await refuseUnpublishedBrokerSuccessor(
303
+ child,
304
+ realBrokerPid,
305
+ expectedBrokerIncarnation,
306
+ spawnOutcome.handoff,
307
+ "spawn_exited_before_publication",
308
+ );
309
+ if (
310
+ observation.status === "present" &&
311
+ expectedBrokerIncarnation === undefined &&
312
+ realBrokerPid === child.pid
313
+ ) {
314
+ expectedBrokerIncarnation = observation.incarnation;
315
+ }
316
+ }
317
+ let discovered: BrokerDiscovery | null;
318
+ try {
319
+ discovered = await readBeforeBrokerDeadline(
320
+ readBrokerDiscovery(options.agentDir),
321
+ until,
322
+ "Timed out waiting for broker discovery before successor expiry.",
323
+ );
324
+ } catch (error) {
325
+ if (!(error instanceof BrokerSuccessorDeadlineError)) throw error;
326
+ return await refuseUnpublishedBrokerSuccessor(
327
+ child,
328
+ realBrokerPid,
329
+ expectedBrokerIncarnation,
330
+ spawnOutcome.handoff,
331
+ "publication_timeout",
332
+ );
154
333
  }
155
- if (spawnOutcome.isTrampoline && trampolineExited && spawnOutcome.trampolineOutputEnded() && !successorPid)
156
- return { kind: "refused", reason: "spawn_exited_before_publication" };
157
- if (spawnOutcome.isTrampoline && trampolineExited && (trampolineSignal !== null || trampolineExitCode !== 0))
158
- return { kind: "refused", reason: "spawn_exited_before_publication" };
159
- const discovered = await readBrokerDiscovery(options.agentDir);
160
334
  if (
161
335
  discovered &&
336
+ discovered.pid === realBrokerPid &&
337
+ discovered.incarnation === expectedBrokerIncarnation &&
162
338
  discovered.packageGeneration === options.packageGeneration &&
163
339
  discovered.restartRequestId === options.requestId
164
- )
165
- if (!spawnOutcome.isTrampoline || (trampolineExited && spawnOutcome.trampolineOutputEnded()))
166
- return { kind: "spawned", discovery: discovered };
167
- if (Date.now() >= until) return { kind: "refused", reason: "publication_timeout" };
340
+ ) {
341
+ if (Date.now() >= until)
342
+ return await refuseUnpublishedBrokerSuccessor(
343
+ child,
344
+ realBrokerPid,
345
+ expectedBrokerIncarnation,
346
+ spawnOutcome.handoff,
347
+ "publication_timeout",
348
+ );
349
+ try {
350
+ await spawnOutcome.handoff?.release();
351
+ } catch (error) {
352
+ const refusal = await refuseUnpublishedBrokerSuccessor(
353
+ child,
354
+ realBrokerPid,
355
+ expectedBrokerIncarnation,
356
+ spawnOutcome.handoff,
357
+ "spawn_exited_before_publication",
358
+ );
359
+ return {
360
+ ...refusal,
361
+ detail: `Broker successor release failed: ${error instanceof Error ? error.message : String(error)}${
362
+ refusal.detail ? `; ${refusal.detail}` : ""
363
+ }`,
364
+ };
365
+ }
366
+ return {
367
+ kind: "spawned" as const,
368
+ child,
369
+ realBrokerPid,
370
+ realBrokerIncarnation: expectedBrokerIncarnation,
371
+ handoff: spawnOutcome.handoff,
372
+ discovery: discovered,
373
+ };
374
+ }
375
+ if (Date.now() >= until)
376
+ return await refuseUnpublishedBrokerSuccessor(
377
+ child,
378
+ realBrokerPid,
379
+ expectedBrokerIncarnation,
380
+ spawnOutcome.handoff,
381
+ "publication_timeout",
382
+ );
168
383
  await Bun.sleep(SUCCESSOR_SPAWN_POLL_MS);
169
384
  }
170
385
  }
171
386
 
387
+ /** Test hook: retries exact cleanup before mapping a launch error to refusal. */
388
+ export function refuseFailedBrokerLaunchForTest(
389
+ launched: BrokerLaunchResult,
390
+ ): Promise<Extract<AuthorizedBrokerSuccessorResult, { kind: "refused" }>> {
391
+ return refuseFailedBrokerLaunch(launched);
392
+ }
393
+
172
394
  /**
173
395
  * Positive OS-confirmed absence of the exact prior owner, distinguished from
174
396
  * every merely-inconclusive outcome (permission denial, PID reuse ambiguity,
@@ -48,7 +48,8 @@ export type DoctorBrokerRestartOutcome =
48
48
  | "intent_not_committed"
49
49
  | "spawn_failed"
50
50
  | "spawn_exited_before_publication"
51
- | "publication_timeout";
51
+ | "publication_timeout"
52
+ | "startup_lock_unavailable";
52
53
  detail?: string;
53
54
  };
54
55