@nmakarov/cli-toolkit 0.47.0 → 0.50.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -5793,10 +5793,9 @@ function buildEcosystemConfig(service, paths) {
5793
5793
  const outLog = join9(paths.logs, `${pm2.appName}.out.log`);
5794
5794
  const errLog = join9(paths.logs, `${pm2.appName}.err.log`);
5795
5795
  return `/**
5796
- * pm2 ecosystem for ${service.name} \u2014 seeded by cli-toolkit deploy (init).
5797
- *
5798
- * cwd points at the \`current\` symlink (created on first deploy).
5799
- * Tweak \`args\` here, then: pm2 reload ${paths.ecosystem} --update-env
5796
+ * pm2 ecosystem for ${service.name} \u2014 written by cli-toolkit deploy from the
5797
+ * service manifest (init/deploy). Do not hand-edit; change pm2.* in the
5798
+ * manifest and redeploy. cwd \u2192 \`current\` symlink.
5800
5799
  */
5801
5800
  module.exports = {
5802
5801
  apps: [
@@ -5804,7 +5803,7 @@ module.exports = {
5804
5803
  name: "${pm2.appName}",
5805
5804
  script: "${pm2.script}",
5806
5805
  cwd: "${paths.current}",
5807
- args: "${pm2.args}",
5806
+ args: "${pm2.args ?? ""}",
5808
5807
  instances: 1,
5809
5808
  exec_mode: "fork",
5810
5809
  autorestart: true,
@@ -5843,17 +5842,17 @@ async function initServiceStructure(service, options = {}) {
5843
5842
  if (!dryRun) await mkdir6(dir, { recursive: true });
5844
5843
  created.push(dir);
5845
5844
  }
5846
- let ecosystemCreated = false;
5847
- if (await pathExists4(paths.ecosystem)) {
5848
- skipped.push(paths.ecosystem);
5845
+ const ecosystemExisted = await pathExists4(paths.ecosystem);
5846
+ if (!dryRun) {
5847
+ await writeFile5(paths.ecosystem, buildEcosystemConfig(service, paths), { mode: 420 });
5848
+ }
5849
+ if (ecosystemExisted) {
5850
+ logger.info(`synced ${paths.ecosystem} from manifest`);
5849
5851
  } else {
5850
- if (!dryRun) {
5851
- await writeFile5(paths.ecosystem, buildEcosystemConfig(service, paths), { mode: 420 });
5852
- }
5853
- ecosystemCreated = true;
5854
5852
  created.push(paths.ecosystem);
5853
+ logger.info(`seeded ${paths.ecosystem}`);
5855
5854
  }
5856
- const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] init-structure service=${service.name} dryRun=${dryRun} created=${created.length} skipped=${skipped.length}
5855
+ const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] init-structure service=${service.name} dryRun=${dryRun} created=${created.length} skipped=${skipped.length} ecosystem=synced
5857
5856
  `;
5858
5857
  if (!dryRun) {
5859
5858
  await mkdir6(paths.logs, { recursive: true });
@@ -5862,8 +5861,7 @@ async function initServiceStructure(service, options = {}) {
5862
5861
  logger.info(`service=${service.name} appsRoot=${paths.root}`);
5863
5862
  logger.info(`created: ${created.length ? created.join(", ") : "(none)"}`);
5864
5863
  logger.info(`already present: ${skipped.length ? skipped.join(", ") : "(none)"}`);
5865
- if (ecosystemCreated) logger.info(`seeded ${paths.ecosystem}`);
5866
- return { paths, created, skipped, ecosystemCreated };
5864
+ return { paths, created, skipped, ecosystemCreated: !ecosystemExisted, ecosystemSynced: true };
5867
5865
  }
5868
5866
 
5869
5867
  // src/deploy/bootstrap-host.js
@@ -5960,11 +5958,55 @@ async function installLogrotate(service, options = {}) {
5960
5958
  await runShell(`sudo cp '${tmp}' '${conf}'`, { logger });
5961
5959
  logger.info(`logrotate config written: ${conf}`);
5962
5960
  }
5961
+ var OPERATOR_SHELL_BEGIN = "# >>> mlsfarm-operator >>>";
5962
+ var OPERATOR_SHELL_END = "# <<< mlsfarm-operator <<<";
5963
+ async function installOperatorShell(service, options = {}) {
5964
+ const { user = "ubuntu", dryRun = false, logger = console } = options;
5965
+ const appsRoot = service.appsRoot;
5966
+ if (!appsRoot) {
5967
+ logger.warn("installOperatorShell: service.appsRoot missing \u2014 skipped");
5968
+ return;
5969
+ }
5970
+ const home = user === "ubuntu" || user === process.env.USER ? join10(homedir(), ".bashrc") : `/home/${user}/.bashrc`;
5971
+ const currentDir = join10(appsRoot, "current");
5972
+ const block = [
5973
+ OPERATOR_SHELL_BEGIN,
5974
+ "# Managed by cli-toolkit deploy bootstrap \u2014 do not edit by hand.",
5975
+ "export ENV=production",
5976
+ `if [ -d "${currentDir}" ]; then`,
5977
+ ` cd "${currentDir}" || true`,
5978
+ "fi",
5979
+ OPERATOR_SHELL_END,
5980
+ ""
5981
+ ].join("\n");
5982
+ if (dryRun) {
5983
+ logger.info(`[dryRun] would update ${home} (ENV=production, cd ${currentDir})`);
5984
+ return;
5985
+ }
5986
+ let existing = "";
5987
+ if (await pathExists5(home)) {
5988
+ existing = await readFile4(home, "utf8");
5989
+ }
5990
+ const begin = existing.indexOf(OPERATOR_SHELL_BEGIN);
5991
+ const end = existing.indexOf(OPERATOR_SHELL_END);
5992
+ let next;
5993
+ if (begin !== -1 && end !== -1 && end > begin) {
5994
+ const afterEnd = end + OPERATOR_SHELL_END.length;
5995
+ const rest = existing.slice(afterEnd).replace(/^\n/, "");
5996
+ next = `${existing.slice(0, begin).replace(/\s*$/, "\n")}${block}${rest}`;
5997
+ } else {
5998
+ next = `${existing.replace(/\s*$/, "\n")}
5999
+ ${block}`;
6000
+ }
6001
+ await writeFile6(home, next, { mode: 420 });
6002
+ logger.info(`operator shell: ${home} \u2192 ENV=production, cd ${currentDir}`);
6003
+ }
5963
6004
  async function bootstrapHost(service, options = {}) {
5964
6005
  const { deployKey = service.deployKey, user = "ubuntu", dryRun = false, logger = console } = options;
5965
6006
  await ensurePm2Startup({ user, dryRun, logger });
5966
6007
  if (deployKey) await installDeployKey(deployKey, { dryRun, logger });
5967
6008
  await installLogrotate(service, { dryRun, logger });
6009
+ await installOperatorShell(service, { user, dryRun, logger });
5968
6010
  logger.info("bootstrap-host complete");
5969
6011
  }
5970
6012
 
@@ -7754,6 +7796,273 @@ var TaskGetLogs = class extends AbstractTask {
7754
7796
  }
7755
7797
  };
7756
7798
 
7799
+ // src/tasks/runtimeParams.js
7800
+ var LOOP_RUNTIME_KEYS = ["maxParallel", "pollMs", "claimJitterMs", "scanLimit"];
7801
+ var LOGGER_RUNTIME_KEYS = [
7802
+ "levels",
7803
+ "silent",
7804
+ "showLevel",
7805
+ "timestamp",
7806
+ "mode",
7807
+ "route",
7808
+ "prefix",
7809
+ "progressWithTimes",
7810
+ "progressThrottleMs"
7811
+ ];
7812
+ var CONTROL_LANE_TASK_NAMES = ["stopRunner", "stop", "setRuntimeParam", "setRunnerParam"];
7813
+ function controlLaneTaskNames() {
7814
+ return [...CONTROL_LANE_TASK_NAMES];
7815
+ }
7816
+ function asPositiveInt(value, key, { min = 1 } = {}) {
7817
+ const n = Number(value);
7818
+ if (!Number.isFinite(n) || n < min) {
7819
+ throw new ParamError(
7820
+ `setRuntimeParam: ${key} must be a number >= ${min} (got ${JSON.stringify(value)})`
7821
+ );
7822
+ }
7823
+ return Math.floor(n);
7824
+ }
7825
+ function coerceRuntimeValue(key, value) {
7826
+ switch (key) {
7827
+ case "maxParallel":
7828
+ return asPositiveInt(value, key, { min: 1 });
7829
+ case "pollMs":
7830
+ return asPositiveInt(value, key, { min: 50 });
7831
+ case "claimJitterMs":
7832
+ return asPositiveInt(value, key, { min: 0 });
7833
+ case "scanLimit":
7834
+ return asPositiveInt(value, key, { min: 1 });
7835
+ case "silent":
7836
+ case "showLevel":
7837
+ case "timestamp":
7838
+ case "progressWithTimes":
7839
+ if (typeof value === "boolean") return value;
7840
+ if (value === "true" || value === "1") return true;
7841
+ if (value === "false" || value === "0") return false;
7842
+ throw new ParamError(
7843
+ `setRuntimeParam: ${key} must be boolean (got ${JSON.stringify(value)})`
7844
+ );
7845
+ case "progressThrottleMs":
7846
+ return asPositiveInt(value, key, { min: 0 });
7847
+ case "levels":
7848
+ case "mode":
7849
+ case "route":
7850
+ case "prefix":
7851
+ return value;
7852
+ default:
7853
+ return value;
7854
+ }
7855
+ }
7856
+ function ensureTasksRuntime(context, seed = {}) {
7857
+ if (!context.tasksRuntime || typeof context.tasksRuntime !== "object") {
7858
+ context.tasksRuntime = {};
7859
+ }
7860
+ const rt = context.tasksRuntime;
7861
+ if (rt.maxParallel === void 0) rt.maxParallel = seed.maxParallel ?? 32;
7862
+ if (rt.pollMs === void 0) rt.pollMs = seed.pollMs ?? 1e3;
7863
+ if (rt.claimJitterMs === void 0) rt.claimJitterMs = seed.claimJitterMs ?? 0;
7864
+ if (rt.scanLimit === void 0) rt.scanLimit = seed.scanLimit ?? 100;
7865
+ return rt;
7866
+ }
7867
+ async function applyRuntimeParam(context, key, value) {
7868
+ const k = String(key ?? "").trim();
7869
+ if (!k) throw new ParamError("setRuntimeParam: key is required");
7870
+ const runtime = ensureTasksRuntime(context);
7871
+ const next = coerceRuntimeValue(k, value);
7872
+ const previous = runtime[k];
7873
+ const applied = [];
7874
+ runtime[k] = next;
7875
+ applied.push("tasksRuntime");
7876
+ if (LOGGER_RUNTIME_KEYS.includes(k) && context.logger?.configure) {
7877
+ context.logger.configure({ [k]: next });
7878
+ applied.push("logger");
7879
+ }
7880
+ const hook = context.tasksRuntimeOnParam;
7881
+ if (typeof hook === "function") {
7882
+ await hook(k, next, runtime, context);
7883
+ applied.push("onRuntimeParam");
7884
+ }
7885
+ const reg = context.servicesRegistry;
7886
+ if (reg?.rowId && reg?.registryTable) {
7887
+ try {
7888
+ const loopSnapshot = {};
7889
+ for (const lk of LOOP_RUNTIME_KEYS) {
7890
+ if (runtime[lk] !== void 0) loopSnapshot[lk] = runtime[lk];
7891
+ }
7892
+ await updateServicesRegistryMetadata(context, reg, {
7893
+ runtime: loopSnapshot,
7894
+ runtimeUpdatedAt: (/* @__PURE__ */ new Date()).toISOString()
7895
+ });
7896
+ applied.push("servicesRegistry");
7897
+ } catch (err) {
7898
+ context.logger?.warn?.(
7899
+ `[setRuntimeParam] registry metadata update failed: ${err?.message ?? String(err)}`
7900
+ );
7901
+ }
7902
+ }
7903
+ return { key: k, previous, next, applied };
7904
+ }
7905
+ async function applyRuntimePatch(context, patch) {
7906
+ if (!patch || typeof patch !== "object" || Array.isArray(patch)) {
7907
+ throw new ParamError("setRuntimeParam: patch must be a plain object");
7908
+ }
7909
+ const entries = Object.entries(patch);
7910
+ if (entries.length === 0) {
7911
+ throw new ParamError("setRuntimeParam: patch is empty");
7912
+ }
7913
+ const out = [];
7914
+ for (const [key, value] of entries) {
7915
+ out.push(await applyRuntimeParam(context, key, value));
7916
+ }
7917
+ return out;
7918
+ }
7919
+ function readLoopRuntime(context) {
7920
+ const rt = ensureTasksRuntime(context);
7921
+ return {
7922
+ maxParallel: Math.max(1, Number(rt.maxParallel) || 1),
7923
+ pollMs: Math.max(50, Number(rt.pollMs) || 1e3),
7924
+ claimJitterMs: Math.max(0, Number(rt.claimJitterMs) || 0),
7925
+ scanLimit: Math.max(1, Number(rt.scanLimit) || 100)
7926
+ };
7927
+ }
7928
+
7929
+ // src/tasks/coreTasks/TaskSetRuntimeParam.js
7930
+ var TaskSetRuntimeParam = class extends AbstractTask {
7931
+ static defaultWaitForResult = true;
7932
+ /**
7933
+ * @param {object} context
7934
+ * @param {Record<string, unknown>} [overrides]
7935
+ * @returns {Promise<object>}
7936
+ */
7937
+ static async resolveParams(context, overrides = {}) {
7938
+ const main = await super.resolveParams(context, overrides);
7939
+ if (!main.serviceName && !main.serviceGroup) {
7940
+ throw new ParamError(
7941
+ "setRuntimeParam requires --serviceName (one instance) or --serviceGroup (broadcast to alive instances)"
7942
+ );
7943
+ }
7944
+ return main;
7945
+ }
7946
+ /**
7947
+ * @param {object} context
7948
+ * @param {Record<string, unknown>} [overrides]
7949
+ * @returns {Promise<{ key?: string, value?: unknown, patch?: Record<string, unknown> }>}
7950
+ */
7951
+ static async resolveCustomParams(context, overrides = {}) {
7952
+ const merged = AbstractTask._mergeTypedParams(
7953
+ context,
7954
+ "task-set-runtime-param",
7955
+ {
7956
+ paramKey: "string",
7957
+ paramValue: "string",
7958
+ key: "string",
7959
+ value: "string"
7960
+ },
7961
+ overrides
7962
+ );
7963
+ if (merged.patch && typeof merged.patch === "object" && !Array.isArray(merged.patch)) {
7964
+ if (Object.keys(merged.patch).length === 0) {
7965
+ throw new ParamError("setRuntimeParam: patch is empty");
7966
+ }
7967
+ return { patch: { ...merged.patch } };
7968
+ }
7969
+ const key = merged.paramKey || merged.key;
7970
+ const value = merged.paramValue !== void 0 ? merged.paramValue : merged.value;
7971
+ if (!key) {
7972
+ throw new ParamError(
7973
+ `setRuntimeParam requires --paramKey/--paramValue, or --paramsJson '{"key":"maxParallel","value":16}' / '{"patch":{...}}'`
7974
+ );
7975
+ }
7976
+ let parsed = value;
7977
+ if (typeof value === "string") {
7978
+ const t = value.trim();
7979
+ if (t === "true") parsed = true;
7980
+ else if (t === "false") parsed = false;
7981
+ else if (t !== "" && !Number.isNaN(Number(t)) && /^-?\d+(\.\d+)?$/.test(t)) {
7982
+ parsed = Number(t);
7983
+ } else if (t.startsWith("{") && t.endsWith("}") || t.startsWith("[") && t.endsWith("]") || t.startsWith('"') && t.endsWith('"')) {
7984
+ try {
7985
+ parsed = JSON.parse(t);
7986
+ } catch {
7987
+ parsed = value;
7988
+ }
7989
+ }
7990
+ }
7991
+ return { key: String(key), value: parsed };
7992
+ }
7993
+ /**
7994
+ * Enqueue one or many setRuntimeParam tasks. Prefer this over a bare
7995
+ * `enqueueTask` when broadcasting to a service group.
7996
+ *
7997
+ * @param {object} context
7998
+ * @param {Record<string, unknown>} [overrides]
7999
+ * @returns {Promise<{ ids: string[], targets: string[] }>}
8000
+ */
8001
+ static async enqueue(context, overrides = {}) {
8002
+ const payload = await this.resolveParams(context, { ...overrides, name: "setRuntimeParam" });
8003
+ const queueName = payload.queueName ?? "tasks";
8004
+ if (payload.serviceName) {
8005
+ const id = await enqueueTask(context, payload);
8006
+ return { ids: [id], targets: [payload.serviceName] };
8007
+ }
8008
+ const group = String(payload.serviceGroup || "").trim();
8009
+ if (!group) {
8010
+ throw new ParamError("setRuntimeParam.enqueue: serviceGroup required for broadcast");
8011
+ }
8012
+ const alive = await listServicesRegistry(context, {
8013
+ queueName,
8014
+ serviceGroup: group,
8015
+ staleMs: overrides.staleMs ?? 45e3
8016
+ });
8017
+ if (!alive.length) {
8018
+ throw new ParamError(
8019
+ `setRuntimeParam: no alive services in group="${group}" queue="${queueName}"`
8020
+ );
8021
+ }
8022
+ const ids = [];
8023
+ const targets = [];
8024
+ for (const reg of alive) {
8025
+ const id = await enqueueTask(context, {
8026
+ ...payload,
8027
+ serviceGroup: group,
8028
+ serviceName: reg.service_name,
8029
+ serverName: reg.server_name ?? null,
8030
+ instanceNumber: reg.instance_number ?? null
8031
+ });
8032
+ ids.push(id);
8033
+ targets.push(reg.service_name);
8034
+ }
8035
+ context.logger?.info?.(
8036
+ `[setRuntimeParam] broadcast to ${targets.length} instance(s) in group=${group}: ${targets.join(", ")}`
8037
+ );
8038
+ return { ids, targets };
8039
+ }
8040
+ /**
8041
+ * @returns {Promise<{ success: true, results: object }>}
8042
+ */
8043
+ async run() {
8044
+ const params = this.task?.params ?? {};
8045
+ let changes;
8046
+ if (params.patch && typeof params.patch === "object") {
8047
+ changes = await applyRuntimePatch(this.context, params.patch);
8048
+ } else {
8049
+ changes = [await applyRuntimeParam(this.context, params.key, params.value)];
8050
+ }
8051
+ const summary = changes.map((c) => `${c.key}: ${JSON.stringify(c.previous)} \u2192 ${JSON.stringify(c.next)}`);
8052
+ this.context.logger.warn?.(
8053
+ `[TaskSetRuntimeParam] applied on ${this.context.servicesRegistry?.serviceName ?? "runner"}: ${summary.join("; ")}`
8054
+ );
8055
+ return {
8056
+ success: true,
8057
+ results: {
8058
+ runtimeParamApplied: true,
8059
+ changes,
8060
+ runtime: { ...this.context.tasksRuntime ?? {} }
8061
+ }
8062
+ };
8063
+ }
8064
+ };
8065
+
7757
8066
  // src/tasks/TasksRegistry.js
7758
8067
  var TasksRegistry = class _TasksRegistry {
7759
8068
  /**
@@ -7772,7 +8081,7 @@ var TasksRegistry = class _TasksRegistry {
7772
8081
  * @returns {TasksRegistry}
7773
8082
  */
7774
8083
  static withCoreTasks() {
7775
- 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);
8084
+ 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).add("setRuntimeParam", TaskSetRuntimeParam).add("setRunnerParam", TaskSetRuntimeParam);
7776
8085
  }
7777
8086
  /**
7778
8087
  * Register a single task class under a name. Overwrites any previous entry.
@@ -7878,7 +8187,9 @@ var SERVICE_TASK_NAMES = [
7878
8187
  "shellCommand",
7879
8188
  "systemInfo",
7880
8189
  "info",
7881
- "getLogs"
8190
+ "getLogs",
8191
+ "setRuntimeParam",
8192
+ "setRunnerParam"
7882
8193
  ];
7883
8194
  function normalizeAllowedTasks(value) {
7884
8195
  if (!value) return void 0;
@@ -8305,18 +8616,23 @@ async function claimNextRunnableTask(context, tasksTable, serviceGroup, registry
8305
8616
  async function runTasksLoop(context, options) {
8306
8617
  const queueName = options.queueName ?? "tasks";
8307
8618
  const target = options.target;
8308
- const pollMs = options.pollMs ?? 1e3;
8309
- const claimJitterMs = options.claimJitterMs ?? 0;
8310
- const maxParallel = options.maxParallel ?? 32;
8311
- const scanLimit = options.scanLimit ?? 100;
8312
8619
  const allowedTasks = normalizeAllowedTasks(options.allowedTasks);
8313
8620
  const registry = normalizeRegistry(options.registry);
8314
8621
  const { tasksTable, historyTable } = queueToTableNames(queueName);
8315
8622
  if (!target) throw new Error("runTasksLoop: target is required");
8316
8623
  context.tasksQueueName = queueName;
8624
+ ensureTasksRuntime(context, {
8625
+ maxParallel: options.maxParallel ?? 32,
8626
+ pollMs: options.pollMs ?? 1e3,
8627
+ claimJitterMs: options.claimJitterMs ?? 0,
8628
+ scanLimit: options.scanLimit ?? 100
8629
+ });
8630
+ if (typeof options.onRuntimeParam === "function") {
8631
+ context.tasksRuntimeOnParam = options.onRuntimeParam;
8632
+ }
8317
8633
  const runningPromises = /* @__PURE__ */ new Set();
8318
8634
  const runningTaskInstances = /* @__PURE__ */ new Map();
8319
- let runningStopControlPromise = null;
8635
+ let runningControlPromise = null;
8320
8636
  let stopRequested = false;
8321
8637
  let stopAllowanceMs = 5e3;
8322
8638
  context.tasksRunnerStop = false;
@@ -8327,9 +8643,16 @@ async function runTasksLoop(context, options) {
8327
8643
  if (hbGroup) {
8328
8644
  const hbIntervalMs = options.runnerHeartbeatIntervalMs ?? 1e4;
8329
8645
  const staleMs = options.runnerHeartbeatStaleMs ?? 45e3;
8646
+ const loop0 = readLoopRuntime(context);
8330
8647
  const defaultMeta = {
8331
8648
  component: "tasks-runner",
8332
- allowedTasks: allowedTasks?.length ? allowedTasks.join(",") : "all"
8649
+ allowedTasks: allowedTasks?.length ? allowedTasks.join(",") : "all",
8650
+ runtime: {
8651
+ maxParallel: loop0.maxParallel,
8652
+ pollMs: loop0.pollMs,
8653
+ claimJitterMs: loop0.claimJitterMs,
8654
+ scanLimit: loop0.scanLimit
8655
+ }
8333
8656
  };
8334
8657
  registryReg = await registerInServicesRegistry(context, {
8335
8658
  queueName,
@@ -8342,6 +8665,7 @@ async function runTasksLoop(context, options) {
8342
8665
  enforceMaxInstances: options.runnerEnforceMaxInstances ?? true,
8343
8666
  metadata: options.runnerMetadata ?? defaultMeta
8344
8667
  });
8668
+ context.servicesRegistry = registryReg;
8345
8669
  runnerIdentity = {
8346
8670
  service_name: registryReg.serviceName,
8347
8671
  server_name: os3.hostname(),
@@ -8355,22 +8679,23 @@ async function runTasksLoop(context, options) {
8355
8679
  }
8356
8680
  try {
8357
8681
  while (!context.isStop() && !stopRequested && context.tasksRunnerStop !== true) {
8358
- if (!runningStopControlPromise) {
8359
- const claimedStopTask = await claimNextRunnableTask(
8682
+ const { maxParallel, pollMs, claimJitterMs, scanLimit } = readLoopRuntime(context);
8683
+ if (!runningControlPromise) {
8684
+ const claimedControlTask = await claimNextRunnableTask(
8360
8685
  context,
8361
8686
  tasksTable,
8362
8687
  target,
8363
8688
  registry,
8364
8689
  10,
8365
- ["stopRunner", "stop"],
8690
+ controlLaneTaskNames(),
8366
8691
  runnerIdentity
8367
8692
  );
8368
- if (claimedStopTask) {
8369
- runningStopControlPromise = executeClaimedTask(
8693
+ if (claimedControlTask) {
8694
+ runningControlPromise = executeClaimedTask(
8370
8695
  context,
8371
8696
  tasksTable,
8372
8697
  historyTable,
8373
- claimedStopTask,
8698
+ claimedControlTask,
8374
8699
  registry,
8375
8700
  runningTaskInstances
8376
8701
  ).then(async (outcome) => {
@@ -8381,7 +8706,7 @@ async function runTasksLoop(context, options) {
8381
8706
  await signalRunningTasksStop(context, runningTaskInstances, stopAllowanceMs);
8382
8707
  }
8383
8708
  }).finally(() => {
8384
- runningStopControlPromise = null;
8709
+ runningControlPromise = null;
8385
8710
  });
8386
8711
  }
8387
8712
  }
@@ -8412,8 +8737,8 @@ async function runTasksLoop(context, options) {
8412
8737
  runningPromises.add(p);
8413
8738
  }
8414
8739
  const wakePromises = [...runningPromises];
8415
- if (runningStopControlPromise) {
8416
- wakePromises.push(runningStopControlPromise);
8740
+ if (runningControlPromise) {
8741
+ wakePromises.push(runningControlPromise);
8417
8742
  }
8418
8743
  if (wakePromises.length === 0) {
8419
8744
  await sleepMs(pollMs);
@@ -8641,6 +8966,8 @@ export {
8641
8966
  FooterPresets,
8642
8967
  GridCell,
8643
8968
  InputField,
8969
+ LOGGER_RUNTIME_KEYS,
8970
+ LOOP_RUNTIME_KEYS,
8644
8971
  ListComponent,
8645
8972
  ListItem,
8646
8973
  MultiColumnListComponent,
@@ -8659,6 +8986,7 @@ export {
8659
8986
  TaskGetLogs,
8660
8987
  TaskPing,
8661
8988
  TaskSampleProcess,
8989
+ TaskSetRuntimeParam,
8662
8990
  TaskShellCommand,
8663
8991
  TaskStopRunner,
8664
8992
  TaskSumAB,
@@ -8670,12 +8998,16 @@ export {
8670
8998
  activateRelease,
8671
8999
  appendDeployLog,
8672
9000
  appendTaskIpcLog,
9001
+ applyRuntimeParam,
9002
+ applyRuntimePatch,
8673
9003
  bootstrapHost,
8674
9004
  buildBreadcrumb,
8675
9005
  buildDetailBreadcrumb,
8676
9006
  buildFooter,
8677
9007
  bumpPatchVersion,
8678
9008
  cloneRepo,
9009
+ coerceRuntimeValue,
9010
+ controlLaneTaskNames,
8679
9011
  convertPattern,
8680
9012
  createRelease,
8681
9013
  defaultFileSynopsisFunction,
@@ -8697,11 +9029,13 @@ export {
8697
9029
  ensureSchemaEverywhere,
8698
9030
  ensureTable,
8699
9031
  ensureTaskTables,
9032
+ ensureTasksRuntime,
8700
9033
  flushTaskIpcLogs,
8701
9034
  getArgsInstance,
8702
9035
  createElement2 as h,
8703
9036
  initServiceStructure,
8704
9037
  installDeps,
9038
+ installOperatorShell,
8705
9039
  ipcFileLogsTableNameForSourceResource,
8706
9040
  joiEdateType,
8707
9041
  joiStringArrayType,
@@ -8727,6 +9061,7 @@ export {
8727
9061
  pullRepo,
8728
9062
  queueToTableNames,
8729
9063
  readCurrentRelease,
9064
+ readLoopRuntime,
8730
9065
  readReleaseBuildInfo,
8731
9066
  readTaskIpcLogsSnapshot,
8732
9067
  registerInServicesRegistry,