@indigoai-us/hq-cli 5.345.47 → 5.345.48

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.
@@ -13,7 +13,8 @@
13
13
  * - restart/trigger/stop requests from the CLI are polled and applied; a
14
14
  * request for a unit that is not planned yet re-reads the plan first;
15
15
  * - stop() sends SIGTERM to every child, escalates to SIGKILL after
16
- * STOP_ESCALATION_MS, and resolves once every child has exited;
16
+ * STOP_ESCALATION_MS except for a sync runner already stopping, which it lets
17
+ * finish persisting pending pushes before resolving;
17
18
  * - release() instead leaves every child running and reports the long-running
18
19
  * ones, so the next daemon can adopt them (`adopt`). Adopted units whose
19
20
  * command is unchanged keep running; changed ones are replaced one at a
@@ -27,6 +28,7 @@ const BACKOFF_BASE_MS = 1_000;
27
28
  const BACKOFF_MAX_MS = 60_000;
28
29
  const HEALTHY_RUN_MS = 60_000;
29
30
  export const STOP_ESCALATION_MS = 10_000;
31
+ export const SYNC_RUNNER_PERSIST_GRACE_MS = 60_000;
30
32
  const CLI_UPDATE_SYNC_IDLE_MAX_MS = 10 * 60_000;
31
33
  export const CLI_UPDATE_SYNC_IDLE_KILL_GRACE_MS = 60_000;
32
34
  const MESH_OWNER_RECHECK_MS = 15_000;
@@ -91,6 +93,7 @@ export function createSupervisor(deps) {
91
93
  let handoffTimer;
92
94
  /** Every child process not yet known to have exited, including ones the slots have let go of. */
93
95
  const live = new Set();
96
+ const liveOwners = new Map();
94
97
  let drained = null;
95
98
  let onDrained = null;
96
99
  let deferredCliRestart = false;
@@ -247,6 +250,16 @@ export function createSupervisor(deps) {
247
250
  deps.log("error", `could not send ${signal} to ${slot.plan.id}: ${errorMessage(err)}`);
248
251
  }
249
252
  };
253
+ const syncRunnerIsStopping = (slot) => slot?.plan.service === "sync" && slot.lastProtocolStatus === "stopping";
254
+ const scheduleSyncRunnerPersistEscalation = (slot, child) => {
255
+ slot.killTimer = setT(() => {
256
+ slot.killTimer = undefined;
257
+ if (slot.child !== child)
258
+ return;
259
+ deps.log("warn", `${slot.plan.id} did not finish persisting pending pushes within ${SYNC_RUNNER_PERSIST_GRACE_MS / 1_000} s; sending SIGKILL${lastProtocolHint(slot)}`);
260
+ signalChild(slot, child, "SIGKILL");
261
+ }, SYNC_RUNNER_PERSIST_GRACE_MS);
262
+ };
250
263
  /**
251
264
  * Stop the slot's child (if any), then run `then` once it has exited. With
252
265
  * `drain`, a unit that has a drain signal gets it first and SIGTERM only
@@ -276,6 +289,11 @@ export function createSupervisor(deps) {
276
289
  slot.killTimer = undefined;
277
290
  if (slot.child !== child)
278
291
  return;
292
+ if (syncRunnerIsStopping(slot)) {
293
+ deps.log("info", `${slot.plan.id} is stopping; allowing ${SYNC_RUNNER_PERSIST_GRACE_MS / 1_000} s for pending pushes to persist`);
294
+ scheduleSyncRunnerPersistEscalation(slot, child);
295
+ return;
296
+ }
279
297
  deps.log("warn", `${slot.plan.id} did not exit within ${killGraceMs / 1_000} s of SIGTERM; sending SIGKILL${lastProtocolHint(slot)}`);
280
298
  signalChild(slot, child, "SIGKILL");
281
299
  }, killGraceMs);
@@ -313,6 +331,7 @@ export function createSupervisor(deps) {
313
331
  };
314
332
  const watch = (slot, child) => {
315
333
  live.add(child);
334
+ liveOwners.set(child, slot);
316
335
  let ended = false;
317
336
  const timeoutMs = slot.plan.heartbeatTimeoutMs;
318
337
  const decoder = timeoutMs ? new StringDecoder("utf8") : null;
@@ -350,6 +369,8 @@ export function createSupervisor(deps) {
350
369
  slot.syncPassActive = false;
351
370
  else if (event.type === "status" && typeof event.state === "string") {
352
371
  slot.lastProtocolStatus = event.state;
372
+ if (event.state === "stopping")
373
+ deps.onSyncRunnerStopping?.();
353
374
  if (event.state === "stopped" || event.state === "waiting-for-lock")
354
375
  slot.syncPassActive = false;
355
376
  else if (slot.syncPassActive === undefined && (event.state === "starting" || event.state === "running")) {
@@ -405,6 +426,7 @@ export function createSupervisor(deps) {
405
426
  return;
406
427
  ended = true;
407
428
  live.delete(child);
429
+ liveOwners.delete(child);
408
430
  if (live.size === 0)
409
431
  onDrained?.();
410
432
  if (slot.child !== child)
@@ -991,10 +1013,8 @@ export function createSupervisor(deps) {
991
1013
  clearT(handoffTimer);
992
1014
  clearDeferredCliRestart();
993
1015
  clearPendingSyncCommandRestart();
994
- const childOwners = new Map();
1016
+ const childOwners = new Map(liveOwners);
995
1017
  for (const slot of slots.values()) {
996
- if (slot.child)
997
- childOwners.set(slot.child, slot);
998
1018
  clearTimer(slot);
999
1019
  clearHeartbeatTimer(slot);
1000
1020
  slot.child = undefined;
@@ -1020,9 +1040,16 @@ export function createSupervisor(deps) {
1020
1040
  resolve();
1021
1041
  return;
1022
1042
  }
1043
+ const persistingSyncRunners = new Set();
1044
+ let persistEscalation;
1023
1045
  const escalation = setT(() => {
1024
1046
  for (const child of live) {
1025
1047
  const owner = childOwners.get(child);
1048
+ if (owner && syncRunnerIsStopping(owner)) {
1049
+ deps.log("info", `${owner.plan.id} is stopping; allowing ${SYNC_RUNNER_PERSIST_GRACE_MS / 1_000} s for pending pushes to persist`);
1050
+ persistingSyncRunners.add(child);
1051
+ continue;
1052
+ }
1026
1053
  deps.log("warn", `${owner?.plan.id ?? `pid ${child.pid ?? "?"}`} did not exit within ${STOP_ESCALATION_MS / 1_000} s of SIGTERM; sending SIGKILL${owner ? lastProtocolHint(owner) : ""}`);
1027
1054
  try {
1028
1055
  child.kill("SIGKILL");
@@ -1031,10 +1058,28 @@ export function createSupervisor(deps) {
1031
1058
  deps.log("error", `could not SIGKILL pid ${child.pid ?? "?"}: ${errorMessage(err)}`);
1032
1059
  }
1033
1060
  }
1061
+ if (persistingSyncRunners.size > 0) {
1062
+ persistEscalation = setT(() => {
1063
+ for (const child of persistingSyncRunners) {
1064
+ if (!live.has(child))
1065
+ continue;
1066
+ const owner = childOwners.get(child);
1067
+ deps.log("warn", `${owner?.plan.id ?? `pid ${child.pid ?? "?"}`} did not finish persisting pending pushes within ${SYNC_RUNNER_PERSIST_GRACE_MS / 1_000} s; sending SIGKILL${owner ? lastProtocolHint(owner) : ""}`);
1068
+ try {
1069
+ child.kill("SIGKILL");
1070
+ }
1071
+ catch (err) {
1072
+ deps.log("error", `could not SIGKILL pid ${child.pid ?? "?"}: ${errorMessage(err)}`);
1073
+ }
1074
+ }
1075
+ }, SYNC_RUNNER_PERSIST_GRACE_MS);
1076
+ }
1034
1077
  }, STOP_ESCALATION_MS);
1035
1078
  onDrained = () => {
1036
1079
  onDrained = null;
1037
1080
  clearT(escalation);
1081
+ if (persistEscalation !== undefined)
1082
+ clearT(persistEscalation);
1038
1083
  resolve();
1039
1084
  };
1040
1085
  });
@@ -1042,6 +1087,7 @@ export function createSupervisor(deps) {
1042
1087
  },
1043
1088
  reconcile,
1044
1089
  state: snapshot,
1090
+ hasStoppingSyncRunner: () => [...liveOwners.values()].some(syncRunnerIsStopping),
1045
1091
  release() {
1046
1092
  const released = [];
1047
1093
  for (const slot of slots.values()) {
@@ -83,6 +83,8 @@ export const RESIDENT_PACKAGE_MODULES = {
83
83
  "../utils/cli-telemetry.js",
84
84
  "../utils/cognito-session.js",
85
85
  "../lib/first-sync-next-step.js",
86
+ "../lib/process-flag-company-context.js",
87
+ "../lib/flag-snapshot-file.js",
86
88
  "@indigoai-us/hq-cloud",
87
89
  ],
88
90
  };
@@ -352,6 +354,8 @@ const LOADERS = {
352
354
  () => import("../utils/cli-telemetry.js"),
353
355
  () => import("../utils/cognito-session.js"),
354
356
  () => import("../lib/first-sync-next-step.js"),
357
+ () => import("../lib/process-flag-company-context.js"),
358
+ () => import("../lib/flag-snapshot-file.js"),
355
359
  () => import("@indigoai-us/hq-cloud"),
356
360
  ],
357
361
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.345.47",
3
+ "version": "5.345.48",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {