@gajae-code/coding-agent 0.15.6 → 0.16.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 (270) hide show
  1. package/CHANGELOG.md +99 -0
  2. package/dist/types/async/job-manager.d.ts +32 -1
  3. package/dist/types/browser-backend.d.ts +12 -0
  4. package/dist/types/cli/args.d.ts +2 -0
  5. package/dist/types/cli/root-flags.d.ts +14 -0
  6. package/dist/types/cli/update-cli.d.ts +14 -0
  7. package/dist/types/cli.d.ts +7 -0
  8. package/dist/types/commands/launch.d.ts +10 -0
  9. package/dist/types/commands/sdk.d.ts +2 -0
  10. package/dist/types/config/autorouting-tier-map.d.ts +24 -0
  11. package/dist/types/config/billing-path.d.ts +25 -0
  12. package/dist/types/config/config-schema-version.d.ts +13 -1
  13. package/dist/types/config/file-lock.d.ts +5 -0
  14. package/dist/types/config/model-registry.d.ts +22 -0
  15. package/dist/types/config/settings-schema.d.ts +27 -8
  16. package/dist/types/config/settings.d.ts +2 -0
  17. package/dist/types/coordinator-mcp/projection-scan.d.ts +8 -0
  18. package/dist/types/crash/record-loader.d.ts +15 -0
  19. package/dist/types/extensibility/extensions/types.d.ts +8 -3
  20. package/dist/types/gjc-runtime/launch-worktree-reservation.d.ts +21 -0
  21. package/dist/types/gjc-runtime/launch-worktree.d.ts +25 -1
  22. package/dist/types/gjc-runtime/managed-owner-supervisor.d.ts +2 -0
  23. package/dist/types/gjc-runtime/session-state-lock.d.ts +22 -0
  24. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +66 -1
  25. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +1 -0
  26. package/dist/types/gjc-runtime/tmux-sessions.d.ts +27 -0
  27. package/dist/types/main.d.ts +1 -0
  28. package/dist/types/master-mode/context.d.ts +21 -0
  29. package/dist/types/master-mode/first-request.d.ts +34 -0
  30. package/dist/types/master-mode/snapshot.d.ts +32 -0
  31. package/dist/types/modes/acp/acp-agent.d.ts +21 -21
  32. package/dist/types/modes/components/frictionless-onboarding-selector.d.ts +10 -0
  33. package/dist/types/modes/components/model-selector.d.ts +4 -0
  34. package/dist/types/modes/components/oauth-selector.d.ts +1 -0
  35. package/dist/types/modes/components/session-selector.d.ts +1 -1
  36. package/dist/types/modes/components/sessions-dashboard.d.ts +2 -1
  37. package/dist/types/modes/components/status-line/model-name.d.ts +18 -0
  38. package/dist/types/modes/components/status-line/priority-row.d.ts +34 -0
  39. package/dist/types/modes/components/status-line/segments.d.ts +10 -0
  40. package/dist/types/modes/interactive-mode.d.ts +16 -2
  41. package/dist/types/modes/print-mode.d.ts +17 -0
  42. package/dist/types/modes/theme/theme.d.ts +2 -1
  43. package/dist/types/modes/types.d.ts +2 -0
  44. package/dist/types/modes/ui-language.d.ts +7 -1
  45. package/dist/types/sdk/broker/broker.d.ts +79 -2
  46. package/dist/types/sdk/broker/endpoint-authority.d.ts +24 -0
  47. package/dist/types/sdk/broker/ensure.d.ts +3 -0
  48. package/dist/types/sdk/broker/index.d.ts +3 -0
  49. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +6 -0
  50. package/dist/types/sdk/broker/lifecycle.d.ts +40 -0
  51. package/dist/types/sdk/broker/master-capability.d.ts +10 -0
  52. package/dist/types/sdk/broker/session-index.d.ts +40 -11
  53. package/dist/types/sdk/broker/session-scope.d.ts +72 -0
  54. package/dist/types/sdk/broker/spawn-authority.d.ts +201 -0
  55. package/dist/types/sdk/broker/spawn-substrate.d.ts +30 -0
  56. package/dist/types/sdk/broker/startup-budget.d.ts +52 -0
  57. package/dist/types/sdk/broker/state-version.d.ts +21 -1
  58. package/dist/types/sdk/broker/worktree-occupancy.d.ts +28 -0
  59. package/dist/types/sdk/bus/chat-daemon-control.d.ts +3 -0
  60. package/dist/types/sdk/bus/discord-daemon.d.ts +1 -1
  61. package/dist/types/sdk/bus/index.d.ts +9 -1
  62. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +1 -1
  63. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +1 -1
  64. package/dist/types/sdk/bus/slack-conversation.d.ts +1 -1
  65. package/dist/types/sdk/bus/slack-daemon.d.ts +1 -1
  66. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
  67. package/dist/types/sdk/cli/index.d.ts +1 -0
  68. package/dist/types/sdk/cli/master-cli.d.ts +63 -0
  69. package/dist/types/sdk/cli/rows.d.ts +12 -5
  70. package/dist/types/sdk/cli/session-cli.d.ts +20 -3
  71. package/dist/types/sdk/client/client.d.ts +3 -2
  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/events.d.ts +1 -0
  77. package/dist/types/sdk/host/host.d.ts +10 -0
  78. package/dist/types/sdk/host/query/cursor.d.ts +4 -2
  79. package/dist/types/sdk/host/query/handlers.d.ts +1 -0
  80. package/dist/types/sdk/host/query/revision-store.d.ts +1 -0
  81. package/dist/types/sdk/host/session-runtime.d.ts +70 -17
  82. package/dist/types/sdk/lifecycle/broker-client.d.ts +6 -0
  83. package/dist/types/sdk/lifecycle/service.d.ts +15 -3
  84. package/dist/types/sdk/lifecycle-session.d.ts +3 -1
  85. package/dist/types/sdk/prompt-deadline-manager.d.ts +2 -5
  86. package/dist/types/sdk/receipt-state.d.ts +2 -0
  87. package/dist/types/sdk/router/session-router.d.ts +6 -0
  88. package/dist/types/sdk/session.d.ts +16 -0
  89. package/dist/types/sdk/startup-capability.d.ts +2 -1
  90. package/dist/types/sdk/turn-result.d.ts +6 -0
  91. package/dist/types/session/agent-session.d.ts +52 -7
  92. package/dist/types/session/credential-store-errors.d.ts +1 -0
  93. package/dist/types/session/fold-coordinator.d.ts +29 -2
  94. package/dist/types/session/session-manager.d.ts +79 -2
  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/tools/ask.d.ts +1 -0
  98. package/dist/types/tools/bash.d.ts +16 -0
  99. package/dist/types/tools/browser/launch.d.ts +10 -0
  100. package/dist/types/tools/browser/registry.d.ts +8 -0
  101. package/dist/types/tools/gh.d.ts +8 -0
  102. package/dist/types/tools/index.d.ts +10 -10
  103. package/dist/types/tools/job.d.ts +3 -1
  104. package/dist/types/utils/external-editor.d.ts +7 -0
  105. package/examples/extensions/README.md +1 -1
  106. package/examples/extensions/pirate.ts +7 -4
  107. package/package.json +7 -7
  108. package/scripts/generate-sdk-operation-inventory.ts +13 -1
  109. package/scripts/verify-gjc-sdk-canonicalization.ts +35 -1
  110. package/src/async/job-manager.ts +107 -9
  111. package/src/browser-backend.ts +29 -0
  112. package/src/cli/accounts-cli.ts +9 -5
  113. package/src/cli/args.ts +17 -0
  114. package/src/cli/fast-help.ts +1 -0
  115. package/src/cli/plugin-cli.ts +29 -0
  116. package/src/cli/root-flags.ts +5 -0
  117. package/src/cli/update-cli.ts +140 -23
  118. package/src/commands/launch.ts +86 -18
  119. package/src/commands/sdk.ts +135 -28
  120. package/src/config/autorouting-tier-map.ts +12 -0
  121. package/src/config/billing-path.ts +42 -0
  122. package/src/config/config-schema-version.ts +18 -1
  123. package/src/config/file-lock.ts +158 -83
  124. package/src/config/model-preset-registry.ts +178 -23
  125. package/src/config/model-profiles.ts +5 -5
  126. package/src/config/model-registry.ts +291 -42
  127. package/src/config/model-resolver.ts +5 -0
  128. package/src/config/settings-schema.ts +27 -8
  129. package/src/config/settings.ts +51 -15
  130. package/src/coordinator-mcp/projection-scan.ts +27 -6
  131. package/src/coordinator-mcp/question-state.ts +9 -11
  132. package/src/coordinator-mcp/server.ts +115 -21
  133. package/src/crash/index-store.ts +2 -0
  134. package/src/crash/record-loader.ts +10 -0
  135. package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -1
  136. package/src/extensibility/extensions/runner.ts +27 -0
  137. package/src/extensibility/extensions/types.ts +8 -3
  138. package/src/extensibility/gjc-plugins/installer.ts +51 -11
  139. package/src/extensibility/runtime-skill-discovery.ts +110 -3
  140. package/src/extensibility/skills.ts +22 -12
  141. package/src/gjc-runtime/launch-worktree-reservation.ts +242 -0
  142. package/src/gjc-runtime/launch-worktree.ts +285 -4
  143. package/src/gjc-runtime/managed-owner-supervisor.ts +63 -51
  144. package/src/gjc-runtime/session-state-lock.ts +213 -14
  145. package/src/gjc-runtime/session-state-sidecar.ts +938 -39
  146. package/src/gjc-runtime/tmux-owner-isolation-cli.ts +4 -7
  147. package/src/gjc-runtime/tmux-owner-isolation.ts +18 -3
  148. package/src/gjc-runtime/tmux-sessions.ts +252 -13
  149. package/src/hooks/skill-state.ts +6 -0
  150. package/src/internal-urls/docs-index.generated.ts +15 -15
  151. package/src/internal-urls/issue-pr-protocol.ts +4 -2
  152. package/src/main.ts +61 -3
  153. package/src/master-mode/context.ts +50 -0
  154. package/src/master-mode/first-request.ts +65 -0
  155. package/src/master-mode/snapshot.ts +205 -0
  156. package/src/modes/acp/acp-agent.ts +610 -161
  157. package/src/modes/components/footer.ts +4 -2
  158. package/src/modes/components/frictionless-onboarding-selector.ts +48 -2
  159. package/src/modes/components/model-selector.ts +76 -8
  160. package/src/modes/components/oauth-selector.ts +6 -1
  161. package/src/modes/components/session-selector.ts +23 -15
  162. package/src/modes/components/sessions-dashboard.ts +8 -5
  163. package/src/modes/components/settings-selector.ts +1 -0
  164. package/src/modes/components/status-line/model-name.ts +53 -0
  165. package/src/modes/components/status-line/priority-row.ts +107 -0
  166. package/src/modes/components/status-line/segments.ts +23 -9
  167. package/src/modes/components/tool-status-header.ts +151 -15
  168. package/src/modes/controllers/command-controller.ts +1 -1
  169. package/src/modes/controllers/event-controller.ts +30 -4
  170. package/src/modes/controllers/extension-ui-controller.ts +13 -13
  171. package/src/modes/controllers/input-controller.ts +30 -2
  172. package/src/modes/controllers/selector-controller.ts +200 -98
  173. package/src/modes/interactive-mode.ts +36 -12
  174. package/src/modes/print-mode.ts +64 -0
  175. package/src/modes/prompt-action-autocomplete.ts +30 -4
  176. package/src/modes/runtime-init.ts +12 -9
  177. package/src/modes/theme/theme.ts +5 -0
  178. package/src/modes/types.ts +2 -0
  179. package/src/modes/ui-language.ts +49 -2
  180. package/src/modes/utils/ui-helpers.ts +22 -3
  181. package/src/prompts/agent-fragments/browser-aside.md +22 -0
  182. package/src/prompts/system/master-mode.md +28 -0
  183. package/src/prompts/tools/ask.md +1 -1
  184. package/src/prompts/tools/bash.md +1 -0
  185. package/src/prompts/tools/job.md +1 -1
  186. package/src/sdk/acp/adapter.ts +5 -0
  187. package/src/sdk/broker/broker.ts +1646 -27
  188. package/src/sdk/broker/endpoint-authority.ts +86 -0
  189. package/src/sdk/broker/ensure.ts +81 -3
  190. package/src/sdk/broker/index.ts +3 -0
  191. package/src/sdk/broker/lifecycle-ledger.ts +6 -0
  192. package/src/sdk/broker/lifecycle.ts +440 -75
  193. package/src/sdk/broker/master-capability.ts +232 -0
  194. package/src/sdk/broker/session-index.ts +121 -17
  195. package/src/sdk/broker/session-scope.ts +272 -0
  196. package/src/sdk/broker/spawn-authority.ts +1031 -0
  197. package/src/sdk/broker/spawn-substrate.ts +591 -0
  198. package/src/sdk/broker/startup-budget.ts +144 -2
  199. package/src/sdk/broker/state-version.ts +43 -14
  200. package/src/sdk/broker/transport.ts +1 -0
  201. package/src/sdk/broker/worktree-occupancy.ts +90 -0
  202. package/src/sdk/bus/chat-daemon-control.ts +5 -2
  203. package/src/sdk/bus/chat-daemon-runtime.ts +136 -18
  204. package/src/sdk/bus/discord-daemon.ts +50 -10
  205. package/src/sdk/bus/index.ts +84 -7
  206. package/src/sdk/bus/kind-aware-reconciliation.ts +296 -48
  207. package/src/sdk/bus/lifecycle-commands.ts +4 -0
  208. package/src/sdk/bus/notification-service.ts +22 -5
  209. package/src/sdk/bus/prompt-reconciliation.ts +7 -2
  210. package/src/sdk/bus/reconciliation-store.ts +55 -31
  211. package/src/sdk/bus/slack-conversation.ts +1 -1
  212. package/src/sdk/bus/slack-daemon.ts +72 -38
  213. package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
  214. package/src/sdk/cli/index.ts +1 -0
  215. package/src/sdk/cli/master-cli.ts +259 -0
  216. package/src/sdk/cli/rows.ts +41 -15
  217. package/src/sdk/cli/session-cli.ts +297 -26
  218. package/src/sdk/client/client.ts +14 -8
  219. package/src/sdk/host/control/dispatch.ts +101 -40
  220. package/src/sdk/host/control/index.ts +2 -0
  221. package/src/sdk/host/control/operations.ts +2 -0
  222. package/src/sdk/host/control/runtime-gate.ts +203 -11
  223. package/src/sdk/host/events.ts +1 -0
  224. package/src/sdk/host/host.ts +29 -2
  225. package/src/sdk/host/query/cursor.ts +52 -7
  226. package/src/sdk/host/query/handlers.ts +33 -12
  227. package/src/sdk/host/query/revision-store.ts +12 -0
  228. package/src/sdk/host/session-runtime.test.ts +673 -14
  229. package/src/sdk/host/session-runtime.ts +674 -103
  230. package/src/sdk/host/surface-policy.ts +3 -3
  231. package/src/sdk/lifecycle/broker-client.ts +28 -0
  232. package/src/sdk/lifecycle/service.ts +279 -20
  233. package/src/sdk/lifecycle-session.ts +17 -4
  234. package/src/sdk/prompt-deadline-manager.ts +2 -1
  235. package/src/sdk/protocol/operation-inventory.generated.json +105 -3
  236. package/src/sdk/protocol/operation-registry.ts +14 -4
  237. package/src/sdk/receipt-state.ts +10 -0
  238. package/src/sdk/router/session-router.ts +219 -118
  239. package/src/sdk/session.ts +416 -195
  240. package/src/sdk/startup-capability.ts +1 -0
  241. package/src/sdk/turn-result.ts +22 -0
  242. package/src/session/agent-session.ts +1628 -318
  243. package/src/session/blob-store.ts +15 -5
  244. package/src/session/credential-store-errors.ts +2 -0
  245. package/src/session/fold-coordinator.ts +40 -3
  246. package/src/session/internal/managed-session-storage.ts +153 -78
  247. package/src/session/session-manager.ts +850 -97
  248. package/src/session/yield-queue.ts +24 -10
  249. package/src/setup/frictionless-onboarding.ts +2 -0
  250. package/src/slash-commands/builtin-registry.ts +41 -0
  251. package/src/task/executor.ts +9 -4
  252. package/src/telemetry/events.ts +1138 -12
  253. package/src/tools/ask-contract.test.ts +64 -0
  254. package/src/tools/ask-contract.ts +36 -1
  255. package/src/tools/ask.ts +10 -8
  256. package/src/tools/bash-interactive.ts +11 -2
  257. package/src/tools/bash.ts +412 -75
  258. package/src/tools/browser/launch.ts +37 -0
  259. package/src/tools/browser/registry.ts +99 -9
  260. package/src/tools/cron.ts +6 -4
  261. package/src/tools/descriptors.test.ts +15 -0
  262. package/src/tools/descriptors.ts +5 -1
  263. package/src/tools/gh.ts +104 -48
  264. package/src/tools/github-cache.ts +2 -2
  265. package/src/tools/index.ts +10 -6
  266. package/src/tools/job.ts +4 -1
  267. package/src/tools/monitor.ts +12 -9
  268. package/src/tools/resolve.ts +30 -10
  269. package/src/tools/tool-catalog.generated.ts +2 -2
  270. package/src/utils/external-editor.ts +72 -1
@@ -26,10 +26,13 @@ import {
26
26
  import { mergeModelProfiles } from "../../config/model-profiles";
27
27
  import { ModelsConfigFile } from "../../config/model-registry";
28
28
  import {
29
- ensureLaunchWorktree,
30
- ensureReusableNodeModules,
29
+ DependencyPreparationTimeoutError,
30
+ ensureLaunchWorktreeCancellable,
31
+ ensureReusableNodeModulesCancellable,
31
32
  type GjcLaunchWorktreePlan,
32
33
  planLaunchWorktree,
34
+ removeOwnedLaunchWorktree,
35
+ WorktreePreparationTimeoutError,
33
36
  } from "../../gjc-runtime/launch-worktree";
34
37
  import { probeLinuxProcPidSync } from "../../gjc-runtime/linux-proc";
35
38
  import {
@@ -66,6 +69,7 @@ import {
66
69
  sanitizeSdkStartupMessage,
67
70
  } from "../startup-capability";
68
71
  import type { Broker, BrokerCleanupEvidence, BrokerCleanupIdentity, BrokerResponse } from "./broker";
72
+ import { matchesIndexedEndpointFile, readEndpointFile } from "./endpoint-authority";
69
73
  import { decodeLifecycleUtf8, parseLifecycleJson } from "./lifecycle-codec";
70
74
  import type {
71
75
  LifecycleCleanupProof,
@@ -81,14 +85,23 @@ import {
81
85
  processIncarnation,
82
86
  } from "./process-incarnation";
83
87
  import { resolveSdkInternalSpawnCommand, type SdkInternalSpawnCommand } from "./runtime";
84
- import { type IndexedSession, isSessionAuthorityEligible } from "./session-index";
88
+ import {
89
+ type IndexedSession,
90
+ isSessionAuthorityEligible,
91
+ resolveSessionLocator,
92
+ type SessionLocatorV2,
93
+ } from "./session-index";
85
94
  import {
86
95
  cancellableSleep,
87
96
  DEFAULT_READINESS_TIMEOUT_MS,
97
+ deriveLifecycleOuterDeadlines,
88
98
  isValidReadinessTimeoutMs,
99
+ PREPARATION_TIMEOUT_INVALID_MESSAGE,
89
100
  READINESS_TIMEOUT_INVALID_MESSAGE,
101
+ readPreparationTimeouts,
90
102
  startupQueueWaitMs,
91
103
  } from "./startup-budget";
104
+ import { worktreeOccupant } from "./worktree-occupancy";
92
105
 
93
106
  export {
94
107
  type ProcessIncarnationCommandRunner,
@@ -194,6 +207,17 @@ const lifecycleCommandResolversForTest = new WeakMap<Broker, LifecycleCommandRes
194
207
  const lifecycleCleanupHooksForTest = new WeakMap<Broker, () => void>();
195
208
  const startupAdmittedInputs = new WeakSet<Input>();
196
209
  const startupLaunchInputs = new WeakMap<Input, SessionLaunch>();
210
+ type EnsureLaunchWorktreeForTest = (
211
+ plan: GjcLaunchWorktreePlan,
212
+ opts: { signal: AbortSignal; deadlineAt: number },
213
+ ) => Promise<SessionLifecycleWorktreeReceipt & { createdBranch: boolean }>;
214
+ type EnsureReusableNodeModulesForTest = (
215
+ sourceRoot: string,
216
+ worktreePath: string,
217
+ opts: { signal: AbortSignal; deadlineAt: number },
218
+ ) => Promise<"symlink" | "present" | "missing">;
219
+ let ensureLaunchWorktreeForTest: EnsureLaunchWorktreeForTest | undefined;
220
+ let ensureReusableNodeModulesForTest: EnsureReusableNodeModulesForTest | undefined;
197
221
 
198
222
  /** Test-only hook for simulating a crash immediately after one exact lifecycle detach. */
199
223
  export function setLifecycleCleanupHookForTest(broker: Broker, hook: (() => void) | undefined): void {
@@ -213,6 +237,20 @@ export function setLifecycleTimingForTest(broker: Broker, timing: LifecycleTimin
213
237
  if (timing) lifecycleTimingsForTest.set(broker, timing);
214
238
  else lifecycleTimingsForTest.delete(broker);
215
239
  }
240
+ export function setEnsureLaunchWorktreeForTest(
241
+ _broker: Broker | undefined,
242
+ fn?: EnsureLaunchWorktreeForTest | undefined,
243
+ ): void {
244
+ ensureLaunchWorktreeForTest = typeof _broker === "function" ? (_broker as EnsureLaunchWorktreeForTest) : fn;
245
+ }
246
+
247
+ export function setEnsureReusableNodeModulesForTest(
248
+ _broker: Broker | undefined,
249
+ fn?: EnsureReusableNodeModulesForTest | undefined,
250
+ ): void {
251
+ ensureReusableNodeModulesForTest =
252
+ typeof _broker === "function" ? (_broker as EnsureReusableNodeModulesForTest) : fn;
253
+ }
216
254
 
217
255
  let lifecycleHostPlatformForTest: NodeJS.Platform | undefined;
218
256
 
@@ -293,6 +331,7 @@ export interface SessionLifecycleWorktreeReceipt {
293
331
  cwd: string;
294
332
  created: boolean;
295
333
  reused: boolean;
334
+ createdBranch: boolean;
296
335
  branch?: string;
297
336
  }
298
337
 
@@ -612,7 +651,7 @@ function text(value: unknown): string | undefined {
612
651
  return typeof value === "string" && value ? value : undefined;
613
652
  }
614
653
 
615
- function validateBrokerModelPresetSync(agentDir: string, requestedProfile: string): string | BrokerResponse {
654
+ export function validateBrokerModelPresetSync(agentDir: string, requestedProfile: string): string | BrokerResponse {
616
655
  const modelsConfigFile = ModelsConfigFile.relocate(path.join(agentDir, "models.yml"));
617
656
  modelsConfigFile.invalidate();
618
657
  const loaded = modelsConfigFile.tryLoad();
@@ -727,16 +766,19 @@ function isLifecycleWorktreeTarget(value: unknown): value is SessionLifecycleWor
727
766
 
728
767
  function lifecycleWorktreeTarget(input: Input): SessionLifecycleWorktreeTarget | null | undefined {
729
768
  const target = input.target as Record<string, unknown> | undefined;
730
- const worktree = target?.worktree;
769
+ const worktree = target?.worktree ?? input.worktree;
731
770
  if (worktree === undefined) return undefined;
732
771
  return isLifecycleWorktreeTarget(worktree) ? worktree : null;
733
772
  }
734
773
 
735
774
  type LiveResumeRecord = {
736
- locator: { repo: string; stateRoot: string };
775
+ sessionId: string;
776
+ locator: SessionLocatorV2;
737
777
  endpointGeneration: number;
738
778
  pid: number;
739
779
  endpointMtimeMs?: number;
780
+ processIncarnation?: string;
781
+ hostIncarnation?: string;
740
782
  live: boolean;
741
783
  ambiguous: boolean;
742
784
  };
@@ -755,7 +797,7 @@ type ResumeScope = {
755
797
  };
756
798
  function sameResumeLocator(record: LiveResumeRecord, cwd: string, root: string): boolean {
757
799
  return (
758
- resolveEquivalentPath(record.locator.repo) === resolveEquivalentPath(cwd) &&
800
+ resolveEquivalentPath(record.locator.cwd) === resolveEquivalentPath(cwd) &&
759
801
  resolveEquivalentPath(record.locator.stateRoot) === resolveEquivalentPath(root)
760
802
  );
761
803
  }
@@ -777,10 +819,24 @@ function sameLiveResumeRecord(expected: LiveResumeRecord, current: LiveResumeRec
777
819
  current.endpointGeneration === expected.endpointGeneration &&
778
820
  current.pid === expected.pid &&
779
821
  current.endpointMtimeMs === expected.endpointMtimeMs &&
780
- sameResumeLocator(current, expected.locator.repo, expected.locator.stateRoot)
822
+ (current.hostIncarnation ?? current.processIncarnation) ===
823
+ (expected.hostIncarnation ?? expected.processIncarnation) &&
824
+ sameResumeLocator(current, expected.locator.cwd, expected.locator.stateRoot)
781
825
  );
782
826
  }
783
827
 
828
+ function liveResumeAuthority(
829
+ sessions: readonly LiveResumeRecord[],
830
+ sessionId: string,
831
+ ): { kind: "none" } | { kind: "ambiguous" } | { kind: "live"; record: LiveResumeRecord } {
832
+ const indexedCandidates = sessions.filter(session => session.sessionId === sessionId);
833
+ if (indexedCandidates.some(session => !isSessionAuthorityEligible(session))) return { kind: "ambiguous" };
834
+ const liveCandidates = indexedCandidates.filter(session => session.live);
835
+ if (liveCandidates.length > 1) return { kind: "ambiguous" };
836
+ const record = liveCandidates[0];
837
+ return record ? { kind: "live", record } : { kind: "none" };
838
+ }
839
+
784
840
  type ValidatedTranscript = {
785
841
  path: string;
786
842
  id: string;
@@ -912,18 +968,36 @@ async function validateLiveResumeScope(
912
968
  sessionIdentity: session.identity,
913
969
  };
914
970
  }
915
- async function reconcileReadyScope(broker: Broker, id: string, scope: string | undefined): Promise<void> {
971
+ async function reconcileReadyScope(
972
+ broker: Broker,
973
+ id: string,
974
+ scope: string | undefined,
975
+ root: string,
976
+ expected: EffectMarker,
977
+ ): Promise<void> {
916
978
  if (!scope) return;
917
979
  await broker.index.refresh();
918
- const record = broker.index.listSessions().sessions.find(session => session.sessionId === id);
919
- if (!record || record.locator.repo === scope) return;
920
- // The host records its physical cwd, which Darwin canonicalizes from /var to
921
- // /private/var. Preserve the lifecycle caller's lexical cwd for ACP's scoped
922
- // listing while retaining the host-provided state root for endpoint binding.
980
+ const record = broker.index
981
+ .listSessions()
982
+ .sessions.find(
983
+ session =>
984
+ session.sessionId === id &&
985
+ resolveEquivalentPath(session.locator.stateRoot) === resolveEquivalentPath(root) &&
986
+ session.pid === expected.pid &&
987
+ (session.hostIncarnation ?? session.processIncarnation) === expected.incarnation &&
988
+ session.lifecycleRequestId === expected.effectMarker,
989
+ );
990
+ if (!record) return;
991
+ const cwd = canonicalExistingPath(scope);
992
+ if (record.locator.cwd === cwd) return;
993
+ // Locator cwd is canonical everywhere. Reconcile only a pre-existing host row
994
+ // whose canonical registration race observed a different path; it never retains
995
+ // a caller's lexical spelling for ACP or any other scope consumer.
996
+ const locator = await resolveSessionLocator(scope, record.locator.stateRoot);
923
997
  await broker.index.append({
924
998
  type: "record_reconciled",
925
999
  sessionId: id,
926
- locator: { ...record.locator, repo: scope },
1000
+ locator,
927
1001
  endpointGeneration: record.endpointGeneration,
928
1002
  pid: record.pid,
929
1003
  // Reconciliation only re-scopes the locator, so every identity fact the host
@@ -933,6 +1007,8 @@ async function reconcileReadyScope(broker: Broker, id: string, scope: string | u
933
1007
  ...(record.hostIncarnation === undefined ? {} : { hostIncarnation: record.hostIncarnation }),
934
1008
  ...(record.lifecycleRequestId === undefined ? {} : { lifecycleRequestId: record.lifecycleRequestId }),
935
1009
  endpointMtimeMs: record.endpointMtimeMs,
1010
+ ...(record.endpointFileId === undefined ? {} : { endpointFileId: record.endpointFileId }),
1011
+ ...(record.masterRole === undefined ? {} : { masterRole: record.masterRole }),
936
1012
  });
937
1013
  }
938
1014
 
@@ -964,6 +1040,66 @@ function command(broker: Broker): LifecycleCommand {
964
1040
  return lifecycleCommandResolversForTest.get(broker)?.() ?? resolveSdkInternalSpawnCommand("session-host-internal");
965
1041
  }
966
1042
 
1043
+ /**
1044
+ * Reuses the ordinary lifecycle host bootstrap without performing its direct
1045
+ * process spawn. Broker-owned substrate providers receive this exact argv and
1046
+ * split inherited/child-specific environment contract as their only launch
1047
+ * authority for session.spawn.
1048
+ */
1049
+ export type SpawnChildHostLaunch = {
1050
+ childId: string;
1051
+ cwd: string;
1052
+ stateRoot: string;
1053
+ argv: readonly string[];
1054
+ inheritedEnv: Readonly<Record<string, string>>;
1055
+ env: Readonly<Record<string, string>>;
1056
+ effectMarker: string;
1057
+ };
1058
+
1059
+ export function prepareSpawnChildHostLaunch(
1060
+ broker: Broker,
1061
+ input: { cwd: string; modelId?: string; modelPreset?: string; childId?: string; receivedAt?: number },
1062
+ ): SpawnChildHostLaunch {
1063
+ const cwd = path.resolve(input.cwd);
1064
+ const childId = input.childId ?? randomUUID();
1065
+ const stateRoot = defaultStateRoot(cwd);
1066
+ const effectMarker = randomUUID();
1067
+ const deadlines = deriveLifecycleDeadlines(input.receivedAt ?? Date.now(), DEFAULT_READINESS_TIMEOUT_MS);
1068
+ const request: SessionLifecycleLaunchRequest = {
1069
+ operation: "session.create",
1070
+ sessionId: childId,
1071
+ cwd,
1072
+ stateRoot,
1073
+ effectMarker,
1074
+ ...deadlines,
1075
+ ...(input.modelId === undefined ? {} : { modelId: input.modelId }),
1076
+ ...(input.modelPreset === undefined ? {} : { modelPreset: input.modelPreset }),
1077
+ };
1078
+ const cmd = command(broker);
1079
+ const inherited = "kind" in cmd ? cmd.env : process.env;
1080
+ const inheritedEnv = Object.fromEntries(
1081
+ Object.entries(inherited).filter(
1082
+ (entry): entry is [string, string] => entry[0] !== "GJC_MASTER_CAPABILITY" && typeof entry[1] === "string",
1083
+ ),
1084
+ );
1085
+ return {
1086
+ childId,
1087
+ cwd,
1088
+ stateRoot,
1089
+ argv: [cmd.file, ...cmd.args],
1090
+ inheritedEnv,
1091
+ env: {
1092
+ GJC_AGENT_DIR: broker.settings.agentDir,
1093
+ GJC_CODING_AGENT_DIR: broker.settings.agentDir,
1094
+ GJC_SESSION_ID: childId,
1095
+ GJC_STATE_ROOT: stateRoot,
1096
+ GJC_LIFECYCLE_REQUEST_ID: effectMarker,
1097
+ GJC_SDK_LIFECYCLE_REQUEST: JSON.stringify(request),
1098
+ },
1099
+ effectMarker,
1100
+ };
1101
+ }
1102
+
967
1103
  const lifecycleMarkerPath = (root: string, id: string) => path.join(root, "sdk", `${id}.lifecycle.json`);
968
1104
  const lifecycleReadyPath = (root: string, id: string) => path.join(root, "sdk", `${id}.lifecycle.ready.json`);
969
1105
  const lifecycleFailurePath = (root: string, id: string, effectMarker: string) =>
@@ -973,6 +1109,7 @@ type ReadyAuthority = {
973
1109
  endpoint: Record<string, unknown>;
974
1110
  endpointSource: string;
975
1111
  endpointMtimeMs: number;
1112
+ endpointFileId?: string;
976
1113
  endpointGeneration: number;
977
1114
  };
978
1115
  type ReadinessResult =
@@ -3351,7 +3488,7 @@ async function recordTerminalUncertain(
3351
3488
  await broker.index.append({
3352
3489
  type: "lifecycle_terminal",
3353
3490
  sessionId: id,
3354
- locator: { repo: "unknown", stateRoot: root },
3491
+ locator: { cwd: "unknown", worktreeRoot: null, stateRoot: root },
3355
3492
  endpointGeneration: 0,
3356
3493
  pid,
3357
3494
  terminalUncertain: true,
@@ -3667,7 +3804,10 @@ async function removeExactDeadSessionEndpoint(
3667
3804
  : finalEndpointPath;
3668
3805
  let handle: fs.FileHandle | undefined;
3669
3806
  try {
3670
- handle = await fs.open(endpointSource, fsSync.constants.O_RDONLY | fsSync.constants.O_NOFOLLOW);
3807
+ handle = await fs.open(
3808
+ endpointSource,
3809
+ fsSync.constants.O_RDONLY | (fsSync.constants.O_NOFOLLOW ?? 0) | (fsSync.constants.O_NONBLOCK ?? 0),
3810
+ );
3671
3811
  const metadata = await handle.stat({ bigint: true });
3672
3812
  if (!metadata.isFile() || metadata.nlink !== 1n || metadata.size > 4096n) return false;
3673
3813
  const bytes = Buffer.alloc(Number(metadata.size) + 1);
@@ -3847,10 +3987,9 @@ async function currentReadyAuthority(
3847
3987
  if (!(await hasOwnedReadinessEvidence(broker, root, id, expected))) return undefined;
3848
3988
  const endpointPath = path.join(root, "sdk", `${id}.json`);
3849
3989
  try {
3850
- const [endpointSource, endpointMetadata] = await Promise.all([
3851
- fs.readFile(endpointPath, "utf8"),
3852
- fs.stat(endpointPath),
3853
- ]);
3990
+ const endpointFile = await readEndpointFile(endpointPath);
3991
+ if (!endpointFile) return undefined;
3992
+ const endpointSource = endpointFile.source;
3854
3993
  const endpoint = JSON.parse(endpointSource) as {
3855
3994
  sessionId?: unknown;
3856
3995
  url?: unknown;
@@ -3863,14 +4002,24 @@ async function currentReadyAuthority(
3863
4002
  // native-dead child; do not refuse a native-alive ready host for a stale live bit.
3864
4003
  await broker.heartbeatSessions();
3865
4004
  await broker.index.refresh();
3866
- const record = broker.index.listSessions().sessions.find(session => session.sessionId === id);
4005
+ const record = broker.index
4006
+ .listSessions()
4007
+ .sessions.find(
4008
+ session =>
4009
+ session.sessionId === id &&
4010
+ session.pid === expected.pid &&
4011
+ resolveEquivalentPath(session.locator.stateRoot) === resolveEquivalentPath(root) &&
4012
+ (session.hostIncarnation ?? session.processIncarnation) === expected.incarnation &&
4013
+ session.lifecycleRequestId === expected.effectMarker,
4014
+ );
3867
4015
  if (
3868
4016
  !record ||
3869
4017
  record.terminal ||
3870
4018
  record.terminalUncertain ||
3871
4019
  record.pid !== expected.pid ||
3872
4020
  resolveEquivalentPath(record.locator.stateRoot) !== resolveEquivalentPath(root) ||
3873
- record.endpointMtimeMs !== endpointMetadata.mtimeMs ||
4021
+ (record.hostIncarnation ?? record.processIncarnation) !== expected.incarnation ||
4022
+ !matchesIndexedEndpointFile(endpointFile, record) ||
3874
4023
  endpoint.pid !== expected.pid ||
3875
4024
  endpoint.sessionId !== id ||
3876
4025
  typeof endpoint.url !== "string" ||
@@ -3885,7 +4034,8 @@ async function currentReadyAuthority(
3885
4034
  return {
3886
4035
  endpoint: endpoint as Record<string, unknown>,
3887
4036
  endpointSource,
3888
- endpointMtimeMs: endpointMetadata.mtimeMs,
4037
+ endpointMtimeMs: endpointFile.mtimeMs,
4038
+ ...(record.endpointFileId === undefined ? {} : { endpointFileId: record.endpointFileId }),
3889
4039
  endpointGeneration: record.endpointGeneration,
3890
4040
  };
3891
4041
  } catch {
@@ -3896,7 +4046,8 @@ async function currentReadyAuthority(
3896
4046
  function sameReadyAuthority(left: ReadyAuthority, right: ReadyAuthority): boolean {
3897
4047
  return (
3898
4048
  left.endpointSource === right.endpointSource &&
3899
- left.endpointMtimeMs === right.endpointMtimeMs &&
4049
+ Math.abs(left.endpointMtimeMs - right.endpointMtimeMs) <= 0.001 &&
4050
+ left.endpointFileId === right.endpointFileId &&
3900
4051
  left.endpointGeneration === right.endpointGeneration
3901
4052
  );
3902
4053
  }
@@ -4022,8 +4173,31 @@ function worktreeIntent(plan: GjcLaunchWorktreePlan | undefined): LifecycleWorkt
4022
4173
  };
4023
4174
  }
4024
4175
 
4025
- function preparePlannedWorktree(plan: GjcLaunchWorktreePlan): SessionLifecycleWorktreeReceipt {
4026
- const prepared = ensureLaunchWorktree(plan);
4176
+ /** Test seam for the worktree occupancy boundary. */
4177
+ export function worktreeOccupantForTest(
4178
+ sessions: IndexedSession[],
4179
+ worktreePath: string,
4180
+ observe: (pid: number, expectedIncarnation: string | undefined) => ProcessObservation = observeProcess,
4181
+ ): string | null {
4182
+ return worktreeOccupant(sessions, worktreePath, observe);
4183
+ }
4184
+
4185
+ async function preparePlannedWorktree(
4186
+ plan: GjcLaunchWorktreePlan,
4187
+ opts: { signal: AbortSignal; deadlineAt: number; now: () => number },
4188
+ ): Promise<SessionLifecycleWorktreeReceipt> {
4189
+ const hooked = ensureLaunchWorktreeForTest;
4190
+ if (hooked) {
4191
+ const receipt = await hooked(plan, { signal: opts.signal, deadlineAt: opts.deadlineAt });
4192
+ if (!receipt.enabled || path.resolve(receipt.cwd) !== path.resolve(plan.worktreePath))
4193
+ throw new Error("Lifecycle worktree preparation did not preserve the durable worktree identity.");
4194
+ return receipt;
4195
+ }
4196
+ const prepared = await ensureLaunchWorktreeCancellable(plan, {
4197
+ signal: opts.signal,
4198
+ deadlineAt: opts.deadlineAt,
4199
+ now: opts.now,
4200
+ });
4027
4201
  if (!prepared.enabled || path.resolve(prepared.worktreePath) !== path.resolve(plan.worktreePath))
4028
4202
  throw new Error("Lifecycle worktree preparation did not preserve the durable worktree identity.");
4029
4203
  return {
@@ -4031,9 +4205,67 @@ function preparePlannedWorktree(plan: GjcLaunchWorktreePlan): SessionLifecycleWo
4031
4205
  cwd: path.resolve(prepared.worktreePath),
4032
4206
  created: prepared.created,
4033
4207
  reused: prepared.reused,
4208
+ createdBranch: prepared.createdBranch,
4034
4209
  ...(prepared.branchName ? { branch: prepared.branchName } : {}),
4035
4210
  };
4036
4211
  }
4212
+
4213
+ function mapPreparationFailure(error: unknown): BrokerResponse {
4214
+ const message = sanitizeSdkStartupMessage(error);
4215
+ if (
4216
+ error instanceof WorktreePreparationTimeoutError ||
4217
+ (error as { code?: string }).code === "worktree_preparation_timeout"
4218
+ ) {
4219
+ return fail("worktree_preparation_timeout", message);
4220
+ }
4221
+ if (
4222
+ error instanceof DependencyPreparationTimeoutError ||
4223
+ (error as { code?: string }).code === "dependency_preparation_timeout"
4224
+ ) {
4225
+ return fail("dependency_preparation_timeout", message);
4226
+ }
4227
+ return fail("spawn_failed", `Unable to prepare lifecycle worktree: ${message}`);
4228
+ }
4229
+
4230
+ function abortWhenDue(
4231
+ controller: AbortController,
4232
+ deadlineAt: number,
4233
+ now: () => number,
4234
+ ): ReturnType<typeof setInterval> {
4235
+ const tick = (): void => {
4236
+ if (now() >= deadlineAt && !controller.signal.aborted) controller.abort();
4237
+ };
4238
+ tick();
4239
+ return setInterval(tick, 10);
4240
+ }
4241
+
4242
+ function durableWorktreeEffects(
4243
+ receipt: SessionLifecycleWorktreeReceipt,
4244
+ timings: { worktreePreparationMs?: number; dependencyPreparationMs?: number; spawnAuthorizedAtOffsetMs?: number },
4245
+ ): LifecycleDurableEffectsReceipt {
4246
+ const worktree = {
4247
+ cwdDigest: createHash("sha256").update(receipt.cwd, "utf8").digest("hex"),
4248
+ created: receipt.created,
4249
+ reused: receipt.reused,
4250
+ createdBranch: receipt.createdBranch,
4251
+ ...(receipt.branch ? { branchDigest: createHash("sha256").update(receipt.branch, "utf8").digest("hex") } : {}),
4252
+ };
4253
+ return {
4254
+ worktree,
4255
+ timings,
4256
+ digest: createHash("sha256").update(canonicalJson({ worktree, timings })).digest("hex"),
4257
+ };
4258
+ }
4259
+
4260
+ function deadlineFieldsPresent(input: Input): boolean {
4261
+ return [
4262
+ input.receivedAt,
4263
+ input.requestedReadinessTimeoutMs,
4264
+ input.semanticReadyDeadlineAt,
4265
+ input.terminationStartDeadlineAt,
4266
+ input.lifecycleCleanupDeadlineAt,
4267
+ ].some(value => value !== undefined);
4268
+ }
4037
4269
  async function launchInput(
4038
4270
  broker: Broker,
4039
4271
  operation: "session.create" | "session.fork" | "session.resume",
@@ -4453,7 +4685,7 @@ async function validateDeletePath(
4453
4685
  broker: Broker,
4454
4686
  input: Input,
4455
4687
  id: string,
4456
- record: { locator: { repo: string; stateRoot: string } } | undefined,
4688
+ record: { locator: SessionLocatorV2 } | undefined,
4457
4689
  cleanup?: CleanupEvidence,
4458
4690
  ): Promise<ValidatedDelete | BrokerResponse> {
4459
4691
  const sessionPath = text(input.sessionPath);
@@ -4467,7 +4699,7 @@ async function validateDeletePath(
4467
4699
  const canonicalRequestedRoot = canonicalExistingPath(requestedRoot);
4468
4700
  if (
4469
4701
  record &&
4470
- (canonicalExistingPath(record.locator.repo) !== cwd ||
4702
+ (canonicalExistingPath(record.locator.cwd) !== cwd ||
4471
4703
  canonicalExistingPath(record.locator.stateRoot) !== canonicalRequestedRoot)
4472
4704
  )
4473
4705
  return fail("invalid_input", "session.delete locator does not match the indexed session.");
@@ -4557,7 +4789,7 @@ async function validateDeletePath(
4557
4789
  }
4558
4790
  type CloseAuthority = { endpointGeneration: number; endpointIncarnation: string };
4559
4791
  type CloseRecord = {
4560
- locator: { repo: string; stateRoot: string };
4792
+ locator: SessionLocatorV2;
4561
4793
  endpointGeneration: number;
4562
4794
  pid: number;
4563
4795
  endpointMtimeMs?: number;
@@ -4621,7 +4853,7 @@ function sameCloseStoredProcessIdentity(expected: CloseRecord, current: CloseRec
4621
4853
  typeof expected.lifecycleRequestId === "string" &&
4622
4854
  expected.lifecycleRequestId.length > 0 &&
4623
4855
  current.lifecycleRequestId === expected.lifecycleRequestId &&
4624
- path.resolve(current.locator.repo) === path.resolve(expected.locator.repo) &&
4856
+ path.resolve(current.locator.cwd) === path.resolve(expected.locator.cwd) &&
4625
4857
  path.resolve(current.locator.stateRoot) === path.resolve(expected.locator.stateRoot)
4626
4858
  );
4627
4859
  }
@@ -4639,7 +4871,7 @@ function sameCloseEndpointIdentity(expected: CloseRecord, current: CloseRecord):
4639
4871
  current.endpointMtimeMs !== undefined &&
4640
4872
  expected.endpointMtimeMs !== undefined &&
4641
4873
  current.lifecycleRequestId === expected.lifecycleRequestId &&
4642
- path.resolve(current.locator.repo) === path.resolve(expected.locator.repo) &&
4874
+ path.resolve(current.locator.cwd) === path.resolve(expected.locator.cwd) &&
4643
4875
  path.resolve(current.locator.stateRoot) === path.resolve(expected.locator.stateRoot)
4644
4876
  );
4645
4877
  }
@@ -4656,7 +4888,7 @@ function sameCloseGeneration(expected: CloseRecord, current: CloseRecord & { liv
4656
4888
  current.endpointMtimeMs === expected.endpointMtimeMs &&
4657
4889
  current.lifecycleRequestId === expected.lifecycleRequestId &&
4658
4890
  current.processIncarnation === expected.processIncarnation &&
4659
- path.resolve(current.locator.repo) === path.resolve(expected.locator.repo) &&
4891
+ path.resolve(current.locator.cwd) === path.resolve(expected.locator.cwd) &&
4660
4892
  path.resolve(current.locator.stateRoot) === path.resolve(expected.locator.stateRoot)
4661
4893
  );
4662
4894
  }
@@ -4720,13 +4952,15 @@ async function executeLifecycleResponse(
4720
4952
  if (operation === "session.create" || operation === "session.fork" || operation === "session.resume") {
4721
4953
  await broker.index.refresh();
4722
4954
  await broker.heartbeatSessions();
4955
+ await broker.index.refresh();
4723
4956
  if (operation === "session.resume") {
4724
4957
  const requestedSessionId = sessionId(input);
4725
- const existing = requestedSessionId
4726
- ? broker.index.listSessions().sessions.find(session => session.sessionId === requestedSessionId)
4727
- : undefined;
4728
- if (existing && !isSessionAuthorityEligible(existing))
4958
+ const authority = requestedSessionId
4959
+ ? liveResumeAuthority(broker.index.listSessions().sessions, requestedSessionId)
4960
+ : { kind: "none" as const };
4961
+ if (authority.kind === "ambiguous")
4729
4962
  return fail("endpoint_stale", "Session authority is ambiguous and cannot be resumed safely.");
4963
+ const existing = authority.kind === "live" ? authority.record : undefined;
4730
4964
  if (existing?.live) {
4731
4965
  const initialScope = await validateLiveResumeScope(broker, input, requestedSessionId!, existing);
4732
4966
  if ("ok" in initialScope) return initialScope;
@@ -4743,16 +4977,17 @@ async function executeLifecycleResponse(
4743
4977
  "live_session",
4744
4978
  "Session is already live but its incarnation-bound endpoint is unavailable.",
4745
4979
  );
4746
- await broker.index.refresh();
4747
- const current = broker.index
4748
- .listSessions()
4749
- .sessions.find(session => session.sessionId === requestedSessionId);
4750
- if (!current || !sameLiveResumeRecord(existing, current))
4751
- return fail("endpoint_stale", "Live session changed while its resume authority was being verified.");
4752
- const finalScope = await validateLiveResumeScope(broker, input, requestedSessionId!, current);
4980
+ const finalScope = await validateLiveResumeScope(broker, input, requestedSessionId!, existing);
4753
4981
  if ("ok" in finalScope) return finalScope;
4754
4982
  if (!sameResumeSessionIdentity(initialScope, finalScope))
4755
4983
  return fail("endpoint_stale", "Saved session changed while its resume authority was being verified.");
4984
+ await broker.index.refresh();
4985
+ const finalAuthority = liveResumeAuthority(broker.index.listSessions().sessions, requestedSessionId!);
4986
+ if (finalAuthority.kind === "ambiguous")
4987
+ return fail("endpoint_stale", "Session authority became ambiguous while it was being verified.");
4988
+ const current = finalAuthority.kind === "live" ? finalAuthority.record : undefined;
4989
+ if (!current || !sameLiveResumeRecord(existing, current))
4990
+ return fail("endpoint_stale", "Live session changed while its resume authority was being verified.");
4756
4991
  if (current.endpointMtimeMs === undefined)
4757
4992
  return fail("endpoint_stale", "Live session endpoint authority is incomplete.");
4758
4993
  return {
@@ -4792,8 +5027,16 @@ async function executeLifecycleResponse(
4792
5027
  const queueWaitMs = startupQueueWaitMs(requestedReadinessTimeoutMs);
4793
5028
  const launch = await launchInput(broker, operation, input);
4794
5029
  if ("ok" in launch) return launch;
5030
+ if (launch.worktreePlan && deadlineFieldsPresent(input)) {
5031
+ return fail(
5032
+ "invalid_input",
5033
+ "Lifecycle worktree launches cannot carry a caller-supplied child deadline tuple.",
5034
+ );
5035
+ }
4795
5036
  const admitted = await broker.runStartup(queueWaitMs, timing, async admittedAt => {
4796
- const admittedInput = { ...input, ...deriveLifecycleDeadlines(admittedAt, requestedReadinessTimeoutMs) };
5037
+ const admittedInput = launch.worktreePlan
5038
+ ? { ...input, admittedAt, requestedReadinessTimeoutMs }
5039
+ : { ...input, ...deriveLifecycleDeadlines(admittedAt, requestedReadinessTimeoutMs) };
4797
5040
  startupAdmittedInputs.add(admittedInput);
4798
5041
  startupLaunchInputs.set(admittedInput, launch);
4799
5042
  try {
@@ -4813,27 +5056,73 @@ async function executeLifecycleResponse(
4813
5056
  return fail("startup_admission_timeout", failure.message);
4814
5057
  }
4815
5058
 
4816
- const deadlines = lifecycleDeadlines(input, input.receivedAt as number);
4817
-
4818
- if ("ok" in deadlines) return deadlines;
4819
- const lifecycleDeadline = deadlines.lifecycleCleanupDeadlineAt;
4820
- const readinessDeadline = deadlines.semanticReadyDeadlineAt;
4821
- const terminationStartDeadline = deadlines.terminationStartDeadlineAt;
4822
-
4823
5059
  const launch = startupLaunchInputs.get(input) ?? (await launchInput(broker, operation, input));
4824
5060
  if ("ok" in launch) return launch;
5061
+ if (launch.worktreePlan && deadlineFieldsPresent(input) && input.admittedAt === undefined) {
5062
+ return fail(
5063
+ "invalid_input",
5064
+ "Lifecycle worktree launches cannot carry a caller-supplied child deadline tuple.",
5065
+ );
5066
+ }
5067
+
5068
+ let childDeadlines: LifecycleDeadlines | undefined;
5069
+ let lifecycleDeadline: number;
5070
+ let readinessDeadline: number;
5071
+ let terminationStartDeadline: number;
5072
+ let outerCleanupDeadlineAt: number | undefined;
5073
+ if (launch.worktreePlan) {
5074
+ const prepTimeouts = readPreparationTimeouts(input);
5075
+ if (!prepTimeouts.ok) return fail("invalid_input", PREPARATION_TIMEOUT_INVALID_MESSAGE);
5076
+ const admittedAt =
5077
+ typeof input.admittedAt === "number" && Number.isSafeInteger(input.admittedAt)
5078
+ ? input.admittedAt
5079
+ : timing.now();
5080
+ const requestedReadinessTimeoutMs =
5081
+ typeof input.requestedReadinessTimeoutMs === "number" &&
5082
+ isValidReadinessTimeoutMs(input.requestedReadinessTimeoutMs)
5083
+ ? input.requestedReadinessTimeoutMs
5084
+ : readinessTimeout(input);
5085
+ if (typeof requestedReadinessTimeoutMs !== "number") return requestedReadinessTimeoutMs;
5086
+ const outer = deriveLifecycleOuterDeadlines({
5087
+ admittedAt,
5088
+ worktreePrepTimeoutMs: prepTimeouts.worktreePrepTimeoutMs,
5089
+ dependencyPrepTimeoutMs: prepTimeouts.dependencyPrepTimeoutMs,
5090
+ requestedReadinessTimeoutMs,
5091
+ });
5092
+ outerCleanupDeadlineAt = outer.lifecycleCleanupDeadlineAt;
5093
+ lifecycleDeadline = outer.lifecycleCleanupDeadlineAt;
5094
+ readinessDeadline = outer.lifecycleCleanupDeadlineAt;
5095
+ terminationStartDeadline = outer.lifecycleCleanupDeadlineAt;
5096
+ } else {
5097
+ const deadlines = lifecycleDeadlines(input, input.receivedAt as number);
5098
+ if ("ok" in deadlines) return deadlines;
5099
+ childDeadlines = deadlines;
5100
+ lifecycleDeadline = deadlines.lifecycleCleanupDeadlineAt;
5101
+ readinessDeadline = deadlines.semanticReadyDeadlineAt;
5102
+ terminationStartDeadline = deadlines.terminationStartDeadlineAt;
5103
+ }
5104
+
4825
5105
  if (!hasProcessIncarnationAuthority())
4826
5106
  return fail(
4827
5107
  "incarnation_unavailable",
4828
5108
  "OS process incarnation authority is unavailable; refusing to spawn a lifecycle session.",
4829
5109
  );
5110
+ if (launch.worktreePlan) {
5111
+ const occupant = worktreeOccupant(broker.index.listSessions().sessions, launch.worktreePlan.worktreePath);
5112
+ if (occupant && occupant !== launch.id)
5113
+ return fail(
5114
+ "worktree_in_use",
5115
+ `The requested worktree is already held by session ${occupant}. Choose another worktree name or stop that session.`,
5116
+ );
5117
+ }
4830
5118
  const effectMarker = randomUUID();
4831
5119
  const plannedWorktreeIntent = worktreeIntent(launch.worktreePlan);
4832
5120
  const effectIntent: LifecycleEffectIntentWithDeadline = {
4833
5121
  sessionId: launch.id,
4834
5122
  stateRoot: launch.root,
4835
5123
  childOwnershipEstablished: false,
4836
- lifecycleCleanupDeadlineAt: deadlines.lifecycleCleanupDeadlineAt,
5124
+ lifecycleCleanupDeadlineAt:
5125
+ outerCleanupDeadlineAt ?? (childDeadlines as LifecycleDeadlines).lifecycleCleanupDeadlineAt,
4837
5126
  ...(plannedWorktreeIntent ? { worktree: plannedWorktreeIntent } : {}),
4838
5127
  };
4839
5128
 
@@ -4850,39 +5139,109 @@ async function executeLifecycleResponse(
4850
5139
  let worktreeReceipt: SessionLifecycleWorktreeReceipt | undefined;
4851
5140
  try {
4852
5141
  if (launch.worktreePlan) {
4853
- worktreeReceipt = preparePlannedWorktree(launch.worktreePlan);
4854
- const worktree = {
4855
- cwdDigest: createHash("sha256").update(worktreeReceipt.cwd, "utf8").digest("hex"),
4856
- created: worktreeReceipt.created,
4857
- reused: worktreeReceipt.reused,
4858
- ...(worktreeReceipt.branch
4859
- ? { branchDigest: createHash("sha256").update(worktreeReceipt.branch, "utf8").digest("hex") }
4860
- : {}),
4861
- };
4862
- const durableEffects: LifecycleDurableEffectsReceipt = {
4863
- worktree,
4864
- digest: createHash("sha256").update(canonicalJson({ worktree })).digest("hex"),
4865
- };
4866
- await broker.ledger.transition(identity, "effect_started", { durableEffects });
4867
- ensureReusableNodeModules(launch.worktreePlan.repoRoot, launch.worktreePlan.worktreePath);
5142
+ const prepTimeouts = readPreparationTimeouts(input);
5143
+ if (!prepTimeouts.ok) return fail("invalid_input", PREPARATION_TIMEOUT_INVALID_MESSAGE);
5144
+ const admittedAt =
5145
+ typeof input.admittedAt === "number" && Number.isSafeInteger(input.admittedAt)
5146
+ ? input.admittedAt
5147
+ : timing.now();
5148
+ const requestedReadinessTimeoutMs =
5149
+ typeof input.requestedReadinessTimeoutMs === "number" &&
5150
+ isValidReadinessTimeoutMs(input.requestedReadinessTimeoutMs)
5151
+ ? input.requestedReadinessTimeoutMs
5152
+ : readinessTimeout(input);
5153
+ if (typeof requestedReadinessTimeoutMs !== "number") return requestedReadinessTimeoutMs;
5154
+ const outer = deriveLifecycleOuterDeadlines({
5155
+ admittedAt,
5156
+ worktreePrepTimeoutMs: prepTimeouts.worktreePrepTimeoutMs,
5157
+ dependencyPrepTimeoutMs: prepTimeouts.dependencyPrepTimeoutMs,
5158
+ requestedReadinessTimeoutMs,
5159
+ });
5160
+ const controller = new AbortController();
5161
+ const worktreeStartedAt = timing.now();
5162
+ const worktreeWatch = abortWhenDue(controller, outer.worktreePreparationDeadlineAt, () => timing.now());
5163
+ try {
5164
+ worktreeReceipt = await preparePlannedWorktree(launch.worktreePlan, {
5165
+ signal: controller.signal,
5166
+ deadlineAt: outer.worktreePreparationDeadlineAt,
5167
+ now: () => timing.now(),
5168
+ });
5169
+ } finally {
5170
+ clearInterval(worktreeWatch);
5171
+ }
5172
+ const worktreeDoneAt = timing.now();
5173
+ const worktreePreparationMs = Math.max(0, worktreeDoneAt - worktreeStartedAt);
5174
+ await broker.ledger.transition(identity, "effect_started", {
5175
+ durableEffects: durableWorktreeEffects(worktreeReceipt, { worktreePreparationMs }),
5176
+ });
5177
+ const remainingDepBudget = Math.min(
5178
+ prepTimeouts.dependencyPrepTimeoutMs,
5179
+ Math.max(0, outer.lifecycleCleanupDeadlineAt - worktreeDoneAt - requestedReadinessTimeoutMs),
5180
+ );
5181
+ const dependencyDeadlineAt = worktreeDoneAt + remainingDepBudget;
5182
+ if (timing.now() >= dependencyDeadlineAt) throw new DependencyPreparationTimeoutError();
5183
+ const depWatch = abortWhenDue(controller, dependencyDeadlineAt, () => timing.now());
5184
+ const depHook = ensureReusableNodeModulesForTest;
5185
+ const dependencyStartedAt = timing.now();
5186
+ try {
5187
+ if (depHook) {
5188
+ await depHook(launch.worktreePlan.repoRoot, launch.worktreePlan.worktreePath, {
5189
+ signal: controller.signal,
5190
+ deadlineAt: dependencyDeadlineAt,
5191
+ });
5192
+ } else {
5193
+ await ensureReusableNodeModulesCancellable(
5194
+ launch.worktreePlan.repoRoot,
5195
+ launch.worktreePlan.worktreePath,
5196
+ {
5197
+ signal: controller.signal,
5198
+ deadlineAt: dependencyDeadlineAt,
5199
+ now: () => timing.now(),
5200
+ },
5201
+ );
5202
+ }
5203
+ } finally {
5204
+ clearInterval(depWatch);
5205
+ }
5206
+ const dependencyPreparationMs = Math.max(0, timing.now() - dependencyStartedAt);
5207
+ await broker.ledger.transition(identity, "effect_started", {
5208
+ durableEffects: durableWorktreeEffects(worktreeReceipt, {
5209
+ worktreePreparationMs,
5210
+ dependencyPreparationMs,
5211
+ spawnAuthorizedAtOffsetMs: Math.max(0, timing.now() - admittedAt),
5212
+ }),
5213
+ });
5214
+ const prepSucceededAt = timing.now();
5215
+ childDeadlines = deriveLifecycleDeadlines(prepSucceededAt, requestedReadinessTimeoutMs);
5216
+ readinessDeadline = childDeadlines.semanticReadyDeadlineAt;
5217
+ terminationStartDeadline = childDeadlines.terminationStartDeadlineAt;
5218
+ lifecycleDeadline = childDeadlines.lifecycleCleanupDeadlineAt;
4868
5219
  }
4869
5220
  await reapDeadLifecycleMarkers(launch.root);
4870
5221
  } catch (error) {
4871
- return fail(
4872
- "spawn_failed",
4873
- `Unable to prepare lifecycle worktree: ${error instanceof Error ? error.message : String(error)}`,
4874
- );
5222
+ if (launch.worktreePlan && worktreeReceipt?.created && !worktreeReceipt.reused) {
5223
+ removeOwnedLaunchWorktree(launch.worktreePlan, worktreeReceipt);
5224
+ }
5225
+ return mapPreparationFailure(error);
4875
5226
  }
4876
- if (timing.now() >= readinessDeadline)
5227
+ if (!launch.worktreePlan && timing.now() >= readinessDeadline)
4877
5228
  return fail(
4878
5229
  "readiness_timeout",
4879
5230
  "Lifecycle preparation exhausted the semantic readiness deadline before spawning.",
4880
5231
  );
5232
+ if (launch.worktreePlan && !childDeadlines)
5233
+ return fail(
5234
+ worktreeReceipt ? "dependency_preparation_timeout" : "worktree_preparation_timeout",
5235
+ worktreeReceipt
5236
+ ? "Dependency preparation exceeded its deadline before the session host was spawned."
5237
+ : "Worktree preparation exceeded its deadline before the session host was spawned.",
5238
+ );
4881
5239
  if (!hasProcessIncarnationAuthority())
4882
5240
  return fail(
4883
5241
  "incarnation_unavailable",
4884
5242
  "OS process incarnation authority is unavailable; refusing to spawn a lifecycle session.",
4885
5243
  );
5244
+ const deadlines = childDeadlines as LifecycleDeadlines;
4886
5245
 
4887
5246
  const coordinatorSidecarTarget =
4888
5247
  typeof launch.coordinatorStateDir === "string" && typeof launch.coordinatorSidecarKeyId === "string"
@@ -4933,7 +5292,13 @@ async function executeLifecycleResponse(
4933
5292
  // by the OS rather than captured (#4712 review).
4934
5293
  stdio: "ignore",
4935
5294
  env: {
4936
- ...("kind" in cmd ? cmd.env : process.env),
5295
+ // Master capability is process-local to direct Bash children and must
5296
+ // never cross a broker lifecycle launch boundary.
5297
+ ...Object.fromEntries(
5298
+ Object.entries("kind" in cmd ? cmd.env : process.env).filter(
5299
+ ([key]) => key !== "GJC_MASTER_CAPABILITY",
5300
+ ),
5301
+ ),
4937
5302
  GJC_AGENT_DIR: broker.settings.agentDir,
4938
5303
  GJC_CODING_AGENT_DIR: broker.settings.agentDir,
4939
5304
  GJC_SESSION_ID: launch.id,
@@ -5073,7 +5438,7 @@ async function executeLifecycleResponse(
5073
5438
  `Session ${launch.id} did not register an endpoint before the readiness timeout.`,
5074
5439
  );
5075
5440
  }
5076
- await reconcileReadyScope(broker, launch.id, launch.cwd);
5441
+ await reconcileReadyScope(broker, launch.id, launch.cwd, launch.root, spawnedAuthority);
5077
5442
  const verified = await currentReadyAuthority(broker, launch.id, launch.root, spawnedAuthority);
5078
5443
  if (!verified || !sameReadyAuthority(readiness.authority, verified)) {
5079
5444
  const exited =