@indigoai-us/hq-cli 5.88.0 → 5.88.1

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/CHANGELOG.md CHANGED
@@ -2,6 +2,17 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.88.1]
6
+
7
+ ### Fixed
8
+
9
+ - `hq core checkpoint` no longer wedges its maintenance-sibling queue forever
10
+ when the recorded lock PID is recycled by another user's process. An `EPERM`
11
+ from the liveness probe now means "not our sibling" rather than "sibling
12
+ alive", locks expire after 60 minutes, and `pending.jsonl` is capped at the
13
+ 50 newest payloads with the queue depth reported in the busy message so a
14
+ stall is visible instead of silent. (#313)
15
+
5
16
  ## [5.88.0]
6
17
 
7
18
  ### Changed
@@ -19,6 +19,10 @@ const CODEX_SIBLING_MODEL = "gpt-5.6-terra";
19
19
  const CODEX_SIBLING_REASONING_EFFORT = "high";
20
20
  const CLAUDE_SIBLING_MODEL = "claude-opus-5";
21
21
  const CLAUDE_SIBLING_EFFORT = "medium";
22
+ /** A sibling still holding the lock after this long is treated as abandoned. */
23
+ const SIBLING_LOCK_TTL_MS = 60 * 60 * 1000;
24
+ /** Newest-wins cap so a wedged sibling cannot grow an unbounded queue. */
25
+ const MAX_PENDING_PAYLOADS = 50;
22
26
  class CheckpointUsageError extends Error {
23
27
  }
24
28
  function printResult(line) {
@@ -405,26 +409,58 @@ function siblingPayload(input, threadPath) {
405
409
  pending_payloads: [],
406
410
  };
407
411
  }
408
- function existingSiblingPid(lockPath) {
412
+ /**
413
+ * PID of a sibling that is genuinely still working, or null when the lock is
414
+ * stale and this process should take it over.
415
+ *
416
+ * A lock is stale when the PID is gone, when it cannot be signalled, or when
417
+ * the run has outlived {@link SIBLING_LOCK_TTL_MS}. Siblings are spawned as
418
+ * this user, so an EPERM means the PID was recycled by somebody else's
419
+ * process — not that our sibling is alive. Treating an unsignalable PID as
420
+ * alive wedges the queue permanently: on 2026-08-03 a lock landed on a
421
+ * root-owned kernel thread and every checkpoint for the next two days queued
422
+ * behind a sibling that had never existed.
423
+ */
424
+ function existingSiblingPid(lockPath, now) {
425
+ let raw;
426
+ let writtenAtMs;
427
+ try {
428
+ raw = fs.readFileSync(lockPath, "utf8").trim();
429
+ writtenAtMs = fs.statSync(lockPath).mtimeMs;
430
+ }
431
+ catch {
432
+ return null;
433
+ }
434
+ const pid = Number.parseInt(raw, 10);
435
+ if (!Number.isSafeInteger(pid) || pid <= 0)
436
+ return null;
437
+ if (now - writtenAtMs >= SIBLING_LOCK_TTL_MS)
438
+ return null;
409
439
  try {
410
- const pid = Number.parseInt(fs.readFileSync(lockPath, "utf8").trim(), 10);
411
- if (!Number.isSafeInteger(pid) || pid <= 0)
412
- return null;
413
440
  process.kill(pid, 0);
414
441
  return pid;
415
442
  }
416
- catch (error) {
417
- if (error?.code === "EPERM") {
418
- try {
419
- return Number.parseInt(fs.readFileSync(lockPath, "utf8").trim(), 10);
420
- }
421
- catch {
422
- return null;
423
- }
424
- }
443
+ catch {
425
444
  return null;
426
445
  }
427
446
  }
447
+ /**
448
+ * Queue a payload for the next sibling, keeping only the newest entries. An
449
+ * unbounded queue hides a wedged sibling instead of surfacing it. Returns the
450
+ * resulting queue depth so the caller can report it.
451
+ */
452
+ function appendPending(pendingPath, payload) {
453
+ fs.appendFileSync(pendingPath, `${JSON.stringify(payload)}\n`);
454
+ const lines = fs
455
+ .readFileSync(pendingPath, "utf8")
456
+ .split("\n")
457
+ .filter((line) => line.trim());
458
+ if (lines.length <= MAX_PENDING_PAYLOADS)
459
+ return lines.length;
460
+ const kept = lines.slice(-MAX_PENDING_PAYLOADS);
461
+ fs.writeFileSync(pendingPath, `${kept.join("\n")}\n`);
462
+ return kept.length;
463
+ }
428
464
  function drainPending(pendingPath) {
429
465
  if (!fs.existsSync(pendingPath))
430
466
  return [];
@@ -449,11 +485,11 @@ function startSibling(liveRoot, input, threadPath, backend) {
449
485
  const pendingPath = path.join(siblingRoot, "pending.jsonl");
450
486
  const lockPath = path.join(stateDir, "checkpoint-sibling.lock");
451
487
  const payload = siblingPayload(input, threadPath);
452
- const activePid = existingSiblingPid(lockPath);
488
+ const activePid = existingSiblingPid(lockPath, Date.now());
453
489
  fs.mkdirSync(siblingRoot, { recursive: true });
454
490
  if (activePid !== null) {
455
- fs.appendFileSync(pendingPath, `${JSON.stringify(payload)}\n`);
456
- printResult("checkpoint: sibling busy — payload queued");
491
+ const depth = appendPending(pendingPath, payload);
492
+ printResult(`checkpoint: sibling busy — payload queued (${depth} pending)`);
457
493
  return;
458
494
  }
459
495
  const runDir = path.join(siblingRoot, `${formatTimestamp(new Date())}-${summarySlug(input.summary ?? "checkpoint")}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.88.0",
3
+ "version": "5.88.1",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {