@agentdocumentformat/cli 0.7.8 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/README.md +1 -1
  2. package/dist/adf.mjs +59 -8
  3. package/dist/chunks/{APEv2Parser-2CNNHMXV.mjs → APEv2Parser-WIXRJLHB.mjs} +2 -2
  4. package/dist/chunks/{AiffParser-7BR4RL6G.mjs → AiffParser-ZC4QFOVJ.mjs} +5 -5
  5. package/dist/chunks/{App-7RIIOHJJ.mjs → App-A5GUFCFJ.mjs} +279 -293
  6. package/dist/chunks/{DsdiffParser-CMD4AX7A.mjs → DsdiffParser-UAPAJHFX.mjs} +5 -5
  7. package/dist/chunks/{DsfParser-UOGZDS2M.mjs → DsfParser-NNYZK2QX.mjs} +6 -6
  8. package/dist/chunks/{FlacParser-HB3T3CD4.mjs → FlacParser-MW2W4LO3.mjs} +6 -6
  9. package/dist/chunks/{MP4Parser-66HBRUSX.mjs → MP4Parser-Z57SYOOQ.mjs} +4 -4
  10. package/dist/chunks/{MpegParser-YW6LIUEW.mjs → MpegParser-XIIPAZVM.mjs} +7 -7
  11. package/dist/chunks/{MusepackParser-PAUSXP7D.mjs → MusepackParser-QWNDEYHJ.mjs} +6 -6
  12. package/dist/chunks/{OggParser-NUBV3GWC.mjs → OggParser-WRX53F77.mjs} +9 -9
  13. package/dist/chunks/{WavPackParser-WQAGT7TC.mjs → WavPackParser-MJ7WCSJ5.mjs} +2 -2
  14. package/dist/chunks/{WaveParser-EWLQVDX4.mjs → WaveParser-RAVLHZ5S.mjs} +7 -7
  15. package/dist/chunks/{adf-schema-US4NFHMR.mjs → adf-schema-YJSGLMA4.mjs} +2 -2
  16. package/dist/chunks/auth-manager-AJ32FZWS.mjs +19 -0
  17. package/dist/chunks/auth-manager-C5OBEQK2.mjs +18 -0
  18. package/dist/chunks/auth-manager-TBNKCJ45.mjs +17 -0
  19. package/dist/chunks/{chatgpt-subscription-E4MXTIAT.mjs → chatgpt-subscription-MMGNW5KJ.mjs} +5 -2
  20. package/dist/chunks/chunk-3UGZFWIF.mjs +26 -0
  21. package/dist/chunks/{chunk-5PO5PKRV.mjs → chunk-4OGZFUKA.mjs} +126 -67
  22. package/dist/chunks/{chunk-VN7UH7I5.mjs → chunk-6XQZGYMK.mjs} +246 -67
  23. package/dist/chunks/{chunk-XNPZBLYL.mjs → chunk-75DC5EWN.mjs} +13 -2
  24. package/dist/chunks/chunk-AZYOXXVL.mjs +865 -0
  25. package/dist/chunks/{chunk-6VQEXLE2.mjs → chunk-BHY7QDCM.mjs} +41 -3
  26. package/dist/chunks/chunk-C2S64Y6F.mjs +203 -0
  27. package/dist/chunks/chunk-C4GA5SI4.mjs +12799 -0
  28. package/dist/chunks/chunk-DDTTSKGG.mjs +2443 -0
  29. package/dist/chunks/{chunk-VAUOM3IN.mjs → chunk-DLWCKKIO.mjs} +1 -1
  30. package/dist/chunks/{chunk-QPLEAE6V.mjs → chunk-G6NBFYG5.mjs} +152 -824
  31. package/dist/chunks/{chunk-KXEF4F4E.mjs → chunk-GAZO6UMI.mjs} +1 -1
  32. package/dist/chunks/{chunk-XYTWGHFS.mjs → chunk-H3XLRWVE.mjs} +1 -1
  33. package/dist/chunks/{chunk-7T2YMTWL.mjs → chunk-H4C65MTP.mjs} +4 -4
  34. package/dist/chunks/chunk-HBOFZRUS.mjs +738 -0
  35. package/dist/chunks/chunk-JVPZIMXW.mjs +976 -0
  36. package/dist/chunks/{chunk-XYV3VCIX.mjs → chunk-JZFONJSL.mjs} +79 -20
  37. package/dist/chunks/chunk-KDDVQKCG.mjs +65 -0
  38. package/dist/chunks/chunk-KRPKWOMB.mjs +290 -0
  39. package/dist/chunks/{chunk-G6PH7RPW.mjs → chunk-LCMANRYY.mjs} +3 -3
  40. package/dist/chunks/{chunk-ZESQMP66.mjs → chunk-LGL6ML37.mjs} +3 -3
  41. package/dist/chunks/chunk-LKFHERRV.mjs +145 -0
  42. package/dist/chunks/chunk-LTX74NU6.mjs +29 -0
  43. package/dist/chunks/chunk-M2F7C2UJ.mjs +425 -0
  44. package/dist/chunks/{chunk-62NE4LG4.mjs → chunk-MQAMI5BF.mjs} +5 -5
  45. package/dist/chunks/chunk-MZRXFWPQ.mjs +278 -0
  46. package/dist/chunks/{chunk-PRMGZZM5.mjs → chunk-NZLEPJ5M.mjs} +2 -0
  47. package/dist/chunks/{chunk-CD3ATSXE.mjs → chunk-NZO5O4VN.mjs} +1 -1
  48. package/dist/chunks/{chunk-I3UYSITV.mjs → chunk-O7XKQ6CH.mjs} +1 -1
  49. package/dist/chunks/chunk-P3RG5LQA.mjs +244 -0
  50. package/dist/chunks/chunk-PAK5JCO2.mjs +48 -0
  51. package/dist/chunks/chunk-PC63YFB7.mjs +34 -0
  52. package/dist/chunks/{daemon-control-HKFVSQTQ.mjs → chunk-PRPTKMQW.mjs} +67 -26
  53. package/dist/chunks/{chunk-LFMB4PAA.mjs → chunk-TKXIH27X.mjs} +974 -113
  54. package/dist/chunks/{chunk-5W7KES6P.mjs → chunk-UZPDETEU.mjs} +1 -1
  55. package/dist/chunks/chunk-V2T6NZX2.mjs +517 -0
  56. package/dist/chunks/chunk-V3MM6PCH.mjs +366 -0
  57. package/dist/chunks/chunk-WJGPNHGX.mjs +67 -0
  58. package/dist/chunks/chunk-WVTPPOKK.mjs +290 -0
  59. package/dist/chunks/{chunk-J74STFKE.mjs → chunk-WX6QCUWI.mjs} +272 -51
  60. package/dist/chunks/{chunk-Y5ZLKFLE.mjs → chunk-XNNJQCZP.mjs} +196 -165
  61. package/dist/chunks/{chunk-HDBPBORT.mjs → chunk-XT2BJUFC.mjs} +15 -6
  62. package/dist/chunks/{chunk-HSBV6YFU.mjs → chunk-YRMKSHW3.mjs} +614 -92
  63. package/dist/chunks/{chunk-BNXUAOO4.mjs → chunk-ZGXUOAWZ.mjs} +3186 -959
  64. package/dist/chunks/chunk-ZPB3HYM7.mjs +45 -0
  65. package/dist/chunks/claude-subscription-33BMZB6I.mjs +29 -0
  66. package/dist/chunks/{cli-NMFIAR7Z.mjs → cli-I47IFDHJ.mjs} +797 -56
  67. package/dist/chunks/{commands-KPJWFRUE.mjs → commands-E4EQUHE5.mjs} +4 -3
  68. package/dist/chunks/{daemon-YDIK3AAM.mjs → daemon-FQ5BV3AP.mjs} +82058 -63958
  69. package/dist/chunks/daemon-control-L3CLG63P.mjs +59 -0
  70. package/dist/chunks/{daemon-token-WRSULP7G.mjs → daemon-token-L5QKTHU5.mjs} +3 -1
  71. package/dist/chunks/{discord-DSP4HUWI.mjs → discord-IR62565M.mjs} +3 -52
  72. package/dist/chunks/{email-BRG7KUO2.mjs → email-KHHTJBDW.mjs} +4 -2
  73. package/dist/chunks/{grok-subscription-4PYX7TBF.mjs → grok-subscription-E74FS5EG.mjs} +5 -2
  74. package/dist/chunks/{lib-N7FCZWKH.mjs → lib-L27L5PXM.mjs} +22 -22
  75. package/dist/chunks/{open-VAU7PXT6.mjs → open-TLGS2Z3Q.mjs} +4 -4
  76. package/dist/chunks/{parser-JVY2PWSM.mjs → parser-7GARNT5L.mjs} +1 -1
  77. package/dist/chunks/{pipeline-executor-O77NTGLP.mjs → pipeline-executor-FD4FBWQA.mjs} +6 -5
  78. package/dist/chunks/prompt-RRHZX4YT.mjs +13 -0
  79. package/dist/chunks/remote-access-cli-XXAPQWH6.mjs +268 -0
  80. package/dist/chunks/remote-hello-UFWUBDQI.mjs +37 -0
  81. package/dist/chunks/sdk-5FD2ZP7N.mjs +28422 -0
  82. package/dist/chunks/{slack-OCNNF4DX.mjs → slack-IYC5RAB5.mjs} +3 -4
  83. package/dist/chunks/{store-HUVDJPYN.mjs → store-ZRN2WCFB.mjs} +6 -3
  84. package/dist/chunks/tailscale-WLJATFMB.mjs +25 -0
  85. package/dist/chunks/{theme-SFE3JMA2.mjs → theme-HLBDTF6F.mjs} +1 -1
  86. package/dist/chunks/{themes-YLXLOIJF.mjs → themes-BQ72P446.mjs} +2 -2
  87. package/dist/chunks/{token-6G3HCMNX.mjs → token-6D22WECM.mjs} +20 -16
  88. package/dist/chunks/{token-util-UXYSDQIW.mjs → token-util-YF3ZWBZ4.mjs} +1 -2
  89. package/dist/chunks/{tui-UQJLK7XI.mjs → tui-SPSPDADM.mjs} +17 -14
  90. package/dist/chunks/{welcome-XHXFV7SH.mjs → welcome-J5JZZAM5.mjs} +1 -1
  91. package/dist/chunks/{whatsapp-AJMRQBT7.mjs → whatsapp-YJFFVN4Q.mjs} +9 -9
  92. package/package.json +1 -1
  93. package/dist/chunks/chunk-GKKCMXIZ.mjs +0 -94
  94. package/dist/chunks/chunk-IHDDCZBS.mjs +0 -112
  95. package/dist/chunks/chunk-KWFOS5WC.mjs +0 -41
  96. package/dist/chunks/chunk-UEZR2CNS.mjs +0 -65
  97. package/dist/chunks/chunk-VMHTYDH4.mjs +0 -172
  98. package/dist/chunks/chunk-Y2QA53AN.mjs +0 -259
  99. package/dist/chunks/{chunk-NBTUWMGP.mjs → chunk-2MTE22CR.mjs} +3 -3
  100. package/dist/chunks/{telegram-2EFGKIVK.mjs → telegram-JYY2VDTI.mjs} +5 -5
@@ -0,0 +1,976 @@
1
+ import { createRequire as __adfCreateRequire } from 'node:module';
2
+ import { fileURLToPath as __adfFileURLToPath } from 'node:url';
3
+ import { dirname as __adfDirname } from 'node:path';
4
+ const require = __adfCreateRequire(import.meta.url);
5
+ const __filename = __adfFileURLToPath(import.meta.url);
6
+ const __dirname = __adfDirname(__filename);
7
+ import {
8
+ testSeamsAllowed
9
+ } from "./chunk-WVTPPOKK.mjs";
10
+ import {
11
+ getUserDataPath
12
+ } from "./chunk-PAK5JCO2.mjs";
13
+
14
+ // src/main/runtime/agent-lease.ts
15
+ import { execFile } from "node:child_process";
16
+ import { createHash, randomBytes } from "node:crypto";
17
+ import { promises as fsp, readFileSync, unlinkSync } from "node:fs";
18
+ import { createConnection, createServer } from "node:net";
19
+ import { homedir, hostname as osHostname, tmpdir } from "node:os";
20
+ import { dirname, join, resolve } from "node:path";
21
+ var AGENT_LEASED_CODE = "agent_leased", DEFAULT_DAEMON_PORT = 7385, PROBE_TIMEOUT_MS = 1500, PARTIAL_WRITE_GRACE_MS = 2e3, ACQUIRE_BUDGET_MS = 5e3, MAX_SOCKET_PATH = 100, TRANSIENT_FS_CODES = /* @__PURE__ */ new Set(["EPERM", "EBUSY", "EACCES"]), UNUSABLE_DIR_MEMO_MS = 6e4, TAKEOVER_LOCK_TTL_MS = 1e4, TAKEOVER_LOCK_WAIT_MS = 2e3, TAKEOVER_LOCK_REPROBE_MS = 250, TAKEOVER_LOCK_SUSPECT_MS = 500, DIR_MODE = 448, FILE_MODE = 384;
22
+ function leaseDir(env = process.env) {
23
+ if (env.ADF_LEASE_DIR) return resolve(env.ADF_LEASE_DIR);
24
+ let base;
25
+ return process.platform === "darwin" ? base = join(homedir(), "Library", "Application Support") : process.platform === "win32" ? base = env.LOCALAPPDATA ?? join(homedir(), "AppData", "Local") : base = env.XDG_CONFIG_HOME ?? join(homedir(), ".config"), join(base, "adf", "leases");
26
+ }
27
+ async function canonicalLeasePath(filePath) {
28
+ let abs = resolve(filePath);
29
+ try {
30
+ return stripLongPathPrefix(await fsp.realpath(abs));
31
+ } catch {
32
+ return stripLongPathPrefix(abs);
33
+ }
34
+ }
35
+ function leaseKeyForCanonicalPath(canonicalPath, platform = process.platform) {
36
+ let norm = canonicalPath;
37
+ return platform === "win32" ? norm = norm.replace(/\//g, "\\").toLowerCase() : platform === "darwin" && (norm = norm.normalize("NFC").toLowerCase()), createHash("sha256").update(norm).digest("hex");
38
+ }
39
+ function stripLongPathPrefix(p) {
40
+ return process.platform !== "win32" ? p : p.startsWith("\\\\?\\UNC\\") ? `\\\\${p.slice(8)}` : p.startsWith("\\\\?\\") ? p.slice(4) : p;
41
+ }
42
+ function agentNameOf(path) {
43
+ return (path.split(/[\\/]/).pop() ?? path).replace(/\.adf$/i, "");
44
+ }
45
+ function samePath(a, b, platform = process.platform) {
46
+ let norm = (p) => {
47
+ let abs = platform === "darwin" ? resolve(p).normalize("NFC") : resolve(p);
48
+ return platform === "linux" ? abs : abs.toLowerCase();
49
+ };
50
+ return norm(a) === norm(b);
51
+ }
52
+ function agentLeasedMessage(record, ownSettingsPath, otherMachine = !1, check = {}) {
53
+ let name = agentNameOf(record.path), h = record.holder, text;
54
+ if (otherMachine)
55
+ text = `${name} is already running on another machine (${h.hostname || "unknown host"}, pid ${h.pid}) that shares this lease folder. Stop it there, then start it here.`;
56
+ else if (h.kind === "daemon") {
57
+ let url = h.controlUrl && !/^http:\/\/(127\.0\.0\.1|localhost):7385\/?$/.test(h.controlUrl) ? h.controlUrl : null, target = url ? ` --url ${url}` : "";
58
+ text = `${name} is already running in the ADF daemon (pid ${h.pid}${url ? `, ${url}` : ""}). Stop it there with "adf${target} stop ${name}", or stop that daemon with "adf daemon stop", then start it here.`;
59
+ } else
60
+ text = `${name} is already running in another ADF Studio (pid ${h.pid}). Stop the agent there or quit that Studio, then start it here.`;
61
+ return ownSettingsPath && h.settingsPath && !samePath(ownSettingsPath, h.settingsPath) && (text += ` That process uses the settings file ${h.settingsPath}.`), otherMachine || (check.unconfirmedLeaseFile ? text += ` ADF cannot confirm that pid ${h.pid} is still that process: it recorded no endpoint to check. If no ADF Studio or daemon runs ${name}, delete the lease file ${check.unconfirmedLeaseFile} and start it again.` : check.noAnswerFrom && (text += ` ADF could not confirm it: ${check.noAnswerFrom} did not answer the check, which a busy daemon also does. Wait a moment and try again; "adf daemon status" shows whether that daemon runs.`)), text;
62
+ }
63
+ function agentIdLeasedMessage(path, agentId, record, ownProcess, otherMachine = !1) {
64
+ let h = record.holder, where = ownProcess ? h.kind === "daemon" ? "in this ADF daemon" : "in this ADF Studio" : otherMachine ? `on another machine (${h.hostname || "unknown host"}, pid ${h.pid}) that shares this lease folder` : h.kind === "daemon" ? `in the ADF daemon (pid ${h.pid}${h.controlUrl ? `, ${h.controlUrl}` : ""})` : `in another ADF Studio (pid ${h.pid})`;
65
+ return `${agentNameOf(path)} was not started: ${path} has the agent id ${agentId}, and ${record.path} with the same id is already running ${where}. The two files are copies of one agent; running both would answer its timers and messages twice and poll its Telegram bot twice. Stop the running copy first, or keep only one of the two files.`;
66
+ }
67
+ function describeHolder(h) {
68
+ return `${h.kind === "daemon" ? "the ADF daemon" : "ADF Studio"} pid ${h.pid}`;
69
+ }
70
+ function delay(ms) {
71
+ return new Promise((r) => setTimeout(r, ms));
72
+ }
73
+ function errCode(err) {
74
+ return err?.code;
75
+ }
76
+ var MACHINE_ID_ATTEMPT_TIMEOUTS_MS = [3e3, 1e4], MACHINE_ID_RETRY_MS = 15e3, MACHINE_ID_WAIT_MS = 3e3, machineIdReader = readMachineId, machineIdRetryMs = MACHINE_ID_RETRY_MS, machineIdWaitMs = MACHINE_ID_WAIT_MS, machineIdLookup = null;
77
+ function machineId() {
78
+ let cached = machineIdLookup;
79
+ if (cached && (cached.failedAt === null || Date.now() - cached.failedAt < machineIdRetryMs)) return cached.promise;
80
+ let entry = { promise: Promise.resolve(null), failedAt: null };
81
+ return entry.promise = lookupMachineId().then((outcome) => ("failed" in outcome && (entry.failedAt = Date.now()), "id" in outcome ? outcome.id : null)), machineIdLookup = entry, entry.promise;
82
+ }
83
+ function machineIdLookupFailed() {
84
+ return machineIdLookup?.failedAt != null;
85
+ }
86
+ function machineIdWithin(waitMs = machineIdWaitMs) {
87
+ let lookup = machineId(), timer, waited = new Promise((resolve2) => {
88
+ timer = setTimeout(() => resolve2(null), waitMs), timer.unref?.();
89
+ });
90
+ return Promise.race([lookup, waited]).finally(() => clearTimeout(timer));
91
+ }
92
+ async function lookupMachineId() {
93
+ let lastError = null;
94
+ for (let timeoutMs of MACHINE_ID_ATTEMPT_TIMEOUTS_MS)
95
+ try {
96
+ let id = (await machineIdReader(timeoutMs))?.trim().toLowerCase() || null;
97
+ return id ? { id } : (console.warn("[AgentLease] This machine has no machine id (e.g. no /etc/machine-id in a container); this process's records carry none, and records of other runtimes are judged by this machine's pids"), { none: !0 });
98
+ } catch (err) {
99
+ lastError = err;
100
+ }
101
+ return console.warn(`[AgentLease] Could not read this machine's id (${lastError instanceof Error ? lastError.message : String(lastError)}); asked again after ${Math.ceil(machineIdRetryMs / 1e3)} s. Until then this process's records carry no machine id and records of other runtimes are judged by this machine's pids`), { failed: !0 };
102
+ }
103
+ function run(cmd, args, timeoutMs) {
104
+ return new Promise((ok, fail) => {
105
+ try {
106
+ execFile(cmd, args, { timeout: timeoutMs, windowsHide: !0, encoding: "utf-8" }, (err, stdout) => err ? fail(err) : ok(stdout));
107
+ } catch (err) {
108
+ fail(err);
109
+ }
110
+ });
111
+ }
112
+ async function readMachineId(timeoutMs) {
113
+ if (process.env.ADF_MACHINE_ID && testSeamsAllowed()) return process.env.ADF_MACHINE_ID;
114
+ if (process.platform === "linux") {
115
+ for (let file of ["/etc/machine-id", "/var/lib/dbus/machine-id"])
116
+ try {
117
+ let id = (await fsp.readFile(file, "utf-8")).trim();
118
+ if (id) return id;
119
+ } catch {
120
+ }
121
+ return null;
122
+ }
123
+ if (process.platform === "darwin")
124
+ return /"IOPlatformUUID"\s*=\s*"([^"]+)"/.exec(await run("ioreg", ["-rd1", "-c", "IOPlatformExpertDevice"], timeoutMs))?.[1] ?? null;
125
+ if (process.platform === "win32") {
126
+ let reg = join(process.env.SystemRoot ?? "C:\\Windows", "System32", "reg.exe");
127
+ return /MachineGuid\s+REG_SZ\s+(\S+)/i.exec(await run(reg, ["query", "HKLM\\SOFTWARE\\Microsoft\\Cryptography", "/v", "MachineGuid"], timeoutMs))?.[1] ?? null;
128
+ }
129
+ return null;
130
+ }
131
+ async function fromOtherMachine(record) {
132
+ if (!record.machineId) return !1;
133
+ let ours = await machineIdWithin();
134
+ return !!ours && record.machineId !== ours;
135
+ }
136
+ async function leaseHolderElsewhere(leases, filePath) {
137
+ if (!leases) return null;
138
+ try {
139
+ if (await leases.isHeld(filePath)) return null;
140
+ let canonical = await canonicalLeasePath(filePath), raw;
141
+ try {
142
+ raw = await fsp.readFile(join(leases.dir, `${leaseKeyForCanonicalPath(canonical)}.json`), "utf-8");
143
+ } catch {
144
+ return null;
145
+ }
146
+ let record = JSON.parse(raw);
147
+ if (record?.v !== 1 || typeof record.holder?.pid != "number" || typeof record.holder.runtimeId != "string" || record.holder.runtimeId === leases.runtimeId) return null;
148
+ let liveness = await holderLiveness(record.holder, leases.dir);
149
+ return liveness.alive ? agentLeasedMessage(record, void 0, liveness.otherMachine) : null;
150
+ } catch (err) {
151
+ return console.warn(`[AgentLease] Lease check of ${agentNameOf(filePath)} failed:`, err), null;
152
+ }
153
+ }
154
+ async function openHereOrLeasedElsewhere(leases, filePath, openHere) {
155
+ for (let p of openHere) if (samePath(p, filePath)) return !0;
156
+ return await leases.isHeld(filePath) ? !0 : await leaseHolderElsewhere(leases, filePath) !== null;
157
+ }
158
+ async function pidAlive(pid) {
159
+ if (!Number.isInteger(pid) || pid <= 0) return !1;
160
+ try {
161
+ process.kill(pid, 0);
162
+ } catch (err) {
163
+ return errCode(err) === "EPERM";
164
+ }
165
+ if (process.platform === "linux")
166
+ try {
167
+ let stat = await fsp.readFile(`/proc/${pid}/stat`, "utf-8");
168
+ if (stat.slice(stat.lastIndexOf(")") + 2).startsWith("Z")) return !1;
169
+ } catch {
170
+ }
171
+ return !0;
172
+ }
173
+ function probeEndpoint(endpoint, timeoutMs = PROBE_TIMEOUT_MS) {
174
+ return new Promise((resolveProbe) => {
175
+ let settled = !1, sock = null, done = (result) => {
176
+ settled || (settled = !0, clearTimeout(timer), sock?.destroy(), resolveProbe(result));
177
+ }, timer = setTimeout(() => done("unknown"), timeoutMs);
178
+ try {
179
+ sock = createConnection(endpoint);
180
+ } catch {
181
+ done("unknown");
182
+ return;
183
+ }
184
+ sock.once("connect", () => done("connected")), sock.once("error", (err) => {
185
+ let code = errCode(err);
186
+ done(code === "ECONNREFUSED" ? "refused" : code === "ENOENT" ? "missing" : "unknown");
187
+ });
188
+ });
189
+ }
190
+ function trustedEndpoint(holder, dir) {
191
+ let { endpoint, runtimeId } = holder;
192
+ return !endpoint || !/^[0-9a-f]{16}$/.test(runtimeId) ? null : process.platform === "win32" ? endpoint === pipeName(runtimeId) ? endpoint : null : endpoint === join(dir, "hosts", `${runtimeId}.sock`) || endpoint === join(tmpdir(), `adf-runtime-${runtimeId}.sock`) ? endpoint : null;
193
+ }
194
+ function probeProvesDead(endpoint, probe, dir) {
195
+ return probe === "refused" ? !0 : probe !== "missing" ? !1 : process.platform === "win32" || dirname(endpoint) === join(dir, "hosts");
196
+ }
197
+ function pipeName(runtimeId) {
198
+ return `\\\\.\\pipe\\adf-runtime-${runtimeId}`;
199
+ }
200
+ async function fetchDaemonPid(controlUrl) {
201
+ try {
202
+ let res = await fetch(`${controlUrl.replace(/\/+$/, "")}/version`, { signal: AbortSignal.timeout(PROBE_TIMEOUT_MS) });
203
+ if (!res.ok) return null;
204
+ let body = await res.json();
205
+ return typeof body.pid == "number" ? body.pid : null;
206
+ } catch {
207
+ return null;
208
+ }
209
+ }
210
+ async function holderLiveness(holder, dir = leaseDir()) {
211
+ if (await fromOtherMachine(holder)) return { alive: !0, otherMachine: !0, reason: "held on another machine" };
212
+ if (!await pidAlive(holder.pid)) return { alive: !1, reason: `pid ${holder.pid} is not running` };
213
+ let endpoint = trustedEndpoint(holder, dir);
214
+ if (endpoint) {
215
+ let probe = await probeEndpoint(endpoint);
216
+ return probe === "connected" ? { alive: !0, reason: "endpoint answered" } : probeProvesDead(endpoint, probe, dir) ? { alive: !1, reason: `pid ${holder.pid} now belongs to another program` } : { alive: !0, reason: `pid ${holder.pid} is running (endpoint ${probe})` };
217
+ }
218
+ if (holder.kind === "daemon" && holder.controlUrl) {
219
+ let pid = await fetchDaemonPid(holder.controlUrl);
220
+ return pid !== null && pid !== holder.pid ? { alive: !1, reason: `${holder.controlUrl} is served by pid ${pid}, not ${holder.pid}` } : pid === holder.pid ? { alive: !0, reason: `${holder.controlUrl} is served by pid ${pid}` } : { alive: !0, noAnswer: !0, reason: `pid ${holder.pid} is running; ${holder.controlUrl} did not answer` };
221
+ }
222
+ return !holder.endpoint && !holder.controlUrl ? { alive: !0, unconfirmed: !0, reason: `pid ${holder.pid} is running` } : { alive: !0, reason: `pid ${holder.pid} is running` };
223
+ }
224
+ function parseRecord(raw) {
225
+ try {
226
+ let rec = JSON.parse(raw);
227
+ return rec?.v !== 1 || typeof rec.path != "string" || typeof rec.holder?.pid != "number" || typeof rec.holder.runtimeId != "string" ? null : rec;
228
+ } catch {
229
+ return null;
230
+ }
231
+ }
232
+ async function readLease(file) {
233
+ try {
234
+ let [raw, st] = await Promise.all([fsp.readFile(file, "utf-8"), fsp.stat(file)]);
235
+ return { raw, record: parseRecord(raw), mtimeMs: st.mtimeMs };
236
+ } catch (err) {
237
+ if (errCode(err) === "ENOENT") return null;
238
+ throw err;
239
+ }
240
+ }
241
+ async function writeAtomic(file, data) {
242
+ let tmp = `${file}.${process.pid}-${randomBytes(4).toString("hex")}.tmp`;
243
+ await fsp.writeFile(tmp, data, { mode: FILE_MODE });
244
+ for (let attempt = 0; ; attempt++)
245
+ try {
246
+ await fsp.rename(tmp, file);
247
+ return;
248
+ } catch (err) {
249
+ let code = errCode(err);
250
+ if (attempt < 5 && code && TRANSIENT_FS_CODES.has(code)) {
251
+ await delay(20 * (attempt + 1));
252
+ continue;
253
+ }
254
+ throw await fsp.rm(tmp, { force: !0 }).catch(() => {
255
+ }), err;
256
+ }
257
+ }
258
+ async function unlockTakeover(lock) {
259
+ for (let attempt = 0; attempt < 200; attempt++)
260
+ try {
261
+ await fsp.rm(lock, { force: !0 });
262
+ return;
263
+ } catch (err) {
264
+ let code = errCode(err);
265
+ if (!code || !TRANSIENT_FS_CODES.has(code)) break;
266
+ await delay(5);
267
+ }
268
+ console.warn(`[AgentLease] Could not remove the takeover lock ${lock}; it expires after ${TAKEOVER_LOCK_TTL_MS / 1e3}s`);
269
+ }
270
+ var exitHookRegistries = /* @__PURE__ */ new Set(), exitHookInstalled = !1;
271
+ function inferKind() {
272
+ return !!process.versions.electron && process.env.ELECTRON_RUN_AS_NODE !== "1" && process.type === "browser" ? "studio-embedded" : "daemon";
273
+ }
274
+ function defaultControlUrl(env) {
275
+ let port = Number(env.ADF_DAEMON_PORT) || DEFAULT_DAEMON_PORT, host = env.ADF_DAEMON_HOST ?? "127.0.0.1";
276
+ return (host === "0.0.0.0" || host === "::" || host === "localhost") && (host = "127.0.0.1"), `http://${host.includes(":") ? `[${host}]` : host}:${port}`;
277
+ }
278
+ var AgentLeaseRegistry = class {
279
+ dir;
280
+ runtimeId = randomBytes(8).toString("hex");
281
+ startedAt = Date.now();
282
+ info;
283
+ held = /* @__PURE__ */ new Map();
284
+ chains = /* @__PURE__ */ new Map();
285
+ probes = /* @__PURE__ */ new Set();
286
+ /** abs path → target, for paths whose realpath resolved. */
287
+ targetCache = /* @__PURE__ */ new Map();
288
+ endpoint = null;
289
+ server = null;
290
+ ready = null;
291
+ hostEntryWritten = !1;
292
+ clientMode = null;
293
+ generation = 0;
294
+ pid;
295
+ hostnameOverride;
296
+ machineIdValue = null;
297
+ machineIdOverride;
298
+ /** Pending retry of a failed machine id lookup (scheduleMachineIdRetry). */
299
+ machineIdRetry = null;
300
+ takeoverLockTtlMs;
301
+ beforeTakeoverReplace;
302
+ sweepEntries;
303
+ /** Containers this runtime uses, written into its host record. */
304
+ containers = [];
305
+ /** The lease dir refused writes: remembered so every start does not stall on it. */
306
+ unusable = null;
307
+ constructor(opts = {}) {
308
+ this.dir = opts.dir ?? leaseDir(), this.pid = opts.pid ?? process.pid, this.hostnameOverride = opts.hostname, this.machineIdOverride = opts.machineId, this.takeoverLockTtlMs = opts.takeoverLockTtlMs ?? TAKEOVER_LOCK_TTL_MS, this.beforeTakeoverReplace = opts.beforeTakeoverReplace, this.sweepEntries = opts.sweepDeadEntries ?? !0;
309
+ let kind = opts.kind ?? inferKind();
310
+ this.info = {
311
+ kind,
312
+ settingsPath: opts.settingsPath ?? process.env.ADF_DAEMON_SETTINGS ?? join(getUserDataPath(), "adf-settings.json"),
313
+ version: opts.version ?? process.env.npm_package_version ?? null,
314
+ mode: opts.mode ?? (kind === "daemon" ? "daemon" : "embedded"),
315
+ controlUrl: opts.controlUrl ?? (kind === "daemon" ? defaultControlUrl(process.env) : void 0)
316
+ };
317
+ }
318
+ /**
319
+ * Who this process is, for lease and host records. Composition roots call
320
+ * it at boot (Studio embedded: registerAllIpcHandlers; daemon: its index)
321
+ * and it (re)writes the host entry.
322
+ */
323
+ configure(info) {
324
+ return this.info = {
325
+ ...this.info,
326
+ ...Object.fromEntries(Object.entries(info).filter(([, v]) => v !== void 0))
327
+ }, info.kind && !info.mode && (this.info.mode = info.kind === "daemon" ? "daemon" : "embedded"), info.kind === "studio-embedded" && !info.controlUrl && delete this.info.controlUrl, this.ensureReady(!0);
328
+ }
329
+ /** Register a host's view of the agents it runs or is starting. Returns the unregister function. */
330
+ addHostedPathsProbe(probe) {
331
+ return this.probes.add(probe), () => {
332
+ this.probes.delete(probe);
333
+ };
334
+ }
335
+ holder() {
336
+ return {
337
+ kind: this.info.kind,
338
+ pid: this.pid,
339
+ runtimeId: this.runtimeId,
340
+ hostname: this.hostnameOverride ?? osHostname(),
341
+ machineId: this.machineIdValue,
342
+ settingsPath: this.info.settingsPath,
343
+ startedAt: this.startedAt,
344
+ version: this.info.version,
345
+ endpoint: this.endpoint,
346
+ ...(this.info.kind === "daemon" || this.clientMode) && this.info.controlUrl ? { controlUrl: this.info.controlUrl } : {}
347
+ };
348
+ }
349
+ /**
350
+ * Take the lease for `filePath` before preparing the agent. Re-entrant
351
+ * within the process. The caller MUST keep the path visible to its probe
352
+ * from before this call until it no longer runs the agent, and call
353
+ * reconcile() when it stops (or fails to start).
354
+ */
355
+ async acquire(filePath, opts = {}) {
356
+ let target = await this.target(filePath), agentId = opts.agentId ?? null, { result, fresh } = await this.withKey(target.key, async () => {
357
+ let heldBefore = this.held.has(target.key);
358
+ return { result: await this.acquireLocked(target, agentId), fresh: !heldBefore };
359
+ });
360
+ if (!result.ok || result.unprotected || !agentId) return result;
361
+ let generation = this.held.get(target.key)?.generation, conflict = await this.agentIdConflict(target, agentId);
362
+ return conflict ? (fresh && await this.withKey(target.key, async () => {
363
+ this.held.get(target.key)?.generation === generation && await this.releaseLocked(target.key);
364
+ }), conflict) : result;
365
+ }
366
+ /** Release every held lease no probe lists. Never throws. */
367
+ async reconcile() {
368
+ if (this.held.size !== 0)
369
+ try {
370
+ let paths = [];
371
+ for (let probe of [...this.probes])
372
+ try {
373
+ for (let p of probe()) paths.push(p);
374
+ } catch (err) {
375
+ console.error("[AgentLease] Hosted-paths probe failed; nothing released:", err);
376
+ return;
377
+ }
378
+ let generations = new Map([...this.held].map(([key, h]) => [key, h.generation])), hosted = /* @__PURE__ */ new Set();
379
+ for (let p of paths) hosted.add((await this.target(p)).key);
380
+ for (let [key, generation] of generations)
381
+ hosted.has(key) || await this.withKey(key, async () => {
382
+ this.held.get(key)?.generation === generation && await this.releaseLocked(key);
383
+ });
384
+ } catch (err) {
385
+ console.error("[AgentLease] Reconcile failed:", err);
386
+ }
387
+ }
388
+ /**
389
+ * The agent file moved (rename while held). Hosts update their own path
390
+ * maps first, then call this: the lease moves to the new path's key.
391
+ */
392
+ async rekey(oldPath, newPath) {
393
+ let oldKey = await this.findHeldKey(oldPath);
394
+ if (!oldKey) return null;
395
+ let target = await this.target(newPath), previous = this.held.get(oldKey);
396
+ if (target.key === oldKey)
397
+ return this.withKey(oldKey, async () => {
398
+ let entry = this.held.get(oldKey);
399
+ if (!entry) return null;
400
+ entry.record = { ...entry.record, path: target.canonical };
401
+ try {
402
+ await writeAtomic(entry.file, JSON.stringify(entry.record, null, 2));
403
+ } catch (err) {
404
+ console.warn(`[AgentLease] Could not update the lease of ${agentNameOf(target.canonical)}:`, err);
405
+ }
406
+ return { ok: !0, leaseFile: entry.file };
407
+ });
408
+ let result = await this.withKey(target.key, () => this.acquireLocked(target, previous.record.agentId));
409
+ return result.ok || console.warn(`[AgentLease] Rename of ${agentNameOf(previous.record.path)}: ${result.message}`), await this.withKey(oldKey, () => this.releaseLocked(oldKey)), result;
410
+ }
411
+ /**
412
+ * Record the agent id in a lease taken before the file was opened (a
413
+ * daemon load and a background start acquire first, then read the
414
+ * config), then check it as acquire() does: a refusal when another live
415
+ * holder runs that agent id from another file, else null. No-op (null)
416
+ * when this process does not hold the lease. Never throws.
417
+ */
418
+ async noteAgentId(filePath, agentId) {
419
+ try {
420
+ let target = await this.target(filePath);
421
+ return await this.withKey(target.key, async () => {
422
+ let entry = this.held.get(target.key);
423
+ return entry ? (entry.record.agentId === agentId || (entry.record = { ...entry.record, agentId }, await writeAtomic(entry.file, JSON.stringify(entry.record, null, 2))), !0) : !1;
424
+ }) ? await this.agentIdConflict(target, agentId) : null;
425
+ } catch (err) {
426
+ return console.warn(`[AgentLease] Could not record the agent id in the lease of ${agentNameOf(filePath)}:`, err), null;
427
+ }
428
+ }
429
+ /** True when this process holds the lease for `filePath`. */
430
+ async isHeld(filePath) {
431
+ return this.held.has((await this.target(filePath)).key);
432
+ }
433
+ /** Canonical paths of the agents this process holds leases for. */
434
+ heldPaths() {
435
+ return [...this.held.values()].map((h) => h.record.path);
436
+ }
437
+ /**
438
+ * Studio in local/remote mode runs no agents, so it is not a host; it
439
+ * records itself as a client so `adf daemon start` knows the running
440
+ * Studio is not a conflict.
441
+ */
442
+ registerClient(mode, info = {}) {
443
+ return this.clientMode = mode, info.settingsPath && (this.info.settingsPath = info.settingsPath), info.version !== void 0 && (this.info.version = info.version), info.controlUrl ? this.info.controlUrl = info.controlUrl : delete this.info.controlUrl, this.ensureReady(!0);
444
+ }
445
+ /** Test / shutdown helper: release everything, remove host entries, close the endpoint. */
446
+ async close() {
447
+ await this.ready?.catch(() => {
448
+ }), this.machineIdRetry && clearTimeout(this.machineIdRetry), this.machineIdRetry = null;
449
+ for (let key of [...this.held.keys()]) await this.withKey(key, () => this.releaseLocked(key));
450
+ await this.removeOwnEntries();
451
+ let server = this.server;
452
+ this.server = null, this.endpoint = null, this.ready = null, this.hostEntryWritten = !1, server && await new Promise((r) => server.close(() => r())), exitHookRegistries.delete(this);
453
+ }
454
+ // --- internals -----------------------------------------------------------
455
+ async target(filePath) {
456
+ let abs = resolve(filePath), cached = this.targetCache.get(abs);
457
+ if (cached) return cached;
458
+ let canonical, resolved = !0;
459
+ try {
460
+ canonical = stripLongPathPrefix(await fsp.realpath(abs));
461
+ } catch {
462
+ canonical = stripLongPathPrefix(abs), resolved = !1;
463
+ }
464
+ let key = leaseKeyForCanonicalPath(canonical), target = { key, canonical, file: join(this.dir, `${key}.json`) };
465
+ return resolved && this.targetCache.set(abs, target), target;
466
+ }
467
+ async findHeldKey(path) {
468
+ let key = (await this.target(path)).key;
469
+ if (this.held.has(key)) return key;
470
+ let abs = resolve(path);
471
+ for (let [k, h] of this.held) {
472
+ if (samePath(h.record.path, abs)) return k;
473
+ for (let [raw, t] of this.targetCache)
474
+ if (t.key === k && samePath(raw, abs)) return k;
475
+ }
476
+ return null;
477
+ }
478
+ withKey(key, fn) {
479
+ let run2 = (this.chains.get(key) ?? Promise.resolve()).then(fn, fn), tail = run2.catch(() => {
480
+ });
481
+ return this.chains.set(key, tail), tail.then(() => {
482
+ this.chains.get(key) === tail && this.chains.delete(key);
483
+ }), run2;
484
+ }
485
+ async acquireLocked(target, agentId) {
486
+ await this.ensureReady(!1);
487
+ let name = agentNameOf(target.canonical), own = this.held.get(target.key);
488
+ if (own)
489
+ return own.generation = ++this.generation, await this.verifyOwn(target, own), { ok: !0, leaseFile: target.file };
490
+ if (this.unusable && Date.now() - this.unusable.at < UNUSABLE_DIR_MEMO_MS)
491
+ return this.unprotected(name, target, this.unusable.reason, !1);
492
+ let record = { v: 1, path: target.canonical, agentId, holder: this.holder(), acquiredAt: Date.now() }, body = JSON.stringify(record, null, 2), deadline = Date.now() + ACQUIRE_BUDGET_MS, won = async () => {
493
+ let entry = { file: target.file, record, generation: ++this.generation };
494
+ this.held.set(target.key, entry), this.unusable = null, this.installExitHook();
495
+ let id = this.machineIdValue;
496
+ return id && record.holder.machineId !== id && await this.stampLease(entry, id), { ok: !0, leaseFile: target.file };
497
+ }, lastError = null;
498
+ try {
499
+ await fsp.mkdir(this.dir, { recursive: !0, mode: DIR_MODE });
500
+ } catch (err) {
501
+ return this.unprotected(name, target, err instanceof Error ? err.message : String(err), !0);
502
+ }
503
+ for (; Date.now() < deadline; ) {
504
+ try {
505
+ return await fsp.writeFile(target.file, body, { flag: "wx", mode: FILE_MODE }), await won();
506
+ } catch (err) {
507
+ let code = errCode(err);
508
+ if (code !== "EEXIST") {
509
+ if (lastError = err, code && TRANSIENT_FS_CODES.has(code) && await this.dirWritable()) {
510
+ await delay(20);
511
+ continue;
512
+ }
513
+ return this.unprotected(name, target, err instanceof Error ? err.message : String(err), !0);
514
+ }
515
+ }
516
+ let existing;
517
+ try {
518
+ existing = await readLease(target.file);
519
+ } catch (err) {
520
+ lastError = err, await delay(50);
521
+ continue;
522
+ }
523
+ if (!existing) continue;
524
+ if (!existing.record) {
525
+ let age = Date.now() - existing.mtimeMs;
526
+ if (age < PARTIAL_WRITE_GRACE_MS) {
527
+ await delay(Math.min(250, PARTIAL_WRITE_GRACE_MS - age + 10));
528
+ continue;
529
+ }
530
+ if (console.warn(`[AgentLease] ${name}: replacing an unreadable lease file ${target.file}`), await this.takeOver(target.file, existing.raw, body)) return await won();
531
+ continue;
532
+ }
533
+ let holder = existing.record.holder;
534
+ if (holder.runtimeId === this.runtimeId)
535
+ return this.held.set(target.key, { file: target.file, record: existing.record, generation: ++this.generation }), { ok: !0, leaseFile: target.file };
536
+ let liveness = await holderLiveness(holder, this.dir);
537
+ if (liveness.alive)
538
+ return {
539
+ ok: !1,
540
+ code: AGENT_LEASED_CODE,
541
+ leaseFile: target.file,
542
+ record: existing.record,
543
+ message: agentLeasedMessage(existing.record, this.info.settingsPath, liveness.otherMachine, {
544
+ ...liveness.unconfirmed ? { unconfirmedLeaseFile: target.file } : {},
545
+ ...liveness.noAnswer && holder.controlUrl ? { noAnswerFrom: holder.controlUrl } : {}
546
+ })
547
+ };
548
+ if (console.warn(`[AgentLease] ${name}: replacing a stale lease held by ${describeHolder(holder)} (${liveness.reason})`), await this.takeOver(target.file, existing.raw, body)) return await won();
549
+ }
550
+ let reason = lastError instanceof Error ? lastError.message : "the lease file kept changing";
551
+ console.error(`[AgentLease] ${name}: could not take the lease (${reason})`);
552
+ let placeholder = { ...record, holder: { ...record.holder, pid: 0 } };
553
+ return {
554
+ ok: !1,
555
+ code: AGENT_LEASED_CODE,
556
+ leaseFile: target.file,
557
+ record: placeholder,
558
+ message: `${name} could not be started: its lease file ${target.file} could not be read or replaced (${reason}). If no other ADF Studio or daemon runs this agent, delete that file and start it again.`
559
+ };
560
+ }
561
+ /**
562
+ * The lease is keyed by path, so a copied or restored `.adf` (same agent
563
+ * id, another file) got its own lease, and two runtimes, or one runtime
564
+ * twice, ran one agent: both answered its timers and both polled its
565
+ * Telegram bot (W5-D3). Checked after our path lease is written: two
566
+ * hosts starting two copies at once each see the other's lease (at worst
567
+ * both refuse; never both run). Another holder's lease that is stale or
568
+ * unreadable is ignored here; whoever starts its path replaces it.
569
+ */
570
+ async agentIdConflict(target, agentId) {
571
+ for (let [key, h] of this.held)
572
+ if (key !== target.key && h.record.agentId === agentId)
573
+ return this.agentIdRefusal(target, agentId, h.file, h.record, !0);
574
+ let names;
575
+ try {
576
+ names = await fsp.readdir(this.dir);
577
+ } catch {
578
+ return null;
579
+ }
580
+ for (let name of names) {
581
+ let m = /^([0-9a-f]{64})\.json$/.exec(name);
582
+ if (!m || m[1] === target.key || this.held.has(m[1])) continue;
583
+ let file = join(this.dir, name), record = null;
584
+ try {
585
+ record = (await readLease(file))?.record ?? null;
586
+ } catch {
587
+ continue;
588
+ }
589
+ if (!record || record.agentId !== agentId || record.holder.runtimeId === this.runtimeId) continue;
590
+ let liveness = await holderLiveness(record.holder, this.dir);
591
+ if (liveness.alive)
592
+ return this.agentIdRefusal(target, agentId, file, record, !1, liveness.otherMachine);
593
+ }
594
+ return null;
595
+ }
596
+ agentIdRefusal(target, agentId, leaseFile, record, ownProcess, otherMachine = !1) {
597
+ let message = agentIdLeasedMessage(target.canonical, agentId, record, ownProcess, otherMachine);
598
+ return console.warn(`[AgentLease] ${message}`), { ok: !1, code: AGENT_LEASED_CODE, leaseFile, record, message };
599
+ }
600
+ /** Can a fresh file be created in the lease dir? Tells a delete-pending lease from an unusable dir. */
601
+ async dirWritable() {
602
+ let probe = join(this.dir, `.${this.runtimeId}-${randomBytes(4).toString("hex")}.probe`);
603
+ try {
604
+ return await fsp.writeFile(probe, "", { flag: "wx", mode: FILE_MODE }), await fsp.rm(probe, { force: !0 }), !0;
605
+ } catch {
606
+ return !1;
607
+ }
608
+ }
609
+ /**
610
+ * The lease dir refused a write: proceed without a lease and say so. The
611
+ * dir is remembered as unusable for a minute (`remember`), so the next
612
+ * starts skip it instead of retrying.
613
+ */
614
+ unprotected(name, target, reason, remember) {
615
+ let message = `Agent leases unavailable: ${this.dir} cannot be written (${reason}). ${name} runs without protection against a second ADF Studio or daemon on this machine running it.`;
616
+ return remember && (this.unusable = { reason, at: Date.now() }, console.error(`[AgentLease] ${message}`)), { ok: !0, leaseFile: target.file, unprotected: message };
617
+ }
618
+ /** Our lease file should exist and name us. Recreate it if it was deleted. */
619
+ async verifyOwn(target, own) {
620
+ try {
621
+ let current = await readLease(own.file);
622
+ current ? current.record && current.record.holder.runtimeId !== this.runtimeId && console.warn(`[AgentLease] ${agentNameOf(target.canonical)}: lease file now names ${describeHolder(current.record.holder)} while this process runs the agent`) : (await fsp.mkdir(this.dir, { recursive: !0, mode: DIR_MODE }), await fsp.writeFile(own.file, JSON.stringify(own.record, null, 2), { flag: "wx", mode: FILE_MODE }), console.warn(`[AgentLease] ${agentNameOf(target.canonical)}: lease file was missing; recreated`));
623
+ } catch (err) {
624
+ console.warn(`[AgentLease] ${agentNameOf(target.canonical)}: could not verify the lease:`, err);
625
+ }
626
+ }
627
+ /**
628
+ * Replace a lease judged stale (its bytes were `expectedRaw`) with ours,
629
+ * under the takeover lock. Under the lock the lease is re-read: it is
630
+ * removed only if it still holds exactly those bytes, and ours is written
631
+ * exclusively (a plain acquirer that slipped in wins; the caller then
632
+ * re-evaluates). Returns true when our lease was written.
633
+ */
634
+ async takeOver(file, expectedRaw, body) {
635
+ let lock = `${file}.takeover`;
636
+ if (!await this.lockTakeover(lock)) return !1;
637
+ try {
638
+ let current = null;
639
+ try {
640
+ current = await fsp.readFile(file, "utf-8");
641
+ } catch (err) {
642
+ if (errCode(err) !== "ENOENT") return !1;
643
+ }
644
+ if (current !== null && current !== expectedRaw) return !1;
645
+ await this.beforeTakeoverReplace?.();
646
+ try {
647
+ current = await fsp.readFile(file, "utf-8");
648
+ } catch (err) {
649
+ if (errCode(err) !== "ENOENT") return !1;
650
+ current = null;
651
+ }
652
+ if (current !== null && current !== expectedRaw) return !1;
653
+ current !== null && await fsp.rm(file, { force: !0 });
654
+ for (let attempt = 0; ; attempt++)
655
+ try {
656
+ return await fsp.writeFile(file, body, { flag: "wx", mode: FILE_MODE }), !0;
657
+ } catch (err) {
658
+ let code = errCode(err);
659
+ if (code === "EEXIST") return !1;
660
+ if (!code || !TRANSIENT_FS_CODES.has(code) || attempt >= 20) throw err;
661
+ await delay(10);
662
+ }
663
+ } catch (err) {
664
+ return console.warn(`[AgentLease] Takeover of ${file} failed:`, err instanceof Error ? err.message : err), !1;
665
+ } finally {
666
+ await unlockTakeover(lock);
667
+ }
668
+ }
669
+ /**
670
+ * Create `<lease>.takeover` exclusively. A lock left by a crash (its pid
671
+ * gone, or older than the TTL, while the critical section takes
672
+ * milliseconds) is removed.
673
+ */
674
+ async lockTakeover(lock) {
675
+ let deadline = Date.now() + TAKEOVER_LOCK_WAIT_MS, judged = /* @__PURE__ */ new Map();
676
+ for (; Date.now() < deadline; ) {
677
+ try {
678
+ return await fsp.writeFile(lock, JSON.stringify({ pid: this.pid, runtimeId: this.runtimeId, endpoint: this.endpoint, machineId: this.machineIdValue, at: Date.now() }), { flag: "wx", mode: FILE_MODE }), !0;
679
+ } catch (err) {
680
+ let code = errCode(err);
681
+ if (code && TRANSIENT_FS_CODES.has(code)) {
682
+ await delay(5);
683
+ continue;
684
+ }
685
+ if (code !== "EEXIST") return !1;
686
+ }
687
+ try {
688
+ let st = await fsp.stat(lock), age = Date.now() - st.mtimeMs;
689
+ if (age > TAKEOVER_LOCK_SUSPECT_MS) {
690
+ let raw = await fsp.readFile(lock, "utf-8"), last = judged.get(raw), fresh = last && (last.state !== "live" || Date.now() - last.at < TAKEOVER_LOCK_REPROBE_MS), state = fresh ? last.state : await this.lockHolderState(raw);
691
+ if (fresh || judged.set(raw, { state, at: Date.now() }), state === "dead" || state === "unknown" && age > this.takeoverLockTtlMs) {
692
+ await fsp.readFile(lock, "utf-8") === raw && (console.warn(`[AgentLease] Removing a takeover lock left by a ${state === "dead" ? "crashed" : "stuck"} process: ${lock}`), await fsp.rm(lock, { force: !0 }));
693
+ continue;
694
+ }
695
+ }
696
+ } catch {
697
+ }
698
+ await delay(5 + Math.floor(Math.random() * 15));
699
+ }
700
+ return !1;
701
+ }
702
+ /**
703
+ * Who holds a takeover lock: 'dead' (pid gone, endpoint missing or
704
+ * refusing) is removed; 'live' (anything else while the pid runs,
705
+ * including a probe timeout) never expires; 'unknown' (no readable pid,
706
+ * e.g. a crash between create and write) waits for the TTL.
707
+ */
708
+ async lockHolderState(raw) {
709
+ let holder;
710
+ try {
711
+ holder = JSON.parse(raw);
712
+ } catch {
713
+ return "unknown";
714
+ }
715
+ let pid = Number(holder.pid), rec = {
716
+ pid,
717
+ runtimeId: typeof holder.runtimeId == "string" ? holder.runtimeId : "",
718
+ endpoint: typeof holder.endpoint == "string" ? holder.endpoint : null,
719
+ machineId: typeof holder.machineId == "string" ? holder.machineId : null
720
+ };
721
+ if (!Number.isInteger(pid) || pid <= 0) return "unknown";
722
+ if (await fromOtherMachine(rec)) return "live";
723
+ if (!await pidAlive(pid)) return "dead";
724
+ let endpoint = trustedEndpoint(rec, this.dir);
725
+ if (!endpoint) return "live";
726
+ let probe = await probeEndpoint(endpoint);
727
+ return probeProvesDead(endpoint, probe, this.dir) ? "dead" : "live";
728
+ }
729
+ async releaseLocked(key) {
730
+ let entry = this.held.get(key);
731
+ if (entry) {
732
+ this.held.delete(key);
733
+ try {
734
+ let current = await readLease(entry.file);
735
+ if (!current) return;
736
+ if (current.record && current.record.holder.runtimeId !== this.runtimeId) {
737
+ console.warn(`[AgentLease] ${agentNameOf(entry.record.path)}: lease now names ${describeHolder(current.record.holder)}; left in place`);
738
+ return;
739
+ }
740
+ await fsp.rm(entry.file, { force: !0 });
741
+ } catch (err) {
742
+ console.error(`[AgentLease] Could not release the lease of ${agentNameOf(entry.record.path)}:`, err);
743
+ }
744
+ }
745
+ }
746
+ installExitHook() {
747
+ exitHookRegistries.add(this), !exitHookInstalled && (exitHookInstalled = !0, process.once("exit", () => {
748
+ for (let reg of exitHookRegistries) reg.releaseAllSync();
749
+ }));
750
+ }
751
+ /** Process exit: synchronous by necessity. Removes only files that still name us. */
752
+ releaseAllSync() {
753
+ for (let entry of this.held.values()) this.unlinkIfOwnSync(entry.file);
754
+ this.held.clear(), this.hostEntryWritten && this.unlinkIfOwnSync(this.entryFile());
755
+ }
756
+ unlinkIfOwnSync(file) {
757
+ try {
758
+ let rec = JSON.parse(readFileSync(file, "utf-8"));
759
+ (rec.holder?.runtimeId ?? rec.runtimeId) === this.runtimeId && unlinkSync(file);
760
+ } catch {
761
+ }
762
+ }
763
+ entryFile() {
764
+ return join(this.dir, this.clientMode ? "clients" : "hosts", `${this.pid}.json`);
765
+ }
766
+ /** This runtime's host record (`hosts/<pid>.json`). */
767
+ get hostEntryFile() {
768
+ return this.entryFile();
769
+ }
770
+ /**
771
+ * services/compute-users.ts: record the containers this runtime uses in
772
+ * its host record. Rejects when the record cannot be written.
773
+ */
774
+ setContainers(names) {
775
+ this.containers = [...new Set(names)].sort();
776
+ let run2 = this.ensureReady(!1).then(() => this.writeEntryOrThrow());
777
+ return this.ready = run2.catch(() => {
778
+ }), run2;
779
+ }
780
+ /** Refresh the host record's mtime (compute-users heartbeat). Rejects when it is gone. */
781
+ async touchEntry() {
782
+ let now = /* @__PURE__ */ new Date();
783
+ await fsp.utimes(this.entryFile(), now, now);
784
+ }
785
+ ensureReady(rewriteEntry) {
786
+ if (this.ready)
787
+ rewriteEntry && (this.ready = this.ready.then(() => this.writeEntry()));
788
+ else {
789
+ let lookup;
790
+ this.ready = this.startEndpoint().then(async () => {
791
+ if (this.machineIdOverride !== void 0) {
792
+ this.machineIdValue = this.machineIdOverride;
793
+ return;
794
+ }
795
+ lookup = machineId(), this.machineIdValue = await machineIdWithin();
796
+ }).then(() => this.writeEntry()).then(() => this.scheduleMachineIdRetry(lookup));
797
+ }
798
+ return this.ready;
799
+ }
800
+ /**
801
+ * The machine id was not read when the host record was first written: wait
802
+ * for the lookup still running (`pending`), else, after a failed lookup,
803
+ * ask again every MACHINE_ID_RETRY_MS (15 s), and once read rewrite the
804
+ * host record and every lease this process holds. A platform with no
805
+ * machine id at all is not asked again. A record without a machine id is judged
806
+ * by the reader's own pids, so a runtime on another machine sharing the
807
+ * lease folder could take a live agent from this one.
808
+ */
809
+ scheduleMachineIdRetry(pending) {
810
+ if (this.machineIdValue || this.machineIdOverride !== void 0 || this.machineIdRetry || !this.ready) return;
811
+ let settle = (lookup) => {
812
+ lookup.then(async (id) => {
813
+ if (!(!this.ready || this.machineIdValue)) {
814
+ if (!id) {
815
+ machineIdLookupFailed() && this.scheduleMachineIdRetry();
816
+ return;
817
+ }
818
+ this.machineIdValue = id, await this.stampMachineId(id);
819
+ }
820
+ });
821
+ };
822
+ if (pending) {
823
+ settle(pending);
824
+ return;
825
+ }
826
+ this.machineIdRetry = setTimeout(() => {
827
+ this.machineIdRetry = null, settle(machineId());
828
+ }, machineIdRetryMs), this.machineIdRetry.unref?.();
829
+ }
830
+ /** Write `id` into the host record and into every held lease that still names this runtime. */
831
+ async stampMachineId(id) {
832
+ this.ready && await this.ensureReady(!0);
833
+ let leases = 0;
834
+ for (let [key, entry] of [...this.held])
835
+ await this.withKey(key, async () => {
836
+ this.held.get(key) === entry && await this.stampLease(entry, id) && leases++;
837
+ });
838
+ console.log(`[AgentLease] Read this machine's id late; added it to the host record and ${leases} held lease(s)`);
839
+ }
840
+ /**
841
+ * Rewrite one held lease with machine id `id`. The caller holds the key's
842
+ * chain. The read and the write run under the lease's takeover lock, and
843
+ * only a lease that still names this runtime is rewritten: a taker that
844
+ * judged it stale and replaced it keeps its own.
845
+ */
846
+ async stampLease(entry, id) {
847
+ let lock = `${entry.file}.takeover`;
848
+ try {
849
+ if (!await this.lockTakeover(lock)) return !1;
850
+ } catch (err) {
851
+ return console.warn(`[AgentLease] ${agentNameOf(entry.record.path)}: could not add the machine id to its lease:`, err instanceof Error ? err.message : err), !1;
852
+ }
853
+ try {
854
+ let current = await readLease(entry.file);
855
+ if (!current?.record || current.record.holder.runtimeId !== this.runtimeId) return !1;
856
+ let record = { ...current.record, holder: { ...current.record.holder, machineId: id } };
857
+ return await writeAtomic(entry.file, JSON.stringify(record, null, 2)), entry.record = record, !0;
858
+ } catch (err) {
859
+ return console.warn(`[AgentLease] ${agentNameOf(entry.record.path)}: could not add the machine id to its lease:`, err instanceof Error ? err.message : err), !1;
860
+ } finally {
861
+ await unlockTakeover(lock);
862
+ }
863
+ }
864
+ async startEndpoint() {
865
+ let endpoint;
866
+ process.platform === "win32" ? endpoint = pipeName(this.runtimeId) : (endpoint = join(this.dir, "hosts", `${this.runtimeId}.sock`), endpoint.length > MAX_SOCKET_PATH && (endpoint = join(tmpdir(), `adf-runtime-${this.runtimeId}.sock`)));
867
+ try {
868
+ process.platform !== "win32" && await fsp.mkdir(join(this.dir, "hosts"), { recursive: !0, mode: DIR_MODE });
869
+ let server = createServer((socket) => {
870
+ socket.on("error", () => {
871
+ }), socket.end(`${JSON.stringify({ pid: process.pid, runtimeId: this.runtimeId })}
872
+ `);
873
+ });
874
+ await new Promise((ok, fail) => {
875
+ server.once("error", fail), server.listen(endpoint, () => {
876
+ server.off("error", fail), ok();
877
+ });
878
+ }), server.on("error", (err) => console.error("[AgentLease] Endpoint error:", err)), server.unref(), this.server = server, this.endpoint = endpoint;
879
+ } catch (err) {
880
+ console.warn(`[AgentLease] Could not listen on ${endpoint}; leases fall back to pid checks:`, err instanceof Error ? err.message : err);
881
+ }
882
+ }
883
+ async writeEntry() {
884
+ try {
885
+ await this.writeEntryOrThrow();
886
+ } catch (err) {
887
+ console.warn(`[AgentLease] Could not write the runtime host entry in ${this.dir}:`, err instanceof Error ? err.message : err);
888
+ }
889
+ }
890
+ async writeEntryOrThrow() {
891
+ let kind = this.clientMode ? "studio-client" : this.info.kind, other = join(this.dir, this.clientMode ? "hosts" : "clients", `${this.pid}.json`);
892
+ this.unlinkIfOwnSync(other);
893
+ let entry = {
894
+ v: 1,
895
+ ...this.holder(),
896
+ kind,
897
+ mode: this.clientMode ?? this.info.mode,
898
+ ...this.clientMode ? {} : { containers: this.containers }
899
+ };
900
+ await fsp.mkdir(join(this.dir, this.clientMode ? "clients" : "hosts"), { recursive: !0, mode: DIR_MODE }), await writeAtomic(this.entryFile(), JSON.stringify(entry, null, 2)), this.hostEntryWritten = !0, this.installExitHook(), this.sweepEntries && sweepDeadEntries(this.dir, this.pid).catch(() => {
901
+ });
902
+ }
903
+ async removeOwnEntries() {
904
+ for (let sub of ["hosts", "clients"]) this.unlinkIfOwnSync(join(this.dir, sub, `${this.pid}.json`));
905
+ this.hostEntryWritten = !1;
906
+ }
907
+ };
908
+ function parseHostRecord(raw) {
909
+ try {
910
+ let record = JSON.parse(raw);
911
+ return record?.v !== 1 || typeof record.pid != "number" || typeof record.runtimeId != "string" || record.containers !== void 0 && !(Array.isArray(record.containers) && record.containers.every((c) => typeof c == "string")) ? null : record;
912
+ } catch {
913
+ return null;
914
+ }
915
+ }
916
+ async function readEntries(dir) {
917
+ let names;
918
+ try {
919
+ names = await fsp.readdir(dir);
920
+ } catch {
921
+ return [];
922
+ }
923
+ let out = [];
924
+ for (let name of names) {
925
+ if (!/^\d+\.json$/.test(name)) continue;
926
+ let file = join(dir, name);
927
+ try {
928
+ let raw = await fsp.readFile(file, "utf-8"), record = parseHostRecord(raw);
929
+ record && out.push({ file, raw, record });
930
+ } catch {
931
+ }
932
+ }
933
+ return out;
934
+ }
935
+ async function sweepDeadEntries(dir, ownPid) {
936
+ for (let sub of ["hosts", "clients"])
937
+ for (let { file, raw, record } of await readEntries(join(dir, sub)))
938
+ if (record.pid !== ownPid && !(await holderLiveness(record, dir)).alive)
939
+ try {
940
+ await fsp.readFile(file, "utf-8") === raw && await fsp.rm(file, { force: !0 });
941
+ } catch {
942
+ }
943
+ }
944
+ async function listRuntimeHosts(dir = leaseDir()) {
945
+ return listEntries(dir, "hosts");
946
+ }
947
+ async function listStudioClients(dir = leaseDir()) {
948
+ return listEntries(dir, "clients");
949
+ }
950
+ async function listEntries(dir, sub) {
951
+ let entries = await readEntries(join(dir, sub));
952
+ return Promise.all(entries.map(async ({ record }) => ({
953
+ ...record,
954
+ alive: record.pid === process.pid ? !0 : (await holderLiveness(record, dir)).alive
955
+ })));
956
+ }
957
+ var processRegistry = null;
958
+ function processAgentLeases() {
959
+ return processRegistry ??= new AgentLeaseRegistry(), processRegistry;
960
+ }
961
+
962
+ export {
963
+ AGENT_LEASED_CODE,
964
+ leaseDir,
965
+ fromOtherMachine,
966
+ leaseHolderElsewhere,
967
+ openHereOrLeasedElsewhere,
968
+ pidAlive,
969
+ trustedEndpoint,
970
+ holderLiveness,
971
+ AgentLeaseRegistry,
972
+ parseHostRecord,
973
+ listRuntimeHosts,
974
+ listStudioClients,
975
+ processAgentLeases
976
+ };