@infersec/conduit 1.107.0 → 1.108.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.
- package/dist/cli.js +278 -52
- package/dist/cli.sea.cjs +277 -51
- package/dist/modelManagement/ModelManager.d.ts +1 -2
- package/dist/modelManagement/downloadLock.d.ts +32 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -10,7 +10,7 @@ import path$1, { join, win32, posix, dirname, resolve as resolve$1, basename, se
|
|
|
10
10
|
import * as require$$3$4 from 'node:fs';
|
|
11
11
|
import require$$3__default, { existsSync, createWriteStream, statSync, readFileSync, appendFileSync, writeFileSync, createReadStream } from 'node:fs';
|
|
12
12
|
import process$3, { platform, hrtime, execPath, execArgv } from 'node:process';
|
|
13
|
-
import crypto from 'node:crypto';
|
|
13
|
+
import crypto, { randomUUID } from 'node:crypto';
|
|
14
14
|
import require$$0$h, { Readable, Transform, PassThrough, getDefaultHighWaterMark, Duplex, Writable } from 'node:stream';
|
|
15
15
|
import require$$1$a, { setDefaultResultOrder } from 'node:dns';
|
|
16
16
|
import require$$0$9 from 'os';
|
|
@@ -21288,6 +21288,42 @@ object$1({
|
|
|
21288
21288
|
name: ResourceNameSchema
|
|
21289
21289
|
});
|
|
21290
21290
|
|
|
21291
|
+
const FitMethodSchema = _enum$1(["gguf-header", "model-config", "parameter-heuristic"]);
|
|
21292
|
+
const FitConfidenceSchema = _enum$1(["high", "low", "medium"]);
|
|
21293
|
+
object$1({
|
|
21294
|
+
architecture: string$2().nullable(),
|
|
21295
|
+
contextTrainTokens: number$1().int().positive().nullable(),
|
|
21296
|
+
embeddingLength: number$1().int().positive().nullable(),
|
|
21297
|
+
headCount: number$1().int().positive().nullable(),
|
|
21298
|
+
headCountKV: number$1().int().positive().nullable(),
|
|
21299
|
+
headDim: number$1().int().positive().nullable(),
|
|
21300
|
+
layerCount: number$1().int().positive().nullable(),
|
|
21301
|
+
method: FitMethodSchema,
|
|
21302
|
+
probedAt: string$2(),
|
|
21303
|
+
unresolved: boolean$1().optional()
|
|
21304
|
+
});
|
|
21305
|
+
const FitMemoryDomainKindSchema = _enum$1(["gpu", "system", "unified"]);
|
|
21306
|
+
const FitMemoryDomainSchema = object$1({
|
|
21307
|
+
capacityBytes: number$1().int().nonnegative(),
|
|
21308
|
+
domainId: string$2(),
|
|
21309
|
+
kind: FitMemoryDomainKindSchema,
|
|
21310
|
+
label: string$2(),
|
|
21311
|
+
remainingBytes: number$1().int(),
|
|
21312
|
+
requiredBytes: number$1().int().nonnegative(),
|
|
21313
|
+
reserveBytes: number$1().int().nonnegative()
|
|
21314
|
+
});
|
|
21315
|
+
object$1({
|
|
21316
|
+
confidence: FitConfidenceSchema,
|
|
21317
|
+
contextTokens: number$1().int().positive(),
|
|
21318
|
+
deficitBytes: number$1().int().nonnegative(),
|
|
21319
|
+
domains: array(FitMemoryDomainSchema),
|
|
21320
|
+
engine: LLMEngineSchema,
|
|
21321
|
+
fits: boolean$1(),
|
|
21322
|
+
limitingDomain: string$2().nullable(),
|
|
21323
|
+
method: FitMethodSchema,
|
|
21324
|
+
totalRequiredBytes: number$1().int().nonnegative()
|
|
21325
|
+
});
|
|
21326
|
+
|
|
21291
21327
|
const ENGINE_API_COMPATIBILITY = {
|
|
21292
21328
|
exllamav3: {
|
|
21293
21329
|
nativeAnthropicMessages: false,
|
|
@@ -123297,6 +123333,181 @@ async function downloadFileWithRange({ accessToken, filePath, fileSize, modelSlu
|
|
|
123297
123333
|
throw new Error(errorMessage);
|
|
123298
123334
|
}
|
|
123299
123335
|
|
|
123336
|
+
const RESTORE_RETRY_ATTEMPTS = 50;
|
|
123337
|
+
const RESTORE_RETRY_DELAY_MS = 5;
|
|
123338
|
+
function errorCode(error) {
|
|
123339
|
+
if (typeof error === "object" && error !== null) {
|
|
123340
|
+
const code = error.code;
|
|
123341
|
+
if (typeof code === "string")
|
|
123342
|
+
return code;
|
|
123343
|
+
}
|
|
123344
|
+
return null;
|
|
123345
|
+
}
|
|
123346
|
+
function isFileExistsError(error) {
|
|
123347
|
+
return errorCode(error) === "EEXIST";
|
|
123348
|
+
}
|
|
123349
|
+
function isFileNotFoundError(error) {
|
|
123350
|
+
return errorCode(error) === "ENOENT";
|
|
123351
|
+
}
|
|
123352
|
+
async function readLockContents({ lockFilePath }) {
|
|
123353
|
+
try {
|
|
123354
|
+
const raw = await readFile(lockFilePath, "utf-8");
|
|
123355
|
+
const data = JSON.parse(raw);
|
|
123356
|
+
if (typeof data.acquiredAt !== "number" || typeof data.token !== "string")
|
|
123357
|
+
return null;
|
|
123358
|
+
return data;
|
|
123359
|
+
}
|
|
123360
|
+
catch {
|
|
123361
|
+
return null;
|
|
123362
|
+
}
|
|
123363
|
+
}
|
|
123364
|
+
async function observeLock({ lockFilePath }) {
|
|
123365
|
+
const [contents, stats] = await Promise.all([
|
|
123366
|
+
readLockContents({ lockFilePath }),
|
|
123367
|
+
stat(lockFilePath).catch(error => {
|
|
123368
|
+
if (isFileNotFoundError(error))
|
|
123369
|
+
return null;
|
|
123370
|
+
throw error;
|
|
123371
|
+
})
|
|
123372
|
+
]);
|
|
123373
|
+
return {
|
|
123374
|
+
contents,
|
|
123375
|
+
exists: stats !== null,
|
|
123376
|
+
mtimeAgeMs: stats ? Date.now() - stats.mtimeMs : null
|
|
123377
|
+
};
|
|
123378
|
+
}
|
|
123379
|
+
/**
|
|
123380
|
+
* Remove a lock file only if it is the exact instance that was observed and
|
|
123381
|
+
* verified. The candidate is first renamed to a unique quarantine path
|
|
123382
|
+
* (rename is atomic, so exactly one competing process can isolate a given
|
|
123383
|
+
* instance); the quarantined file is then re-verified against the expected
|
|
123384
|
+
* identity. A mismatch means a replacement lock was captured by the rename -
|
|
123385
|
+
* it is renamed back into place rather than deleted.
|
|
123386
|
+
*
|
|
123387
|
+
* Residual race: a restore rename could theoretically clobber a third lock
|
|
123388
|
+
* created in the microsecond window while the replacement sat in quarantine.
|
|
123389
|
+
* Files alone cannot provide a fully linearizable compare-and-delete; closing
|
|
123390
|
+
* that window entirely would require an OS-level lock primitive (flock).
|
|
123391
|
+
*/
|
|
123392
|
+
async function removeVerifiedLockInstance({ isTargetInstance, lockFilePath, logger, quarantinePrefix }) {
|
|
123393
|
+
const quarantinePath = `${lockFilePath}.${quarantinePrefix}-${process.pid}-${randomUUID()}`;
|
|
123394
|
+
try {
|
|
123395
|
+
await rename(lockFilePath, quarantinePath);
|
|
123396
|
+
}
|
|
123397
|
+
catch (error) {
|
|
123398
|
+
if (isFileNotFoundError(error))
|
|
123399
|
+
return false;
|
|
123400
|
+
throw error;
|
|
123401
|
+
}
|
|
123402
|
+
const quarantined = await observeLock({ lockFilePath: quarantinePath });
|
|
123403
|
+
if (isTargetInstance(quarantined)) {
|
|
123404
|
+
await rm(quarantinePath, { force: true });
|
|
123405
|
+
return true;
|
|
123406
|
+
}
|
|
123407
|
+
// A replacement lock was captured - restore it before continuing
|
|
123408
|
+
for (let attempt = 1;; attempt++) {
|
|
123409
|
+
try {
|
|
123410
|
+
await rename(quarantinePath, lockFilePath);
|
|
123411
|
+
return false;
|
|
123412
|
+
}
|
|
123413
|
+
catch (error) {
|
|
123414
|
+
const code = errorCode(error);
|
|
123415
|
+
if (code !== "ENOTEMPTY" && code !== "EEXIST" && code !== "EPERM")
|
|
123416
|
+
throw error;
|
|
123417
|
+
if (attempt >= RESTORE_RETRY_ATTEMPTS) {
|
|
123418
|
+
logger.warn("Failed restoring quarantined replacement lock", {
|
|
123419
|
+
lockFilePath,
|
|
123420
|
+
quarantinePath
|
|
123421
|
+
});
|
|
123422
|
+
return false;
|
|
123423
|
+
}
|
|
123424
|
+
await new Promise(resolve => setTimeout(resolve, RESTORE_RETRY_DELAY_MS));
|
|
123425
|
+
}
|
|
123426
|
+
}
|
|
123427
|
+
}
|
|
123428
|
+
/**
|
|
123429
|
+
* Acquire an exclusive cross-process file lock. Creation uses the "wx" flag
|
|
123430
|
+
* so the check-and-create step is atomic: concurrent processes can never both
|
|
123431
|
+
* observe a missing lock file and both claim it. Each acquisition owns a
|
|
123432
|
+
* unique token; only a lock whose stored token matches the holder's handle
|
|
123433
|
+
* may be released by that holder. An active (non-stale) lock is never broken
|
|
123434
|
+
* because a waiter timed out - the waiter fails instead.
|
|
123435
|
+
*
|
|
123436
|
+
* Staleness is judged per observed instance: a parsed lock is stale when its
|
|
123437
|
+
* recorded acquisition age exceeds staleMs; an unparseable lock (possible
|
|
123438
|
+
* mid-write observation) is stale only when its mtime is itself old, so a
|
|
123439
|
+
* fresh lock can never be quarantined off the back of a partial read.
|
|
123440
|
+
*/
|
|
123441
|
+
async function acquireExclusiveFileLock({ lockFilePath, logger, pollIntervalMs, staleMs, timeoutMs }) {
|
|
123442
|
+
await mkdir(dirname(lockFilePath), { recursive: true });
|
|
123443
|
+
const token = `${process.pid}-${randomUUID()}`;
|
|
123444
|
+
const deadline = Date.now() + timeoutMs;
|
|
123445
|
+
while (true) {
|
|
123446
|
+
try {
|
|
123447
|
+
const contents = { acquiredAt: Date.now(), token };
|
|
123448
|
+
await writeFile(lockFilePath, JSON.stringify(contents), { flag: "wx" });
|
|
123449
|
+
logger.info("Acquired download lock", { lockFilePath });
|
|
123450
|
+
return { lockFilePath, token };
|
|
123451
|
+
}
|
|
123452
|
+
catch (error) {
|
|
123453
|
+
if (!isFileExistsError(error))
|
|
123454
|
+
throw error;
|
|
123455
|
+
}
|
|
123456
|
+
const observation = await observeLock({ lockFilePath });
|
|
123457
|
+
if (!observation.exists) {
|
|
123458
|
+
// Lock vanished between create and read - retry creation now
|
|
123459
|
+
continue;
|
|
123460
|
+
}
|
|
123461
|
+
const parsedAge = observation.contents !== null ? Date.now() - observation.contents.acquiredAt : null;
|
|
123462
|
+
const staleByContents = parsedAge !== null && parsedAge >= staleMs;
|
|
123463
|
+
const staleByMtime = observation.contents === null &&
|
|
123464
|
+
observation.mtimeAgeMs !== null &&
|
|
123465
|
+
observation.mtimeAgeMs >= staleMs;
|
|
123466
|
+
if (staleByContents) {
|
|
123467
|
+
const observedToken = observation.contents?.token;
|
|
123468
|
+
logger.info("Stale download lock found, breaking", { lockFilePath });
|
|
123469
|
+
await removeVerifiedLockInstance({
|
|
123470
|
+
isTargetInstance: candidate => candidate.contents?.token === observedToken,
|
|
123471
|
+
lockFilePath,
|
|
123472
|
+
logger,
|
|
123473
|
+
quarantinePrefix: "stale"
|
|
123474
|
+
});
|
|
123475
|
+
continue;
|
|
123476
|
+
}
|
|
123477
|
+
if (staleByMtime) {
|
|
123478
|
+
logger.info("Unreadable stale download lock found, breaking", { lockFilePath });
|
|
123479
|
+
await removeVerifiedLockInstance({
|
|
123480
|
+
isTargetInstance: candidate => candidate.contents === null &&
|
|
123481
|
+
candidate.mtimeAgeMs !== null &&
|
|
123482
|
+
candidate.mtimeAgeMs >= staleMs,
|
|
123483
|
+
lockFilePath,
|
|
123484
|
+
logger,
|
|
123485
|
+
quarantinePrefix: "stale"
|
|
123486
|
+
});
|
|
123487
|
+
continue;
|
|
123488
|
+
}
|
|
123489
|
+
if (Date.now() >= deadline) {
|
|
123490
|
+
throw new Error(`Failed acquiring download lock within ${timeoutMs}ms: ${lockFilePath}`);
|
|
123491
|
+
}
|
|
123492
|
+
logger.info("Download lock held by another process, waiting", {
|
|
123493
|
+
lockAgeMs: parsedAge ?? observation.mtimeAgeMs,
|
|
123494
|
+
lockFilePath
|
|
123495
|
+
});
|
|
123496
|
+
await new Promise(resolve => setTimeout(resolve, pollIntervalMs));
|
|
123497
|
+
}
|
|
123498
|
+
}
|
|
123499
|
+
async function releaseExclusiveFileLock({ handle }) {
|
|
123500
|
+
// Removal is verified against the handle's token at quarantine time, so
|
|
123501
|
+
// a replacement lock created after this holder's release/read can never
|
|
123502
|
+
// be unlinked, and a lock already gone is a no-op
|
|
123503
|
+
await removeVerifiedLockInstance({
|
|
123504
|
+
isTargetInstance: candidate => candidate.contents?.token === handle.token,
|
|
123505
|
+
lockFilePath: handle.lockFilePath,
|
|
123506
|
+
logger: { info: () => { }, warn: () => { } },
|
|
123507
|
+
quarantinePrefix: "release"
|
|
123508
|
+
});
|
|
123509
|
+
}
|
|
123510
|
+
|
|
123300
123511
|
const EXLLAMAV3_EXECUTABLE = process.env.EXLLAMAV3_EXECUTABLE ?? "python3";
|
|
123301
123512
|
const SERVER_SCRIPT = join(dirname(fileURLToPath(import.meta.url)), "server.py");
|
|
123302
123513
|
const DEFAULT_EXLLAMAV3_CONTEXT_LENGTH = 4096;
|
|
@@ -123372,7 +123583,6 @@ async function startLlamacpp({ enginePort, targetDirectory }) {
|
|
|
123372
123583
|
console.warn(`[llamacpp] Multimodal enabled but no projector file found in ${targetDirectory}. ` +
|
|
123373
123584
|
"Vision functionality will not work.");
|
|
123374
123585
|
}
|
|
123375
|
-
args.push("--special");
|
|
123376
123586
|
}
|
|
123377
123587
|
if (process.env.LLAMACPP_REASONING === "off") {
|
|
123378
123588
|
args.push("--reasoning", "off");
|
|
@@ -123507,6 +123717,7 @@ class ModelManager extends EventEmitter {
|
|
|
123507
123717
|
healthPollInterval = null;
|
|
123508
123718
|
lastEngineError = null;
|
|
123509
123719
|
lifecycleState = "stopped";
|
|
123720
|
+
downloadLockHandle = null;
|
|
123510
123721
|
stopRequested = false;
|
|
123511
123722
|
modelsDirectory;
|
|
123512
123723
|
constructor({ contextLength, engineConfig, enginePort, engineType, logger, model, root }) {
|
|
@@ -123824,35 +124035,13 @@ class ModelManager extends EventEmitter {
|
|
|
123824
124035
|
return join(this.modelsDirectory, `${this.uniqueName}.lock`);
|
|
123825
124036
|
}
|
|
123826
124037
|
async acquireDownloadLock() {
|
|
123827
|
-
|
|
123828
|
-
|
|
123829
|
-
|
|
123830
|
-
|
|
123831
|
-
|
|
123832
|
-
|
|
123833
|
-
|
|
123834
|
-
});
|
|
123835
|
-
await this.waitForDownloadLock();
|
|
123836
|
-
}
|
|
123837
|
-
else {
|
|
123838
|
-
this.logger.info("Stale download lock found, breaking", {
|
|
123839
|
-
lockFilePath: this.lockFilePath
|
|
123840
|
-
});
|
|
123841
|
-
await this.releaseDownloadLock();
|
|
123842
|
-
}
|
|
123843
|
-
}
|
|
123844
|
-
await writeFile(this.lockFilePath, JSON.stringify({ acquiredAt: Date.now() }));
|
|
123845
|
-
this.logger.info("Acquired download lock", { lockFilePath: this.lockFilePath });
|
|
123846
|
-
}
|
|
123847
|
-
async getLockAge() {
|
|
123848
|
-
try {
|
|
123849
|
-
const raw = await readFile(this.lockFilePath, "utf-8");
|
|
123850
|
-
const data = JSON.parse(raw);
|
|
123851
|
-
return Date.now() - data.acquiredAt;
|
|
123852
|
-
}
|
|
123853
|
-
catch {
|
|
123854
|
-
return null;
|
|
123855
|
-
}
|
|
124038
|
+
this.downloadLockHandle = await acquireExclusiveFileLock({
|
|
124039
|
+
lockFilePath: this.lockFilePath,
|
|
124040
|
+
logger: this.logger,
|
|
124041
|
+
pollIntervalMs: DOWNLOAD_LOCK_POLL_INTERVAL_MS,
|
|
124042
|
+
staleMs: DOWNLOAD_LOCK_TIMEOUT_MS,
|
|
124043
|
+
timeoutMs: DOWNLOAD_LOCK_TIMEOUT_MS
|
|
124044
|
+
});
|
|
123856
124045
|
}
|
|
123857
124046
|
recordEngineError(err) {
|
|
123858
124047
|
this.lifecycleState = "errored";
|
|
@@ -123860,27 +124049,11 @@ class ModelManager extends EventEmitter {
|
|
|
123860
124049
|
this.emit("engineError", err);
|
|
123861
124050
|
}
|
|
123862
124051
|
async releaseDownloadLock() {
|
|
123863
|
-
|
|
123864
|
-
|
|
123865
|
-
|
|
123866
|
-
|
|
123867
|
-
|
|
123868
|
-
}
|
|
123869
|
-
}
|
|
123870
|
-
async waitForDownloadLock() {
|
|
123871
|
-
const start = Date.now();
|
|
123872
|
-
while (Date.now() - start < DOWNLOAD_LOCK_TIMEOUT_MS) {
|
|
123873
|
-
await new Promise(resolve => setTimeout(resolve, DOWNLOAD_LOCK_POLL_INTERVAL_MS));
|
|
123874
|
-
if (!existsSync(this.lockFilePath)) {
|
|
123875
|
-
return;
|
|
123876
|
-
}
|
|
123877
|
-
const age = await this.getLockAge();
|
|
123878
|
-
if (age === null || age >= DOWNLOAD_LOCK_TIMEOUT_MS) {
|
|
123879
|
-
await this.releaseDownloadLock();
|
|
123880
|
-
return;
|
|
123881
|
-
}
|
|
123882
|
-
}
|
|
123883
|
-
await this.releaseDownloadLock();
|
|
124052
|
+
const handle = this.downloadLockHandle;
|
|
124053
|
+
if (!handle)
|
|
124054
|
+
return;
|
|
124055
|
+
this.downloadLockHandle = null;
|
|
124056
|
+
await releaseExclusiveFileLock({ handle });
|
|
123884
124057
|
}
|
|
123885
124058
|
bindEngineProcessEvents(processManager) {
|
|
123886
124059
|
let hasTerminated = false;
|
|
@@ -137058,6 +137231,59 @@ function registerModelsCommands({ program }) {
|
|
|
137058
137231
|
}
|
|
137059
137232
|
console.log(`Cleared ${targets.length} model(s), freed ${formatBytes(totalBytes)}`);
|
|
137060
137233
|
});
|
|
137234
|
+
models
|
|
137235
|
+
.command("download")
|
|
137236
|
+
.description("Pre-download a HuggingFace model into the local cache")
|
|
137237
|
+
.requiredOption("--slug <slug>", "Fully qualified model slug, optionally with a quantization variant (owner/repo:Q4_K_M)")
|
|
137238
|
+
.option("--format <format>", "Model format (gguf, safetensors, pytorch, ...)", "gguf")
|
|
137239
|
+
.option("--root <path>", "Root directory (or ROOT_DIRECTORY env)")
|
|
137240
|
+
.option("--token <token>", "HuggingFace access token (or HF_TOKEN env)")
|
|
137241
|
+
.action(async (options) => {
|
|
137242
|
+
const slug = options.slug?.trim() ?? "";
|
|
137243
|
+
if (!slug.includes("/")) {
|
|
137244
|
+
console.error("Slug must be a fully qualified owner/repo slug");
|
|
137245
|
+
process.exitCode = 1;
|
|
137246
|
+
return;
|
|
137247
|
+
}
|
|
137248
|
+
const format = LLMModelFormatSchema.safeParse(options.format);
|
|
137249
|
+
if (!format.success) {
|
|
137250
|
+
console.error(`Invalid format "${options.format}". Must be one of: ${LLMModelFormatSchema.options.join(", ")}`);
|
|
137251
|
+
process.exitCode = 1;
|
|
137252
|
+
return;
|
|
137253
|
+
}
|
|
137254
|
+
const huggingFaceToken = options.token?.trim() || null;
|
|
137255
|
+
const model = {
|
|
137256
|
+
format: format.data,
|
|
137257
|
+
id: slug,
|
|
137258
|
+
multimodalEnabled: false,
|
|
137259
|
+
source: {
|
|
137260
|
+
modelSecret: huggingFaceToken,
|
|
137261
|
+
slug,
|
|
137262
|
+
type: "huggingface"
|
|
137263
|
+
},
|
|
137264
|
+
taskType: "text-generation"
|
|
137265
|
+
};
|
|
137266
|
+
const rootDirectory = getRootDirectory(options.root);
|
|
137267
|
+
const modelsDir = join(rootDirectory, "models");
|
|
137268
|
+
const uniqueName = createModelStorageKey(model);
|
|
137269
|
+
const targetDirectory = join(modelsDir, uniqueName);
|
|
137270
|
+
console.log(`Downloading ${slug} (${format.data})`);
|
|
137271
|
+
console.log(` Target: ${targetDirectory}`);
|
|
137272
|
+
try {
|
|
137273
|
+
await downloadModelViaHuggingFace({
|
|
137274
|
+
format: model.format,
|
|
137275
|
+
huggingFaceToken,
|
|
137276
|
+
modelSlug: slug,
|
|
137277
|
+
progressFilePath: join(modelsDir, `${uniqueName}.progress.json`),
|
|
137278
|
+
targetDirectory
|
|
137279
|
+
});
|
|
137280
|
+
console.log(`Model cached: ${targetDirectory}`);
|
|
137281
|
+
}
|
|
137282
|
+
catch (err) {
|
|
137283
|
+
console.error(`Failed downloading model: ${asError(err).message}`);
|
|
137284
|
+
process.exitCode = 1;
|
|
137285
|
+
}
|
|
137286
|
+
});
|
|
137061
137287
|
}
|
|
137062
137288
|
|
|
137063
137289
|
function parseAllowList(raw) {
|
package/dist/cli.sea.cjs
CHANGED
|
@@ -21303,6 +21303,42 @@ object$1({
|
|
|
21303
21303
|
name: ResourceNameSchema
|
|
21304
21304
|
});
|
|
21305
21305
|
|
|
21306
|
+
const FitMethodSchema = _enum$1(["gguf-header", "model-config", "parameter-heuristic"]);
|
|
21307
|
+
const FitConfidenceSchema = _enum$1(["high", "low", "medium"]);
|
|
21308
|
+
object$1({
|
|
21309
|
+
architecture: string$2().nullable(),
|
|
21310
|
+
contextTrainTokens: number$1().int().positive().nullable(),
|
|
21311
|
+
embeddingLength: number$1().int().positive().nullable(),
|
|
21312
|
+
headCount: number$1().int().positive().nullable(),
|
|
21313
|
+
headCountKV: number$1().int().positive().nullable(),
|
|
21314
|
+
headDim: number$1().int().positive().nullable(),
|
|
21315
|
+
layerCount: number$1().int().positive().nullable(),
|
|
21316
|
+
method: FitMethodSchema,
|
|
21317
|
+
probedAt: string$2(),
|
|
21318
|
+
unresolved: boolean$1().optional()
|
|
21319
|
+
});
|
|
21320
|
+
const FitMemoryDomainKindSchema = _enum$1(["gpu", "system", "unified"]);
|
|
21321
|
+
const FitMemoryDomainSchema = object$1({
|
|
21322
|
+
capacityBytes: number$1().int().nonnegative(),
|
|
21323
|
+
domainId: string$2(),
|
|
21324
|
+
kind: FitMemoryDomainKindSchema,
|
|
21325
|
+
label: string$2(),
|
|
21326
|
+
remainingBytes: number$1().int(),
|
|
21327
|
+
requiredBytes: number$1().int().nonnegative(),
|
|
21328
|
+
reserveBytes: number$1().int().nonnegative()
|
|
21329
|
+
});
|
|
21330
|
+
object$1({
|
|
21331
|
+
confidence: FitConfidenceSchema,
|
|
21332
|
+
contextTokens: number$1().int().positive(),
|
|
21333
|
+
deficitBytes: number$1().int().nonnegative(),
|
|
21334
|
+
domains: array(FitMemoryDomainSchema),
|
|
21335
|
+
engine: LLMEngineSchema,
|
|
21336
|
+
fits: boolean$1(),
|
|
21337
|
+
limitingDomain: string$2().nullable(),
|
|
21338
|
+
method: FitMethodSchema,
|
|
21339
|
+
totalRequiredBytes: number$1().int().nonnegative()
|
|
21340
|
+
});
|
|
21341
|
+
|
|
21306
21342
|
const ENGINE_API_COMPATIBILITY = {
|
|
21307
21343
|
exllamav3: {
|
|
21308
21344
|
nativeAnthropicMessages: false,
|
|
@@ -123312,6 +123348,181 @@ async function downloadFileWithRange({ accessToken, filePath, fileSize, modelSlu
|
|
|
123312
123348
|
throw new Error(errorMessage);
|
|
123313
123349
|
}
|
|
123314
123350
|
|
|
123351
|
+
const RESTORE_RETRY_ATTEMPTS = 50;
|
|
123352
|
+
const RESTORE_RETRY_DELAY_MS = 5;
|
|
123353
|
+
function errorCode(error) {
|
|
123354
|
+
if (typeof error === "object" && error !== null) {
|
|
123355
|
+
const code = error.code;
|
|
123356
|
+
if (typeof code === "string")
|
|
123357
|
+
return code;
|
|
123358
|
+
}
|
|
123359
|
+
return null;
|
|
123360
|
+
}
|
|
123361
|
+
function isFileExistsError(error) {
|
|
123362
|
+
return errorCode(error) === "EEXIST";
|
|
123363
|
+
}
|
|
123364
|
+
function isFileNotFoundError(error) {
|
|
123365
|
+
return errorCode(error) === "ENOENT";
|
|
123366
|
+
}
|
|
123367
|
+
async function readLockContents({ lockFilePath }) {
|
|
123368
|
+
try {
|
|
123369
|
+
const raw = await require$$0$m.readFile(lockFilePath, "utf-8");
|
|
123370
|
+
const data = JSON.parse(raw);
|
|
123371
|
+
if (typeof data.acquiredAt !== "number" || typeof data.token !== "string")
|
|
123372
|
+
return null;
|
|
123373
|
+
return data;
|
|
123374
|
+
}
|
|
123375
|
+
catch {
|
|
123376
|
+
return null;
|
|
123377
|
+
}
|
|
123378
|
+
}
|
|
123379
|
+
async function observeLock({ lockFilePath }) {
|
|
123380
|
+
const [contents, stats] = await Promise.all([
|
|
123381
|
+
readLockContents({ lockFilePath }),
|
|
123382
|
+
require$$0$m.stat(lockFilePath).catch(error => {
|
|
123383
|
+
if (isFileNotFoundError(error))
|
|
123384
|
+
return null;
|
|
123385
|
+
throw error;
|
|
123386
|
+
})
|
|
123387
|
+
]);
|
|
123388
|
+
return {
|
|
123389
|
+
contents,
|
|
123390
|
+
exists: stats !== null,
|
|
123391
|
+
mtimeAgeMs: stats ? Date.now() - stats.mtimeMs : null
|
|
123392
|
+
};
|
|
123393
|
+
}
|
|
123394
|
+
/**
|
|
123395
|
+
* Remove a lock file only if it is the exact instance that was observed and
|
|
123396
|
+
* verified. The candidate is first renamed to a unique quarantine path
|
|
123397
|
+
* (rename is atomic, so exactly one competing process can isolate a given
|
|
123398
|
+
* instance); the quarantined file is then re-verified against the expected
|
|
123399
|
+
* identity. A mismatch means a replacement lock was captured by the rename -
|
|
123400
|
+
* it is renamed back into place rather than deleted.
|
|
123401
|
+
*
|
|
123402
|
+
* Residual race: a restore rename could theoretically clobber a third lock
|
|
123403
|
+
* created in the microsecond window while the replacement sat in quarantine.
|
|
123404
|
+
* Files alone cannot provide a fully linearizable compare-and-delete; closing
|
|
123405
|
+
* that window entirely would require an OS-level lock primitive (flock).
|
|
123406
|
+
*/
|
|
123407
|
+
async function removeVerifiedLockInstance({ isTargetInstance, lockFilePath, logger, quarantinePrefix }) {
|
|
123408
|
+
const quarantinePath = `${lockFilePath}.${quarantinePrefix}-${process.pid}-${crypto.randomUUID()}`;
|
|
123409
|
+
try {
|
|
123410
|
+
await require$$0$m.rename(lockFilePath, quarantinePath);
|
|
123411
|
+
}
|
|
123412
|
+
catch (error) {
|
|
123413
|
+
if (isFileNotFoundError(error))
|
|
123414
|
+
return false;
|
|
123415
|
+
throw error;
|
|
123416
|
+
}
|
|
123417
|
+
const quarantined = await observeLock({ lockFilePath: quarantinePath });
|
|
123418
|
+
if (isTargetInstance(quarantined)) {
|
|
123419
|
+
await require$$0$m.rm(quarantinePath, { force: true });
|
|
123420
|
+
return true;
|
|
123421
|
+
}
|
|
123422
|
+
// A replacement lock was captured - restore it before continuing
|
|
123423
|
+
for (let attempt = 1;; attempt++) {
|
|
123424
|
+
try {
|
|
123425
|
+
await require$$0$m.rename(quarantinePath, lockFilePath);
|
|
123426
|
+
return false;
|
|
123427
|
+
}
|
|
123428
|
+
catch (error) {
|
|
123429
|
+
const code = errorCode(error);
|
|
123430
|
+
if (code !== "ENOTEMPTY" && code !== "EEXIST" && code !== "EPERM")
|
|
123431
|
+
throw error;
|
|
123432
|
+
if (attempt >= RESTORE_RETRY_ATTEMPTS) {
|
|
123433
|
+
logger.warn("Failed restoring quarantined replacement lock", {
|
|
123434
|
+
lockFilePath,
|
|
123435
|
+
quarantinePath
|
|
123436
|
+
});
|
|
123437
|
+
return false;
|
|
123438
|
+
}
|
|
123439
|
+
await new Promise(resolve => setTimeout(resolve, RESTORE_RETRY_DELAY_MS));
|
|
123440
|
+
}
|
|
123441
|
+
}
|
|
123442
|
+
}
|
|
123443
|
+
/**
|
|
123444
|
+
* Acquire an exclusive cross-process file lock. Creation uses the "wx" flag
|
|
123445
|
+
* so the check-and-create step is atomic: concurrent processes can never both
|
|
123446
|
+
* observe a missing lock file and both claim it. Each acquisition owns a
|
|
123447
|
+
* unique token; only a lock whose stored token matches the holder's handle
|
|
123448
|
+
* may be released by that holder. An active (non-stale) lock is never broken
|
|
123449
|
+
* because a waiter timed out - the waiter fails instead.
|
|
123450
|
+
*
|
|
123451
|
+
* Staleness is judged per observed instance: a parsed lock is stale when its
|
|
123452
|
+
* recorded acquisition age exceeds staleMs; an unparseable lock (possible
|
|
123453
|
+
* mid-write observation) is stale only when its mtime is itself old, so a
|
|
123454
|
+
* fresh lock can never be quarantined off the back of a partial read.
|
|
123455
|
+
*/
|
|
123456
|
+
async function acquireExclusiveFileLock({ lockFilePath, logger, pollIntervalMs, staleMs, timeoutMs }) {
|
|
123457
|
+
await require$$0$m.mkdir(path$1.dirname(lockFilePath), { recursive: true });
|
|
123458
|
+
const token = `${process.pid}-${crypto.randomUUID()}`;
|
|
123459
|
+
const deadline = Date.now() + timeoutMs;
|
|
123460
|
+
while (true) {
|
|
123461
|
+
try {
|
|
123462
|
+
const contents = { acquiredAt: Date.now(), token };
|
|
123463
|
+
await require$$0$m.writeFile(lockFilePath, JSON.stringify(contents), { flag: "wx" });
|
|
123464
|
+
logger.info("Acquired download lock", { lockFilePath });
|
|
123465
|
+
return { lockFilePath, token };
|
|
123466
|
+
}
|
|
123467
|
+
catch (error) {
|
|
123468
|
+
if (!isFileExistsError(error))
|
|
123469
|
+
throw error;
|
|
123470
|
+
}
|
|
123471
|
+
const observation = await observeLock({ lockFilePath });
|
|
123472
|
+
if (!observation.exists) {
|
|
123473
|
+
// Lock vanished between create and read - retry creation now
|
|
123474
|
+
continue;
|
|
123475
|
+
}
|
|
123476
|
+
const parsedAge = observation.contents !== null ? Date.now() - observation.contents.acquiredAt : null;
|
|
123477
|
+
const staleByContents = parsedAge !== null && parsedAge >= staleMs;
|
|
123478
|
+
const staleByMtime = observation.contents === null &&
|
|
123479
|
+
observation.mtimeAgeMs !== null &&
|
|
123480
|
+
observation.mtimeAgeMs >= staleMs;
|
|
123481
|
+
if (staleByContents) {
|
|
123482
|
+
const observedToken = observation.contents?.token;
|
|
123483
|
+
logger.info("Stale download lock found, breaking", { lockFilePath });
|
|
123484
|
+
await removeVerifiedLockInstance({
|
|
123485
|
+
isTargetInstance: candidate => candidate.contents?.token === observedToken,
|
|
123486
|
+
lockFilePath,
|
|
123487
|
+
logger,
|
|
123488
|
+
quarantinePrefix: "stale"
|
|
123489
|
+
});
|
|
123490
|
+
continue;
|
|
123491
|
+
}
|
|
123492
|
+
if (staleByMtime) {
|
|
123493
|
+
logger.info("Unreadable stale download lock found, breaking", { lockFilePath });
|
|
123494
|
+
await removeVerifiedLockInstance({
|
|
123495
|
+
isTargetInstance: candidate => candidate.contents === null &&
|
|
123496
|
+
candidate.mtimeAgeMs !== null &&
|
|
123497
|
+
candidate.mtimeAgeMs >= staleMs,
|
|
123498
|
+
lockFilePath,
|
|
123499
|
+
logger,
|
|
123500
|
+
quarantinePrefix: "stale"
|
|
123501
|
+
});
|
|
123502
|
+
continue;
|
|
123503
|
+
}
|
|
123504
|
+
if (Date.now() >= deadline) {
|
|
123505
|
+
throw new Error(`Failed acquiring download lock within ${timeoutMs}ms: ${lockFilePath}`);
|
|
123506
|
+
}
|
|
123507
|
+
logger.info("Download lock held by another process, waiting", {
|
|
123508
|
+
lockAgeMs: parsedAge ?? observation.mtimeAgeMs,
|
|
123509
|
+
lockFilePath
|
|
123510
|
+
});
|
|
123511
|
+
await new Promise(resolve => setTimeout(resolve, pollIntervalMs));
|
|
123512
|
+
}
|
|
123513
|
+
}
|
|
123514
|
+
async function releaseExclusiveFileLock({ handle }) {
|
|
123515
|
+
// Removal is verified against the handle's token at quarantine time, so
|
|
123516
|
+
// a replacement lock created after this holder's release/read can never
|
|
123517
|
+
// be unlinked, and a lock already gone is a no-op
|
|
123518
|
+
await removeVerifiedLockInstance({
|
|
123519
|
+
isTargetInstance: candidate => candidate.contents?.token === handle.token,
|
|
123520
|
+
lockFilePath: handle.lockFilePath,
|
|
123521
|
+
logger: { info: () => { }, warn: () => { } },
|
|
123522
|
+
quarantinePrefix: "release"
|
|
123523
|
+
});
|
|
123524
|
+
}
|
|
123525
|
+
|
|
123315
123526
|
const EXLLAMAV3_EXECUTABLE = process.env.EXLLAMAV3_EXECUTABLE ?? "python3";
|
|
123316
123527
|
const SERVER_SCRIPT = path$1.join(path$1.dirname(require$$0$l.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('cli.sea.cjs', document.baseURI).href)))), "server.py");
|
|
123317
123528
|
const DEFAULT_EXLLAMAV3_CONTEXT_LENGTH = 4096;
|
|
@@ -123387,7 +123598,6 @@ async function startLlamacpp({ enginePort, targetDirectory }) {
|
|
|
123387
123598
|
console.warn(`[llamacpp] Multimodal enabled but no projector file found in ${targetDirectory}. ` +
|
|
123388
123599
|
"Vision functionality will not work.");
|
|
123389
123600
|
}
|
|
123390
|
-
args.push("--special");
|
|
123391
123601
|
}
|
|
123392
123602
|
if (process.env.LLAMACPP_REASONING === "off") {
|
|
123393
123603
|
args.push("--reasoning", "off");
|
|
@@ -123522,6 +123732,7 @@ class ModelManager extends EventEmitter {
|
|
|
123522
123732
|
healthPollInterval = null;
|
|
123523
123733
|
lastEngineError = null;
|
|
123524
123734
|
lifecycleState = "stopped";
|
|
123735
|
+
downloadLockHandle = null;
|
|
123525
123736
|
stopRequested = false;
|
|
123526
123737
|
modelsDirectory;
|
|
123527
123738
|
constructor({ contextLength, engineConfig, enginePort, engineType, logger, model, root }) {
|
|
@@ -123839,35 +124050,13 @@ class ModelManager extends EventEmitter {
|
|
|
123839
124050
|
return path$1.join(this.modelsDirectory, `${this.uniqueName}.lock`);
|
|
123840
124051
|
}
|
|
123841
124052
|
async acquireDownloadLock() {
|
|
123842
|
-
|
|
123843
|
-
|
|
123844
|
-
|
|
123845
|
-
|
|
123846
|
-
|
|
123847
|
-
|
|
123848
|
-
|
|
123849
|
-
});
|
|
123850
|
-
await this.waitForDownloadLock();
|
|
123851
|
-
}
|
|
123852
|
-
else {
|
|
123853
|
-
this.logger.info("Stale download lock found, breaking", {
|
|
123854
|
-
lockFilePath: this.lockFilePath
|
|
123855
|
-
});
|
|
123856
|
-
await this.releaseDownloadLock();
|
|
123857
|
-
}
|
|
123858
|
-
}
|
|
123859
|
-
await require$$0$m.writeFile(this.lockFilePath, JSON.stringify({ acquiredAt: Date.now() }));
|
|
123860
|
-
this.logger.info("Acquired download lock", { lockFilePath: this.lockFilePath });
|
|
123861
|
-
}
|
|
123862
|
-
async getLockAge() {
|
|
123863
|
-
try {
|
|
123864
|
-
const raw = await require$$0$m.readFile(this.lockFilePath, "utf-8");
|
|
123865
|
-
const data = JSON.parse(raw);
|
|
123866
|
-
return Date.now() - data.acquiredAt;
|
|
123867
|
-
}
|
|
123868
|
-
catch {
|
|
123869
|
-
return null;
|
|
123870
|
-
}
|
|
124053
|
+
this.downloadLockHandle = await acquireExclusiveFileLock({
|
|
124054
|
+
lockFilePath: this.lockFilePath,
|
|
124055
|
+
logger: this.logger,
|
|
124056
|
+
pollIntervalMs: DOWNLOAD_LOCK_POLL_INTERVAL_MS,
|
|
124057
|
+
staleMs: DOWNLOAD_LOCK_TIMEOUT_MS,
|
|
124058
|
+
timeoutMs: DOWNLOAD_LOCK_TIMEOUT_MS
|
|
124059
|
+
});
|
|
123871
124060
|
}
|
|
123872
124061
|
recordEngineError(err) {
|
|
123873
124062
|
this.lifecycleState = "errored";
|
|
@@ -123875,27 +124064,11 @@ class ModelManager extends EventEmitter {
|
|
|
123875
124064
|
this.emit("engineError", err);
|
|
123876
124065
|
}
|
|
123877
124066
|
async releaseDownloadLock() {
|
|
123878
|
-
|
|
123879
|
-
|
|
123880
|
-
|
|
123881
|
-
|
|
123882
|
-
|
|
123883
|
-
}
|
|
123884
|
-
}
|
|
123885
|
-
async waitForDownloadLock() {
|
|
123886
|
-
const start = Date.now();
|
|
123887
|
-
while (Date.now() - start < DOWNLOAD_LOCK_TIMEOUT_MS) {
|
|
123888
|
-
await new Promise(resolve => setTimeout(resolve, DOWNLOAD_LOCK_POLL_INTERVAL_MS));
|
|
123889
|
-
if (!require$$3$4.existsSync(this.lockFilePath)) {
|
|
123890
|
-
return;
|
|
123891
|
-
}
|
|
123892
|
-
const age = await this.getLockAge();
|
|
123893
|
-
if (age === null || age >= DOWNLOAD_LOCK_TIMEOUT_MS) {
|
|
123894
|
-
await this.releaseDownloadLock();
|
|
123895
|
-
return;
|
|
123896
|
-
}
|
|
123897
|
-
}
|
|
123898
|
-
await this.releaseDownloadLock();
|
|
124067
|
+
const handle = this.downloadLockHandle;
|
|
124068
|
+
if (!handle)
|
|
124069
|
+
return;
|
|
124070
|
+
this.downloadLockHandle = null;
|
|
124071
|
+
await releaseExclusiveFileLock({ handle });
|
|
123899
124072
|
}
|
|
123900
124073
|
bindEngineProcessEvents(processManager) {
|
|
123901
124074
|
let hasTerminated = false;
|
|
@@ -157288,6 +157461,59 @@ function registerModelsCommands({ program }) {
|
|
|
157288
157461
|
}
|
|
157289
157462
|
console.log(`Cleared ${targets.length} model(s), freed ${formatBytes(totalBytes)}`);
|
|
157290
157463
|
});
|
|
157464
|
+
models
|
|
157465
|
+
.command("download")
|
|
157466
|
+
.description("Pre-download a HuggingFace model into the local cache")
|
|
157467
|
+
.requiredOption("--slug <slug>", "Fully qualified model slug, optionally with a quantization variant (owner/repo:Q4_K_M)")
|
|
157468
|
+
.option("--format <format>", "Model format (gguf, safetensors, pytorch, ...)", "gguf")
|
|
157469
|
+
.option("--root <path>", "Root directory (or ROOT_DIRECTORY env)")
|
|
157470
|
+
.option("--token <token>", "HuggingFace access token (or HF_TOKEN env)")
|
|
157471
|
+
.action(async (options) => {
|
|
157472
|
+
const slug = options.slug?.trim() ?? "";
|
|
157473
|
+
if (!slug.includes("/")) {
|
|
157474
|
+
console.error("Slug must be a fully qualified owner/repo slug");
|
|
157475
|
+
process.exitCode = 1;
|
|
157476
|
+
return;
|
|
157477
|
+
}
|
|
157478
|
+
const format = LLMModelFormatSchema.safeParse(options.format);
|
|
157479
|
+
if (!format.success) {
|
|
157480
|
+
console.error(`Invalid format "${options.format}". Must be one of: ${LLMModelFormatSchema.options.join(", ")}`);
|
|
157481
|
+
process.exitCode = 1;
|
|
157482
|
+
return;
|
|
157483
|
+
}
|
|
157484
|
+
const huggingFaceToken = options.token?.trim() || null;
|
|
157485
|
+
const model = {
|
|
157486
|
+
format: format.data,
|
|
157487
|
+
id: slug,
|
|
157488
|
+
multimodalEnabled: false,
|
|
157489
|
+
source: {
|
|
157490
|
+
modelSecret: huggingFaceToken,
|
|
157491
|
+
slug,
|
|
157492
|
+
type: "huggingface"
|
|
157493
|
+
},
|
|
157494
|
+
taskType: "text-generation"
|
|
157495
|
+
};
|
|
157496
|
+
const rootDirectory = getRootDirectory(options.root);
|
|
157497
|
+
const modelsDir = path$1.join(rootDirectory, "models");
|
|
157498
|
+
const uniqueName = createModelStorageKey(model);
|
|
157499
|
+
const targetDirectory = path$1.join(modelsDir, uniqueName);
|
|
157500
|
+
console.log(`Downloading ${slug} (${format.data})`);
|
|
157501
|
+
console.log(` Target: ${targetDirectory}`);
|
|
157502
|
+
try {
|
|
157503
|
+
await downloadModelViaHuggingFace({
|
|
157504
|
+
format: model.format,
|
|
157505
|
+
huggingFaceToken,
|
|
157506
|
+
modelSlug: slug,
|
|
157507
|
+
progressFilePath: path$1.join(modelsDir, `${uniqueName}.progress.json`),
|
|
157508
|
+
targetDirectory
|
|
157509
|
+
});
|
|
157510
|
+
console.log(`Model cached: ${targetDirectory}`);
|
|
157511
|
+
}
|
|
157512
|
+
catch (err) {
|
|
157513
|
+
console.error(`Failed downloading model: ${asError(err).message}`);
|
|
157514
|
+
process.exitCode = 1;
|
|
157515
|
+
}
|
|
157516
|
+
});
|
|
157291
157517
|
}
|
|
157292
157518
|
|
|
157293
157519
|
function parseAllowList(raw) {
|
|
@@ -22,6 +22,7 @@ export declare class ModelManager extends EventEmitter<ModelManagerEvents> {
|
|
|
22
22
|
private healthPollInterval;
|
|
23
23
|
private lastEngineError;
|
|
24
24
|
private lifecycleState;
|
|
25
|
+
private downloadLockHandle;
|
|
25
26
|
private stopRequested;
|
|
26
27
|
protected readonly modelsDirectory: string;
|
|
27
28
|
constructor({ contextLength, engineConfig, enginePort, engineType, logger, model, root }: {
|
|
@@ -52,10 +53,8 @@ export declare class ModelManager extends EventEmitter<ModelManagerEvents> {
|
|
|
52
53
|
private startHealthPoll;
|
|
53
54
|
private get lockFilePath();
|
|
54
55
|
private acquireDownloadLock;
|
|
55
|
-
private getLockAge;
|
|
56
56
|
private recordEngineError;
|
|
57
57
|
private releaseDownloadLock;
|
|
58
|
-
private waitForDownloadLock;
|
|
59
58
|
private bindEngineProcessEvents;
|
|
60
59
|
private startEngineProcess;
|
|
61
60
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
interface LockLogger {
|
|
2
|
+
info(message: string, attributes?: Record<string, unknown>): void;
|
|
3
|
+
warn(message: string, attributes?: Record<string, unknown>): void;
|
|
4
|
+
}
|
|
5
|
+
export interface DownloadLockHandle {
|
|
6
|
+
lockFilePath: string;
|
|
7
|
+
token: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Acquire an exclusive cross-process file lock. Creation uses the "wx" flag
|
|
11
|
+
* so the check-and-create step is atomic: concurrent processes can never both
|
|
12
|
+
* observe a missing lock file and both claim it. Each acquisition owns a
|
|
13
|
+
* unique token; only a lock whose stored token matches the holder's handle
|
|
14
|
+
* may be released by that holder. An active (non-stale) lock is never broken
|
|
15
|
+
* because a waiter timed out - the waiter fails instead.
|
|
16
|
+
*
|
|
17
|
+
* Staleness is judged per observed instance: a parsed lock is stale when its
|
|
18
|
+
* recorded acquisition age exceeds staleMs; an unparseable lock (possible
|
|
19
|
+
* mid-write observation) is stale only when its mtime is itself old, so a
|
|
20
|
+
* fresh lock can never be quarantined off the back of a partial read.
|
|
21
|
+
*/
|
|
22
|
+
export declare function acquireExclusiveFileLock({ lockFilePath, logger, pollIntervalMs, staleMs, timeoutMs }: {
|
|
23
|
+
lockFilePath: string;
|
|
24
|
+
logger: LockLogger;
|
|
25
|
+
pollIntervalMs: number;
|
|
26
|
+
staleMs: number;
|
|
27
|
+
timeoutMs: number;
|
|
28
|
+
}): Promise<DownloadLockHandle>;
|
|
29
|
+
export declare function releaseExclusiveFileLock({ handle }: {
|
|
30
|
+
handle: DownloadLockHandle;
|
|
31
|
+
}): Promise<void>;
|
|
32
|
+
export {};
|