@nmakarov/cli-toolkit 0.18.0 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/README.md +9 -0
  2. package/dist/args.cjs +1 -4
  3. package/dist/args.cjs.map +1 -1
  4. package/dist/args.js +1 -1
  5. package/dist/args.js.map +1 -1
  6. package/dist/cli-runner.cjs +1493 -516
  7. package/dist/cli-runner.cjs.map +1 -1
  8. package/dist/cli-runner.js +1509 -531
  9. package/dist/cli-runner.js.map +1 -1
  10. package/dist/db.cjs +85 -157
  11. package/dist/db.cjs.map +1 -1
  12. package/dist/db.js +84 -150
  13. package/dist/db.js.map +1 -1
  14. package/dist/errors.cjs +2 -2
  15. package/dist/errors.cjs.map +1 -1
  16. package/dist/errors.js +2 -1
  17. package/dist/errors.js.map +1 -1
  18. package/dist/filedatabase.cjs +19 -19
  19. package/dist/filedatabase.cjs.map +1 -1
  20. package/dist/filedatabase.js +19 -16
  21. package/dist/filedatabase.js.map +1 -1
  22. package/dist/http-client.cjs +9 -11
  23. package/dist/http-client.cjs.map +1 -1
  24. package/dist/http-client.js +10 -9
  25. package/dist/http-client.js.map +1 -1
  26. package/dist/http-client2.cjs +34 -33
  27. package/dist/http-client2.cjs.map +1 -1
  28. package/dist/http-client2.js +34 -30
  29. package/dist/http-client2.js.map +1 -1
  30. package/dist/index.cjs +2063 -658
  31. package/dist/index.cjs.map +1 -1
  32. package/dist/index.js +2063 -663
  33. package/dist/index.js.map +1 -1
  34. package/dist/init.cjs +97 -69
  35. package/dist/init.cjs.map +1 -1
  36. package/dist/init.js +112 -83
  37. package/dist/init.js.map +1 -1
  38. package/dist/logger.cjs +5 -5
  39. package/dist/logger.cjs.map +1 -1
  40. package/dist/logger.js +5 -4
  41. package/dist/logger.js.map +1 -1
  42. package/dist/mock-server.cjs +21 -33
  43. package/dist/mock-server.cjs.map +1 -1
  44. package/dist/mock-server.js +21 -28
  45. package/dist/mock-server.js.map +1 -1
  46. package/dist/params.cjs +22 -10
  47. package/dist/params.cjs.map +1 -1
  48. package/dist/params.js +22 -7
  49. package/dist/params.js.map +1 -1
  50. package/dist/s3.cjs +286 -0
  51. package/dist/s3.cjs.map +1 -0
  52. package/dist/s3.js +273 -0
  53. package/dist/s3.js.map +1 -0
  54. package/dist/screen.cjs +34 -39
  55. package/dist/screen.cjs.map +1 -1
  56. package/dist/screen.js +48 -46
  57. package/dist/screen.js.map +1 -1
  58. package/dist/tasks.cjs +1640 -416
  59. package/dist/tasks.cjs.map +1 -1
  60. package/dist/tasks.js +1614 -412
  61. package/dist/tasks.js.map +1 -1
  62. package/dist/utils.cjs +7 -8
  63. package/dist/utils.cjs.map +1 -1
  64. package/dist/utils.js +6 -6
  65. package/dist/utils.js.map +1 -1
  66. package/package.json +36 -44
  67. package/scripts/ssm/parse-cli.js +35 -0
  68. package/scripts/ssm/ssm-admin.js +151 -0
  69. package/scripts/ssm/ssm-pull.js +147 -0
package/dist/tasks.cjs CHANGED
@@ -1,4 +1,3 @@
1
- "use strict";
2
1
  var __create = Object.create;
3
2
  var __defProp = Object.defineProperty;
4
3
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
@@ -27,10 +26,12 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
27
26
  ));
28
27
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
28
 
30
- // src/tasks.ts
29
+ // src/tasks/index.js
31
30
  var tasks_exports = {};
32
31
  __export(tasks_exports, {
33
- TaskMaster: () => TaskMaster,
32
+ AbstractTask: () => AbstractTask,
33
+ SERVICE_TASK_NAMES: () => SERVICE_TASK_NAMES,
34
+ TaskGetLogs: () => TaskGetLogs,
34
35
  TaskPing: () => TaskPing,
35
36
  TaskSampleProcess: () => TaskSampleProcess,
36
37
  TaskShellCommand: () => TaskShellCommand,
@@ -40,19 +41,43 @@ __export(tasks_exports, {
40
41
  TasksManager: () => TasksManager,
41
42
  TasksRegistry: () => TasksRegistry,
42
43
  appendTaskIpcLog: () => appendTaskIpcLog,
44
+ convertPattern: () => convertPattern,
43
45
  defaultTasksRegistry: () => defaultTasksRegistry,
44
46
  enqueueStopTask: () => enqueueStopTask,
45
47
  enqueueTask: () => enqueueTask,
46
48
  ensureTaskTables: () => ensureTaskTables,
49
+ flushTaskIpcLogs: () => flushTaskIpcLogs,
50
+ ipcFileLogsTableNameForSourceResource: () => ipcFileLogsTableNameForSourceResource,
51
+ listAliveRunnerHeartbeats: () => listServicesRegistry,
52
+ listServicesRegistry: () => listServicesRegistry,
53
+ matchesParsedPattern: () => matchesParsedPattern,
54
+ mergeAllowedTasksWithServiceTasks: () => mergeAllowedTasksWithServiceTasks,
55
+ nextTimeMatch: () => nextTimeMatch,
56
+ normalizeAllowedTasks: () => normalizeAllowedTasks,
47
57
  queueToTableNames: () => queueToTableNames,
58
+ readTaskIpcLogsSnapshot: () => readTaskIpcLogsSnapshot,
59
+ registerInServicesRegistry: () => registerInServicesRegistry,
60
+ registerRunnerHeartbeat: () => registerInServicesRegistry,
61
+ resolveAsterisks: () => resolveAsterisks,
62
+ resolveIpcFileLogsDir: () => resolveIpcFileLogsDir,
63
+ resolveRanges: () => resolveRanges,
64
+ resolveSteps: () => resolveSteps,
48
65
  runNodeTaskScript: () => runNodeTaskScript,
49
66
  runTasksLoop: () => runTasksLoop,
67
+ taskHistoryInsertFromQueueRow: () => taskHistoryInsertFromQueueRow,
68
+ timeMatcher: () => timeMatcher,
69
+ touchRunnerHeartbeat: () => touchServicesRegistry,
70
+ touchServicesRegistry: () => touchServicesRegistry,
71
+ unregisterRunnerHeartbeat: () => unregisterServicesRegistry,
72
+ unregisterServicesRegistry: () => unregisterServicesRegistry,
73
+ updateServicesRegistryMetadata: () => updateServicesRegistryMetadata,
50
74
  updateTaskProgress: () => updateTaskProgress,
51
75
  waitForTaskResult: () => waitForTaskResult
52
76
  });
53
77
  module.exports = __toCommonJS(tasks_exports);
78
+ var import_node_os3 = __toESM(require("os"), 1);
54
79
 
55
- // src/utils/date-utils.ts
80
+ // src/utils/date-utils.js
56
81
  function isTimestampFolder(folderName) {
57
82
  const isoRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(Z|\.\d{3}Z)$/;
58
83
  if (!isoRegex.test(folderName)) {
@@ -62,7 +87,7 @@ function isTimestampFolder(folderName) {
62
87
  return !isNaN(date.getTime()) && date.getTime() > 0;
63
88
  }
64
89
 
65
- // src/utils/fs-utils.ts
90
+ // src/utils/fs-utils.js
66
91
  var import_fs = __toESM(require("fs"), 1);
67
92
  var import_path = __toESM(require("path"), 1);
68
93
  async function ensurePath(...pathParts) {
@@ -86,7 +111,7 @@ function getFileExtension(dataType) {
86
111
  }
87
112
  }
88
113
 
89
- // src/utils/os-utils.ts
114
+ // src/utils/os-utils.js
90
115
  var import_fs2 = __toESM(require("fs"), 1);
91
116
  var import_path2 = __toESM(require("path"), 1);
92
117
  var import_child_process = require("child_process");
@@ -115,7 +140,7 @@ function getFreeDiskSpace(targetPath) {
115
140
  }
116
141
  }
117
142
 
118
- // src/utils/format-utils.ts
143
+ // src/utils/format-utils.js
119
144
  function bytesToHumanReadable(bytes) {
120
145
  if (bytes === 0) return "0 B";
121
146
  const k = 1024;
@@ -124,7 +149,7 @@ function bytesToHumanReadable(bytes) {
124
149
  return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
125
150
  }
126
151
 
127
- // src/utils/core-utils.ts
152
+ // src/utils/core-utils.js
128
153
  function sleepMs(ms) {
129
154
  return new Promise((resolve) => setTimeout(resolve, ms));
130
155
  }
@@ -133,8 +158,82 @@ function toJsonColumn(value) {
133
158
  return JSON.stringify(value);
134
159
  }
135
160
 
136
- // src/tasks/taskUtils.ts
161
+ // src/tasks/servicesRegistry.js
162
+ var import_node_os = __toESM(require("os"), 1);
163
+
164
+ // src/tasks/taskUtils.js
137
165
  var import_node_crypto = require("crypto");
166
+
167
+ // src/tasks/time-matcher.js
168
+ var RANGES = ["0-59", "0-59", "0-23", "1-31", "1-12", "0-6"];
169
+ function resolveAsterisks(field, range) {
170
+ return field.includes("*") ? field.replace("*", range) : field;
171
+ }
172
+ function resolveRanges(field) {
173
+ const regex = /(\d+)-(\d+)/;
174
+ let current = field;
175
+ while (true) {
176
+ const match = regex.exec(current);
177
+ if (!match) break;
178
+ const raw = match[0];
179
+ let first = Number(match[1]);
180
+ let last = Number(match[2]);
181
+ if (last < first) {
182
+ [first, last] = [last, first];
183
+ }
184
+ const values = [];
185
+ for (let i = first; i <= last; i += 1) {
186
+ values.push(i);
187
+ }
188
+ current = current.replace(raw, values.join(","));
189
+ }
190
+ return current;
191
+ }
192
+ function resolveSteps(field) {
193
+ const match = /^(.+)\/(\d+)$/.exec(field);
194
+ if (!match) return field;
195
+ const base = match[1];
196
+ const step = Number(match[2]);
197
+ if (!Number.isFinite(step) || step <= 0) return field;
198
+ return base.split(",").map((v) => Number(v)).filter((v) => Number.isFinite(v) && v % step === 0).join(",");
199
+ }
200
+ function convertPattern(pattern) {
201
+ const parts = pattern.trim().split(/\s+/);
202
+ if (parts.length !== 6) {
203
+ throw new Error(`Invalid schedule "${pattern}". Expected 6 fields: sec min hour day month weekday`);
204
+ }
205
+ return parts.map((field, idx) => resolveAsterisks(field, RANGES[idx])).map((field) => resolveRanges(field)).map((field) => resolveSteps(field));
206
+ }
207
+ function fieldMatches(field, value) {
208
+ const allowed = field.split(",").map((v) => Number(v));
209
+ return allowed.includes(value);
210
+ }
211
+ function matchesParsedPattern(parsed, date) {
212
+ return fieldMatches(parsed[0], date.getSeconds()) && fieldMatches(parsed[1], date.getMinutes()) && fieldMatches(parsed[2], date.getHours()) && fieldMatches(parsed[3], date.getDate()) && fieldMatches(parsed[4], date.getMonth() + 1) && fieldMatches(parsed[5], date.getDay());
213
+ }
214
+ function timeMatcher(pattern, date = /* @__PURE__ */ new Date()) {
215
+ const parsed = convertPattern(pattern);
216
+ return matchesParsedPattern(parsed, date);
217
+ }
218
+ var MS_PER_SECOND = 1e3;
219
+ var DEFAULT_SEARCH_HORIZON_MS = 10 * 365 * 24 * 60 * 60 * MS_PER_SECOND;
220
+ function nextTimeMatch(pattern, from = /* @__PURE__ */ new Date(), maxSearchMs = DEFAULT_SEARCH_HORIZON_MS) {
221
+ const parsed = convertPattern(pattern);
222
+ let t = Math.ceil((from.getTime() + 1) / MS_PER_SECOND) * MS_PER_SECOND;
223
+ const end = t + maxSearchMs;
224
+ while (t <= end) {
225
+ const date = new Date(t);
226
+ if (matchesParsedPattern(parsed, date)) {
227
+ return date;
228
+ }
229
+ t += MS_PER_SECOND;
230
+ }
231
+ throw new Error(
232
+ `nextTimeMatch: no match for "${pattern}" within ${maxSearchMs}ms after ${from.toISOString()}`
233
+ );
234
+ }
235
+
236
+ // src/tasks/taskUtils.js
138
237
  function getDb(context) {
139
238
  const db = context.db;
140
239
  if (!db) {
@@ -142,105 +241,117 @@ function getDb(context) {
142
241
  }
143
242
  return db;
144
243
  }
145
- function queueToTableNames(queue) {
146
- if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(queue)) {
147
- throw new Error(`Invalid queue name "${queue}". Use letters, numbers, underscore only.`);
148
- }
244
+ function queueToTableNames(queueName) {
245
+ return {
246
+ tasksTable: queueName,
247
+ historyTable: `${queueName}_history`,
248
+ registryTable: `${queueName}_services_registry`
249
+ };
250
+ }
251
+ function defineTasksTable(t, db, tableNameForIndex) {
252
+ t.uuid("id").primary().defaultTo(db.raw("uuid_generate_v4()"));
253
+ t.timestamp("created_at").notNullable().defaultTo(db.fn.now());
254
+ t.timestamp("started_at");
255
+ t.timestamp("completed_at");
256
+ t.integer("priority").notNullable().defaultTo(50);
257
+ t.text("schedule");
258
+ t.timestamp("next_run_at").defaultTo(null);
259
+ t.timestamp("past_due").defaultTo(null);
260
+ t.text("name").notNullable();
261
+ t.text("opid");
262
+ t.json("params");
263
+ t.text("service_group");
264
+ t.integer("instance_number");
265
+ t.text("service_name");
266
+ t.text("server_name");
267
+ t.text("status").notNullable().defaultTo("idle");
268
+ t.timestamp("status_changed_at").defaultTo(null);
269
+ t.text("progress");
270
+ t.boolean("success");
271
+ t.json("results");
272
+ t.index(["service_group", "status", "priority", "created_at"], `${tableNameForIndex}_claim_idx`);
273
+ t.index(["service_group", "name"], `${tableNameForIndex}_group_name_idx`);
274
+ }
275
+ function taskHistoryInsertFromQueueRow(row, overrides) {
276
+ const { id, ...snapshot } = row;
277
+ void id;
149
278
  return {
150
- tasksTable: queue,
151
- historyTable: `${queue}_history`
279
+ ...snapshot,
280
+ ...overrides
152
281
  };
153
282
  }
154
283
  async function ensureTaskTables(context, options = {}) {
155
- const queue = options.queue ?? "tasks";
284
+ const queueName = options.queueName ?? "tasks";
156
285
  const recreate = options.recreate ?? false;
157
286
  const db = getDb(context);
158
- const { tasksTable, historyTable } = queueToTableNames(queue);
287
+ const { tasksTable, historyTable, registryTable } = queueToTableNames(queueName);
159
288
  const needsTasks = recreate ? true : !await db.tableExists(tasksTable);
160
289
  const needsHistory = recreate ? true : !await db.tableExists(historyTable);
290
+ const needsRegistry = recreate ? true : !await db.tableExists(registryTable);
161
291
  if (recreate) {
162
292
  await db.schema.dropTableIfExists(historyTable);
163
293
  await db.schema.dropTableIfExists(tasksTable);
294
+ await db.schema.dropTableIfExists(registryTable);
164
295
  }
165
296
  if (needsTasks) {
166
297
  await db.raw(`CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`);
167
298
  await db.schema.createTable(tasksTable, (t) => {
168
- t.uuid("id").primary().defaultTo(db.raw("uuid_generate_v4()"));
169
- t.timestamp("created_at").notNullable().defaultTo(db.fn.now());
170
- t.timestamp("started_at");
171
- t.timestamp("completed_at");
172
- t.integer("priority").notNullable().defaultTo(0);
173
- t.text("schedule");
174
- t.timestamp("past_due").defaultTo(null);
175
- t.text("target").notNullable();
176
- t.text("task").notNullable();
177
- t.json("params");
178
- t.text("opid");
179
- t.timestamp("paused_at").defaultTo(null);
180
- t.text("progress");
181
- t.boolean("success");
182
- t.json("results");
183
- });
184
- await db.schema.alterTable(tasksTable, (t) => {
185
- t.index(["target", "started_at", "created_at"], `${tasksTable}_target_started_created_idx`);
186
- t.index(["target", "past_due", "priority", "created_at"], `${tasksTable}_target_past_due_priority_created_idx`);
187
- t.index(["target", "task"], `${tasksTable}_target_task_idx`);
188
- });
189
- }
190
- const tasksHasOpid = await db.schema.hasColumn(tasksTable, "opid");
191
- if (!tasksHasOpid) {
192
- await db.schema.alterTable(tasksTable, (t) => {
193
- t.text("opid");
194
- });
195
- }
196
- const tasksHasPausedAt = await db.schema.hasColumn(tasksTable, "paused_at");
197
- if (!tasksHasPausedAt) {
198
- await db.schema.alterTable(tasksTable, (t) => {
199
- t.timestamp("paused_at").defaultTo(null);
299
+ defineTasksTable(t, db, tasksTable);
200
300
  });
201
301
  }
202
302
  if (needsHistory) {
203
303
  await db.schema.createTable(historyTable, (t) => {
204
- t.uuid("id").notNullable();
205
- t.timestamp("created_at").notNullable();
206
- t.timestamp("started_at");
207
- t.timestamp("completed_at");
208
- t.integer("priority").notNullable().defaultTo(0);
209
- t.text("schedule");
210
- t.timestamp("past_due").defaultTo(null);
211
- t.text("target").notNullable();
212
- t.text("task").notNullable();
213
- t.json("params");
214
- t.text("opid");
215
- t.text("progress");
216
- t.boolean("success");
217
- t.json("results");
218
- });
219
- await db.schema.alterTable(historyTable, (t) => {
220
- t.index(["target", "created_at"], `${historyTable}_target_created_idx`);
221
- t.index(["task", "created_at"], `${historyTable}_task_created_idx`);
304
+ defineTasksTable(t, db, historyTable);
222
305
  });
223
306
  }
224
- const historyHasOpid = await db.schema.hasColumn(historyTable, "opid");
225
- if (!historyHasOpid) {
226
- await db.schema.alterTable(historyTable, (t) => {
227
- t.text("opid");
307
+ if (needsRegistry) {
308
+ await db.schema.createTable(registryTable, (t) => {
309
+ t.uuid("id").primary().defaultTo(db.raw("uuid_generate_v4()"));
310
+ t.text("queue_name").notNullable();
311
+ t.text("service_group").notNullable();
312
+ t.integer("instance_number").notNullable().defaultTo(1);
313
+ t.text("service_name").notNullable();
314
+ t.text("server_name").notNullable();
315
+ t.integer("pid");
316
+ t.json("metadata");
317
+ t.timestamp("created_at").notNullable().defaultTo(db.fn.now());
318
+ t.timestamp("last_seen_at").notNullable().defaultTo(db.fn.now());
319
+ t.unique(["queue_name", "service_name"], `${registryTable}_queue_name_service_name_uniq`);
320
+ t.index(["queue_name", "service_group", "last_seen_at"], `${registryTable}_queue_group_seen_idx`);
321
+ t.index(["queue_name", "last_seen_at"], `${registryTable}_queue_seen_idx`);
228
322
  });
229
323
  }
230
324
  }
231
325
  async function enqueueTask(context, options) {
232
326
  const db = getDb(context);
233
- const queue = options.queue ?? "tasks";
234
- const { tasksTable } = queueToTableNames(queue);
327
+ const queueName = options.queueName ?? "tasks";
328
+ const { tasksTable } = queueToTableNames(queueName);
235
329
  const id = (0, import_node_crypto.randomUUID)();
330
+ const name = options.name ?? options.task;
331
+ if (!name) {
332
+ throw new Error("enqueueTask: name (or task) is required");
333
+ }
334
+ const schedule = options.schedule?.trim() ? options.schedule : null;
335
+ let nextRunAt = null;
336
+ if (options.nextRunAt !== void 0) {
337
+ nextRunAt = options.nextRunAt == null ? null : new Date(options.nextRunAt);
338
+ } else if (schedule) {
339
+ nextRunAt = nextTimeMatch(schedule, /* @__PURE__ */ new Date());
340
+ }
236
341
  await db(tasksTable).insert({
237
342
  id,
238
- target: options.target,
239
- task: options.task,
343
+ name,
240
344
  params: toJsonColumn(options.params ?? null),
241
345
  opid: options.opid ?? null,
242
- priority: options.priority ?? 0,
243
- schedule: options.schedule ?? null
346
+ priority: options.priority ?? 50,
347
+ schedule,
348
+ next_run_at: nextRunAt,
349
+ service_group: options.serviceGroup ?? null,
350
+ instance_number: options.instanceNumber ?? null,
351
+ service_name: options.serviceName ?? null,
352
+ server_name: options.serverName ?? null,
353
+ status: "idle",
354
+ status_changed_at: db.fn.now()
244
355
  });
245
356
  return id;
246
357
  }
@@ -251,11 +362,261 @@ async function updateTaskProgress(context, tasksTable, taskId, progress) {
251
362
  });
252
363
  }
253
364
 
254
- // src/filedatabase/index.ts
365
+ // src/tasks/servicesRegistry.js
366
+ function getDb2(context) {
367
+ const db = context.db;
368
+ if (!db) {
369
+ throw new Error("Services registry requires context.db");
370
+ }
371
+ return db;
372
+ }
373
+ function parseMetadataColumn(value) {
374
+ if (!value) return {};
375
+ if (typeof value === "object" && !Array.isArray(value)) return value;
376
+ if (typeof value === "string") {
377
+ try {
378
+ const p = JSON.parse(value);
379
+ return p && typeof p === "object" && !Array.isArray(value) ? p : {};
380
+ } catch {
381
+ return {};
382
+ }
383
+ }
384
+ return {};
385
+ }
386
+ var DEFAULT_GROUP_MAX_INSTANCES = {
387
+ intake: 1,
388
+ harvest: 1,
389
+ harvester: 0,
390
+ loader: 0,
391
+ photos: 0,
392
+ photosprocessor: 0,
393
+ ingest: 0
394
+ };
395
+ function sanitizeNamePart(raw) {
396
+ const s = String(raw || "").trim().replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
397
+ return s.slice(0, 80) || "runner";
398
+ }
399
+ function resolveMaxInstances(serviceGroup, override) {
400
+ if (override !== void 0 && Number.isFinite(override)) {
401
+ return Math.max(0, Math.floor(Number(override)));
402
+ }
403
+ const g = serviceGroup.trim().toLowerCase();
404
+ return DEFAULT_GROUP_MAX_INSTANCES[g] ?? 0;
405
+ }
406
+ async function countAliveInGroup(db, registryTable, queueName, serviceGroup, staleMs, excludeRowId) {
407
+ const cutoff = new Date(Date.now() - staleMs);
408
+ let q = db(registryTable).where({ queue_name: queueName, service_group: serviceGroup }).where("last_seen_at", ">", cutoff);
409
+ if (excludeRowId) {
410
+ q = q.whereNot("id", excludeRowId);
411
+ }
412
+ const row = await q.count("id as count").first();
413
+ return Number(row?.count ?? 0);
414
+ }
415
+ async function getOccupiedInstanceSlots(db, registryTable, queueName, serviceGroup, staleMs) {
416
+ const cutoff = new Date(Date.now() - staleMs);
417
+ const rows = await db(registryTable).where({ queue_name: queueName, service_group: serviceGroup }).where("last_seen_at", ">", cutoff).select("instance_number");
418
+ const set = /* @__PURE__ */ new Set();
419
+ for (const r of rows) {
420
+ const n = Number(r.instance_number);
421
+ if (Number.isFinite(n) && n >= 1) set.add(Math.floor(n));
422
+ }
423
+ return set;
424
+ }
425
+ function isUniqueViolation(error) {
426
+ const code = error?.code ?? error?.errno;
427
+ return code === "23505" || String(error?.message || "").includes("duplicate key");
428
+ }
429
+ function buildMetadata(options) {
430
+ const base = options.metadata && typeof options.metadata === "object" ? { ...options.metadata } : {};
431
+ if (options.target) {
432
+ base.runnerTarget = options.target;
433
+ }
434
+ return toJsonColumn(Object.keys(base).length ? base : null);
435
+ }
436
+ function allocateInstanceNumber(occupied, explicit, maxSlots) {
437
+ if (explicit !== void 0 && explicit !== null && Number.isFinite(Number(explicit))) {
438
+ const e = Math.max(1, Math.floor(Number(explicit)));
439
+ if (occupied.has(e)) {
440
+ throw new Error(`[services-registry] instance slot ${e} is already occupied by an alive peer`);
441
+ }
442
+ if (maxSlots !== void 0 && maxSlots > 0 && e > maxSlots) {
443
+ throw new Error(`[services-registry] instance ${e} exceeds configured max slots ${maxSlots} for this group`);
444
+ }
445
+ return e;
446
+ }
447
+ const cap = maxSlots !== void 0 && maxSlots > 0 ? maxSlots : 1e4;
448
+ for (let n = 1; n <= cap; n++) {
449
+ if (!occupied.has(n)) return n;
450
+ }
451
+ throw new Error(`[services-registry] no free instance slot (searched 1..${cap})`);
452
+ }
453
+ function defaultServiceName(groupBase, hostBase, instanceNumber) {
454
+ return `${groupBase}-${hostBase}-${instanceNumber}`;
455
+ }
456
+ async function registerInServicesRegistry(context, options) {
457
+ const db = getDb2(context);
458
+ const registryTable = queueToTableNames(options.queueName).registryTable;
459
+ const serviceGroup = options.serviceGroup.trim();
460
+ if (!serviceGroup) {
461
+ throw new Error("registerInServicesRegistry: serviceGroup is required");
462
+ }
463
+ const serverName = import_node_os.default.hostname();
464
+ const pid = typeof process.pid === "number" ? process.pid : null;
465
+ const meta = buildMetadata(options);
466
+ const groupBase = sanitizeNamePart(serviceGroup);
467
+ const hostBase = sanitizeNamePart(serverName);
468
+ const maxAllowed = resolveMaxInstances(serviceGroup, options.groupMaxInstances);
469
+ const aliveCount = await countAliveInGroup(db, registryTable, options.queueName, serviceGroup, options.staleMs, void 0);
470
+ if (maxAllowed > 0 && aliveCount >= maxAllowed) {
471
+ const msg = `[services-registry] group limit reached for "${serviceGroup}": ${aliveCount} alive (max ${maxAllowed}, queue=${options.queueName}).`;
472
+ if (options.enforceMaxInstances) {
473
+ throw new Error(msg);
474
+ }
475
+ context.logger.warn?.(`${msg} Starting anyway (runnerEnforceMaxInstances=false).`);
476
+ }
477
+ const maxSlots = maxAllowed > 0 ? maxAllowed : void 0;
478
+ const cutoff = new Date(Date.now() - options.staleMs);
479
+ const MAX_ATTEMPTS = 8;
480
+ for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
481
+ const occupied = await getOccupiedInstanceSlots(db, registryTable, options.queueName, serviceGroup, options.staleMs);
482
+ const instanceNumber = allocateInstanceNumber(occupied, options.instanceNumber, maxSlots);
483
+ const serviceNameRaw = options.serviceName?.trim() ? sanitizeNamePart(options.serviceName.trim()) : defaultServiceName(groupBase, hostBase, instanceNumber);
484
+ const existing = await db(registryTable).where({ queue_name: options.queueName, service_name: serviceNameRaw }).first();
485
+ if (existing) {
486
+ const lastSeen = new Date(existing.last_seen_at);
487
+ const isAlive = !Number.isNaN(lastSeen.getTime()) && lastSeen > cutoff;
488
+ if (isAlive) {
489
+ if (options.serviceName?.trim()) {
490
+ throw new Error(
491
+ `[services-registry] service_name "${serviceNameRaw}" is already registered by an alive peer`
492
+ );
493
+ }
494
+ context.logger.warn?.(
495
+ `[services-registry] service_name "${serviceNameRaw}" already alive; retrying allocation (attempt ${attempt + 1})`
496
+ );
497
+ if (options.instanceNumber !== void 0 && options.instanceNumber !== null) {
498
+ throw new Error(
499
+ `[services-registry] instance slot ${instanceNumber} / name "${serviceNameRaw}" is already held by an alive peer`
500
+ );
501
+ }
502
+ await new Promise((r) => setTimeout(r, 50 + attempt * 30));
503
+ continue;
504
+ }
505
+ await db(registryTable).where({ id: existing.id }).update({
506
+ server_name: serverName,
507
+ pid,
508
+ metadata: meta,
509
+ service_group: serviceGroup,
510
+ instance_number: instanceNumber,
511
+ last_seen_at: db.fn.now()
512
+ });
513
+ const reg = {
514
+ serviceName: serviceNameRaw,
515
+ serviceGroup,
516
+ queueName: options.queueName,
517
+ target: options.target,
518
+ rowId: String(existing.id),
519
+ registryTable,
520
+ instanceNumber
521
+ };
522
+ context.servicesRegistry = reg;
523
+ context.runnerHeartbeat = reg;
524
+ context.logger.info?.(
525
+ `[services-registry] took over stale row name=${serviceNameRaw} instance=${instanceNumber} group=${serviceGroup} queue=${options.queueName}`
526
+ );
527
+ return reg;
528
+ }
529
+ try {
530
+ const rows = await db(registryTable).insert({
531
+ queue_name: options.queueName,
532
+ service_group: serviceGroup,
533
+ instance_number: instanceNumber,
534
+ service_name: serviceNameRaw,
535
+ server_name: serverName,
536
+ pid,
537
+ metadata: meta,
538
+ last_seen_at: db.fn.now(),
539
+ created_at: db.fn.now()
540
+ }).returning(["id", "service_name"]);
541
+ const row = Array.isArray(rows) ? rows[0] : rows;
542
+ let rowId = row && typeof row === "object" ? String(row.id ?? "") : "";
543
+ if (!rowId) {
544
+ const again = await db(registryTable).where({ queue_name: options.queueName, service_name: serviceNameRaw }).first();
545
+ rowId = again?.id != null ? String(again.id) : "";
546
+ }
547
+ if (!rowId) continue;
548
+ const regNew = {
549
+ serviceName: String(row?.service_name ?? serviceNameRaw),
550
+ serviceGroup,
551
+ queueName: options.queueName,
552
+ target: options.target,
553
+ rowId,
554
+ registryTable,
555
+ instanceNumber
556
+ };
557
+ context.servicesRegistry = regNew;
558
+ context.runnerHeartbeat = regNew;
559
+ context.logger.info?.(
560
+ `[services-registry] registered name=${regNew.serviceName} instance=${instanceNumber} group=${serviceGroup} queue=${options.queueName}`
561
+ );
562
+ return regNew;
563
+ } catch (error) {
564
+ if (!isUniqueViolation(error)) {
565
+ throw error;
566
+ }
567
+ context.logger.warn?.(`[services-registry] insert race on "${serviceNameRaw}", retrying (attempt ${attempt + 1})`);
568
+ }
569
+ }
570
+ throw new Error(
571
+ `[services-registry] could not allocate a registry row for group=${serviceGroup} queue=${options.queueName} after ${MAX_ATTEMPTS} attempts`
572
+ );
573
+ }
574
+ async function touchServicesRegistry(context, registration) {
575
+ const db = getDb2(context);
576
+ const serverName = import_node_os.default.hostname();
577
+ const pid = typeof process.pid === "number" ? process.pid : null;
578
+ await db(registration.registryTable).where({ id: registration.rowId }).update({
579
+ last_seen_at: db.fn.now(),
580
+ server_name: serverName,
581
+ pid
582
+ });
583
+ }
584
+ async function updateServicesRegistryMetadata(context, registration, patch) {
585
+ const db = getDb2(context);
586
+ const row = await db(registration.registryTable).where({ id: registration.rowId }).first();
587
+ const prev = parseMetadataColumn(row?.metadata);
588
+ const merged = { ...prev, ...patch };
589
+ await db(registration.registryTable).where({ id: registration.rowId }).update({
590
+ metadata: toJsonColumn(merged),
591
+ last_seen_at: db.fn.now()
592
+ });
593
+ context.logger.info?.(`[services-registry] metadata updated for ${registration.serviceName}`);
594
+ }
595
+ async function unregisterServicesRegistry(context, registration) {
596
+ const db = getDb2(context);
597
+ await db(registration.registryTable).where({ id: registration.rowId }).delete();
598
+ context.logger.info?.(`[services-registry] unregistered name=${registration.serviceName} id=${registration.rowId}`);
599
+ }
600
+ async function listServicesRegistry(context, options = { queueName: "tasks" }) {
601
+ const db = getDb2(context);
602
+ const staleMs = options.staleMs ?? 6e4;
603
+ const cutoff = new Date(Date.now() - staleMs);
604
+ const table = queueToTableNames(options.queueName).registryTable;
605
+ let q = db(table).where("last_seen_at", ">", cutoff).orderBy([{ column: "service_group", order: "asc" }, { column: "service_name", order: "asc" }]);
606
+ if (options.serviceGroup?.trim()) {
607
+ q = q.where({ service_group: options.serviceGroup.trim() });
608
+ }
609
+ return await q;
610
+ }
611
+
612
+ // src/tasks/taskLogs.js
613
+ var import_node_path = __toESM(require("path"), 1);
614
+
615
+ // src/filedatabase/index.js
255
616
  var import_fs3 = __toESM(require("fs"), 1);
256
617
  var import_path3 = __toESM(require("path"), 1);
257
618
 
258
- // src/filedatabase/serializers.ts
619
+ // src/filedatabase/serializers.js
259
620
  function detectDataType(data) {
260
621
  if (Array.isArray(data)) {
261
622
  return "json-array";
@@ -287,7 +648,7 @@ function deserializeData(rawData, dataType) {
287
648
  }
288
649
  }
289
650
 
290
- // src/errors.ts
651
+ // src/errors.js
291
652
  var FrameworkError = class extends Error {
292
653
  constructor(message) {
293
654
  super(message);
@@ -307,7 +668,7 @@ var FileDatabaseError = class extends FrameworkError {
307
668
  }
308
669
  };
309
670
 
310
- // src/filedatabase/index.ts
671
+ // src/filedatabase/index.js
311
672
  var FileDatabase = class _FileDatabase {
312
673
  basePath;
313
674
  namespace;
@@ -393,7 +754,7 @@ var FileDatabase = class _FileDatabase {
393
754
  if (errors.length) {
394
755
  throw new FileDatabaseError(`[FileDatabase] ${errors.join("; ")}`);
395
756
  }
396
- let parts = [this.basePath, this.namespace];
757
+ const parts = [this.basePath, this.namespace];
397
758
  if (this.tableName) {
398
759
  parts.push(...this.tableName.split("/"));
399
760
  }
@@ -886,14 +1247,17 @@ var FileDatabase = class _FileDatabase {
886
1247
  * Prepare the instance for read or write operations
887
1248
  * This discovers state and sets up internal members based on mode and current data
888
1249
  */
889
- async prepare({ write, read, version }) {
1250
+ async prepare(options) {
1251
+ const { write, read, version, deferInitialVersion } = options;
890
1252
  if (write) {
891
1253
  if (this.versioned) {
892
1254
  if (this.currentVersion === null) {
893
- await this.makeNewVersion();
894
- this.metadata = this.getDefaultMetadata();
895
- this.metadata.version = this.currentVersion;
896
- this.makeNewFile();
1255
+ if (!deferInitialVersion) {
1256
+ await this.makeNewVersion();
1257
+ this.metadata = this.getDefaultMetadata();
1258
+ this.metadata.version = this.currentVersion;
1259
+ this.makeNewFile();
1260
+ }
897
1261
  } else {
898
1262
  if (!this.metadata.files.length) {
899
1263
  this.metadata = await this.figureMetadata(this.currentVersion);
@@ -1003,7 +1367,7 @@ var FileDatabase = class _FileDatabase {
1003
1367
  if (options.forceNewVersion && !this.versioned) {
1004
1368
  throw new FileDatabaseError("Cannot use forceNewVersion in non-versioned mode");
1005
1369
  }
1006
- await this.prepare({ write: true });
1370
+ await this.prepare({ write: true, deferInitialVersion: !!(options.forceNewVersion && this.versioned) });
1007
1371
  const incomingDataType = detectDataType(data);
1008
1372
  this.metadata.dataType = incomingDataType;
1009
1373
  if (options.forceNewVersion) {
@@ -1297,7 +1661,7 @@ var FileDatabase = class _FileDatabase {
1297
1661
  }
1298
1662
  };
1299
1663
 
1300
- // src/tasks/taskLogs.ts
1664
+ // src/tasks/taskLogs.js
1301
1665
  function getLogsState(context) {
1302
1666
  const holder = context;
1303
1667
  if (holder.__tasksLogsState) return holder.__tasksLogsState;
@@ -1350,6 +1714,96 @@ function getLogsState(context) {
1350
1714
  holder.__tasksLogsState = state;
1351
1715
  return state;
1352
1716
  }
1717
+ function ipcLogTargetKey(target) {
1718
+ const bp = target.basePath ?? "";
1719
+ const ns = target.namespace ?? "";
1720
+ return `${bp}::${ns}::${target.tableName}`;
1721
+ }
1722
+ function ipcFileLogsTableNameForSourceResource(source, resource) {
1723
+ const seg = (s) => {
1724
+ const t = String(s).trim().replace(/[^a-zA-Z0-9._-]+/g, "_").replace(/^_+|_+$/g, "");
1725
+ return t.length ? t : "x";
1726
+ };
1727
+ return `${seg(source)}/${seg(resource)}`;
1728
+ }
1729
+ async function readTaskIpcLogsSnapshot(context, options) {
1730
+ const holder = context;
1731
+ const basePath = holder.params?.get?.("tasksLogsBasePath") ?? "./data";
1732
+ const namespace = holder.params?.get?.("tasksLogsNamespace") ?? "tasks-logs";
1733
+ const tableName = ipcFileLogsTableNameForSourceResource(options.source, options.resource);
1734
+ const tail = Math.max(1, Math.min(1e4, Number(options.tail) > 0 ? Number(options.tail) : 100));
1735
+ const fd = new FileDatabase({
1736
+ basePath,
1737
+ namespace,
1738
+ tableName,
1739
+ versioned: true,
1740
+ useMetadata: true,
1741
+ maxVersions: 30,
1742
+ pageSize: 2e3,
1743
+ logger: holder.logger
1744
+ });
1745
+ const versions = await fd.getVersions();
1746
+ if (versions.length === 0) {
1747
+ return { records: [], latestTs: null };
1748
+ }
1749
+ const latest = versions[versions.length - 1];
1750
+ const raw = await fd.read({ version: latest });
1751
+ const arr = Array.isArray(raw) ? raw : [];
1752
+ let filtered = arr;
1753
+ if (options.afterTs && String(options.afterTs).trim()) {
1754
+ const cut = String(options.afterTs).trim();
1755
+ filtered = arr.filter((r) => r && typeof r.ts === "string" && String(r.ts) > cut);
1756
+ }
1757
+ let latestTs = null;
1758
+ for (const r of filtered) {
1759
+ const ts = typeof r?.ts === "string" ? String(r.ts) : null;
1760
+ if (ts && (!latestTs || ts > latestTs)) latestTs = ts;
1761
+ }
1762
+ const incremental = !!(options.afterTs && String(options.afterTs).trim());
1763
+ const maxReturn = incremental ? 1e4 : tail;
1764
+ const sliced = filtered.length > maxReturn ? filtered.slice(-maxReturn) : filtered;
1765
+ return { records: sliced, latestTs };
1766
+ }
1767
+ function resolveIpcFileLogsDir(context, target) {
1768
+ const holder = context;
1769
+ const basePath = target.basePath ?? (holder.params?.get?.("tasksLogsBasePath") || "./data");
1770
+ const namespace = target.namespace ?? (holder.params?.get?.("tasksLogsNamespace") || "tasks-logs");
1771
+ const segments = target.tableName.split("/").filter(Boolean);
1772
+ return import_node_path.default.resolve(basePath, namespace, ...segments);
1773
+ }
1774
+ function getLogsStateForTarget(context, target) {
1775
+ const holder = context;
1776
+ const enabledRaw = holder.params?.get?.("tasksLogsEnabled");
1777
+ const enabled = enabledRaw === void 0 ? true : !!enabledRaw;
1778
+ if (!enabled) return null;
1779
+ if (!holder.__tasksLogsTargetStates) holder.__tasksLogsTargetStates = /* @__PURE__ */ new Map();
1780
+ const map = holder.__tasksLogsTargetStates;
1781
+ const key = ipcLogTargetKey(target);
1782
+ if (map.has(key)) return map.get(key);
1783
+ const basePath = target.basePath ?? (holder.params?.get?.("tasksLogsBasePath") || "./data");
1784
+ const namespace = target.namespace ?? (holder.params?.get?.("tasksLogsNamespace") || "tasks-logs");
1785
+ const maxVersionsRaw = Number(holder.params?.get?.("tasksLogsMaxVersions"));
1786
+ const pageSizeRaw = Number(holder.params?.get?.("tasksLogsPageSize"));
1787
+ const db = new FileDatabase({
1788
+ basePath,
1789
+ namespace,
1790
+ tableName: target.tableName,
1791
+ versioned: true,
1792
+ useMetadata: true,
1793
+ maxVersions: Number.isFinite(maxVersionsRaw) && maxVersionsRaw > 0 ? maxVersionsRaw : 20,
1794
+ pageSize: Number.isFinite(pageSizeRaw) && pageSizeRaw > 0 ? pageSizeRaw : 2e3,
1795
+ logger: holder.logger
1796
+ });
1797
+ const state = {
1798
+ db,
1799
+ errorDb: null,
1800
+ queue: Promise.resolve(),
1801
+ initialized: false,
1802
+ errorInitialized: false
1803
+ };
1804
+ map.set(key, state);
1805
+ return state;
1806
+ }
1353
1807
  function isErrorPayload(payload) {
1354
1808
  if (!payload) return false;
1355
1809
  if (typeof payload === "object") {
@@ -1369,14 +1823,26 @@ function buildLogRecord(task, payload) {
1369
1823
  ts: (/* @__PURE__ */ new Date()).toISOString(),
1370
1824
  opid: task.opid ?? null,
1371
1825
  taskId: task.id,
1372
- taskName: task.task,
1373
- target: task.target,
1826
+ taskName: task.name,
1827
+ target: task.service_group,
1374
1828
  source: typeof params.source === "string" ? params.source : null,
1375
1829
  resource: typeof params.resource === "string" ? params.resource : null,
1376
1830
  payload
1377
1831
  };
1378
1832
  }
1379
- function appendTaskIpcLog(context, task, payload) {
1833
+ function appendTaskIpcLog(context, task, payload, target) {
1834
+ if (target) {
1835
+ const state2 = getLogsStateForTarget(context, target);
1836
+ if (!state2?.db) return;
1837
+ const record2 = buildLogRecord(task, payload);
1838
+ state2.queue = state2.queue.then(async () => {
1839
+ await state2.db.write([record2], { forceNewVersion: !state2.initialized });
1840
+ state2.initialized = true;
1841
+ }).catch((error) => {
1842
+ context.logger.warn?.("[tasks] failed to persist IPC log entry (targeted):", error);
1843
+ });
1844
+ return;
1845
+ }
1380
1846
  const state = getLogsState(context);
1381
1847
  if (!state.db && !state.errorDb) return;
1382
1848
  const record = buildLogRecord(task, payload);
@@ -1393,99 +1859,316 @@ function appendTaskIpcLog(context, task, payload) {
1393
1859
  context.logger.warn?.("[tasks] failed to persist IPC log entry:", error);
1394
1860
  });
1395
1861
  }
1396
-
1397
- // src/tasks/time-matcher.ts
1398
- var RANGES = ["0-59", "0-59", "0-23", "1-31", "1-12", "0-6"];
1399
- function resolveAsterisks(field, range) {
1400
- return field.includes("*") ? field.replace("*", range) : field;
1401
- }
1402
- function resolveRanges(field) {
1403
- const regex = /(\d+)-(\d+)/;
1404
- let current = field;
1405
- while (true) {
1406
- const match = regex.exec(current);
1407
- if (!match) break;
1408
- const raw = match[0];
1409
- let first = Number(match[1]);
1410
- let last = Number(match[2]);
1411
- if (last < first) {
1412
- [first, last] = [last, first];
1413
- }
1414
- const values = [];
1415
- for (let i = first; i <= last; i += 1) {
1416
- values.push(i);
1862
+ async function flushTaskIpcLogs(context) {
1863
+ const holder = context;
1864
+ const promises = [];
1865
+ if (holder.__tasksLogsState?.queue) promises.push(holder.__tasksLogsState.queue);
1866
+ const map = holder.__tasksLogsTargetStates;
1867
+ if (map) {
1868
+ for (const s of map.values()) {
1869
+ if (s.queue) promises.push(s.queue);
1417
1870
  }
1418
- current = current.replace(raw, values.join(","));
1419
- }
1420
- return current;
1421
- }
1422
- function resolveSteps(field) {
1423
- const match = /^(.+)\/(\d+)$/.exec(field);
1424
- if (!match) return field;
1425
- const base = match[1];
1426
- const step = Number(match[2]);
1427
- if (!Number.isFinite(step) || step <= 0) return field;
1428
- return base.split(",").map((v) => Number(v)).filter((v) => Number.isFinite(v) && v % step === 0).join(",");
1429
- }
1430
- function convertPattern(pattern) {
1431
- const parts = pattern.trim().split(/\s+/);
1432
- if (parts.length !== 6) {
1433
- throw new Error(`Invalid schedule "${pattern}". Expected 6 fields: sec min hour day month weekday`);
1434
1871
  }
1435
- return parts.map((field, idx) => resolveAsterisks(field, RANGES[idx])).map((field) => resolveRanges(field)).map((field) => resolveSteps(field));
1436
- }
1437
- function fieldMatches(field, value) {
1438
- const allowed = field.split(",").map((v) => Number(v));
1439
- return allowed.includes(value);
1440
- }
1441
- function timeMatcher(pattern, date = /* @__PURE__ */ new Date()) {
1442
- const parsed = convertPattern(pattern);
1443
- return fieldMatches(parsed[0], date.getSeconds()) && fieldMatches(parsed[1], date.getMinutes()) && fieldMatches(parsed[2], date.getHours()) && fieldMatches(parsed[3], date.getDate()) && fieldMatches(parsed[4], date.getMonth() + 1) && fieldMatches(parsed[5], date.getDay());
1872
+ await Promise.all(promises);
1444
1873
  }
1445
1874
 
1446
- // src/tasks/TaskMaster.ts
1447
- var TaskMaster = class {
1448
- context;
1449
- task;
1875
+ // src/tasks/AbstractTask.js
1876
+ var AbstractTask = class _AbstractTask {
1877
+ /**
1878
+ * Whether `send-task` should wait for completion (and print a result
1879
+ * report) when no explicit `--wait` / `--noWait` flag is given. Defaults
1880
+ * to false; short-lived probe tasks (e.g. `ping`) override to true.
1881
+ *
1882
+ * @type {boolean}
1883
+ */
1884
+ static defaultWaitForResult = false;
1885
+ /**
1886
+ * @param {object} context Runner context (db, logger, params, emitter...).
1887
+ * @param {object} task Task row as claimed from the queue.
1888
+ */
1450
1889
  constructor(context, task) {
1451
1890
  this.context = context;
1452
1891
  this.task = task;
1453
1892
  }
1893
+ /**
1894
+ * Return a short reason string when the task should be deferred (e.g. "locked
1895
+ * by source"), or `false`/falsy when it is free to run. Default: always `false`.
1896
+ *
1897
+ * @returns {string | false | Promise<string | false>}
1898
+ */
1454
1899
  cantRunReason() {
1455
1900
  return false;
1456
1901
  }
1902
+ /**
1903
+ * Called by the runner when a stop has been requested. Subclasses running
1904
+ * long loops should flip a flag here and check it between iterations.
1905
+ *
1906
+ * @param {number} [_allowanceMs] Grace period the runner promises before hard exit.
1907
+ */
1457
1908
  requestStop(_allowanceMs) {
1458
1909
  }
1459
- };
1460
-
1461
- // src/tasks/coreTasks/TaskPing.ts
1462
- var TaskPing = class extends TaskMaster {
1463
- async run() {
1464
- this.context.logger.info?.(`[TaskPing] pong (${this.task.id})`);
1465
- return { success: true, results: "pong" };
1910
+ /**
1911
+ * Perform the task. Must be implemented by subclasses.
1912
+ *
1913
+ * @param {(progress: unknown) => Promise<void>} _reportProgress
1914
+ * Updates the DB `progress` column. Accepts any serializable value;
1915
+ * strings are stored verbatim, objects are JSON-stringified.
1916
+ * @returns {Promise<{ success: boolean, results: unknown }>}
1917
+ */
1918
+ async run(_reportProgress) {
1919
+ throw new Error("AbstractTask.run must be implemented by subclass");
1466
1920
  }
1467
- };
1468
-
1469
- // src/tasks/coreTasks/TaskSampleProcess.ts
1470
- var TaskSampleProcess = class extends TaskMaster {
1471
- stopRequested = false;
1472
- stopAllowanceMs = 0;
1473
- stopDecisionLogged = false;
1474
- requestStop(allowanceMs) {
1475
- this.stopRequested = true;
1476
- this.stopAllowanceMs = Number.isFinite(allowanceMs) && allowanceMs > 0 ? allowanceMs : 0;
1477
- this.context.logger.warn?.(
1478
- `[TaskSampleProcess] stop signal received (${this.task.id}), allowanceMs=${this.stopAllowanceMs}`
1479
- );
1921
+ /**
1922
+ * Resolve a complete row payload for this task — envelope fields (queue,
1923
+ * priority, targeting, schedule…) plus the inner `params` blob produced by
1924
+ * {@link AbstractTask.resolveCustomParams}. Output shape matches
1925
+ * {@link enqueueTask}'s `options` argument, so the typical call is:
1926
+ *
1927
+ * const payload = await TaskClass.resolveParams(context, { name });
1928
+ * await enqueueTask(context, payload);
1929
+ *
1930
+ * Validation failures throw {@link ParamError} so the script aborts before
1931
+ * a malformed row hits the DB.
1932
+ *
1933
+ * @param {object} context
1934
+ * @param {Record<string, unknown>} [overrides] Partial overrides; takes precedence over CLI/env.
1935
+ * Recognised keys: `name`, `queueName`, `priority`, `serviceGroup`,
1936
+ * `serviceName`, `instanceNumber`, `serverName`, `opid`, `schedule`,
1937
+ * `nextRunAt`, plus `params` (object — overlay onto inner blob).
1938
+ * @returns {Promise<object>}
1939
+ */
1940
+ static async resolveParams(context, overrides = {}) {
1941
+ const main = _AbstractTask._resolveMainFields(context, overrides);
1942
+ const params = await this.resolveCustomParams(context, overrides);
1943
+ return { ...main, params };
1480
1944
  }
1481
- async run(reportProgress) {
1482
- const totalRaw = this.task?.params?.total ?? 10;
1483
- const delayRaw = this.task?.params?.delay ?? 1e3;
1484
- const nameRaw = this.task?.params?.name;
1485
- const total = Number(totalRaw);
1486
- const delay = Number(delayRaw);
1487
- const name = typeof nameRaw === "string" && nameRaw.trim() ? nameRaw.trim() : "sampleProcess";
1488
- const errors = [];
1945
+ /**
1946
+ * Resolve the inner JSON blob stored in the `params` column. Default
1947
+ * implementation passes through `--paramsJson` (parsed as a JSON object)
1948
+ * overlaid with `overrides.params` when supplied; returns `null` when
1949
+ * neither is provided.
1950
+ *
1951
+ * Subclasses with typed fields should override and call
1952
+ * {@link AbstractTask._mergeTypedParams} for layered CLI/env/JSON/override
1953
+ * resolution, then validate and throw {@link ParamError} on bad input.
1954
+ *
1955
+ * @param {object} context
1956
+ * @param {Record<string, unknown>} [overrides]
1957
+ * @returns {Promise<object|null>}
1958
+ */
1959
+ static async resolveCustomParams(context, overrides = {}) {
1960
+ return _AbstractTask._defaultParamsBlob(context, overrides);
1961
+ }
1962
+ /**
1963
+ * Read main task envelope fields from `context.params` (CLI/env), with
1964
+ * any matching key on `overrides` taking precedence. Internal; called by
1965
+ * {@link AbstractTask.resolveParams}.
1966
+ *
1967
+ * @param {object} context
1968
+ * @param {Record<string, unknown>} [overrides]
1969
+ * @returns {object}
1970
+ */
1971
+ static _resolveMainFields(context, overrides = {}) {
1972
+ const defs = {
1973
+ queueName: "string default tasks",
1974
+ priority: "number default 50",
1975
+ serviceGroup: "string",
1976
+ serviceName: "string",
1977
+ instanceNumber: "number",
1978
+ serverName: "string",
1979
+ opid: "string",
1980
+ schedule: "string"
1981
+ };
1982
+ const cli = context.params.getAllForModule("task-envelope", defs);
1983
+ const name = emptyToUndef(overrides.name) ?? emptyToUndef(context.params.get("name", "string"));
1984
+ if (!name) {
1985
+ throw new ParamError("Task --name is required (e.g. ping, stop, dummyHarvest)");
1986
+ }
1987
+ let instanceNumber;
1988
+ const rawInstance = overrides.instanceNumber ?? cli.instanceNumber;
1989
+ if (rawInstance !== void 0 && rawInstance !== null && String(rawInstance).trim() !== "") {
1990
+ const n = Number(rawInstance);
1991
+ if (!Number.isFinite(n) || !Number.isInteger(n) || n < 1) {
1992
+ throw new ParamError("--instanceNumber must be a positive integer when set");
1993
+ }
1994
+ instanceNumber = n;
1995
+ } else {
1996
+ instanceNumber = null;
1997
+ }
1998
+ const priorityRaw = overrides.priority ?? cli.priority ?? 50;
1999
+ const priority = Number(priorityRaw);
2000
+ if (!Number.isFinite(priority)) {
2001
+ throw new ParamError(`--priority must be a number (got ${JSON.stringify(priorityRaw)})`);
2002
+ }
2003
+ return {
2004
+ name,
2005
+ queueName: overrides.queueName ?? emptyToUndef(cli.queueName) ?? "tasks",
2006
+ priority,
2007
+ serviceGroup: emptyToUndef(overrides.serviceGroup) ?? emptyToUndef(cli.serviceGroup) ?? null,
2008
+ serviceName: emptyToUndef(overrides.serviceName) ?? emptyToUndef(cli.serviceName) ?? null,
2009
+ instanceNumber,
2010
+ serverName: emptyToUndef(overrides.serverName) ?? emptyToUndef(cli.serverName) ?? null,
2011
+ opid: emptyToUndef(overrides.opid) ?? emptyToUndef(cli.opid) ?? null,
2012
+ schedule: emptyToUndef(overrides.schedule) ?? emptyToUndef(cli.schedule) ?? null,
2013
+ nextRunAt: overrides.nextRunAt ?? null
2014
+ };
2015
+ }
2016
+ /**
2017
+ * Default inner-params resolver: parses `--paramsJson` (must be a JSON
2018
+ * object), then overlays `overrides.params` on top. Returns `null` when
2019
+ * neither is provided.
2020
+ *
2021
+ * @param {object} context
2022
+ * @param {Record<string, unknown>} [overrides]
2023
+ * @returns {object|null}
2024
+ */
2025
+ static _defaultParamsBlob(context, overrides = {}) {
2026
+ const cli = context.params.getAllForModule("task-params", { paramsJson: "string" });
2027
+ const fromJson = parseParamsJson(cli.paramsJson);
2028
+ const fromOverride = pickParamsObject(overrides);
2029
+ if (!fromJson && !fromOverride) return null;
2030
+ return { ...fromJson ?? {}, ...fromOverride ?? {} };
2031
+ }
2032
+ /**
2033
+ * Helper for subclass `resolveCustomParams` overrides. Reads typed CLI
2034
+ * params (per `defs`) plus `--paramsJson` under a module namespace, then
2035
+ * merges them with explicit `overrides.params` in increasing priority:
2036
+ *
2037
+ * typed CLI flags → --paramsJson → overrides.params
2038
+ *
2039
+ * Undefined values are dropped so defaults declared in `defs` aren't
2040
+ * overwritten by missing-flag noise. Returns the merged object; the
2041
+ * caller is responsible for validation and throwing `ParamError`.
2042
+ *
2043
+ * @param {object} context
2044
+ * @param {string} moduleName Namespace for `--showUsedParams` grouping.
2045
+ * @param {Record<string, string>} defs Param defs in `getAllForModule` syntax.
2046
+ * @param {Record<string, unknown>} [overrides] As passed to `resolveCustomParams`.
2047
+ * @returns {Record<string, unknown>}
2048
+ */
2049
+ static _mergeTypedParams(context, moduleName, defs, overrides = {}) {
2050
+ const fullDefs = { ...defs, paramsJson: "string" };
2051
+ const cliRaw = context.params.getAllForModule(moduleName, fullDefs);
2052
+ const fromJson = parseParamsJson(cliRaw.paramsJson) ?? {};
2053
+ const fromCli = {};
2054
+ for (const [k, v] of Object.entries(cliRaw)) {
2055
+ if (k === "paramsJson") continue;
2056
+ if (v !== void 0 && v !== null) fromCli[k] = v;
2057
+ }
2058
+ const fromOverride = pickParamsObject(overrides) ?? {};
2059
+ return { ...fromCli, ...fromJson, ...fromOverride };
2060
+ }
2061
+ };
2062
+ function emptyToUndef(s) {
2063
+ if (s === void 0 || s === null) return void 0;
2064
+ if (typeof s !== "string") return s;
2065
+ const t = s.trim();
2066
+ return t.length ? t : void 0;
2067
+ }
2068
+ function parseParamsJson(raw) {
2069
+ if (raw == null) return null;
2070
+ const t = String(raw).trim();
2071
+ if (!t) return null;
2072
+ let parsed;
2073
+ try {
2074
+ parsed = JSON.parse(t);
2075
+ } catch (e) {
2076
+ throw new ParamError(`--paramsJson: not valid JSON: ${e?.message ?? String(e)}`);
2077
+ }
2078
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
2079
+ throw new ParamError("--paramsJson must be a JSON object");
2080
+ }
2081
+ return parsed;
2082
+ }
2083
+ function pickParamsObject(overrides) {
2084
+ const p = overrides?.params;
2085
+ if (p && typeof p === "object" && !Array.isArray(p)) return p;
2086
+ return void 0;
2087
+ }
2088
+
2089
+ // src/tasks/coreTasks/TaskPing.js
2090
+ var TaskPing = class extends AbstractTask {
2091
+ /** Default-wait so `send-task --name=ping` prints a result without `--wait`. */
2092
+ static defaultWaitForResult = true;
2093
+ /** Ping takes no params. */
2094
+ static async resolveCustomParams() {
2095
+ return null;
2096
+ }
2097
+ /**
2098
+ * @returns {Promise<{ success: true, results: "pong" }>}
2099
+ */
2100
+ async run() {
2101
+ this.context.logger.info?.(`[TaskPing] pong (${this.task.id})`);
2102
+ return { success: true, results: "pong" };
2103
+ }
2104
+ };
2105
+
2106
+ // src/tasks/coreTasks/TaskSampleProcess.js
2107
+ var TaskSampleProcess = class extends AbstractTask {
2108
+ /**
2109
+ * @param {object} context
2110
+ * @param {Record<string, unknown>} [overrides]
2111
+ * @returns {Promise<{ total: number, delay: number, name?: string }>}
2112
+ */
2113
+ static async resolveCustomParams(context, overrides = {}) {
2114
+ const merged = AbstractTask._mergeTypedParams(context, "task-sample-process", {
2115
+ total: "number default 10",
2116
+ delay: "number default 1000",
2117
+ name: "string"
2118
+ }, overrides);
2119
+ const total = Number(merged.total);
2120
+ const delay = Number(merged.delay);
2121
+ if (!Number.isInteger(total) || total <= 0) {
2122
+ throw new ParamError(`sampleProcess: param "total" must be a positive integer (got ${JSON.stringify(merged.total)})`);
2123
+ }
2124
+ if (!Number.isInteger(delay) || delay < 0) {
2125
+ throw new ParamError(`sampleProcess: param "delay" must be an integer >= 0 (got ${JSON.stringify(merged.delay)})`);
2126
+ }
2127
+ const out = { total, delay };
2128
+ if (typeof merged.name === "string" && merged.name.trim()) {
2129
+ out.name = merged.name.trim();
2130
+ }
2131
+ return out;
2132
+ }
2133
+ /**
2134
+ * @param {object} context
2135
+ * @param {object} task
2136
+ */
2137
+ constructor(context, task) {
2138
+ super(context, task);
2139
+ this.stopRequested = false;
2140
+ this.stopAllowanceMs = 0;
2141
+ this.stopDecisionLogged = false;
2142
+ }
2143
+ /**
2144
+ * Runner-facing stop signal. Records the allowance window so the main loop
2145
+ * can decide per-iteration whether to finish or abort early.
2146
+ *
2147
+ * @param {number} allowanceMs
2148
+ */
2149
+ requestStop(allowanceMs) {
2150
+ this.stopRequested = true;
2151
+ this.stopAllowanceMs = Number.isFinite(allowanceMs) && allowanceMs > 0 ? allowanceMs : 0;
2152
+ this.context.logger.warn?.(
2153
+ `[TaskSampleProcess] stop signal received (${this.task.id}), allowanceMs=${this.stopAllowanceMs}`
2154
+ );
2155
+ }
2156
+ /**
2157
+ * Iterate `total` times, sleeping `delay` ms between ticks and reporting
2158
+ * progress every iteration. Validates params up front; invalid values short-
2159
+ * circuit to a structured failure without starting the loop.
2160
+ *
2161
+ * @param {(progress: object) => Promise<void>} reportProgress
2162
+ * @returns {Promise<{ success: boolean, results: unknown }>}
2163
+ */
2164
+ async run(reportProgress) {
2165
+ const totalRaw = this.task?.params?.total ?? 10;
2166
+ const delayRaw = this.task?.params?.delay ?? 1e3;
2167
+ const nameRaw = this.task?.params?.name;
2168
+ const total = Number(totalRaw);
2169
+ const delay = Number(delayRaw);
2170
+ const name = typeof nameRaw === "string" && nameRaw.trim() ? nameRaw.trim() : "sampleProcess";
2171
+ const errors = [];
1489
2172
  if (!Number.isInteger(total) || total <= 0) {
1490
2173
  errors.push('param "total" must be a positive integer');
1491
2174
  }
@@ -1559,7 +2242,7 @@ var TaskSampleProcess = class extends TaskMaster {
1559
2242
  }
1560
2243
  };
1561
2244
 
1562
- // src/tasks/coreTasks/TaskShellCommand.ts
2245
+ // src/tasks/coreTasks/TaskShellCommand.js
1563
2246
  var import_node_child_process = require("child_process");
1564
2247
  function runShellCommand(command, cwd) {
1565
2248
  return new Promise((resolve, reject) => {
@@ -1589,7 +2272,27 @@ function runShellCommand(command, cwd) {
1589
2272
  });
1590
2273
  });
1591
2274
  }
1592
- var TaskShellCommand = class extends TaskMaster {
2275
+ var TaskShellCommand = class extends AbstractTask {
2276
+ /**
2277
+ * @param {object} context
2278
+ * @param {Record<string, unknown>} [overrides]
2279
+ * @returns {Promise<{ command: string, cwd?: string }>}
2280
+ */
2281
+ static async resolveCustomParams(context, overrides = {}) {
2282
+ const merged = AbstractTask._mergeTypedParams(context, "task-shell", {
2283
+ command: "string",
2284
+ cwd: "string"
2285
+ }, overrides);
2286
+ const command = typeof merged.command === "string" ? merged.command.trim() : "";
2287
+ if (!command) {
2288
+ throw new ParamError('shellCommand: param "command" must be a non-empty string');
2289
+ }
2290
+ const cwd = typeof merged.cwd === "string" && merged.cwd.trim() ? merged.cwd.trim() : null;
2291
+ return cwd ? { command, cwd } : { command };
2292
+ }
2293
+ /**
2294
+ * @returns {Promise<{ success: boolean, results: unknown }>}
2295
+ */
1593
2296
  async run() {
1594
2297
  const params = this.task?.params;
1595
2298
  const commandRaw = typeof params === "string" ? params : params?.command;
@@ -1638,8 +2341,8 @@ var TaskShellCommand = class extends TaskMaster {
1638
2341
  }
1639
2342
  };
1640
2343
 
1641
- // src/tasks/coreTasks/TaskSystemInfo.ts
1642
- var import_node_os = __toESM(require("os"), 1);
2344
+ // src/tasks/coreTasks/TaskSystemInfo.js
2345
+ var import_node_os2 = __toESM(require("os"), 1);
1643
2346
  var import_promises = __toESM(require("fs/promises"), 1);
1644
2347
  function toGb(valueBytes) {
1645
2348
  return `${(valueBytes / 1024 ** 3).toFixed(2)} GB`;
@@ -1658,13 +2361,22 @@ async function getDiskStats() {
1658
2361
  free: toGb(free)
1659
2362
  };
1660
2363
  }
1661
- var TaskSystemInfo = class extends TaskMaster {
2364
+ var TaskSystemInfo = class extends AbstractTask {
2365
+ /** Same UX expectation as `ping` — short probe, print the result. */
2366
+ static defaultWaitForResult = true;
2367
+ /** systemInfo takes no params. */
2368
+ static async resolveCustomParams() {
2369
+ return null;
2370
+ }
2371
+ /**
2372
+ * @returns {Promise<{ success: boolean, results: unknown }>}
2373
+ */
1662
2374
  async run() {
1663
2375
  try {
1664
- const totalMemory = import_node_os.default.totalmem();
1665
- const freeMemory = import_node_os.default.freemem();
2376
+ const totalMemory = import_node_os2.default.totalmem();
2377
+ const freeMemory = import_node_os2.default.freemem();
1666
2378
  const usedMemory = totalMemory - freeMemory;
1667
- const cpus = import_node_os.default.cpus();
2379
+ const cpus = import_node_os2.default.cpus();
1668
2380
  const cpuUtilization = cpus.map((cpu) => {
1669
2381
  const total = Object.values(cpu.times).reduce((acc, time) => acc + time, 0);
1670
2382
  const usage = (total - cpu.times.idle) / total * 100;
@@ -1690,10 +2402,10 @@ var TaskSystemInfo = class extends TaskMaster {
1690
2402
  utilization: cpuUtilization
1691
2403
  },
1692
2404
  runtime: {
1693
- platform: import_node_os.default.platform(),
1694
- arch: import_node_os.default.arch(),
1695
- uptimeSec: import_node_os.default.uptime(),
1696
- hostname: import_node_os.default.hostname()
2405
+ platform: import_node_os2.default.platform(),
2406
+ arch: import_node_os2.default.arch(),
2407
+ uptimeSec: import_node_os2.default.uptime(),
2408
+ hostname: import_node_os2.default.hostname()
1697
2409
  }
1698
2410
  };
1699
2411
  this.context.logger.info?.(`[TaskSystemInfo] collected system metrics (${this.task.id})`);
@@ -1710,8 +2422,31 @@ var TaskSystemInfo = class extends TaskMaster {
1710
2422
  }
1711
2423
  };
1712
2424
 
1713
- // src/tasks/coreTasks/TaskSumAB.ts
1714
- var TaskSumAB = class extends TaskMaster {
2425
+ // src/tasks/coreTasks/TaskSumAB.js
2426
+ var TaskSumAB = class extends AbstractTask {
2427
+ /** Short, deterministic — wait by default so callers see the sum. */
2428
+ static defaultWaitForResult = true;
2429
+ /**
2430
+ * @param {object} context
2431
+ * @param {Record<string, unknown>} [overrides]
2432
+ * @returns {Promise<{ a: number, b: number }>}
2433
+ */
2434
+ static async resolveCustomParams(context, overrides = {}) {
2435
+ const merged = AbstractTask._mergeTypedParams(context, "task-sumab", {
2436
+ a: "number",
2437
+ b: "number"
2438
+ }, overrides);
2439
+ if (typeof merged.a !== "number" || Number.isNaN(merged.a)) {
2440
+ throw new ParamError(`taskSumAB: param "a" must be a valid number (got ${JSON.stringify(merged.a)})`);
2441
+ }
2442
+ if (typeof merged.b !== "number" || Number.isNaN(merged.b)) {
2443
+ throw new ParamError(`taskSumAB: param "b" must be a valid number (got ${JSON.stringify(merged.b)})`);
2444
+ }
2445
+ return { a: merged.a, b: merged.b };
2446
+ }
2447
+ /**
2448
+ * @returns {Promise<{ success: boolean, results: unknown }>}
2449
+ */
1715
2450
  async run() {
1716
2451
  const a = this.task?.params?.a;
1717
2452
  const b = this.task?.params?.b;
@@ -1742,8 +2477,46 @@ var TaskSumAB = class extends TaskMaster {
1742
2477
  }
1743
2478
  };
1744
2479
 
1745
- // src/tasks/coreTasks/TaskStopRunner.ts
1746
- var TaskStopRunner = class extends TaskMaster {
2480
+ // src/tasks/coreTasks/TaskStopRunner.js
2481
+ var TaskStopRunner = class extends AbstractTask {
2482
+ /**
2483
+ * Stop tasks must target a concrete instance — without `serviceName` the
2484
+ * row would race against any worker on the queue. Layered on top of the
2485
+ * envelope built by {@link AbstractTask.resolveParams}.
2486
+ *
2487
+ * @param {object} context
2488
+ * @param {Record<string, unknown>} [overrides]
2489
+ * @returns {Promise<object>}
2490
+ */
2491
+ static async resolveParams(context, overrides = {}) {
2492
+ const main = await super.resolveParams(context, overrides);
2493
+ if (!main.serviceName) {
2494
+ throw new ParamError(
2495
+ "stop/stopRunner requires --serviceName (registry instance name; optional --serviceGroup, --instanceNumber, --serverName to narrow targeting)"
2496
+ );
2497
+ }
2498
+ return main;
2499
+ }
2500
+ /**
2501
+ * @param {object} context
2502
+ * @param {Record<string, unknown>} [overrides]
2503
+ * @returns {Promise<{ allowanceMs: number }>}
2504
+ */
2505
+ static async resolveCustomParams(context, overrides = {}) {
2506
+ const merged = AbstractTask._mergeTypedParams(context, "task-stop", {
2507
+ allowanceMs: "number default 5000"
2508
+ }, overrides);
2509
+ const allowanceMs = Number(merged.allowanceMs);
2510
+ if (!Number.isFinite(allowanceMs) || allowanceMs < 0) {
2511
+ throw new ParamError(
2512
+ `stopRunner: allowanceMs must be a non-negative number (got ${JSON.stringify(merged.allowanceMs)})`
2513
+ );
2514
+ }
2515
+ return { allowanceMs };
2516
+ }
2517
+ /**
2518
+ * @returns {Promise<{ success: true, results: { stopRunner: true, allowanceMs: number, message: string } }>}
2519
+ */
1747
2520
  async run() {
1748
2521
  const allowanceMs = Number(this.task?.params?.allowanceMs ?? 5e3);
1749
2522
  this.context.logger.warn?.(`[TaskStopRunner] stop requested (allowanceMs=${allowanceMs})`);
@@ -1758,175 +2531,399 @@ var TaskStopRunner = class extends TaskMaster {
1758
2531
  }
1759
2532
  };
1760
2533
 
1761
- // src/tasks/TasksRegistry.ts
2534
+ // src/tasks/coreTasks/TaskGetLogs.js
2535
+ var TaskGetLogs = class extends AbstractTask {
2536
+ /**
2537
+ * @param {object} context
2538
+ * @param {Record<string, unknown>} [overrides]
2539
+ * @returns {Promise<{ source: string, resource: string, tail: number, afterTs?: string }>}
2540
+ */
2541
+ static async resolveCustomParams(context, overrides = {}) {
2542
+ const merged = AbstractTask._mergeTypedParams(context, "task-get-logs", {
2543
+ source: "string",
2544
+ resource: "string",
2545
+ tail: "number default 100",
2546
+ afterTs: "string"
2547
+ }, overrides);
2548
+ const source = typeof merged.source === "string" ? merged.source.trim() : "";
2549
+ const resource = typeof merged.resource === "string" ? merged.resource.trim() : "";
2550
+ if (!source) throw new ParamError('getLogs: param "source" is required');
2551
+ if (!resource) throw new ParamError('getLogs: param "resource" is required');
2552
+ let tail = Number(merged.tail);
2553
+ if (!Number.isFinite(tail) || tail < 1) tail = 100;
2554
+ tail = Math.min(1e4, Math.max(1, Math.floor(tail)));
2555
+ const out = { source, resource, tail };
2556
+ if (typeof merged.afterTs === "string" && merged.afterTs.trim()) {
2557
+ out.afterTs = merged.afterTs.trim();
2558
+ }
2559
+ return out;
2560
+ }
2561
+ /**
2562
+ * @param {unknown} _reportProgress Unused (single-shot read, no progress events).
2563
+ * @returns {Promise<{ success: boolean, results: unknown }>}
2564
+ */
2565
+ async run(_reportProgress) {
2566
+ const p = this.task.params ?? {};
2567
+ const source = String(p.source ?? "").trim();
2568
+ const resource = String(p.resource ?? "").trim();
2569
+ const tail = Math.max(1, Math.min(1e4, Number(p.tail) > 0 ? Number(p.tail) : 100));
2570
+ const afterTs = p.afterTs != null && String(p.afterTs).trim() ? String(p.afterTs).trim() : null;
2571
+ if (!source || !resource) {
2572
+ return {
2573
+ success: false,
2574
+ results: { error: 'getLogs requires params "source" and "resource"' }
2575
+ };
2576
+ }
2577
+ try {
2578
+ const { records, latestTs } = await readTaskIpcLogsSnapshot(this.context, {
2579
+ source,
2580
+ resource,
2581
+ tail,
2582
+ afterTs
2583
+ });
2584
+ return {
2585
+ success: true,
2586
+ results: { records, latestTs, source, resource }
2587
+ };
2588
+ } catch (e) {
2589
+ return {
2590
+ success: false,
2591
+ results: { error: e?.message ?? String(e) }
2592
+ };
2593
+ }
2594
+ }
2595
+ };
2596
+
2597
+ // src/tasks/TasksRegistry.js
1762
2598
  var TasksRegistry = class _TasksRegistry {
1763
- map = {};
2599
+ /**
2600
+ * @param {Record<string, Function>} [initial] Optional seed entries to copy in.
2601
+ */
1764
2602
  constructor(initial) {
2603
+ this.map = {};
1765
2604
  if (initial) {
1766
2605
  this.addMany(initial);
1767
2606
  }
1768
2607
  }
2608
+ /**
2609
+ * Build a registry pre-populated with every core task plus legacy aliases.
2610
+ * Prefer this over `new TasksRegistry()` for anything that wants `ping`/`stop`/etc.
2611
+ *
2612
+ * @returns {TasksRegistry}
2613
+ */
1769
2614
  static withCoreTasks() {
1770
- return new _TasksRegistry().add("ping", TaskPing).add("sampleProcess", TaskSampleProcess).add("shellCommand", TaskShellCommand).add("systemInfo", TaskSystemInfo).add("taskSumAB", TaskSumAB).add("stopRunner", TaskStopRunner).add("stop", TaskStopRunner);
2615
+ return new _TasksRegistry().add("ping", TaskPing).add("sampleProcess", TaskSampleProcess).add("shellCommand", TaskShellCommand).add("systemInfo", TaskSystemInfo).add("info", TaskSystemInfo).add("taskSumAB", TaskSumAB).add("stopRunner", TaskStopRunner).add("stop", TaskStopRunner).add("getLogs", TaskGetLogs);
1771
2616
  }
2617
+ /**
2618
+ * Register a single task class under a name. Overwrites any previous entry.
2619
+ *
2620
+ * @param {string} taskName
2621
+ * @param {Function} taskClass Subclass of `AbstractTask`.
2622
+ * @returns {this}
2623
+ */
1772
2624
  add(taskName, taskClass) {
1773
2625
  this.map[taskName] = taskClass;
1774
2626
  return this;
1775
2627
  }
2628
+ /**
2629
+ * Bulk-register a name → class map. Later calls override earlier ones.
2630
+ *
2631
+ * @param {Record<string, Function>} entries
2632
+ * @returns {this}
2633
+ */
1776
2634
  addMany(entries) {
1777
2635
  for (const [name, klass] of Object.entries(entries)) {
1778
2636
  this.add(name, klass);
1779
2637
  }
1780
2638
  return this;
1781
2639
  }
2640
+ /**
2641
+ * Look up a task class by name. Returns `undefined` when the name is unknown;
2642
+ * the runner treats that as "some other worker may handle this" and skips.
2643
+ *
2644
+ * @param {string} taskName
2645
+ * @returns {Function | undefined}
2646
+ */
1782
2647
  get(taskName) {
1783
2648
  return this.map[taskName];
1784
2649
  }
2650
+ /**
2651
+ * Strict variant of {@link get}: throws {@link ParamError} (with the list
2652
+ * of supported names) when `taskName` is unknown. Use from enqueuer code
2653
+ * paths where an unknown name is a hard CLI/programmer error.
2654
+ *
2655
+ * @param {string} taskName
2656
+ * @returns {Function}
2657
+ */
2658
+ requireClass(taskName) {
2659
+ const TaskClass = taskName ? this.map[taskName] : void 0;
2660
+ if (!TaskClass) {
2661
+ const supported = this.listSupportedTasks().join(", ") || "(none)";
2662
+ throw new ParamError(
2663
+ `Unknown task "${taskName ?? ""}". Supported on this registry: ${supported}`
2664
+ );
2665
+ }
2666
+ return TaskClass;
2667
+ }
2668
+ /**
2669
+ * Dispatcher used by `send-task` / programmatic enqueuers: pick `name`
2670
+ * from `overrides` or `context.params`, look up the class, and delegate
2671
+ * to its static {@link AbstractTask.resolveParams} with `name` seeded into
2672
+ * the overrides. The returned object is shaped for {@link enqueueTask}.
2673
+ *
2674
+ * Validation failures (unknown task, missing required custom params, etc.)
2675
+ * surface as {@link ParamError} so the caller aborts cleanly before any
2676
+ * row is inserted.
2677
+ *
2678
+ * @param {object} context
2679
+ * @param {Record<string, unknown>} [overrides]
2680
+ * @returns {Promise<object>}
2681
+ */
2682
+ async resolveTaskParams(context, overrides = {}) {
2683
+ const overrideName = typeof overrides.name === "string" ? overrides.name.trim() : overrides.name;
2684
+ const fromCli = context.params.get("name", "string");
2685
+ const cliName = typeof fromCli === "string" ? fromCli.trim() : fromCli;
2686
+ const name = overrideName || cliName;
2687
+ if (!name) {
2688
+ throw new ParamError("Task --name is required (e.g. ping, stop, dummyHarvest)");
2689
+ }
2690
+ const TaskClass = this.requireClass(name);
2691
+ return TaskClass.resolveParams(context, { ...overrides, name });
2692
+ }
2693
+ /**
2694
+ * Names of every registered task, sorted alphabetically (useful for CLI output
2695
+ * and allowlist sanity checks).
2696
+ *
2697
+ * @returns {string[]}
2698
+ */
1785
2699
  listSupportedTasks() {
1786
2700
  return Object.keys(this.map).sort();
1787
2701
  }
2702
+ /**
2703
+ * Shallow copy of the internal map, for handing to `addMany` on another registry
2704
+ * or for serialization.
2705
+ *
2706
+ * @returns {Record<string, Function>}
2707
+ */
1788
2708
  toObject() {
1789
2709
  return { ...this.map };
1790
2710
  }
1791
2711
  };
1792
2712
 
1793
- // src/tasks/taskScriptRunner.ts
2713
+ // src/tasks/serviceTaskAllowlist.js
2714
+ var SERVICE_TASK_NAMES = [
2715
+ "ping",
2716
+ "stop",
2717
+ "stopRunner",
2718
+ "shellCommand",
2719
+ "systemInfo",
2720
+ "info",
2721
+ "getLogs"
2722
+ ];
2723
+ function normalizeAllowedTasks(value) {
2724
+ if (!value) return void 0;
2725
+ if (Array.isArray(value)) {
2726
+ const out2 = value.map((v) => String(v).trim()).filter(Boolean);
2727
+ return out2.length ? out2 : void 0;
2728
+ }
2729
+ const out = String(value).split(",").map((v) => v.trim()).filter(Boolean);
2730
+ return out.length ? out : void 0;
2731
+ }
2732
+ function mergeAllowedTasksWithServiceTasks(names) {
2733
+ const set = /* @__PURE__ */ new Set([...SERVICE_TASK_NAMES, ...names ?? []]);
2734
+ return Array.from(set).sort();
2735
+ }
2736
+
2737
+ // src/tasks/taskScriptRunner.js
1794
2738
  var import_node_child_process2 = require("child_process");
2739
+ var MAX_PROGRESS_TEXT_LEN = 4e3;
1795
2740
  function toCliArgs(args = []) {
1796
2741
  return args.filter((a) => typeof a === "string" && a.length > 0);
1797
2742
  }
1798
2743
  function formatChildLogPrefix(task) {
1799
- return `${task.task}:${task.id.slice(0, 8)}${task.opid ? `:${task.opid}` : ""}`;
2744
+ return `${task.name}:${task.id.slice(0, 8)}${task.opid ? `:${task.opid}` : ""}`;
1800
2745
  }
1801
- async function runNodeTaskScript(context, options) {
1802
- const cliArgs = toCliArgs(["--route=ipc", "--mode=json", ...options.args || []]);
2746
+ function isProgressPayload(payload) {
2747
+ if (!payload || typeof payload !== "object") return false;
2748
+ if (payload.level !== "progress") return false;
2749
+ const count = Number(payload.count);
2750
+ const total = Number(payload.total);
2751
+ return Number.isFinite(count) && Number.isFinite(total) && total > 0;
2752
+ }
2753
+ function formatProgressText(payload, fallbackPrefix) {
2754
+ const pfx = payload.prefix ? `${payload.prefix} ` : fallbackPrefix ? `${fallbackPrefix} ` : "";
2755
+ const label = typeof payload.message === "string" && payload.message ? `${payload.message} ` : "";
2756
+ return `${pfx}${label}${payload.count}/${payload.total}`;
2757
+ }
2758
+ function forwardChildLogToParent(context, prefix, message) {
2759
+ if (!message || typeof message !== "object") return;
2760
+ const text = typeof message.message === "string" ? message.message : null;
2761
+ if (!text) return;
2762
+ const level = typeof message.level === "string" ? message.level.toLowerCase() : "";
2763
+ const line = `[child:${prefix}] ${text}`;
2764
+ const logger = context.logger;
2765
+ switch (level) {
2766
+ case "error":
2767
+ case "fatal":
2768
+ logger.error?.(line);
2769
+ return;
2770
+ case "warn":
2771
+ case "warning":
2772
+ logger.warn?.(line);
2773
+ return;
2774
+ case "debug":
2775
+ logger.debug?.(line);
2776
+ return;
2777
+ case "info":
2778
+ default:
2779
+ logger.info?.(line);
2780
+ }
2781
+ }
2782
+ function buildNodeArgs(scriptPath, cliArgs) {
1803
2783
  const inheritedExecArgs = Array.isArray(process.execArgv) ? [...process.execArgv] : [];
1804
2784
  const hasTsRuntimeInParent = inheritedExecArgs.some((arg) => /tsx|ts-node/i.test(arg));
1805
- const nodeArgs = hasTsRuntimeInParent ? [...inheritedExecArgs, options.scriptPath, ...cliArgs] : ["--import", "tsx", options.scriptPath, ...cliArgs];
1806
- const child = (0, import_node_child_process2.spawn)(
1807
- process.execPath,
1808
- nodeArgs,
1809
- {
1810
- cwd: options.cwd || process.cwd(),
1811
- stdio: ["ignore", "pipe", "pipe", "ipc"],
1812
- env: {
1813
- ...process.env,
1814
- TASK_ID: options.task.id,
1815
- TASK_NAME: options.task.task,
1816
- TASK_OPID: options.task.opid || ""
1817
- }
1818
- }
2785
+ return hasTsRuntimeInParent ? [...inheritedExecArgs, scriptPath, ...cliArgs] : ["--import", "tsx", scriptPath, ...cliArgs];
2786
+ }
2787
+ function resolveTasksTableName(context) {
2788
+ return context.tasksQueueName || context.params?.get?.("table") || "tasks";
2789
+ }
2790
+ function announceIpcFileLogsTarget(context, options) {
2791
+ if (!options.ipcFileLogs) return;
2792
+ const logsDir = resolveIpcFileLogsDir(context, options.ipcFileLogs);
2793
+ const enabledRaw = context.params?.get?.("tasksLogsEnabled");
2794
+ const logsEnabled = enabledRaw === void 0 ? true : !!enabledRaw;
2795
+ context.logger.info?.(
2796
+ `[tasks] IPC file logs: ${logsDir}` + (logsEnabled ? "" : " (tasksLogsEnabled=false; not persisted)")
1819
2797
  );
1820
- let stdout = "";
1821
- let stderr = "";
1822
- let workerResult = null;
1823
- let hadErrorMessage = false;
1824
- const prefix = formatChildLogPrefix(options.task);
1825
- const db = context.db;
1826
- const tasksTable = context.params?.get?.("table") || "tasks";
1827
- let progressWriteChain = Promise.resolve();
1828
- let progressCallbackChain = Promise.resolve();
1829
- const updateProgress = (text) => {
1830
- if (!db || !text || !text.trim()) return;
1831
- progressWriteChain = progressWriteChain.then(async () => {
1832
- await db(tasksTable).where({ id: options.task.id }).update({ progress: text.slice(0, 4e3) });
1833
- }).catch((error) => {
1834
- context.logger.warn?.(
1835
- `[tasks] failed to update progress for task=${options.task.id}: ${error?.message ?? String(error)}`
1836
- );
1837
- });
1838
- if (options.onProgress) {
1839
- progressCallbackChain = progressCallbackChain.then(async () => {
1840
- await options.onProgress?.(text.slice(0, 4e3));
1841
- }).catch((error) => {
1842
- context.logger.warn?.(
1843
- `[tasks] reportProgress callback failed for task=${options.task.id}: ${error?.message ?? String(error)}`
1844
- );
2798
+ }
2799
+ function createSerializedQueue() {
2800
+ let chain = Promise.resolve();
2801
+ return {
2802
+ push(fn) {
2803
+ chain = chain.then(fn, () => {
2804
+ }).catch(() => {
2805
+ });
2806
+ return chain;
2807
+ },
2808
+ drain() {
2809
+ return chain.catch(() => {
1845
2810
  });
1846
2811
  }
1847
2812
  };
1848
- const payloadToProgressText = (payload) => {
1849
- if (!payload) return "";
1850
- if (typeof payload === "string") return payload;
1851
- if (typeof payload.message === "string" && payload.level === "progress" && payload.count !== void 0 && payload.total !== void 0) {
1852
- const pfx = payload.prefix ? `${payload.prefix} ` : "";
1853
- return `${pfx}${payload.message} ${payload.count}/${payload.total}`;
1854
- }
1855
- if (typeof payload.message === "string") return payload.message;
1856
- if (payload.level === "progress" && payload.count !== void 0 && payload.total !== void 0) {
1857
- const pfx = payload.prefix ? `${payload.prefix} ` : "";
1858
- return `${pfx}${payload.count}/${payload.total}`;
1859
- }
1860
- return "";
2813
+ }
2814
+ async function runNodeTaskScript(context, options) {
2815
+ const cliArgs = toCliArgs(["--route=ipc", "--mode=json", ...options.args || []]);
2816
+ const nodeArgs = buildNodeArgs(options.scriptPath, cliArgs);
2817
+ const child = (0, import_node_child_process2.spawn)(process.execPath, nodeArgs, {
2818
+ cwd: options.cwd || process.cwd(),
2819
+ stdio: ["ignore", "pipe", "pipe", "ipc"],
2820
+ env: {
2821
+ ...process.env,
2822
+ TASK_ID: options.task.id,
2823
+ TASK_NAME: options.task.name,
2824
+ TASK_OPID: options.task.opid || ""
2825
+ }
2826
+ });
2827
+ announceIpcFileLogsTarget(context, options);
2828
+ const prefix = formatChildLogPrefix(options.task);
2829
+ const tasksTable = resolveTasksTableName(context);
2830
+ const progressQueue = createSerializedQueue();
2831
+ const state = {
2832
+ stdout: "",
2833
+ stderr: "",
2834
+ workerResult: null,
2835
+ hadErrorMessage: false
1861
2836
  };
1862
- child.stdout.on("data", (chunk) => {
2837
+ const writeProgress = (text) => {
2838
+ const trimmed = typeof text === "string" ? text.slice(0, MAX_PROGRESS_TEXT_LEN) : "";
2839
+ if (!trimmed) return;
2840
+ progressQueue.push(async () => {
2841
+ const db = context.db;
2842
+ if (db) {
2843
+ try {
2844
+ await db(tasksTable).where({ id: options.task.id }).update({ progress: trimmed });
2845
+ } catch (error) {
2846
+ context.logger.warn?.(
2847
+ `[tasks] failed to update progress for task=${options.task.id}: ${error?.message ?? String(error)}`
2848
+ );
2849
+ }
2850
+ }
2851
+ if (options.onProgress) {
2852
+ try {
2853
+ await options.onProgress(trimmed);
2854
+ } catch (error) {
2855
+ context.logger.warn?.(
2856
+ `[tasks] reportProgress callback failed for task=${options.task.id}: ${error?.message ?? String(error)}`
2857
+ );
2858
+ }
2859
+ }
2860
+ });
2861
+ };
2862
+ child.stdout?.on("data", (chunk) => {
1863
2863
  const text = String(chunk);
1864
- stdout += text;
2864
+ state.stdout += text;
1865
2865
  if (text.trim()) {
1866
2866
  context.logger.info?.(`[child:${prefix}] ${text.trimEnd()}`);
1867
- updateProgress(text.trim().replace(/\s+/g, " ").slice(0, 400));
1868
2867
  }
1869
2868
  });
1870
- child.stderr.on("data", (chunk) => {
2869
+ child.stderr?.on("data", (chunk) => {
1871
2870
  const text = String(chunk);
1872
- stderr += text;
2871
+ state.stderr += text;
1873
2872
  if (text.trim()) {
1874
2873
  context.logger.warn?.(`[child:${prefix}] ${text.trimEnd()}`);
1875
2874
  }
1876
2875
  });
1877
2876
  child.on("message", (message) => {
1878
2877
  if (message && typeof message === "object" && "__taskWorkerResult" in message) {
1879
- workerResult = message.__taskWorkerResult;
2878
+ state.workerResult = message.__taskWorkerResult;
1880
2879
  return;
1881
2880
  }
1882
2881
  if (message && typeof message === "object") {
1883
2882
  const level = typeof message.level === "string" ? message.level.toLowerCase() : "";
1884
2883
  if (level === "error" || level === "fatal") {
1885
- hadErrorMessage = true;
2884
+ state.hadErrorMessage = true;
1886
2885
  }
1887
2886
  }
1888
- appendTaskIpcLog(context, options.task, message);
1889
- const progressText = payloadToProgressText(message);
1890
- if (progressText) {
1891
- updateProgress(progressText);
1892
- if (typeof message === "object" && message?.level === "progress" && message.count !== void 0 && message.total !== void 0) {
1893
- const countNum = Number(String(message.count).trim());
1894
- const totalNum = Number(message.total);
1895
- if (Number.isFinite(countNum) && Number.isFinite(totalNum) && totalNum > 0) {
1896
- context.logger.progress(message.message || "progress", {
1897
- prefix: message.prefix || prefix,
1898
- count: countNum,
1899
- total: totalNum
1900
- });
1901
- } else {
1902
- context.logger.info?.(`[child:${prefix}] ${progressText}`);
1903
- }
1904
- } else {
1905
- context.logger.info?.(`[child:${prefix}] ${progressText}`);
1906
- }
2887
+ appendTaskIpcLog(context, options.task, message, options.ipcFileLogs);
2888
+ try {
2889
+ options.onChildIpcMessage?.(message);
2890
+ } catch (e) {
2891
+ context.logger.warn?.(`[tasks] onChildIpcMessage failed: ${e?.message ?? String(e)}`);
1907
2892
  }
2893
+ if (isProgressPayload(message)) {
2894
+ context.logger.progress(message.message || "progress", {
2895
+ prefix: message.prefix || prefix,
2896
+ count: Number(message.count),
2897
+ total: Number(message.total)
2898
+ });
2899
+ writeProgress(formatProgressText(message, prefix));
2900
+ return;
2901
+ }
2902
+ forwardChildLogToParent(context, prefix, message);
1908
2903
  });
1909
2904
  return await new Promise((resolve, reject) => {
1910
2905
  child.on("error", (error) => reject(error));
1911
2906
  child.on("close", (exitCode, signal) => {
1912
- Promise.allSettled([progressWriteChain, progressCallbackChain]).finally(() => {
2907
+ void (async () => {
2908
+ await flushTaskIpcLogs(context);
2909
+ await progressQueue.drain();
1913
2910
  resolve({
1914
2911
  exitCode,
1915
2912
  signal,
1916
- stdout: stdout.trim(),
1917
- stderr: stderr.trim(),
1918
- workerResult,
1919
- hadErrorMessage
2913
+ stdout: state.stdout.trim(),
2914
+ stderr: state.stderr.trim(),
2915
+ workerResult: state.workerResult,
2916
+ hadErrorMessage: state.hadErrorMessage
1920
2917
  });
1921
- });
2918
+ })();
1922
2919
  });
1923
2920
  });
1924
2921
  }
1925
2922
 
1926
- // src/tasks/index.ts
2923
+ // src/tasks/index.js
1927
2924
  var LOCKED_BY_ERROR_MESSAGE = "locked by error";
1928
2925
  var defaultTasksRegistry = TasksRegistry.withCoreTasks();
1929
- function getDb2(context) {
2926
+ function getDb3(context) {
1930
2927
  const db = context.db;
1931
2928
  if (!db) {
1932
2929
  throw new Error("Tasks component requires context.db. Initialize DB first and attach to context.");
@@ -1938,22 +2935,13 @@ function normalizeRegistry(registry) {
1938
2935
  if (registry instanceof TasksRegistry) return registry;
1939
2936
  return new TasksRegistry().addMany(registry);
1940
2937
  }
1941
- function normalizeAllowedTasks(value) {
1942
- if (!value) return void 0;
1943
- if (Array.isArray(value)) {
1944
- const out2 = value.map((v) => String(v).trim()).filter(Boolean);
1945
- return out2.length ? out2 : void 0;
1946
- }
1947
- const out = String(value).split(",").map((v) => v.trim()).filter(Boolean);
1948
- return out.length ? out : void 0;
1949
- }
1950
- async function enqueueStopTask(context, target, queue = "tasks", allowanceMs = 5e3) {
2938
+ async function enqueueStopTask(context, serviceGroup, queueName = "tasks", allowanceMs = 5e3) {
1951
2939
  return enqueueTask(context, {
1952
- queue,
1953
- target,
1954
- task: "stopRunner",
2940
+ queueName,
2941
+ name: "stopRunner",
1955
2942
  params: { allowanceMs },
1956
- priority: 1e6
2943
+ priority: 0,
2944
+ serviceGroup
1957
2945
  });
1958
2946
  }
1959
2947
  async function signalRunningTasksStop(context, runningTaskInstances, allowanceMs) {
@@ -1970,19 +2958,21 @@ async function signalRunningTasksStop(context, runningTaskInstances, allowanceMs
1970
2958
  context.emitter.emit("stop", allowanceMs);
1971
2959
  }
1972
2960
  async function executeClaimedTask(context, tasksTable, historyTable, row, registry, runningTaskInstances) {
1973
- const db = getDb2(context);
1974
- const taskName = row.task;
2961
+ const db = getDb3(context);
2962
+ const taskName = row.name;
1975
2963
  const TaskClass = registry.get(taskName);
1976
- const { paused_at: _pausedAt, ...rowForHistory } = row;
1977
2964
  if (!TaskClass) {
1978
2965
  const err = { message: `Unknown task "${taskName}"` };
1979
- await db(historyTable).insert({
1980
- ...rowForHistory,
1981
- completed_at: /* @__PURE__ */ new Date(),
1982
- success: false,
1983
- params: toJsonColumn(row.params),
1984
- results: toJsonColumn(err)
1985
- });
2966
+ await db(historyTable).insert(
2967
+ taskHistoryInsertFromQueueRow(row, {
2968
+ completed_at: /* @__PURE__ */ new Date(),
2969
+ success: false,
2970
+ status: "failed",
2971
+ status_changed_at: db.fn.now(),
2972
+ params: toJsonColumn(row.params),
2973
+ results: toJsonColumn(err)
2974
+ })
2975
+ );
1986
2976
  if (row.schedule) {
1987
2977
  await db(tasksTable).where({ id: row.id }).update({
1988
2978
  started_at: null,
@@ -1990,7 +2980,8 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
1990
2980
  success: false,
1991
2981
  results: toJsonColumn(err),
1992
2982
  past_due: null,
1993
- paused_at: db.fn.now(),
2983
+ status: "paused",
2984
+ status_changed_at: db.fn.now(),
1994
2985
  progress: LOCKED_BY_ERROR_MESSAGE
1995
2986
  });
1996
2987
  } else {
@@ -2017,13 +3008,16 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
2017
3008
  } finally {
2018
3009
  runningTaskInstances.delete(row.id);
2019
3010
  }
2020
- await db(historyTable).insert({
2021
- ...rowForHistory,
2022
- completed_at: /* @__PURE__ */ new Date(),
2023
- success,
2024
- params: toJsonColumn(row.params),
2025
- results: toJsonColumn(results)
2026
- });
3011
+ await db(historyTable).insert(
3012
+ taskHistoryInsertFromQueueRow(row, {
3013
+ completed_at: /* @__PURE__ */ new Date(),
3014
+ success,
3015
+ status: success ? "completed" : "failed",
3016
+ status_changed_at: db.fn.now(),
3017
+ params: toJsonColumn(row.params),
3018
+ results: toJsonColumn(results)
3019
+ })
3020
+ );
2027
3021
  if (!success) {
2028
3022
  const dbName = String(context?.params?.get?.("dbName") || "local");
2029
3023
  const tableName = String(context?.params?.get?.("table") || "tasks");
@@ -2038,19 +3032,32 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
2038
3032
  const rerunCommand = results && typeof results === "object" && results.rerunCommand ? results.rerunCommand : fallbackRecoverCommand;
2039
3033
  appendTaskIpcLog(context, row, {
2040
3034
  level: "error",
2041
- message: `[tasks] task failed: ${row.task} id=${row.id}. Once problem is fixed, re-run: ${rerunCommand}`,
3035
+ message: `[tasks] task failed: ${row.name} id=${row.id}. Once problem is fixed, re-run: ${rerunCommand}`,
2042
3036
  details: results
2043
3037
  });
2044
3038
  }
2045
3039
  if (row.schedule) {
2046
3040
  if (success) {
3041
+ let nextRunAt = null;
3042
+ try {
3043
+ nextRunAt = nextTimeMatch(row.schedule, /* @__PURE__ */ new Date());
3044
+ } catch (e) {
3045
+ context.logger?.warn?.(`[tasks] nextTimeMatch after success for task ${row.id}: ${e?.message ?? String(e)}`);
3046
+ }
2047
3047
  await db(tasksTable).where({ id: row.id }).update({
2048
3048
  started_at: null,
2049
3049
  completed_at: /* @__PURE__ */ new Date(),
2050
3050
  success,
2051
3051
  results: toJsonColumn(results),
2052
3052
  progress: null,
2053
- past_due: null
3053
+ past_due: null,
3054
+ status: "idle",
3055
+ status_changed_at: db.fn.now(),
3056
+ next_run_at: nextRunAt,
3057
+ // Claim overwrites these; clear so idle rows stay “any worker” (see claimNextRunnableTask).
3058
+ service_name: null,
3059
+ server_name: null,
3060
+ instance_number: null
2054
3061
  });
2055
3062
  } else {
2056
3063
  await db(tasksTable).where({ id: row.id }).update({
@@ -2058,7 +3065,8 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
2058
3065
  completed_at: /* @__PURE__ */ new Date(),
2059
3066
  success,
2060
3067
  results: toJsonColumn(results),
2061
- paused_at: db.fn.now(),
3068
+ status: "paused",
3069
+ status_changed_at: db.fn.now(),
2062
3070
  progress: LOCKED_BY_ERROR_MESSAGE,
2063
3071
  past_due: null
2064
3072
  });
@@ -2070,209 +3078,402 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
2070
3078
  const stopAllowanceMs = stopRunnerRequested ? Number(results.allowanceMs ?? 5e3) : 0;
2071
3079
  return { stopRunnerRequested, stopAllowanceMs };
2072
3080
  }
2073
- async function claimNextRunnableTask(context, tasksTable, target, registry, scanLimit, taskNames) {
2074
- const db = getDb2(context);
2075
- 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);
3081
+ function shuffleTaskRowsInPlace(rows) {
3082
+ for (let i = rows.length - 1; i > 0; i--) {
3083
+ const j = Math.floor(Math.random() * (i + 1));
3084
+ const t = rows[i];
3085
+ rows[i] = rows[j];
3086
+ rows[j] = t;
3087
+ }
3088
+ }
3089
+ async function claimNextRunnableTask(context, tasksTable, serviceGroup, registry, scanLimit, taskNames, runnerIdentity) {
3090
+ const db = getDb3(context);
3091
+ let query = db(tasksTable).where({ status: "idle" }).where(function() {
3092
+ this.whereNull("service_group").orWhere({ service_group: serviceGroup });
3093
+ }).orderByRaw("CASE WHEN past_due IS NULL THEN 1 ELSE 0 END ASC").orderBy([{ column: "priority", order: "asc" }]).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);
2076
3094
  if (taskNames && taskNames.length > 0) {
2077
- query = query.whereIn("task", taskNames);
3095
+ query = query.whereIn("name", taskNames);
3096
+ }
3097
+ if (runnerIdentity) {
3098
+ query = query.where(function() {
3099
+ this.whereNull("service_name").orWhere({ service_name: runnerIdentity.service_name });
3100
+ }).where(function() {
3101
+ this.whereNull("instance_number").orWhere({ instance_number: runnerIdentity.instance_number });
3102
+ }).where(function() {
3103
+ this.whereNull("server_name").orWhere({ server_name: runnerIdentity.server_name });
3104
+ });
3105
+ } else {
3106
+ query = query.whereNull("service_name").whereNull("instance_number").whereNull("server_name");
2078
3107
  }
2079
3108
  const candidates = await query;
3109
+ shuffleTaskRowsInPlace(candidates);
2080
3110
  for (const row of candidates) {
2081
3111
  if (!row.past_due && row.schedule && !timeMatcher(row.schedule)) {
2082
3112
  continue;
2083
3113
  }
2084
- const TaskClass = registry.get(row.task);
2085
- if (TaskClass) {
2086
- const taskInstance = new TaskClass(context, row);
2087
- const reason = taskInstance.cantRunReason ? await taskInstance.cantRunReason() : null;
2088
- if (reason) {
2089
- if (!row.past_due) {
2090
- await db(tasksTable).where({ id: row.id }).update({
2091
- past_due: db.fn.now(),
2092
- progress: String(reason)
2093
- });
2094
- }
2095
- continue;
3114
+ const TaskClass = registry.get(row.name);
3115
+ if (!TaskClass) {
3116
+ continue;
3117
+ }
3118
+ const taskInstance = new TaskClass(context, row);
3119
+ const reason = taskInstance.cantRunReason ? await taskInstance.cantRunReason() : null;
3120
+ if (reason) {
3121
+ if (!row.past_due) {
3122
+ await db(tasksTable).where({ id: row.id }).update({
3123
+ past_due: db.fn.now(),
3124
+ progress: String(reason)
3125
+ });
2096
3126
  }
3127
+ continue;
2097
3128
  }
2098
- const updated = await db(tasksTable).where({ id: row.id }).whereNull("started_at").whereNull("paused_at").update({ started_at: db.fn.now() }).returning("*");
3129
+ const claimPatch = {
3130
+ started_at: db.fn.now(),
3131
+ status: "running",
3132
+ status_changed_at: db.fn.now()
3133
+ };
3134
+ if (runnerIdentity) {
3135
+ claimPatch.service_name = runnerIdentity.service_name;
3136
+ claimPatch.server_name = runnerIdentity.server_name;
3137
+ claimPatch.instance_number = runnerIdentity.instance_number;
3138
+ }
3139
+ const updated = await db(tasksTable).where({ id: row.id, status: "idle" }).update(claimPatch).returning("*");
2099
3140
  const claimed = Array.isArray(updated) ? updated[0] : null;
2100
3141
  if (claimed) return claimed;
2101
3142
  }
2102
3143
  return null;
2103
3144
  }
2104
3145
  async function runTasksLoop(context, options) {
2105
- const queue = options.queue ?? "tasks";
3146
+ const queueName = options.queueName ?? "tasks";
2106
3147
  const target = options.target;
2107
3148
  const pollMs = options.pollMs ?? 1e3;
2108
- const maxParallel = options.maxParallel ?? 1;
3149
+ const claimJitterMs = options.claimJitterMs ?? 0;
3150
+ const maxParallel = options.maxParallel ?? 32;
2109
3151
  const scanLimit = options.scanLimit ?? 100;
2110
3152
  const allowedTasks = normalizeAllowedTasks(options.allowedTasks);
2111
3153
  const registry = normalizeRegistry(options.registry);
2112
- const { tasksTable, historyTable } = queueToTableNames(queue);
3154
+ const { tasksTable, historyTable } = queueToTableNames(queueName);
2113
3155
  if (!target) throw new Error("runTasksLoop: target is required");
3156
+ context.tasksQueueName = queueName;
2114
3157
  const runningPromises = /* @__PURE__ */ new Set();
2115
3158
  const runningTaskInstances = /* @__PURE__ */ new Map();
2116
3159
  let runningStopControlPromise = null;
2117
3160
  let stopRequested = false;
2118
3161
  let stopAllowanceMs = 5e3;
2119
- context.__tasksRunnerStop = false;
2120
- while (!context.isStop() && !stopRequested && !context.__tasksRunnerStop) {
2121
- if (!runningStopControlPromise) {
2122
- const claimedStopTask = await claimNextRunnableTask(
2123
- context,
2124
- tasksTable,
2125
- target,
2126
- registry,
2127
- 10,
2128
- ["stopRunner", "stop"]
2129
- );
2130
- if (claimedStopTask) {
2131
- runningStopControlPromise = executeClaimedTask(
3162
+ context.tasksRunnerStop = false;
3163
+ let registryReg = null;
3164
+ let registryInterval = null;
3165
+ let runnerIdentity = null;
3166
+ const hbGroup = options.runnerServiceGroup?.trim();
3167
+ if (hbGroup) {
3168
+ const hbIntervalMs = options.runnerHeartbeatIntervalMs ?? 1e4;
3169
+ const staleMs = options.runnerHeartbeatStaleMs ?? 45e3;
3170
+ const defaultMeta = {
3171
+ component: "tasks-runner",
3172
+ allowedTasks: allowedTasks?.length ? allowedTasks.join(",") : "all"
3173
+ };
3174
+ registryReg = await registerInServicesRegistry(context, {
3175
+ queueName,
3176
+ target,
3177
+ serviceGroup: hbGroup,
3178
+ serviceName: options.runnerServiceName,
3179
+ instanceNumber: options.runnerInstanceNumber,
3180
+ staleMs,
3181
+ groupMaxInstances: options.runnerGroupMaxInstances,
3182
+ enforceMaxInstances: options.runnerEnforceMaxInstances ?? true,
3183
+ metadata: options.runnerMetadata ?? defaultMeta
3184
+ });
3185
+ runnerIdentity = {
3186
+ service_name: registryReg.serviceName,
3187
+ server_name: import_node_os3.default.hostname(),
3188
+ instance_number: registryReg.instanceNumber
3189
+ };
3190
+ registryInterval = setInterval(() => {
3191
+ void touchServicesRegistry(context, registryReg).catch((err) => {
3192
+ context.logger.warn?.(`[services-registry] touch failed: ${err?.message ?? String(err)}`);
3193
+ });
3194
+ }, hbIntervalMs);
3195
+ }
3196
+ try {
3197
+ while (!context.isStop() && !stopRequested && context.tasksRunnerStop !== true) {
3198
+ if (!runningStopControlPromise) {
3199
+ const claimedStopTask = await claimNextRunnableTask(
2132
3200
  context,
2133
3201
  tasksTable,
2134
- historyTable,
2135
- claimedStopTask,
3202
+ target,
2136
3203
  registry,
2137
- runningTaskInstances
2138
- ).then(async (outcome) => {
3204
+ 10,
3205
+ ["stopRunner", "stop"],
3206
+ runnerIdentity
3207
+ );
3208
+ if (claimedStopTask) {
3209
+ runningStopControlPromise = executeClaimedTask(
3210
+ context,
3211
+ tasksTable,
3212
+ historyTable,
3213
+ claimedStopTask,
3214
+ registry,
3215
+ runningTaskInstances
3216
+ ).then(async (outcome) => {
3217
+ if (outcome.stopRunnerRequested && !stopRequested) {
3218
+ stopRequested = true;
3219
+ stopAllowanceMs = outcome.stopAllowanceMs || 5e3;
3220
+ context.tasksRunnerStop = true;
3221
+ await signalRunningTasksStop(context, runningTaskInstances, stopAllowanceMs);
3222
+ }
3223
+ }).finally(() => {
3224
+ runningStopControlPromise = null;
3225
+ });
3226
+ }
3227
+ }
3228
+ if (claimJitterMs > 0) {
3229
+ await sleepMs(Math.floor(Math.random() * (claimJitterMs + 1)));
3230
+ }
3231
+ while (runningPromises.size < maxParallel) {
3232
+ const claimed = await claimNextRunnableTask(
3233
+ context,
3234
+ tasksTable,
3235
+ target,
3236
+ registry,
3237
+ scanLimit,
3238
+ allowedTasks,
3239
+ runnerIdentity
3240
+ );
3241
+ if (!claimed) break;
3242
+ const p = executeClaimedTask(context, tasksTable, historyTable, claimed, registry, runningTaskInstances).then(async (outcome) => {
2139
3243
  if (outcome.stopRunnerRequested && !stopRequested) {
2140
3244
  stopRequested = true;
2141
3245
  stopAllowanceMs = outcome.stopAllowanceMs || 5e3;
2142
- context.__tasksRunnerStop = true;
3246
+ context.tasksRunnerStop = true;
2143
3247
  await signalRunningTasksStop(context, runningTaskInstances, stopAllowanceMs);
2144
3248
  }
2145
3249
  }).finally(() => {
2146
- runningStopControlPromise = null;
3250
+ runningPromises.delete(p);
2147
3251
  });
3252
+ runningPromises.add(p);
3253
+ }
3254
+ const wakePromises = [...runningPromises];
3255
+ if (runningStopControlPromise) {
3256
+ wakePromises.push(runningStopControlPromise);
3257
+ }
3258
+ if (wakePromises.length === 0) {
3259
+ await sleepMs(pollMs);
3260
+ } else {
3261
+ const safe = wakePromises.map((p) => p.catch(() => void 0));
3262
+ await Promise.race([sleepMs(pollMs), Promise.race(safe)]);
2148
3263
  }
2149
3264
  }
2150
- while (runningPromises.size < maxParallel) {
2151
- const claimed = await claimNextRunnableTask(
2152
- context,
2153
- tasksTable,
2154
- target,
2155
- registry,
2156
- scanLimit,
2157
- allowedTasks
2158
- );
2159
- if (!claimed) break;
2160
- const p = executeClaimedTask(context, tasksTable, historyTable, claimed, registry, runningTaskInstances).then(async (outcome) => {
2161
- if (outcome.stopRunnerRequested && !stopRequested) {
2162
- stopRequested = true;
2163
- stopAllowanceMs = outcome.stopAllowanceMs || 5e3;
2164
- context.__tasksRunnerStop = true;
2165
- await signalRunningTasksStop(context, runningTaskInstances, stopAllowanceMs);
2166
- }
2167
- }).finally(() => {
2168
- runningPromises.delete(p);
2169
- });
2170
- runningPromises.add(p);
3265
+ if (context.isStop() && !stopRequested) {
3266
+ await signalRunningTasksStop(context, runningTaskInstances, 5e3);
2171
3267
  }
2172
- await sleepMs(pollMs);
2173
- }
2174
- if (context.isStop() && !stopRequested) {
2175
- await signalRunningTasksStop(context, runningTaskInstances, 5e3);
2176
- }
2177
- if (runningPromises.size > 0) {
2178
- if (stopRequested) {
2179
- await Promise.race([
2180
- Promise.allSettled(Array.from(runningPromises)),
2181
- sleepMs(stopAllowanceMs).then(() => {
2182
- context.logger.warn?.(
2183
- `[tasks] stop allowance (${stopAllowanceMs}ms) reached; ${runningPromises.size} task(s) still running`
2184
- );
2185
- })
2186
- ]);
2187
- } else {
2188
- await Promise.allSettled(Array.from(runningPromises));
3268
+ if (runningPromises.size > 0) {
3269
+ if (stopRequested) {
3270
+ await Promise.race([
3271
+ Promise.allSettled(Array.from(runningPromises)),
3272
+ sleepMs(stopAllowanceMs).then(() => {
3273
+ context.logger.warn?.(
3274
+ `[tasks] stop allowance (${stopAllowanceMs}ms) reached; ${runningPromises.size} task(s) still running`
3275
+ );
3276
+ })
3277
+ ]);
3278
+ } else {
3279
+ await Promise.allSettled(Array.from(runningPromises));
3280
+ }
3281
+ }
3282
+ } finally {
3283
+ if (registryInterval) {
3284
+ clearInterval(registryInterval);
3285
+ registryInterval = null;
3286
+ }
3287
+ if (registryReg) {
3288
+ await unregisterServicesRegistry(context, registryReg).catch((err) => {
3289
+ context.logger.warn?.(`[services-registry] unregister failed: ${err?.message ?? String(err)}`);
3290
+ });
3291
+ registryReg = null;
3292
+ delete context.servicesRegistry;
3293
+ delete context.runnerHeartbeat;
2189
3294
  }
2190
3295
  }
2191
3296
  }
2192
3297
  async function waitForTaskResult(context, taskId, options = {}) {
2193
- const db = getDb2(context);
2194
- const queue = options.queue ?? "tasks";
3298
+ const db = getDb3(context);
3299
+ const queueName = options.queueName ?? "tasks";
2195
3300
  const timeoutMs = options.timeoutMs ?? 6e4;
2196
3301
  const pollMs = options.pollMs ?? 500;
2197
- const { tasksTable, historyTable } = queueToTableNames(queue);
3302
+ const { tasksTable, historyTable } = queueToTableNames(queueName);
2198
3303
  const deadline = Date.now() + timeoutMs;
3304
+ const waitStartedAt = /* @__PURE__ */ new Date();
3305
+ let cachedNameOpid = null;
3306
+ async function historySinceWait(name, opid) {
3307
+ let q = db(historyTable).where({ name }).where("completed_at", ">=", waitStartedAt);
3308
+ if (opid == null || opid === "") {
3309
+ q = q.whereNull("opid");
3310
+ } else {
3311
+ q = q.where({ opid });
3312
+ }
3313
+ return await q.orderBy("completed_at", "desc").first();
3314
+ }
2199
3315
  while (Date.now() <= deadline) {
2200
- const done = await db(historyTable).where({ id: taskId }).orderBy("created_at", "desc").first();
2201
- if (done) return done;
3316
+ const legacy = await db(historyTable).where({ id: taskId }).orderBy("created_at", "desc").first();
3317
+ if (legacy) {
3318
+ return legacy;
3319
+ }
2202
3320
  const pending = await db(tasksTable).where({ id: taskId }).first();
2203
- if (!pending) {
2204
- const maybeDone = await db(historyTable).where({ id: taskId }).orderBy("created_at", "desc").first();
2205
- return maybeDone ?? null;
3321
+ if (pending) {
3322
+ cachedNameOpid = { name: pending.name, opid: pending.opid };
3323
+ const done = await historySinceWait(pending.name, pending.opid);
3324
+ if (done) {
3325
+ return done;
3326
+ }
3327
+ } else if (cachedNameOpid) {
3328
+ const done = await historySinceWait(cachedNameOpid.name, cachedNameOpid.opid);
3329
+ if (done) {
3330
+ return done;
3331
+ }
3332
+ return null;
3333
+ } else {
3334
+ return null;
2206
3335
  }
2207
3336
  await sleepMs(pollMs);
2208
3337
  }
2209
3338
  return null;
2210
3339
  }
2211
3340
  var TasksManager = class _TasksManager {
2212
- context;
2213
- queue;
2214
- target;
2215
- recreateTaskTables;
2216
- pollMs;
2217
- maxParallel;
2218
- scanLimit;
2219
- allowedTasks;
2220
- registry;
3341
+ /**
3342
+ * @param {object} context
3343
+ * @param {{
3344
+ * queueName?: string,
3345
+ * target?: string,
3346
+ * recreateTaskTables?: boolean,
3347
+ * pollMs?: number,
3348
+ * claimJitterMs?: number,
3349
+ * maxParallel?: number,
3350
+ * scanLimit?: number,
3351
+ * allowedTasks?: string | string[],
3352
+ * registry?: TasksRegistry | Record<string, Function>,
3353
+ * runnerServiceGroup?: string,
3354
+ * runnerServiceName?: string,
3355
+ * runnerInstanceNumber?: number,
3356
+ * runnerHeartbeatIntervalMs?: number,
3357
+ * runnerHeartbeatStaleMs?: number,
3358
+ * runnerGroupMaxInstances?: number,
3359
+ * runnerEnforceMaxInstances?: boolean,
3360
+ * runnerMetadata?: Record<string, unknown>,
3361
+ * }} [options]
3362
+ */
2221
3363
  constructor(context, options = {}) {
2222
3364
  this.context = context;
2223
- this.queue = options.queue ?? "tasks";
3365
+ this.queueName = options.queueName ?? "tasks";
2224
3366
  this.target = options.target ?? "localRunner";
2225
3367
  this.recreateTaskTables = options.recreateTaskTables ?? false;
2226
3368
  this.pollMs = options.pollMs ?? 1e3;
3369
+ this.claimJitterMs = options.claimJitterMs ?? 0;
2227
3370
  this.maxParallel = options.maxParallel ?? 1;
2228
3371
  this.scanLimit = options.scanLimit ?? 100;
2229
3372
  this.allowedTasks = normalizeAllowedTasks(options.allowedTasks);
2230
3373
  this.registry = normalizeRegistry(options.registry);
3374
+ this.runnerServiceGroup = options.runnerServiceGroup;
3375
+ this.runnerServiceName = options.runnerServiceName;
3376
+ this.runnerInstanceNumber = options.runnerInstanceNumber;
3377
+ this.runnerHeartbeatIntervalMs = options.runnerHeartbeatIntervalMs;
3378
+ this.runnerHeartbeatStaleMs = options.runnerHeartbeatStaleMs;
3379
+ this.runnerGroupMaxInstances = options.runnerGroupMaxInstances;
3380
+ this.runnerEnforceMaxInstances = options.runnerEnforceMaxInstances;
3381
+ this.runnerMetadata = options.runnerMetadata;
2231
3382
  }
3383
+ /**
3384
+ * Preferred factory: reads defaults from `context.params` (module namespace
3385
+ * `"tasks"`), then overlays explicit `options`. Keeps CLI flags, env vars,
3386
+ * and inline options in one consistent resolver.
3387
+ *
3388
+ * @param {object} context
3389
+ * @param {ConstructorParameters<typeof TasksManager>[1]} [options]
3390
+ * @returns {TasksManager}
3391
+ */
2232
3392
  static init(context, options = {}) {
2233
3393
  const defs = {
2234
3394
  table: "string default tasks",
2235
3395
  target: "string default localRunner",
2236
3396
  recreateTaskTables: "boolean default false",
2237
3397
  pollMs: "number default 1000",
3398
+ claimJitterMs: "number default 0",
2238
3399
  maxParallel: "number default 1",
2239
3400
  scanLimit: "number default 100",
2240
- allowedTasks: "string"
3401
+ allowedTasks: "string",
3402
+ runnerServiceGroup: "string",
3403
+ runnerServiceName: "string",
3404
+ runnerInstanceNumber: "number",
3405
+ runnerHeartbeatIntervalMs: "number default 10000",
3406
+ runnerHeartbeatStaleMs: "number default 45000",
3407
+ runnerGroupMaxInstances: "number",
3408
+ runnerEnforceMaxInstances: "boolean default true"
2241
3409
  };
2242
- const discovered = context.params.getAllForModule(defs);
3410
+ const discovered = context.params.getAllForModule("tasks", defs);
2243
3411
  const resolved = {
2244
- queue: discovered.table,
3412
+ queueName: discovered.table,
2245
3413
  target: discovered.target,
2246
3414
  recreateTaskTables: discovered.recreateTaskTables,
2247
3415
  pollMs: discovered.pollMs,
3416
+ claimJitterMs: discovered.claimJitterMs,
2248
3417
  maxParallel: discovered.maxParallel,
2249
3418
  scanLimit: discovered.scanLimit,
2250
3419
  allowedTasks: discovered.allowedTasks,
3420
+ runnerServiceGroup: discovered.runnerServiceGroup,
3421
+ runnerServiceName: discovered.runnerServiceName,
3422
+ runnerInstanceNumber: discovered.runnerInstanceNumber,
3423
+ runnerHeartbeatIntervalMs: discovered.runnerHeartbeatIntervalMs,
3424
+ runnerHeartbeatStaleMs: discovered.runnerHeartbeatStaleMs,
3425
+ runnerGroupMaxInstances: discovered.runnerGroupMaxInstances,
3426
+ runnerEnforceMaxInstances: discovered.runnerEnforceMaxInstances,
2251
3427
  ...options
2252
3428
  };
2253
3429
  return new _TasksManager(context, resolved);
2254
3430
  }
3431
+ /**
3432
+ * Idempotently ensure the three backing tables exist for this queue.
3433
+ *
3434
+ * @param {{ recreate?: boolean }} [options]
3435
+ * @returns {Promise<void>}
3436
+ */
2255
3437
  async ensureTaskTables(options = {}) {
2256
3438
  await ensureTaskTables(this.context, {
2257
- queue: this.queue,
3439
+ queueName: this.queueName,
2258
3440
  recreate: options.recreate ?? this.recreateTaskTables
2259
3441
  });
2260
3442
  }
3443
+ /**
3444
+ * Start the runner loop using this manager's resolved config. Per-call
3445
+ * options override the stored defaults, but `runnerMetadata` still falls
3446
+ * through when omitted.
3447
+ *
3448
+ * @param {Partial<ConstructorParameters<typeof TasksManager>[1]>} [options]
3449
+ * @returns {Promise<void>}
3450
+ */
2261
3451
  async runTasksLoop(options = {}) {
2262
3452
  await runTasksLoop(this.context, {
2263
- queue: options.queue ?? this.queue,
3453
+ queueName: options.queueName ?? this.queueName,
2264
3454
  target: options.target ?? this.target,
2265
3455
  pollMs: options.pollMs ?? this.pollMs,
3456
+ claimJitterMs: options.claimJitterMs ?? this.claimJitterMs,
2266
3457
  maxParallel: options.maxParallel ?? this.maxParallel,
2267
3458
  scanLimit: options.scanLimit ?? this.scanLimit,
2268
3459
  allowedTasks: options.allowedTasks ?? this.allowedTasks,
2269
- registry: options.registry ?? this.registry
3460
+ registry: options.registry ?? this.registry,
3461
+ runnerServiceGroup: options.runnerServiceGroup ?? this.runnerServiceGroup,
3462
+ runnerServiceName: options.runnerServiceName ?? this.runnerServiceName,
3463
+ runnerInstanceNumber: options.runnerInstanceNumber ?? this.runnerInstanceNumber,
3464
+ runnerHeartbeatIntervalMs: options.runnerHeartbeatIntervalMs ?? this.runnerHeartbeatIntervalMs,
3465
+ runnerHeartbeatStaleMs: options.runnerHeartbeatStaleMs ?? this.runnerHeartbeatStaleMs,
3466
+ runnerGroupMaxInstances: options.runnerGroupMaxInstances ?? this.runnerGroupMaxInstances,
3467
+ runnerEnforceMaxInstances: options.runnerEnforceMaxInstances ?? this.runnerEnforceMaxInstances,
3468
+ runnerMetadata: options.runnerMetadata ?? this.runnerMetadata
2270
3469
  });
2271
3470
  }
2272
3471
  };
2273
3472
  // Annotate the CommonJS export names for ESM import in node:
2274
3473
  0 && (module.exports = {
2275
- TaskMaster,
3474
+ AbstractTask,
3475
+ SERVICE_TASK_NAMES,
3476
+ TaskGetLogs,
2276
3477
  TaskPing,
2277
3478
  TaskSampleProcess,
2278
3479
  TaskShellCommand,
@@ -2282,13 +3483,36 @@ var TasksManager = class _TasksManager {
2282
3483
  TasksManager,
2283
3484
  TasksRegistry,
2284
3485
  appendTaskIpcLog,
3486
+ convertPattern,
2285
3487
  defaultTasksRegistry,
2286
3488
  enqueueStopTask,
2287
3489
  enqueueTask,
2288
3490
  ensureTaskTables,
3491
+ flushTaskIpcLogs,
3492
+ ipcFileLogsTableNameForSourceResource,
3493
+ listAliveRunnerHeartbeats,
3494
+ listServicesRegistry,
3495
+ matchesParsedPattern,
3496
+ mergeAllowedTasksWithServiceTasks,
3497
+ nextTimeMatch,
3498
+ normalizeAllowedTasks,
2289
3499
  queueToTableNames,
3500
+ readTaskIpcLogsSnapshot,
3501
+ registerInServicesRegistry,
3502
+ registerRunnerHeartbeat,
3503
+ resolveAsterisks,
3504
+ resolveIpcFileLogsDir,
3505
+ resolveRanges,
3506
+ resolveSteps,
2290
3507
  runNodeTaskScript,
2291
3508
  runTasksLoop,
3509
+ taskHistoryInsertFromQueueRow,
3510
+ timeMatcher,
3511
+ touchRunnerHeartbeat,
3512
+ touchServicesRegistry,
3513
+ unregisterRunnerHeartbeat,
3514
+ unregisterServicesRegistry,
3515
+ updateServicesRegistryMetadata,
2292
3516
  updateTaskProgress,
2293
3517
  waitForTaskResult
2294
3518
  });