@indigoai-us/hq-cli 5.345.155 → 5.345.159
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 +289 -0
- package/assets/scaffold/core/scripts/rebuild-company-knowledge-index.sh +110 -2
- package/assets/scaffold/core/scripts/rebuild-projects-index.sh +101 -5
- package/assets/scaffold/core/scripts/worktree.sh +4 -0
- package/dist/command-catalog.generated.d.ts +536 -7
- package/dist/command-catalog.generated.js +676 -24
- package/dist/command-registration-plan.d.ts +18 -0
- package/dist/command-registration-plan.js +4 -0
- package/dist/commands/_patterns.d.ts +17 -0
- package/dist/commands/_patterns.js +22 -0
- package/dist/commands/agent-inbox.js +2 -2
- package/dist/commands/agent-kit.js +8 -6
- package/dist/commands/bot.js +7 -4
- package/dist/commands/channel-suggest.d.ts +56 -0
- package/dist/commands/channel-suggest.js +170 -0
- package/dist/commands/channels.js +7 -1
- package/dist/commands/cloud.d.ts +45 -0
- package/dist/commands/cloud.js +150 -10
- package/dist/commands/company.js +4 -3
- package/dist/commands/core-checkpoint-native.d.ts +18 -0
- package/dist/commands/core-checkpoint-native.js +67 -2
- package/dist/commands/core-pipeline-driver-native.d.ts +12 -1
- package/dist/commands/core-pipeline-driver-native.js +131 -5
- package/dist/commands/core.js +13 -1
- package/dist/commands/crm.js +83 -6
- package/dist/commands/deploy-access.d.ts +33 -0
- package/dist/commands/deploy-access.js +322 -0
- package/dist/commands/dm-attach.d.ts +106 -0
- package/dist/commands/dm-attach.js +184 -0
- package/dist/commands/dm.d.ts +37 -0
- package/dist/commands/dm.js +172 -10
- package/dist/commands/feedback-show.d.ts +40 -0
- package/dist/commands/feedback-show.js +261 -0
- package/dist/commands/feedback.d.ts +21 -0
- package/dist/commands/feedback.js +152 -2
- package/dist/commands/files-browse.d.ts +16 -0
- package/dist/commands/files-browse.js +52 -1
- package/dist/commands/files.d.ts +24 -0
- package/dist/commands/files.js +52 -1
- package/dist/commands/integrations-connect.js +7 -3
- package/dist/commands/integrations-core.d.ts +13 -1
- package/dist/commands/integrations-core.js +49 -4
- package/dist/commands/integrations.js +4 -2
- package/dist/commands/lanes-accounts.js +16 -1
- package/dist/commands/lanes-loop.js +14 -4
- package/dist/commands/lanes-message.js +6 -4
- package/dist/commands/lanes-resume.js +36 -13
- package/dist/commands/lanes-shared.d.ts +5 -0
- package/dist/commands/lanes-shared.js +13 -3
- package/dist/commands/lanes-watch.js +1 -0
- package/dist/commands/lanes.d.ts +20 -1
- package/dist/commands/lanes.js +201 -51
- package/dist/commands/mesh.js +149 -1
- package/dist/commands/monitor.d.ts +1 -0
- package/dist/commands/monitor.js +4 -1
- package/dist/commands/repo.d.ts +41 -0
- package/dist/commands/repo.js +331 -0
- package/dist/commands/scan.d.ts +3 -1
- package/dist/commands/scan.js +7 -3
- package/dist/commands/secrets.d.ts +37 -0
- package/dist/commands/secrets.js +185 -16
- package/dist/commands/signals.js +17 -5
- package/dist/commands/staff/listings.d.ts +41 -0
- package/dist/commands/staff/listings.js +160 -0
- package/dist/commands/staff/reset-test-account.d.ts +3 -2
- package/dist/commands/staff/reset-test-account.js +4 -3
- package/dist/hq-cloud-sync-runner/bin/sync-runner.js +2629 -1036
- package/dist/hq-cloud-sync-runner/package.json +1 -1
- package/dist/lib/agent-kit/inbox-state.d.ts +7 -0
- package/dist/lib/agent-kit/inbox-state.js +5 -1
- package/dist/lib/agent-kit/mcp/tools.js +2 -1
- package/dist/lib/agent-kit/run/inbox.js +2 -1
- package/dist/lib/agent-kit/wake-secrets.d.ts +7 -0
- package/dist/lib/agent-kit/wake-secrets.js +32 -0
- package/dist/lib/agent-kit/wake.d.ts +4 -0
- package/dist/lib/agent-kit/wake.js +49 -3
- package/dist/lib/bot/inflight.d.ts +7 -1
- package/dist/lib/bot/inflight.js +10 -2
- package/dist/lib/bot/prompt.js +3 -0
- package/dist/lib/bot/run.js +72 -20
- package/dist/lib/bot/session.d.ts +14 -0
- package/dist/lib/bot/session.js +70 -0
- package/dist/lib/capacity/collectors/claude-oauth.d.ts +6 -0
- package/dist/lib/capacity/collectors/claude-oauth.js +12 -0
- package/dist/lib/capacity/lane-login.d.ts +6 -0
- package/dist/lib/capacity/lane-login.js +5 -0
- package/dist/lib/core-utils/worktree.js +2 -0
- package/dist/lib/daemon/exit-cause.d.ts +43 -0
- package/dist/lib/daemon/exit-cause.js +88 -0
- package/dist/lib/daemon/handoff.d.ts +2 -0
- package/dist/lib/daemon/handoff.js +6 -1
- package/dist/lib/daemon/hq-child-bootstrap.mjs +20 -3
- package/dist/lib/daemon/paths.d.ts +2 -0
- package/dist/lib/daemon/paths.js +1 -0
- package/dist/lib/daemon/run.js +13 -0
- package/dist/lib/daemon/supervisor.d.ts +54 -1
- package/dist/lib/daemon/supervisor.js +297 -21
- package/dist/lib/daemon/unit-holds.d.ts +22 -0
- package/dist/lib/daemon/unit-holds.js +86 -0
- package/dist/lib/doctor/checks/sync-health.d.ts +9 -0
- package/dist/lib/doctor/checks/sync-health.js +66 -1
- package/dist/lib/doctor/company-resolution-state.d.ts +30 -0
- package/dist/lib/doctor/company-resolution-state.js +89 -0
- package/dist/lib/doctor/fix/apply.d.ts +36 -6
- package/dist/lib/doctor/fix/apply.js +128 -12
- package/dist/lib/doctor/types.d.ts +1 -1
- package/dist/lib/doctor/types.js +1 -0
- package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
- package/dist/lib/flag-registry-worker-source.generated.js +1 -1
- package/dist/lib/hook-engine/resolve.js +6 -2
- package/dist/lib/index-render/company-knowledge.d.ts +1 -1
- package/dist/lib/index-render/company-knowledge.js +4 -5
- package/dist/lib/index-render/index.js +44 -3
- package/dist/lib/index-render/projects.d.ts +1 -1
- package/dist/lib/index-render/projects.js +5 -5
- package/dist/lib/lanes/account-company.d.ts +28 -0
- package/dist/lib/lanes/account-company.js +53 -0
- package/dist/lib/lanes/accounts.d.ts +22 -3
- package/dist/lib/lanes/accounts.js +38 -2
- package/dist/lib/lanes/claude-balance.d.ts +6 -1
- package/dist/lib/lanes/claude-balance.js +6 -1
- package/dist/lib/lanes/claude-credential.d.ts +78 -0
- package/dist/lib/lanes/claude-credential.js +163 -0
- package/dist/lib/lanes/compaction.d.ts +2 -0
- package/dist/lib/lanes/compaction.js +9 -0
- package/dist/lib/lanes/config.js +6 -0
- package/dist/lib/lanes/envelope-repair-dispatch.js +2 -1
- package/dist/lib/lanes/hooks-install.js +2 -1
- package/dist/lib/lanes/host-capacity.d.ts +2 -0
- package/dist/lib/lanes/host-capacity.js +1 -0
- package/dist/lib/lanes/keep-alive.d.ts +1 -1
- package/dist/lib/lanes/keep-alive.js +3 -1
- package/dist/lib/lanes/links.js +10 -4
- package/dist/lib/lanes/loop.js +16 -1
- package/dist/lib/lanes/node-binary.d.ts +17 -0
- package/dist/lib/lanes/node-binary.js +30 -0
- package/dist/lib/lanes/outpost-dispatch.d.ts +2 -1
- package/dist/lib/lanes/outpost-dispatch.js +1 -0
- package/dist/lib/lanes/reconcile.js +4 -0
- package/dist/lib/lanes/remote-control-lane.js +2 -0
- package/dist/lib/lanes/spawn.d.ts +3 -0
- package/dist/lib/lanes/spawn.js +119 -9
- package/dist/lib/lanes/store.d.ts +6 -0
- package/dist/lib/lanes/store.js +107 -18
- package/dist/lib/lanes/types.d.ts +2 -0
- package/dist/lib/lanes/watch.js +28 -5
- package/dist/lib/lanes/watcher-main.js +4 -3
- package/dist/lib/lanes/watcher.d.ts +4 -0
- package/dist/lib/lanes/watcher.js +110 -21
- package/dist/lib/mesh/api.d.ts +32 -0
- package/dist/lib/mesh/api.js +75 -0
- package/dist/lib/secret-scan/detect.js +9 -0
- package/dist/lib/secret-scan/repo-config.d.ts +41 -0
- package/dist/lib/secret-scan/repo-config.js +237 -0
- package/dist/lib/secret-scan/rules.d.ts +7 -1
- package/dist/lib/secret-scan/rules.js +3 -1
- package/dist/lib/secret-scan/staged.d.ts +5 -0
- package/dist/lib/secret-scan/staged.js +19 -2
- package/dist/lib/secret-scan/types.d.ts +2 -0
- package/dist/lib/sync/server-owned-local-only.d.ts +29 -0
- package/dist/lib/sync/server-owned-local-only.js +75 -0
- package/dist/sentry-before-send.js +1 -0
- package/dist/utils/access-denied-hint.js +6 -0
- package/dist/utils/client-health.d.ts +34 -0
- package/dist/utils/client-health.js +96 -3
- package/dist/utils/package-use-lease.js +27 -9
- package/dist/utils/version-gate.d.ts +7 -0
- package/dist/utils/version-gate.js +66 -0
- package/package.json +8 -8
|
@@ -287,6 +287,12 @@ export declare function tokenFingerprint(token: string): string;
|
|
|
287
287
|
export declare function readClaudeRefreshLast(root: string, home: string): ClaudeRefreshLastEntry | null;
|
|
288
288
|
/** @deprecated prefer {@link revokedMarkerForCurrentCredential}; this is the pre-fingerprint shim. */
|
|
289
289
|
export declare function isAccountRevokedPersisted(root: string, accountId: string): boolean;
|
|
290
|
+
/**
|
|
291
|
+
* True when the capacity collector has recorded this refresh token as
|
|
292
|
+
* revoked (by token fingerprint, under any account). Lanes use it to tell a
|
|
293
|
+
* dead sign-in from an access token that Claude Code will renew at startup.
|
|
294
|
+
*/
|
|
295
|
+
export declare function isRefreshTokenRevoked(root: string, refreshToken: string): boolean;
|
|
290
296
|
/**
|
|
291
297
|
* Compare the persisted revoked marker (if any) against the refresh token the
|
|
292
298
|
* caller is about to use.
|
|
@@ -1556,6 +1556,18 @@ export function isAccountRevokedPersisted(root, accountId) {
|
|
|
1556
1556
|
const file = readRefreshLast(root);
|
|
1557
1557
|
return Object.prototype.hasOwnProperty.call(file.revokedAccounts, accountId);
|
|
1558
1558
|
}
|
|
1559
|
+
/**
|
|
1560
|
+
* True when the capacity collector has recorded this refresh token as
|
|
1561
|
+
* revoked (by token fingerprint, under any account). Lanes use it to tell a
|
|
1562
|
+
* dead sign-in from an access token that Claude Code will renew at startup.
|
|
1563
|
+
*/
|
|
1564
|
+
export function isRefreshTokenRevoked(root, refreshToken) {
|
|
1565
|
+
const file = readRefreshLast(root);
|
|
1566
|
+
const fp = tokenFingerprint(refreshToken);
|
|
1567
|
+
if (file.revokedTokens[fp] !== undefined)
|
|
1568
|
+
return true;
|
|
1569
|
+
return Object.values(file.revokedAccounts).some((entry) => entry.fingerprint === fp);
|
|
1570
|
+
}
|
|
1559
1571
|
/**
|
|
1560
1572
|
* Compare the persisted revoked marker (if any) against the refresh token the
|
|
1561
1573
|
* caller is about to use.
|
|
@@ -20,6 +20,12 @@ export interface LaneLoginInput {
|
|
|
20
20
|
* login confirms sign-in through this probe. Defaults to the lanes probe.
|
|
21
21
|
*/
|
|
22
22
|
probeProvider?: Ctx["probeProvider"];
|
|
23
|
+
/**
|
|
24
|
+
* Claude access-token expiry check for the post-login status. Defaults to
|
|
25
|
+
* the real Keychain/file read when the default probe is used; a caller that
|
|
26
|
+
* injects a probe passes its own reader or gets no expiry check.
|
|
27
|
+
*/
|
|
28
|
+
readClaudeCredential?: Ctx["readClaudeCredential"];
|
|
23
29
|
}
|
|
24
30
|
export interface LaneLoginResult {
|
|
25
31
|
id: string;
|
|
@@ -13,6 +13,7 @@ import { createLaneAccountsHandlers } from "../../commands/lanes-accounts.js";
|
|
|
13
13
|
import { effectiveAccountHome } from "../lanes/account-home.js";
|
|
14
14
|
import { canonicalizeHome, DEFAULT_ACCOUNT_HOMES, loadLanesConfig } from "../lanes/config.js";
|
|
15
15
|
import { resolveLanesHqRoot } from "../lanes/hq-root.js";
|
|
16
|
+
import { readLaneClaudeCredential } from "../lanes/claude-credential.js";
|
|
16
17
|
import { defaultProbe } from "../lanes/models.js";
|
|
17
18
|
import { parseLaneProvider } from "../lanes/types.js";
|
|
18
19
|
const ACCOUNT_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
@@ -108,6 +109,10 @@ function loginCtx(input, hqRoot) {
|
|
|
108
109
|
existsSync: (target) => fs.existsSync(target),
|
|
109
110
|
spawnLogin: defaultHomeEnv(input.spawnLogin ?? defaultSpawnLogin, input.homedir ?? os.homedir),
|
|
110
111
|
probeProvider: defaultHomeProbe(input.probeProvider ?? defaultProbe, input.homedir ?? os.homedir),
|
|
112
|
+
// Always a real field: the Proxy below turns a missing one into a throwing
|
|
113
|
+
// function, which accountStatus would call and report as a failed check.
|
|
114
|
+
readClaudeCredential: input.readClaudeCredential ??
|
|
115
|
+
(input.probeProvider === undefined ? readLaneClaudeCredential : undefined),
|
|
111
116
|
};
|
|
112
117
|
return new Proxy(fields, {
|
|
113
118
|
get(target, prop) {
|
|
@@ -42,6 +42,8 @@ export function createWorktree(args = [], options = {}) {
|
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
const die = (message) => { line(stderr, `ERROR: ${message}`); return 1; };
|
|
45
|
+
// Accept "--base origin/<branch>" as well as "--base <branch>"; fetch and ref lookups add origin/ themselves.
|
|
46
|
+
base = base.replace(/^origin\//, "");
|
|
45
47
|
if (!name)
|
|
46
48
|
return die("--name is required (kebab-case slug, e.g. 'fix-login-bug')");
|
|
47
49
|
if (name.includes("/") || name.includes(" "))
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Classifies why a supervised long-running unit stopped, so the supervisor can
|
|
3
|
+
* tell a failure that a restart will not fix (permanent) from one that may
|
|
4
|
+
* clear on its own (transient).
|
|
5
|
+
*
|
|
6
|
+
* Permanent causes stop the restart loop and hold the unit until a manual
|
|
7
|
+
* `hq daemon restart <unit>`, a command change, or a CLI update. Anything not
|
|
8
|
+
* recognised is transient, so an unknown failure keeps the bounded retry.
|
|
9
|
+
*
|
|
10
|
+
* Output is never substring-matched. Only npm's own diagnostic line
|
|
11
|
+
* (`npm error code <CODE>` from npm 10+, `npm ERR! code <CODE>` from older
|
|
12
|
+
* npm) counts, and only for a code in PERMANENT_NPM_CODES. The Desktop app
|
|
13
|
+
* uses the same prefixes and list.
|
|
14
|
+
*/
|
|
15
|
+
export type PermanentExitCause = "missing-binary" | "not-executable" | "package-resolution" | "engine-mismatch" | "permission-denied" | "bad-config" | "auth-refused";
|
|
16
|
+
export type ExitCause = {
|
|
17
|
+
kind: "transient";
|
|
18
|
+
} | {
|
|
19
|
+
kind: "permanent";
|
|
20
|
+
cause: PermanentExitCause;
|
|
21
|
+
detail: string;
|
|
22
|
+
};
|
|
23
|
+
export interface ExitEvidence {
|
|
24
|
+
code: number | null;
|
|
25
|
+
signal: string | null;
|
|
26
|
+
/** The launch error, when the process could not be started. */
|
|
27
|
+
spawnError?: unknown;
|
|
28
|
+
/** Output the process wrote during this run only (not earlier runs). */
|
|
29
|
+
outputTail?: string;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* npm error codes that a retry cannot fix without a change by a person.
|
|
33
|
+
* Network and cache codes (ECONNRESET, ETIMEDOUT, ENOTFOUND, EAI_AGAIN,
|
|
34
|
+
* ECONNREFUSED, EINTEGRITY, EPERM, EBUSY, ENOSPC) stay transient on purpose.
|
|
35
|
+
*/
|
|
36
|
+
export declare const PERMANENT_NPM_CODES: Readonly<Record<string, PermanentExitCause>>;
|
|
37
|
+
/** The first npm error code line naming a permanent code, as `{ code, line }`. */
|
|
38
|
+
export declare function permanentNpmCode(output: string): {
|
|
39
|
+
code: string;
|
|
40
|
+
cause: PermanentExitCause;
|
|
41
|
+
} | undefined;
|
|
42
|
+
export declare function classifyExit(evidence: ExitEvidence): ExitCause;
|
|
43
|
+
//# sourceMappingURL=exit-cause.d.ts.map
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Classifies why a supervised long-running unit stopped, so the supervisor can
|
|
3
|
+
* tell a failure that a restart will not fix (permanent) from one that may
|
|
4
|
+
* clear on its own (transient).
|
|
5
|
+
*
|
|
6
|
+
* Permanent causes stop the restart loop and hold the unit until a manual
|
|
7
|
+
* `hq daemon restart <unit>`, a command change, or a CLI update. Anything not
|
|
8
|
+
* recognised is transient, so an unknown failure keeps the bounded retry.
|
|
9
|
+
*
|
|
10
|
+
* Output is never substring-matched. Only npm's own diagnostic line
|
|
11
|
+
* (`npm error code <CODE>` from npm 10+, `npm ERR! code <CODE>` from older
|
|
12
|
+
* npm) counts, and only for a code in PERMANENT_NPM_CODES. The Desktop app
|
|
13
|
+
* uses the same prefixes and list.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* npm error codes that a retry cannot fix without a change by a person.
|
|
17
|
+
* Network and cache codes (ECONNRESET, ETIMEDOUT, ENOTFOUND, EAI_AGAIN,
|
|
18
|
+
* ECONNREFUSED, EINTEGRITY, EPERM, EBUSY, ENOSPC) stay transient on purpose.
|
|
19
|
+
*/
|
|
20
|
+
export const PERMANENT_NPM_CODES = {
|
|
21
|
+
ETARGET: "package-resolution", // no version matches the requested range or tag
|
|
22
|
+
E404: "package-resolution", // package or version not in the registry
|
|
23
|
+
EINVALIDPACKAGENAME: "package-resolution",
|
|
24
|
+
EUNSUPPORTEDPROTOCOL: "package-resolution", // unsupported spec, e.g. a bad URL scheme
|
|
25
|
+
EBADENGINE: "engine-mismatch", // engine-strict and node does not satisfy engines
|
|
26
|
+
EACCES: "permission-denied", // cache or prefix not writable by this user
|
|
27
|
+
E401: "auth-refused",
|
|
28
|
+
E403: "auth-refused",
|
|
29
|
+
ENEEDAUTH: "auth-refused",
|
|
30
|
+
};
|
|
31
|
+
const NPM_CODE_PREFIXES = ["npm error code ", "npm ERR! code "];
|
|
32
|
+
/** sysexits.h EX_NOPERM / EX_CONFIG, and the shell's not-executable / not-found codes. */
|
|
33
|
+
const EXIT_CODE_CAUSES = {
|
|
34
|
+
77: { cause: "auth-refused", detail: "exit code 77 (permission or auth refused)" },
|
|
35
|
+
78: { cause: "bad-config", detail: "exit code 78 (configuration error)" },
|
|
36
|
+
126: { cause: "not-executable", detail: "exit code 126 (command not executable)" },
|
|
37
|
+
127: { cause: "missing-binary", detail: "exit code 127 (command not found)" },
|
|
38
|
+
};
|
|
39
|
+
/** Launch errno codes, read from the error's `code` field only. */
|
|
40
|
+
const SPAWN_ERROR_CAUSES = {
|
|
41
|
+
ENOENT: "missing-binary",
|
|
42
|
+
EACCES: "not-executable",
|
|
43
|
+
ENOEXEC: "not-executable",
|
|
44
|
+
};
|
|
45
|
+
function errorCode(err) {
|
|
46
|
+
if (err && typeof err === "object" && "code" in err && typeof err.code === "string") {
|
|
47
|
+
return err.code;
|
|
48
|
+
}
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
/** The first npm error code line naming a permanent code, as `{ code, line }`. */
|
|
52
|
+
export function permanentNpmCode(output) {
|
|
53
|
+
for (const raw of output.split(/\r?\n/)) {
|
|
54
|
+
const line = raw.trim();
|
|
55
|
+
for (const prefix of NPM_CODE_PREFIXES) {
|
|
56
|
+
if (!line.startsWith(prefix))
|
|
57
|
+
continue;
|
|
58
|
+
const code = line.slice(prefix.length).split(/\s+/)[0] ?? "";
|
|
59
|
+
const cause = Object.prototype.hasOwnProperty.call(PERMANENT_NPM_CODES, code) ? PERMANENT_NPM_CODES[code] : undefined;
|
|
60
|
+
if (cause)
|
|
61
|
+
return { code, cause };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
export function classifyExit(evidence) {
|
|
67
|
+
if (evidence.spawnError !== undefined) {
|
|
68
|
+
const code = errorCode(evidence.spawnError);
|
|
69
|
+
const cause = code ? SPAWN_ERROR_CAUSES[code] : undefined;
|
|
70
|
+
if (cause)
|
|
71
|
+
return { kind: "permanent", cause, detail: `could not start (${code})` };
|
|
72
|
+
}
|
|
73
|
+
// A process killed by a signal says nothing about its own configuration.
|
|
74
|
+
if (evidence.signal)
|
|
75
|
+
return { kind: "transient" };
|
|
76
|
+
if (evidence.code === 0)
|
|
77
|
+
return { kind: "transient" };
|
|
78
|
+
if (evidence.code !== null) {
|
|
79
|
+
const byCode = EXIT_CODE_CAUSES[evidence.code];
|
|
80
|
+
if (byCode)
|
|
81
|
+
return { kind: "permanent", ...byCode };
|
|
82
|
+
}
|
|
83
|
+
const npm = evidence.outputTail ? permanentNpmCode(evidence.outputTail) : undefined;
|
|
84
|
+
if (npm)
|
|
85
|
+
return { kind: "permanent", cause: npm.cause, detail: `npm error code ${npm.code}` };
|
|
86
|
+
return { kind: "transient" };
|
|
87
|
+
}
|
|
88
|
+
//# sourceMappingURL=exit-cause.js.map
|
|
@@ -9,6 +9,8 @@ import { type MachineType } from "./machine-type.js";
|
|
|
9
9
|
import { type DaemonPaths } from "./paths.js";
|
|
10
10
|
/** A live daemon runs `unitId` (or held it after it was stopped from outside). */
|
|
11
11
|
export declare function daemonSupervises(paths: DaemonPaths, unitId: string, alive?: (pid: number) => boolean): boolean;
|
|
12
|
+
/** Current supervisor hold, including its explicit recovery command; never infer one from stale state. */
|
|
13
|
+
export declare function daemonHoldReason(paths: DaemonPaths, unitId: string, alive?: (pid: number) => boolean): string | null;
|
|
12
14
|
/**
|
|
13
15
|
* A live daemon runs the service `unitId` belongs to (`bots` for
|
|
14
16
|
* `bots:<name>`), so it will run this instance once its plan includes it. For
|
|
@@ -16,7 +16,7 @@ import { detectMachineType } from "./machine-type.js";
|
|
|
16
16
|
import { daemonPaths } from "./paths.js";
|
|
17
17
|
import { writeControlRequest } from "./requests.js";
|
|
18
18
|
/** Statuses where the daemon itself owns the unit's process. */
|
|
19
|
-
const OWNED = new Set(["running", "backoff", "exited", "idle", "stopped"]);
|
|
19
|
+
const OWNED = new Set(["running", "backoff", "exited", "idle", "stopped", "held"]);
|
|
20
20
|
const RECENT_HOSTED_DAEMON_MS = 5 * 60_000;
|
|
21
21
|
/** The updater's postinstall may run in the short gap before the desktop app relaunches its daemon. */
|
|
22
22
|
function recentlyHostedDaemon(paths) {
|
|
@@ -44,6 +44,11 @@ export function daemonSupervises(paths, unitId, alive = (pid) => isDaemonProcess
|
|
|
44
44
|
const unit = liveState(paths, alive)?.units?.find((u) => u.id === unitId);
|
|
45
45
|
return unit !== undefined && OWNED.has(unit.status);
|
|
46
46
|
}
|
|
47
|
+
/** Current supervisor hold, including its explicit recovery command; never infer one from stale state. */
|
|
48
|
+
export function daemonHoldReason(paths, unitId, alive = (pid) => isDaemonProcess(pid)) {
|
|
49
|
+
const unit = liveState(paths, alive)?.units?.find((u) => u.id === unitId);
|
|
50
|
+
return unit?.status === "held" ? unit.reason ?? `held; \`hq daemon restart ${unitId}\` retries` : null;
|
|
51
|
+
}
|
|
47
52
|
/**
|
|
48
53
|
* A live daemon runs the service `unitId` belongs to (`bots` for
|
|
49
54
|
* `bots:<name>`), so it will run this instance once its plan includes it. For
|
|
@@ -43,14 +43,31 @@ function updateRequestIsActive(requestPath) {
|
|
|
43
43
|
const pid = request.pid;
|
|
44
44
|
if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0)
|
|
45
45
|
throw new Error("The HQ CLI update request is invalid");
|
|
46
|
+
// Same staleness rule as src/utils/package-use-lease.ts: a request written
|
|
47
|
+
// before the current boot (pid may be reused), or whose pid is gone (ESRCH)
|
|
48
|
+
// or owned by another user (EPERM), is not a live updater.
|
|
49
|
+
let stale = false;
|
|
46
50
|
try {
|
|
47
|
-
|
|
51
|
+
stale = fs.statSync(requestPath).mtimeMs < Date.now() - os.uptime() * 1000 - 5_000;
|
|
48
52
|
}
|
|
49
53
|
catch (error) {
|
|
50
|
-
if (error.code === "
|
|
51
|
-
fs.rmSync(requestPath, { force: true });
|
|
54
|
+
if (error.code === "ENOENT")
|
|
52
55
|
return false;
|
|
56
|
+
throw new Error("Unable to read the HQ CLI update request");
|
|
57
|
+
}
|
|
58
|
+
if (!stale) {
|
|
59
|
+
try {
|
|
60
|
+
process.kill(pid, 0);
|
|
53
61
|
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
const code = error.code;
|
|
64
|
+
if (code === "ESRCH" || code === "EPERM")
|
|
65
|
+
stale = true;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (stale) {
|
|
69
|
+
fs.rmSync(requestPath, { force: true });
|
|
70
|
+
return false;
|
|
54
71
|
}
|
|
55
72
|
return true;
|
|
56
73
|
}
|
|
@@ -8,6 +8,8 @@ export interface DaemonPaths {
|
|
|
8
8
|
configPath: string;
|
|
9
9
|
remoteControlPoolsPath: string;
|
|
10
10
|
statePath: string;
|
|
11
|
+
/** Long-running units the supervisor holds down, with why (see unit-holds.ts). */
|
|
12
|
+
holdsPath: string;
|
|
11
13
|
lockPath: string;
|
|
12
14
|
logsDir: string;
|
|
13
15
|
requestsDir: string;
|
package/dist/lib/daemon/paths.js
CHANGED
|
@@ -14,6 +14,7 @@ export function daemonPaths(home = os.homedir(), env = process.env) {
|
|
|
14
14
|
configPath: path.join(root, "config.json"),
|
|
15
15
|
remoteControlPoolsPath: path.join(root, "remote-control-pools.json"),
|
|
16
16
|
statePath: path.join(root, "state.json"),
|
|
17
|
+
holdsPath: path.join(root, "holds.json"),
|
|
17
18
|
lockPath: path.join(root, "daemon.pid"),
|
|
18
19
|
logsDir,
|
|
19
20
|
requestsDir: path.join(root, "requests"),
|
package/dist/lib/daemon/run.js
CHANGED
|
@@ -56,6 +56,7 @@ import { adoptChild, createChildTracker, finishSuccession, handoffEligible, read
|
|
|
56
56
|
import { listSyncRunnerProcesses, readWatchRunnerOwnerStatus, syncHeartbeatTimeoutMs, syncRunnerNodeOptions, watchRunnerOwnerStatusPath, } from "./sync-runner.js";
|
|
57
57
|
import { STOP_ESCALATION_MS, SYNC_RUNNER_PERSIST_GRACE_MS, createSupervisor, } from "./supervisor.js";
|
|
58
58
|
import { userToolDirs } from "./tools.js";
|
|
59
|
+
import { logOffset, readLogSince, readUnitHolds, writeUnitHolds } from "./unit-holds.js";
|
|
59
60
|
import { daemonRunGuard } from "./lanes-reconcile-guard.js";
|
|
60
61
|
export const LOG_ROTATE_BYTES = 5 * 1024 * 1024;
|
|
61
62
|
/** Exit code for "restart me": non-zero so every service manager relaunches (Windows tasks only restart on failure). */
|
|
@@ -1002,10 +1003,13 @@ export async function runDaemon(opts) {
|
|
|
1002
1003
|
}
|
|
1003
1004
|
return plans;
|
|
1004
1005
|
};
|
|
1006
|
+
/** Where each unit's log ended when its current run started, so its exit is classified on that run's output only. */
|
|
1007
|
+
const unitLogOffsets = new Map();
|
|
1005
1008
|
const spawnUnit = (unit) => {
|
|
1006
1009
|
const command = unit.command;
|
|
1007
1010
|
if (!command || command.type === "internal")
|
|
1008
1011
|
throw new Error(`${unit.id} has no process to start`);
|
|
1012
|
+
unitLogOffsets.set(unit.id, logOffset(paths.unitLogPath(unit.id)));
|
|
1009
1013
|
let [file, args] = command.type === "hq"
|
|
1010
1014
|
? daemonHqChildCommand(opts.nodeBinary, opts.hqBinary, command.args, stagedHqChildBootstrap?.bootstrapPath ?? null, cliPackagePrefix, childBootstrapHandoffWatchFlag.status().enabled)
|
|
1011
1015
|
: command.type === "node"
|
|
@@ -1136,6 +1140,15 @@ export async function runDaemon(opts) {
|
|
|
1136
1140
|
restartDaemon: () => restartDaemon(),
|
|
1137
1141
|
runGuard: daemonRunGuard,
|
|
1138
1142
|
writeState: (state) => writeJsonAtomic(paths.statePath, state),
|
|
1143
|
+
holds: {
|
|
1144
|
+
read: () => readUnitHolds(paths.holdsPath),
|
|
1145
|
+
write: (holds) => writeUnitHolds(paths.holdsPath, holds),
|
|
1146
|
+
},
|
|
1147
|
+
exitOutputTail: (unitId) => {
|
|
1148
|
+
// An adopted unit was started by an earlier daemon; its output start is unknown.
|
|
1149
|
+
const offset = unitLogOffsets.get(unitId);
|
|
1150
|
+
return offset === undefined ? undefined : readLogSince(paths.unitLogPath(unitId), offset);
|
|
1151
|
+
},
|
|
1139
1152
|
log,
|
|
1140
1153
|
onSyncRunnerStopping: () => extendSyncShutdownCap?.(),
|
|
1141
1154
|
syncOwnerBlockReason: () => {
|
|
@@ -3,6 +3,11 @@
|
|
|
3
3
|
*
|
|
4
4
|
* - long-running units are restarted after they exit, with per-unit backoff
|
|
5
5
|
* (1 s doubling to 60 s, reset once a unit has stayed up for a minute);
|
|
6
|
+
* a unit that fails EPISODE_MAX_FAILURES times without a run of
|
|
7
|
+
* EPISODE_HEALTHY_RESET_MS, or fails for a permanent cause (see exit-cause.ts), is held
|
|
8
|
+
* instead. Permanent holds need a restart request, command change or CLI update.
|
|
9
|
+
* Exhausted transient sync/mesh units make one durable supervised probe after
|
|
10
|
+
* a 30-minute cooldown; a failed probe re-holds, and an explicit stop stays stopped;
|
|
6
11
|
* `on-failure` units stay down after a clean exit or a SIGTERM/SIGINT from
|
|
7
12
|
* outside, and any unit stays down after a stop request (`hq bot stop`),
|
|
8
13
|
* until a restart request;
|
|
@@ -26,6 +31,14 @@ import type { UnitPlan } from "./catalog.js";
|
|
|
26
31
|
import type { MachineType } from "./machine-type.js";
|
|
27
32
|
import type { ControlRequest } from "./requests.js";
|
|
28
33
|
import { syncPassResult } from "./sync-runner.js";
|
|
34
|
+
export declare const HEALTHY_RUN_MS = 60000;
|
|
35
|
+
/**
|
|
36
|
+
* Failures in one episode before the unit is held: the first start plus five
|
|
37
|
+
* automatic retries. Matches the Desktop app's MAX_FAILURES.
|
|
38
|
+
*/
|
|
39
|
+
export declare const EPISODE_MAX_FAILURES = 6;
|
|
40
|
+
/** A run this long ends the failure episode. Matches the Desktop app's HEALTHY_RUN_WINDOW. */
|
|
41
|
+
export declare const EPISODE_HEALTHY_RESET_MS: number;
|
|
29
42
|
export declare const STOP_ESCALATION_MS = 10000;
|
|
30
43
|
export declare const SYNC_RUNNER_PERSIST_GRACE_MS = 60000;
|
|
31
44
|
export declare const CLI_UPDATE_SYNC_IDLE_KILL_GRACE_MS = 60000;
|
|
@@ -42,7 +55,38 @@ export interface SupervisedChild {
|
|
|
42
55
|
on(event: "error", handler: (err: Error) => void): unknown;
|
|
43
56
|
kill(signal?: NodeJS.Signals | number): boolean;
|
|
44
57
|
}
|
|
45
|
-
type UnitStatus = "running" | "waiting" | "unavailable" | "backoff" | "exited" | "idle" | "stopped";
|
|
58
|
+
type UnitStatus = "running" | "waiting" | "unavailable" | "backoff" | "exited" | "idle" | "stopped" | "held";
|
|
59
|
+
/**
|
|
60
|
+
* Why a long-running unit is held down. `permanent`: the last exit had a cause
|
|
61
|
+
* a restart cannot fix. `exhausted`: EPISODE_MAX_FAILURES failures without a
|
|
62
|
+
* run of EPISODE_HEALTHY_RESET_MS. Persisted (see SupervisorDeps.holds)
|
|
63
|
+
* so a daemon restart keeps the unit held.
|
|
64
|
+
*/
|
|
65
|
+
export interface UnitHold {
|
|
66
|
+
kind: "permanent" | "exhausted";
|
|
67
|
+
/** A PermanentExitCause, or "transient" for an exhausted episode. */
|
|
68
|
+
cause: string;
|
|
69
|
+
detail: string;
|
|
70
|
+
/** Epoch ms when the hold started. */
|
|
71
|
+
since: number;
|
|
72
|
+
/** Failures in the episode that ended in the hold, the last one included. */
|
|
73
|
+
failures: number;
|
|
74
|
+
lastExit?: {
|
|
75
|
+
code: number | null;
|
|
76
|
+
signal: string | null;
|
|
77
|
+
at: number;
|
|
78
|
+
};
|
|
79
|
+
/** sha256 of the unit's plan key; a hold only applies to the command it was recorded for. */
|
|
80
|
+
keyHash: string;
|
|
81
|
+
/** Exhausted sync/mesh only: next durable supervised-probe deadline. */
|
|
82
|
+
retryAt?: number;
|
|
83
|
+
/** A probe claim remains durable until that running generation is healthy. */
|
|
84
|
+
probing?: boolean;
|
|
85
|
+
/** Explicit stop of a recovering unit; only an explicit restart clears it. */
|
|
86
|
+
retryStopped?: boolean;
|
|
87
|
+
}
|
|
88
|
+
/** The holds file: unit id to hold. */
|
|
89
|
+
export type PersistedHolds = Record<string, UnitHold>;
|
|
46
90
|
interface UnitState {
|
|
47
91
|
id: string;
|
|
48
92
|
service: string;
|
|
@@ -65,6 +109,8 @@ interface UnitState {
|
|
|
65
109
|
lastPassResult?: ReturnType<typeof syncPassResult>;
|
|
66
110
|
/** The pass-request folder the running child was started with; absent when it was not. */
|
|
67
111
|
passRequestDir?: string;
|
|
112
|
+
/** Also retained during a supervised recovery probe or its explicit manual stop. */
|
|
113
|
+
hold?: UnitHold;
|
|
68
114
|
}
|
|
69
115
|
/** An app that runs the daemon as its own child process instead of an OS unit. */
|
|
70
116
|
export type HostApp = "desktop";
|
|
@@ -119,6 +165,13 @@ export interface SupervisorDeps {
|
|
|
119
165
|
adopt?: readonly AdoptedUnit[];
|
|
120
166
|
/** Pre-run check for scheduled units that declare `runGuard`; a throw counts as `spawn`. */
|
|
121
167
|
runGuard?: (unit: UnitPlan) => "spawn" | "idle" | "skip";
|
|
168
|
+
/** What a long-running unit wrote during the run that just ended, for exit classification. */
|
|
169
|
+
exitOutputTail?: (unitId: string) => string | undefined;
|
|
170
|
+
/** Durable storage for unit holds, so a held unit stays held across daemon restarts. */
|
|
171
|
+
holds?: {
|
|
172
|
+
read(): PersistedHolds;
|
|
173
|
+
write(holds: PersistedHolds): void;
|
|
174
|
+
};
|
|
122
175
|
}
|
|
123
176
|
export interface Supervisor {
|
|
124
177
|
start(): void;
|