@nmakarov/cli-toolkit 0.21.0 → 0.25.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 (68) hide show
  1. package/dist/args.cjs +1 -4
  2. package/dist/args.cjs.map +1 -1
  3. package/dist/args.js +1 -1
  4. package/dist/args.js.map +1 -1
  5. package/dist/cli-runner.cjs +1370 -623
  6. package/dist/cli-runner.cjs.map +1 -1
  7. package/dist/cli-runner.js +1415 -667
  8. package/dist/cli-runner.js.map +1 -1
  9. package/dist/db.cjs +173 -158
  10. package/dist/db.cjs.map +1 -1
  11. package/dist/db.js +172 -151
  12. package/dist/db.js.map +1 -1
  13. package/dist/errors.cjs +2 -2
  14. package/dist/errors.cjs.map +1 -1
  15. package/dist/errors.js +2 -1
  16. package/dist/errors.js.map +1 -1
  17. package/dist/filedatabase.cjs +19 -19
  18. package/dist/filedatabase.cjs.map +1 -1
  19. package/dist/filedatabase.js +19 -16
  20. package/dist/filedatabase.js.map +1 -1
  21. package/dist/http-client.cjs +9 -11
  22. package/dist/http-client.cjs.map +1 -1
  23. package/dist/http-client.js +10 -9
  24. package/dist/http-client.js.map +1 -1
  25. package/dist/http-client2.cjs +34 -37
  26. package/dist/http-client2.cjs.map +1 -1
  27. package/dist/http-client2.js +34 -34
  28. package/dist/http-client2.js.map +1 -1
  29. package/dist/index.cjs +1831 -713
  30. package/dist/index.cjs.map +1 -1
  31. package/dist/index.js +1837 -713
  32. package/dist/index.js.map +1 -1
  33. package/dist/init.cjs +93 -68
  34. package/dist/init.cjs.map +1 -1
  35. package/dist/init.js +108 -82
  36. package/dist/init.js.map +1 -1
  37. package/dist/logger.cjs +5 -5
  38. package/dist/logger.cjs.map +1 -1
  39. package/dist/logger.js +5 -4
  40. package/dist/logger.js.map +1 -1
  41. package/dist/mock-server.cjs +21 -33
  42. package/dist/mock-server.cjs.map +1 -1
  43. package/dist/mock-server.js +21 -28
  44. package/dist/mock-server.js.map +1 -1
  45. package/dist/params.cjs +18 -9
  46. package/dist/params.cjs.map +1 -1
  47. package/dist/params.js +18 -6
  48. package/dist/params.js.map +1 -1
  49. package/dist/s3.cjs +286 -0
  50. package/dist/s3.cjs.map +1 -0
  51. package/dist/s3.js +273 -0
  52. package/dist/s3.js.map +1 -0
  53. package/dist/screen.cjs +34 -39
  54. package/dist/screen.cjs.map +1 -1
  55. package/dist/screen.js +48 -46
  56. package/dist/screen.js.map +1 -1
  57. package/dist/tasks.cjs +1354 -501
  58. package/dist/tasks.cjs.map +1 -1
  59. package/dist/tasks.js +1369 -527
  60. package/dist/tasks.js.map +1 -1
  61. package/dist/utils.cjs +7 -8
  62. package/dist/utils.cjs.map +1 -1
  63. package/dist/utils.js +6 -6
  64. package/dist/utils.js.map +1 -1
  65. package/package.json +32 -47
  66. package/scripts/ssm/{parse-cli.ts → parse-cli.js} +4 -4
  67. package/scripts/ssm/{ssm-admin.ts → ssm-admin.js} +12 -12
  68. package/scripts/ssm/{ssm-pull.ts → ssm-pull.js} +10 -13
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,31 @@ __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,
47
51
  listAliveRunnerHeartbeats: () => listServicesRegistry,
48
52
  listServicesRegistry: () => listServicesRegistry,
53
+ matchesParsedPattern: () => matchesParsedPattern,
54
+ mergeAllowedTasksWithServiceTasks: () => mergeAllowedTasksWithServiceTasks,
55
+ nextTimeMatch: () => nextTimeMatch,
56
+ normalizeAllowedTasks: () => normalizeAllowedTasks,
49
57
  queueToTableNames: () => queueToTableNames,
58
+ readTaskIpcLogsSnapshot: () => readTaskIpcLogsSnapshot,
50
59
  registerInServicesRegistry: () => registerInServicesRegistry,
51
60
  registerRunnerHeartbeat: () => registerInServicesRegistry,
61
+ resolveAsterisks: () => resolveAsterisks,
62
+ resolveIpcFileLogsDir: () => resolveIpcFileLogsDir,
63
+ resolveRanges: () => resolveRanges,
64
+ resolveSteps: () => resolveSteps,
52
65
  runNodeTaskScript: () => runNodeTaskScript,
53
66
  runTasksLoop: () => runTasksLoop,
54
- runnerHeartbeatsTable: () => servicesRegistryTable,
55
- servicesRegistryTable: () => servicesRegistryTable,
67
+ taskHistoryInsertFromQueueRow: () => taskHistoryInsertFromQueueRow,
68
+ timeMatcher: () => timeMatcher,
56
69
  touchRunnerHeartbeat: () => touchServicesRegistry,
57
70
  touchServicesRegistry: () => touchServicesRegistry,
58
71
  unregisterRunnerHeartbeat: () => unregisterServicesRegistry,
@@ -62,8 +75,9 @@ __export(tasks_exports, {
62
75
  waitForTaskResult: () => waitForTaskResult
63
76
  });
64
77
  module.exports = __toCommonJS(tasks_exports);
78
+ var import_node_os3 = __toESM(require("os"), 1);
65
79
 
66
- // src/utils/date-utils.ts
80
+ // src/utils/date-utils.js
67
81
  function isTimestampFolder(folderName) {
68
82
  const isoRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(Z|\.\d{3}Z)$/;
69
83
  if (!isoRegex.test(folderName)) {
@@ -73,7 +87,7 @@ function isTimestampFolder(folderName) {
73
87
  return !isNaN(date.getTime()) && date.getTime() > 0;
74
88
  }
75
89
 
76
- // src/utils/fs-utils.ts
90
+ // src/utils/fs-utils.js
77
91
  var import_fs = __toESM(require("fs"), 1);
78
92
  var import_path = __toESM(require("path"), 1);
79
93
  async function ensurePath(...pathParts) {
@@ -97,7 +111,7 @@ function getFileExtension(dataType) {
97
111
  }
98
112
  }
99
113
 
100
- // src/utils/os-utils.ts
114
+ // src/utils/os-utils.js
101
115
  var import_fs2 = __toESM(require("fs"), 1);
102
116
  var import_path2 = __toESM(require("path"), 1);
103
117
  var import_child_process = require("child_process");
@@ -126,7 +140,7 @@ function getFreeDiskSpace(targetPath) {
126
140
  }
127
141
  }
128
142
 
129
- // src/utils/format-utils.ts
143
+ // src/utils/format-utils.js
130
144
  function bytesToHumanReadable(bytes) {
131
145
  if (bytes === 0) return "0 B";
132
146
  const k = 1024;
@@ -135,7 +149,7 @@ function bytesToHumanReadable(bytes) {
135
149
  return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
136
150
  }
137
151
 
138
- // src/utils/core-utils.ts
152
+ // src/utils/core-utils.js
139
153
  function sleepMs(ms) {
140
154
  return new Promise((resolve) => setTimeout(resolve, ms));
141
155
  }
@@ -144,14 +158,82 @@ function toJsonColumn(value) {
144
158
  return JSON.stringify(value);
145
159
  }
146
160
 
147
- // src/tasks/servicesRegistry.ts
148
- var import_node_crypto2 = require("crypto");
149
- var import_promises = require("fs/promises");
161
+ // src/tasks/servicesRegistry.js
150
162
  var import_node_os = __toESM(require("os"), 1);
151
- var import_node_path = __toESM(require("path"), 1);
152
163
 
153
- // src/tasks/taskUtils.ts
164
+ // src/tasks/taskUtils.js
154
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
155
237
  function getDb(context) {
156
238
  const db = context.db;
157
239
  if (!db) {
@@ -159,113 +241,117 @@ function getDb(context) {
159
241
  }
160
242
  return db;
161
243
  }
162
- function queueToTableNames(queue) {
163
- if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(queue)) {
164
- throw new Error(`Invalid queue name "${queue}". Use letters, numbers, underscore only.`);
165
- }
244
+ function queueToTableNames(queueName) {
166
245
  return {
167
- tasksTable: queue,
168
- historyTable: `${queue}_history`
246
+ tasksTable: queueName,
247
+ historyTable: `${queueName}_history`,
248
+ registryTable: `${queueName}_services_registry`
169
249
  };
170
250
  }
171
- function servicesRegistryTable(queue) {
172
- if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(queue)) {
173
- throw new Error(`Invalid queue name "${queue}". Use letters, numbers, underscore only.`);
174
- }
175
- return `${queue}_services_registry`;
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;
278
+ return {
279
+ ...snapshot,
280
+ ...overrides
281
+ };
176
282
  }
177
283
  async function ensureTaskTables(context, options = {}) {
178
- const queue = options.queue ?? "tasks";
284
+ const queueName = options.queueName ?? "tasks";
179
285
  const recreate = options.recreate ?? false;
180
286
  const db = getDb(context);
181
- const { tasksTable, historyTable } = queueToTableNames(queue);
287
+ const { tasksTable, historyTable, registryTable } = queueToTableNames(queueName);
182
288
  const needsTasks = recreate ? true : !await db.tableExists(tasksTable);
183
289
  const needsHistory = recreate ? true : !await db.tableExists(historyTable);
290
+ const needsRegistry = recreate ? true : !await db.tableExists(registryTable);
184
291
  if (recreate) {
185
292
  await db.schema.dropTableIfExists(historyTable);
186
293
  await db.schema.dropTableIfExists(tasksTable);
294
+ await db.schema.dropTableIfExists(registryTable);
187
295
  }
188
296
  if (needsTasks) {
189
297
  await db.raw(`CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`);
190
298
  await db.schema.createTable(tasksTable, (t) => {
191
- t.uuid("id").primary().defaultTo(db.raw("uuid_generate_v4()"));
192
- t.timestamp("created_at").notNullable().defaultTo(db.fn.now());
193
- t.timestamp("started_at");
194
- t.timestamp("completed_at");
195
- t.integer("priority").notNullable().defaultTo(0);
196
- t.text("schedule");
197
- t.timestamp("past_due").defaultTo(null);
198
- t.text("target").notNullable();
199
- t.text("task").notNullable();
200
- t.json("params");
201
- t.text("opid");
202
- t.timestamp("paused_at").defaultTo(null);
203
- t.text("progress");
204
- t.boolean("success");
205
- t.json("results");
206
- });
207
- await db.schema.alterTable(tasksTable, (t) => {
208
- t.index(["target", "started_at", "created_at"], `${tasksTable}_target_started_created_idx`);
209
- t.index(["target", "past_due", "priority", "created_at"], `${tasksTable}_target_past_due_priority_created_idx`);
210
- t.index(["target", "task"], `${tasksTable}_target_task_idx`);
299
+ defineTasksTable(t, db, tasksTable);
211
300
  });
212
301
  }
213
302
  if (needsHistory) {
214
303
  await db.schema.createTable(historyTable, (t) => {
215
- t.uuid("id").notNullable();
216
- t.timestamp("created_at").notNullable();
217
- t.timestamp("started_at");
218
- t.timestamp("completed_at");
219
- t.integer("priority").notNullable().defaultTo(0);
220
- t.text("schedule");
221
- t.timestamp("past_due").defaultTo(null);
222
- t.text("target").notNullable();
223
- t.text("task").notNullable();
224
- t.json("params");
225
- t.text("opid");
226
- t.text("progress");
227
- t.boolean("success");
228
- t.json("results");
229
- });
230
- await db.schema.alterTable(historyTable, (t) => {
231
- t.index(["target", "created_at"], `${historyTable}_target_created_idx`);
232
- t.index(["task", "created_at"], `${historyTable}_task_created_idx`);
304
+ defineTasksTable(t, db, historyTable);
233
305
  });
234
306
  }
235
- const registryTable = servicesRegistryTable(queue);
236
- const needsRegistry = !await db.tableExists(registryTable);
237
307
  if (needsRegistry) {
238
308
  await db.schema.createTable(registryTable, (t) => {
239
309
  t.uuid("id").primary().defaultTo(db.raw("uuid_generate_v4()"));
240
- t.uuid("instance_id").notNullable().unique();
241
- t.text("queue").notNullable();
310
+ t.text("queue_name").notNullable();
242
311
  t.text("service_group").notNullable();
312
+ t.integer("instance_number").notNullable().defaultTo(1);
243
313
  t.text("service_name").notNullable();
244
- t.text("target").notNullable();
245
- t.text("hostname");
314
+ t.text("server_name").notNullable();
246
315
  t.integer("pid");
247
316
  t.json("metadata");
248
317
  t.timestamp("created_at").notNullable().defaultTo(db.fn.now());
249
318
  t.timestamp("last_seen_at").notNullable().defaultTo(db.fn.now());
250
- t.unique(["queue", "service_name"], `${registryTable}_queue_service_name_uniq`);
251
- t.index(["queue", "service_group", "last_seen_at"], `${registryTable}_queue_group_seen_idx`);
252
- t.index(["queue", "last_seen_at"], `${registryTable}_queue_seen_idx`);
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`);
253
322
  });
254
323
  }
255
324
  }
256
325
  async function enqueueTask(context, options) {
257
326
  const db = getDb(context);
258
- const queue = options.queue ?? "tasks";
259
- const { tasksTable } = queueToTableNames(queue);
327
+ const queueName = options.queueName ?? "tasks";
328
+ const { tasksTable } = queueToTableNames(queueName);
260
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
+ }
261
341
  await db(tasksTable).insert({
262
342
  id,
263
- target: options.target,
264
- task: options.task,
343
+ name,
265
344
  params: toJsonColumn(options.params ?? null),
266
345
  opid: options.opid ?? null,
267
- priority: options.priority ?? 0,
268
- 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()
269
355
  });
270
356
  return id;
271
357
  }
@@ -276,7 +362,7 @@ async function updateTaskProgress(context, tasksTable, taskId, progress) {
276
362
  });
277
363
  }
278
364
 
279
- // src/tasks/servicesRegistry.ts
365
+ // src/tasks/servicesRegistry.js
280
366
  function getDb2(context) {
281
367
  const db = context.db;
282
368
  if (!db) {
@@ -290,7 +376,7 @@ function parseMetadataColumn(value) {
290
376
  if (typeof value === "string") {
291
377
  try {
292
378
  const p = JSON.parse(value);
293
- return p && typeof p === "object" && !Array.isArray(p) ? p : {};
379
+ return p && typeof p === "object" && !Array.isArray(value) ? p : {};
294
380
  } catch {
295
381
  return {};
296
382
  }
@@ -300,6 +386,7 @@ function parseMetadataColumn(value) {
300
386
  var DEFAULT_GROUP_MAX_INSTANCES = {
301
387
  intake: 1,
302
388
  harvest: 1,
389
+ harvester: 0,
303
390
  loader: 0,
304
391
  photos: 0,
305
392
  photosprocessor: 0,
@@ -309,25 +396,6 @@ function sanitizeNamePart(raw) {
309
396
  const s = String(raw || "").trim().replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
310
397
  return s.slice(0, 80) || "runner";
311
398
  }
312
- function identityFilePath(identityDir, queue, serviceGroup) {
313
- const safeQ = sanitizeNamePart(queue);
314
- const safeG = sanitizeNamePart(serviceGroup);
315
- return import_node_path.default.join(identityDir, `${safeQ}_${safeG}.json`);
316
- }
317
- async function readIdentityFile(filePath) {
318
- try {
319
- const text = await (0, import_promises.readFile)(filePath, "utf8");
320
- const parsed = JSON.parse(text);
321
- return parsed && typeof parsed === "object" ? parsed : {};
322
- } catch {
323
- return {};
324
- }
325
- }
326
- async function writeIdentityFile(filePath, data) {
327
- await (0, import_promises.mkdir)(import_node_path.default.dirname(filePath), { recursive: true });
328
- await (0, import_promises.writeFile)(filePath, `${JSON.stringify(data, null, 2)}
329
- `, "utf8");
330
- }
331
399
  function resolveMaxInstances(serviceGroup, override) {
332
400
  if (override !== void 0 && Number.isFinite(override)) {
333
401
  return Math.max(0, Math.floor(Number(override)));
@@ -335,179 +403,190 @@ function resolveMaxInstances(serviceGroup, override) {
335
403
  const g = serviceGroup.trim().toLowerCase();
336
404
  return DEFAULT_GROUP_MAX_INSTANCES[g] ?? 0;
337
405
  }
338
- async function countAliveInGroup(db, registryTable, queue, serviceGroup, staleMs, excludeInstanceId) {
406
+ async function countAliveInGroup(db, registryTable, queueName, serviceGroup, staleMs, excludeRowId) {
339
407
  const cutoff = new Date(Date.now() - staleMs);
340
- let q = db(registryTable).where({ queue, service_group: serviceGroup }).where("last_seen_at", ">", cutoff);
341
- if (excludeInstanceId) {
342
- q = q.whereNot("instance_id", excludeInstanceId);
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);
343
411
  }
344
412
  const row = await q.count("id as count").first();
345
413
  return Number(row?.count ?? 0);
346
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
+ }
347
425
  function isUniqueViolation(error) {
348
426
  const code = error?.code ?? error?.errno;
349
427
  return code === "23505" || String(error?.message || "").includes("duplicate key");
350
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
+ }
351
456
  async function registerInServicesRegistry(context, options) {
352
457
  const db = getDb2(context);
353
- const registryTable = servicesRegistryTable(options.queue);
458
+ const registryTable = queueToTableNames(options.queueName).registryTable;
354
459
  const serviceGroup = options.serviceGroup.trim();
355
460
  if (!serviceGroup) {
356
461
  throw new Error("registerInServicesRegistry: serviceGroup is required");
357
462
  }
358
- const identityPath = identityFilePath(options.identityDir, options.queue, serviceGroup);
359
- let identity = await readIdentityFile(identityPath);
360
- let instanceId = typeof identity.instanceId === "string" && identity.instanceId.trim() ? identity.instanceId.trim() : (0, import_node_crypto2.randomUUID)();
361
- identity.instanceId = instanceId;
362
- await writeIdentityFile(identityPath, identity);
363
- const hostname = import_node_os.default.hostname();
463
+ const serverName = import_node_os.default.hostname();
364
464
  const pid = typeof process.pid === "number" ? process.pid : null;
365
- const meta = toJsonColumn(options.metadata ?? null);
366
- const existing = await db(registryTable).where({ instance_id: instanceId }).first();
367
- if (existing) {
368
- await db(registryTable).where({ instance_id: instanceId }).update({
369
- target: options.target,
370
- hostname,
371
- pid,
372
- metadata: meta,
373
- last_seen_at: db.fn.now()
374
- });
375
- const serviceName = String(existing.service_name);
376
- identity.serviceName = serviceName;
377
- await writeIdentityFile(identityPath, identity);
378
- const reg = {
379
- instanceId,
380
- serviceName,
381
- serviceGroup,
382
- queue: options.queue,
383
- target: options.target,
384
- rowId: String(existing.id)
385
- };
386
- context.servicesRegistry = reg;
387
- context.runnerHeartbeat = reg;
388
- context.logger.info?.(
389
- `[services-registry] resumed instance_id=${instanceId} name=${serviceName} group=${serviceGroup} queue=${options.queue}`
390
- );
391
- return {
392
- instanceId,
393
- serviceName,
394
- serviceGroup,
395
- queue: options.queue,
396
- target: options.target,
397
- rowId: String(existing.id),
398
- registryTable
399
- };
400
- }
465
+ const meta = buildMetadata(options);
466
+ const groupBase = sanitizeNamePart(serviceGroup);
467
+ const hostBase = sanitizeNamePart(serverName);
401
468
  const maxAllowed = resolveMaxInstances(serviceGroup, options.groupMaxInstances);
402
- const aliveOthers = await countAliveInGroup(
403
- db,
404
- registryTable,
405
- options.queue,
406
- serviceGroup,
407
- options.staleMs,
408
- instanceId
409
- );
410
- if (maxAllowed > 0 && aliveOthers >= maxAllowed) {
411
- const msg = `[services-registry] group limit reached for "${serviceGroup}": ${aliveOthers} alive (max ${maxAllowed}, queue=${options.queue}).`;
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}).`;
412
472
  if (options.enforceMaxInstances) {
413
473
  throw new Error(msg);
414
474
  }
415
475
  context.logger.warn?.(`${msg} Starting anyway (runnerEnforceMaxInstances=false).`);
416
476
  }
417
- const explicitName = options.serviceName?.trim();
418
- const fromFile = typeof identity.serviceName === "string" ? identity.serviceName.trim() : "";
419
- const hostBase = sanitizeNamePart(hostname);
420
- const groupBase = sanitizeNamePart(serviceGroup);
421
- const baseCandidates = [];
422
- if (explicitName) baseCandidates.push(sanitizeNamePart(explicitName));
423
- if (fromFile) baseCandidates.push(sanitizeNamePart(fromFile));
424
- baseCandidates.push(`${groupBase}-${hostBase}`);
425
- baseCandidates.push(groupBase);
426
- function* eachServiceNameCandidate(bases) {
427
- const seen = /* @__PURE__ */ new Set();
428
- for (const rawBase of bases) {
429
- const base = sanitizeNamePart(rawBase);
430
- if (!base) continue;
431
- const seq = [base];
432
- for (let n = 2; n <= 500; n++) seq.push(`${base}-${n}`);
433
- for (const c of seq) {
434
- if (seen.has(c)) continue;
435
- seen.add(c);
436
- yield c;
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;
437
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;
438
528
  }
439
- }
440
- let inserted;
441
- for (const candidate of eachServiceNameCandidate(baseCandidates)) {
442
529
  try {
443
530
  const rows = await db(registryTable).insert({
444
- instance_id: instanceId,
445
- queue: options.queue,
531
+ queue_name: options.queueName,
446
532
  service_group: serviceGroup,
447
- service_name: candidate,
448
- target: options.target,
449
- hostname,
533
+ instance_number: instanceNumber,
534
+ service_name: serviceNameRaw,
535
+ server_name: serverName,
450
536
  pid,
451
537
  metadata: meta,
452
- last_seen_at: db.fn.now()
538
+ last_seen_at: db.fn.now(),
539
+ created_at: db.fn.now()
453
540
  }).returning(["id", "service_name"]);
454
541
  const row = Array.isArray(rows) ? rows[0] : rows;
455
- if (row) {
456
- inserted = { id: String(row.id), service_name: String(row.service_name) };
457
- break;
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) : "";
458
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;
459
563
  } catch (error) {
460
564
  if (!isUniqueViolation(error)) {
461
565
  throw error;
462
566
  }
567
+ context.logger.warn?.(`[services-registry] insert race on "${serviceNameRaw}", retrying (attempt ${attempt + 1})`);
463
568
  }
464
569
  }
465
- if (!inserted) {
466
- throw new Error(
467
- `[services-registry] could not allocate a unique service_name for group=${serviceGroup} queue=${options.queue} (too many collisions).`
468
- );
469
- }
470
- identity.serviceName = inserted.service_name;
471
- await writeIdentityFile(identityPath, identity);
472
- const regNew = {
473
- instanceId,
474
- serviceName: inserted.service_name,
475
- serviceGroup,
476
- queue: options.queue,
477
- target: options.target,
478
- rowId: inserted.id
479
- };
480
- context.servicesRegistry = regNew;
481
- context.runnerHeartbeat = regNew;
482
- context.logger.info?.(
483
- `[services-registry] registered instance_id=${instanceId} name=${inserted.service_name} group=${serviceGroup} queue=${options.queue} target=${options.target}`
570
+ throw new Error(
571
+ `[services-registry] could not allocate a registry row for group=${serviceGroup} queue=${options.queueName} after ${MAX_ATTEMPTS} attempts`
484
572
  );
485
- return {
486
- instanceId,
487
- serviceName: inserted.service_name,
488
- serviceGroup,
489
- queue: options.queue,
490
- target: options.target,
491
- rowId: inserted.id,
492
- registryTable
493
- };
494
573
  }
495
574
  async function touchServicesRegistry(context, registration) {
496
575
  const db = getDb2(context);
497
- const hostname = import_node_os.default.hostname();
576
+ const serverName = import_node_os.default.hostname();
498
577
  const pid = typeof process.pid === "number" ? process.pid : null;
499
- await db(registration.registryTable).where({ instance_id: registration.instanceId }).update({
578
+ await db(registration.registryTable).where({ id: registration.rowId }).update({
500
579
  last_seen_at: db.fn.now(),
501
- hostname,
580
+ server_name: serverName,
502
581
  pid
503
582
  });
504
583
  }
505
584
  async function updateServicesRegistryMetadata(context, registration, patch) {
506
585
  const db = getDb2(context);
507
- const row = await db(registration.registryTable).where({ instance_id: registration.instanceId }).first();
586
+ const row = await db(registration.registryTable).where({ id: registration.rowId }).first();
508
587
  const prev = parseMetadataColumn(row?.metadata);
509
588
  const merged = { ...prev, ...patch };
510
- await db(registration.registryTable).where({ instance_id: registration.instanceId }).update({
589
+ await db(registration.registryTable).where({ id: registration.rowId }).update({
511
590
  metadata: toJsonColumn(merged),
512
591
  last_seen_at: db.fn.now()
513
592
  });
@@ -515,14 +594,14 @@ async function updateServicesRegistryMetadata(context, registration, patch) {
515
594
  }
516
595
  async function unregisterServicesRegistry(context, registration) {
517
596
  const db = getDb2(context);
518
- await db(registration.registryTable).where({ instance_id: registration.instanceId }).delete();
519
- context.logger.info?.(`[services-registry] unregistered name=${registration.serviceName} instance_id=${registration.instanceId}`);
597
+ await db(registration.registryTable).where({ id: registration.rowId }).delete();
598
+ context.logger.info?.(`[services-registry] unregistered name=${registration.serviceName} id=${registration.rowId}`);
520
599
  }
521
- async function listServicesRegistry(context, options = { queue: "tasks" }) {
600
+ async function listServicesRegistry(context, options = { queueName: "tasks" }) {
522
601
  const db = getDb2(context);
523
602
  const staleMs = options.staleMs ?? 6e4;
524
603
  const cutoff = new Date(Date.now() - staleMs);
525
- const table = servicesRegistryTable(options.queue);
604
+ const table = queueToTableNames(options.queueName).registryTable;
526
605
  let q = db(table).where("last_seen_at", ">", cutoff).orderBy([{ column: "service_group", order: "asc" }, { column: "service_name", order: "asc" }]);
527
606
  if (options.serviceGroup?.trim()) {
528
607
  q = q.where({ service_group: options.serviceGroup.trim() });
@@ -530,11 +609,14 @@ async function listServicesRegistry(context, options = { queue: "tasks" }) {
530
609
  return await q;
531
610
  }
532
611
 
533
- // src/filedatabase/index.ts
612
+ // src/tasks/taskLogs.js
613
+ var import_node_path = __toESM(require("path"), 1);
614
+
615
+ // src/filedatabase/index.js
534
616
  var import_fs3 = __toESM(require("fs"), 1);
535
617
  var import_path3 = __toESM(require("path"), 1);
536
618
 
537
- // src/filedatabase/serializers.ts
619
+ // src/filedatabase/serializers.js
538
620
  function detectDataType(data) {
539
621
  if (Array.isArray(data)) {
540
622
  return "json-array";
@@ -566,7 +648,7 @@ function deserializeData(rawData, dataType) {
566
648
  }
567
649
  }
568
650
 
569
- // src/errors.ts
651
+ // src/errors.js
570
652
  var FrameworkError = class extends Error {
571
653
  constructor(message) {
572
654
  super(message);
@@ -586,7 +668,7 @@ var FileDatabaseError = class extends FrameworkError {
586
668
  }
587
669
  };
588
670
 
589
- // src/filedatabase/index.ts
671
+ // src/filedatabase/index.js
590
672
  var FileDatabase = class _FileDatabase {
591
673
  basePath;
592
674
  namespace;
@@ -672,7 +754,7 @@ var FileDatabase = class _FileDatabase {
672
754
  if (errors.length) {
673
755
  throw new FileDatabaseError(`[FileDatabase] ${errors.join("; ")}`);
674
756
  }
675
- let parts = [this.basePath, this.namespace];
757
+ const parts = [this.basePath, this.namespace];
676
758
  if (this.tableName) {
677
759
  parts.push(...this.tableName.split("/"));
678
760
  }
@@ -1165,14 +1247,17 @@ var FileDatabase = class _FileDatabase {
1165
1247
  * Prepare the instance for read or write operations
1166
1248
  * This discovers state and sets up internal members based on mode and current data
1167
1249
  */
1168
- async prepare({ write, read, version }) {
1250
+ async prepare(options) {
1251
+ const { write, read, version, deferInitialVersion } = options;
1169
1252
  if (write) {
1170
1253
  if (this.versioned) {
1171
1254
  if (this.currentVersion === null) {
1172
- await this.makeNewVersion();
1173
- this.metadata = this.getDefaultMetadata();
1174
- this.metadata.version = this.currentVersion;
1175
- this.makeNewFile();
1255
+ if (!deferInitialVersion) {
1256
+ await this.makeNewVersion();
1257
+ this.metadata = this.getDefaultMetadata();
1258
+ this.metadata.version = this.currentVersion;
1259
+ this.makeNewFile();
1260
+ }
1176
1261
  } else {
1177
1262
  if (!this.metadata.files.length) {
1178
1263
  this.metadata = await this.figureMetadata(this.currentVersion);
@@ -1282,7 +1367,7 @@ var FileDatabase = class _FileDatabase {
1282
1367
  if (options.forceNewVersion && !this.versioned) {
1283
1368
  throw new FileDatabaseError("Cannot use forceNewVersion in non-versioned mode");
1284
1369
  }
1285
- await this.prepare({ write: true });
1370
+ await this.prepare({ write: true, deferInitialVersion: !!(options.forceNewVersion && this.versioned) });
1286
1371
  const incomingDataType = detectDataType(data);
1287
1372
  this.metadata.dataType = incomingDataType;
1288
1373
  if (options.forceNewVersion) {
@@ -1576,7 +1661,7 @@ var FileDatabase = class _FileDatabase {
1576
1661
  }
1577
1662
  };
1578
1663
 
1579
- // src/tasks/taskLogs.ts
1664
+ // src/tasks/taskLogs.js
1580
1665
  function getLogsState(context) {
1581
1666
  const holder = context;
1582
1667
  if (holder.__tasksLogsState) return holder.__tasksLogsState;
@@ -1629,6 +1714,96 @@ function getLogsState(context) {
1629
1714
  holder.__tasksLogsState = state;
1630
1715
  return state;
1631
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
+ }
1632
1807
  function isErrorPayload(payload) {
1633
1808
  if (!payload) return false;
1634
1809
  if (typeof payload === "object") {
@@ -1648,14 +1823,26 @@ function buildLogRecord(task, payload) {
1648
1823
  ts: (/* @__PURE__ */ new Date()).toISOString(),
1649
1824
  opid: task.opid ?? null,
1650
1825
  taskId: task.id,
1651
- taskName: task.task,
1652
- target: task.target,
1826
+ taskName: task.name,
1827
+ target: task.service_group,
1653
1828
  source: typeof params.source === "string" ? params.source : null,
1654
1829
  resource: typeof params.resource === "string" ? params.resource : null,
1655
1830
  payload
1656
1831
  };
1657
1832
  }
1658
- 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
+ }
1659
1846
  const state = getLogsState(context);
1660
1847
  if (!state.db && !state.errorDb) return;
1661
1848
  const record = buildLogRecord(task, payload);
@@ -1672,84 +1859,293 @@ function appendTaskIpcLog(context, task, payload) {
1672
1859
  context.logger.warn?.("[tasks] failed to persist IPC log entry:", error);
1673
1860
  });
1674
1861
  }
1675
-
1676
- // src/tasks/time-matcher.ts
1677
- var RANGES = ["0-59", "0-59", "0-23", "1-31", "1-12", "0-6"];
1678
- function resolveAsterisks(field, range) {
1679
- return field.includes("*") ? field.replace("*", range) : field;
1680
- }
1681
- function resolveRanges(field) {
1682
- const regex = /(\d+)-(\d+)/;
1683
- let current = field;
1684
- while (true) {
1685
- const match = regex.exec(current);
1686
- if (!match) break;
1687
- const raw = match[0];
1688
- let first = Number(match[1]);
1689
- let last = Number(match[2]);
1690
- if (last < first) {
1691
- [first, last] = [last, first];
1692
- }
1693
- const values = [];
1694
- for (let i = first; i <= last; i += 1) {
1695
- 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);
1696
1870
  }
1697
- current = current.replace(raw, values.join(","));
1698
1871
  }
1699
- return current;
1700
- }
1701
- function resolveSteps(field) {
1702
- const match = /^(.+)\/(\d+)$/.exec(field);
1703
- if (!match) return field;
1704
- const base = match[1];
1705
- const step = Number(match[2]);
1706
- if (!Number.isFinite(step) || step <= 0) return field;
1707
- return base.split(",").map((v) => Number(v)).filter((v) => Number.isFinite(v) && v % step === 0).join(",");
1708
- }
1709
- function convertPattern(pattern) {
1710
- const parts = pattern.trim().split(/\s+/);
1711
- if (parts.length !== 6) {
1712
- throw new Error(`Invalid schedule "${pattern}". Expected 6 fields: sec min hour day month weekday`);
1713
- }
1714
- return parts.map((field, idx) => resolveAsterisks(field, RANGES[idx])).map((field) => resolveRanges(field)).map((field) => resolveSteps(field));
1715
- }
1716
- function fieldMatches(field, value) {
1717
- const allowed = field.split(",").map((v) => Number(v));
1718
- return allowed.includes(value);
1719
- }
1720
- function timeMatcher(pattern, date = /* @__PURE__ */ new Date()) {
1721
- const parsed = convertPattern(pattern);
1722
- 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);
1723
1873
  }
1724
1874
 
1725
- // src/tasks/TaskMaster.ts
1726
- var TaskMaster = class {
1727
- context;
1728
- 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
+ */
1729
1889
  constructor(context, task) {
1730
1890
  this.context = context;
1731
1891
  this.task = task;
1732
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
+ */
1733
1899
  cantRunReason() {
1734
1900
  return false;
1735
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
+ */
1736
1908
  requestStop(_allowanceMs) {
1737
1909
  }
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");
1920
+ }
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 };
1944
+ }
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
+ }
1738
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
+ }
1739
2088
 
1740
- // src/tasks/coreTasks/TaskPing.ts
1741
- var TaskPing = class extends TaskMaster {
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
+ */
1742
2100
  async run() {
1743
2101
  this.context.logger.info?.(`[TaskPing] pong (${this.task.id})`);
1744
2102
  return { success: true, results: "pong" };
1745
2103
  }
1746
2104
  };
1747
2105
 
1748
- // src/tasks/coreTasks/TaskSampleProcess.ts
1749
- var TaskSampleProcess = class extends TaskMaster {
1750
- stopRequested = false;
1751
- stopAllowanceMs = 0;
1752
- stopDecisionLogged = false;
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
+ */
1753
2149
  requestStop(allowanceMs) {
1754
2150
  this.stopRequested = true;
1755
2151
  this.stopAllowanceMs = Number.isFinite(allowanceMs) && allowanceMs > 0 ? allowanceMs : 0;
@@ -1757,6 +2153,14 @@ var TaskSampleProcess = class extends TaskMaster {
1757
2153
  `[TaskSampleProcess] stop signal received (${this.task.id}), allowanceMs=${this.stopAllowanceMs}`
1758
2154
  );
1759
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
+ */
1760
2164
  async run(reportProgress) {
1761
2165
  const totalRaw = this.task?.params?.total ?? 10;
1762
2166
  const delayRaw = this.task?.params?.delay ?? 1e3;
@@ -1838,7 +2242,7 @@ var TaskSampleProcess = class extends TaskMaster {
1838
2242
  }
1839
2243
  };
1840
2244
 
1841
- // src/tasks/coreTasks/TaskShellCommand.ts
2245
+ // src/tasks/coreTasks/TaskShellCommand.js
1842
2246
  var import_node_child_process = require("child_process");
1843
2247
  function runShellCommand(command, cwd) {
1844
2248
  return new Promise((resolve, reject) => {
@@ -1868,7 +2272,27 @@ function runShellCommand(command, cwd) {
1868
2272
  });
1869
2273
  });
1870
2274
  }
1871
- 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
+ */
1872
2296
  async run() {
1873
2297
  const params = this.task?.params;
1874
2298
  const commandRaw = typeof params === "string" ? params : params?.command;
@@ -1917,9 +2341,9 @@ var TaskShellCommand = class extends TaskMaster {
1917
2341
  }
1918
2342
  };
1919
2343
 
1920
- // src/tasks/coreTasks/TaskSystemInfo.ts
2344
+ // src/tasks/coreTasks/TaskSystemInfo.js
1921
2345
  var import_node_os2 = __toESM(require("os"), 1);
1922
- var import_promises2 = __toESM(require("fs/promises"), 1);
2346
+ var import_promises = __toESM(require("fs/promises"), 1);
1923
2347
  function toGb(valueBytes) {
1924
2348
  return `${(valueBytes / 1024 ** 3).toFixed(2)} GB`;
1925
2349
  }
@@ -1927,7 +2351,7 @@ function toMb(valueBytes) {
1927
2351
  return `${(valueBytes / 1024 ** 2).toFixed(2)} MB`;
1928
2352
  }
1929
2353
  async function getDiskStats() {
1930
- const stats = await import_promises2.default.statfs("/");
2354
+ const stats = await import_promises.default.statfs("/");
1931
2355
  const total = Number(stats.bsize) * Number(stats.blocks);
1932
2356
  const free = Number(stats.bsize) * Number(stats.bavail);
1933
2357
  const used = total - free;
@@ -1937,7 +2361,16 @@ async function getDiskStats() {
1937
2361
  free: toGb(free)
1938
2362
  };
1939
2363
  }
1940
- 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
+ */
1941
2374
  async run() {
1942
2375
  try {
1943
2376
  const totalMemory = import_node_os2.default.totalmem();
@@ -1989,8 +2422,31 @@ var TaskSystemInfo = class extends TaskMaster {
1989
2422
  }
1990
2423
  };
1991
2424
 
1992
- // src/tasks/coreTasks/TaskSumAB.ts
1993
- 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
+ */
1994
2450
  async run() {
1995
2451
  const a = this.task?.params?.a;
1996
2452
  const b = this.task?.params?.b;
@@ -2021,8 +2477,46 @@ var TaskSumAB = class extends TaskMaster {
2021
2477
  }
2022
2478
  };
2023
2479
 
2024
- // src/tasks/coreTasks/TaskStopRunner.ts
2025
- 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
+ */
2026
2520
  async run() {
2027
2521
  const allowanceMs = Number(this.task?.params?.allowanceMs ?? 5e3);
2028
2522
  this.context.logger.warn?.(`[TaskStopRunner] stop requested (allowanceMs=${allowanceMs})`);
@@ -2037,172 +2531,396 @@ var TaskStopRunner = class extends TaskMaster {
2037
2531
  }
2038
2532
  };
2039
2533
 
2040
- // 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
2041
2598
  var TasksRegistry = class _TasksRegistry {
2042
- map = {};
2599
+ /**
2600
+ * @param {Record<string, Function>} [initial] Optional seed entries to copy in.
2601
+ */
2043
2602
  constructor(initial) {
2603
+ this.map = {};
2044
2604
  if (initial) {
2045
2605
  this.addMany(initial);
2046
2606
  }
2047
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
+ */
2048
2614
  static withCoreTasks() {
2049
- 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);
2050
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
+ */
2051
2624
  add(taskName, taskClass) {
2052
2625
  this.map[taskName] = taskClass;
2053
2626
  return this;
2054
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
+ */
2055
2634
  addMany(entries) {
2056
2635
  for (const [name, klass] of Object.entries(entries)) {
2057
2636
  this.add(name, klass);
2058
2637
  }
2059
2638
  return this;
2060
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
+ */
2061
2647
  get(taskName) {
2062
2648
  return this.map[taskName];
2063
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
+ */
2064
2699
  listSupportedTasks() {
2065
2700
  return Object.keys(this.map).sort();
2066
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
+ */
2067
2708
  toObject() {
2068
2709
  return { ...this.map };
2069
2710
  }
2070
2711
  };
2071
2712
 
2072
- // 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
2073
2738
  var import_node_child_process2 = require("child_process");
2739
+ var MAX_PROGRESS_TEXT_LEN = 4e3;
2074
2740
  function toCliArgs(args = []) {
2075
2741
  return args.filter((a) => typeof a === "string" && a.length > 0);
2076
2742
  }
2077
2743
  function formatChildLogPrefix(task) {
2078
- 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}` : ""}`;
2079
2745
  }
2080
- async function runNodeTaskScript(context, options) {
2081
- 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) {
2082
2783
  const inheritedExecArgs = Array.isArray(process.execArgv) ? [...process.execArgv] : [];
2083
2784
  const hasTsRuntimeInParent = inheritedExecArgs.some((arg) => /tsx|ts-node/i.test(arg));
2084
- const nodeArgs = hasTsRuntimeInParent ? [...inheritedExecArgs, options.scriptPath, ...cliArgs] : ["--import", "tsx", options.scriptPath, ...cliArgs];
2085
- const child = (0, import_node_child_process2.spawn)(
2086
- process.execPath,
2087
- nodeArgs,
2088
- {
2089
- cwd: options.cwd || process.cwd(),
2090
- stdio: ["ignore", "pipe", "pipe", "ipc"],
2091
- env: {
2092
- ...process.env,
2093
- TASK_ID: options.task.id,
2094
- TASK_NAME: options.task.task,
2095
- TASK_OPID: options.task.opid || ""
2096
- }
2097
- }
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)")
2098
2797
  );
2099
- let stdout = "";
2100
- let stderr = "";
2101
- let workerResult = null;
2102
- let hadErrorMessage = false;
2103
- const prefix = formatChildLogPrefix(options.task);
2104
- const db = context.db;
2105
- const tasksTable = context.params?.get?.("table") || "tasks";
2106
- let progressWriteChain = Promise.resolve();
2107
- let progressCallbackChain = Promise.resolve();
2108
- const updateProgress = (text) => {
2109
- if (!db || !text || !text.trim()) return;
2110
- progressWriteChain = progressWriteChain.then(async () => {
2111
- await db(tasksTable).where({ id: options.task.id }).update({ progress: text.slice(0, 4e3) });
2112
- }).catch((error) => {
2113
- context.logger.warn?.(
2114
- `[tasks] failed to update progress for task=${options.task.id}: ${error?.message ?? String(error)}`
2115
- );
2116
- });
2117
- if (options.onProgress) {
2118
- progressCallbackChain = progressCallbackChain.then(async () => {
2119
- await options.onProgress?.(text.slice(0, 4e3));
2120
- }).catch((error) => {
2121
- context.logger.warn?.(
2122
- `[tasks] reportProgress callback failed for task=${options.task.id}: ${error?.message ?? String(error)}`
2123
- );
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(() => {
2124
2810
  });
2125
2811
  }
2126
2812
  };
2127
- const payloadToProgressText = (payload) => {
2128
- if (!payload) return "";
2129
- if (typeof payload === "string") return payload;
2130
- if (typeof payload.message === "string" && payload.level === "progress" && payload.count !== void 0 && payload.total !== void 0) {
2131
- const pfx = payload.prefix ? `${payload.prefix} ` : "";
2132
- return `${pfx}${payload.message} ${payload.count}/${payload.total}`;
2133
- }
2134
- if (typeof payload.message === "string") return payload.message;
2135
- if (payload.level === "progress" && payload.count !== void 0 && payload.total !== void 0) {
2136
- const pfx = payload.prefix ? `${payload.prefix} ` : "";
2137
- return `${pfx}${payload.count}/${payload.total}`;
2138
- }
2139
- 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
2140
2836
  };
2141
- 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) => {
2142
2863
  const text = String(chunk);
2143
- stdout += text;
2864
+ state.stdout += text;
2144
2865
  if (text.trim()) {
2145
2866
  context.logger.info?.(`[child:${prefix}] ${text.trimEnd()}`);
2146
- updateProgress(text.trim().replace(/\s+/g, " ").slice(0, 400));
2147
2867
  }
2148
2868
  });
2149
- child.stderr.on("data", (chunk) => {
2869
+ child.stderr?.on("data", (chunk) => {
2150
2870
  const text = String(chunk);
2151
- stderr += text;
2871
+ state.stderr += text;
2152
2872
  if (text.trim()) {
2153
2873
  context.logger.warn?.(`[child:${prefix}] ${text.trimEnd()}`);
2154
2874
  }
2155
2875
  });
2156
2876
  child.on("message", (message) => {
2157
2877
  if (message && typeof message === "object" && "__taskWorkerResult" in message) {
2158
- workerResult = message.__taskWorkerResult;
2878
+ state.workerResult = message.__taskWorkerResult;
2159
2879
  return;
2160
2880
  }
2161
2881
  if (message && typeof message === "object") {
2162
2882
  const level = typeof message.level === "string" ? message.level.toLowerCase() : "";
2163
2883
  if (level === "error" || level === "fatal") {
2164
- hadErrorMessage = true;
2884
+ state.hadErrorMessage = true;
2165
2885
  }
2166
2886
  }
2167
- appendTaskIpcLog(context, options.task, message);
2168
- const progressText = payloadToProgressText(message);
2169
- if (progressText) {
2170
- updateProgress(progressText);
2171
- if (typeof message === "object" && message?.level === "progress" && message.count !== void 0 && message.total !== void 0) {
2172
- const countNum = Number(String(message.count).trim());
2173
- const totalNum = Number(message.total);
2174
- if (Number.isFinite(countNum) && Number.isFinite(totalNum) && totalNum > 0) {
2175
- context.logger.progress(message.message || "progress", {
2176
- prefix: message.prefix || prefix,
2177
- count: countNum,
2178
- total: totalNum
2179
- });
2180
- } else {
2181
- context.logger.info?.(`[child:${prefix}] ${progressText}`);
2182
- }
2183
- } else {
2184
- context.logger.info?.(`[child:${prefix}] ${progressText}`);
2185
- }
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)}`);
2186
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);
2187
2903
  });
2188
2904
  return await new Promise((resolve, reject) => {
2189
2905
  child.on("error", (error) => reject(error));
2190
2906
  child.on("close", (exitCode, signal) => {
2191
- Promise.allSettled([progressWriteChain, progressCallbackChain]).finally(() => {
2907
+ void (async () => {
2908
+ await flushTaskIpcLogs(context);
2909
+ await progressQueue.drain();
2192
2910
  resolve({
2193
2911
  exitCode,
2194
2912
  signal,
2195
- stdout: stdout.trim(),
2196
- stderr: stderr.trim(),
2197
- workerResult,
2198
- hadErrorMessage
2913
+ stdout: state.stdout.trim(),
2914
+ stderr: state.stderr.trim(),
2915
+ workerResult: state.workerResult,
2916
+ hadErrorMessage: state.hadErrorMessage
2199
2917
  });
2200
- });
2918
+ })();
2201
2919
  });
2202
2920
  });
2203
2921
  }
2204
2922
 
2205
- // src/tasks/index.ts
2923
+ // src/tasks/index.js
2206
2924
  var LOCKED_BY_ERROR_MESSAGE = "locked by error";
2207
2925
  var defaultTasksRegistry = TasksRegistry.withCoreTasks();
2208
2926
  function getDb3(context) {
@@ -2217,22 +2935,13 @@ function normalizeRegistry(registry) {
2217
2935
  if (registry instanceof TasksRegistry) return registry;
2218
2936
  return new TasksRegistry().addMany(registry);
2219
2937
  }
2220
- function normalizeAllowedTasks(value) {
2221
- if (!value) return void 0;
2222
- if (Array.isArray(value)) {
2223
- const out2 = value.map((v) => String(v).trim()).filter(Boolean);
2224
- return out2.length ? out2 : void 0;
2225
- }
2226
- const out = String(value).split(",").map((v) => v.trim()).filter(Boolean);
2227
- return out.length ? out : void 0;
2228
- }
2229
- async function enqueueStopTask(context, target, queue = "tasks", allowanceMs = 5e3) {
2938
+ async function enqueueStopTask(context, serviceGroup, queueName = "tasks", allowanceMs = 5e3) {
2230
2939
  return enqueueTask(context, {
2231
- queue,
2232
- target,
2233
- task: "stopRunner",
2940
+ queueName,
2941
+ name: "stopRunner",
2234
2942
  params: { allowanceMs },
2235
- priority: 1e6
2943
+ priority: 0,
2944
+ serviceGroup
2236
2945
  });
2237
2946
  }
2238
2947
  async function signalRunningTasksStop(context, runningTaskInstances, allowanceMs) {
@@ -2250,18 +2959,20 @@ async function signalRunningTasksStop(context, runningTaskInstances, allowanceMs
2250
2959
  }
2251
2960
  async function executeClaimedTask(context, tasksTable, historyTable, row, registry, runningTaskInstances) {
2252
2961
  const db = getDb3(context);
2253
- const taskName = row.task;
2962
+ const taskName = row.name;
2254
2963
  const TaskClass = registry.get(taskName);
2255
- const { paused_at: _pausedAt, ...rowForHistory } = row;
2256
2964
  if (!TaskClass) {
2257
2965
  const err = { message: `Unknown task "${taskName}"` };
2258
- await db(historyTable).insert({
2259
- ...rowForHistory,
2260
- completed_at: /* @__PURE__ */ new Date(),
2261
- success: false,
2262
- params: toJsonColumn(row.params),
2263
- results: toJsonColumn(err)
2264
- });
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
+ );
2265
2976
  if (row.schedule) {
2266
2977
  await db(tasksTable).where({ id: row.id }).update({
2267
2978
  started_at: null,
@@ -2269,7 +2980,8 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
2269
2980
  success: false,
2270
2981
  results: toJsonColumn(err),
2271
2982
  past_due: null,
2272
- paused_at: db.fn.now(),
2983
+ status: "paused",
2984
+ status_changed_at: db.fn.now(),
2273
2985
  progress: LOCKED_BY_ERROR_MESSAGE
2274
2986
  });
2275
2987
  } else {
@@ -2296,13 +3008,16 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
2296
3008
  } finally {
2297
3009
  runningTaskInstances.delete(row.id);
2298
3010
  }
2299
- await db(historyTable).insert({
2300
- ...rowForHistory,
2301
- completed_at: /* @__PURE__ */ new Date(),
2302
- success,
2303
- params: toJsonColumn(row.params),
2304
- results: toJsonColumn(results)
2305
- });
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
+ );
2306
3021
  if (!success) {
2307
3022
  const dbName = String(context?.params?.get?.("dbName") || "local");
2308
3023
  const tableName = String(context?.params?.get?.("table") || "tasks");
@@ -2317,19 +3032,32 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
2317
3032
  const rerunCommand = results && typeof results === "object" && results.rerunCommand ? results.rerunCommand : fallbackRecoverCommand;
2318
3033
  appendTaskIpcLog(context, row, {
2319
3034
  level: "error",
2320
- 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}`,
2321
3036
  details: results
2322
3037
  });
2323
3038
  }
2324
3039
  if (row.schedule) {
2325
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
+ }
2326
3047
  await db(tasksTable).where({ id: row.id }).update({
2327
3048
  started_at: null,
2328
3049
  completed_at: /* @__PURE__ */ new Date(),
2329
3050
  success,
2330
3051
  results: toJsonColumn(results),
2331
3052
  progress: null,
2332
- 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
2333
3061
  });
2334
3062
  } else {
2335
3063
  await db(tasksTable).where({ id: row.id }).update({
@@ -2337,7 +3065,8 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
2337
3065
  completed_at: /* @__PURE__ */ new Date(),
2338
3066
  success,
2339
3067
  results: toJsonColumn(results),
2340
- paused_at: db.fn.now(),
3068
+ status: "paused",
3069
+ status_changed_at: db.fn.now(),
2341
3070
  progress: LOCKED_BY_ERROR_MESSAGE,
2342
3071
  past_due: null
2343
3072
  });
@@ -2349,58 +3078,93 @@ async function executeClaimedTask(context, tasksTable, historyTable, row, regist
2349
3078
  const stopAllowanceMs = stopRunnerRequested ? Number(results.allowanceMs ?? 5e3) : 0;
2350
3079
  return { stopRunnerRequested, stopAllowanceMs };
2351
3080
  }
2352
- async function claimNextRunnableTask(context, tasksTable, target, registry, scanLimit, taskNames) {
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) {
2353
3090
  const db = getDb3(context);
2354
- 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);
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);
2355
3094
  if (taskNames && taskNames.length > 0) {
2356
- 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");
2357
3107
  }
2358
3108
  const candidates = await query;
3109
+ shuffleTaskRowsInPlace(candidates);
2359
3110
  for (const row of candidates) {
2360
3111
  if (!row.past_due && row.schedule && !timeMatcher(row.schedule)) {
2361
3112
  continue;
2362
3113
  }
2363
- const TaskClass = registry.get(row.task);
2364
- if (TaskClass) {
2365
- const taskInstance = new TaskClass(context, row);
2366
- const reason = taskInstance.cantRunReason ? await taskInstance.cantRunReason() : null;
2367
- if (reason) {
2368
- if (!row.past_due) {
2369
- await db(tasksTable).where({ id: row.id }).update({
2370
- past_due: db.fn.now(),
2371
- progress: String(reason)
2372
- });
2373
- }
2374
- 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
+ });
2375
3126
  }
3127
+ continue;
3128
+ }
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;
2376
3138
  }
2377
- const updated = await db(tasksTable).where({ id: row.id }).whereNull("started_at").whereNull("paused_at").update({ started_at: db.fn.now() }).returning("*");
3139
+ const updated = await db(tasksTable).where({ id: row.id, status: "idle" }).update(claimPatch).returning("*");
2378
3140
  const claimed = Array.isArray(updated) ? updated[0] : null;
2379
3141
  if (claimed) return claimed;
2380
3142
  }
2381
3143
  return null;
2382
3144
  }
2383
3145
  async function runTasksLoop(context, options) {
2384
- const queue = options.queue ?? "tasks";
3146
+ const queueName = options.queueName ?? "tasks";
2385
3147
  const target = options.target;
2386
3148
  const pollMs = options.pollMs ?? 1e3;
2387
- const maxParallel = options.maxParallel ?? 1;
3149
+ const claimJitterMs = options.claimJitterMs ?? 0;
3150
+ const maxParallel = options.maxParallel ?? 32;
2388
3151
  const scanLimit = options.scanLimit ?? 100;
2389
3152
  const allowedTasks = normalizeAllowedTasks(options.allowedTasks);
2390
3153
  const registry = normalizeRegistry(options.registry);
2391
- const { tasksTable, historyTable } = queueToTableNames(queue);
3154
+ const { tasksTable, historyTable } = queueToTableNames(queueName);
2392
3155
  if (!target) throw new Error("runTasksLoop: target is required");
3156
+ context.tasksQueueName = queueName;
2393
3157
  const runningPromises = /* @__PURE__ */ new Set();
2394
3158
  const runningTaskInstances = /* @__PURE__ */ new Map();
2395
3159
  let runningStopControlPromise = null;
2396
3160
  let stopRequested = false;
2397
3161
  let stopAllowanceMs = 5e3;
2398
- context.__tasksRunnerStop = false;
3162
+ context.tasksRunnerStop = false;
2399
3163
  let registryReg = null;
2400
3164
  let registryInterval = null;
3165
+ let runnerIdentity = null;
2401
3166
  const hbGroup = options.runnerServiceGroup?.trim();
2402
3167
  if (hbGroup) {
2403
- const identityDir = options.runnerIdentityDir ?? "./data/runner-identities";
2404
3168
  const hbIntervalMs = options.runnerHeartbeatIntervalMs ?? 1e4;
2405
3169
  const staleMs = options.runnerHeartbeatStaleMs ?? 45e3;
2406
3170
  const defaultMeta = {
@@ -2408,16 +3172,21 @@ async function runTasksLoop(context, options) {
2408
3172
  allowedTasks: allowedTasks?.length ? allowedTasks.join(",") : "all"
2409
3173
  };
2410
3174
  registryReg = await registerInServicesRegistry(context, {
2411
- queue,
3175
+ queueName,
2412
3176
  target,
2413
3177
  serviceGroup: hbGroup,
2414
3178
  serviceName: options.runnerServiceName,
2415
- identityDir,
3179
+ instanceNumber: options.runnerInstanceNumber,
2416
3180
  staleMs,
2417
3181
  groupMaxInstances: options.runnerGroupMaxInstances,
2418
3182
  enforceMaxInstances: options.runnerEnforceMaxInstances ?? true,
2419
3183
  metadata: options.runnerMetadata ?? defaultMeta
2420
3184
  });
3185
+ runnerIdentity = {
3186
+ service_name: registryReg.serviceName,
3187
+ server_name: import_node_os3.default.hostname(),
3188
+ instance_number: registryReg.instanceNumber
3189
+ };
2421
3190
  registryInterval = setInterval(() => {
2422
3191
  void touchServicesRegistry(context, registryReg).catch((err) => {
2423
3192
  context.logger.warn?.(`[services-registry] touch failed: ${err?.message ?? String(err)}`);
@@ -2425,7 +3194,7 @@ async function runTasksLoop(context, options) {
2425
3194
  }, hbIntervalMs);
2426
3195
  }
2427
3196
  try {
2428
- while (!context.isStop() && !stopRequested && !context.__tasksRunnerStop) {
3197
+ while (!context.isStop() && !stopRequested && context.tasksRunnerStop !== true) {
2429
3198
  if (!runningStopControlPromise) {
2430
3199
  const claimedStopTask = await claimNextRunnableTask(
2431
3200
  context,
@@ -2433,7 +3202,8 @@ async function runTasksLoop(context, options) {
2433
3202
  target,
2434
3203
  registry,
2435
3204
  10,
2436
- ["stopRunner", "stop"]
3205
+ ["stopRunner", "stop"],
3206
+ runnerIdentity
2437
3207
  );
2438
3208
  if (claimedStopTask) {
2439
3209
  runningStopControlPromise = executeClaimedTask(
@@ -2447,7 +3217,7 @@ async function runTasksLoop(context, options) {
2447
3217
  if (outcome.stopRunnerRequested && !stopRequested) {
2448
3218
  stopRequested = true;
2449
3219
  stopAllowanceMs = outcome.stopAllowanceMs || 5e3;
2450
- context.__tasksRunnerStop = true;
3220
+ context.tasksRunnerStop = true;
2451
3221
  await signalRunningTasksStop(context, runningTaskInstances, stopAllowanceMs);
2452
3222
  }
2453
3223
  }).finally(() => {
@@ -2455,6 +3225,9 @@ async function runTasksLoop(context, options) {
2455
3225
  });
2456
3226
  }
2457
3227
  }
3228
+ if (claimJitterMs > 0) {
3229
+ await sleepMs(Math.floor(Math.random() * (claimJitterMs + 1)));
3230
+ }
2458
3231
  while (runningPromises.size < maxParallel) {
2459
3232
  const claimed = await claimNextRunnableTask(
2460
3233
  context,
@@ -2462,14 +3235,15 @@ async function runTasksLoop(context, options) {
2462
3235
  target,
2463
3236
  registry,
2464
3237
  scanLimit,
2465
- allowedTasks
3238
+ allowedTasks,
3239
+ runnerIdentity
2466
3240
  );
2467
3241
  if (!claimed) break;
2468
3242
  const p = executeClaimedTask(context, tasksTable, historyTable, claimed, registry, runningTaskInstances).then(async (outcome) => {
2469
3243
  if (outcome.stopRunnerRequested && !stopRequested) {
2470
3244
  stopRequested = true;
2471
3245
  stopAllowanceMs = outcome.stopAllowanceMs || 5e3;
2472
- context.__tasksRunnerStop = true;
3246
+ context.tasksRunnerStop = true;
2473
3247
  await signalRunningTasksStop(context, runningTaskInstances, stopAllowanceMs);
2474
3248
  }
2475
3249
  }).finally(() => {
@@ -2477,7 +3251,16 @@ async function runTasksLoop(context, options) {
2477
3251
  });
2478
3252
  runningPromises.add(p);
2479
3253
  }
2480
- await sleepMs(pollMs);
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)]);
3263
+ }
2481
3264
  }
2482
3265
  if (context.isStop() && !stopRequested) {
2483
3266
  await signalRunningTasksStop(context, runningTaskInstances, 5e3);
@@ -2513,89 +3296,130 @@ async function runTasksLoop(context, options) {
2513
3296
  }
2514
3297
  async function waitForTaskResult(context, taskId, options = {}) {
2515
3298
  const db = getDb3(context);
2516
- const queue = options.queue ?? "tasks";
3299
+ const queueName = options.queueName ?? "tasks";
2517
3300
  const timeoutMs = options.timeoutMs ?? 6e4;
2518
3301
  const pollMs = options.pollMs ?? 500;
2519
- const { tasksTable, historyTable } = queueToTableNames(queue);
3302
+ const { tasksTable, historyTable } = queueToTableNames(queueName);
2520
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
+ }
2521
3315
  while (Date.now() <= deadline) {
2522
- const done = await db(historyTable).where({ id: taskId }).orderBy("created_at", "desc").first();
2523
- 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
+ }
2524
3320
  const pending = await db(tasksTable).where({ id: taskId }).first();
2525
- if (!pending) {
2526
- const maybeDone = await db(historyTable).where({ id: taskId }).orderBy("created_at", "desc").first();
2527
- 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;
2528
3335
  }
2529
3336
  await sleepMs(pollMs);
2530
3337
  }
2531
3338
  return null;
2532
3339
  }
2533
3340
  var TasksManager = class _TasksManager {
2534
- context;
2535
- queue;
2536
- target;
2537
- recreateTaskTables;
2538
- pollMs;
2539
- maxParallel;
2540
- scanLimit;
2541
- allowedTasks;
2542
- registry;
2543
- runnerServiceGroup;
2544
- runnerServiceName;
2545
- runnerIdentityDir;
2546
- runnerHeartbeatIntervalMs;
2547
- runnerHeartbeatStaleMs;
2548
- runnerGroupMaxInstances;
2549
- runnerEnforceMaxInstances;
2550
- runnerMetadata;
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
+ */
2551
3363
  constructor(context, options = {}) {
2552
3364
  this.context = context;
2553
- this.queue = options.queue ?? "tasks";
3365
+ this.queueName = options.queueName ?? "tasks";
2554
3366
  this.target = options.target ?? "localRunner";
2555
3367
  this.recreateTaskTables = options.recreateTaskTables ?? false;
2556
3368
  this.pollMs = options.pollMs ?? 1e3;
3369
+ this.claimJitterMs = options.claimJitterMs ?? 0;
2557
3370
  this.maxParallel = options.maxParallel ?? 1;
2558
3371
  this.scanLimit = options.scanLimit ?? 100;
2559
3372
  this.allowedTasks = normalizeAllowedTasks(options.allowedTasks);
2560
3373
  this.registry = normalizeRegistry(options.registry);
2561
3374
  this.runnerServiceGroup = options.runnerServiceGroup;
2562
3375
  this.runnerServiceName = options.runnerServiceName;
2563
- this.runnerIdentityDir = options.runnerIdentityDir;
3376
+ this.runnerInstanceNumber = options.runnerInstanceNumber;
2564
3377
  this.runnerHeartbeatIntervalMs = options.runnerHeartbeatIntervalMs;
2565
3378
  this.runnerHeartbeatStaleMs = options.runnerHeartbeatStaleMs;
2566
3379
  this.runnerGroupMaxInstances = options.runnerGroupMaxInstances;
2567
3380
  this.runnerEnforceMaxInstances = options.runnerEnforceMaxInstances;
2568
3381
  this.runnerMetadata = options.runnerMetadata;
2569
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
+ */
2570
3392
  static init(context, options = {}) {
2571
3393
  const defs = {
2572
3394
  table: "string default tasks",
2573
3395
  target: "string default localRunner",
2574
3396
  recreateTaskTables: "boolean default false",
2575
3397
  pollMs: "number default 1000",
3398
+ claimJitterMs: "number default 0",
2576
3399
  maxParallel: "number default 1",
2577
3400
  scanLimit: "number default 100",
2578
3401
  allowedTasks: "string",
2579
3402
  runnerServiceGroup: "string",
2580
3403
  runnerServiceName: "string",
2581
- runnerIdentityDir: "string default ./data/runner-identities",
3404
+ runnerInstanceNumber: "number",
2582
3405
  runnerHeartbeatIntervalMs: "number default 10000",
2583
3406
  runnerHeartbeatStaleMs: "number default 45000",
2584
3407
  runnerGroupMaxInstances: "number",
2585
3408
  runnerEnforceMaxInstances: "boolean default true"
2586
3409
  };
2587
- const discovered = context.params.getAllForModule(defs);
3410
+ const discovered = context.params.getAllForModule("tasks", defs);
2588
3411
  const resolved = {
2589
- queue: discovered.table,
3412
+ queueName: discovered.table,
2590
3413
  target: discovered.target,
2591
3414
  recreateTaskTables: discovered.recreateTaskTables,
2592
3415
  pollMs: discovered.pollMs,
3416
+ claimJitterMs: discovered.claimJitterMs,
2593
3417
  maxParallel: discovered.maxParallel,
2594
3418
  scanLimit: discovered.scanLimit,
2595
3419
  allowedTasks: discovered.allowedTasks,
2596
3420
  runnerServiceGroup: discovered.runnerServiceGroup,
2597
3421
  runnerServiceName: discovered.runnerServiceName,
2598
- runnerIdentityDir: discovered.runnerIdentityDir,
3422
+ runnerInstanceNumber: discovered.runnerInstanceNumber,
2599
3423
  runnerHeartbeatIntervalMs: discovered.runnerHeartbeatIntervalMs,
2600
3424
  runnerHeartbeatStaleMs: discovered.runnerHeartbeatStaleMs,
2601
3425
  runnerGroupMaxInstances: discovered.runnerGroupMaxInstances,
@@ -2604,24 +3428,39 @@ var TasksManager = class _TasksManager {
2604
3428
  };
2605
3429
  return new _TasksManager(context, resolved);
2606
3430
  }
3431
+ /**
3432
+ * Idempotently ensure the three backing tables exist for this queue.
3433
+ *
3434
+ * @param {{ recreate?: boolean }} [options]
3435
+ * @returns {Promise<void>}
3436
+ */
2607
3437
  async ensureTaskTables(options = {}) {
2608
3438
  await ensureTaskTables(this.context, {
2609
- queue: this.queue,
3439
+ queueName: this.queueName,
2610
3440
  recreate: options.recreate ?? this.recreateTaskTables
2611
3441
  });
2612
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
+ */
2613
3451
  async runTasksLoop(options = {}) {
2614
3452
  await runTasksLoop(this.context, {
2615
- queue: options.queue ?? this.queue,
3453
+ queueName: options.queueName ?? this.queueName,
2616
3454
  target: options.target ?? this.target,
2617
3455
  pollMs: options.pollMs ?? this.pollMs,
3456
+ claimJitterMs: options.claimJitterMs ?? this.claimJitterMs,
2618
3457
  maxParallel: options.maxParallel ?? this.maxParallel,
2619
3458
  scanLimit: options.scanLimit ?? this.scanLimit,
2620
3459
  allowedTasks: options.allowedTasks ?? this.allowedTasks,
2621
3460
  registry: options.registry ?? this.registry,
2622
3461
  runnerServiceGroup: options.runnerServiceGroup ?? this.runnerServiceGroup,
2623
3462
  runnerServiceName: options.runnerServiceName ?? this.runnerServiceName,
2624
- runnerIdentityDir: options.runnerIdentityDir ?? this.runnerIdentityDir,
3463
+ runnerInstanceNumber: options.runnerInstanceNumber ?? this.runnerInstanceNumber,
2625
3464
  runnerHeartbeatIntervalMs: options.runnerHeartbeatIntervalMs ?? this.runnerHeartbeatIntervalMs,
2626
3465
  runnerHeartbeatStaleMs: options.runnerHeartbeatStaleMs ?? this.runnerHeartbeatStaleMs,
2627
3466
  runnerGroupMaxInstances: options.runnerGroupMaxInstances ?? this.runnerGroupMaxInstances,
@@ -2632,7 +3471,9 @@ var TasksManager = class _TasksManager {
2632
3471
  };
2633
3472
  // Annotate the CommonJS export names for ESM import in node:
2634
3473
  0 && (module.exports = {
2635
- TaskMaster,
3474
+ AbstractTask,
3475
+ SERVICE_TASK_NAMES,
3476
+ TaskGetLogs,
2636
3477
  TaskPing,
2637
3478
  TaskSampleProcess,
2638
3479
  TaskShellCommand,
@@ -2642,19 +3483,31 @@ var TasksManager = class _TasksManager {
2642
3483
  TasksManager,
2643
3484
  TasksRegistry,
2644
3485
  appendTaskIpcLog,
3486
+ convertPattern,
2645
3487
  defaultTasksRegistry,
2646
3488
  enqueueStopTask,
2647
3489
  enqueueTask,
2648
3490
  ensureTaskTables,
3491
+ flushTaskIpcLogs,
3492
+ ipcFileLogsTableNameForSourceResource,
2649
3493
  listAliveRunnerHeartbeats,
2650
3494
  listServicesRegistry,
3495
+ matchesParsedPattern,
3496
+ mergeAllowedTasksWithServiceTasks,
3497
+ nextTimeMatch,
3498
+ normalizeAllowedTasks,
2651
3499
  queueToTableNames,
3500
+ readTaskIpcLogsSnapshot,
2652
3501
  registerInServicesRegistry,
2653
3502
  registerRunnerHeartbeat,
3503
+ resolveAsterisks,
3504
+ resolveIpcFileLogsDir,
3505
+ resolveRanges,
3506
+ resolveSteps,
2654
3507
  runNodeTaskScript,
2655
3508
  runTasksLoop,
2656
- runnerHeartbeatsTable,
2657
- servicesRegistryTable,
3509
+ taskHistoryInsertFromQueueRow,
3510
+ timeMatcher,
2658
3511
  touchRunnerHeartbeat,
2659
3512
  touchServicesRegistry,
2660
3513
  unregisterRunnerHeartbeat,