@infersec/conduit 1.107.0 → 1.109.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 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
- await mkdir(this.modelsDirectory, { recursive: true });
123828
- if (existsSync(this.lockFilePath)) {
123829
- const age = await this.getLockAge();
123830
- if (age !== null && age < DOWNLOAD_LOCK_TIMEOUT_MS) {
123831
- this.logger.info("Download lock held by another process, waiting", {
123832
- lockAgeMs: age,
123833
- lockFilePath: this.lockFilePath
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
- try {
123864
- await unlink(this.lockFilePath);
123865
- }
123866
- catch {
123867
- // already released
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
- await require$$0$m.mkdir(this.modelsDirectory, { recursive: true });
123843
- if (require$$3$4.existsSync(this.lockFilePath)) {
123844
- const age = await this.getLockAge();
123845
- if (age !== null && age < DOWNLOAD_LOCK_TIMEOUT_MS) {
123846
- this.logger.info("Download lock held by another process, waiting", {
123847
- lockAgeMs: age,
123848
- lockFilePath: this.lockFilePath
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
- try {
123879
- await require$$0$m.unlink(this.lockFilePath);
123880
- }
123881
- catch {
123882
- // already released
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 {};
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@infersec/conduit",
3
3
  "description": "End user conduit agent for connecting local LLMs to the cloud.",
4
- "version": "1.107.0",
4
+ "version": "1.109.0",
5
5
  "bin": {
6
6
  "infersec-conduit": "./dist/cli.js"
7
7
  },