@gajae-code/coding-agent 0.18.8 → 0.18.10

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 (113) hide show
  1. package/CHANGELOG.md +100 -0
  2. package/dist/types/async/endpoint-id.d.ts +16 -0
  3. package/dist/types/async/support.d.ts +0 -14
  4. package/dist/types/commands/mcp-serve.d.ts +4 -0
  5. package/dist/types/discovery/mcp-json.d.ts +1 -0
  6. package/dist/types/eval/py/kernel.d.ts +18 -0
  7. package/dist/types/export/html/html-export-destination.d.ts +2 -0
  8. package/dist/types/extensibility/extensions/function-hooks-internal.d.ts +5 -0
  9. package/dist/types/extensibility/skill-management.d.ts +1 -0
  10. package/dist/types/gjc-runtime/psmux-detect.d.ts +3 -3
  11. package/dist/types/gjc-runtime/sidecar-bootstrap-env.d.ts +6 -0
  12. package/dist/types/gjc-runtime/state-writer.d.ts +3 -0
  13. package/dist/types/gjc-runtime/tmux-common.d.ts +8 -5
  14. package/dist/types/lsp/config.d.ts +8 -5
  15. package/dist/types/runtime-mcp/manager.d.ts +2 -1
  16. package/dist/types/runtime-mcp/mcp-redirect.d.ts +9 -0
  17. package/dist/types/runtime-mcp/oauth-public-url.d.ts +3 -1
  18. package/dist/types/runtime-mcp/project-host-values.d.ts +27 -0
  19. package/dist/types/sdk/broker/broker.d.ts +6 -1
  20. package/dist/types/sdk/broker/doctor-restart.d.ts +5 -0
  21. package/dist/types/sdk/broker/ensure.d.ts +14 -1
  22. package/dist/types/session/session-storage.d.ts +7 -0
  23. package/dist/types/skill-state/workflow-mutation-guard.d.ts +3 -0
  24. package/dist/types/task/isolation-dir.d.ts +26 -0
  25. package/dist/types/tools/ast-edit-write-path.d.ts +13 -0
  26. package/dist/types/tools/browser/cdp-url.d.ts +2 -0
  27. package/dist/types/tools/github-side-effect.d.ts +22 -0
  28. package/dist/types/tools/monitor-mutation.d.ts +9 -0
  29. package/package.json +7 -7
  30. package/src/async/endpoint-id.ts +26 -0
  31. package/src/async/job-manager.ts +19 -10
  32. package/src/async/support.ts +0 -25
  33. package/src/capability/index.ts +14 -16
  34. package/src/cli/doctor/repairs.ts +4 -1
  35. package/src/cli/doctor-supervisor.ts +3 -0
  36. package/src/cli/public-command-registry.ts +28 -6
  37. package/src/commands/mcp-serve.ts +7 -0
  38. package/src/commands/sdk.ts +453 -415
  39. package/src/config/file-lock.ts +9 -11
  40. package/src/config/model-profile-activation.ts +5 -5
  41. package/src/coordinator-mcp/server.ts +2 -0
  42. package/src/customization/inventory.ts +1 -13
  43. package/src/dap/config.ts +2 -2
  44. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.ts +10 -6
  45. package/src/discovery/builtin.ts +3 -1
  46. package/src/discovery/helpers.ts +12 -9
  47. package/src/discovery/mcp-json.ts +20 -12
  48. package/src/eval/py/executor.ts +459 -77
  49. package/src/eval/py/kernel.ts +79 -13
  50. package/src/eval/py/tool-bridge.ts +4 -0
  51. package/src/export/html/html-export-destination.ts +16 -0
  52. package/src/export/html/index.ts +2 -6
  53. package/src/extensibility/extensions/function-hooks-internal.ts +27 -0
  54. package/src/extensibility/extensions/runner.ts +11 -1
  55. package/src/extensibility/gjc-plugins/mcp-policy.ts +2 -1
  56. package/src/extensibility/gjc-plugins/runtime-adapters.ts +27 -15
  57. package/src/extensibility/skill-management.ts +3 -0
  58. package/src/gjc-runtime/launch-worktree.ts +56 -5
  59. package/src/gjc-runtime/psmux-detect.ts +28 -4
  60. package/src/gjc-runtime/session-state-sidecar.ts +3 -2
  61. package/src/gjc-runtime/sidecar-bootstrap-env.ts +19 -0
  62. package/src/gjc-runtime/state-writer.ts +22 -8
  63. package/src/gjc-runtime/tmux-common.ts +8 -5
  64. package/src/internal-urls/docs-index.generated.ts +7 -7
  65. package/src/lsp/config.ts +12 -63
  66. package/src/memories/index.ts +88 -7
  67. package/src/modes/components/hook-editor.ts +1 -1
  68. package/src/modes/components/hook-selector.ts +1 -1
  69. package/src/modes/controllers/input-controller.ts +2 -2
  70. package/src/modes/controllers/runtime-mcp-command-controller.ts +21 -13
  71. package/src/modes/controllers/selector-controller.ts +3 -7
  72. package/src/modes/controllers/todo-command-controller.ts +1 -1
  73. package/src/modes/interactive-mode.ts +1 -1
  74. package/src/runtime-mcp/client.ts +4 -2
  75. package/src/runtime-mcp/json-rpc.ts +2 -1
  76. package/src/runtime-mcp/manager.ts +29 -11
  77. package/src/runtime-mcp/mcp-redirect.ts +73 -0
  78. package/src/runtime-mcp/oauth-flow.ts +11 -4
  79. package/src/runtime-mcp/oauth-public-url.ts +8 -2
  80. package/src/runtime-mcp/pool.ts +6 -0
  81. package/src/runtime-mcp/project-host-values.ts +46 -0
  82. package/src/runtime-mcp/transports/http.ts +2 -1
  83. package/src/sdk/acp/adapter.ts +2 -0
  84. package/src/sdk/broker/broker.ts +6 -0
  85. package/src/sdk/broker/doctor-restart.ts +4 -0
  86. package/src/sdk/broker/ensure.ts +29 -2
  87. package/src/sdk/host/session-runtime.test.ts +58 -9
  88. package/src/sdk/host/session-runtime.ts +65 -35
  89. package/src/sdk/router/session-router.ts +7 -6
  90. package/src/sdk/session.ts +2 -7
  91. package/src/session/agent-session.ts +59 -17
  92. package/src/session/artifacts.ts +3 -2
  93. package/src/session/internal/managed-session-scope.ts +403 -81
  94. package/src/session/internal/managed-session-storage.ts +125 -17
  95. package/src/session/session-manager.ts +12 -0
  96. package/src/session/session-storage.ts +30 -1
  97. package/src/skill-state/active-state.ts +13 -0
  98. package/src/skill-state/workflow-mutation-guard.ts +102 -10
  99. package/src/slash-commands/builtin-registry.ts +1 -1
  100. package/src/slash-commands/helpers/mcp.ts +8 -4
  101. package/src/task/isolation-dir.ts +143 -0
  102. package/src/task/worktree.ts +16 -7
  103. package/src/tools/ast-edit-write-path.ts +68 -0
  104. package/src/tools/ast-edit.ts +7 -2
  105. package/src/tools/browser/cdp-url.ts +18 -0
  106. package/src/tools/browser.ts +8 -1
  107. package/src/tools/gh.ts +10 -0
  108. package/src/tools/github-side-effect.ts +112 -0
  109. package/src/tools/monitor-mutation.ts +27 -0
  110. package/src/tools/monitor.ts +7 -0
  111. package/src/tools/python.ts +7 -6
  112. package/src/utils/external-editor.ts +3 -2
  113. package/src/web/insane/url-guard.ts +2 -1
@@ -10,7 +10,7 @@ import {
10
10
  type NativeDirectoryTreeSnapshot,
11
11
  snapshotDirectoryTree,
12
12
  } from "@gajae-code/natives";
13
- import { logger, postmortem } from "@gajae-code/utils";
13
+ import { getAgentDir, logger, postmortem } from "@gajae-code/utils";
14
14
  import { CliParseError, Command } from "@gajae-code/utils/cli";
15
15
  import type { Args as ParsedArgs } from "../cli/args";
16
16
  import { isSafeSdkInternalAgentDir, scanPublicCommand } from "../cli/public-command-entry";
@@ -35,6 +35,7 @@ import {
35
35
  BROKER_DISCOVERY_BUDGET,
36
36
  emitBrokerStartupTestSignal,
37
37
  reconcileBrokerGenerationForStartup,
38
+ SDK_BROKER_AUTOSTART_ENV,
38
39
  withBrokerStartupLock,
39
40
  } from "../sdk/broker/ensure";
40
41
  import {
@@ -1604,7 +1605,16 @@ export default class Sdk extends Command {
1604
1605
  const scan = scanPublicCommand("sdk", this.argv);
1605
1606
  if (scan.kind !== "operation") throw new PublicCommandFailure({ kind: "usage", proof: "pre-effect" });
1606
1607
  const { args, flags } = scan;
1608
+ // `--attach-only` is the flag form of GJC_SDK_BROKER_AUTOSTART=0 for this invocation.
1609
+ if (flags["attach-only"] === true) process.env[SDK_BROKER_AUTOSTART_ENV] = "0";
1607
1610
  const operation = scan.descriptor.command[1];
1611
+ if (operation === "broker") {
1612
+ const brokerAgentDir = path.resolve((flags["agent-dir"] as string | undefined) ?? getAgentDir());
1613
+ if (!isSafeSdkInternalAgentDir(brokerAgentDir))
1614
+ throw new PublicCommandFailure({ kind: "usage", proof: "pre-effect" });
1615
+ await runSdkInternal({ action: "broker-internal", agentDir: brokerAgentDir }, true);
1616
+ return;
1617
+ }
1608
1618
  const stringFlag = (name: string): string | undefined => flags[name] as string | undefined;
1609
1619
  const timeoutMs = flags["timeout-ms"] === undefined ? undefined : Number(flags["timeout-ms"]);
1610
1620
  if (operation === "spawn") {
@@ -1699,439 +1709,467 @@ export default class Sdk extends Command {
1699
1709
  throw new PublicCommandFailure({ kind: "usage", proof: "pre-effect" });
1700
1710
  }
1701
1711
 
1702
- const internal = parseSdkInternalArgv(this.argv);
1703
- if (internal.action === "broker-trampoline-internal") {
1704
- if (process.platform === "win32") throw new CliParseError("Broker trampoline is unavailable on Windows.");
1705
- const command = resolveSdkInternalSpawnCommand("broker-internal");
1706
- let child: ChildProcess;
1707
- try {
1708
- child = spawn(command.file, [...command.args, "--agent-dir", internal.agentDir], {
1709
- detached: true,
1710
- stdio: ["ignore", "ignore", "inherit"],
1711
- env: command.env,
1712
- ...(command.kind === "bun-source" ? { cwd: command.cwd } : {}),
1713
- });
1714
- } catch (error) {
1715
- throw new Error("Broker trampoline could not start broker.", { cause: error });
1716
- }
1717
- let spawnError: Error | undefined;
1718
- child.once("error", error => {
1719
- spawnError = error;
1712
+ await runSdkInternal(parseSdkInternalArgv(this.argv));
1713
+ }
1714
+ }
1715
+
1716
+ /**
1717
+ * Runs one parsed SDK internal entry point. `publicBrokerRun` is set only by
1718
+ * the stable `gjc sdk broker run` contract, which must report a lost ownership
1719
+ * race as a failure so its supervisor sees it, instead of the quiet exit 0 that
1720
+ * the detached autostart entry uses.
1721
+ */
1722
+ async function runSdkInternal(internal: SdkInternalArgv, publicBrokerRun = false): Promise<void> {
1723
+ if (internal.action === "broker-trampoline-internal") {
1724
+ if (process.platform === "win32") throw new CliParseError("Broker trampoline is unavailable on Windows.");
1725
+ const command = resolveSdkInternalSpawnCommand("broker-internal");
1726
+ let child: ChildProcess;
1727
+ try {
1728
+ child = spawn(command.file, [...command.args, "--agent-dir", internal.agentDir], {
1729
+ detached: true,
1730
+ stdio: ["ignore", "ignore", "inherit"],
1731
+ env: command.env,
1732
+ ...(command.kind === "bun-source" ? { cwd: command.cwd } : {}),
1720
1733
  });
1721
- if (child.pid === undefined) throw new Error("Broker trampoline could not start broker.");
1722
- const incarnation = processIncarnation(child.pid);
1723
- if (!incarnation) {
1724
- try {
1725
- child.kill("SIGKILL");
1726
- } catch {
1727
- // The child may already have exited; either way identity was not established.
1728
- }
1729
- throw new Error("Broker trampoline could not observe broker identity.");
1734
+ } catch (error) {
1735
+ throw new Error("Broker trampoline could not start broker.", { cause: error });
1736
+ }
1737
+ let spawnError: Error | undefined;
1738
+ child.once("error", error => {
1739
+ spawnError = error;
1740
+ });
1741
+ if (child.pid === undefined) throw new Error("Broker trampoline could not start broker.");
1742
+ const incarnation = processIncarnation(child.pid);
1743
+ if (!incarnation) {
1744
+ try {
1745
+ child.kill("SIGKILL");
1746
+ } catch {
1747
+ // The child may already have exited; either way identity was not established.
1730
1748
  }
1731
- if (spawnError) throw new Error("Broker trampoline could not start broker.", { cause: spawnError });
1732
- const stdoutWrite = Promise.withResolvers<void>();
1733
- process.stdout.write(`${child.pid}\t${incarnation}\n`, error => {
1734
- if (error) stdoutWrite.reject(error);
1735
- else stdoutWrite.resolve();
1736
- });
1749
+ throw new Error("Broker trampoline could not observe broker identity.");
1750
+ }
1751
+ if (spawnError) throw new Error("Broker trampoline could not start broker.", { cause: spawnError });
1752
+ const stdoutWrite = Promise.withResolvers<void>();
1753
+ process.stdout.write(`${child.pid}\t${incarnation}\n`, error => {
1754
+ if (error) stdoutWrite.reject(error);
1755
+ else stdoutWrite.resolve();
1756
+ });
1757
+ try {
1758
+ await stdoutWrite.promise;
1759
+ } catch (error) {
1737
1760
  try {
1738
- await stdoutWrite.promise;
1739
- } catch (error) {
1740
- try {
1741
- child.kill("SIGKILL");
1742
- } catch {
1743
- // The child may already have exited; reporting failure remains authoritative.
1744
- }
1745
- throw error;
1761
+ child.kill("SIGKILL");
1762
+ } catch {
1763
+ // The child may already have exited; reporting failure remains authoritative.
1746
1764
  }
1747
- child.unref();
1748
- return;
1765
+ throw error;
1749
1766
  }
1750
- if (internal.action === "broker-internal" || internal.action === "session-host-internal")
1751
- usePostmortemSignalExitAuthority();
1752
- if (internal.action === "session-host-internal") {
1753
- await runSessionHost();
1767
+ child.unref();
1768
+ return;
1769
+ }
1770
+ if (internal.action === "broker-internal" || internal.action === "session-host-internal")
1771
+ usePostmortemSignalExitAuthority();
1772
+ if (internal.action === "session-host-internal") {
1773
+ await runSessionHost();
1774
+ return;
1775
+ }
1776
+ if (internal.action === "stderr-drain-internal") {
1777
+ await runSdkStderrDrainer(internal.logPath, internal.maxBytes);
1778
+ return;
1779
+ }
1780
+ const agentDir = path.resolve(internal.agentDir);
1781
+ let broker: Broker | undefined;
1782
+ let runningBroker: Broker | undefined;
1783
+ let stopSweep: (() => void) | undefined;
1784
+ let startupExitRequested = false;
1785
+ let startupExitReason: BrokerStartupExitRecord["reason"] | undefined;
1786
+ let startupExitLogged = false;
1787
+ let pendingShutdownSignal: "SIGTERM" | "SIGINT" | undefined;
1788
+ let startupExitTask: Promise<boolean> | undefined;
1789
+ const startupStartedAt = process.hrtime.bigint();
1790
+ let startupExitWrite: Promise<BrokerStartupExitWriteStatus> | undefined;
1791
+ const startupAbortController = new AbortController();
1792
+ let candidateStartTask: Promise<unknown> | undefined;
1793
+ const waitForStartupAbortCleanup = async (): Promise<void> => {
1794
+ const task = candidateStartTask;
1795
+ if (!task) return;
1796
+ await Promise.race([
1797
+ task.then(
1798
+ () => undefined,
1799
+ () => undefined,
1800
+ ),
1801
+ Bun.sleep(BROKER_STARTUP_MARKER_WRITE_TIMEOUT_MS),
1802
+ ]);
1803
+ };
1804
+ const startupSignalExitCode = (signal: "SIGTERM" | "SIGINT"): 130 | 143 => (signal === "SIGINT" ? 130 : 143);
1805
+ const makeStartupExitRecord = (
1806
+ reason: BrokerStartupExitRecord["reason"],
1807
+ exitCode: 1 | 130 | 143,
1808
+ signal: "SIGTERM" | "SIGINT" | null,
1809
+ timeoutMs?: number,
1810
+ blockingLockPath?: string,
1811
+ ): BrokerStartupExitRecord & Record<string, unknown> => ({
1812
+ version: 1,
1813
+ reason,
1814
+ mode: "startup",
1815
+ fenceReason: null,
1816
+ fencedForMs: 0,
1817
+ uptimeMs: Math.max(0, Number((process.hrtime.bigint() - startupStartedAt) / 1_000_000n)),
1818
+ pid: process.pid,
1819
+ signal,
1820
+ exitCode,
1821
+ timeoutMs: timeoutMs ?? null,
1822
+ writtenAt: Date.now(),
1823
+ ...(blockingLockPath === undefined ? {} : { blockingLockPath }),
1824
+ });
1825
+ const beginStartupExitRecordWrite = (record: BrokerStartupExitRecord): Promise<BrokerStartupExitWriteStatus> => {
1826
+ const testDelayMs = Number(process.env.GJC_SDK_TEST_BROKER_STARTUP_EXIT_RECORD_DELAY_MS ?? 0);
1827
+ const beforeWriteForTest =
1828
+ Number.isSafeInteger(testDelayMs) && testDelayMs > 0 && testDelayMs <= 10_000
1829
+ ? async () => {
1830
+ await emitBrokerStartupTestSignal("startup-exit-record-fallback-waiting");
1831
+ await Bun.sleep(testDelayMs);
1832
+ }
1833
+ : undefined;
1834
+ startupExitWrite ??= writeBrokerStartupExitRecordBounded(
1835
+ agentDir,
1836
+ record,
1837
+ record.reason === "startup-signal"
1838
+ ? BROKER_STARTUP_SIGNAL_EXIT_RECORD_WRITE_TIMEOUT_MS
1839
+ : BROKER_STARTUP_MARKER_WRITE_TIMEOUT_MS,
1840
+ beforeWriteForTest,
1841
+ );
1842
+ return startupExitWrite;
1843
+ };
1844
+ const brokerReadyForSignal = (): Broker | undefined => runningBroker ?? (broker?.ownsDiscovery ? broker : undefined);
1845
+ const writeStartupExitLog = (record: BrokerStartupExitRecord & Record<string, unknown>, message: string): void => {
1846
+ if (startupExitLogged) return;
1847
+ startupExitLogged = true;
1848
+ const level = record.exitCode === 1 ? "error" : "warn";
1849
+ if (record.exitCode === 1) logger.error("sdk broker: startup watchdog forcing exit", record);
1850
+ else logger.warn("sdk broker: startup interrupted", record);
1851
+ process.stderr.write(`${JSON.stringify({ level, message, ...record })}\n`);
1852
+ };
1853
+ const exitDuringStartup = (
1854
+ reason: BrokerStartupExitRecord["reason"],
1855
+ exitCode: 1 | 130 | 143,
1856
+ signal: "SIGTERM" | "SIGINT" | null,
1857
+ timeoutMs?: number,
1858
+ blockingLockPath?: string,
1859
+ ): Promise<boolean> => {
1860
+ // The first startup exit cause owns both the persisted reason and the process exit code.
1861
+ if (startupExitTask) return startupExitReason === reason ? startupExitTask : Promise.resolve(false);
1862
+ if (startupExitRequested) return Promise.resolve(false);
1863
+ startupExitRequested = true;
1864
+ startupExitReason = reason;
1865
+ const exitRecord = makeStartupExitRecord(reason, exitCode, signal, timeoutMs, blockingLockPath);
1866
+ const message =
1867
+ reason === "startup-lock-blocked" && blockingLockPath
1868
+ ? `SDK broker startup blocked by retained removal transition ${blockingLockPath}.`
1869
+ : reason === "startup-deadline"
1870
+ ? `SDK broker startup exceeded its ${timeoutMs}ms fence deadline.`
1871
+ : `SDK broker startup interrupted by ${signal} before readiness.`;
1872
+ writeStartupExitLog(exitRecord, message);
1873
+ startupExitTask = (async () => {
1874
+ const exitRecordWrite = await beginStartupExitRecordWrite(exitRecord);
1875
+ const exitRecordWritten = exitRecordWrite.kind === "written";
1876
+ if (!exitRecordWritten)
1877
+ logger.error("sdk broker: startup exit record write timed out", {
1878
+ reason: "startup-exit-record-write-failed-or-timed-out",
1879
+ exitReason: reason,
1880
+ exitRecordWriteStatus: exitRecordWrite.kind,
1881
+ ...(exitRecordWrite.kind === "failed" && exitRecordWrite.code
1882
+ ? { exitRecordWriteErrorCode: exitRecordWrite.code }
1883
+ : {}),
1884
+ pid: process.pid,
1885
+ exitCode,
1886
+ });
1887
+ return true;
1888
+ })();
1889
+ return startupExitTask;
1890
+ };
1891
+ const unregisterPostmortem = postmortem.register("sdk-broker:exit", async reason => {
1892
+ const signal =
1893
+ reason === postmortem.Reason.SIGTERM
1894
+ ? "SIGTERM"
1895
+ : reason === postmortem.Reason.SIGINT
1896
+ ? "SIGINT"
1897
+ : pendingShutdownSignal;
1898
+ if (!signal) return;
1899
+ pendingShutdownSignal = signal;
1900
+ if (startupExitTask && startupExitReason !== "startup-signal") {
1901
+ await startupExitTask;
1902
+ await waitForStartupAbortCleanup();
1903
+ process.exit(1);
1754
1904
  return;
1755
1905
  }
1756
- if (internal.action === "stderr-drain-internal") {
1757
- await runSdkStderrDrainer(internal.logPath, internal.maxBytes);
1906
+ const activeBroker = brokerReadyForSignal();
1907
+ if (activeBroker) {
1908
+ runningBroker = activeBroker;
1909
+ stopSweep?.();
1910
+ await activeBroker.stop({ kind: "signal", signal });
1758
1911
  return;
1759
1912
  }
1760
- const agentDir = path.resolve(internal.agentDir);
1761
- let broker: Broker | undefined;
1762
- let runningBroker: Broker | undefined;
1763
- let stopSweep: (() => void) | undefined;
1764
- let startupExitRequested = false;
1765
- let startupExitReason: BrokerStartupExitRecord["reason"] | undefined;
1766
- let startupExitLogged = false;
1767
- let pendingShutdownSignal: "SIGTERM" | "SIGINT" | undefined;
1768
- let startupExitTask: Promise<boolean> | undefined;
1769
- const startupStartedAt = process.hrtime.bigint();
1770
- let startupExitWrite: Promise<BrokerStartupExitWriteStatus> | undefined;
1771
- const startupAbortController = new AbortController();
1772
- let candidateStartTask: Promise<unknown> | undefined;
1773
- const waitForStartupAbortCleanup = async (): Promise<void> => {
1774
- const task = candidateStartTask;
1775
- if (!task) return;
1776
- await Promise.race([
1777
- task.then(
1778
- () => undefined,
1779
- () => undefined,
1780
- ),
1781
- Bun.sleep(BROKER_STARTUP_MARKER_WRITE_TIMEOUT_MS),
1782
- ]);
1783
- };
1784
- const startupSignalExitCode = (signal: "SIGTERM" | "SIGINT"): 130 | 143 => (signal === "SIGINT" ? 130 : 143);
1785
- const makeStartupExitRecord = (
1786
- reason: BrokerStartupExitRecord["reason"],
1787
- exitCode: 1 | 130 | 143,
1788
- signal: "SIGTERM" | "SIGINT" | null,
1789
- timeoutMs?: number,
1790
- blockingLockPath?: string,
1791
- ): BrokerStartupExitRecord & Record<string, unknown> => ({
1792
- version: 1,
1793
- reason,
1794
- mode: "startup",
1795
- fenceReason: null,
1796
- fencedForMs: 0,
1797
- uptimeMs: Math.max(0, Number((process.hrtime.bigint() - startupStartedAt) / 1_000_000n)),
1798
- pid: process.pid,
1799
- signal,
1800
- exitCode,
1801
- timeoutMs: timeoutMs ?? null,
1802
- writtenAt: Date.now(),
1803
- ...(blockingLockPath === undefined ? {} : { blockingLockPath }),
1804
- });
1805
- const beginStartupExitRecordWrite = (record: BrokerStartupExitRecord): Promise<BrokerStartupExitWriteStatus> => {
1806
- const testDelayMs = Number(process.env.GJC_SDK_TEST_BROKER_STARTUP_EXIT_RECORD_DELAY_MS ?? 0);
1807
- const beforeWriteForTest =
1808
- Number.isSafeInteger(testDelayMs) && testDelayMs > 0 && testDelayMs <= 10_000
1809
- ? async () => {
1810
- await emitBrokerStartupTestSignal("startup-exit-record-fallback-waiting");
1811
- await Bun.sleep(testDelayMs);
1812
- }
1813
- : undefined;
1814
- startupExitWrite ??= writeBrokerStartupExitRecordBounded(
1815
- agentDir,
1816
- record,
1817
- record.reason === "startup-signal"
1818
- ? BROKER_STARTUP_SIGNAL_EXIT_RECORD_WRITE_TIMEOUT_MS
1819
- : BROKER_STARTUP_MARKER_WRITE_TIMEOUT_MS,
1820
- beforeWriteForTest,
1821
- );
1822
- return startupExitWrite;
1823
- };
1824
- const brokerReadyForSignal = (): Broker | undefined =>
1825
- runningBroker ?? (broker?.ownsDiscovery ? broker : undefined);
1826
- const writeStartupExitLog = (
1827
- record: BrokerStartupExitRecord & Record<string, unknown>,
1828
- message: string,
1829
- ): void => {
1830
- if (startupExitLogged) return;
1831
- startupExitLogged = true;
1832
- const level = record.exitCode === 1 ? "error" : "warn";
1833
- if (record.exitCode === 1) logger.error("sdk broker: startup watchdog forcing exit", record);
1834
- else logger.warn("sdk broker: startup interrupted", record);
1835
- process.stderr.write(`${JSON.stringify({ level, message, ...record })}\n`);
1836
- };
1837
- const exitDuringStartup = (
1838
- reason: BrokerStartupExitRecord["reason"],
1839
- exitCode: 1 | 130 | 143,
1840
- signal: "SIGTERM" | "SIGINT" | null,
1841
- timeoutMs?: number,
1842
- blockingLockPath?: string,
1843
- ): Promise<boolean> => {
1844
- // The first startup exit cause owns both the persisted reason and the process exit code.
1845
- if (startupExitTask) return startupExitReason === reason ? startupExitTask : Promise.resolve(false);
1846
- if (startupExitRequested) return Promise.resolve(false);
1847
- startupExitRequested = true;
1848
- startupExitReason = reason;
1849
- const exitRecord = makeStartupExitRecord(reason, exitCode, signal, timeoutMs, blockingLockPath);
1850
- const message =
1851
- reason === "startup-lock-blocked" && blockingLockPath
1852
- ? `SDK broker startup blocked by retained removal transition ${blockingLockPath}.`
1853
- : reason === "startup-deadline"
1854
- ? `SDK broker startup exceeded its ${timeoutMs}ms fence deadline.`
1855
- : `SDK broker startup interrupted by ${signal} before readiness.`;
1856
- writeStartupExitLog(exitRecord, message);
1857
- startupExitTask = (async () => {
1858
- const exitRecordWrite = await beginStartupExitRecordWrite(exitRecord);
1859
- const exitRecordWritten = exitRecordWrite.kind === "written";
1860
- if (!exitRecordWritten)
1861
- logger.error("sdk broker: startup exit record write timed out", {
1862
- reason: "startup-exit-record-write-failed-or-timed-out",
1863
- exitReason: reason,
1864
- exitRecordWriteStatus: exitRecordWrite.kind,
1865
- ...(exitRecordWrite.kind === "failed" && exitRecordWrite.code
1866
- ? { exitRecordWriteErrorCode: exitRecordWrite.code }
1867
- : {}),
1868
- pid: process.pid,
1869
- exitCode,
1870
- });
1871
- return true;
1872
- })();
1873
- return startupExitTask;
1874
- };
1875
- const unregisterPostmortem = postmortem.register("sdk-broker:exit", async reason => {
1876
- const signal =
1877
- reason === postmortem.Reason.SIGTERM
1878
- ? "SIGTERM"
1879
- : reason === postmortem.Reason.SIGINT
1880
- ? "SIGINT"
1881
- : pendingShutdownSignal;
1882
- if (!signal) return;
1883
- pendingShutdownSignal = signal;
1884
- if (startupExitTask && startupExitReason !== "startup-signal") {
1885
- await startupExitTask;
1886
- await waitForStartupAbortCleanup();
1887
- process.exit(1);
1888
- return;
1889
- }
1890
- const activeBroker = brokerReadyForSignal();
1891
- if (activeBroker) {
1892
- runningBroker = activeBroker;
1893
- stopSweep?.();
1894
- await activeBroker.stop({ kind: "signal", signal });
1895
- return;
1896
- }
1897
- startupAbortController.abort();
1898
- await exitDuringStartup("startup-signal", startupSignalExitCode(signal), signal);
1899
- // Startup may remain blocked in awaited I/O after abort. Bound best-effort
1900
- // rollback so shared postmortem can exit with the original signal status.
1901
- await waitForStartupAbortCleanup();
1902
- });
1903
- const finishPendingStartupSignal = async (): Promise<boolean> => {
1904
- const signal = pendingShutdownSignal;
1905
- if (!signal) return false;
1906
- await exitDuringStartup("startup-signal", startupSignalExitCode(signal), signal);
1907
- return true;
1908
- };
1909
- try {
1910
- const startupOperation = async (deadline: number): Promise<Broker | undefined> => {
1911
- const remainingMs = Math.max(1, deadline - Date.now());
1912
- const testWatchdogMs = Number(process.env.GJC_SDK_TEST_BROKER_STARTUP_WATCHDOG_MS ?? 0);
1913
- const watchdogMs =
1914
- Number.isSafeInteger(testWatchdogMs) && testWatchdogMs > 0 && testWatchdogMs <= remainingMs
1915
- ? testWatchdogMs
1916
- : remainingMs;
1917
- const startupCheckpointDeadline = performance.now() + watchdogMs - 1_000;
1918
- const startupWatchdog = setTimeout(() => {
1919
- startupAbortController.abort();
1920
- void exitDuringStartup("startup-deadline", 1, null, watchdogMs).then(async started => {
1921
- if (!started) {
1922
- // A prior startup signal owns the exit cause; its postmortem callback
1923
- // returns after bounded cleanup and preserves the signal status.
1924
- return;
1925
- }
1926
- await waitForStartupAbortCleanup();
1927
- process.exit(1);
1928
- });
1929
- }, watchdogMs);
1930
- try {
1931
- const existing = await reconcileBrokerGenerationForStartup({ agentDir }, deadline);
1932
- if (startupAbortController.signal.aborted) return undefined;
1933
- if (existing) {
1934
- logger.info("sdk broker: startup reused an existing owner", {
1935
- reason: "existing-owner-reused",
1936
- ownerId: existing.ownerId,
1937
- pid: existing.pid,
1938
- exitCode: 0,
1939
- });
1940
- return undefined;
1941
- }
1942
- if (process.env.GJC_SDK_TEST_BROKER_STARTUP_STALL === "1") {
1943
- const stalled = Promise.withResolvers<void>();
1944
- await stalled.promise;
1945
- }
1946
- const testStartupSignal = process.env.GJC_SDK_TEST_BROKER_STARTUP_SIGNAL;
1947
- if (testStartupSignal === "SIGTERM" || testStartupSignal === "SIGINT") {
1948
- await emitBrokerStartupTestSignal("startup-signal-ready");
1949
- process.kill(process.pid, testStartupSignal);
1950
- const waiting = Promise.withResolvers<void>();
1951
- await waiting.promise;
1952
- }
1953
- const startupGateFile = process.env.GJC_SDK_TEST_BROKER_STARTUP_GATE_FILE;
1954
- if (startupGateFile) {
1955
- await emitBrokerStartupTestSignal("startup-gate-waiting");
1956
- await waitForBrokerStartupTestGate(startupGateFile);
1957
- await emitBrokerStartupTestSignal("startup-gate-released");
1958
- }
1959
- const startupDelayMs = Number(process.env.GJC_SDK_TEST_BROKER_STARTUP_DELAY_MS ?? 0);
1960
- if (Number.isSafeInteger(startupDelayMs) && startupDelayMs > 0 && startupDelayMs <= 10_000) {
1961
- await Bun.sleep(startupDelayMs);
1913
+ startupAbortController.abort();
1914
+ await exitDuringStartup("startup-signal", startupSignalExitCode(signal), signal);
1915
+ // Startup may remain blocked in awaited I/O after abort. Bound best-effort
1916
+ // rollback so shared postmortem can exit with the original signal status.
1917
+ await waitForStartupAbortCleanup();
1918
+ });
1919
+ const finishPendingStartupSignal = async (): Promise<boolean> => {
1920
+ const signal = pendingShutdownSignal;
1921
+ if (!signal) return false;
1922
+ await exitDuringStartup("startup-signal", startupSignalExitCode(signal), signal);
1923
+ return true;
1924
+ };
1925
+ try {
1926
+ const startupOperation = async (deadline: number): Promise<Broker | undefined> => {
1927
+ const remainingMs = Math.max(1, deadline - Date.now());
1928
+ const testWatchdogMs = Number(process.env.GJC_SDK_TEST_BROKER_STARTUP_WATCHDOG_MS ?? 0);
1929
+ const watchdogMs =
1930
+ Number.isSafeInteger(testWatchdogMs) && testWatchdogMs > 0 && testWatchdogMs <= remainingMs
1931
+ ? testWatchdogMs
1932
+ : remainingMs;
1933
+ const startupCheckpointDeadline = performance.now() + watchdogMs - 1_000;
1934
+ const startupWatchdog = setTimeout(() => {
1935
+ startupAbortController.abort();
1936
+ void exitDuringStartup("startup-deadline", 1, null, watchdogMs).then(async started => {
1937
+ if (!started) {
1938
+ // A prior startup signal owns the exit cause; its postmortem callback
1939
+ // returns after bounded cleanup and preserves the signal status.
1940
+ return;
1962
1941
  }
1963
- if (startupAbortController.signal.aborted) return undefined;
1964
- // The real broker-internal entry point is the only place that reads this
1965
- // launcher-supplied environment variable; it is validated here and handed
1966
- // to Broker as a typed setting, never read a second time inside broker.ts
1967
- // from process.env directly.
1968
- const restartRequestEnv = process.env.GJC_BROKER_RESTART_REQUEST;
1969
- const restartRequestId =
1970
- typeof restartRequestEnv === "string" && /^[A-Za-z0-9_-]{1,256}$/.test(restartRequestEnv)
1971
- ? restartRequestEnv
1972
- : undefined;
1973
- const testPostPublicationDelayMs = Number(
1974
- process.env.GJC_SDK_TEST_BROKER_POST_PUBLICATION_DELAY_MS ?? 0,
1975
- );
1976
- const startupPostPublicationDelayMs =
1977
- Number.isSafeInteger(testPostPublicationDelayMs) &&
1978
- testPostPublicationDelayMs > 0 &&
1979
- testPostPublicationDelayMs <= 10_000
1980
- ? testPostPublicationDelayMs
1981
- : undefined;
1982
- const testPrePublicationDelayMs = Number(process.env.GJC_SDK_TEST_BROKER_PRE_PUBLICATION_DELAY_MS ?? 0);
1983
- const startupPrePublicationDelayMs =
1984
- Number.isSafeInteger(testPrePublicationDelayMs) &&
1985
- testPrePublicationDelayMs > 0 &&
1986
- testPrePublicationDelayMs <= 10_000
1987
- ? testPrePublicationDelayMs
1988
- : undefined;
1989
- const testAfterDiscoveryWriteDelayMs = Number(
1990
- process.env.GJC_SDK_TEST_BROKER_AFTER_DISCOVERY_WRITE_DELAY_MS ?? 0,
1991
- );
1992
- const startupAfterDiscoveryWriteTestHook =
1993
- Number.isSafeInteger(testAfterDiscoveryWriteDelayMs) &&
1994
- testAfterDiscoveryWriteDelayMs > 0 &&
1995
- testAfterDiscoveryWriteDelayMs <= 10_000
1996
- ? async () => {
1997
- await emitBrokerStartupTestSignal("discovery-written-before-retain");
1998
- const signal = startupAbortController.signal;
1999
- if (signal.aborted) return;
2000
- const wait = Promise.withResolvers<void>();
2001
- const timer = setTimeout(wait.resolve, testAfterDiscoveryWriteDelayMs);
2002
- const onAbort = (): void => wait.resolve();
2003
- signal.addEventListener("abort", onAbort, { once: true });
2004
- try {
2005
- await wait.promise;
2006
- } finally {
2007
- clearTimeout(timer);
2008
- signal.removeEventListener("abort", onAbort);
2009
- }
2010
- }
2011
- : undefined;
2012
- const candidate = new Broker({
2013
- agentDir,
2014
- startupAbortSignal: startupAbortController.signal,
2015
- startupCheckpointDeadline,
2016
- onStartupReady: () => clearTimeout(startupWatchdog),
2017
- masterOrphanGraceMs: (await Settings.loadForScope({ cwd: process.cwd(), agentDir })).get(
2018
- "sdk.masterOrphanGraceMs",
2019
- ),
2020
- resolveDirectoryMigration: async cwd => {
2021
- const settings = await Settings.loadForScope({ cwd, agentDir });
2022
- try {
2023
- const policy = settings.get("session.directoryMigration");
2024
- return policy === "disabled" ? "disabled" : "copy-retain";
2025
- } finally {
2026
- await settings.close();
2027
- }
2028
- },
2029
- ...(startupPrePublicationDelayMs === undefined
2030
- ? {}
2031
- : {
2032
- startupPrePublicationDelayMs,
2033
- startupPrePublicationTestHook: async () => {
2034
- await emitBrokerStartupTestSignal("pre-publication-ready");
2035
- },
2036
- }),
2037
- ...(startupAfterDiscoveryWriteTestHook === undefined ? {} : { startupAfterDiscoveryWriteTestHook }),
2038
- ...(startupPostPublicationDelayMs === undefined ? {} : { startupPostPublicationDelayMs }),
2039
- ...(restartRequestId === undefined ? {} : { restartRequestId }),
1942
+ await waitForStartupAbortCleanup();
1943
+ process.exit(1);
1944
+ });
1945
+ }, watchdogMs);
1946
+ try {
1947
+ // `gjc sdk broker run` never retires another owner, even one this generation cannot
1948
+ // reuse: any live incumbent is a refusal. Autostart repairs unusable generations.
1949
+ const existing = publicBrokerRun
1950
+ ? ((await readBrokerDiscovery(agentDir)) ?? undefined)
1951
+ : await reconcileBrokerGenerationForStartup({ agentDir }, deadline);
1952
+ if (startupAbortController.signal.aborted) return undefined;
1953
+ if (existing) {
1954
+ logger.info("sdk broker: startup reused an existing owner", {
1955
+ reason: "existing-owner-reused",
1956
+ ownerId: existing.ownerId,
1957
+ pid: existing.pid,
1958
+ exitCode: 0,
2040
1959
  });
2041
- broker = candidate;
2042
- candidateStartTask = candidate.start();
2043
- await candidateStartTask;
2044
- if (candidate.ownsDiscovery) runningBroker = candidate;
2045
- return candidate;
2046
- } finally {
2047
- clearTimeout(startupWatchdog);
1960
+ return undefined;
2048
1961
  }
2049
- };
2050
- broker = await withBrokerStartupLock(agentDir, startupOperation, {
2051
- onAcquired: () => void emitBrokerStartupTestSignal("fence-acquired"),
2052
- onContended: () => void emitBrokerStartupTestSignal("fence-contended"),
2053
- });
2054
- } catch (error) {
2055
- if (pendingShutdownSignal) {
2056
- await finishPendingStartupSignal();
2057
- return;
2058
- }
2059
- // A retained removal transition is an explicit acquire failure, not only
2060
- // an exhausted ordinary contention timeout.
2061
- if (error instanceof FileLockAcquireError && error.orphanPath) {
2062
- await exitDuringStartup(
2063
- "startup-lock-blocked",
2064
- 1,
2065
- null,
2066
- BROKER_DISCOVERY_BUDGET.startupLockWaitMs,
2067
- error.orphanPath,
2068
- );
2069
- }
2070
- if (broker) {
2071
- try {
2072
- await broker.stop({ kind: "startup-failure" });
2073
- } catch {
2074
- // Preserve the startup/fence failure that made this bootstrap unusable.
1962
+ if (process.env.GJC_SDK_TEST_BROKER_STARTUP_STALL === "1") {
1963
+ const stalled = Promise.withResolvers<void>();
1964
+ await stalled.promise;
2075
1965
  }
1966
+ const testStartupSignal = process.env.GJC_SDK_TEST_BROKER_STARTUP_SIGNAL;
1967
+ if (testStartupSignal === "SIGTERM" || testStartupSignal === "SIGINT") {
1968
+ await emitBrokerStartupTestSignal("startup-signal-ready");
1969
+ process.kill(process.pid, testStartupSignal);
1970
+ const waiting = Promise.withResolvers<void>();
1971
+ await waiting.promise;
1972
+ }
1973
+ const startupGateFile = process.env.GJC_SDK_TEST_BROKER_STARTUP_GATE_FILE;
1974
+ if (startupGateFile) {
1975
+ await emitBrokerStartupTestSignal("startup-gate-waiting");
1976
+ await waitForBrokerStartupTestGate(startupGateFile);
1977
+ await emitBrokerStartupTestSignal("startup-gate-released");
1978
+ }
1979
+ const startupDelayMs = Number(process.env.GJC_SDK_TEST_BROKER_STARTUP_DELAY_MS ?? 0);
1980
+ if (Number.isSafeInteger(startupDelayMs) && startupDelayMs > 0 && startupDelayMs <= 10_000) {
1981
+ await Bun.sleep(startupDelayMs);
1982
+ }
1983
+ if (startupAbortController.signal.aborted) return undefined;
1984
+ // The real broker-internal entry point is the only place that reads this
1985
+ // launcher-supplied environment variable; it is validated here and handed
1986
+ // to Broker as a typed setting, never read a second time inside broker.ts
1987
+ // from process.env directly.
1988
+ const restartRequestEnv = process.env.GJC_BROKER_RESTART_REQUEST;
1989
+ const restartRequestId =
1990
+ typeof restartRequestEnv === "string" && /^[A-Za-z0-9_-]{1,256}$/.test(restartRequestEnv)
1991
+ ? restartRequestEnv
1992
+ : undefined;
1993
+ const testPostPublicationDelayMs = Number(process.env.GJC_SDK_TEST_BROKER_POST_PUBLICATION_DELAY_MS ?? 0);
1994
+ const startupPostPublicationDelayMs =
1995
+ Number.isSafeInteger(testPostPublicationDelayMs) &&
1996
+ testPostPublicationDelayMs > 0 &&
1997
+ testPostPublicationDelayMs <= 10_000
1998
+ ? testPostPublicationDelayMs
1999
+ : undefined;
2000
+ const testPrePublicationDelayMs = Number(process.env.GJC_SDK_TEST_BROKER_PRE_PUBLICATION_DELAY_MS ?? 0);
2001
+ const startupPrePublicationDelayMs =
2002
+ Number.isSafeInteger(testPrePublicationDelayMs) &&
2003
+ testPrePublicationDelayMs > 0 &&
2004
+ testPrePublicationDelayMs <= 10_000
2005
+ ? testPrePublicationDelayMs
2006
+ : undefined;
2007
+ const testAfterDiscoveryWriteDelayMs = Number(
2008
+ process.env.GJC_SDK_TEST_BROKER_AFTER_DISCOVERY_WRITE_DELAY_MS ?? 0,
2009
+ );
2010
+ const startupAfterDiscoveryWriteTestHook =
2011
+ Number.isSafeInteger(testAfterDiscoveryWriteDelayMs) &&
2012
+ testAfterDiscoveryWriteDelayMs > 0 &&
2013
+ testAfterDiscoveryWriteDelayMs <= 10_000
2014
+ ? async () => {
2015
+ await emitBrokerStartupTestSignal("discovery-written-before-retain");
2016
+ const signal = startupAbortController.signal;
2017
+ if (signal.aborted) return;
2018
+ const wait = Promise.withResolvers<void>();
2019
+ const timer = setTimeout(wait.resolve, testAfterDiscoveryWriteDelayMs);
2020
+ const onAbort = (): void => wait.resolve();
2021
+ signal.addEventListener("abort", onAbort, { once: true });
2022
+ try {
2023
+ await wait.promise;
2024
+ } finally {
2025
+ clearTimeout(timer);
2026
+ signal.removeEventListener("abort", onAbort);
2027
+ }
2028
+ }
2029
+ : undefined;
2030
+ const candidate = new Broker({
2031
+ agentDir,
2032
+ startupAbortSignal: startupAbortController.signal,
2033
+ startupCheckpointDeadline,
2034
+ onStartupReady: () => clearTimeout(startupWatchdog),
2035
+ masterOrphanGraceMs: (await Settings.loadForScope({ cwd: process.cwd(), agentDir })).get(
2036
+ "sdk.masterOrphanGraceMs",
2037
+ ),
2038
+ resolveDirectoryMigration: async cwd => {
2039
+ const settings = await Settings.loadForScope({ cwd, agentDir });
2040
+ try {
2041
+ const policy = settings.get("session.directoryMigration");
2042
+ return policy === "disabled" ? "disabled" : "copy-retain";
2043
+ } finally {
2044
+ await settings.close();
2045
+ }
2046
+ },
2047
+ ...(startupPrePublicationDelayMs === undefined
2048
+ ? {}
2049
+ : {
2050
+ startupPrePublicationDelayMs,
2051
+ startupPrePublicationTestHook: async () => {
2052
+ await emitBrokerStartupTestSignal("pre-publication-ready");
2053
+ },
2054
+ }),
2055
+ ...(startupAfterDiscoveryWriteTestHook === undefined ? {} : { startupAfterDiscoveryWriteTestHook }),
2056
+ ...(startupPostPublicationDelayMs === undefined ? {} : { startupPostPublicationDelayMs }),
2057
+ ...(restartRequestId === undefined ? {} : { restartRequestId }),
2058
+ });
2059
+ broker = candidate;
2060
+ candidateStartTask = candidate.start();
2061
+ await candidateStartTask;
2062
+ if (candidate.ownsDiscovery) runningBroker = candidate;
2063
+ return candidate;
2064
+ } finally {
2065
+ clearTimeout(startupWatchdog);
2076
2066
  }
2077
- // This process spawns detached with stdio ignored (see ensure.ts), so the
2078
- // durable marker is the only channel the caller has to see why start()
2079
- // failed instead of a bare exit code (#3963).
2080
- const startupFailureIncarnation = processIncarnation(process.pid);
2081
- const markerWritten = await writeBrokerStartupFailureMarkerBounded(agentDir, {
2082
- reason: error instanceof Error ? error.message : String(error),
2083
- exitCode: 1,
2084
- signal: null,
2085
- pid: process.pid,
2086
- ...(startupFailureIncarnation === undefined ? {} : { incarnation: startupFailureIncarnation }),
2087
- });
2088
- logger.error("sdk broker: startup failed", {
2089
- reason: "startup-failure",
2090
- pid: process.pid,
2091
- exitCode: 1,
2092
- markerWritten,
2093
- });
2094
- unregisterPostmortem();
2095
- throw error;
2096
- }
2097
- if (!broker) {
2098
- if (pendingShutdownSignal) {
2099
- await finishPendingStartupSignal();
2100
- return;
2101
- }
2102
- unregisterPostmortem();
2067
+ };
2068
+ broker = await withBrokerStartupLock(agentDir, startupOperation, {
2069
+ onAcquired: () => void emitBrokerStartupTestSignal("fence-acquired"),
2070
+ onContended: () => void emitBrokerStartupTestSignal("fence-contended"),
2071
+ });
2072
+ } catch (error) {
2073
+ if (pendingShutdownSignal) {
2074
+ await finishPendingStartupSignal();
2103
2075
  return;
2104
2076
  }
2105
- if (!broker.ownsDiscovery) {
2106
- if (pendingShutdownSignal) {
2107
- if (runningBroker) await runningBroker.completion;
2108
- else await finishPendingStartupSignal();
2109
- return;
2077
+ // A retained removal transition is an explicit acquire failure, not only
2078
+ // an exhausted ordinary contention timeout.
2079
+ if (error instanceof FileLockAcquireError && error.orphanPath) {
2080
+ await exitDuringStartup(
2081
+ "startup-lock-blocked",
2082
+ 1,
2083
+ null,
2084
+ BROKER_DISCOVERY_BUDGET.startupLockWaitMs,
2085
+ error.orphanPath,
2086
+ );
2087
+ }
2088
+ if (broker) {
2089
+ try {
2090
+ await broker.stop({ kind: "startup-failure" });
2091
+ } catch {
2092
+ // Preserve the startup/fence failure that made this bootstrap unusable.
2110
2093
  }
2111
- // Another broker owns discovery; this process exits cleanly (code 0) as
2112
- // the race loser. Record why so a caller polling for a winner that never
2113
- // appears can diagnose the loss instead of seeing only a bare exit 0.
2114
- const losingIncarnation = processIncarnation(process.pid);
2115
- await writeBrokerStartupFailureMarkerBounded(agentDir, {
2116
- reason: "Another broker owns the lock/discovery; this broker exited as the race loser.",
2117
- exitCode: 0,
2118
- signal: null,
2119
- pid: process.pid,
2120
- ...(losingIncarnation === undefined ? {} : { incarnation: losingIncarnation }),
2121
- });
2122
- unregisterPostmortem();
2094
+ }
2095
+ // This process spawns detached with stdio ignored (see ensure.ts), so the
2096
+ // durable marker is the only channel the caller has to see why start()
2097
+ // failed instead of a bare exit code (#3963).
2098
+ const startupFailureIncarnation = processIncarnation(process.pid);
2099
+ const markerWritten = await writeBrokerStartupFailureMarkerBounded(agentDir, {
2100
+ reason: error instanceof Error ? error.message : String(error),
2101
+ exitCode: 1,
2102
+ signal: null,
2103
+ pid: process.pid,
2104
+ ...(startupFailureIncarnation === undefined ? {} : { incarnation: startupFailureIncarnation }),
2105
+ });
2106
+ logger.error("sdk broker: startup failed", {
2107
+ reason: "startup-failure",
2108
+ pid: process.pid,
2109
+ exitCode: 1,
2110
+ markerWritten,
2111
+ });
2112
+ unregisterPostmortem();
2113
+ throw error;
2114
+ }
2115
+ if (!broker) {
2116
+ if (pendingShutdownSignal) {
2117
+ await finishPendingStartupSignal();
2123
2118
  return;
2124
2119
  }
2125
- runningBroker = broker;
2126
- // A live broker must not keep advertising sessions whose host process is
2127
- // gone; the sweep is the broker-side half of the host reaping bound.
2128
- stopSweep = startBrokerDeadRegistrationSweep(runningBroker);
2129
- try {
2130
- await runningBroker.completion;
2131
- } finally {
2132
- stopSweep?.();
2133
- if (!pendingShutdownSignal) unregisterPostmortem();
2120
+ unregisterPostmortem();
2121
+ if (publicBrokerRun) failPublicBrokerRun(agentDir);
2122
+ return;
2123
+ }
2124
+ if (!broker.ownsDiscovery) {
2125
+ if (pendingShutdownSignal) {
2126
+ if (runningBroker) await runningBroker.completion;
2127
+ else await finishPendingStartupSignal();
2128
+ return;
2134
2129
  }
2135
- if (!pendingShutdownSignal) process.exit(0);
2130
+ // Another broker owns discovery. The detached autostart entry exits cleanly
2131
+ // (code 0) as the race loser; `gjc sdk broker run` fails instead, because
2132
+ // its supervisor must not mistake a competing owner for its own broker.
2133
+ // Record why so a caller polling for a winner that never appears can
2134
+ // diagnose the loss instead of seeing only a bare exit code.
2135
+ const losingIncarnation = processIncarnation(process.pid);
2136
+ await writeBrokerStartupFailureMarkerBounded(agentDir, {
2137
+ reason: "Another broker owns the lock/discovery; this broker exited as the race loser.",
2138
+ exitCode: publicBrokerRun ? 1 : 0,
2139
+ signal: null,
2140
+ pid: process.pid,
2141
+ ...(losingIncarnation === undefined ? {} : { incarnation: losingIncarnation }),
2142
+ });
2143
+ unregisterPostmortem();
2144
+ if (publicBrokerRun) failPublicBrokerRun(agentDir);
2145
+ return;
2146
+ }
2147
+ runningBroker = broker;
2148
+ // A live broker must not keep advertising sessions whose host process is
2149
+ // gone; the sweep is the broker-side half of the host reaping bound.
2150
+ stopSweep = startBrokerDeadRegistrationSweep(runningBroker);
2151
+ try {
2152
+ await runningBroker.completion;
2153
+ } finally {
2154
+ stopSweep?.();
2155
+ if (!pendingShutdownSignal) unregisterPostmortem();
2136
2156
  }
2157
+ if (pendingShutdownSignal) return;
2158
+ // A supervised broker that lost its root or could not renew its heartbeat
2159
+ // must exit as a failure, so the supervisor restarts it. A requested
2160
+ // shutdown or a committed restart handoff is a clean stop.
2161
+ const terminal = runningBroker.terminalExit;
2162
+ if (publicBrokerRun && (terminal?.mode !== "owned-root" || terminal.reason === "heartbeat-renewal-blocked")) {
2163
+ process.stderr.write(
2164
+ `gjc sdk broker run: SDK broker stopped abnormally (${terminal?.mode ?? "unknown"}: ${terminal?.reason ?? "unknown"}).\n`,
2165
+ );
2166
+ process.exit(1);
2167
+ }
2168
+ process.exit(0);
2169
+ }
2170
+
2171
+ /** `gjc sdk broker run` did not become the owner: another broker already serves this agent dir. */
2172
+ function failPublicBrokerRun(agentDir: string): void {
2173
+ process.stderr.write(`gjc sdk broker run: another SDK broker already owns ${agentDir}.\n`);
2174
+ process.exitCode = 1;
2137
2175
  }