@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
@@ -0,0 +1,591 @@
1
+ import * as crypto from "node:crypto";
2
+ import * as fs from "node:fs/promises";
3
+ import * as os from "node:os";
4
+ import * as path from "node:path";
5
+ import { logger } from "@gajae-code/utils";
6
+ import { nativeProcessBindings } from "@gajae-code/utils/native-process";
7
+ import { resolveGjcTmuxBinary } from "../../gjc-runtime/psmux-detect";
8
+ import { sessionRuntimeDir } from "../../gjc-runtime/session-layout";
9
+ import { resolveGjcTmuxProviderContext } from "../../gjc-runtime/tmux-provider-context";
10
+ import {
11
+ createManagedGjcTmuxSession,
12
+ forceCloseManagedGjcTmuxSession,
13
+ type ManagedTmuxLaunchProof,
14
+ verifyManagedGjcTmuxSession,
15
+ } from "../../gjc-runtime/tmux-sessions";
16
+ import { processIncarnation } from "./process-incarnation";
17
+ import type { SpawnSubstrateLaunchSpec, SpawnSubstrateProof, SpawnSubstrateProvider } from "./spawn-authority";
18
+
19
+ export type SpawnMultiplexerSelection = "none" | "tmux" | "psmux" | "proof_failed";
20
+
21
+ export interface SpawnHeadlessProcess {
22
+ pid: number;
23
+ terminate(): void;
24
+ }
25
+
26
+ export interface SpawnSubstrateProviderDependencies {
27
+ platform?: NodeJS.Platform;
28
+ env?: NodeJS.ProcessEnv;
29
+ /** @internal Test seam for deterministic provider selection. */
30
+ selectMultiplexer?: () => SpawnMultiplexerSelection;
31
+ /** @internal Test seam for the managed multiplexer command layer. */
32
+ launchManaged?: (
33
+ spec: SpawnSubstrateLaunchSpec,
34
+ env: NodeJS.ProcessEnv,
35
+ platform: NodeJS.Platform,
36
+ ) => ManagedTmuxLaunchProof;
37
+ verifyManaged?: (proof: ManagedTmuxLaunchProof, env: NodeJS.ProcessEnv) => "verified" | "mismatch" | "gone";
38
+ closeManaged?: (proof: ManagedTmuxLaunchProof, env: NodeJS.ProcessEnv) => Promise<void>;
39
+ processIncarnation?: (pid: number) => string | undefined;
40
+ startHeadless?: (spec: SpawnSubstrateLaunchSpec, env: NodeJS.ProcessEnv) => SpawnHeadlessProcess;
41
+ signalHeadless?: (
42
+ pid: number,
43
+ incarnation: string,
44
+ platform: NodeJS.Platform,
45
+ signal: "SIGTERM" | "SIGKILL",
46
+ ) => boolean;
47
+ isProcessGone?: (pid: number) => boolean;
48
+ /** @internal Test seam for bounded close polling. */
49
+ sleep?: (milliseconds: number) => Promise<void>;
50
+ /** @internal Test seam for non-secret inherited-environment launch diagnostics. */
51
+ onInheritedEnvironmentDrop?: (names: readonly string[]) => void;
52
+ }
53
+
54
+ type HeadlessState = {
55
+ version: 1;
56
+ pid: number;
57
+ processIncarnation: string;
58
+ providerIdentity: string;
59
+ childSessionId: string;
60
+ createdAt: number;
61
+ };
62
+
63
+ const MAX_SUBSTRATE_DIAGNOSTIC_LENGTH = 512;
64
+
65
+ const HEADLESS_STATE_KEYS = new Set([
66
+ "version",
67
+ "pid",
68
+ "processIncarnation",
69
+ "providerIdentity",
70
+ "childSessionId",
71
+ "createdAt",
72
+ ]);
73
+
74
+ const ENVIRONMENT_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
75
+ const HEADLESS_CLOSE_GRACE_MS = 2_000;
76
+ const HEADLESS_CLOSE_POLL_MS = 50;
77
+
78
+ function messageFor(code: "substrate_unavailable" | "substrate_proof_failed"): string {
79
+ return code === "substrate_unavailable"
80
+ ? "No safe spawn substrate is available."
81
+ : "The selected spawn substrate could not be proven exactly.";
82
+ }
83
+
84
+ function launchFailureMessage(selected: "tmux" | "psmux" | "headless", error: unknown): string {
85
+ const detail = error instanceof Error ? error.message : String(error);
86
+ const bounded = detail
87
+ .replace(/[\u0000-\u001f\u007f]/g, " ")
88
+ .trim()
89
+ .slice(0, MAX_SUBSTRATE_DIAGNOSTIC_LENGTH);
90
+ return `${selected} substrate launch failed${bounded ? `: ${bounded}` : ""}`;
91
+ }
92
+
93
+ function cleanupFailureMessage(selected: "tmux" | "psmux", error: unknown): string {
94
+ const detail = error instanceof Error ? error.message : String(error);
95
+ const bounded = detail
96
+ .replace(/[\u0000-\u001f\u007f]/g, " ")
97
+ .trim()
98
+ .slice(0, MAX_SUBSTRATE_DIAGNOSTIC_LENGTH);
99
+ return `${selected} substrate cleanup failed${bounded ? `: ${bounded}` : ""}`;
100
+ }
101
+
102
+ function cleanupUncertaintyMessage(selected: "tmux" | "psmux"): string {
103
+ return `${selected} substrate cleanup could not be verified`;
104
+ }
105
+
106
+ function cleanupWasVerifiedAfterManagedLaunchFailure(error: unknown): boolean {
107
+ if (error instanceof AggregateError) return false;
108
+ const detail = error instanceof Error ? error.message : String(error);
109
+ // The owner-isolation layer reports a failed tmux allocation before a
110
+ // managed session exists. That is the only managed launch failure that is
111
+ // safe to replace with the exact headless substrate. Every other thrown
112
+ // error may follow a partial create/proof path and therefore needs explicit
113
+ // cleanup evidence instead of optimistic fallback.
114
+ return detail.includes("planned_spawn_failed") && !detail.includes("cleanup_uncertain");
115
+ }
116
+
117
+ function hasOnlyKeys(value: Record<string, unknown>, keys: Set<string>): boolean {
118
+ return Object.keys(value).every(key => keys.has(key));
119
+ }
120
+
121
+ function isNonEmptyString(value: unknown): value is string {
122
+ return typeof value === "string" && value.length > 0 && value.length <= 4096;
123
+ }
124
+
125
+ /** An environment VALUE may be empty but is still length-bounded. */
126
+ function isBoundedString(value: unknown): value is string {
127
+ return typeof value === "string" && value.length <= 4096;
128
+ }
129
+
130
+ function isPositiveInteger(value: unknown): value is number {
131
+ return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
132
+ }
133
+
134
+ function isTimestamp(value: unknown): value is number {
135
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
136
+ }
137
+
138
+ function validLaunchSpec(spec: SpawnSubstrateLaunchSpec, platform: NodeJS.Platform): boolean {
139
+ const pathModule = platform === "win32" ? path.win32 : path;
140
+ return (
141
+ isNonEmptyString(spec.childSessionId) &&
142
+ !spec.childSessionId.includes("\0") &&
143
+ isNonEmptyString(spec.cwd) &&
144
+ !spec.cwd.includes("\0") &&
145
+ pathModule.isAbsolute(spec.cwd) &&
146
+ Array.isArray(spec.argv) &&
147
+ spec.argv.length > 0 &&
148
+ spec.argv.every(value => isNonEmptyString(value) && !value.includes("\0")) &&
149
+ (spec.env === undefined ||
150
+ Object.entries(spec.env).every(
151
+ ([name, value]) => ENVIRONMENT_NAME.test(name) && isBoundedString(value) && !value.includes("\0"),
152
+ ))
153
+ );
154
+ }
155
+
156
+ function filterInheritedEnvironment(environment: NodeJS.ProcessEnv | Readonly<Record<string, string>>): {
157
+ environment: NodeJS.ProcessEnv;
158
+ dropped: string[];
159
+ } {
160
+ const filtered: NodeJS.ProcessEnv = {};
161
+ const dropped: string[] = [];
162
+ for (const [name, value] of Object.entries(environment).sort(([left], [right]) =>
163
+ left < right ? -1 : left > right ? 1 : 0,
164
+ )) {
165
+ if (ENVIRONMENT_NAME.test(name) && isBoundedString(value) && !value.includes("\0")) filtered[name] = value;
166
+ else dropped.push(name);
167
+ }
168
+ return { environment: filtered, dropped };
169
+ }
170
+
171
+ function isHeadlessState(value: unknown): value is HeadlessState {
172
+ return (
173
+ typeof value === "object" &&
174
+ value !== null &&
175
+ hasOnlyKeys(value as Record<string, unknown>, HEADLESS_STATE_KEYS) &&
176
+ (value as { version?: unknown }).version === 1 &&
177
+ isPositiveInteger((value as { pid?: unknown }).pid) &&
178
+ isNonEmptyString((value as { processIncarnation?: unknown }).processIncarnation) &&
179
+ isNonEmptyString((value as { providerIdentity?: unknown }).providerIdentity) &&
180
+ isNonEmptyString((value as { childSessionId?: unknown }).childSessionId) &&
181
+ isTimestamp((value as { createdAt?: unknown }).createdAt)
182
+ );
183
+ }
184
+
185
+ function stateFileFor(spec: SpawnSubstrateLaunchSpec, providerIdentity: string): string {
186
+ return path.join(sessionRuntimeDir(spec.cwd, spec.childSessionId), "spawn-substrates", `${providerIdentity}.json`);
187
+ }
188
+
189
+ async function writeHeadlessState(file: string, value: HeadlessState): Promise<void> {
190
+ await fs.mkdir(path.dirname(file), { recursive: true, mode: 0o700 });
191
+ const handle = await fs.open(file, "wx", 0o600);
192
+ try {
193
+ await handle.writeFile(`${JSON.stringify(value)}\n`);
194
+ await handle.sync();
195
+ } finally {
196
+ await handle.close();
197
+ }
198
+ const directory = await fs.open(path.dirname(file), "r");
199
+ try {
200
+ await directory.sync();
201
+ } finally {
202
+ await directory.close();
203
+ }
204
+ }
205
+
206
+ async function readHeadlessState(file: string): Promise<HeadlessState | null> {
207
+ try {
208
+ const parsed: unknown = JSON.parse(await fs.readFile(file, "utf8"));
209
+ return isHeadlessState(parsed) ? parsed : null;
210
+ } catch {
211
+ return null;
212
+ }
213
+ }
214
+
215
+ function commandAvailable(command: string): boolean {
216
+ return Bun.which(command) !== null;
217
+ }
218
+
219
+ function defaultMultiplexerSelection(platform: NodeJS.Platform, env: NodeJS.ProcessEnv): SpawnMultiplexerSelection {
220
+ try {
221
+ if (platform === "darwin" || platform === "linux") {
222
+ const binary = resolveGjcTmuxBinary({ env, platform });
223
+ if (!commandAvailable(binary.command)) return "none";
224
+ if (binary.isPsmux) return "proof_failed";
225
+ const provider = resolveGjcTmuxProviderContext({ env, platform, binary });
226
+ return provider.kind === "native-tmux" ? "tmux" : "proof_failed";
227
+ }
228
+ if (platform !== "win32") return "none";
229
+ const binary = resolveGjcTmuxBinary({ env, platform });
230
+ if (!commandAvailable(binary.command)) return "none";
231
+ if (!binary.isPsmux) return env.GJC_TMUX_COMMAND?.trim() ? "proof_failed" : "none";
232
+ const provider = resolveGjcTmuxProviderContext({ env, platform, binary });
233
+ return provider.kind === "windows-psmux" ? "psmux" : "proof_failed";
234
+ } catch {
235
+ return "proof_failed";
236
+ }
237
+ }
238
+
239
+ function startHeadless(spec: SpawnSubstrateLaunchSpec, env: NodeJS.ProcessEnv): SpawnHeadlessProcess {
240
+ const child = Bun.spawn({
241
+ cmd: [...spec.argv],
242
+ cwd: spec.cwd,
243
+ env,
244
+ stdin: "ignore",
245
+ stdout: "ignore",
246
+ stderr: "ignore",
247
+ });
248
+ return { pid: child.pid, terminate: () => child.kill() };
249
+ }
250
+
251
+ function defaultIsProcessGone(pid: number): boolean {
252
+ try {
253
+ process.kill(pid, 0);
254
+ return false;
255
+ } catch (error) {
256
+ return typeof error === "object" && error !== null && "code" in error && error.code === "ESRCH";
257
+ }
258
+ }
259
+
260
+ function signalExactHeadless(
261
+ pid: number,
262
+ expectedIncarnation: string,
263
+ platform: NodeJS.Platform,
264
+ signal: "SIGTERM" | "SIGKILL",
265
+ ): boolean {
266
+ try {
267
+ const reference = nativeProcessBindings().Process.fromPid(pid);
268
+ if (!reference || reference.incarnation !== expectedIncarnation) return false;
269
+ if (platform === "darwin") {
270
+ const current = nativeProcessBindings().Process.fromPid(pid);
271
+ if (!current || current.incarnation !== expectedIncarnation) return false;
272
+ process.kill(pid, signal);
273
+ return true;
274
+ }
275
+ const rootProcess = reference as typeof reference & { signalRoot(signal: number): boolean };
276
+ const signalNumber = signal === "SIGTERM" ? os.constants.signals.SIGTERM : os.constants.signals.SIGKILL;
277
+ return signalNumber !== undefined && rootProcess.signalRoot(signalNumber);
278
+ } catch {
279
+ return false;
280
+ }
281
+ }
282
+
283
+ function readStateProof(proof: SpawnSubstrateProof): { stateFile: string; childSessionId: string } | null {
284
+ const stateFile = proof.stateFileProof?.stateFile;
285
+ const childSessionId = proof.stateFileProof?.childSessionId;
286
+ return isNonEmptyString(stateFile) && isNonEmptyString(childSessionId) ? { stateFile, childSessionId } : null;
287
+ }
288
+
289
+ function managedProofFrom(proof: SpawnSubstrateProof): ManagedTmuxLaunchProof | null {
290
+ if (proof.substrateKind !== "tmux" && proof.substrateKind !== "psmux") return null;
291
+ const stateProof = proof.stateFileProof;
292
+ const name = stateProof?.sessionName;
293
+ const sessionId = stateProof?.sessionId;
294
+ const sessionStateFile = stateProof?.sessionStateFile;
295
+ const ownerGeneration = stateProof?.ownerGeneration;
296
+ const serverPid = stateProof?.serverPid;
297
+ const serverStartTime = stateProof?.serverStartTime;
298
+ const psmuxIncarnation = stateProof?.psmuxIncarnation;
299
+ const validatedPsmuxIncarnation = isNonEmptyString(psmuxIncarnation) ? psmuxIncarnation : undefined;
300
+ if (
301
+ !isNonEmptyString(proof.nativeSessionId) ||
302
+ !isPositiveInteger(proof.pid) ||
303
+ !isNonEmptyString(name) ||
304
+ !isNonEmptyString(sessionId) ||
305
+ !isNonEmptyString(sessionStateFile) ||
306
+ !isNonEmptyString(ownerGeneration) ||
307
+ !isPositiveInteger(serverPid) ||
308
+ !isNonEmptyString(serverStartTime) ||
309
+ (proof.substrateKind === "psmux" && validatedPsmuxIncarnation === undefined) ||
310
+ (proof.substrateKind === "tmux" && psmuxIncarnation !== undefined)
311
+ )
312
+ return null;
313
+ return {
314
+ name,
315
+ nativeSessionId: proof.nativeSessionId,
316
+ serverPid,
317
+ serverStartTime,
318
+ ownerGeneration,
319
+ sessionId,
320
+ sessionStateFile,
321
+ pid: proof.pid,
322
+ providerIdentity: proof.providerIdentity,
323
+ ...(validatedPsmuxIncarnation === undefined ? {} : { psmuxIncarnation: validatedPsmuxIncarnation }),
324
+ };
325
+ }
326
+
327
+ function headlessProofMatches(proof: SpawnSubstrateProof, state: HeadlessState): boolean {
328
+ const stateProof = readStateProof(proof);
329
+ return (
330
+ proof.substrateKind === "headless" &&
331
+ isPositiveInteger(proof.pid) &&
332
+ isNonEmptyString(proof.processIncarnation) &&
333
+ stateProof !== null &&
334
+ proof.pid === state.pid &&
335
+ proof.processIncarnation === state.processIncarnation &&
336
+ proof.providerIdentity === state.providerIdentity &&
337
+ stateProof.childSessionId === state.childSessionId
338
+ );
339
+ }
340
+
341
+ async function waitForHeadlessExit(
342
+ proof: SpawnSubstrateProof,
343
+ verify: (proof: SpawnSubstrateProof) => Promise<"verified" | "mismatch" | "gone">,
344
+ sleep: (milliseconds: number) => Promise<void>,
345
+ ): Promise<"verified" | "mismatch" | "gone"> {
346
+ let verdict = await verify(proof);
347
+ if (verdict !== "verified") return verdict;
348
+ const polls = Math.ceil(HEADLESS_CLOSE_GRACE_MS / HEADLESS_CLOSE_POLL_MS);
349
+ for (let attempt = 0; attempt < polls; attempt++) {
350
+ await sleep(HEADLESS_CLOSE_POLL_MS);
351
+ verdict = await verify(proof);
352
+ if (verdict !== "verified") return verdict;
353
+ }
354
+ return verdict;
355
+ }
356
+
357
+ function managedStateProof(proof: ManagedTmuxLaunchProof): Readonly<Record<string, string | number>> {
358
+ return {
359
+ sessionName: proof.name,
360
+ sessionId: proof.sessionId,
361
+ sessionStateFile: proof.sessionStateFile,
362
+ ownerGeneration: proof.ownerGeneration,
363
+ serverPid: proof.serverPid,
364
+ serverStartTime: proof.serverStartTime,
365
+ ...(proof.psmuxIncarnation === undefined ? {} : { psmuxIncarnation: proof.psmuxIncarnation }),
366
+ };
367
+ }
368
+
369
+ /**
370
+ * Provides the Broker's only substrate authority. Every close and replay starts
371
+ * from its durable opaque proof; no PID/name-only operation is exposed.
372
+ */
373
+ export function createSpawnSubstrateProvider(
374
+ dependencies: SpawnSubstrateProviderDependencies = {},
375
+ ): SpawnSubstrateProvider {
376
+ const platform = dependencies.platform ?? process.platform;
377
+ const env = dependencies.env ?? process.env;
378
+ const selectMultiplexer = dependencies.selectMultiplexer ?? (() => defaultMultiplexerSelection(platform, env));
379
+ const launchManaged =
380
+ dependencies.launchManaged ??
381
+ ((spec, launchEnv, launchPlatform) => createManagedGjcTmuxSession(spec, launchEnv, { platform: launchPlatform }));
382
+ const verifyManaged = dependencies.verifyManaged ?? verifyManagedGjcTmuxSession;
383
+ const closeManaged =
384
+ dependencies.closeManaged ??
385
+ (async (proof, closeEnv) => {
386
+ await forceCloseManagedGjcTmuxSession(proof, closeEnv);
387
+ });
388
+ const readIncarnation = dependencies.processIncarnation ?? processIncarnation;
389
+ const launchHeadless = dependencies.startHeadless ?? startHeadless;
390
+ const signalHeadless = dependencies.signalHeadless ?? signalExactHeadless;
391
+ const isGone = dependencies.isProcessGone ?? defaultIsProcessGone;
392
+ const sleep = dependencies.sleep ?? (milliseconds => Bun.sleep(milliseconds));
393
+ const launchHeadlessSubstrate = async (spec: SpawnSubstrateLaunchSpec, launchEnv: NodeJS.ProcessEnv) => {
394
+ let child: SpawnHeadlessProcess;
395
+ try {
396
+ child = launchHeadless(spec, launchEnv);
397
+ } catch {
398
+ return {
399
+ ok: false as const,
400
+ code: "substrate_unavailable" as const,
401
+ message: messageFor("substrate_unavailable"),
402
+ };
403
+ }
404
+ const incarnation = readIncarnation(child.pid);
405
+ if (!incarnation) {
406
+ child.terminate();
407
+ return {
408
+ ok: false as const,
409
+ code: "substrate_proof_failed" as const,
410
+ message: messageFor("substrate_proof_failed"),
411
+ };
412
+ }
413
+ const providerIdentity = crypto.randomUUID();
414
+ const state: HeadlessState = {
415
+ version: 1,
416
+ pid: child.pid,
417
+ processIncarnation: incarnation,
418
+ providerIdentity,
419
+ childSessionId: spec.childSessionId,
420
+ createdAt: Date.now(),
421
+ };
422
+ const stateFile = stateFileFor(spec, providerIdentity);
423
+ try {
424
+ await writeHeadlessState(stateFile, state);
425
+ } catch {
426
+ child.terminate();
427
+ return {
428
+ ok: false as const,
429
+ code: "substrate_proof_failed" as const,
430
+ message: messageFor("substrate_proof_failed"),
431
+ };
432
+ }
433
+ return {
434
+ ok: true as const,
435
+ proof: {
436
+ substrateKind: "headless" as const,
437
+ providerIdentity,
438
+ pid: child.pid,
439
+ processIncarnation: incarnation,
440
+ stateFileProof: { stateFile, childSessionId: spec.childSessionId },
441
+ },
442
+ };
443
+ };
444
+
445
+ const provider: SpawnSubstrateProvider = {
446
+ async launch(spec) {
447
+ if (!validLaunchSpec(spec, platform))
448
+ return { ok: false, code: "substrate_proof_failed", message: messageFor("substrate_proof_failed") };
449
+ const inherited = filterInheritedEnvironment(spec.inheritedEnv ?? env);
450
+ if (inherited.dropped.length > 0) {
451
+ if (dependencies.onInheritedEnvironmentDrop) dependencies.onInheritedEnvironmentDrop(inherited.dropped);
452
+ else
453
+ logger.warn("sdk broker dropped unsupported inherited child environment entries", {
454
+ names: inherited.dropped,
455
+ });
456
+ }
457
+ const launchEnv: NodeJS.ProcessEnv = { ...inherited.environment, ...(spec.env ?? {}) };
458
+ const selected = selectMultiplexer();
459
+ if (selected === "proof_failed")
460
+ return { ok: false, code: "substrate_proof_failed", message: messageFor("substrate_proof_failed") };
461
+ if (selected === "tmux" || selected === "psmux") {
462
+ let managed: ManagedTmuxLaunchProof;
463
+ try {
464
+ managed = launchManaged(spec, launchEnv, platform);
465
+ } catch (error) {
466
+ const failure = launchFailureMessage(selected, error);
467
+ if (!cleanupWasVerifiedAfterManagedLaunchFailure(error))
468
+ return {
469
+ ok: false,
470
+ code: "substrate_proof_failed",
471
+ message: `${failure}; ${
472
+ error instanceof AggregateError
473
+ ? cleanupFailureMessage(selected, error)
474
+ : cleanupUncertaintyMessage(selected)
475
+ }`,
476
+ };
477
+ try {
478
+ const fallback = await launchHeadlessSubstrate(spec, launchEnv);
479
+ if (fallback.ok) return fallback;
480
+ return {
481
+ ok: false,
482
+ code: fallback.code,
483
+ message: `${failure}; headless substrate failed: ${fallback.message}`,
484
+ };
485
+ } catch (fallbackError) {
486
+ return {
487
+ ok: false,
488
+ code: "substrate_unavailable",
489
+ message: `${failure}; headless substrate failed: ${launchFailureMessage("headless", fallbackError)}`,
490
+ };
491
+ }
492
+ }
493
+ const incarnation = readIncarnation(managed.pid);
494
+ if (!incarnation || verifyManaged(managed, launchEnv) !== "verified") {
495
+ let cleanupVerified = false;
496
+ let cleanupError: unknown;
497
+ try {
498
+ await closeManaged(managed, launchEnv);
499
+ cleanupVerified = true;
500
+ } catch (error) {
501
+ cleanupError = error;
502
+ }
503
+ const failure = launchFailureMessage(selected, new Error("exact substrate proof failed"));
504
+ if (!cleanupVerified)
505
+ return {
506
+ ok: false,
507
+ code: "substrate_proof_failed",
508
+ message: `${failure}; ${cleanupFailureMessage(selected, cleanupError)}`,
509
+ };
510
+ const fallback = await launchHeadlessSubstrate(spec, launchEnv);
511
+ if (fallback.ok) return fallback;
512
+ return {
513
+ ok: false,
514
+ code: fallback.code,
515
+ message: `${failure}; headless substrate failed: ${fallback.message}`,
516
+ };
517
+ }
518
+ return {
519
+ ok: true,
520
+ proof: {
521
+ substrateKind: selected,
522
+ providerIdentity: managed.providerIdentity,
523
+ nativeSessionId: managed.nativeSessionId,
524
+ pid: managed.pid,
525
+ processIncarnation: incarnation,
526
+ stateFileProof: managedStateProof(managed),
527
+ },
528
+ };
529
+ }
530
+ return launchHeadlessSubstrate(spec, launchEnv);
531
+ },
532
+ async verify(proof) {
533
+ const managed = managedProofFrom(proof);
534
+ if (managed) {
535
+ const verdict = verifyManaged(managed, env);
536
+ if (verdict !== "verified") return verdict;
537
+ const incarnation = proof.pid === undefined ? undefined : readIncarnation(proof.pid);
538
+ if (!incarnation) return proof.pid !== undefined && isGone(proof.pid) ? "gone" : "mismatch";
539
+ return incarnation === proof.processIncarnation ? "verified" : "mismatch";
540
+ }
541
+ if (proof.substrateKind !== "headless") return "mismatch";
542
+ const stateProof = readStateProof(proof);
543
+ if (!stateProof) return "mismatch";
544
+ const state = await readHeadlessState(stateProof.stateFile);
545
+ if (!state || !headlessProofMatches(proof, state)) return "mismatch";
546
+ const incarnation = proof.pid === undefined ? undefined : readIncarnation(proof.pid);
547
+ if (!incarnation) return proof.pid !== undefined && isGone(proof.pid) ? "gone" : "mismatch";
548
+ return incarnation === proof.processIncarnation ? "verified" : "mismatch";
549
+ },
550
+ async close(proof) {
551
+ const verification = await provider.verify(proof);
552
+ if (verification !== "verified") return { ok: false, code: `substrate_${verification}` };
553
+ const managed = managedProofFrom(proof);
554
+ if (managed) {
555
+ try {
556
+ await closeManaged(managed, env);
557
+ return { ok: true };
558
+ } catch {
559
+ return { ok: false, code: "substrate_close_failed" };
560
+ }
561
+ }
562
+ if (!isPositiveInteger(proof.pid) || !isNonEmptyString(proof.processIncarnation))
563
+ return { ok: false, code: "substrate_mismatch" };
564
+ const pid = proof.pid;
565
+ const closeFailure = (verdict: "verified" | "mismatch" | "gone") => ({
566
+ ok: false,
567
+ code: verdict === "verified" ? "substrate_close_pending" : `substrate_${verdict}`,
568
+ });
569
+ const verifyAfterSignal = async (): Promise<"verified" | "mismatch" | "gone"> => {
570
+ const verdict = await provider.verify(proof);
571
+ if (verdict === "mismatch" && readIncarnation(pid) === undefined && isGone(pid)) return "gone";
572
+ return verdict;
573
+ };
574
+ // Delivery starts shutdown; only a later exact proof of absence completes it.
575
+ if (!signalHeadless(proof.pid, proof.processIncarnation, platform, "SIGTERM")) {
576
+ const afterSignalFailure = await verifyAfterSignal();
577
+ return afterSignalFailure === "gone" ? { ok: true } : closeFailure(afterSignalFailure);
578
+ }
579
+ const afterTerm = await waitForHeadlessExit(proof, verifyAfterSignal, sleep);
580
+ if (afterTerm === "gone") return { ok: true };
581
+ if (afterTerm !== "verified") return closeFailure(afterTerm);
582
+ if (!signalHeadless(proof.pid, proof.processIncarnation, platform, "SIGKILL")) {
583
+ const afterSignalFailure = await verifyAfterSignal();
584
+ return afterSignalFailure === "gone" ? { ok: true } : closeFailure(afterSignalFailure);
585
+ }
586
+ const afterKill = await waitForHeadlessExit(proof, verifyAfterSignal, sleep);
587
+ return afterKill === "gone" ? { ok: true } : closeFailure(afterKill);
588
+ },
589
+ };
590
+ return provider;
591
+ }