@vtxmacro/cli 2026.9.68 → 2026.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/vtx-mcp.js +21 -2
- package/bin/vtx-service-bootstrap.js +293 -126
- package/bin/vtx-service-launcher.js +289 -122
- package/bin/vtx.js +582 -240
- package/package.json +1 -1
package/bin/vtx-mcp.js
CHANGED
|
@@ -53,7 +53,8 @@ async function parseError(response, fallback) {
|
|
|
53
53
|
detail = null;
|
|
54
54
|
}
|
|
55
55
|
const rawDetail = detail && typeof detail === "object" && "detail" in detail ? detail.detail : detail;
|
|
56
|
-
const
|
|
56
|
+
const detailMessage = rawDetail && typeof rawDetail === "object" && typeof rawDetail.message === "string" ? String(rawDetail.message).trim() : "";
|
|
57
|
+
const message = typeof rawDetail === "string" ? rawDetail : detailMessage || fallback;
|
|
57
58
|
const code = rawDetail && typeof rawDetail === "object" && "code" in rawDetail ? String(rawDetail.code || "") : void 0;
|
|
58
59
|
return new AgentClientError({
|
|
59
60
|
status: response.status,
|
|
@@ -610,7 +611,16 @@ var VTX_MCP_TOOLS = [
|
|
|
610
611
|
size: { type: "number" },
|
|
611
612
|
slippage: { type: "number" },
|
|
612
613
|
estimatedPrice: { type: "number" },
|
|
613
|
-
estimatedPriceTimestampMs: { type: "number" }
|
|
614
|
+
estimatedPriceTimestampMs: { type: "number" },
|
|
615
|
+
reduceOnly: { type: "boolean" },
|
|
616
|
+
protection: {
|
|
617
|
+
type: "object",
|
|
618
|
+
description: 'Optional fixed TP/SL like the AI page Auto SL/TP: take_profit and/or stop_loss, each { mode: "percent" | "usdc", value }. Placed as reduce-only market triggers from the average fill; not allowed with reduceOnly; no trailing stops.',
|
|
619
|
+
properties: {
|
|
620
|
+
take_profit: { type: "object", required: ["mode", "value"], properties: { mode: { type: "string", enum: ["percent", "usdc"] }, value: { type: "number" } } },
|
|
621
|
+
stop_loss: { type: "object", required: ["mode", "value"], properties: { mode: { type: "string", enum: ["percent", "usdc"] }, value: { type: "number" }, stop_type: { type: "string", enum: ["fixed"] } } }
|
|
622
|
+
}
|
|
623
|
+
}
|
|
614
624
|
}
|
|
615
625
|
}
|
|
616
626
|
},
|
|
@@ -815,6 +825,15 @@ function marketOrderPayload(input) {
|
|
|
815
825
|
if (estimatedPriceTimestampMs !== void 0) {
|
|
816
826
|
payload.estimated_price_timestamp_ms = estimatedPriceTimestampMs;
|
|
817
827
|
}
|
|
828
|
+
if (input.reduceOnly === true) {
|
|
829
|
+
payload.reduce_only = true;
|
|
830
|
+
}
|
|
831
|
+
if (input.protection !== void 0) {
|
|
832
|
+
if (input.protection === null || typeof input.protection !== "object" || Array.isArray(input.protection)) {
|
|
833
|
+
throw new Error("protection must be an object with take_profit and/or stop_loss");
|
|
834
|
+
}
|
|
835
|
+
payload.protection = input.protection;
|
|
836
|
+
}
|
|
818
837
|
return payload;
|
|
819
838
|
}
|
|
820
839
|
function limitOrderPayload(input) {
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
appendFile,
|
|
7
7
|
mkdir as mkdir2,
|
|
8
8
|
readFile as readFile3,
|
|
9
|
-
readdir,
|
|
9
|
+
readdir as readdir2,
|
|
10
10
|
rename as rename2,
|
|
11
11
|
rm as rm3
|
|
12
12
|
} from "node:fs/promises";
|
|
@@ -16,7 +16,7 @@ import { fileURLToPath } from "node:url";
|
|
|
16
16
|
// agent-cli-release.json
|
|
17
17
|
var agent_cli_release_default = {
|
|
18
18
|
package_name: "@vtxmacro/cli",
|
|
19
|
-
package_version: "2026.
|
|
19
|
+
package_version: "2026.10.1",
|
|
20
20
|
codex_package_name: "@openai/codex",
|
|
21
21
|
codex_version: "0.155.0",
|
|
22
22
|
copilot_sdk_package_name: "@github/copilot-sdk",
|
|
@@ -358,9 +358,9 @@ import { dirname as dirname2, isAbsolute as isAbsolute2, join as join2, relative
|
|
|
358
358
|
import { randomBytes } from "node:crypto";
|
|
359
359
|
import { spawn } from "node:child_process";
|
|
360
360
|
import { constants } from "node:fs";
|
|
361
|
-
import { lstat, mkdir, open, readFile, realpath, rename, rm } from "node:fs/promises";
|
|
361
|
+
import { lstat, mkdir, open, readdir, readFile, realpath, rename, rm } from "node:fs/promises";
|
|
362
362
|
import { homedir } from "node:os";
|
|
363
|
-
import { dirname, isAbsolute, join, relative, resolve, win32 as windowsPath } from "node:path";
|
|
363
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, win32 as windowsPath } from "node:path";
|
|
364
364
|
var InferenceHostProcessLockBusyError = class extends Error {
|
|
365
365
|
constructor() {
|
|
366
366
|
super("Another inference host process already owns this local host.");
|
|
@@ -900,10 +900,22 @@ var defaultProcessIdentity = async (pid) => {
|
|
|
900
900
|
if (process.platform === "darwin") return await darwinProcessIdentity(pid);
|
|
901
901
|
return null;
|
|
902
902
|
};
|
|
903
|
+
var cachedCurrentProcessIdentity = null;
|
|
903
904
|
var defaultCurrentProcessIdentity = async () => {
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
905
|
+
if (!cachedCurrentProcessIdentity) {
|
|
906
|
+
cachedCurrentProcessIdentity = (async () => {
|
|
907
|
+
const identity = await defaultProcessIdentity(process.pid);
|
|
908
|
+
if (!identity) throw new Error("Inference host could not establish its process-start identity.");
|
|
909
|
+
return identity;
|
|
910
|
+
})();
|
|
911
|
+
}
|
|
912
|
+
const pending = cachedCurrentProcessIdentity;
|
|
913
|
+
try {
|
|
914
|
+
return await pending;
|
|
915
|
+
} catch (error) {
|
|
916
|
+
if (cachedCurrentProcessIdentity === pending) cachedCurrentProcessIdentity = null;
|
|
917
|
+
throw error;
|
|
918
|
+
}
|
|
907
919
|
};
|
|
908
920
|
var processLockObservationCache = /* @__PURE__ */ new Map();
|
|
909
921
|
var inferenceProcessIdentitiesMatch = (stored, observed) => stored === observed;
|
|
@@ -1052,150 +1064,305 @@ async function writeAtomicInferencePrivateFile(path, contents, dependencies = {}
|
|
|
1052
1064
|
throw error;
|
|
1053
1065
|
}
|
|
1054
1066
|
}
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1067
|
+
var PROCESS_LOCK_PUBLICATION_GRACE_MS = 1e4;
|
|
1068
|
+
var PROCESS_LOCK_MAX_RECORD_BYTES = 64 * 1024;
|
|
1069
|
+
var PROCESS_LOCK_GUARD_ELECTION_LEASE_MS = 5e3;
|
|
1070
|
+
var PROCESS_LOCK_GUARD_ELECTION_STALE_MS = 3e4;
|
|
1071
|
+
var PROCESS_LOCK_GUARD_ELECTION_ATTEMPTS = 8;
|
|
1072
|
+
var PROCESS_LOCK_GUARD_ELECTION_BACKOFF_MS = 10;
|
|
1073
|
+
var PROCESS_LOCK_LABEL = "Inference host process lock";
|
|
1074
|
+
var PROCESS_LOCK_GUARD_LABEL = "Inference host process lock reclamation guard";
|
|
1075
|
+
var sameProcessLockFile = (left, right) => left.dev === right.dev && left.ino === right.ino && left.size === right.size && left.mtimeMs === right.mtimeMs && left.ctimeMs === right.ctimeMs;
|
|
1076
|
+
var openExclusiveProcessLockFile = async (path) => {
|
|
1077
|
+
try {
|
|
1078
|
+
return await open(
|
|
1079
|
+
path,
|
|
1080
|
+
constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | constants.O_NOFOLLOW,
|
|
1081
|
+
384
|
|
1082
|
+
);
|
|
1083
|
+
} catch (error) {
|
|
1084
|
+
if (error.code === "EEXIST") return null;
|
|
1085
|
+
throw error;
|
|
1086
|
+
}
|
|
1087
|
+
};
|
|
1088
|
+
var inspectProcessLockFile = async (path, label, probes) => {
|
|
1089
|
+
let before;
|
|
1090
|
+
try {
|
|
1091
|
+
before = await lstat(path);
|
|
1092
|
+
} catch (error) {
|
|
1093
|
+
if (error.code === "ENOENT") return { state: "missing" };
|
|
1094
|
+
throw error;
|
|
1095
|
+
}
|
|
1096
|
+
if (before.isSymbolicLink() || !before.isFile() || process.platform !== "win32" && (before.mode & 63) !== 0 || typeof process.getuid === "function" && before.uid !== process.getuid()) {
|
|
1097
|
+
throw new Error(`${label} is unsafe.`);
|
|
1098
|
+
}
|
|
1099
|
+
let handle;
|
|
1100
|
+
try {
|
|
1101
|
+
await secureExistingWindowsPath(path, "file");
|
|
1102
|
+
handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
1103
|
+
} catch (error) {
|
|
1104
|
+
if (error.code === "ENOENT") return { state: "missing" };
|
|
1105
|
+
throw error;
|
|
1106
|
+
}
|
|
1107
|
+
let stale = false;
|
|
1108
|
+
try {
|
|
1109
|
+
const opened = await handle.stat();
|
|
1110
|
+
if (!opened.isFile() || opened.dev !== before.dev || opened.ino !== before.ino) {
|
|
1111
|
+
return { state: "busy" };
|
|
1112
|
+
}
|
|
1113
|
+
const raw = opened.size > PROCESS_LOCK_MAX_RECORD_BYTES ? "" : await handle.readFile("utf8");
|
|
1114
|
+
const stat = await handle.stat();
|
|
1115
|
+
if (!sameProcessLockFile(opened, stat)) return { state: "busy" };
|
|
1116
|
+
let record = null;
|
|
1062
1117
|
try {
|
|
1063
|
-
|
|
1064
|
-
|
|
1118
|
+
const parsed = JSON.parse(raw);
|
|
1119
|
+
if (parsed && typeof parsed === "object") record = parsed;
|
|
1120
|
+
} catch {
|
|
1121
|
+
record = null;
|
|
1122
|
+
}
|
|
1123
|
+
if (!record || record.schema_version !== 2 || !Number.isSafeInteger(record.pid) || Number(record.pid) < 1 || typeof record.owner_token !== "string" || !record.owner_token || typeof record.process_identity !== "string" || !record.process_identity || typeof record.boot_identity !== "string" || !record.boot_identity) {
|
|
1124
|
+
if (Date.now() - stat.mtimeMs < PROCESS_LOCK_PUBLICATION_GRACE_MS) return { state: "busy" };
|
|
1125
|
+
stale = true;
|
|
1126
|
+
return { state: "stale", handle, stat };
|
|
1127
|
+
}
|
|
1128
|
+
const ownerPid = Number(record.pid);
|
|
1129
|
+
const storedIdentity = record.process_identity;
|
|
1130
|
+
const cacheKey = resolve(path);
|
|
1131
|
+
if (await probes.currentBootIdentity() === record.boot_identity && probes.isProcessAlive(ownerPid)) {
|
|
1132
|
+
const cached = processLockObservationCache.get(cacheKey);
|
|
1133
|
+
const observedIdentity = cached && cached.dev === stat.dev && cached.ino === stat.ino && cached.pid === ownerPid && cached.storedIdentity === storedIdentity && Date.now() - cached.observedAtMs < 5e3 ? cached.observedIdentity : await probes.observeIdentity(ownerPid);
|
|
1134
|
+
if (!observedIdentity) {
|
|
1135
|
+
throw new Error("Inference host could not verify the existing lock owner identity.");
|
|
1136
|
+
}
|
|
1137
|
+
processLockObservationCache.set(cacheKey, {
|
|
1138
|
+
dev: stat.dev,
|
|
1139
|
+
ino: stat.ino,
|
|
1140
|
+
pid: ownerPid,
|
|
1141
|
+
storedIdentity,
|
|
1142
|
+
observedIdentity,
|
|
1143
|
+
observedAtMs: Date.now()
|
|
1144
|
+
});
|
|
1145
|
+
if (inferenceProcessIdentitiesMatch(storedIdentity, observedIdentity)) {
|
|
1146
|
+
return { state: "busy" };
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
stale = true;
|
|
1150
|
+
return { state: "stale", handle, stat };
|
|
1151
|
+
} finally {
|
|
1152
|
+
if (!stale) await handle.close().catch(() => void 0);
|
|
1153
|
+
}
|
|
1154
|
+
};
|
|
1155
|
+
var removeVerifiedStaleProcessLockFile = async (path, inspection, assertExclusive) => {
|
|
1156
|
+
try {
|
|
1157
|
+
await assertExclusive();
|
|
1158
|
+
let current;
|
|
1159
|
+
try {
|
|
1160
|
+
current = await lstat(path);
|
|
1065
1161
|
} catch (error) {
|
|
1066
|
-
if (error.code === "
|
|
1162
|
+
if (error.code === "ENOENT") return;
|
|
1067
1163
|
throw error;
|
|
1068
1164
|
}
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1165
|
+
const opened = await inspection.handle.stat();
|
|
1166
|
+
if (current.isSymbolicLink() || !sameProcessLockFile(current, opened) || !sameProcessLockFile(opened, inspection.stat)) {
|
|
1167
|
+
throw new InferenceHostProcessLockBusyError();
|
|
1168
|
+
}
|
|
1169
|
+
if (process.platform === "win32") await inspection.handle.close();
|
|
1072
1170
|
try {
|
|
1073
|
-
|
|
1074
|
-
path,
|
|
1075
|
-
constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | constants.O_NOFOLLOW,
|
|
1076
|
-
384
|
|
1077
|
-
);
|
|
1078
|
-
break;
|
|
1171
|
+
await rm(path);
|
|
1079
1172
|
} catch (error) {
|
|
1080
|
-
if (error.code !== "
|
|
1081
|
-
|
|
1082
|
-
|
|
1173
|
+
if (error.code !== "ENOENT") throw error;
|
|
1174
|
+
}
|
|
1175
|
+
} finally {
|
|
1176
|
+
await inspection.handle.close().catch(() => void 0);
|
|
1177
|
+
}
|
|
1178
|
+
processLockObservationCache.delete(resolve(path));
|
|
1179
|
+
invalidateWindowsAclCache(path);
|
|
1180
|
+
await syncInferenceDirectory(dirname(path));
|
|
1181
|
+
};
|
|
1182
|
+
var removeStaleProcessLockReclaimGuard = async (lockPath, guardPath, inspection) => {
|
|
1183
|
+
const directory = dirname(lockPath);
|
|
1184
|
+
const prefix = `${basename(lockPath)}.reclaim-ballot.`;
|
|
1185
|
+
try {
|
|
1186
|
+
for (let attempt = 0; attempt < PROCESS_LOCK_GUARD_ELECTION_ATTEMPTS; attempt += 1) {
|
|
1187
|
+
if (attempt > 0) {
|
|
1188
|
+
await new Promise((resolvePromise) => setTimeout(
|
|
1189
|
+
resolvePromise,
|
|
1190
|
+
Math.floor(Math.random() * PROCESS_LOCK_GUARD_ELECTION_BACKOFF_MS * 2 ** Math.min(attempt, 4))
|
|
1191
|
+
));
|
|
1192
|
+
let current;
|
|
1193
|
+
try {
|
|
1194
|
+
current = await lstat(guardPath);
|
|
1195
|
+
} catch (error) {
|
|
1196
|
+
if (error.code === "ENOENT") return;
|
|
1197
|
+
throw error;
|
|
1198
|
+
}
|
|
1199
|
+
if (!sameProcessLockFile(current, inspection.stat)) return;
|
|
1083
1200
|
}
|
|
1084
|
-
|
|
1201
|
+
const ballotName = `${prefix}${randomBytes(16).toString("hex")}`;
|
|
1202
|
+
const ballotPath = join(directory, ballotName);
|
|
1203
|
+
const ballot = await openExclusiveProcessLockFile(ballotPath);
|
|
1204
|
+
if (!ballot) continue;
|
|
1205
|
+
await ballot.close();
|
|
1206
|
+
const electedAt = performance.now();
|
|
1085
1207
|
try {
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1208
|
+
let contested = false;
|
|
1209
|
+
for (const name of await readdir(directory)) {
|
|
1210
|
+
if (!name.startsWith(prefix) || name === ballotName) continue;
|
|
1211
|
+
const otherPath = join(directory, name);
|
|
1212
|
+
let other;
|
|
1213
|
+
try {
|
|
1214
|
+
other = await lstat(otherPath);
|
|
1215
|
+
} catch (error) {
|
|
1216
|
+
if (error.code === "ENOENT") continue;
|
|
1217
|
+
throw error;
|
|
1218
|
+
}
|
|
1219
|
+
if (!other.isDirectory() && Date.now() - other.mtimeMs > PROCESS_LOCK_GUARD_ELECTION_STALE_MS) {
|
|
1220
|
+
await rm(otherPath, { force: true });
|
|
1221
|
+
continue;
|
|
1222
|
+
}
|
|
1223
|
+
contested = true;
|
|
1224
|
+
}
|
|
1225
|
+
if (contested) continue;
|
|
1226
|
+
await removeVerifiedStaleProcessLockFile(guardPath, inspection, async () => {
|
|
1227
|
+
if (performance.now() - electedAt > PROCESS_LOCK_GUARD_ELECTION_LEASE_MS) {
|
|
1228
|
+
throw new InferenceHostProcessLockBusyError();
|
|
1229
|
+
}
|
|
1230
|
+
});
|
|
1231
|
+
return;
|
|
1232
|
+
} finally {
|
|
1233
|
+
await rm(ballotPath, { force: true }).catch(() => void 0);
|
|
1090
1234
|
}
|
|
1091
|
-
|
|
1092
|
-
|
|
1235
|
+
}
|
|
1236
|
+
throw new InferenceHostProcessLockBusyError();
|
|
1237
|
+
} finally {
|
|
1238
|
+
await inspection.handle.close().catch(() => void 0);
|
|
1239
|
+
}
|
|
1240
|
+
};
|
|
1241
|
+
var acquireProcessLockReclaimGuard = async (lockPath, ownerRecord, probes) => {
|
|
1242
|
+
const guardPath = `${lockPath}.reclaim`;
|
|
1243
|
+
const guardToken = randomBytes(24).toString("base64url");
|
|
1244
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
1245
|
+
const handle = await openExclusiveProcessLockFile(guardPath);
|
|
1246
|
+
if (handle) {
|
|
1247
|
+
try {
|
|
1248
|
+
await handle.writeFile(ownerRecord(guardToken), "utf8");
|
|
1249
|
+
} catch (error) {
|
|
1250
|
+
await handle.close().catch(() => void 0);
|
|
1251
|
+
await rm(guardPath, { force: true }).catch(() => void 0);
|
|
1252
|
+
throw error;
|
|
1093
1253
|
}
|
|
1094
|
-
|
|
1095
|
-
|
|
1254
|
+
await handle.close();
|
|
1255
|
+
await secureWindowsPrivatePath(guardPath, "file", "harden");
|
|
1256
|
+
const isHeld = async () => {
|
|
1257
|
+
let reader;
|
|
1096
1258
|
try {
|
|
1097
|
-
|
|
1098
|
-
} catch (
|
|
1099
|
-
if (
|
|
1100
|
-
throw
|
|
1101
|
-
}
|
|
1102
|
-
if (current2.isSymbolicLink() || !current2.isFile() || current2.dev !== before.dev || current2.ino !== before.ino || process.platform !== "win32" && (current2.mode & 63) !== 0 || typeof process.getuid === "function" && current2.uid !== process.getuid()) {
|
|
1103
|
-
throw new Error("Inference host process lock publication residue changed or is unsafe.");
|
|
1259
|
+
reader = await open(guardPath, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
1260
|
+
} catch (error) {
|
|
1261
|
+
if (error.code === "ENOENT") return false;
|
|
1262
|
+
throw error;
|
|
1104
1263
|
}
|
|
1105
1264
|
try {
|
|
1106
|
-
await
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1265
|
+
const [opened, current] = await Promise.all([reader.stat(), lstat(guardPath)]);
|
|
1266
|
+
if (current.isSymbolicLink() || current.dev !== opened.dev || current.ino !== opened.ino) {
|
|
1267
|
+
return false;
|
|
1268
|
+
}
|
|
1269
|
+
const parsed = JSON.parse(await reader.readFile("utf8"));
|
|
1270
|
+
return parsed.owner_token === guardToken;
|
|
1271
|
+
} catch {
|
|
1272
|
+
return false;
|
|
1273
|
+
} finally {
|
|
1274
|
+
await reader.close().catch(() => void 0);
|
|
1275
|
+
}
|
|
1276
|
+
};
|
|
1277
|
+
return {
|
|
1278
|
+
assertHeld: async () => {
|
|
1279
|
+
if (!await isHeld()) throw new InferenceHostProcessLockBusyError();
|
|
1280
|
+
},
|
|
1281
|
+
// Best effort: a guard left behind carries this live process identity
|
|
1282
|
+
// and becomes reclaimable once this process exits.
|
|
1283
|
+
release: async () => {
|
|
1284
|
+
try {
|
|
1285
|
+
if (!await isHeld()) return;
|
|
1286
|
+
await rm(guardPath);
|
|
1287
|
+
processLockObservationCache.delete(resolve(guardPath));
|
|
1288
|
+
invalidateWindowsAclCache(guardPath);
|
|
1289
|
+
} catch {
|
|
1290
|
+
}
|
|
1110
1291
|
}
|
|
1111
|
-
processLockObservationCache.delete(resolve(path));
|
|
1112
|
-
await syncInferenceDirectory(directory);
|
|
1113
|
-
return true;
|
|
1114
1292
|
};
|
|
1293
|
+
}
|
|
1294
|
+
if (attempt > 0) break;
|
|
1295
|
+
const inspection = await inspectProcessLockFile(guardPath, PROCESS_LOCK_GUARD_LABEL, probes);
|
|
1296
|
+
if (inspection.state === "busy") break;
|
|
1297
|
+
if (inspection.state === "stale") {
|
|
1298
|
+
await removeStaleProcessLockReclaimGuard(lockPath, guardPath, inspection);
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
throw new InferenceHostProcessLockBusyError();
|
|
1302
|
+
};
|
|
1303
|
+
async function acquireInferenceHostProcessLock(path, dependencies = {}) {
|
|
1304
|
+
const directory = dirname(path);
|
|
1305
|
+
await ensureInferencePrivateDirectory(directory);
|
|
1306
|
+
const ownerToken = randomBytes(24).toString("base64url");
|
|
1307
|
+
const probes = {
|
|
1308
|
+
currentBootIdentity: dependencies.currentBootIdentity ?? (() => readInferenceSystemBootIdentity()),
|
|
1309
|
+
observeIdentity: dependencies.processIdentity ?? defaultProcessIdentity,
|
|
1310
|
+
isProcessAlive: dependencies.processAlive ?? ((pid) => {
|
|
1115
1311
|
try {
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1312
|
+
process.kill(pid, 0);
|
|
1313
|
+
return true;
|
|
1314
|
+
} catch (error) {
|
|
1315
|
+
if (error.code === "ESRCH") return false;
|
|
1316
|
+
throw error;
|
|
1120
1317
|
}
|
|
1121
|
-
|
|
1318
|
+
})
|
|
1319
|
+
};
|
|
1320
|
+
const [ownerIdentity, bootIdentity] = await Promise.all([
|
|
1321
|
+
(dependencies.currentProcessIdentity ?? defaultCurrentProcessIdentity)(),
|
|
1322
|
+
probes.currentBootIdentity()
|
|
1323
|
+
]);
|
|
1324
|
+
const ownerRecord = (token) => `${JSON.stringify({
|
|
1325
|
+
schema_version: 2,
|
|
1326
|
+
pid: process.pid,
|
|
1327
|
+
owner_token: token,
|
|
1328
|
+
process_identity: ownerIdentity,
|
|
1329
|
+
boot_identity: bootIdentity
|
|
1330
|
+
})}
|
|
1331
|
+
`;
|
|
1332
|
+
let handle = await openExclusiveProcessLockFile(path);
|
|
1333
|
+
if (!handle) {
|
|
1334
|
+
const inspection = await inspectProcessLockFile(path, PROCESS_LOCK_LABEL, probes);
|
|
1335
|
+
if (inspection.state === "busy") throw new InferenceHostProcessLockBusyError();
|
|
1336
|
+
if (inspection.state === "stale") {
|
|
1337
|
+
await inspection.handle.close();
|
|
1338
|
+
const guard = await acquireProcessLockReclaimGuard(path, ownerRecord, probes);
|
|
1122
1339
|
try {
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
if (
|
|
1126
|
-
|
|
1127
|
-
throw new InferenceHostProcessLockBusyError();
|
|
1128
|
-
}
|
|
1129
|
-
await reclaimPublicationResidue();
|
|
1130
|
-
continue;
|
|
1131
|
-
}
|
|
1132
|
-
if (staleOwner.schema_version !== 2 || !Number.isSafeInteger(staleOwner.pid) || Number(staleOwner.pid) < 1 || typeof staleOwner.owner_token !== "string" || !staleOwner.owner_token || typeof staleOwner.process_identity !== "string" || !staleOwner.process_identity || typeof staleOwner.boot_identity !== "string" || !staleOwner.boot_identity) {
|
|
1133
|
-
if (Date.now() - before.mtimeMs < 1e4) {
|
|
1134
|
-
throw new InferenceHostProcessLockBusyError();
|
|
1135
|
-
}
|
|
1136
|
-
await reclaimPublicationResidue();
|
|
1137
|
-
continue;
|
|
1138
|
-
}
|
|
1139
|
-
const stalePid = Number(staleOwner.pid);
|
|
1140
|
-
const bootIdentity2 = await currentBootIdentity();
|
|
1141
|
-
if (bootIdentity2 === staleOwner.boot_identity && isProcessAlive(stalePid)) {
|
|
1142
|
-
const cached = processLockObservationCache.get(resolve(path));
|
|
1143
|
-
const observedIdentity = cached && cached.dev === before.dev && cached.ino === before.ino && cached.pid === stalePid && cached.storedIdentity === staleOwner.process_identity && Date.now() - cached.observedAtMs < 5e3 ? cached.observedIdentity : await observeIdentity(stalePid);
|
|
1144
|
-
if (!observedIdentity) {
|
|
1145
|
-
throw new Error("Inference host could not verify the existing lock owner identity.");
|
|
1146
|
-
}
|
|
1147
|
-
processLockObservationCache.set(resolve(path), {
|
|
1148
|
-
dev: before.dev,
|
|
1149
|
-
ino: before.ino,
|
|
1150
|
-
pid: stalePid,
|
|
1151
|
-
storedIdentity: staleOwner.process_identity,
|
|
1152
|
-
observedIdentity,
|
|
1153
|
-
observedAtMs: Date.now()
|
|
1154
|
-
});
|
|
1155
|
-
if (inferenceProcessIdentitiesMatch(staleOwner.process_identity, observedIdentity)) {
|
|
1156
|
-
throw new InferenceHostProcessLockBusyError();
|
|
1340
|
+
const guarded = await inspectProcessLockFile(path, PROCESS_LOCK_LABEL, probes);
|
|
1341
|
+
if (guarded.state === "busy") throw new InferenceHostProcessLockBusyError();
|
|
1342
|
+
if (guarded.state === "stale") {
|
|
1343
|
+
await removeVerifiedStaleProcessLockFile(path, guarded, guard.assertHeld);
|
|
1157
1344
|
}
|
|
1345
|
+
handle = await openExclusiveProcessLockFile(path);
|
|
1346
|
+
} finally {
|
|
1347
|
+
await guard.release();
|
|
1158
1348
|
}
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
current = await lstat(path);
|
|
1162
|
-
} catch (error2) {
|
|
1163
|
-
if (error2.code === "ENOENT") continue;
|
|
1164
|
-
throw error2;
|
|
1165
|
-
}
|
|
1166
|
-
if (current.dev !== before.dev || current.ino !== before.ino) {
|
|
1167
|
-
throw new Error("Inference host process lock ownership changed.");
|
|
1168
|
-
}
|
|
1169
|
-
await rm(path);
|
|
1170
|
-
processLockObservationCache.delete(resolve(path));
|
|
1171
|
-
await syncInferenceDirectory(directory);
|
|
1349
|
+
} else {
|
|
1350
|
+
handle = await openExclusiveProcessLockFile(path);
|
|
1172
1351
|
}
|
|
1352
|
+
if (!handle) throw new InferenceHostProcessLockBusyError();
|
|
1173
1353
|
}
|
|
1174
|
-
if (!handle) throw new Error("Inference host process lock could not be acquired.");
|
|
1175
|
-
let ownerIdentity;
|
|
1176
|
-
let bootIdentity;
|
|
1177
1354
|
try {
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
currentBootIdentity()
|
|
1181
|
-
]);
|
|
1355
|
+
await handle.writeFile(ownerRecord(ownerToken), "utf8");
|
|
1356
|
+
await handle.sync();
|
|
1182
1357
|
} catch (error) {
|
|
1358
|
+
const created = await handle.stat().catch(() => null);
|
|
1183
1359
|
await handle.close().catch(() => void 0);
|
|
1184
|
-
await
|
|
1360
|
+
const current = await lstat(path).catch(() => null);
|
|
1361
|
+
if (created && current && current.dev === created.dev && current.ino === created.ino) {
|
|
1362
|
+
await rm(path, { force: true }).catch(() => void 0);
|
|
1363
|
+
}
|
|
1185
1364
|
throw error;
|
|
1186
1365
|
}
|
|
1187
|
-
await handle.writeFile(
|
|
1188
|
-
`${JSON.stringify({
|
|
1189
|
-
schema_version: 2,
|
|
1190
|
-
pid: process.pid,
|
|
1191
|
-
owner_token: ownerToken,
|
|
1192
|
-
process_identity: ownerIdentity,
|
|
1193
|
-
boot_identity: bootIdentity
|
|
1194
|
-
})}
|
|
1195
|
-
`,
|
|
1196
|
-
"utf8"
|
|
1197
|
-
);
|
|
1198
|
-
await handle.sync();
|
|
1199
1366
|
await handle.close();
|
|
1200
1367
|
await secureWindowsPrivatePath(path, "file", "harden");
|
|
1201
1368
|
await syncInferenceDirectory(directory);
|
|
@@ -1513,7 +1680,7 @@ var verifyInstalledRelease = async (prefix, targetVersion, executable, platform,
|
|
|
1513
1680
|
return script;
|
|
1514
1681
|
};
|
|
1515
1682
|
var cleanupInterruptedReleaseStaging = async (releasesPath) => {
|
|
1516
|
-
const entries = await
|
|
1683
|
+
const entries = await readdir2(releasesPath, { withFileTypes: true });
|
|
1517
1684
|
for (const entry of entries) {
|
|
1518
1685
|
if (entry.isDirectory() && /^\.(?:staging|download)-\d{4}\.\d{1,2}\.\d{1,3}-[0-9a-f-]{36}$/u.test(entry.name)) {
|
|
1519
1686
|
await rm3(resolve3(releasesPath, entry.name), { recursive: true, force: true });
|
|
@@ -1680,7 +1847,7 @@ var prunePrivateReleases = async (manifestPath, keep) => {
|
|
|
1680
1847
|
);
|
|
1681
1848
|
let entries;
|
|
1682
1849
|
try {
|
|
1683
|
-
entries = await
|
|
1850
|
+
entries = await readdir2(releasesPath, { withFileTypes: true });
|
|
1684
1851
|
} catch (error) {
|
|
1685
1852
|
if (error.code === "ENOENT") return;
|
|
1686
1853
|
throw error;
|