@indigoai-us/hq-cli 5.345.49 → 5.345.50
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 +7 -0
- package/dist/command-catalog.generated.d.ts +56 -11
- package/dist/command-catalog.generated.js +67 -11
- package/dist/commands/db-destroy.js +13 -11
- package/dist/commands/db-dump.js +3 -3
- package/dist/commands/db-remote-deps.d.ts +3 -0
- package/dist/commands/db-remote-deps.js +13 -0
- package/dist/commands/db-sql.js +4 -3
- package/dist/commands/db-usage.js +4 -3
- package/dist/commands/dm-forward-flag.d.ts +4 -0
- package/dist/commands/dm-forward-flag.js +7 -0
- package/dist/commands/dm.d.ts +8 -0
- package/dist/commands/dm.js +219 -27
- package/dist/lib/daemon/hq-cloud.d.ts +2 -2
- package/dist/lib/daemon/hq-cloud.js +38 -9
- package/dist/lib/daemon/run.js +45 -19
- package/dist/lib/db/app-ref.d.ts +21 -0
- package/dist/lib/db/app-ref.js +45 -0
- package/dist/lib/lanes/keep-alive-audit.d.ts +5 -1
- package/dist/lib/lanes/keep-alive-audit.js +85 -3
- package/package.json +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import { LEAD_LANE_KEEP_ALIVE_BASE_BACKOFF_MS, LEAD_LANE_KEEP_ALIVE_NEXT_CHECK_MAX_DELAY_MS } from "./keep-alive.js";
|
|
3
|
+
import { LEAD_LANE_KEEP_ALIVE_BASE_BACKOFF_MS, LEAD_LANE_KEEP_ALIVE_NEXT_CHECK_MAX_DELAY_MS, doneKeepAliveVerdict, hasPendingKeepAliveWork, undeliveredAnsweredQuestions } from "./keep-alive.js";
|
|
4
|
+
import { dropboxDir, dropboxInboxPath } from "./dropbox.js";
|
|
4
5
|
import { LanesError } from "./errors.js";
|
|
5
6
|
import { laneRunDir } from "./spawn.js";
|
|
6
7
|
import { compareEnvelopeNames } from "./envelope-sequence.js";
|
|
@@ -63,6 +64,8 @@ const VERDICTS = [
|
|
|
63
64
|
"keep_alive_off",
|
|
64
65
|
"not_restarted",
|
|
65
66
|
"stranded",
|
|
67
|
+
"finished_story",
|
|
68
|
+
"idle_no_queued_work",
|
|
66
69
|
"waiting",
|
|
67
70
|
];
|
|
68
71
|
function parseTime(value) {
|
|
@@ -174,7 +177,7 @@ export function readStopEnvelopes(hqRoot, laneId) {
|
|
|
174
177
|
if (!isRecord(raw) || raw.schema_version !== "hq-lane-envelope.v1") {
|
|
175
178
|
throw new LanesError("keep_alive_audit_envelope_invalid", `Lane envelope is not hq-lane-envelope.v1: ${file}`, { lane_id: laneId, path: file });
|
|
176
179
|
}
|
|
177
|
-
const doneStop = raw.decision === "done"
|
|
180
|
+
const doneStop = raw.decision === "done";
|
|
178
181
|
const blockedStop = raw.decision === "blocked" && typeof raw.next_check_at === "string" &&
|
|
179
182
|
isIsoDateTime(raw.next_check_at) && Date.parse(raw.next_check_at) > stat.mtimeMs;
|
|
180
183
|
if (!doneStop && !blockedStop)
|
|
@@ -183,6 +186,7 @@ export function readStopEnvelopes(hqRoot, laneId) {
|
|
|
183
186
|
name,
|
|
184
187
|
mtimeMs: stat.mtimeMs,
|
|
185
188
|
decision: blockedStop ? "blocked" : "done",
|
|
189
|
+
storyComplete: raw.story_complete === true,
|
|
186
190
|
...(typeof raw.next_check_at === "string" ? { nextCheckAt: raw.next_check_at } : {}),
|
|
187
191
|
});
|
|
188
192
|
}
|
|
@@ -428,6 +432,48 @@ function firstRestartAfter(lane, stop, nextEnvelopeMs, workerResumeEvents) {
|
|
|
428
432
|
// untimed instead of using the lane's mutable current state as a proxy.
|
|
429
433
|
return nextEnvelopeMs === undefined ? null : { at: null, trigger: "unknown", caller: null };
|
|
430
434
|
}
|
|
435
|
+
/**
|
|
436
|
+
* When the work a finished standing lane is now waiting on first appeared.
|
|
437
|
+
* The earliest signal wins: if work was queued at 10:05 and more arrived at
|
|
438
|
+
* 11:59, keep-alive has owed a launch since 10:05. Returns undefined when no
|
|
439
|
+
* signal carries a time, which leaves the caller unable to open a window.
|
|
440
|
+
*/
|
|
441
|
+
function keepAliveQueuedWorkAt(hqRoot, lane) {
|
|
442
|
+
const candidates = [];
|
|
443
|
+
const pendingAt = parseTime(lane.pending_resume?.at);
|
|
444
|
+
if (pendingAt !== undefined)
|
|
445
|
+
candidates.push(pendingAt);
|
|
446
|
+
for (const candidate of [dropboxInboxPath(hqRoot, lane.lane_id), dropboxDir(hqRoot, lane.lane_id)]) {
|
|
447
|
+
try {
|
|
448
|
+
candidates.push(fs.statSync(candidate).mtimeMs);
|
|
449
|
+
break;
|
|
450
|
+
}
|
|
451
|
+
catch (err) {
|
|
452
|
+
if (err.code !== "ENOENT")
|
|
453
|
+
throw err;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
const questions = [];
|
|
457
|
+
for (const questionId of listQuestionIds(hqRoot)) {
|
|
458
|
+
try {
|
|
459
|
+
const question = tryReadQuestion(hqRoot, questionId);
|
|
460
|
+
if (question)
|
|
461
|
+
questions.push(question);
|
|
462
|
+
}
|
|
463
|
+
catch {
|
|
464
|
+
// One unreadable question file must not fail the whole audit.
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
for (const question of undeliveredAnsweredQuestions(lane, questions)) {
|
|
469
|
+
const answeredAt = parseTime(question.answer?.answered_at);
|
|
470
|
+
if (answeredAt !== undefined)
|
|
471
|
+
candidates.push(answeredAt);
|
|
472
|
+
}
|
|
473
|
+
if (candidates.length === 0)
|
|
474
|
+
return undefined;
|
|
475
|
+
return Math.min(...candidates);
|
|
476
|
+
}
|
|
431
477
|
function classifyVerdict(input) {
|
|
432
478
|
const { dueAt, restart, lane, nowMs } = input;
|
|
433
479
|
if (restart?.trigger === "unknown" && laneWasKeepAliveOffAtStop(lane, input.stoppedAt))
|
|
@@ -478,6 +524,34 @@ function classifyVerdict(input) {
|
|
|
478
524
|
}
|
|
479
525
|
if (laneWasKeepAliveOffAtStop(lane, input.stoppedAt))
|
|
480
526
|
return "keep_alive_off";
|
|
527
|
+
// Split the unrestarted stops by whether keep-alive owes this lane a wake at
|
|
528
|
+
// all. The discriminator is the shared done-state policy, not whether the
|
|
529
|
+
// envelope declared a next_check_at: a stop with story_complete false is owed
|
|
530
|
+
// a wake with or without one. Only done stops with no later envelope reach
|
|
531
|
+
// this branch, because a later envelope means the lane carried on.
|
|
532
|
+
if (input.decision === "done" && input.nextEnvelopeAt === undefined) {
|
|
533
|
+
const eligibility = doneKeepAliveVerdict(lane, {
|
|
534
|
+
decision: "done",
|
|
535
|
+
story_complete: input.storyComplete,
|
|
536
|
+
...(input.nextCheckAt === undefined ? {} : { next_check_at: input.nextCheckAt }),
|
|
537
|
+
}, input.hasQueuedWork, { nowMs });
|
|
538
|
+
// Keep-alive is specified not to wake either case, so neither is a
|
|
539
|
+
// keep-alive liveness failure and neither counts against the KPI.
|
|
540
|
+
if (eligibility === "story_complete")
|
|
541
|
+
return "finished_story";
|
|
542
|
+
if (eligibility === "standing_idle")
|
|
543
|
+
return "idle_no_queued_work";
|
|
544
|
+
// A finished story becomes eligible again only once work is queued, which
|
|
545
|
+
// can be long after the stop. Open the failure window when that work
|
|
546
|
+
// appeared, not at the stop's own due time, or every normal exit from
|
|
547
|
+
// idle_no_queued_work reads as a liveness failure the moment it happens.
|
|
548
|
+
if (input.storyComplete) {
|
|
549
|
+
if (input.queuedWorkAt === undefined)
|
|
550
|
+
return "waiting";
|
|
551
|
+
const anchor = Math.max(dueAt, input.queuedWorkAt);
|
|
552
|
+
return nowMs > anchor + KEEP_ALIVE_NOT_RESTARTED_GRACE_MS ? "not_restarted" : "waiting";
|
|
553
|
+
}
|
|
554
|
+
}
|
|
481
555
|
if (nowMs > dueAt + KEEP_ALIVE_NOT_RESTARTED_GRACE_MS)
|
|
482
556
|
return "not_restarted";
|
|
483
557
|
return "waiting";
|
|
@@ -546,6 +620,10 @@ export function buildKeepAliveAudit(input) {
|
|
|
546
620
|
try {
|
|
547
621
|
const { allMtimes, stops } = readStopEnvelopes(input.hqRoot, lane.lane_id);
|
|
548
622
|
const keepAliveResumeEvents = readKeepAliveResumeEvents(input.hqRoot, lane.lane_id);
|
|
623
|
+
// Read at audit time, not stop time: for a stop with no restart the
|
|
624
|
+
// question is whether anything is going to resume the lane now.
|
|
625
|
+
const hasQueuedWork = lane.keep_alive_standing === true && hasPendingKeepAliveWork(input.hqRoot, lane);
|
|
626
|
+
const queuedWorkAt = hasQueuedWork ? keepAliveQueuedWorkAt(input.hqRoot, lane) : undefined;
|
|
549
627
|
const laneWorkerResumeEvents = readLaneWorkerResumeEvents(input.hqRoot, lane.lane_id);
|
|
550
628
|
const terminalClassifications = readTerminalClassifications(input.hqRoot, lane.lane_id);
|
|
551
629
|
for (const stop of stops) {
|
|
@@ -572,7 +650,8 @@ export function buildKeepAliveAudit(input) {
|
|
|
572
650
|
blocked_outcome: blockedOutcome.status,
|
|
573
651
|
...(blockedOutcome.reason === undefined ? {} : { blocked_outcome_reason: blockedOutcome.reason }),
|
|
574
652
|
} : {}),
|
|
575
|
-
verdict: classifyVerdict({ dueAt: due.at, stoppedAt: stop.mtimeMs, nextCheckAt: stop.nextCheckAt, restart, lane, nowMs, resumeEvents: keepAliveResumeEvents, ...(nextEnvelopeMs === undefined ? {} : { nextEnvelopeAt: nextEnvelopeMs }), hqRoot: input.hqRoot }),
|
|
653
|
+
verdict: classifyVerdict({ dueAt: due.at, stoppedAt: stop.mtimeMs, nextCheckAt: stop.nextCheckAt, restart, decision: stop.decision, storyComplete: stop.storyComplete, hasQueuedWork, ...(queuedWorkAt === undefined ? {} : { queuedWorkAt }), lane, nowMs, resumeEvents: keepAliveResumeEvents, ...(nextEnvelopeMs === undefined ? {} : { nextEnvelopeAt: nextEnvelopeMs }), hqRoot: input.hqRoot }),
|
|
654
|
+
...(stop.decision === "done" ? { queued_work_at_audit_time: hasQueuedWork } : {}),
|
|
576
655
|
});
|
|
577
656
|
}
|
|
578
657
|
}
|
|
@@ -611,6 +690,8 @@ export function buildKeepAliveAudit(input) {
|
|
|
611
690
|
not_restarted: byVerdict.not_restarted,
|
|
612
691
|
stranded: byVerdict.stranded,
|
|
613
692
|
restarted_untimed: byVerdict.restarted_untimed,
|
|
693
|
+
finished_story: byVerdict.finished_story,
|
|
694
|
+
idle_no_queued_work: byVerdict.idle_no_queued_work,
|
|
614
695
|
late_restarts_after_window: lateRestartsAfterWindow,
|
|
615
696
|
late_over_10m: lateOverTenMinutes,
|
|
616
697
|
early_wake_by_others: byVerdict.early_wake,
|
|
@@ -642,6 +723,7 @@ export function formatKeepAliveAuditText(result) {
|
|
|
642
723
|
"",
|
|
643
724
|
`Since ${result.since}; ${counts}`,
|
|
644
725
|
`not_restarted=${result.summary.not_restarted}, stranded=${result.summary.stranded}, late_restarts_after_window=${result.summary.late_restarts_after_window}, late_over_10m=${result.summary.late_over_10m}, early_wake_by_others=${result.summary.early_wake_by_others}, self_wake=${result.summary.self_wake}`,
|
|
726
|
+
`finished_story=${result.summary.finished_story}, idle_no_queued_work=${result.summary.idle_no_queued_work} (neither counts against the keep-alive KPI)`,
|
|
645
727
|
`Stopped and not restarted on their own: ${result.summary.stopped_and_not_restarted_on_their_own}`,
|
|
646
728
|
...result.rows.filter((row) => row.remediation).map((row) => `Stranded ${row.lane}: ${row.remediation}`),
|
|
647
729
|
...result.companyless_lead_lanes.map((lane) => `Company-less lead ${lane.lane} (${lane.project}): ${lane.reason}; repair with hq lanes repair-company ${lane.lane} --company <slug>`),
|