@nmakarov/cli-toolkit 0.18.0 → 0.21.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.
@@ -907,11 +907,11 @@ function buildBreadcrumb(parts) {
907
907
  if (parts.length === 1) return parts[0];
908
908
  return parts.slice(1).map((part) => `\u2190 ${part}`).join(" ");
909
909
  }
910
- function buildDetailBreadcrumb(path5, suffix = "") {
911
- if (path5.length <= 1) {
912
- return suffix ? `\u2190 ${suffix}` : path5[0] || "";
910
+ function buildDetailBreadcrumb(path6, suffix = "") {
911
+ if (path6.length <= 1) {
912
+ return suffix ? `\u2190 ${suffix}` : path6[0] || "";
913
913
  }
914
- const breadcrumb = buildBreadcrumb(path5);
914
+ const breadcrumb = buildBreadcrumb(path6);
915
915
  return suffix ? `${breadcrumb} ${suffix}` : breadcrumb;
916
916
  }
917
917
  var init_utils = __esm({
@@ -1101,7 +1101,7 @@ var init_screen = __esm({
1101
1101
  });
1102
1102
 
1103
1103
  // src/scripts/cli-runner.ts
1104
- import path4 from "path";
1104
+ import path5 from "path";
1105
1105
  import { pathToFileURL } from "url";
1106
1106
 
1107
1107
  // src/args/index.ts
@@ -1927,7 +1927,7 @@ var Params = class _Params {
1927
1927
  throw new ParamError(`default value "${defValObj.value}" type mismatch`);
1928
1928
  }
1929
1929
  type = type.default(defValObj.value);
1930
- } else if (str.match(/required/)) {
1930
+ } else if (str.match(/\s*required\s*/)) {
1931
1931
  type = type.required();
1932
1932
  } else {
1933
1933
  type = type.optional();
@@ -2003,6 +2003,8 @@ var Params = class _Params {
2003
2003
  /**
2004
2004
  * Get all parameters from definitions (main script).
2005
2005
  * Same as getAllForModule("script", defs). Processes left-to-right for cross-parameter references.
2006
+ * Libraries should use {@link getAllForModule} with an explicit module name (or {@link runWithModule}
2007
+ * around {@link get}) so --showUsedParams groups usage correctly.
2006
2008
  */
2007
2009
  getAll(defs2) {
2008
2010
  return this.getAllForModule("script", defs2);
@@ -2039,6 +2041,19 @@ var Params = class _Params {
2039
2041
  this._currentModule = prev;
2040
2042
  }
2041
2043
  }
2044
+ /**
2045
+ * Run a callback with {@link _currentModule} set so single {@link get} calls are tracked
2046
+ * under the same module (for --showUsedParams / getFiguredByModule).
2047
+ */
2048
+ runWithModule(moduleName, fn) {
2049
+ const prev = this._currentModule;
2050
+ this._currentModule = moduleName;
2051
+ try {
2052
+ return fn();
2053
+ } finally {
2054
+ this._currentModule = prev;
2055
+ }
2056
+ }
2042
2057
  /**
2043
2058
  * Infer module name from call stack: first caller outside params/index gives path like .../src/<moduleName>/...
2044
2059
  */
@@ -2052,9 +2067,9 @@ var Params = class _Params {
2052
2067
  if (!parenMatch) continue;
2053
2068
  const parts = parenMatch[1].split(":");
2054
2069
  if (parts.length < 3) continue;
2055
- const path5 = parts.slice(0, -2).join(":").replace(/^file:\/\//, "");
2056
- if (!path5 || path5.includes(paramsIndexPath)) continue;
2057
- const srcMatch = path5.match(/[/\\]src[/\\]([^/\\]+)(?:[/\\]|$)/);
2070
+ const path6 = parts.slice(0, -2).join(":").replace(/^file:\/\//, "");
2071
+ if (!path6 || path6.includes(paramsIndexPath)) continue;
2072
+ const srcMatch = path6.match(/[/\\]src[/\\]([^/\\]+)(?:[/\\]|$)/);
2058
2073
  if (srcMatch) return srcMatch[1];
2059
2074
  }
2060
2075
  return "script";
@@ -2905,7 +2920,7 @@ async function dbFindAndConnect(context, dbNameOrConnectionString) {
2905
2920
  dbConnectionString: "string",
2906
2921
  dbProfile: "boolean default false"
2907
2922
  };
2908
- const paramsConfig = context.params.getAllForModule(defs2);
2923
+ const paramsConfig = context.params.getAll(defs2);
2909
2924
  dbName = paramsConfig.dbName;
2910
2925
  dbConnectionString = paramsConfig.dbConnectionString;
2911
2926
  dbProfile = paramsConfig.dbProfile;
@@ -3010,6 +3025,12 @@ function toJsonColumn(value) {
3010
3025
  return JSON.stringify(value);
3011
3026
  }
3012
3027
 
3028
+ // src/tasks/servicesRegistry.ts
3029
+ import { randomUUID as randomUUID2 } from "crypto";
3030
+ import { mkdir, readFile, writeFile } from "fs/promises";
3031
+ import os from "os";
3032
+ import path3 from "path";
3033
+
3013
3034
  // src/tasks/taskUtils.ts
3014
3035
  import { randomUUID } from "crypto";
3015
3036
  function getDb(context) {
@@ -3028,6 +3049,12 @@ function queueToTableNames(queue) {
3028
3049
  historyTable: `${queue}_history`
3029
3050
  };
3030
3051
  }
3052
+ function servicesRegistryTable(queue) {
3053
+ if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(queue)) {
3054
+ throw new Error(`Invalid queue name "${queue}". Use letters, numbers, underscore only.`);
3055
+ }
3056
+ return `${queue}_services_registry`;
3057
+ }
3031
3058
  async function ensureTaskTables(context, options = {}) {
3032
3059
  const queue = options.queue ?? "tasks";
3033
3060
  const recreate = options.recreate ?? false;
@@ -3064,18 +3091,6 @@ async function ensureTaskTables(context, options = {}) {
3064
3091
  t.index(["target", "task"], `${tasksTable}_target_task_idx`);
3065
3092
  });
3066
3093
  }
3067
- const tasksHasOpid = await db.schema.hasColumn(tasksTable, "opid");
3068
- if (!tasksHasOpid) {
3069
- await db.schema.alterTable(tasksTable, (t) => {
3070
- t.text("opid");
3071
- });
3072
- }
3073
- const tasksHasPausedAt = await db.schema.hasColumn(tasksTable, "paused_at");
3074
- if (!tasksHasPausedAt) {
3075
- await db.schema.alterTable(tasksTable, (t) => {
3076
- t.timestamp("paused_at").defaultTo(null);
3077
- });
3078
- }
3079
3094
  if (needsHistory) {
3080
3095
  await db.schema.createTable(historyTable, (t) => {
3081
3096
  t.uuid("id").notNullable();
@@ -3098,10 +3113,24 @@ async function ensureTaskTables(context, options = {}) {
3098
3113
  t.index(["task", "created_at"], `${historyTable}_task_created_idx`);
3099
3114
  });
3100
3115
  }
3101
- const historyHasOpid = await db.schema.hasColumn(historyTable, "opid");
3102
- if (!historyHasOpid) {
3103
- await db.schema.alterTable(historyTable, (t) => {
3104
- t.text("opid");
3116
+ const registryTable = servicesRegistryTable(queue);
3117
+ const needsRegistry = !await db.tableExists(registryTable);
3118
+ if (needsRegistry) {
3119
+ await db.schema.createTable(registryTable, (t) => {
3120
+ t.uuid("id").primary().defaultTo(db.raw("uuid_generate_v4()"));
3121
+ t.uuid("instance_id").notNullable().unique();
3122
+ t.text("queue").notNullable();
3123
+ t.text("service_group").notNullable();
3124
+ t.text("service_name").notNullable();
3125
+ t.text("target").notNullable();
3126
+ t.text("hostname");
3127
+ t.integer("pid");
3128
+ t.json("metadata");
3129
+ t.timestamp("created_at").notNullable().defaultTo(db.fn.now());
3130
+ t.timestamp("last_seen_at").notNullable().defaultTo(db.fn.now());
3131
+ t.unique(["queue", "service_name"], `${registryTable}_queue_service_name_uniq`);
3132
+ t.index(["queue", "service_group", "last_seen_at"], `${registryTable}_queue_group_seen_idx`);
3133
+ t.index(["queue", "last_seen_at"], `${registryTable}_queue_seen_idx`);
3105
3134
  });
3106
3135
  }
3107
3136
  }
@@ -3112,9 +3141,228 @@ async function updateTaskProgress(context, tasksTable, taskId, progress) {
3112
3141
  });
3113
3142
  }
3114
3143
 
3144
+ // src/tasks/servicesRegistry.ts
3145
+ function getDb2(context) {
3146
+ const db = context.db;
3147
+ if (!db) {
3148
+ throw new Error("Services registry requires context.db");
3149
+ }
3150
+ return db;
3151
+ }
3152
+ var DEFAULT_GROUP_MAX_INSTANCES = {
3153
+ intake: 1,
3154
+ harvest: 1,
3155
+ loader: 0,
3156
+ photos: 0,
3157
+ photosprocessor: 0,
3158
+ ingest: 0
3159
+ };
3160
+ function sanitizeNamePart(raw) {
3161
+ const s = String(raw || "").trim().replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
3162
+ return s.slice(0, 80) || "runner";
3163
+ }
3164
+ function identityFilePath(identityDir, queue, serviceGroup) {
3165
+ const safeQ = sanitizeNamePart(queue);
3166
+ const safeG = sanitizeNamePart(serviceGroup);
3167
+ return path3.join(identityDir, `${safeQ}_${safeG}.json`);
3168
+ }
3169
+ async function readIdentityFile(filePath) {
3170
+ try {
3171
+ const text = await readFile(filePath, "utf8");
3172
+ const parsed = JSON.parse(text);
3173
+ return parsed && typeof parsed === "object" ? parsed : {};
3174
+ } catch {
3175
+ return {};
3176
+ }
3177
+ }
3178
+ async function writeIdentityFile(filePath, data) {
3179
+ await mkdir(path3.dirname(filePath), { recursive: true });
3180
+ await writeFile(filePath, `${JSON.stringify(data, null, 2)}
3181
+ `, "utf8");
3182
+ }
3183
+ function resolveMaxInstances(serviceGroup, override) {
3184
+ if (override !== void 0 && Number.isFinite(override)) {
3185
+ return Math.max(0, Math.floor(Number(override)));
3186
+ }
3187
+ const g = serviceGroup.trim().toLowerCase();
3188
+ return DEFAULT_GROUP_MAX_INSTANCES[g] ?? 0;
3189
+ }
3190
+ async function countAliveInGroup(db, registryTable, queue, serviceGroup, staleMs, excludeInstanceId) {
3191
+ const cutoff = new Date(Date.now() - staleMs);
3192
+ let q = db(registryTable).where({ queue, service_group: serviceGroup }).where("last_seen_at", ">", cutoff);
3193
+ if (excludeInstanceId) {
3194
+ q = q.whereNot("instance_id", excludeInstanceId);
3195
+ }
3196
+ const row = await q.count("id as count").first();
3197
+ return Number(row?.count ?? 0);
3198
+ }
3199
+ function isUniqueViolation(error) {
3200
+ const code = error?.code ?? error?.errno;
3201
+ return code === "23505" || String(error?.message || "").includes("duplicate key");
3202
+ }
3203
+ async function registerInServicesRegistry(context, options) {
3204
+ const db = getDb2(context);
3205
+ const registryTable = servicesRegistryTable(options.queue);
3206
+ const serviceGroup = options.serviceGroup.trim();
3207
+ if (!serviceGroup) {
3208
+ throw new Error("registerInServicesRegistry: serviceGroup is required");
3209
+ }
3210
+ const identityPath = identityFilePath(options.identityDir, options.queue, serviceGroup);
3211
+ let identity = await readIdentityFile(identityPath);
3212
+ let instanceId = typeof identity.instanceId === "string" && identity.instanceId.trim() ? identity.instanceId.trim() : randomUUID2();
3213
+ identity.instanceId = instanceId;
3214
+ await writeIdentityFile(identityPath, identity);
3215
+ const hostname = os.hostname();
3216
+ const pid = typeof process.pid === "number" ? process.pid : null;
3217
+ const meta = toJsonColumn(options.metadata ?? null);
3218
+ const existing = await db(registryTable).where({ instance_id: instanceId }).first();
3219
+ if (existing) {
3220
+ await db(registryTable).where({ instance_id: instanceId }).update({
3221
+ target: options.target,
3222
+ hostname,
3223
+ pid,
3224
+ metadata: meta,
3225
+ last_seen_at: db.fn.now()
3226
+ });
3227
+ const serviceName = String(existing.service_name);
3228
+ identity.serviceName = serviceName;
3229
+ await writeIdentityFile(identityPath, identity);
3230
+ const reg = {
3231
+ instanceId,
3232
+ serviceName,
3233
+ serviceGroup,
3234
+ queue: options.queue,
3235
+ target: options.target,
3236
+ rowId: String(existing.id)
3237
+ };
3238
+ context.servicesRegistry = reg;
3239
+ context.runnerHeartbeat = reg;
3240
+ context.logger.info?.(
3241
+ `[services-registry] resumed instance_id=${instanceId} name=${serviceName} group=${serviceGroup} queue=${options.queue}`
3242
+ );
3243
+ return {
3244
+ instanceId,
3245
+ serviceName,
3246
+ serviceGroup,
3247
+ queue: options.queue,
3248
+ target: options.target,
3249
+ rowId: String(existing.id),
3250
+ registryTable
3251
+ };
3252
+ }
3253
+ const maxAllowed = resolveMaxInstances(serviceGroup, options.groupMaxInstances);
3254
+ const aliveOthers = await countAliveInGroup(
3255
+ db,
3256
+ registryTable,
3257
+ options.queue,
3258
+ serviceGroup,
3259
+ options.staleMs,
3260
+ instanceId
3261
+ );
3262
+ if (maxAllowed > 0 && aliveOthers >= maxAllowed) {
3263
+ const msg = `[services-registry] group limit reached for "${serviceGroup}": ${aliveOthers} alive (max ${maxAllowed}, queue=${options.queue}).`;
3264
+ if (options.enforceMaxInstances) {
3265
+ throw new Error(msg);
3266
+ }
3267
+ context.logger.warn?.(`${msg} Starting anyway (runnerEnforceMaxInstances=false).`);
3268
+ }
3269
+ const explicitName = options.serviceName?.trim();
3270
+ const fromFile = typeof identity.serviceName === "string" ? identity.serviceName.trim() : "";
3271
+ const hostBase = sanitizeNamePart(hostname);
3272
+ const groupBase = sanitizeNamePart(serviceGroup);
3273
+ const baseCandidates = [];
3274
+ if (explicitName) baseCandidates.push(sanitizeNamePart(explicitName));
3275
+ if (fromFile) baseCandidates.push(sanitizeNamePart(fromFile));
3276
+ baseCandidates.push(`${groupBase}-${hostBase}`);
3277
+ baseCandidates.push(groupBase);
3278
+ function* eachServiceNameCandidate(bases) {
3279
+ const seen = /* @__PURE__ */ new Set();
3280
+ for (const rawBase of bases) {
3281
+ const base = sanitizeNamePart(rawBase);
3282
+ if (!base) continue;
3283
+ const seq = [base];
3284
+ for (let n = 2; n <= 500; n++) seq.push(`${base}-${n}`);
3285
+ for (const c of seq) {
3286
+ if (seen.has(c)) continue;
3287
+ seen.add(c);
3288
+ yield c;
3289
+ }
3290
+ }
3291
+ }
3292
+ let inserted;
3293
+ for (const candidate of eachServiceNameCandidate(baseCandidates)) {
3294
+ try {
3295
+ const rows = await db(registryTable).insert({
3296
+ instance_id: instanceId,
3297
+ queue: options.queue,
3298
+ service_group: serviceGroup,
3299
+ service_name: candidate,
3300
+ target: options.target,
3301
+ hostname,
3302
+ pid,
3303
+ metadata: meta,
3304
+ last_seen_at: db.fn.now()
3305
+ }).returning(["id", "service_name"]);
3306
+ const row = Array.isArray(rows) ? rows[0] : rows;
3307
+ if (row) {
3308
+ inserted = { id: String(row.id), service_name: String(row.service_name) };
3309
+ break;
3310
+ }
3311
+ } catch (error) {
3312
+ if (!isUniqueViolation(error)) {
3313
+ throw error;
3314
+ }
3315
+ }
3316
+ }
3317
+ if (!inserted) {
3318
+ throw new Error(
3319
+ `[services-registry] could not allocate a unique service_name for group=${serviceGroup} queue=${options.queue} (too many collisions).`
3320
+ );
3321
+ }
3322
+ identity.serviceName = inserted.service_name;
3323
+ await writeIdentityFile(identityPath, identity);
3324
+ const regNew = {
3325
+ instanceId,
3326
+ serviceName: inserted.service_name,
3327
+ serviceGroup,
3328
+ queue: options.queue,
3329
+ target: options.target,
3330
+ rowId: inserted.id
3331
+ };
3332
+ context.servicesRegistry = regNew;
3333
+ context.runnerHeartbeat = regNew;
3334
+ context.logger.info?.(
3335
+ `[services-registry] registered instance_id=${instanceId} name=${inserted.service_name} group=${serviceGroup} queue=${options.queue} target=${options.target}`
3336
+ );
3337
+ return {
3338
+ instanceId,
3339
+ serviceName: inserted.service_name,
3340
+ serviceGroup,
3341
+ queue: options.queue,
3342
+ target: options.target,
3343
+ rowId: inserted.id,
3344
+ registryTable
3345
+ };
3346
+ }
3347
+ async function touchServicesRegistry(context, registration) {
3348
+ const db = getDb2(context);
3349
+ const hostname = os.hostname();
3350
+ const pid = typeof process.pid === "number" ? process.pid : null;
3351
+ await db(registration.registryTable).where({ instance_id: registration.instanceId }).update({
3352
+ last_seen_at: db.fn.now(),
3353
+ hostname,
3354
+ pid
3355
+ });
3356
+ }
3357
+ async function unregisterServicesRegistry(context, registration) {
3358
+ const db = getDb2(context);
3359
+ await db(registration.registryTable).where({ instance_id: registration.instanceId }).delete();
3360
+ context.logger.info?.(`[services-registry] unregistered name=${registration.serviceName} instance_id=${registration.instanceId}`);
3361
+ }
3362
+
3115
3363
  // src/filedatabase/index.ts
3116
3364
  import fs3 from "fs";
3117
- import path3 from "path";
3365
+ import path4 from "path";
3118
3366
 
3119
3367
  // src/filedatabase/serializers.ts
3120
3368
  function detectDataType(data) {
@@ -3241,7 +3489,7 @@ var FileDatabase = class _FileDatabase {
3241
3489
  if (this.versioned && version) {
3242
3490
  parts.push(version);
3243
3491
  }
3244
- return path3.resolve(...parts);
3492
+ return path4.resolve(...parts);
3245
3493
  }
3246
3494
  /**
3247
3495
  * Set current version and version folder
@@ -3278,7 +3526,7 @@ var FileDatabase = class _FileDatabase {
3278
3526
  this.currentFileNumber = 0;
3279
3527
  const versions = await this.getVersions();
3280
3528
  while (versions.length > this.maxVersions) {
3281
- const versionToDelete = path3.resolve(this.getDestinationPath(), versions.shift());
3529
+ const versionToDelete = path4.resolve(this.getDestinationPath(), versions.shift());
3282
3530
  this.logger.silly?.(`[FileDatabase] Deleting old version: ${versionToDelete}`);
3283
3531
  await fs3.promises.rm(versionToDelete, { recursive: true, force: true });
3284
3532
  }
@@ -3297,7 +3545,7 @@ var FileDatabase = class _FileDatabase {
3297
3545
  await ensurePath(destPath);
3298
3546
  const items = await fs3.promises.readdir(destPath);
3299
3547
  const versions = items.filter((item) => {
3300
- const itemPath = path3.join(destPath, item);
3548
+ const itemPath = path4.join(destPath, item);
3301
3549
  const stat = fs3.statSync(itemPath);
3302
3550
  return stat.isDirectory() && isTimestampFolder(item);
3303
3551
  });
@@ -3354,7 +3602,7 @@ var FileDatabase = class _FileDatabase {
3354
3602
  const items = await fs3.promises.readdir(tablePath);
3355
3603
  if (items.includes("metadata.json")) {
3356
3604
  const metadata = JSON.parse(
3357
- await fs3.promises.readFile(path3.join(tablePath, "metadata.json"), "utf8")
3605
+ await fs3.promises.readFile(path4.join(tablePath, "metadata.json"), "utf8")
3358
3606
  );
3359
3607
  return {
3360
3608
  versioned: false,
@@ -3363,13 +3611,13 @@ var FileDatabase = class _FileDatabase {
3363
3611
  };
3364
3612
  }
3365
3613
  const versionFolders = items.filter((item) => {
3366
- const itemPath = path3.join(tablePath, item);
3614
+ const itemPath = path4.join(tablePath, item);
3367
3615
  const stat = fs3.statSync(itemPath);
3368
3616
  return stat.isDirectory() && isTimestampFolder(item);
3369
3617
  });
3370
3618
  if (versionFolders.length > 0) {
3371
3619
  const latestVersion = versionFolders.sort().pop();
3372
- const versionMetadataPath = path3.join(tablePath, latestVersion, "metadata.json");
3620
+ const versionMetadataPath = path4.join(tablePath, latestVersion, "metadata.json");
3373
3621
  return {
3374
3622
  versioned: true,
3375
3623
  hasMetadata: fs3.existsSync(versionMetadataPath),
@@ -3390,7 +3638,7 @@ var FileDatabase = class _FileDatabase {
3390
3638
  * Load metadata from JSON file
3391
3639
  */
3392
3640
  async loadMetadataJson(version) {
3393
- const metadataFile = path3.join(this.getDestinationPath(), version, "metadata.json");
3641
+ const metadataFile = path4.join(this.getDestinationPath(), version, "metadata.json");
3394
3642
  if (fs3.existsSync(metadataFile)) {
3395
3643
  try {
3396
3644
  const rawData = await fs3.promises.readFile(metadataFile, "utf8");
@@ -3406,7 +3654,7 @@ var FileDatabase = class _FileDatabase {
3406
3654
  * Reads all files to get accurate counts - used when synopsis calculation is needed
3407
3655
  */
3408
3656
  async figureMetadataFromVersionFiles(version) {
3409
- const versionPath = path3.join(this.getDestinationPath(), version);
3657
+ const versionPath = path4.join(this.getDestinationPath(), version);
3410
3658
  if (!fs3.existsSync(versionPath)) {
3411
3659
  return this.getDefaultMetadata();
3412
3660
  }
@@ -3418,10 +3666,10 @@ var FileDatabase = class _FileDatabase {
3418
3666
  let detectedDataType = null;
3419
3667
  for (let i = 0; i < files.length; i++) {
3420
3668
  const fileName = files[i];
3421
- const filePath = path3.join(versionPath, fileName);
3669
+ const filePath = path4.join(versionPath, fileName);
3422
3670
  try {
3423
3671
  const rawData = await fs3.promises.readFile(filePath, "utf8");
3424
- const extension = path3.extname(fileName).toLowerCase();
3672
+ const extension = path4.extname(fileName).toLowerCase();
3425
3673
  let dataType = "text";
3426
3674
  if (extension === ".json") {
3427
3675
  dataType = "json-array";
@@ -3454,7 +3702,7 @@ var FileDatabase = class _FileDatabase {
3454
3702
  * Much faster for large datasets with many files
3455
3703
  */
3456
3704
  async buildMetadataOptimized(version) {
3457
- const versionPath = path3.join(this.getDestinationPath(), version);
3705
+ const versionPath = path4.join(this.getDestinationPath(), version);
3458
3706
  if (!fs3.existsSync(versionPath)) {
3459
3707
  return this.getDefaultMetadata();
3460
3708
  }
@@ -3470,7 +3718,7 @@ var FileDatabase = class _FileDatabase {
3470
3718
  fileName
3471
3719
  }));
3472
3720
  const firstFile = metadata.files[0];
3473
- const firstFilePath = path3.join(versionPath, firstFile.fileName);
3721
+ const firstFilePath = path4.join(versionPath, firstFile.fileName);
3474
3722
  const firstFileRaw = await fs3.promises.readFile(firstFilePath, "utf8");
3475
3723
  let firstFileData;
3476
3724
  try {
@@ -3487,7 +3735,7 @@ var FileDatabase = class _FileDatabase {
3487
3735
  }
3488
3736
  if (files.length > 1) {
3489
3737
  const lastFile = metadata.files[metadata.files.length - 1];
3490
- const lastFilePath = path3.join(versionPath, lastFile.fileName);
3738
+ const lastFilePath = path4.join(versionPath, lastFile.fileName);
3491
3739
  const lastFileRaw = await fs3.promises.readFile(lastFilePath, "utf8");
3492
3740
  const lastFileData = deserializeData(lastFileRaw, metadata.dataType);
3493
3741
  lastFile.recordsCount = Array.isArray(lastFileData) ? lastFileData.length : 1;
@@ -3538,9 +3786,9 @@ var FileDatabase = class _FileDatabase {
3538
3786
  if (!this.currentVersion) {
3539
3787
  return;
3540
3788
  }
3541
- metadataFile = path3.join(this.getDestinationPath(), this.currentVersion, "metadata.json");
3789
+ metadataFile = path4.join(this.getDestinationPath(), this.currentVersion, "metadata.json");
3542
3790
  } else {
3543
- metadataFile = path3.join(this.getDestinationPath(), "metadata.json");
3791
+ metadataFile = path4.join(this.getDestinationPath(), "metadata.json");
3544
3792
  }
3545
3793
  await fs3.promises.writeFile(metadataFile, JSON.stringify(metadataToSave, null, 4), "utf8");
3546
3794
  }
@@ -3603,7 +3851,7 @@ var FileDatabase = class _FileDatabase {
3603
3851
  }
3604
3852
  }
3605
3853
  if (!Array.isArray(data) && !forceNewFile) {
3606
- const lastFileExtension = path3.extname(lastFile.fileName);
3854
+ const lastFileExtension = path4.extname(lastFile.fileName);
3607
3855
  const expectedExtension = `.${getFileExtension(incomingDataType)}`;
3608
3856
  if (lastFileExtension !== expectedExtension) {
3609
3857
  if (lastFileRecordsCount > 0) {
@@ -3613,7 +3861,7 @@ var FileDatabase = class _FileDatabase {
3613
3861
  }
3614
3862
  }
3615
3863
  } else if (!Array.isArray(data) && forceNewFile) {
3616
- const lastFileExtension = path3.extname(lastFile.fileName);
3864
+ const lastFileExtension = path4.extname(lastFile.fileName);
3617
3865
  const expectedExtension = `.${getFileExtension(incomingDataType)}`;
3618
3866
  if (lastFileExtension !== expectedExtension) {
3619
3867
  lastFile.fileName = `${this.currentFileNumber.toString().padStart(6, "0")}.${getFileExtension(incomingDataType)}`;
@@ -3703,7 +3951,7 @@ var FileDatabase = class _FileDatabase {
3703
3951
  */
3704
3952
  async safeWrite(filePath, data) {
3705
3953
  const serializedData = serializeData(data);
3706
- const dir = path3.dirname(filePath);
3954
+ const dir = path4.dirname(filePath);
3707
3955
  const requiredBytes = Buffer.byteLength(serializedData, "utf8");
3708
3956
  const freeBytes = getFreeDiskSpace(dir);
3709
3957
  if (freeBytes !== null) {
@@ -3748,7 +3996,7 @@ var FileDatabase = class _FileDatabase {
3748
3996
  } else {
3749
3997
  await ensurePath(this.getDestinationPath());
3750
3998
  if (this.useMetadata === true) {
3751
- const metadataPath = path3.join(this.getDestinationPath(), "metadata.json");
3999
+ const metadataPath = path4.join(this.getDestinationPath(), "metadata.json");
3752
4000
  if (fs3.existsSync(metadataPath)) {
3753
4001
  try {
3754
4002
  const rawData = await fs3.promises.readFile(metadataPath, "utf8");
@@ -3801,7 +4049,7 @@ var FileDatabase = class _FileDatabase {
3801
4049
  }
3802
4050
  if (this.useMetadata) {
3803
4051
  const destPath = this.getDestinationPath();
3804
- const metadataPath = path3.join(destPath, "metadata.json");
4052
+ const metadataPath = path4.join(destPath, "metadata.json");
3805
4053
  if (fs3.existsSync(metadataPath)) {
3806
4054
  try {
3807
4055
  const rawData = await fs3.promises.readFile(metadataPath, "utf8");
@@ -3837,7 +4085,7 @@ var FileDatabase = class _FileDatabase {
3837
4085
  if (options.filename) {
3838
4086
  const destPath2 = this.getDestinationPath();
3839
4087
  await ensurePath(destPath2);
3840
- const filePath = path3.join(destPath2, options.filename);
4088
+ const filePath = path4.join(destPath2, options.filename);
3841
4089
  await this.safeWrite(filePath, data);
3842
4090
  return;
3843
4091
  }
@@ -3886,11 +4134,11 @@ var FileDatabase = class _FileDatabase {
3886
4134
  const forceNewFile = hasCustomMetadata && targetFileIndex === null;
3887
4135
  let { dataToWrite, dataLeftOver, fileName } = this.figureOutDataAndFileToWrite(data, targetFileIndex, forceNewFile);
3888
4136
  const destPath = this.getDestinationPath(this.currentVersion || void 0);
3889
- await this.safeWrite(path3.join(destPath, fileName), dataToWrite);
4137
+ await this.safeWrite(path4.join(destPath, fileName), dataToWrite);
3890
4138
  this.updateMetadata(dataToWrite, fileName, options.customMetadata);
3891
4139
  while (dataLeftOver && dataLeftOver.length > 0 && targetFileIndex === null) {
3892
4140
  const writeContext = this.figureOutDataAndFileToWrite(dataLeftOver);
3893
- await this.safeWrite(path3.join(destPath, writeContext.fileName), writeContext.dataToWrite);
4141
+ await this.safeWrite(path4.join(destPath, writeContext.fileName), writeContext.dataToWrite);
3894
4142
  this.updateMetadata(writeContext.dataToWrite, writeContext.fileName, options.customMetadata);
3895
4143
  dataLeftOver = writeContext.dataLeftOver;
3896
4144
  }
@@ -3906,7 +4154,7 @@ var FileDatabase = class _FileDatabase {
3906
4154
  const { version, nextPage = false, pageSize, filename } = options;
3907
4155
  if (filename) {
3908
4156
  const destPath = this.getDestinationPath(version);
3909
- const filePath = path3.join(destPath, filename);
4157
+ const filePath = path4.join(destPath, filename);
3910
4158
  try {
3911
4159
  const rawData = await fs3.promises.readFile(filePath, "utf8");
3912
4160
  return JSON.parse(rawData);
@@ -3918,7 +4166,7 @@ var FileDatabase = class _FileDatabase {
3918
4166
  const isNonPaginatedData = this.metadata.dataType === "text" || this.metadata.dataType === "xml" || this.metadata.dataType === "json-object";
3919
4167
  if (isNonPaginatedData) {
3920
4168
  const file = this.metadata.files[0];
3921
- const filePath = path3.join(this.getDestinationPath(this.currentVersion || void 0), file.fileName);
4169
+ const filePath = path4.join(this.getDestinationPath(this.currentVersion || void 0), file.fileName);
3922
4170
  try {
3923
4171
  const rawData = await fs3.promises.readFile(filePath, "utf8");
3924
4172
  return deserializeData(rawData, this.metadata.dataType);
@@ -3956,7 +4204,7 @@ var FileDatabase = class _FileDatabase {
3956
4204
  let cumulativeRecords = currentFileOffset;
3957
4205
  for (let i = currentFileIndex; i < this.metadata.files.length && recordsRead < effectivePageSize; i++) {
3958
4206
  const file = this.metadata.files[i];
3959
- const filePath = path3.join(this.getDestinationPath(this.currentVersion || void 0), file.fileName);
4207
+ const filePath = path4.join(this.getDestinationPath(this.currentVersion || void 0), file.fileName);
3960
4208
  try {
3961
4209
  const rawData = await fs3.promises.readFile(filePath, "utf8");
3962
4210
  const fileData = deserializeData(rawData, this.metadata.dataType);
@@ -4000,7 +4248,7 @@ var FileDatabase = class _FileDatabase {
4000
4248
  * Returns data file names (.json, .txt, .xml) excluding metadata.json.
4001
4249
  */
4002
4250
  async listFilenames() {
4003
- const destPath = this.versioned && this.currentVersion ? path3.join(this.getDestinationPath(), this.currentVersion) : this.getDestinationPath();
4251
+ const destPath = this.versioned && this.currentVersion ? path4.join(this.getDestinationPath(), this.currentVersion) : this.getDestinationPath();
4004
4252
  try {
4005
4253
  const entries = await fs3.promises.readdir(destPath, { withFileTypes: true });
4006
4254
  return entries.filter((e) => e.isFile() && e.name !== "metadata.json" && /\.(json|txt|xml)$/i.test(e.name)).map((e) => e.name);
@@ -4014,8 +4262,8 @@ var FileDatabase = class _FileDatabase {
4014
4262
  * Use with listFilenames() to manage individual files.
4015
4263
  */
4016
4264
  async removeFile(filename) {
4017
- const destPath = this.versioned && this.currentVersion ? path3.join(this.getDestinationPath(), this.currentVersion) : this.getDestinationPath();
4018
- const filePath = path3.join(destPath, filename);
4265
+ const destPath = this.versioned && this.currentVersion ? path4.join(this.getDestinationPath(), this.currentVersion) : this.getDestinationPath();
4266
+ const filePath = path4.join(destPath, filename);
4019
4267
  try {
4020
4268
  await fs3.promises.unlink(filePath);
4021
4269
  } catch (err) {
@@ -4041,7 +4289,7 @@ var FileDatabase = class _FileDatabase {
4041
4289
  this.metadata.files.splice(idx, 1);
4042
4290
  this.metadata.totalRecords = Math.max(0, (this.metadata.totalRecords || 0) - recordsCount);
4043
4291
  const destPath = this.getDestinationPath();
4044
- const filePath = path3.join(destPath, filename);
4292
+ const filePath = path4.join(destPath, filename);
4045
4293
  try {
4046
4294
  await fs3.promises.unlink(filePath);
4047
4295
  } catch (err) {
@@ -4097,7 +4345,7 @@ var FileDatabase = class _FileDatabase {
4097
4345
  });
4098
4346
  if (matches) {
4099
4347
  const destPath = this.getDestinationPath();
4100
- const filePath = path3.join(destPath, fileEntry.fileName);
4348
+ const filePath = path4.join(destPath, fileEntry.fileName);
4101
4349
  const fileData = await fs3.promises.readFile(filePath, "utf8");
4102
4350
  const data = deserializeData(fileData, metadata.dataType || "json-object");
4103
4351
  results.push({
@@ -4120,7 +4368,7 @@ var FileDatabase = class _FileDatabase {
4120
4368
  });
4121
4369
  if (matches) {
4122
4370
  const destPath = this.getDestinationPath(version);
4123
- const filePath = path3.join(destPath, fileEntry.fileName);
4371
+ const filePath = path4.join(destPath, fileEntry.fileName);
4124
4372
  const fileData = await fs3.promises.readFile(filePath, "utf8");
4125
4373
  const data = deserializeData(fileData, metadata.dataType || "json-object");
4126
4374
  results.push({
@@ -4480,7 +4728,7 @@ var TaskShellCommand = class extends TaskMaster {
4480
4728
  };
4481
4729
 
4482
4730
  // src/tasks/coreTasks/TaskSystemInfo.ts
4483
- import os from "os";
4731
+ import os2 from "os";
4484
4732
  import fs4 from "fs/promises";
4485
4733
  function toGb(valueBytes) {
4486
4734
  return `${(valueBytes / 1024 ** 3).toFixed(2)} GB`;
@@ -4502,10 +4750,10 @@ async function getDiskStats() {
4502
4750
  var TaskSystemInfo = class extends TaskMaster {
4503
4751
  async run() {
4504
4752
  try {
4505
- const totalMemory = os.totalmem();
4506
- const freeMemory = os.freemem();
4753
+ const totalMemory = os2.totalmem();
4754
+ const freeMemory = os2.freemem();
4507
4755
  const usedMemory = totalMemory - freeMemory;
4508
- const cpus = os.cpus();
4756
+ const cpus = os2.cpus();
4509
4757
  const cpuUtilization = cpus.map((cpu) => {
4510
4758
  const total = Object.values(cpu.times).reduce((acc, time) => acc + time, 0);
4511
4759
  const usage = (total - cpu.times.idle) / total * 100;
@@ -4531,10 +4779,10 @@ var TaskSystemInfo = class extends TaskMaster {
4531
4779
  utilization: cpuUtilization
4532
4780
  },
4533
4781
  runtime: {
4534
- platform: os.platform(),
4535
- arch: os.arch(),
4536
- uptimeSec: os.uptime(),
4537
- hostname: os.hostname()
4782
+ platform: os2.platform(),
4783
+ arch: os2.arch(),
4784
+ uptimeSec: os2.uptime(),
4785
+ hostname: os2.hostname()
4538
4786
  }
4539
4787
  };
4540
4788
  this.context.logger.info?.(`[TaskSystemInfo] collected system metrics (${this.task.id})`);
@@ -4637,7 +4885,7 @@ import { spawn as spawn2 } from "child_process";
4637
4885
  // src/tasks/index.ts
4638
4886
  var LOCKED_BY_ERROR_MESSAGE = "locked by error";
4639
4887
  var defaultTasksRegistry = TasksRegistry.withCoreTasks();
4640
- function getDb2(context) {
4888
+ function getDb3(context) {
4641
4889
  const db = context.db;
4642
4890
  if (!db) {
4643
4891
  throw new Error("Tasks component requires context.db. Initialize DB first and attach to context.");
@@ -4672,7 +4920,7 @@ async function signalRunningTasksStop(context, runningTaskInstances, allowanceMs
4672
4920
  context.emitter.emit("stop", allowanceMs);
4673
4921
  }
4674
4922
  async function executeClaimedTask(context, tasksTable, historyTable, row, registry, runningTaskInstances) {
4675
- const db = getDb2(context);
4923
+ const db = getDb3(context);
4676
4924
  const taskName = row.task;
4677
4925
  const TaskClass = registry.get(taskName);
4678
4926
  const { paused_at: _pausedAt, ...rowForHistory } = row;
@@ -4773,7 +5021,7 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
4773
5021
  return { stopRunnerRequested, stopAllowanceMs };
4774
5022
  }
4775
5023
  async function claimNextRunnableTask(context, tasksTable, target, registry, scanLimit, taskNames) {
4776
- const db = getDb2(context);
5024
+ const db = getDb3(context);
4777
5025
  let query = db(tasksTable).whereNull("started_at").whereNull("paused_at").where({ target }).orderByRaw("CASE WHEN past_due IS NULL THEN 1 ELSE 0 END ASC").orderBy([{ column: "priority", order: "desc" }]).orderByRaw("CASE WHEN completed_at IS NULL THEN 0 ELSE 1 END ASC").orderBy([{ column: "completed_at", order: "asc" }, { column: "created_at", order: "asc" }]).limit(scanLimit);
4778
5026
  if (taskNames && taskNames.length > 0) {
4779
5027
  query = query.whereIn("task", taskNames);
@@ -4819,25 +5067,76 @@ async function runTasksLoop(context, options) {
4819
5067
  let stopRequested = false;
4820
5068
  let stopAllowanceMs = 5e3;
4821
5069
  context.__tasksRunnerStop = false;
4822
- while (!context.isStop() && !stopRequested && !context.__tasksRunnerStop) {
4823
- if (!runningStopControlPromise) {
4824
- const claimedStopTask = await claimNextRunnableTask(
4825
- context,
4826
- tasksTable,
4827
- target,
4828
- registry,
4829
- 10,
4830
- ["stopRunner", "stop"]
4831
- );
4832
- if (claimedStopTask) {
4833
- runningStopControlPromise = executeClaimedTask(
5070
+ let registryReg = null;
5071
+ let registryInterval = null;
5072
+ const hbGroup = options.runnerServiceGroup?.trim();
5073
+ if (hbGroup) {
5074
+ const identityDir = options.runnerIdentityDir ?? "./data/runner-identities";
5075
+ const hbIntervalMs = options.runnerHeartbeatIntervalMs ?? 1e4;
5076
+ const staleMs = options.runnerHeartbeatStaleMs ?? 45e3;
5077
+ const defaultMeta = {
5078
+ component: "tasks-runner",
5079
+ allowedTasks: allowedTasks?.length ? allowedTasks.join(",") : "all"
5080
+ };
5081
+ registryReg = await registerInServicesRegistry(context, {
5082
+ queue,
5083
+ target,
5084
+ serviceGroup: hbGroup,
5085
+ serviceName: options.runnerServiceName,
5086
+ identityDir,
5087
+ staleMs,
5088
+ groupMaxInstances: options.runnerGroupMaxInstances,
5089
+ enforceMaxInstances: options.runnerEnforceMaxInstances ?? true,
5090
+ metadata: options.runnerMetadata ?? defaultMeta
5091
+ });
5092
+ registryInterval = setInterval(() => {
5093
+ void touchServicesRegistry(context, registryReg).catch((err) => {
5094
+ context.logger.warn?.(`[services-registry] touch failed: ${err?.message ?? String(err)}`);
5095
+ });
5096
+ }, hbIntervalMs);
5097
+ }
5098
+ try {
5099
+ while (!context.isStop() && !stopRequested && !context.__tasksRunnerStop) {
5100
+ if (!runningStopControlPromise) {
5101
+ const claimedStopTask = await claimNextRunnableTask(
5102
+ context,
5103
+ tasksTable,
5104
+ target,
5105
+ registry,
5106
+ 10,
5107
+ ["stopRunner", "stop"]
5108
+ );
5109
+ if (claimedStopTask) {
5110
+ runningStopControlPromise = executeClaimedTask(
5111
+ context,
5112
+ tasksTable,
5113
+ historyTable,
5114
+ claimedStopTask,
5115
+ registry,
5116
+ runningTaskInstances
5117
+ ).then(async (outcome) => {
5118
+ if (outcome.stopRunnerRequested && !stopRequested) {
5119
+ stopRequested = true;
5120
+ stopAllowanceMs = outcome.stopAllowanceMs || 5e3;
5121
+ context.__tasksRunnerStop = true;
5122
+ await signalRunningTasksStop(context, runningTaskInstances, stopAllowanceMs);
5123
+ }
5124
+ }).finally(() => {
5125
+ runningStopControlPromise = null;
5126
+ });
5127
+ }
5128
+ }
5129
+ while (runningPromises.size < maxParallel) {
5130
+ const claimed = await claimNextRunnableTask(
4834
5131
  context,
4835
5132
  tasksTable,
4836
- historyTable,
4837
- claimedStopTask,
5133
+ target,
4838
5134
  registry,
4839
- runningTaskInstances
4840
- ).then(async (outcome) => {
5135
+ scanLimit,
5136
+ allowedTasks
5137
+ );
5138
+ if (!claimed) break;
5139
+ const p = executeClaimedTask(context, tasksTable, historyTable, claimed, registry, runningTaskInstances).then(async (outcome) => {
4841
5140
  if (outcome.stopRunnerRequested && !stopRequested) {
4842
5141
  stopRequested = true;
4843
5142
  stopAllowanceMs = outcome.stopAllowanceMs || 5e3;
@@ -4845,49 +5144,41 @@ async function runTasksLoop(context, options) {
4845
5144
  await signalRunningTasksStop(context, runningTaskInstances, stopAllowanceMs);
4846
5145
  }
4847
5146
  }).finally(() => {
4848
- runningStopControlPromise = null;
5147
+ runningPromises.delete(p);
4849
5148
  });
5149
+ runningPromises.add(p);
5150
+ }
5151
+ await sleepMs(pollMs);
5152
+ }
5153
+ if (context.isStop() && !stopRequested) {
5154
+ await signalRunningTasksStop(context, runningTaskInstances, 5e3);
5155
+ }
5156
+ if (runningPromises.size > 0) {
5157
+ if (stopRequested) {
5158
+ await Promise.race([
5159
+ Promise.allSettled(Array.from(runningPromises)),
5160
+ sleepMs(stopAllowanceMs).then(() => {
5161
+ context.logger.warn?.(
5162
+ `[tasks] stop allowance (${stopAllowanceMs}ms) reached; ${runningPromises.size} task(s) still running`
5163
+ );
5164
+ })
5165
+ ]);
5166
+ } else {
5167
+ await Promise.allSettled(Array.from(runningPromises));
4850
5168
  }
4851
5169
  }
4852
- while (runningPromises.size < maxParallel) {
4853
- const claimed = await claimNextRunnableTask(
4854
- context,
4855
- tasksTable,
4856
- target,
4857
- registry,
4858
- scanLimit,
4859
- allowedTasks
4860
- );
4861
- if (!claimed) break;
4862
- const p = executeClaimedTask(context, tasksTable, historyTable, claimed, registry, runningTaskInstances).then(async (outcome) => {
4863
- if (outcome.stopRunnerRequested && !stopRequested) {
4864
- stopRequested = true;
4865
- stopAllowanceMs = outcome.stopAllowanceMs || 5e3;
4866
- context.__tasksRunnerStop = true;
4867
- await signalRunningTasksStop(context, runningTaskInstances, stopAllowanceMs);
4868
- }
4869
- }).finally(() => {
4870
- runningPromises.delete(p);
4871
- });
4872
- runningPromises.add(p);
5170
+ } finally {
5171
+ if (registryInterval) {
5172
+ clearInterval(registryInterval);
5173
+ registryInterval = null;
4873
5174
  }
4874
- await sleepMs(pollMs);
4875
- }
4876
- if (context.isStop() && !stopRequested) {
4877
- await signalRunningTasksStop(context, runningTaskInstances, 5e3);
4878
- }
4879
- if (runningPromises.size > 0) {
4880
- if (stopRequested) {
4881
- await Promise.race([
4882
- Promise.allSettled(Array.from(runningPromises)),
4883
- sleepMs(stopAllowanceMs).then(() => {
4884
- context.logger.warn?.(
4885
- `[tasks] stop allowance (${stopAllowanceMs}ms) reached; ${runningPromises.size} task(s) still running`
4886
- );
4887
- })
4888
- ]);
4889
- } else {
4890
- await Promise.allSettled(Array.from(runningPromises));
5175
+ if (registryReg) {
5176
+ await unregisterServicesRegistry(context, registryReg).catch((err) => {
5177
+ context.logger.warn?.(`[services-registry] unregister failed: ${err?.message ?? String(err)}`);
5178
+ });
5179
+ registryReg = null;
5180
+ delete context.servicesRegistry;
5181
+ delete context.runnerHeartbeat;
4891
5182
  }
4892
5183
  }
4893
5184
  }
@@ -4901,6 +5192,14 @@ var TasksManager = class _TasksManager {
4901
5192
  scanLimit;
4902
5193
  allowedTasks;
4903
5194
  registry;
5195
+ runnerServiceGroup;
5196
+ runnerServiceName;
5197
+ runnerIdentityDir;
5198
+ runnerHeartbeatIntervalMs;
5199
+ runnerHeartbeatStaleMs;
5200
+ runnerGroupMaxInstances;
5201
+ runnerEnforceMaxInstances;
5202
+ runnerMetadata;
4904
5203
  constructor(context, options = {}) {
4905
5204
  this.context = context;
4906
5205
  this.queue = options.queue ?? "tasks";
@@ -4911,6 +5210,14 @@ var TasksManager = class _TasksManager {
4911
5210
  this.scanLimit = options.scanLimit ?? 100;
4912
5211
  this.allowedTasks = normalizeAllowedTasks(options.allowedTasks);
4913
5212
  this.registry = normalizeRegistry(options.registry);
5213
+ this.runnerServiceGroup = options.runnerServiceGroup;
5214
+ this.runnerServiceName = options.runnerServiceName;
5215
+ this.runnerIdentityDir = options.runnerIdentityDir;
5216
+ this.runnerHeartbeatIntervalMs = options.runnerHeartbeatIntervalMs;
5217
+ this.runnerHeartbeatStaleMs = options.runnerHeartbeatStaleMs;
5218
+ this.runnerGroupMaxInstances = options.runnerGroupMaxInstances;
5219
+ this.runnerEnforceMaxInstances = options.runnerEnforceMaxInstances;
5220
+ this.runnerMetadata = options.runnerMetadata;
4914
5221
  }
4915
5222
  static init(context, options = {}) {
4916
5223
  const defs2 = {
@@ -4920,7 +5227,14 @@ var TasksManager = class _TasksManager {
4920
5227
  pollMs: "number default 1000",
4921
5228
  maxParallel: "number default 1",
4922
5229
  scanLimit: "number default 100",
4923
- allowedTasks: "string"
5230
+ allowedTasks: "string",
5231
+ runnerServiceGroup: "string",
5232
+ runnerServiceName: "string",
5233
+ runnerIdentityDir: "string default ./data/runner-identities",
5234
+ runnerHeartbeatIntervalMs: "number default 10000",
5235
+ runnerHeartbeatStaleMs: "number default 45000",
5236
+ runnerGroupMaxInstances: "number",
5237
+ runnerEnforceMaxInstances: "boolean default true"
4924
5238
  };
4925
5239
  const discovered = context.params.getAllForModule(defs2);
4926
5240
  const resolved = {
@@ -4931,6 +5245,13 @@ var TasksManager = class _TasksManager {
4931
5245
  maxParallel: discovered.maxParallel,
4932
5246
  scanLimit: discovered.scanLimit,
4933
5247
  allowedTasks: discovered.allowedTasks,
5248
+ runnerServiceGroup: discovered.runnerServiceGroup,
5249
+ runnerServiceName: discovered.runnerServiceName,
5250
+ runnerIdentityDir: discovered.runnerIdentityDir,
5251
+ runnerHeartbeatIntervalMs: discovered.runnerHeartbeatIntervalMs,
5252
+ runnerHeartbeatStaleMs: discovered.runnerHeartbeatStaleMs,
5253
+ runnerGroupMaxInstances: discovered.runnerGroupMaxInstances,
5254
+ runnerEnforceMaxInstances: discovered.runnerEnforceMaxInstances,
4934
5255
  ...options
4935
5256
  };
4936
5257
  return new _TasksManager(context, resolved);
@@ -4949,7 +5270,15 @@ var TasksManager = class _TasksManager {
4949
5270
  maxParallel: options.maxParallel ?? this.maxParallel,
4950
5271
  scanLimit: options.scanLimit ?? this.scanLimit,
4951
5272
  allowedTasks: options.allowedTasks ?? this.allowedTasks,
4952
- registry: options.registry ?? this.registry
5273
+ registry: options.registry ?? this.registry,
5274
+ runnerServiceGroup: options.runnerServiceGroup ?? this.runnerServiceGroup,
5275
+ runnerServiceName: options.runnerServiceName ?? this.runnerServiceName,
5276
+ runnerIdentityDir: options.runnerIdentityDir ?? this.runnerIdentityDir,
5277
+ runnerHeartbeatIntervalMs: options.runnerHeartbeatIntervalMs ?? this.runnerHeartbeatIntervalMs,
5278
+ runnerHeartbeatStaleMs: options.runnerHeartbeatStaleMs ?? this.runnerHeartbeatStaleMs,
5279
+ runnerGroupMaxInstances: options.runnerGroupMaxInstances ?? this.runnerGroupMaxInstances,
5280
+ runnerEnforceMaxInstances: options.runnerEnforceMaxInstances ?? this.runnerEnforceMaxInstances,
5281
+ runnerMetadata: options.runnerMetadata ?? this.runnerMetadata
4953
5282
  });
4954
5283
  }
4955
5284
  };
@@ -4960,7 +5289,7 @@ var defs = {
4960
5289
  tasksModule: "string"
4961
5290
  };
4962
5291
  async function loadTasksModule(modulePath) {
4963
- const absolute = path4.isAbsolute(modulePath) ? modulePath : path4.resolve(process.cwd(), modulePath);
5292
+ const absolute = path5.isAbsolute(modulePath) ? modulePath : path5.resolve(process.cwd(), modulePath);
4964
5293
  const imported = await import(pathToFileURL(absolute).href);
4965
5294
  if (!imported.tasksRegistry || typeof imported.tasksRegistry !== "object") {
4966
5295
  throw new Error(`tasksModule "${modulePath}" must export "tasksRegistry" object`);