@indigoai-us/hq-cli 5.305.0 → 5.307.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/CHANGELOG.md +13 -0
- package/assets/scaffold/core/scripts/audit-log.sh +384 -0
- package/assets/scaffold/core/scripts/git-pack-extension-check.sh +164 -0
- package/assets/scaffold/core/scripts/hq-pin.sh +118 -0
- package/assets/scaffold/core/scripts/lib/portable.sh +211 -0
- package/assets/scaffold/core/scripts/refresh-vault-access.sh +188 -0
- package/dist/command-catalog.generated.d.ts +54 -2
- package/dist/command-catalog.generated.js +66 -2
- package/dist/commands/core-audit-log-native.d.ts +9 -0
- package/dist/commands/core-audit-log-native.js +257 -0
- package/dist/commands/core-git-pack-extension-check-native.d.ts +22 -0
- package/dist/commands/core-git-pack-extension-check-native.js +190 -0
- package/dist/commands/core-native-utilities-flag.d.ts +4 -0
- package/dist/commands/core-native-utilities-flag.js +7 -0
- package/dist/commands/core-native-utilities.d.ts +28 -0
- package/dist/commands/core-native-utilities.js +52 -0
- package/dist/commands/core-pin-native.d.ts +14 -0
- package/dist/commands/core-pin-native.js +236 -0
- package/dist/commands/core-refresh-vault-access-native.d.ts +20 -0
- package/dist/commands/core-refresh-vault-access-native.js +229 -0
- package/dist/commands/core.js +15 -0
- package/dist/commands/daemon.js +24 -23
- package/dist/commands/dm.js +6 -1
- package/dist/commands/lanes.js +43 -1
- package/dist/lib/agent-kit/fallback.d.ts +0 -7
- package/dist/lib/agent-kit/fallback.js +4 -4
- package/dist/lib/agent-kit/inbox-state.d.ts +0 -2
- package/dist/lib/agent-kit/inbox-state.js +2 -2
- package/dist/lib/agent-kit/kit-config.d.ts +0 -5
- package/dist/lib/agent-kit/kit-config.js +5 -5
- package/dist/lib/agent-kit/log.d.ts +0 -1
- package/dist/lib/agent-kit/log.js +1 -1
- package/dist/lib/agent-kit/mcp/jsonrpc.d.ts +3 -12
- package/dist/lib/agent-kit/mcp/jsonrpc.js +4 -4
- package/dist/lib/agent-kit/mcp/tools.d.ts +0 -2
- package/dist/lib/agent-kit/mcp/tools.js +2 -2
- package/dist/lib/agent-kit/services.d.ts +0 -1
- package/dist/lib/agent-kit/services.js +1 -1
- package/dist/lib/checkpoint-ontology/apply.d.ts +0 -49
- package/dist/lib/checkpoint-ontology/apply.js +5 -5
- package/dist/lib/checkpoint-ontology/identity.d.ts +2 -1
- package/dist/lib/checkpoint-ontology/identity.js +1 -1
- package/dist/lib/checkpoint-ontology/multi-writer.d.ts +4 -9
- package/dist/lib/checkpoint-ontology/multi-writer.js +6 -6
- package/dist/lib/checkpoint-ontology/plan.d.ts +4 -17
- package/dist/lib/checkpoint-ontology/plan.js +5 -5
- package/dist/lib/checkpoint-ontology/queued-touches.d.ts +0 -3
- package/dist/lib/checkpoint-ontology/queued-touches.js +3 -3
- package/dist/lib/daemon/catalog.js +6 -2
- package/dist/lib/daemon/remote-control.d.ts +19 -8
- package/dist/lib/daemon/remote-control.js +62 -12
- package/dist/lib/daemon/supervisor.d.ts +2 -0
- package/dist/lib/daemon/supervisor.js +11 -0
- package/dist/lib/db/control-plane.js +6 -0
- package/dist/lib/deploy-apps.js +5 -0
- package/dist/startup-registration.js +9 -3
- package/dist/utils/install-tree-torn.d.ts +13 -6
- package/dist/utils/install-tree-torn.js +42 -16
- package/dist/utils/sentry-fingerprint.js +1 -0
- package/package.json +1 -1
|
@@ -283,7 +283,9 @@ function planService(service, ctx) {
|
|
|
283
283
|
base(service, {
|
|
284
284
|
command: {
|
|
285
285
|
type: "hq",
|
|
286
|
-
|
|
286
|
+
// No codex path: the host finds codex itself, so a reinstall that
|
|
287
|
+
// briefly moves the link cannot change the plan and restart it.
|
|
288
|
+
args: ["daemon", "service", "codex-remote-control", "--script", script ?? ""],
|
|
287
289
|
},
|
|
288
290
|
// Restarting the host ends the tmux session people are using.
|
|
289
291
|
keepAcrossCliUpdates: true,
|
|
@@ -303,7 +305,9 @@ function planService(service, ctx) {
|
|
|
303
305
|
// Outposts keep the runner's session name and permission mode.
|
|
304
306
|
const outpost = ctx.machineType === "outpost";
|
|
305
307
|
const name = ctx.remoteControlName ?? (outpost ? "outpost-1" : ctx.hostName);
|
|
306
|
-
|
|
308
|
+
// No claude path: the host finds claude itself, so a reinstall that
|
|
309
|
+
// briefly moves the link cannot change the plan and restart the session.
|
|
310
|
+
const args = ["daemon", "service", "remote-control", "--name", name];
|
|
307
311
|
if (outpost)
|
|
308
312
|
args.push("--permission-mode", "bypassPermissions");
|
|
309
313
|
return [
|
|
@@ -10,10 +10,12 @@
|
|
|
10
10
|
* - the session ends -> the host exits 1 and the daemon restarts it with
|
|
11
11
|
* backoff (replacing the runner's `while true` loop and its watchdogs);
|
|
12
12
|
* - the daemon stops the host -> the host kills the session and exits 0;
|
|
13
|
-
* - the tool
|
|
14
|
-
* once the session has been quiet for 10 minutes, or after
|
|
15
|
-
* latest, the host kills the session and exits 0, and the
|
|
16
|
-
* again on the new version.
|
|
13
|
+
* - a new version of the tool is installed (the updater, or the tool's own
|
|
14
|
+
* auto-updater) -> once the session has been quiet for 10 minutes, or after
|
|
15
|
+
* 2 hours at the latest, the host kills the session and exits 0, and the
|
|
16
|
+
* daemon starts it again on the new version. Reinstalling the same version
|
|
17
|
+
* (a long-running Claude session's auto-updater does that every few
|
|
18
|
+
* minutes) and hq-cli releases leave the session alone.
|
|
17
19
|
*
|
|
18
20
|
* The tmux server is a private socket (`-L hq-daemon`), so it never touches a
|
|
19
21
|
* user's own tmux sessions.
|
|
@@ -32,11 +34,20 @@ export declare function remoteControlArgs(opts: {
|
|
|
32
34
|
*/
|
|
33
35
|
export declare function runnerSessionName(script: string): string | null;
|
|
34
36
|
/**
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
37
|
+
* Which version of a tool is installed: the npm package that installs the
|
|
38
|
+
* binary (`name@version`), else the binary's real path and size. A native
|
|
39
|
+
* install's link points at a versioned file, so its real path is the version.
|
|
40
|
+
* The mtime is left out on purpose: reinstalling the same version rewrites
|
|
41
|
+
* the file, and that must not restart the session. Null while the tool
|
|
42
|
+
* cannot be read.
|
|
38
43
|
*/
|
|
39
|
-
export declare function
|
|
44
|
+
export declare function toolFingerprint(bin: string): string | null;
|
|
45
|
+
/**
|
|
46
|
+
* The tool a host runs: the path it was given, else the one found now. The
|
|
47
|
+
* daemon's plan leaves the path out, so a reinstall that briefly removes the
|
|
48
|
+
* tool's link cannot change the plan and restart the session.
|
|
49
|
+
*/
|
|
50
|
+
export declare function serviceTool(name: string, given: string | undefined, find: (name: string) => string | null): string;
|
|
40
51
|
/** A tool updated on disk restarts its session once quiet this long... */
|
|
41
52
|
export declare const IDLE_BEFORE_RESTART_MS: number;
|
|
42
53
|
/** ...or after this long, however busy the session is. */
|
|
@@ -10,14 +10,18 @@
|
|
|
10
10
|
* - the session ends -> the host exits 1 and the daemon restarts it with
|
|
11
11
|
* backoff (replacing the runner's `while true` loop and its watchdogs);
|
|
12
12
|
* - the daemon stops the host -> the host kills the session and exits 0;
|
|
13
|
-
* - the tool
|
|
14
|
-
* once the session has been quiet for 10 minutes, or after
|
|
15
|
-
* latest, the host kills the session and exits 0, and the
|
|
16
|
-
* again on the new version.
|
|
13
|
+
* - a new version of the tool is installed (the updater, or the tool's own
|
|
14
|
+
* auto-updater) -> once the session has been quiet for 10 minutes, or after
|
|
15
|
+
* 2 hours at the latest, the host kills the session and exits 0, and the
|
|
16
|
+
* daemon starts it again on the new version. Reinstalling the same version
|
|
17
|
+
* (a long-running Claude session's auto-updater does that every few
|
|
18
|
+
* minutes) and hq-cli releases leave the session alone.
|
|
17
19
|
*
|
|
18
20
|
* The tmux server is a private socket (`-L hq-daemon`), so it never touches a
|
|
19
21
|
* user's own tmux sessions.
|
|
20
22
|
*/
|
|
23
|
+
import * as fs from "node:fs";
|
|
24
|
+
import * as path from "node:path";
|
|
21
25
|
export const TMUX_SOCKET = "hq-daemon";
|
|
22
26
|
export const TMUX_SESSION = "remote-control";
|
|
23
27
|
export const CODEX_TMUX_SESSION = "codex-remote-control";
|
|
@@ -35,17 +39,63 @@ export function runnerSessionName(script) {
|
|
|
35
39
|
const m = /remote-control\b[^\n]*?--name[= ]+"?([^\s"]+)"?/.exec(script);
|
|
36
40
|
return m ? m[1] : null;
|
|
37
41
|
}
|
|
42
|
+
/** How far above the binary to look for the package that installs it (bin/claude.exe sits one level down). */
|
|
43
|
+
const PACKAGE_SEARCH_DEPTH = 3;
|
|
38
44
|
/**
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
45
|
+
* Which version of a tool is installed: the npm package that installs the
|
|
46
|
+
* binary (`name@version`), else the binary's real path and size. A native
|
|
47
|
+
* install's link points at a versioned file, so its real path is the version.
|
|
48
|
+
* The mtime is left out on purpose: reinstalling the same version rewrites
|
|
49
|
+
* the file, and that must not restart the session. Null while the tool
|
|
50
|
+
* cannot be read.
|
|
42
51
|
*/
|
|
43
|
-
export function
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
52
|
+
export function toolFingerprint(bin) {
|
|
53
|
+
let real;
|
|
54
|
+
let size;
|
|
55
|
+
try {
|
|
56
|
+
real = fs.realpathSync(bin);
|
|
57
|
+
size = fs.statSync(real).size;
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
let dir = path.dirname(real);
|
|
63
|
+
for (let i = 0; i < PACKAGE_SEARCH_DEPTH; i++) {
|
|
64
|
+
const version = packageVersionFor(dir, real);
|
|
65
|
+
if (version)
|
|
66
|
+
return version;
|
|
67
|
+
const parent = path.dirname(dir);
|
|
68
|
+
if (parent === dir)
|
|
69
|
+
break;
|
|
70
|
+
dir = parent;
|
|
71
|
+
}
|
|
72
|
+
return `${real}:${size}`;
|
|
73
|
+
}
|
|
74
|
+
/** `name@version` when `<dir>/package.json` declares `real` as one of its bins. */
|
|
75
|
+
function packageVersionFor(dir, real) {
|
|
76
|
+
let pkg;
|
|
77
|
+
try {
|
|
78
|
+
pkg = JSON.parse(fs.readFileSync(path.join(dir, "package.json"), "utf8"));
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
47
81
|
return null;
|
|
48
|
-
|
|
82
|
+
}
|
|
83
|
+
if (typeof pkg.name !== "string" || typeof pkg.version !== "string")
|
|
84
|
+
return null;
|
|
85
|
+
const bins = typeof pkg.bin === "string" ? [pkg.bin] : pkg.bin && typeof pkg.bin === "object" ? Object.values(pkg.bin) : [];
|
|
86
|
+
const installs = bins.some((b) => typeof b === "string" && path.resolve(dir, b) === real);
|
|
87
|
+
return installs ? `${pkg.name}@${pkg.version}` : null;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* The tool a host runs: the path it was given, else the one found now. The
|
|
91
|
+
* daemon's plan leaves the path out, so a reinstall that briefly removes the
|
|
92
|
+
* tool's link cannot change the plan and restart the session.
|
|
93
|
+
*/
|
|
94
|
+
export function serviceTool(name, given, find) {
|
|
95
|
+
const tool = given?.trim() || find(name);
|
|
96
|
+
if (!tool)
|
|
97
|
+
throw new Error(`the ${name} CLI is not installed`);
|
|
98
|
+
return tool;
|
|
49
99
|
}
|
|
50
100
|
/** A tool updated on disk restarts its session once quiet this long... */
|
|
51
101
|
export const IDLE_BEFORE_RESTART_MS = 10 * 60_000;
|
|
@@ -123,6 +123,8 @@ export interface Supervisor {
|
|
|
123
123
|
/** Stop supervising without signalling any child; returns the long-running units left running. */
|
|
124
124
|
release(): ReleasedUnit[];
|
|
125
125
|
}
|
|
126
|
+
/** Plan reads in a row that must find a running remote-control host blocked before it is paused. */
|
|
127
|
+
export declare const BLOCKED_READS_BEFORE_PAUSE = 2;
|
|
126
128
|
export declare function backoffMs(attempt: number): number;
|
|
127
129
|
/**
|
|
128
130
|
* What a unit's process was started from; a unit whose key changes is
|
|
@@ -33,6 +33,8 @@ const MESH_OWNER_LOG_INTERVAL_MS = 10 * 60_000;
|
|
|
33
33
|
export const HANDOFF_STAGGER_MS = 15_000;
|
|
34
34
|
/** How long a draining unit may take to finish its work before it gets SIGTERM. */
|
|
35
35
|
export const HANDOFF_DRAIN_MAX_MS = 15 * 60_000;
|
|
36
|
+
/** Plan reads in a row that must find a running remote-control host blocked before it is paused. */
|
|
37
|
+
export const BLOCKED_READS_BEFORE_PAUSE = 2;
|
|
36
38
|
export function backoffMs(attempt) {
|
|
37
39
|
return Math.min(BACKOFF_MAX_MS, BACKOFF_BASE_MS * 2 ** Math.max(0, attempt));
|
|
38
40
|
}
|
|
@@ -505,6 +507,15 @@ export function createSupervisor(deps) {
|
|
|
505
507
|
continue;
|
|
506
508
|
}
|
|
507
509
|
next.set(plan.id, existing);
|
|
510
|
+
// A remote-control host is what people's sessions run in. Its tool can
|
|
511
|
+
// look missing for one plan read while it is being reinstalled, so it is
|
|
512
|
+
// only paused once the tool is missing on two reads in a row.
|
|
513
|
+
if (plan.blocked && existing.plan.blocked === undefined && existing.child && plan.keepAcrossCliUpdates) {
|
|
514
|
+
existing.blockedReads = (existing.blockedReads ?? 0) + 1;
|
|
515
|
+
if (existing.blockedReads < BLOCKED_READS_BEFORE_PAUSE)
|
|
516
|
+
continue;
|
|
517
|
+
}
|
|
518
|
+
existing.blockedReads = 0;
|
|
508
519
|
const adopted = existing.adopted === true;
|
|
509
520
|
existing.adopted = false;
|
|
510
521
|
const key = planKey(plan, deps.cliVersion);
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { planGateErrorFromPayload } from "../../utils/plan-gate-error.js";
|
|
2
|
+
// Provision may allocate remote resources, so allow two minutes and never retry.
|
|
3
|
+
const DB_PROVISION_TIMEOUT_MS = 120_000;
|
|
4
|
+
// Status is a read-only probe; 15 seconds is longer than its normal response time.
|
|
5
|
+
const DB_STATUS_TIMEOUT_MS = 15_000;
|
|
2
6
|
function assertNoPostgresUrl(label, text) {
|
|
3
7
|
if (/postgres:\/\//i.test(text) || /postgresql:\/\//i.test(text)) {
|
|
4
8
|
throw new Error(`${label}: control plane returned a connection string (refusing to surface)`);
|
|
@@ -22,6 +26,7 @@ export class ControlPlaneDbClient {
|
|
|
22
26
|
"Content-Type": "application/json",
|
|
23
27
|
},
|
|
24
28
|
body: JSON.stringify(input),
|
|
29
|
+
signal: AbortSignal.timeout(DB_PROVISION_TIMEOUT_MS),
|
|
25
30
|
});
|
|
26
31
|
const text = await res.text();
|
|
27
32
|
assertNoPostgresUrl("provision", text);
|
|
@@ -53,6 +58,7 @@ export class ControlPlaneDbClient {
|
|
|
53
58
|
const res = await this.fetchImpl(url, {
|
|
54
59
|
method: "GET",
|
|
55
60
|
headers: { Authorization: `Bearer ${token}` },
|
|
61
|
+
signal: AbortSignal.timeout(DB_STATUS_TIMEOUT_MS),
|
|
56
62
|
});
|
|
57
63
|
const text = await res.text();
|
|
58
64
|
assertNoPostgresUrl("status", text);
|
package/dist/lib/deploy-apps.js
CHANGED
|
@@ -7,6 +7,7 @@ export const MAX_DEPLOY_APPS_PAGES = 100;
|
|
|
7
7
|
*/
|
|
8
8
|
export async function listDeployApps(fetchPage) {
|
|
9
9
|
const apps = [];
|
|
10
|
+
const seenCursors = new Set();
|
|
10
11
|
let cursor;
|
|
11
12
|
for (let page = 0; page < MAX_DEPLOY_APPS_PAGES; page += 1) {
|
|
12
13
|
const result = await fetchPage(cursor);
|
|
@@ -17,6 +18,10 @@ export async function listDeployApps(fetchPage) {
|
|
|
17
18
|
if (typeof result.nextCursor !== "string" || result.nextCursor.length === 0) {
|
|
18
19
|
throw new Error("hq-deploy returned an invalid apps-list cursor.");
|
|
19
20
|
}
|
|
21
|
+
if (seenCursors.has(result.nextCursor)) {
|
|
22
|
+
throw new Error("hq-deploy returned a repeated apps list cursor.");
|
|
23
|
+
}
|
|
24
|
+
seenCursors.add(result.nextCursor);
|
|
20
25
|
cursor = result.nextCursor;
|
|
21
26
|
}
|
|
22
27
|
throw new Error(`hq-deploy apps list exceeded the ${MAX_DEPLOY_APPS_PAGES}-page safety cap.`);
|
|
@@ -150,9 +150,15 @@ export async function registerCommandsWithRecovery(args) {
|
|
|
150
150
|
// writer held the shared lock past its own ceiling — report 'lock-held'
|
|
151
151
|
// (its remedy tells the user to let the update finish, never to reinstall
|
|
152
152
|
// over it). Otherwise the tree genuinely never settled with no writer
|
|
153
|
-
// visible.
|
|
154
|
-
// an
|
|
155
|
-
outcome: result.lockHeldAtEnd
|
|
153
|
+
// visible. A partial retired-sibling scan gets its own outcome so we do
|
|
154
|
+
// not claim that an updater is absent when part of its tree was unreadable.
|
|
155
|
+
outcome: result.lockHeldAtEnd
|
|
156
|
+
? "lock-held"
|
|
157
|
+
: result.retiredSiblingUnverified
|
|
158
|
+
? "retired_unverified"
|
|
159
|
+
: result.staleTree
|
|
160
|
+
? "stale_tree"
|
|
161
|
+
: "unsettled",
|
|
156
162
|
treeState,
|
|
157
163
|
attempt: 1,
|
|
158
164
|
waitedMs: result.waitedMs,
|
|
@@ -274,6 +274,8 @@ export interface WaitForInstallTreeSettledResult {
|
|
|
274
274
|
staleTree?: boolean;
|
|
275
275
|
/** Presence on the final poll; null means the sibling directory was unreadable. */
|
|
276
276
|
retiredSiblingPresent?: boolean | null;
|
|
277
|
+
/** A stable partial retired-sibling scan ended the wait without claiming the tree is stale. */
|
|
278
|
+
retiredSiblingUnverified?: boolean;
|
|
277
279
|
}
|
|
278
280
|
export declare const DEFAULT_INSTALL_TREE_STALE_PROBE_MS = 1500;
|
|
279
281
|
/**
|
|
@@ -288,11 +290,14 @@ export declare const DEFAULT_INSTALL_TREE_STALE_PROBE_MS = 1500;
|
|
|
288
290
|
* app, the box's update timer, another `hq`) cannot make a legitimately-in-progress
|
|
289
291
|
* reinstall look stale:
|
|
290
292
|
* - UNLOCKED time — wall time observed with no fresh foreign lock — is bounded
|
|
291
|
-
* by `deadlineMs`. If the target is still missing and the
|
|
292
|
-
*
|
|
293
|
-
*
|
|
294
|
-
*
|
|
295
|
-
*
|
|
293
|
+
* by `deadlineMs`. If the target is still missing and the observable tree
|
|
294
|
+
* signature stays unchanged for `staleProbeMs`, the wait returns `staleTree`
|
|
295
|
+
* early. When only a named retired npm sibling's deep scan is incomplete,
|
|
296
|
+
* a stable partial signature ends the wait early as `retiredSiblingUnverified`
|
|
297
|
+
* instead. That outcome preserves uncertainty and tells the user to wait for
|
|
298
|
+
* any installer before attempting a repair. Any observed signature change
|
|
299
|
+
* keeps the ordinary settle deadline in force. `deadlineMs === 0` evaluates
|
|
300
|
+
* readiness once.
|
|
296
301
|
* - LOCKED time — wall time observed under a fresh foreign lock held by a live
|
|
297
302
|
* pid — is bounded SEPARATELY by `lockWaitMs` (default the lock contract's own
|
|
298
303
|
* 10-minute staleness window) and does NOT consume the settle budget. When
|
|
@@ -331,7 +336,7 @@ export interface InstallRewriteSignalArgs {
|
|
|
331
336
|
*/
|
|
332
337
|
export declare function installRewriteInProgress(args: InstallRewriteSignalArgs): boolean;
|
|
333
338
|
/** The recovery OUTCOME, which — not the dialect — decides capture. */
|
|
334
|
-
export type InstallTreeTornOutcome = "guarded" | "unsettled" | "reexec-failed" | "lock-held" | "stale_tree";
|
|
339
|
+
export type InstallTreeTornOutcome = "guarded" | "unsettled" | "reexec-failed" | "lock-held" | "stale_tree" | "retired_unverified";
|
|
335
340
|
export type InstallTreeState = "in_flight" | "stale";
|
|
336
341
|
export type InstallTreeTornDiagnostics = {
|
|
337
342
|
dialect: ModuleErrorDialect;
|
|
@@ -408,6 +413,8 @@ export declare function scrubHomeDirectoryPrefix(value: string, homeDirectory?:
|
|
|
408
413
|
export declare const INSTALL_TREE_TORN_REMEDY: string;
|
|
409
414
|
/** Fixed repair guidance for an unchanged tree with no active updater signal. */
|
|
410
415
|
export declare const INSTALL_TREE_STALE_REMEDY: string;
|
|
416
|
+
/** Guidance when a retired package sibling's activity could not be verified. */
|
|
417
|
+
export declare const INSTALL_TREE_RETIRED_UNVERIFIED_REMEDY: string;
|
|
411
418
|
/**
|
|
412
419
|
* The fixed, input-free remedy for the 'lock-held' outcome: a cooperating writer
|
|
413
420
|
* held the shared update lock past its own staleness ceiling. Telling the user to
|
|
@@ -700,7 +700,7 @@ function retiredSiblingActivitySignature(packageRoot, fs) {
|
|
|
700
700
|
for (const sibling of siblings) {
|
|
701
701
|
visit(path.join(parent, sibling), sibling, 0);
|
|
702
702
|
}
|
|
703
|
-
return { value: JSON.stringify(snapshots), observed };
|
|
703
|
+
return { value: JSON.stringify(snapshots), observed, present: siblings.length > 0 };
|
|
704
704
|
}
|
|
705
705
|
/** Whether packageRoot's own manifest is healthy (`name === CLI_NAME`). */
|
|
706
706
|
function packageRootHealthy(packageRoot, fs) {
|
|
@@ -787,6 +787,8 @@ function installTreeActivitySignature(target, packageRoot, fs) {
|
|
|
787
787
|
const activity = installTreeActivityPaths(target, packageRoot);
|
|
788
788
|
const snapshots = [];
|
|
789
789
|
let observed = true;
|
|
790
|
+
let nonRetiredObserved = true;
|
|
791
|
+
let retiredSiblingPresent = null;
|
|
790
792
|
const errorCode = (err) => {
|
|
791
793
|
if (err !== null && typeof err === "object" && "code" in err && typeof err.code === "string") {
|
|
792
794
|
return err.code;
|
|
@@ -809,6 +811,7 @@ function installTreeActivitySignature(target, packageRoot, fs) {
|
|
|
809
811
|
}
|
|
810
812
|
catch (err) {
|
|
811
813
|
observed = false;
|
|
814
|
+
nonRetiredObserved = false;
|
|
812
815
|
snapshots.push([candidate, `unreadable:${errorCode(err)}`]);
|
|
813
816
|
}
|
|
814
817
|
}
|
|
@@ -820,6 +823,7 @@ function installTreeActivitySignature(target, packageRoot, fs) {
|
|
|
820
823
|
}
|
|
821
824
|
catch (err) {
|
|
822
825
|
observed = false;
|
|
826
|
+
nonRetiredObserved = false;
|
|
823
827
|
snapshots.push([directory, `entries-unreadable:${errorCode(err)}`]);
|
|
824
828
|
}
|
|
825
829
|
}
|
|
@@ -827,6 +831,7 @@ function installTreeActivitySignature(target, packageRoot, fs) {
|
|
|
827
831
|
try {
|
|
828
832
|
const retired = retiredSiblingActivitySignature(packageRoot, fs);
|
|
829
833
|
snapshots.push(["retired-siblings", retired.value]);
|
|
834
|
+
retiredSiblingPresent = retired.present;
|
|
830
835
|
if (!retired.observed)
|
|
831
836
|
observed = false;
|
|
832
837
|
}
|
|
@@ -835,7 +840,16 @@ function installTreeActivitySignature(target, packageRoot, fs) {
|
|
|
835
840
|
snapshots.push(["retired-siblings-unreadable", errorCode(err)]);
|
|
836
841
|
}
|
|
837
842
|
}
|
|
838
|
-
return { value: JSON.stringify(snapshots), observed };
|
|
843
|
+
return { value: JSON.stringify(snapshots), observed, nonRetiredObserved, retiredSiblingPresent };
|
|
844
|
+
}
|
|
845
|
+
function canProbeUnsettledRetiredSibling(snapshot) {
|
|
846
|
+
// A very large or unreadable retired npm tree can exceed the bounded recursive
|
|
847
|
+
// activity scan. Its stable partial signature is still useful when the live
|
|
848
|
+
// install paths are observable and npm's named retired sibling is present.
|
|
849
|
+
// This only selects the existing stale-tree repair outcome; it never removes
|
|
850
|
+
// files, and any observed signature change still suppresses stale classification.
|
|
851
|
+
return snapshot.observed ||
|
|
852
|
+
(snapshot.nonRetiredObserved && snapshot.retiredSiblingPresent === true);
|
|
839
853
|
}
|
|
840
854
|
export const DEFAULT_INSTALL_TREE_STALE_PROBE_MS = 1_500;
|
|
841
855
|
/**
|
|
@@ -850,11 +864,14 @@ export const DEFAULT_INSTALL_TREE_STALE_PROBE_MS = 1_500;
|
|
|
850
864
|
* app, the box's update timer, another `hq`) cannot make a legitimately-in-progress
|
|
851
865
|
* reinstall look stale:
|
|
852
866
|
* - UNLOCKED time — wall time observed with no fresh foreign lock — is bounded
|
|
853
|
-
* by `deadlineMs`. If the target is still missing and the
|
|
854
|
-
*
|
|
855
|
-
*
|
|
856
|
-
*
|
|
857
|
-
*
|
|
867
|
+
* by `deadlineMs`. If the target is still missing and the observable tree
|
|
868
|
+
* signature stays unchanged for `staleProbeMs`, the wait returns `staleTree`
|
|
869
|
+
* early. When only a named retired npm sibling's deep scan is incomplete,
|
|
870
|
+
* a stable partial signature ends the wait early as `retiredSiblingUnverified`
|
|
871
|
+
* instead. That outcome preserves uncertainty and tells the user to wait for
|
|
872
|
+
* any installer before attempting a repair. Any observed signature change
|
|
873
|
+
* keeps the ordinary settle deadline in force. `deadlineMs === 0` evaluates
|
|
874
|
+
* readiness once.
|
|
858
875
|
* - LOCKED time — wall time observed under a fresh foreign lock held by a live
|
|
859
876
|
* pid — is bounded SEPARATELY by `lockWaitMs` (default the lock contract's own
|
|
860
877
|
* 10-minute staleness window) and does NOT consume the settle budget. When
|
|
@@ -882,7 +899,7 @@ export async function waitForInstallTreeSettled(args) {
|
|
|
882
899
|
const staleProbeMs = Math.max(0, args.staleProbeMs ?? DEFAULT_INSTALL_TREE_STALE_PROBE_MS);
|
|
883
900
|
const start = now();
|
|
884
901
|
let lastActivitySnapshot = installTreeActivitySignature(args.target, args.packageRoot, fs);
|
|
885
|
-
let staleObservationStartedAt = lastActivitySnapshot
|
|
902
|
+
let staleObservationStartedAt = canProbeUnsettledRetiredSibling(lastActivitySnapshot) ? start : null;
|
|
886
903
|
let sawTreeChange = false;
|
|
887
904
|
let readySince = null;
|
|
888
905
|
let sawLock = false;
|
|
@@ -909,16 +926,18 @@ export async function waitForInstallTreeSettled(args) {
|
|
|
909
926
|
}
|
|
910
927
|
if (!sawTreeChange) {
|
|
911
928
|
const activitySnapshot = installTreeActivitySignature(args.target, args.packageRoot, fs);
|
|
912
|
-
|
|
929
|
+
const previousCanProbe = canProbeUnsettledRetiredSibling(lastActivitySnapshot);
|
|
930
|
+
const currentCanProbe = canProbeUnsettledRetiredSibling(activitySnapshot);
|
|
931
|
+
if (previousCanProbe && currentCanProbe && activitySnapshot.value !== lastActivitySnapshot.value) {
|
|
932
|
+
sawTreeChange = true;
|
|
913
933
|
staleObservationStartedAt = null;
|
|
914
934
|
}
|
|
915
|
-
else if (!
|
|
916
|
-
staleObservationStartedAt = t;
|
|
917
|
-
}
|
|
918
|
-
else if (activitySnapshot.value !== lastActivitySnapshot.value) {
|
|
919
|
-
sawTreeChange = true;
|
|
935
|
+
else if (!currentCanProbe) {
|
|
920
936
|
staleObservationStartedAt = null;
|
|
921
937
|
}
|
|
938
|
+
else if (!previousCanProbe) {
|
|
939
|
+
staleObservationStartedAt = t;
|
|
940
|
+
}
|
|
922
941
|
lastActivitySnapshot = activitySnapshot;
|
|
923
942
|
}
|
|
924
943
|
let retiredPresent = null;
|
|
@@ -985,8 +1004,9 @@ export async function waitForInstallTreeSettled(args) {
|
|
|
985
1004
|
lockedMs,
|
|
986
1005
|
lockTool,
|
|
987
1006
|
sawTreeChange,
|
|
988
|
-
staleTree:
|
|
1007
|
+
staleTree: lastActivitySnapshot.observed,
|
|
989
1008
|
retiredSiblingPresent: retiredPresent,
|
|
1009
|
+
retiredSiblingUnverified: !lastActivitySnapshot.observed && lastActivitySnapshot.retiredSiblingPresent === true,
|
|
990
1010
|
};
|
|
991
1011
|
}
|
|
992
1012
|
// Give up when the TREE has had a full unlocked budget with no writer visible,
|
|
@@ -1176,6 +1196,10 @@ export const INSTALL_TREE_STALE_REMEDY = "hq-cli's install tree is incomplete an
|
|
|
1176
1196
|
"Reinstall with `npm install -g @indigoai-us/hq-cli@latest` " +
|
|
1177
1197
|
"(or `pnpm add -g @indigoai-us/hq-cli@latest` / `bun add -g @indigoai-us/hq-cli@latest` " +
|
|
1178
1198
|
"when that manager owns your install), then run the command again.";
|
|
1199
|
+
/** Guidance when a retired package sibling's activity could not be verified. */
|
|
1200
|
+
export const INSTALL_TREE_RETIRED_UNVERIFIED_REMEDY = "hq-cli found a retired install sibling but could not verify whether an installer is still using it. Wait for any npm or desktop CLI update to finish, then run the command again. If no install is running, repair with `npm install -g @indigoai-us/hq-cli@latest` " +
|
|
1201
|
+
"(or `pnpm add -g @indigoai-us/hq-cli@latest` / `bun add -g @indigoai-us/hq-cli@latest` " +
|
|
1202
|
+
"when that manager owns your install).";
|
|
1179
1203
|
/**
|
|
1180
1204
|
* The fixed, input-free remedy for the 'lock-held' outcome: a cooperating writer
|
|
1181
1205
|
* held the shared update lock past its own staleness ceiling. Telling the user to
|
|
@@ -1198,7 +1222,9 @@ export function installTreeTornStderrLine(err) {
|
|
|
1198
1222
|
? INSTALL_TREE_LOCK_HELD_REMEDY
|
|
1199
1223
|
: err.diagnostics.outcome === "stale_tree"
|
|
1200
1224
|
? INSTALL_TREE_STALE_REMEDY
|
|
1201
|
-
:
|
|
1225
|
+
: err.diagnostics.outcome === "retired_unverified"
|
|
1226
|
+
? INSTALL_TREE_RETIRED_UNVERIFIED_REMEDY
|
|
1227
|
+
: INSTALL_TREE_TORN_REMEDY;
|
|
1202
1228
|
const specifier = err.diagnostics.specifier;
|
|
1203
1229
|
return specifier ? `${remedy} (missing: ${specifier})` : remedy;
|
|
1204
1230
|
}
|
|
@@ -189,6 +189,7 @@ const KNOWN_INSTALL_TREE_OUTCOMES = new Set([
|
|
|
189
189
|
"reexec-failed",
|
|
190
190
|
"lock-held",
|
|
191
191
|
"stale_tree",
|
|
192
|
+
"retired_unverified",
|
|
192
193
|
]);
|
|
193
194
|
function installTreeRecoveryOutcomeTag(value) {
|
|
194
195
|
return typeof value === "string" && KNOWN_INSTALL_TREE_OUTCOMES.has(value)
|