@gajae-code/coding-agent 0.18.8 → 0.18.9
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.
- package/CHANGELOG.md +98 -0
- package/dist/types/async/endpoint-id.d.ts +16 -0
- package/dist/types/async/support.d.ts +0 -14
- package/dist/types/commands/mcp-serve.d.ts +4 -0
- package/dist/types/discovery/mcp-json.d.ts +1 -0
- package/dist/types/eval/py/kernel.d.ts +18 -0
- package/dist/types/export/html/html-export-destination.d.ts +2 -0
- package/dist/types/extensibility/extensions/function-hooks-internal.d.ts +5 -0
- package/dist/types/extensibility/skill-management.d.ts +1 -0
- package/dist/types/gjc-runtime/psmux-detect.d.ts +3 -3
- package/dist/types/gjc-runtime/sidecar-bootstrap-env.d.ts +6 -0
- package/dist/types/gjc-runtime/state-writer.d.ts +3 -0
- package/dist/types/gjc-runtime/tmux-common.d.ts +8 -5
- package/dist/types/lsp/config.d.ts +8 -5
- package/dist/types/runtime-mcp/manager.d.ts +2 -1
- package/dist/types/runtime-mcp/mcp-redirect.d.ts +9 -0
- package/dist/types/runtime-mcp/oauth-public-url.d.ts +3 -1
- package/dist/types/runtime-mcp/project-host-values.d.ts +27 -0
- package/dist/types/sdk/broker/broker.d.ts +6 -1
- package/dist/types/sdk/broker/doctor-restart.d.ts +5 -0
- package/dist/types/sdk/broker/ensure.d.ts +14 -1
- package/dist/types/session/session-storage.d.ts +7 -0
- package/dist/types/skill-state/workflow-mutation-guard.d.ts +3 -0
- package/dist/types/task/isolation-dir.d.ts +26 -0
- package/dist/types/tools/ast-edit-write-path.d.ts +13 -0
- package/dist/types/tools/browser/cdp-url.d.ts +2 -0
- package/dist/types/tools/github-side-effect.d.ts +22 -0
- package/dist/types/tools/monitor-mutation.d.ts +9 -0
- package/package.json +7 -7
- package/src/async/endpoint-id.ts +26 -0
- package/src/async/job-manager.ts +19 -10
- package/src/async/support.ts +0 -25
- package/src/capability/index.ts +14 -16
- package/src/cli/doctor/repairs.ts +4 -1
- package/src/cli/doctor-supervisor.ts +3 -0
- package/src/cli/public-command-registry.ts +28 -6
- package/src/commands/mcp-serve.ts +7 -0
- package/src/commands/sdk.ts +453 -415
- package/src/config/file-lock.ts +9 -11
- package/src/config/model-profile-activation.ts +5 -5
- package/src/coordinator-mcp/server.ts +2 -0
- package/src/customization/inventory.ts +1 -13
- package/src/dap/config.ts +2 -2
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.ts +10 -6
- package/src/discovery/builtin.ts +3 -1
- package/src/discovery/helpers.ts +12 -9
- package/src/discovery/mcp-json.ts +20 -12
- package/src/eval/py/executor.ts +459 -77
- package/src/eval/py/kernel.ts +79 -13
- package/src/eval/py/tool-bridge.ts +4 -0
- package/src/export/html/html-export-destination.ts +16 -0
- package/src/export/html/index.ts +2 -6
- package/src/extensibility/extensions/function-hooks-internal.ts +27 -0
- package/src/extensibility/extensions/runner.ts +11 -1
- package/src/extensibility/gjc-plugins/mcp-policy.ts +2 -1
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +27 -15
- package/src/extensibility/skill-management.ts +3 -0
- package/src/gjc-runtime/launch-worktree.ts +56 -5
- package/src/gjc-runtime/psmux-detect.ts +28 -4
- package/src/gjc-runtime/session-state-sidecar.ts +3 -2
- package/src/gjc-runtime/sidecar-bootstrap-env.ts +19 -0
- package/src/gjc-runtime/state-writer.ts +22 -8
- package/src/gjc-runtime/tmux-common.ts +8 -5
- package/src/internal-urls/docs-index.generated.ts +7 -7
- package/src/lsp/config.ts +12 -63
- package/src/memories/index.ts +88 -7
- package/src/modes/components/hook-editor.ts +1 -1
- package/src/modes/components/hook-selector.ts +1 -1
- package/src/modes/controllers/input-controller.ts +2 -2
- package/src/modes/controllers/runtime-mcp-command-controller.ts +21 -13
- package/src/modes/controllers/selector-controller.ts +3 -7
- package/src/modes/controllers/todo-command-controller.ts +1 -1
- package/src/modes/interactive-mode.ts +1 -1
- package/src/runtime-mcp/client.ts +4 -2
- package/src/runtime-mcp/json-rpc.ts +2 -1
- package/src/runtime-mcp/manager.ts +29 -11
- package/src/runtime-mcp/mcp-redirect.ts +73 -0
- package/src/runtime-mcp/oauth-flow.ts +11 -4
- package/src/runtime-mcp/oauth-public-url.ts +8 -2
- package/src/runtime-mcp/pool.ts +6 -0
- package/src/runtime-mcp/project-host-values.ts +46 -0
- package/src/runtime-mcp/transports/http.ts +2 -1
- package/src/sdk/acp/adapter.ts +2 -0
- package/src/sdk/broker/broker.ts +6 -0
- package/src/sdk/broker/doctor-restart.ts +4 -0
- package/src/sdk/broker/ensure.ts +29 -2
- package/src/sdk/host/session-runtime.test.ts +58 -9
- package/src/sdk/host/session-runtime.ts +65 -35
- package/src/sdk/router/session-router.ts +7 -6
- package/src/sdk/session.ts +2 -7
- package/src/session/agent-session.ts +59 -17
- package/src/session/artifacts.ts +3 -2
- package/src/session/internal/managed-session-scope.ts +403 -81
- package/src/session/internal/managed-session-storage.ts +125 -17
- package/src/session/session-manager.ts +12 -0
- package/src/session/session-storage.ts +30 -1
- package/src/skill-state/active-state.ts +13 -0
- package/src/skill-state/workflow-mutation-guard.ts +102 -10
- package/src/slash-commands/builtin-registry.ts +1 -1
- package/src/slash-commands/helpers/mcp.ts +8 -4
- package/src/task/isolation-dir.ts +143 -0
- package/src/task/worktree.ts +16 -7
- package/src/tools/ast-edit-write-path.ts +68 -0
- package/src/tools/ast-edit.ts +7 -2
- package/src/tools/browser/cdp-url.ts +18 -0
- package/src/tools/browser.ts +8 -1
- package/src/tools/gh.ts +10 -0
- package/src/tools/github-side-effect.ts +112 -0
- package/src/tools/monitor-mutation.ts +27 -0
- package/src/tools/monitor.ts +7 -0
- package/src/tools/python.ts +7 -6
- package/src/utils/external-editor.ts +3 -2
- package/src/web/insane/url-guard.ts +2 -1
package/src/commands/sdk.ts
CHANGED
|
@@ -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
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
child.
|
|
1719
|
-
|
|
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
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
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
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
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
|
-
|
|
1739
|
-
} catch
|
|
1740
|
-
|
|
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
|
-
|
|
1748
|
-
return;
|
|
1765
|
+
throw error;
|
|
1749
1766
|
}
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
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
|
-
|
|
1757
|
-
|
|
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
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
const
|
|
1774
|
-
const
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
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
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2051
|
-
|
|
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
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
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
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
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
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
})
|
|
2122
|
-
|
|
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
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
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
|
-
|
|
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
|
}
|