@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 +11 -0
- package/dist/commands/core-checkpoint.js +52 -16
- package/package.json +1 -1
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
|
-
|
|
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
|
|
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
|
-
|
|
456
|
-
printResult(
|
|
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")}`);
|