@gajae-code/coding-agent 0.15.5 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (272) hide show
  1. package/CHANGELOG.md +85 -1
  2. package/dist/types/async/job-manager.d.ts +83 -0
  3. package/dist/types/cli/args.d.ts +2 -0
  4. package/dist/types/cli/auth-gateway-cli.d.ts +17 -0
  5. package/dist/types/cli/model-presets-cli.d.ts +9 -0
  6. package/dist/types/cli/root-flags.d.ts +14 -0
  7. package/dist/types/cli/update-cli.d.ts +15 -0
  8. package/dist/types/cli.d.ts +7 -0
  9. package/dist/types/commands/auth-gateway.d.ts +3 -0
  10. package/dist/types/commands/gc.d.ts +13 -0
  11. package/dist/types/commands/launch.d.ts +10 -0
  12. package/dist/types/commands/model-presets.d.ts +21 -0
  13. package/dist/types/config/autorouting-tier-map.d.ts +120 -0
  14. package/dist/types/config/file-lock.d.ts +54 -2
  15. package/dist/types/config/internal/model-preset-registry-test-state.d.ts +4 -0
  16. package/dist/types/config/internal/model-preset-registry-test-support.d.ts +3 -0
  17. package/dist/types/config/keybindings.d.ts +3 -2
  18. package/dist/types/config/model-preset-registry.d.ts +368 -0
  19. package/dist/types/config/model-profile-activation.d.ts +39 -0
  20. package/dist/types/config/model-profiles.d.ts +5 -3
  21. package/dist/types/config/model-registry.d.ts +17 -5
  22. package/dist/types/config/settings-schema.d.ts +19 -1
  23. package/dist/types/config/settings.d.ts +2 -0
  24. package/dist/types/coordinator-mcp/model-preset.d.ts +7 -5
  25. package/dist/types/coordinator-mcp/projection-scan.d.ts +54 -0
  26. package/dist/types/crash/record-loader.d.ts +15 -0
  27. package/dist/types/extensibility/extensions/types.d.ts +2 -5
  28. package/dist/types/gjc-runtime/empty-delete-gc.d.ts +58 -0
  29. package/dist/types/gjc-runtime/gc-runtime.d.ts +2 -0
  30. package/dist/types/gjc-runtime/launch-worktree-reservation.d.ts +21 -0
  31. package/dist/types/gjc-runtime/managed-owner-supervisor.d.ts +2 -0
  32. package/dist/types/gjc-runtime/session-state-lock.d.ts +35 -6
  33. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +72 -1
  34. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +1 -0
  35. package/dist/types/gjc-runtime/tmux-sessions.d.ts +27 -0
  36. package/dist/types/index.d.ts +1 -0
  37. package/dist/types/main.d.ts +3 -1
  38. package/dist/types/master-mode/context.d.ts +21 -0
  39. package/dist/types/master-mode/first-request.d.ts +34 -0
  40. package/dist/types/master-mode/snapshot.d.ts +32 -0
  41. package/dist/types/modes/components/frictionless-onboarding-selector.d.ts +10 -0
  42. package/dist/types/modes/components/jobs-overlay-model.d.ts +4 -2
  43. package/dist/types/modes/components/oauth-selector.d.ts +1 -0
  44. package/dist/types/modes/components/status-line/model-name.d.ts +18 -0
  45. package/dist/types/modes/components/status-line/priority-row.d.ts +34 -0
  46. package/dist/types/modes/components/status-line/segments.d.ts +10 -0
  47. package/dist/types/modes/jobs-observer.d.ts +36 -0
  48. package/dist/types/modes/print-mode.d.ts +17 -0
  49. package/dist/types/modes/ui-language.d.ts +7 -1
  50. package/dist/types/sdk/broker/broker.d.ts +80 -1
  51. package/dist/types/sdk/broker/endpoint-authority.d.ts +24 -0
  52. package/dist/types/sdk/broker/index.d.ts +3 -0
  53. package/dist/types/sdk/broker/lifecycle.d.ts +26 -0
  54. package/dist/types/sdk/broker/master-capability.d.ts +10 -0
  55. package/dist/types/sdk/broker/session-index.d.ts +40 -11
  56. package/dist/types/sdk/broker/session-scope.d.ts +72 -0
  57. package/dist/types/sdk/broker/spawn-authority.d.ts +201 -0
  58. package/dist/types/sdk/broker/spawn-substrate.d.ts +30 -0
  59. package/dist/types/sdk/broker/state-version.d.ts +4 -1
  60. package/dist/types/sdk/broker/worktree-occupancy.d.ts +28 -0
  61. package/dist/types/sdk/bus/chat-daemon-control.d.ts +3 -0
  62. package/dist/types/sdk/bus/index.d.ts +6 -0
  63. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +3 -0
  64. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +4 -0
  65. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
  66. package/dist/types/sdk/cli/index.d.ts +1 -0
  67. package/dist/types/sdk/cli/master-cli.d.ts +63 -0
  68. package/dist/types/sdk/cli/rows.d.ts +4 -3
  69. package/dist/types/sdk/cli/session-cli.d.ts +18 -2
  70. package/dist/types/sdk/client/client.d.ts +3 -2
  71. package/dist/types/sdk/client/discovery.d.ts +1 -0
  72. package/dist/types/sdk/host/control/dispatch.d.ts +10 -0
  73. package/dist/types/sdk/host/control/index.d.ts +1 -1
  74. package/dist/types/sdk/host/control/operations.d.ts +2 -0
  75. package/dist/types/sdk/host/control/runtime-gate.d.ts +21 -7
  76. package/dist/types/sdk/host/host.d.ts +10 -0
  77. package/dist/types/sdk/host/model-pin.d.ts +7 -2
  78. package/dist/types/sdk/host/sdk-run-capability.d.ts +1 -0
  79. package/dist/types/sdk/host/session-runtime.d.ts +67 -1
  80. package/dist/types/sdk/lifecycle/broker-client.d.ts +6 -0
  81. package/dist/types/sdk/lifecycle/service.d.ts +15 -3
  82. package/dist/types/sdk/lifecycle-session.d.ts +2 -0
  83. package/dist/types/sdk/prompt-deadline-manager.d.ts +11 -1
  84. package/dist/types/sdk/protocol/adapter-validation.d.ts +2 -0
  85. package/dist/types/sdk/router/session-router.d.ts +11 -0
  86. package/dist/types/sdk/session.d.ts +16 -0
  87. package/dist/types/sdk/startup-capability.d.ts +2 -1
  88. package/dist/types/session/account-inventory.d.ts +1 -0
  89. package/dist/types/session/agent-session.d.ts +24 -7
  90. package/dist/types/session/credential-store-errors.d.ts +1 -0
  91. package/dist/types/session/fold-coordinator.d.ts +201 -0
  92. package/dist/types/session/sdk-run-capability-internal.d.ts +7 -0
  93. package/dist/types/session/sdk-run-capability.d.ts +2 -0
  94. package/dist/types/session/session-manager.d.ts +65 -0
  95. package/dist/types/session/yield-queue.d.ts +5 -4
  96. package/dist/types/setup/frictionless-onboarding.d.ts +1 -0
  97. package/dist/types/telemetry/control.d.ts +11 -0
  98. package/dist/types/telemetry/events.d.ts +20 -0
  99. package/dist/types/telemetry/index.d.ts +3 -0
  100. package/dist/types/telemetry/transport.d.ts +22 -0
  101. package/dist/types/tools/bash-interactive.d.ts +23 -0
  102. package/dist/types/tools/bash.d.ts +4 -0
  103. package/dist/types/tools/gh.d.ts +8 -0
  104. package/dist/types/tools/index.d.ts +10 -5
  105. package/dist/types/web/search/providers/base.d.ts +9 -4
  106. package/dist/types/web/search/providers/xai.d.ts +2 -1
  107. package/dist/types/web/search/types.d.ts +14 -0
  108. package/package.json +10 -7
  109. package/scripts/generate-hotkeys-docs.ts +4 -0
  110. package/scripts/generate-sdk-operation-inventory.ts +9 -3
  111. package/scripts/verify-gjc-sdk-canonicalization.ts +35 -1
  112. package/src/async/job-manager.ts +671 -39
  113. package/src/cli/accounts-cli.ts +9 -5
  114. package/src/cli/args.ts +17 -0
  115. package/src/cli/auth-broker-cli.ts +40 -12
  116. package/src/cli/auth-gateway-cli.ts +273 -97
  117. package/src/cli/config-cli.ts +2 -2
  118. package/src/cli/fast-help.ts +4 -0
  119. package/src/cli/model-presets-cli.ts +100 -0
  120. package/src/cli/root-flags.ts +5 -0
  121. package/src/cli/update-cli.ts +117 -16
  122. package/src/cli.ts +6 -2
  123. package/src/commands/auth-gateway.ts +12 -8
  124. package/src/commands/gc.ts +19 -0
  125. package/src/commands/launch.ts +86 -18
  126. package/src/commands/model-presets.ts +24 -0
  127. package/src/commands/sdk.ts +113 -21
  128. package/src/config/autorouting-tier-map.ts +44 -0
  129. package/src/config/file-lock-gc.ts +9 -4
  130. package/src/config/file-lock.ts +890 -99
  131. package/src/config/internal/model-preset-registry-test-state.ts +28 -0
  132. package/src/config/internal/model-preset-registry-test-support.ts +11 -0
  133. package/src/config/keybindings.ts +6 -2
  134. package/src/config/model-preset-registry.ts +2795 -0
  135. package/src/config/model-profile-activation.ts +115 -28
  136. package/src/config/model-profile-contract.ts +4 -1
  137. package/src/config/model-profiles.ts +56 -14
  138. package/src/config/model-registry.ts +1044 -122
  139. package/src/config/model-resolver.ts +5 -0
  140. package/src/config/settings-schema.ts +19 -1
  141. package/src/config/settings.ts +11 -1
  142. package/src/coordinator-mcp/model-preset.ts +32 -17
  143. package/src/coordinator-mcp/projection-scan.ts +592 -0
  144. package/src/coordinator-mcp/question-state.ts +9 -11
  145. package/src/coordinator-mcp/server.ts +111 -42
  146. package/src/coordinator-mcp/session-reaper.ts +9 -3
  147. package/src/crash/index-store.ts +2 -0
  148. package/src/crash/record-loader.ts +10 -0
  149. package/src/extensibility/extensions/loader.ts +0 -1
  150. package/src/extensibility/extensions/types.ts +2 -3
  151. package/src/extensibility/gjc-plugins/runtime-adapters.ts +1 -0
  152. package/src/gjc-runtime/empty-delete-gc.ts +301 -0
  153. package/src/gjc-runtime/gc-render.ts +29 -0
  154. package/src/gjc-runtime/gc-runtime.ts +174 -11
  155. package/src/gjc-runtime/launch-worktree-reservation.ts +242 -0
  156. package/src/gjc-runtime/managed-owner-supervisor.ts +63 -51
  157. package/src/gjc-runtime/session-state-lock.ts +1321 -147
  158. package/src/gjc-runtime/session-state-sidecar.ts +1004 -25
  159. package/src/gjc-runtime/tmux-owner-isolation-cli.ts +4 -7
  160. package/src/gjc-runtime/tmux-owner-isolation.ts +2 -2
  161. package/src/gjc-runtime/tmux-sessions.ts +252 -13
  162. package/src/hooks/skill-state.ts +6 -0
  163. package/src/index.ts +1 -0
  164. package/src/internal-urls/docs-index.generated.ts +14 -13
  165. package/src/internal-urls/issue-pr-protocol.ts +4 -2
  166. package/src/lsp/config.ts +4 -3
  167. package/src/lsp/path-trust.ts +12 -6
  168. package/src/main.ts +66 -4
  169. package/src/master-mode/context.ts +50 -0
  170. package/src/master-mode/first-request.ts +65 -0
  171. package/src/master-mode/snapshot.ts +205 -0
  172. package/src/modes/acp/acp-agent.ts +616 -161
  173. package/src/modes/components/footer.ts +4 -2
  174. package/src/modes/components/frictionless-onboarding-selector.ts +48 -2
  175. package/src/modes/components/jobs-overlay-model.ts +99 -16
  176. package/src/modes/components/model-selector.ts +158 -18
  177. package/src/modes/components/oauth-selector.ts +6 -1
  178. package/src/modes/components/settings-selector.ts +1 -0
  179. package/src/modes/components/status-line/model-name.ts +53 -0
  180. package/src/modes/components/status-line/priority-row.ts +107 -0
  181. package/src/modes/components/status-line/segments.ts +29 -10
  182. package/src/modes/components/tool-status-header.ts +151 -15
  183. package/src/modes/controllers/extension-ui-controller.ts +1 -1
  184. package/src/modes/controllers/input-controller.ts +51 -6
  185. package/src/modes/controllers/selector-controller.ts +191 -96
  186. package/src/modes/interactive-mode.ts +14 -1
  187. package/src/modes/jobs-observer.ts +149 -16
  188. package/src/modes/print-mode.ts +64 -0
  189. package/src/modes/runtime-init.ts +1 -1
  190. package/src/modes/ui-language.ts +49 -2
  191. package/src/modes/utils/injected-user-submission.ts +15 -1
  192. package/src/prompts/system/master-mode.md +28 -0
  193. package/src/prompts/tools/bash.md +2 -2
  194. package/src/prompts/tools/fold-receipt.md +3 -0
  195. package/src/prompts/tools/job.md +1 -1
  196. package/src/sdk/acp/adapter.ts +17 -2
  197. package/src/sdk/broker/broker.ts +1671 -34
  198. package/src/sdk/broker/endpoint-authority.ts +86 -0
  199. package/src/sdk/broker/ensure.ts +5 -0
  200. package/src/sdk/broker/index.ts +3 -0
  201. package/src/sdk/broker/lifecycle.ts +217 -46
  202. package/src/sdk/broker/master-capability.ts +232 -0
  203. package/src/sdk/broker/session-index.ts +121 -17
  204. package/src/sdk/broker/session-scope.ts +272 -0
  205. package/src/sdk/broker/spawn-authority.ts +1031 -0
  206. package/src/sdk/broker/spawn-substrate.ts +548 -0
  207. package/src/sdk/broker/state-version.ts +26 -14
  208. package/src/sdk/broker/transport.ts +1 -0
  209. package/src/sdk/broker/worktree-occupancy.ts +90 -0
  210. package/src/sdk/bus/chat-daemon-control.ts +5 -2
  211. package/src/sdk/bus/chat-daemon-runtime.ts +46 -5
  212. package/src/sdk/bus/index.ts +51 -4
  213. package/src/sdk/bus/kind-aware-reconciliation.ts +36 -6
  214. package/src/sdk/bus/prompt-reconciliation.ts +28 -4
  215. package/src/sdk/bus/slack-conversation.ts +5 -1
  216. package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
  217. package/src/sdk/cli/index.ts +1 -0
  218. package/src/sdk/cli/master-cli.ts +259 -0
  219. package/src/sdk/cli/rows.ts +11 -8
  220. package/src/sdk/cli/session-cli.ts +276 -6
  221. package/src/sdk/client/client.ts +14 -8
  222. package/src/sdk/client/discovery.ts +3 -1
  223. package/src/sdk/host/control/dispatch.ts +101 -37
  224. package/src/sdk/host/control/index.ts +2 -0
  225. package/src/sdk/host/control/operations.ts +2 -0
  226. package/src/sdk/host/control/runtime-gate.ts +203 -11
  227. package/src/sdk/host/host.ts +27 -2
  228. package/src/sdk/host/model-pin.ts +185 -25
  229. package/src/sdk/host/sdk-run-capability.ts +1 -0
  230. package/src/sdk/host/session-runtime.test.ts +581 -43
  231. package/src/sdk/host/session-runtime.ts +655 -101
  232. package/src/sdk/lifecycle/broker-client.ts +28 -0
  233. package/src/sdk/lifecycle/service.ts +279 -20
  234. package/src/sdk/lifecycle-session.ts +10 -2
  235. package/src/sdk/prompt-deadline-manager.ts +16 -1
  236. package/src/sdk/protocol/adapter-validation.ts +27 -0
  237. package/src/sdk/protocol/operation-inventory.generated.json +53 -2
  238. package/src/sdk/protocol/operation-registry.ts +17 -4
  239. package/src/sdk/router/session-router.ts +294 -154
  240. package/src/sdk/session.ts +440 -167
  241. package/src/sdk/startup-capability.ts +1 -0
  242. package/src/session/account-inventory.ts +27 -11
  243. package/src/session/agent-session.ts +1292 -180
  244. package/src/session/blob-store.ts +15 -5
  245. package/src/session/credential-store-errors.ts +2 -0
  246. package/src/session/fold-coordinator.ts +433 -0
  247. package/src/session/internal/managed-session-storage.ts +153 -78
  248. package/src/session/sdk-run-capability-internal.ts +15 -0
  249. package/src/session/sdk-run-capability.ts +2 -0
  250. package/src/session/session-manager.ts +342 -28
  251. package/src/session/yield-queue.ts +24 -10
  252. package/src/setup/frictionless-onboarding.ts +2 -0
  253. package/src/setup/provider-presets.json +1 -1
  254. package/src/slash-commands/helpers/credential-switch.ts +6 -1
  255. package/src/task/executor.ts +19 -3
  256. package/src/telemetry/control.ts +26 -0
  257. package/src/telemetry/events.ts +1268 -0
  258. package/src/telemetry/index.ts +3 -0
  259. package/src/telemetry/transport.ts +87 -0
  260. package/src/tools/bash-interactive.ts +182 -85
  261. package/src/tools/bash.ts +1028 -109
  262. package/src/tools/gh.ts +104 -48
  263. package/src/tools/github-cache.ts +2 -2
  264. package/src/tools/index.ts +11 -5
  265. package/src/tools/tool-catalog.generated.ts +1 -1
  266. package/src/web/search/provider.ts +120 -11
  267. package/src/web/search/providers/anthropic.ts +89 -17
  268. package/src/web/search/providers/base.ts +9 -4
  269. package/src/web/search/providers/gemini.ts +87 -15
  270. package/src/web/search/providers/openai-compatible.ts +23 -8
  271. package/src/web/search/providers/xai.ts +98 -12
  272. package/src/web/search/types.ts +16 -0
@@ -0,0 +1,86 @@
1
+ import * as fsSync from "node:fs";
2
+ import * as fs from "node:fs/promises";
3
+
4
+ const MAX_ENDPOINT_BYTES = 4_096n;
5
+
6
+ export type EndpointFileRead = {
7
+ source: string;
8
+ dev: bigint;
9
+ ino: bigint;
10
+ nlink: bigint;
11
+ size: bigint;
12
+ mtimeNs: bigint;
13
+ mtimeMs: number;
14
+ };
15
+
16
+ export type EndpointAuthorityFilesystem = Pick<typeof fs, "open">;
17
+
18
+ export type IndexedEndpointAuthority = {
19
+ endpointMtimeMs?: number;
20
+ endpointFileId?: string;
21
+ };
22
+
23
+ function readFlags(): number {
24
+ return fsSync.constants.O_RDONLY | (fsSync.constants.O_NOFOLLOW ?? 0) | (fsSync.constants.O_NONBLOCK ?? 0);
25
+ }
26
+
27
+ /**
28
+ * Reads one endpoint from the descriptor opened for its pathname. The opened
29
+ * object must remain a small, singly-linked regular file with stable identity
30
+ * and metadata for the whole read; a pathname replacement cannot affect the
31
+ * bytes returned by this function.
32
+ */
33
+ export async function readEndpointFile(
34
+ filePath: string,
35
+ filesystem: EndpointAuthorityFilesystem = fs,
36
+ ): Promise<EndpointFileRead | undefined> {
37
+ let handle: fs.FileHandle | undefined;
38
+ try {
39
+ handle = await filesystem.open(filePath, readFlags());
40
+ const before = await handle.stat({ bigint: true });
41
+ if (!before.isFile() || before.nlink !== 1n || before.size > MAX_ENDPOINT_BYTES) return undefined;
42
+ if (before.size > BigInt(Number.MAX_SAFE_INTEGER)) return undefined;
43
+ const bytes = Buffer.alloc(Number(before.size));
44
+ let offset = 0;
45
+ while (offset < bytes.length) {
46
+ const { bytesRead } = await handle.read(bytes, offset, bytes.length - offset, offset);
47
+ if (bytesRead <= 0) return undefined;
48
+ offset += bytesRead;
49
+ }
50
+ const after = await handle.stat({ bigint: true });
51
+ if (
52
+ before.dev !== after.dev ||
53
+ before.ino !== after.ino ||
54
+ before.nlink !== after.nlink ||
55
+ before.size !== after.size ||
56
+ before.mtimeNs !== after.mtimeNs
57
+ )
58
+ return undefined;
59
+ return {
60
+ source: bytes.toString("utf8"),
61
+ dev: before.dev,
62
+ ino: before.ino,
63
+ nlink: before.nlink,
64
+ size: before.size,
65
+ mtimeNs: before.mtimeNs,
66
+ mtimeMs: Number(before.mtimeNs) / 1_000_000,
67
+ };
68
+ } catch {
69
+ return undefined;
70
+ } finally {
71
+ await handle?.close().catch(() => undefined);
72
+ }
73
+ }
74
+
75
+ /** Match a descriptor-bound endpoint read to the index representation. */
76
+ export function matchesIndexedEndpointFile(
77
+ file: Pick<EndpointFileRead, "dev" | "ino" | "mtimeMs">,
78
+ authority: IndexedEndpointAuthority,
79
+ ): boolean {
80
+ return (
81
+ authority.endpointMtimeMs !== undefined &&
82
+ Number.isFinite(authority.endpointMtimeMs) &&
83
+ Math.abs(file.mtimeMs - authority.endpointMtimeMs) <= 0.001 &&
84
+ (authority.endpointFileId === undefined || authority.endpointFileId === `${file.dev}:${file.ino}`)
85
+ );
86
+ }
@@ -232,6 +232,11 @@ function registerBrokerOwner(
232
232
  function brokerSpawnEnvironment(command: SdkInternalSpawnCommand, override?: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
233
233
  const environment = { ...(override ?? command.env) };
234
234
  delete environment.BUN_OPTIONS;
235
+ // The master capability is a transient in-memory dispatch input. A broker
236
+ // cold-started from the master's own Bash environment would otherwise inherit
237
+ // it and pass it on to every substrate child it later launches, so it is
238
+ // stripped at the lifecycle boundary exactly as lifecycle children strip it.
239
+ delete environment.GJC_MASTER_CAPABILITY;
235
240
  if (command.kind === "bun-source") {
236
241
  delete environment.PI_COMPILED;
237
242
  delete environment.GJC_COMPILED;
@@ -4,6 +4,9 @@ export * from "./ensure";
4
4
  export * from "./identity";
5
5
  export * from "./lifecycle";
6
6
  export * from "./lifecycle-ledger";
7
+ export * from "./master-capability";
7
8
  export * from "./session-index";
9
+ export * from "./spawn-authority";
10
+ export * from "./spawn-substrate";
8
11
  export * from "./startup-budget";
9
12
  export * from "./transport";
@@ -14,7 +14,10 @@ function nativeLifecycle(): typeof import("@gajae-code/natives") {
14
14
  }
15
15
 
16
16
  import { $credentialEnv, logger, resolveEquivalentPath } from "@gajae-code/utils";
17
-
17
+ import {
18
+ loadAcceptedModelPresetRegistry,
19
+ loadAcceptedModelPresetRegistryAsync,
20
+ } from "../../config/model-preset-registry";
18
21
  import {
19
22
  isModelProfileError,
20
23
  type ModelProfileErrorDetails,
@@ -63,6 +66,7 @@ import {
63
66
  sanitizeSdkStartupMessage,
64
67
  } from "../startup-capability";
65
68
  import type { Broker, BrokerCleanupEvidence, BrokerCleanupIdentity, BrokerResponse } from "./broker";
69
+ import { matchesIndexedEndpointFile, readEndpointFile } from "./endpoint-authority";
66
70
  import { decodeLifecycleUtf8, parseLifecycleJson } from "./lifecycle-codec";
67
71
  import type {
68
72
  LifecycleCleanupProof,
@@ -78,7 +82,12 @@ import {
78
82
  processIncarnation,
79
83
  } from "./process-incarnation";
80
84
  import { resolveSdkInternalSpawnCommand, type SdkInternalSpawnCommand } from "./runtime";
81
- import { type IndexedSession, isSessionAuthorityEligible } from "./session-index";
85
+ import {
86
+ type IndexedSession,
87
+ isSessionAuthorityEligible,
88
+ resolveSessionLocator,
89
+ type SessionLocatorV2,
90
+ } from "./session-index";
82
91
  import {
83
92
  cancellableSleep,
84
93
  DEFAULT_READINESS_TIMEOUT_MS,
@@ -86,6 +95,7 @@ import {
86
95
  READINESS_TIMEOUT_INVALID_MESSAGE,
87
96
  startupQueueWaitMs,
88
97
  } from "./startup-budget";
98
+ import { worktreeOccupant } from "./worktree-occupancy";
89
99
 
90
100
  export {
91
101
  type ProcessIncarnationCommandRunner,
@@ -609,13 +619,36 @@ function text(value: unknown): string | undefined {
609
619
  return typeof value === "string" && value ? value : undefined;
610
620
  }
611
621
 
612
- function validateBrokerModelPreset(agentDir: string, requestedProfile: string): string | BrokerResponse {
622
+ export function validateBrokerModelPresetSync(agentDir: string, requestedProfile: string): string | BrokerResponse {
613
623
  const modelsConfigFile = ModelsConfigFile.relocate(path.join(agentDir, "models.yml"));
614
624
  modelsConfigFile.invalidate();
615
625
  const loaded = modelsConfigFile.tryLoad();
616
- const profiles = mergeModelProfiles(loaded.status === "ok" ? loaded.value.profiles : undefined);
626
+ const accepted = loadAcceptedModelPresetRegistry(agentDir);
627
+ const profiles = mergeModelProfiles(loaded.status === "ok" ? loaded.value.profiles : undefined, accepted.profiles);
617
628
  try {
618
- return validateModelProfileName(requestedProfile, profiles, loaded.status === "error" ? loaded.error : undefined);
629
+ return validateModelProfileName(
630
+ requestedProfile,
631
+ profiles,
632
+ accepted.error ?? (loaded.status === "error" ? loaded.error : undefined),
633
+ );
634
+ } catch (error) {
635
+ if (isModelProfileError(error)) return fail(error.code, error.message, undefined, error.details);
636
+ throw error;
637
+ }
638
+ }
639
+
640
+ async function validateBrokerModelPreset(agentDir: string, requestedProfile: string): Promise<string | BrokerResponse> {
641
+ const modelsConfigFile = ModelsConfigFile.relocate(path.join(agentDir, "models.yml"));
642
+ modelsConfigFile.invalidate();
643
+ const loaded = modelsConfigFile.tryLoad();
644
+ const accepted = await loadAcceptedModelPresetRegistryAsync(agentDir);
645
+ const profiles = mergeModelProfiles(loaded.status === "ok" ? loaded.value.profiles : undefined, accepted.profiles);
646
+ try {
647
+ return validateModelProfileName(
648
+ requestedProfile,
649
+ profiles,
650
+ accepted.error ?? (loaded.status === "error" ? loaded.error : undefined),
651
+ );
619
652
  } catch (error) {
620
653
  if (isModelProfileError(error)) return fail(error.code, error.message, undefined, error.details);
621
654
  throw error;
@@ -623,7 +656,7 @@ function validateBrokerModelPreset(agentDir: string, requestedProfile: string):
623
656
  }
624
657
 
625
658
  export function validateBrokerModelPresetForTest(agentDir: string, requestedProfile: string): string | BrokerResponse {
626
- return validateBrokerModelPreset(agentDir, requestedProfile);
659
+ return validateBrokerModelPresetSync(agentDir, requestedProfile);
627
660
  }
628
661
 
629
662
  function readinessTimeout(input: Input): number | BrokerResponse {
@@ -707,10 +740,13 @@ function lifecycleWorktreeTarget(input: Input): SessionLifecycleWorktreeTarget |
707
740
  }
708
741
 
709
742
  type LiveResumeRecord = {
710
- locator: { repo: string; stateRoot: string };
743
+ sessionId: string;
744
+ locator: SessionLocatorV2;
711
745
  endpointGeneration: number;
712
746
  pid: number;
713
747
  endpointMtimeMs?: number;
748
+ processIncarnation?: string;
749
+ hostIncarnation?: string;
714
750
  live: boolean;
715
751
  ambiguous: boolean;
716
752
  };
@@ -729,7 +765,7 @@ type ResumeScope = {
729
765
  };
730
766
  function sameResumeLocator(record: LiveResumeRecord, cwd: string, root: string): boolean {
731
767
  return (
732
- resolveEquivalentPath(record.locator.repo) === resolveEquivalentPath(cwd) &&
768
+ resolveEquivalentPath(record.locator.cwd) === resolveEquivalentPath(cwd) &&
733
769
  resolveEquivalentPath(record.locator.stateRoot) === resolveEquivalentPath(root)
734
770
  );
735
771
  }
@@ -751,10 +787,24 @@ function sameLiveResumeRecord(expected: LiveResumeRecord, current: LiveResumeRec
751
787
  current.endpointGeneration === expected.endpointGeneration &&
752
788
  current.pid === expected.pid &&
753
789
  current.endpointMtimeMs === expected.endpointMtimeMs &&
754
- sameResumeLocator(current, expected.locator.repo, expected.locator.stateRoot)
790
+ (current.hostIncarnation ?? current.processIncarnation) ===
791
+ (expected.hostIncarnation ?? expected.processIncarnation) &&
792
+ sameResumeLocator(current, expected.locator.cwd, expected.locator.stateRoot)
755
793
  );
756
794
  }
757
795
 
796
+ function liveResumeAuthority(
797
+ sessions: readonly LiveResumeRecord[],
798
+ sessionId: string,
799
+ ): { kind: "none" } | { kind: "ambiguous" } | { kind: "live"; record: LiveResumeRecord } {
800
+ const indexedCandidates = sessions.filter(session => session.sessionId === sessionId);
801
+ if (indexedCandidates.some(session => !isSessionAuthorityEligible(session))) return { kind: "ambiguous" };
802
+ const liveCandidates = indexedCandidates.filter(session => session.live);
803
+ if (liveCandidates.length > 1) return { kind: "ambiguous" };
804
+ const record = liveCandidates[0];
805
+ return record ? { kind: "live", record } : { kind: "none" };
806
+ }
807
+
758
808
  type ValidatedTranscript = {
759
809
  path: string;
760
810
  id: string;
@@ -886,18 +936,36 @@ async function validateLiveResumeScope(
886
936
  sessionIdentity: session.identity,
887
937
  };
888
938
  }
889
- async function reconcileReadyScope(broker: Broker, id: string, scope: string | undefined): Promise<void> {
939
+ async function reconcileReadyScope(
940
+ broker: Broker,
941
+ id: string,
942
+ scope: string | undefined,
943
+ root: string,
944
+ expected: EffectMarker,
945
+ ): Promise<void> {
890
946
  if (!scope) return;
891
947
  await broker.index.refresh();
892
- const record = broker.index.listSessions().sessions.find(session => session.sessionId === id);
893
- if (!record || record.locator.repo === scope) return;
894
- // The host records its physical cwd, which Darwin canonicalizes from /var to
895
- // /private/var. Preserve the lifecycle caller's lexical cwd for ACP's scoped
896
- // listing while retaining the host-provided state root for endpoint binding.
948
+ const record = broker.index
949
+ .listSessions()
950
+ .sessions.find(
951
+ session =>
952
+ session.sessionId === id &&
953
+ resolveEquivalentPath(session.locator.stateRoot) === resolveEquivalentPath(root) &&
954
+ session.pid === expected.pid &&
955
+ (session.hostIncarnation ?? session.processIncarnation) === expected.incarnation &&
956
+ session.lifecycleRequestId === expected.effectMarker,
957
+ );
958
+ if (!record) return;
959
+ const cwd = canonicalExistingPath(scope);
960
+ if (record.locator.cwd === cwd) return;
961
+ // Locator cwd is canonical everywhere. Reconcile only a pre-existing host row
962
+ // whose canonical registration race observed a different path; it never retains
963
+ // a caller's lexical spelling for ACP or any other scope consumer.
964
+ const locator = await resolveSessionLocator(scope, record.locator.stateRoot);
897
965
  await broker.index.append({
898
966
  type: "record_reconciled",
899
967
  sessionId: id,
900
- locator: { ...record.locator, repo: scope },
968
+ locator,
901
969
  endpointGeneration: record.endpointGeneration,
902
970
  pid: record.pid,
903
971
  // Reconciliation only re-scopes the locator, so every identity fact the host
@@ -907,6 +975,8 @@ async function reconcileReadyScope(broker: Broker, id: string, scope: string | u
907
975
  ...(record.hostIncarnation === undefined ? {} : { hostIncarnation: record.hostIncarnation }),
908
976
  ...(record.lifecycleRequestId === undefined ? {} : { lifecycleRequestId: record.lifecycleRequestId }),
909
977
  endpointMtimeMs: record.endpointMtimeMs,
978
+ ...(record.endpointFileId === undefined ? {} : { endpointFileId: record.endpointFileId }),
979
+ ...(record.masterRole === undefined ? {} : { masterRole: record.masterRole }),
910
980
  });
911
981
  }
912
982
 
@@ -938,6 +1008,66 @@ function command(broker: Broker): LifecycleCommand {
938
1008
  return lifecycleCommandResolversForTest.get(broker)?.() ?? resolveSdkInternalSpawnCommand("session-host-internal");
939
1009
  }
940
1010
 
1011
+ /**
1012
+ * Reuses the ordinary lifecycle host bootstrap without performing its direct
1013
+ * process spawn. Broker-owned substrate providers receive this exact argv and
1014
+ * split inherited/child-specific environment contract as their only launch
1015
+ * authority for session.spawn.
1016
+ */
1017
+ export type SpawnChildHostLaunch = {
1018
+ childId: string;
1019
+ cwd: string;
1020
+ stateRoot: string;
1021
+ argv: readonly string[];
1022
+ inheritedEnv: Readonly<Record<string, string>>;
1023
+ env: Readonly<Record<string, string>>;
1024
+ effectMarker: string;
1025
+ };
1026
+
1027
+ export function prepareSpawnChildHostLaunch(
1028
+ broker: Broker,
1029
+ input: { cwd: string; modelId?: string; modelPreset?: string; childId?: string; receivedAt?: number },
1030
+ ): SpawnChildHostLaunch {
1031
+ const cwd = path.resolve(input.cwd);
1032
+ const childId = input.childId ?? randomUUID();
1033
+ const stateRoot = defaultStateRoot(cwd);
1034
+ const effectMarker = randomUUID();
1035
+ const deadlines = deriveLifecycleDeadlines(input.receivedAt ?? Date.now(), DEFAULT_READINESS_TIMEOUT_MS);
1036
+ const request: SessionLifecycleLaunchRequest = {
1037
+ operation: "session.create",
1038
+ sessionId: childId,
1039
+ cwd,
1040
+ stateRoot,
1041
+ effectMarker,
1042
+ ...deadlines,
1043
+ ...(input.modelId === undefined ? {} : { modelId: input.modelId }),
1044
+ ...(input.modelPreset === undefined ? {} : { modelPreset: input.modelPreset }),
1045
+ };
1046
+ const cmd = command(broker);
1047
+ const inherited = "kind" in cmd ? cmd.env : process.env;
1048
+ const inheritedEnv = Object.fromEntries(
1049
+ Object.entries(inherited).filter(
1050
+ (entry): entry is [string, string] => entry[0] !== "GJC_MASTER_CAPABILITY" && typeof entry[1] === "string",
1051
+ ),
1052
+ );
1053
+ return {
1054
+ childId,
1055
+ cwd,
1056
+ stateRoot,
1057
+ argv: [cmd.file, ...cmd.args],
1058
+ inheritedEnv,
1059
+ env: {
1060
+ GJC_AGENT_DIR: broker.settings.agentDir,
1061
+ GJC_CODING_AGENT_DIR: broker.settings.agentDir,
1062
+ GJC_SESSION_ID: childId,
1063
+ GJC_STATE_ROOT: stateRoot,
1064
+ GJC_LIFECYCLE_REQUEST_ID: effectMarker,
1065
+ GJC_SDK_LIFECYCLE_REQUEST: JSON.stringify(request),
1066
+ },
1067
+ effectMarker,
1068
+ };
1069
+ }
1070
+
941
1071
  const lifecycleMarkerPath = (root: string, id: string) => path.join(root, "sdk", `${id}.lifecycle.json`);
942
1072
  const lifecycleReadyPath = (root: string, id: string) => path.join(root, "sdk", `${id}.lifecycle.ready.json`);
943
1073
  const lifecycleFailurePath = (root: string, id: string, effectMarker: string) =>
@@ -947,6 +1077,7 @@ type ReadyAuthority = {
947
1077
  endpoint: Record<string, unknown>;
948
1078
  endpointSource: string;
949
1079
  endpointMtimeMs: number;
1080
+ endpointFileId?: string;
950
1081
  endpointGeneration: number;
951
1082
  };
952
1083
  type ReadinessResult =
@@ -3325,7 +3456,7 @@ async function recordTerminalUncertain(
3325
3456
  await broker.index.append({
3326
3457
  type: "lifecycle_terminal",
3327
3458
  sessionId: id,
3328
- locator: { repo: "unknown", stateRoot: root },
3459
+ locator: { cwd: "unknown", worktreeRoot: null, stateRoot: root },
3329
3460
  endpointGeneration: 0,
3330
3461
  pid,
3331
3462
  terminalUncertain: true,
@@ -3641,7 +3772,10 @@ async function removeExactDeadSessionEndpoint(
3641
3772
  : finalEndpointPath;
3642
3773
  let handle: fs.FileHandle | undefined;
3643
3774
  try {
3644
- handle = await fs.open(endpointSource, fsSync.constants.O_RDONLY | fsSync.constants.O_NOFOLLOW);
3775
+ handle = await fs.open(
3776
+ endpointSource,
3777
+ fsSync.constants.O_RDONLY | (fsSync.constants.O_NOFOLLOW ?? 0) | (fsSync.constants.O_NONBLOCK ?? 0),
3778
+ );
3645
3779
  const metadata = await handle.stat({ bigint: true });
3646
3780
  if (!metadata.isFile() || metadata.nlink !== 1n || metadata.size > 4096n) return false;
3647
3781
  const bytes = Buffer.alloc(Number(metadata.size) + 1);
@@ -3821,10 +3955,9 @@ async function currentReadyAuthority(
3821
3955
  if (!(await hasOwnedReadinessEvidence(broker, root, id, expected))) return undefined;
3822
3956
  const endpointPath = path.join(root, "sdk", `${id}.json`);
3823
3957
  try {
3824
- const [endpointSource, endpointMetadata] = await Promise.all([
3825
- fs.readFile(endpointPath, "utf8"),
3826
- fs.stat(endpointPath),
3827
- ]);
3958
+ const endpointFile = await readEndpointFile(endpointPath);
3959
+ if (!endpointFile) return undefined;
3960
+ const endpointSource = endpointFile.source;
3828
3961
  const endpoint = JSON.parse(endpointSource) as {
3829
3962
  sessionId?: unknown;
3830
3963
  url?: unknown;
@@ -3837,14 +3970,24 @@ async function currentReadyAuthority(
3837
3970
  // native-dead child; do not refuse a native-alive ready host for a stale live bit.
3838
3971
  await broker.heartbeatSessions();
3839
3972
  await broker.index.refresh();
3840
- const record = broker.index.listSessions().sessions.find(session => session.sessionId === id);
3973
+ const record = broker.index
3974
+ .listSessions()
3975
+ .sessions.find(
3976
+ session =>
3977
+ session.sessionId === id &&
3978
+ session.pid === expected.pid &&
3979
+ resolveEquivalentPath(session.locator.stateRoot) === resolveEquivalentPath(root) &&
3980
+ (session.hostIncarnation ?? session.processIncarnation) === expected.incarnation &&
3981
+ session.lifecycleRequestId === expected.effectMarker,
3982
+ );
3841
3983
  if (
3842
3984
  !record ||
3843
3985
  record.terminal ||
3844
3986
  record.terminalUncertain ||
3845
3987
  record.pid !== expected.pid ||
3846
3988
  resolveEquivalentPath(record.locator.stateRoot) !== resolveEquivalentPath(root) ||
3847
- record.endpointMtimeMs !== endpointMetadata.mtimeMs ||
3989
+ (record.hostIncarnation ?? record.processIncarnation) !== expected.incarnation ||
3990
+ !matchesIndexedEndpointFile(endpointFile, record) ||
3848
3991
  endpoint.pid !== expected.pid ||
3849
3992
  endpoint.sessionId !== id ||
3850
3993
  typeof endpoint.url !== "string" ||
@@ -3859,7 +4002,8 @@ async function currentReadyAuthority(
3859
4002
  return {
3860
4003
  endpoint: endpoint as Record<string, unknown>,
3861
4004
  endpointSource,
3862
- endpointMtimeMs: endpointMetadata.mtimeMs,
4005
+ endpointMtimeMs: endpointFile.mtimeMs,
4006
+ ...(record.endpointFileId === undefined ? {} : { endpointFileId: record.endpointFileId }),
3863
4007
  endpointGeneration: record.endpointGeneration,
3864
4008
  };
3865
4009
  } catch {
@@ -3870,7 +4014,8 @@ async function currentReadyAuthority(
3870
4014
  function sameReadyAuthority(left: ReadyAuthority, right: ReadyAuthority): boolean {
3871
4015
  return (
3872
4016
  left.endpointSource === right.endpointSource &&
3873
- left.endpointMtimeMs === right.endpointMtimeMs &&
4017
+ Math.abs(left.endpointMtimeMs - right.endpointMtimeMs) <= 0.001 &&
4018
+ left.endpointFileId === right.endpointFileId &&
3874
4019
  left.endpointGeneration === right.endpointGeneration
3875
4020
  );
3876
4021
  }
@@ -3996,6 +4141,15 @@ function worktreeIntent(plan: GjcLaunchWorktreePlan | undefined): LifecycleWorkt
3996
4141
  };
3997
4142
  }
3998
4143
 
4144
+ /** Test seam for the worktree occupancy boundary. */
4145
+ export function worktreeOccupantForTest(
4146
+ sessions: IndexedSession[],
4147
+ worktreePath: string,
4148
+ observe: (pid: number, expectedIncarnation: string | undefined) => ProcessObservation = observeProcess,
4149
+ ): string | null {
4150
+ return worktreeOccupant(sessions, worktreePath, observe);
4151
+ }
4152
+
3999
4153
  function preparePlannedWorktree(plan: GjcLaunchWorktreePlan): SessionLifecycleWorktreeReceipt {
4000
4154
  const prepared = ensureLaunchWorktree(plan);
4001
4155
  if (!prepared.enabled || path.resolve(prepared.worktreePath) !== path.resolve(plan.worktreePath))
@@ -4030,7 +4184,7 @@ async function launchInput(
4030
4184
  if (input.modelPreset !== undefined && (typeof input.modelPreset !== "string" || input.modelPreset.length === 0))
4031
4185
  return fail("invalid_input", "modelPreset must be a non-empty exact profile ID.");
4032
4186
  if (modelPreset !== undefined) {
4033
- const validatedModelPreset = validateBrokerModelPreset(broker.settings.agentDir, modelPreset);
4187
+ const validatedModelPreset = await validateBrokerModelPreset(broker.settings.agentDir, modelPreset);
4034
4188
  if (typeof validatedModelPreset !== "string") return validatedModelPreset;
4035
4189
  modelPreset = validatedModelPreset;
4036
4190
  }
@@ -4427,7 +4581,7 @@ async function validateDeletePath(
4427
4581
  broker: Broker,
4428
4582
  input: Input,
4429
4583
  id: string,
4430
- record: { locator: { repo: string; stateRoot: string } } | undefined,
4584
+ record: { locator: SessionLocatorV2 } | undefined,
4431
4585
  cleanup?: CleanupEvidence,
4432
4586
  ): Promise<ValidatedDelete | BrokerResponse> {
4433
4587
  const sessionPath = text(input.sessionPath);
@@ -4441,7 +4595,7 @@ async function validateDeletePath(
4441
4595
  const canonicalRequestedRoot = canonicalExistingPath(requestedRoot);
4442
4596
  if (
4443
4597
  record &&
4444
- (canonicalExistingPath(record.locator.repo) !== cwd ||
4598
+ (canonicalExistingPath(record.locator.cwd) !== cwd ||
4445
4599
  canonicalExistingPath(record.locator.stateRoot) !== canonicalRequestedRoot)
4446
4600
  )
4447
4601
  return fail("invalid_input", "session.delete locator does not match the indexed session.");
@@ -4531,7 +4685,7 @@ async function validateDeletePath(
4531
4685
  }
4532
4686
  type CloseAuthority = { endpointGeneration: number; endpointIncarnation: string };
4533
4687
  type CloseRecord = {
4534
- locator: { repo: string; stateRoot: string };
4688
+ locator: SessionLocatorV2;
4535
4689
  endpointGeneration: number;
4536
4690
  pid: number;
4537
4691
  endpointMtimeMs?: number;
@@ -4595,7 +4749,7 @@ function sameCloseStoredProcessIdentity(expected: CloseRecord, current: CloseRec
4595
4749
  typeof expected.lifecycleRequestId === "string" &&
4596
4750
  expected.lifecycleRequestId.length > 0 &&
4597
4751
  current.lifecycleRequestId === expected.lifecycleRequestId &&
4598
- path.resolve(current.locator.repo) === path.resolve(expected.locator.repo) &&
4752
+ path.resolve(current.locator.cwd) === path.resolve(expected.locator.cwd) &&
4599
4753
  path.resolve(current.locator.stateRoot) === path.resolve(expected.locator.stateRoot)
4600
4754
  );
4601
4755
  }
@@ -4613,7 +4767,7 @@ function sameCloseEndpointIdentity(expected: CloseRecord, current: CloseRecord):
4613
4767
  current.endpointMtimeMs !== undefined &&
4614
4768
  expected.endpointMtimeMs !== undefined &&
4615
4769
  current.lifecycleRequestId === expected.lifecycleRequestId &&
4616
- path.resolve(current.locator.repo) === path.resolve(expected.locator.repo) &&
4770
+ path.resolve(current.locator.cwd) === path.resolve(expected.locator.cwd) &&
4617
4771
  path.resolve(current.locator.stateRoot) === path.resolve(expected.locator.stateRoot)
4618
4772
  );
4619
4773
  }
@@ -4630,7 +4784,7 @@ function sameCloseGeneration(expected: CloseRecord, current: CloseRecord & { liv
4630
4784
  current.endpointMtimeMs === expected.endpointMtimeMs &&
4631
4785
  current.lifecycleRequestId === expected.lifecycleRequestId &&
4632
4786
  current.processIncarnation === expected.processIncarnation &&
4633
- path.resolve(current.locator.repo) === path.resolve(expected.locator.repo) &&
4787
+ path.resolve(current.locator.cwd) === path.resolve(expected.locator.cwd) &&
4634
4788
  path.resolve(current.locator.stateRoot) === path.resolve(expected.locator.stateRoot)
4635
4789
  );
4636
4790
  }
@@ -4694,13 +4848,15 @@ async function executeLifecycleResponse(
4694
4848
  if (operation === "session.create" || operation === "session.fork" || operation === "session.resume") {
4695
4849
  await broker.index.refresh();
4696
4850
  await broker.heartbeatSessions();
4851
+ await broker.index.refresh();
4697
4852
  if (operation === "session.resume") {
4698
4853
  const requestedSessionId = sessionId(input);
4699
- const existing = requestedSessionId
4700
- ? broker.index.listSessions().sessions.find(session => session.sessionId === requestedSessionId)
4701
- : undefined;
4702
- if (existing && !isSessionAuthorityEligible(existing))
4854
+ const authority = requestedSessionId
4855
+ ? liveResumeAuthority(broker.index.listSessions().sessions, requestedSessionId)
4856
+ : { kind: "none" as const };
4857
+ if (authority.kind === "ambiguous")
4703
4858
  return fail("endpoint_stale", "Session authority is ambiguous and cannot be resumed safely.");
4859
+ const existing = authority.kind === "live" ? authority.record : undefined;
4704
4860
  if (existing?.live) {
4705
4861
  const initialScope = await validateLiveResumeScope(broker, input, requestedSessionId!, existing);
4706
4862
  if ("ok" in initialScope) return initialScope;
@@ -4717,16 +4873,17 @@ async function executeLifecycleResponse(
4717
4873
  "live_session",
4718
4874
  "Session is already live but its incarnation-bound endpoint is unavailable.",
4719
4875
  );
4720
- await broker.index.refresh();
4721
- const current = broker.index
4722
- .listSessions()
4723
- .sessions.find(session => session.sessionId === requestedSessionId);
4724
- if (!current || !sameLiveResumeRecord(existing, current))
4725
- return fail("endpoint_stale", "Live session changed while its resume authority was being verified.");
4726
- const finalScope = await validateLiveResumeScope(broker, input, requestedSessionId!, current);
4876
+ const finalScope = await validateLiveResumeScope(broker, input, requestedSessionId!, existing);
4727
4877
  if ("ok" in finalScope) return finalScope;
4728
4878
  if (!sameResumeSessionIdentity(initialScope, finalScope))
4729
4879
  return fail("endpoint_stale", "Saved session changed while its resume authority was being verified.");
4880
+ await broker.index.refresh();
4881
+ const finalAuthority = liveResumeAuthority(broker.index.listSessions().sessions, requestedSessionId!);
4882
+ if (finalAuthority.kind === "ambiguous")
4883
+ return fail("endpoint_stale", "Session authority became ambiguous while it was being verified.");
4884
+ const current = finalAuthority.kind === "live" ? finalAuthority.record : undefined;
4885
+ if (!current || !sameLiveResumeRecord(existing, current))
4886
+ return fail("endpoint_stale", "Live session changed while its resume authority was being verified.");
4730
4887
  if (current.endpointMtimeMs === undefined)
4731
4888
  return fail("endpoint_stale", "Live session endpoint authority is incomplete.");
4732
4889
  return {
@@ -4801,6 +4958,14 @@ async function executeLifecycleResponse(
4801
4958
  "incarnation_unavailable",
4802
4959
  "OS process incarnation authority is unavailable; refusing to spawn a lifecycle session.",
4803
4960
  );
4961
+ if (launch.worktreePlan) {
4962
+ const occupant = worktreeOccupant(broker.index.listSessions().sessions, launch.worktreePlan.worktreePath);
4963
+ if (occupant && occupant !== launch.id)
4964
+ return fail(
4965
+ "worktree_in_use",
4966
+ `The requested worktree is already held by session ${occupant}. Choose another worktree name or stop that session.`,
4967
+ );
4968
+ }
4804
4969
  const effectMarker = randomUUID();
4805
4970
  const plannedWorktreeIntent = worktreeIntent(launch.worktreePlan);
4806
4971
  const effectIntent: LifecycleEffectIntentWithDeadline = {
@@ -4907,7 +5072,13 @@ async function executeLifecycleResponse(
4907
5072
  // by the OS rather than captured (#4712 review).
4908
5073
  stdio: "ignore",
4909
5074
  env: {
4910
- ...("kind" in cmd ? cmd.env : process.env),
5075
+ // Master capability is process-local to direct Bash children and must
5076
+ // never cross a broker lifecycle launch boundary.
5077
+ ...Object.fromEntries(
5078
+ Object.entries("kind" in cmd ? cmd.env : process.env).filter(
5079
+ ([key]) => key !== "GJC_MASTER_CAPABILITY",
5080
+ ),
5081
+ ),
4911
5082
  GJC_AGENT_DIR: broker.settings.agentDir,
4912
5083
  GJC_CODING_AGENT_DIR: broker.settings.agentDir,
4913
5084
  GJC_SESSION_ID: launch.id,
@@ -5047,7 +5218,7 @@ async function executeLifecycleResponse(
5047
5218
  `Session ${launch.id} did not register an endpoint before the readiness timeout.`,
5048
5219
  );
5049
5220
  }
5050
- await reconcileReadyScope(broker, launch.id, launch.cwd);
5221
+ await reconcileReadyScope(broker, launch.id, launch.cwd, launch.root, spawnedAuthority);
5051
5222
  const verified = await currentReadyAuthority(broker, launch.id, launch.root, spawnedAuthority);
5052
5223
  if (!verified || !sameReadyAuthority(readiness.authority, verified)) {
5053
5224
  const exited =