@indigoai-us/hq-cli 5.345.174 → 5.345.176
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 +29 -0
- package/dist/command-catalog.generated.d.ts +65 -0
- package/dist/command-catalog.generated.js +80 -0
- package/dist/commands/bot.d.ts +28 -2
- package/dist/commands/bot.js +32 -6
- package/dist/commands/daemon.js +16 -0
- package/dist/commands/dm.d.ts +7 -0
- package/dist/commands/dm.js +16 -4
- package/dist/commands/lanes-loop.d.ts +4 -1
- package/dist/commands/lanes-loop.js +23 -4
- package/dist/commands/lanes-shared.d.ts +5 -0
- package/dist/commands/lanes.d.ts +5 -7
- package/dist/commands/lanes.js +65 -5
- package/dist/commands/sync-core.d.ts +17 -0
- package/dist/commands/sync-core.js +46 -5
- package/dist/commands/sync-manifest.d.ts +6 -0
- package/dist/commands/sync-manifest.js +13 -0
- package/dist/hq-cloud-sync-runner/bin/sync-runner.js +448 -226
- package/dist/hq-cloud-sync-runner/package.json +1 -1
- package/dist/lib/bot/cli-update.d.ts +36 -0
- package/dist/lib/bot/cli-update.js +160 -42
- package/dist/lib/bot/run.d.ts +23 -0
- package/dist/lib/bot/run.js +174 -23
- package/dist/lib/bot/runtime/claude.d.ts +7 -1
- package/dist/lib/bot/runtime/claude.js +29 -0
- package/dist/lib/bot/runtime/codex.d.ts +5 -0
- package/dist/lib/bot/runtime/codex.js +13 -0
- package/dist/lib/bot/runtime/index.js +18 -2
- package/dist/lib/bot/runtime/types.d.ts +23 -0
- package/dist/lib/bot/runtime/types.js +13 -0
- package/dist/lib/bot/runtime-failure.d.ts +17 -6
- package/dist/lib/bot/runtime-failure.js +54 -9
- package/dist/lib/bot/runtime-sign-in-check.d.ts +32 -0
- package/dist/lib/bot/runtime-sign-in-check.js +53 -0
- package/dist/lib/bot/runtime-sign-in.d.ts +32 -3
- package/dist/lib/bot/runtime-sign-in.js +17 -2
- package/dist/lib/bot/status.d.ts +35 -1
- package/dist/lib/bot/status.js +28 -0
- package/dist/lib/daemon/catalog.d.ts +4 -0
- package/dist/lib/daemon/catalog.js +47 -19
- package/dist/lib/daemon/run.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/lanes/loop-disposition.d.ts +44 -0
- package/dist/lib/lanes/loop-disposition.js +302 -0
- package/dist/lib/lanes/loop.d.ts +16 -3
- package/dist/lib/lanes/loop.js +167 -60
- package/dist/lib/lanes/reconcile.d.ts +5 -0
- package/dist/lib/lanes/reconcile.js +79 -4
- package/dist/lib/mesh/live/daemon/work-subscribe.d.ts +3 -0
- package/dist/lib/mesh/live/daemon/work-subscribe.js +41 -12
- package/dist/lib/sync-core/engine-home.d.ts +11 -4
- package/dist/lib/sync-core/engine-home.js +22 -21
- package/dist/lib/sync-core/manifest-scopes.d.ts +15 -0
- package/dist/lib/sync-core/manifest-scopes.js +26 -0
- package/dist/lib/sync-core/reconcile.d.ts +2 -2
- package/dist/lib/sync-core/reconcile.js +70 -46
- package/dist/lib/sync-core/release.d.ts +2 -2
- package/dist/lib/sync-core/release.js +31 -6
- package/dist/lib/sync-core/rollout-state.d.ts +6 -3
- package/dist/lib/sync-core/rollout-state.js +26 -22
- package/package.json +8 -8
|
@@ -21,6 +21,7 @@ export const RUNTIME_REPAIR_ACTIONS = {
|
|
|
21
21
|
"cli-outdated": "update",
|
|
22
22
|
"model-unsupported": "switchModel",
|
|
23
23
|
transient: "tryAgain",
|
|
24
|
+
"runtime-crashed": "restart",
|
|
24
25
|
};
|
|
25
26
|
export function isRuntimeRepairClass(cls) {
|
|
26
27
|
return cls in RUNTIME_REPAIR_ACTIONS;
|
|
@@ -61,6 +62,38 @@ export function classifyRuntimeFailure(text) {
|
|
|
61
62
|
return "transient";
|
|
62
63
|
return "unknown";
|
|
63
64
|
}
|
|
65
|
+
/**
|
|
66
|
+
* Bun's process-level error line (Claude Code is a Bun-compiled binary):
|
|
67
|
+
* "error: An unknown error occurred (Unexpected)". The excerpt joins stderr
|
|
68
|
+
* lines with " | ", so each segment is checked on its own.
|
|
69
|
+
*/
|
|
70
|
+
const BUN_PROCESS_ERROR = /^error: .+ \((?:Unexpected|[A-Z][A-Za-z]+)\)\s*$/m;
|
|
71
|
+
export function isBunProcessError(text) {
|
|
72
|
+
return (text ?? "")
|
|
73
|
+
.split(/\n| \| /)
|
|
74
|
+
.some((segment) => BUN_PROCESS_ERROR.test(segment.trim()));
|
|
75
|
+
}
|
|
76
|
+
/** A process that exits non-zero this soon, with no stream event, never started its turn. */
|
|
77
|
+
export const RUNTIME_CRASH_MAX_MS = 5_000;
|
|
78
|
+
function bunShaped(e) {
|
|
79
|
+
return (isBunProcessError(typeof e.stderrExcerpt === "string" ? e.stderrExcerpt : "") ||
|
|
80
|
+
isBunProcessError(typeof e.stderrFull === "string" ? e.stderrFull : ""));
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* True when the coding tool's process exited before the turn started: a
|
|
84
|
+
* non-zero exit code, not stopped by the owner, no stream event on stdout,
|
|
85
|
+
* and either Bun's process error shape on stderr or a life under 5 s.
|
|
86
|
+
*/
|
|
87
|
+
export function isRuntimeCrash(err) {
|
|
88
|
+
if (!err || typeof err !== "object" || !("stderrExcerpt" in err))
|
|
89
|
+
return false;
|
|
90
|
+
const e = err;
|
|
91
|
+
if (typeof e.exitCode !== "number" || e.exitCode === 0)
|
|
92
|
+
return false;
|
|
93
|
+
if (e.aborted === true || e.sawStreamEvent === true)
|
|
94
|
+
return false;
|
|
95
|
+
return bunShaped(e) || (typeof e.durationMs === "number" && e.durationMs < RUNTIME_CRASH_MAX_MS);
|
|
96
|
+
}
|
|
64
97
|
/**
|
|
65
98
|
* Class of a failed turn's error. A RuntimeError's excerpt (the runtime's own
|
|
66
99
|
* reported error: failureReason, turn.failed, stderr) is the better evidence;
|
|
@@ -73,12 +106,20 @@ export function classifyRuntimeError(err) {
|
|
|
73
106
|
if (err && typeof err === "object" && "stderrExcerpt" in err) {
|
|
74
107
|
const e = err;
|
|
75
108
|
const message = typeof e.message === "string" ? e.message : "";
|
|
76
|
-
|
|
77
|
-
|
|
109
|
+
const crashed = isRuntimeCrash(err);
|
|
110
|
+
// Bun's own process error is the most specific evidence there is.
|
|
111
|
+
if (crashed && bunShaped(err))
|
|
112
|
+
return "runtime-crashed";
|
|
113
|
+
if (e.excerptIsOutputTail === true) {
|
|
114
|
+
const fromTailMessage = classifyRuntimeFailure(message);
|
|
115
|
+
return fromTailMessage === "unknown" && crashed ? "runtime-crashed" : fromTailMessage;
|
|
116
|
+
}
|
|
78
117
|
const fromExcerpt = classifyRuntimeFailure(typeof e.stderrExcerpt === "string" ? e.stderrExcerpt : "");
|
|
79
118
|
if (fromExcerpt !== "unknown")
|
|
80
119
|
return fromExcerpt;
|
|
81
|
-
|
|
120
|
+
const fromMessage = classifyRuntimeFailure(message);
|
|
121
|
+
// A quick exit with no known cause: the process died before the turn.
|
|
122
|
+
return fromMessage === "unknown" && crashed ? "runtime-crashed" : fromMessage;
|
|
82
123
|
}
|
|
83
124
|
return classifyRuntimeFailure(err instanceof Error ? err.message : String(err ?? ""));
|
|
84
125
|
}
|
|
@@ -106,14 +147,18 @@ export function runtimeFailureReason(cls, runtime) {
|
|
|
106
147
|
return `${tool} is busy or out of reach right now`;
|
|
107
148
|
case "not-installed":
|
|
108
149
|
return `${tool} isn't installed on this computer`;
|
|
150
|
+
case "runtime-crashed":
|
|
151
|
+
return `my ${tool} process could not start`;
|
|
109
152
|
default:
|
|
110
153
|
return `${tool} stopped with an error`;
|
|
111
154
|
}
|
|
112
155
|
}
|
|
113
156
|
/** The one next step, as a full sentence. */
|
|
114
|
-
export function runtimeFailureNextStep(cls, runtime, context = "message") {
|
|
157
|
+
export function runtimeFailureNextStep(cls, runtime, context = "message", botName) {
|
|
115
158
|
const tool = runtimeDisplayName(runtime);
|
|
116
159
|
switch (cls) {
|
|
160
|
+
case "runtime-crashed":
|
|
161
|
+
return `Press "Heal bot" in HQ, or run: hq bot restart ${botName ?? "<bot>"}`;
|
|
117
162
|
case "signed-out":
|
|
118
163
|
return `Use "Sign in to ${tool}" in HQ, then send me any message.`;
|
|
119
164
|
case "cli-outdated":
|
|
@@ -136,12 +181,12 @@ function capitalize(s) {
|
|
|
136
181
|
return s ? s[0].toUpperCase() + s.slice(1) : s;
|
|
137
182
|
}
|
|
138
183
|
/** "My Codex app is out of date. Update Codex, then send me any message." */
|
|
139
|
-
export function runtimeFailureText(cls, runtime, context = "message") {
|
|
140
|
-
return `${capitalize(runtimeFailureReason(cls, runtime))}. ${runtimeFailureNextStep(cls, runtime, context)}`;
|
|
184
|
+
export function runtimeFailureText(cls, runtime, context = "message", botName) {
|
|
185
|
+
return `${capitalize(runtimeFailureReason(cls, runtime))}. ${runtimeFailureNextStep(cls, runtime, context, botName)}`;
|
|
141
186
|
}
|
|
142
187
|
/** A failure after the model started (and maybe posted part of the answer). */
|
|
143
|
-
export function runtimeDidNotFinishText(cls, runtime, progressPosted) {
|
|
144
|
-
return `I didn't finish that. ${runtimeFailureText(cls, runtime)}${progressPosted ? " The messages above show how far I got." : ""}`;
|
|
188
|
+
export function runtimeDidNotFinishText(cls, runtime, progressPosted, botName) {
|
|
189
|
+
return `I didn't finish that. ${runtimeFailureText(cls, runtime, "message", botName)}${progressPosted ? " The messages above show how far I got." : ""}`;
|
|
145
190
|
}
|
|
146
191
|
export function runtimeRepairBlock(args) {
|
|
147
192
|
return {
|
|
@@ -177,7 +222,7 @@ export function withRetrySuggestion(text) {
|
|
|
177
222
|
* press.
|
|
178
223
|
*/
|
|
179
224
|
export function runtimeRepairNotice(args) {
|
|
180
|
-
const text = args.text ?? runtimeFailureText(args.cls, args.runtime, args.context);
|
|
225
|
+
const text = args.text ?? runtimeFailureText(args.cls, args.runtime, args.context, args.botName);
|
|
181
226
|
if (!isRuntimeRepairClass(args.cls))
|
|
182
227
|
return { cls: args.cls, text };
|
|
183
228
|
return {
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The bot's own look at its coding tool's sign-in, in the bot's environment.
|
|
3
|
+
*
|
|
4
|
+
* The owner's terminal and the bot are different processes with different
|
|
5
|
+
* environments: a bot started by launchd has none of the shell's variables,
|
|
6
|
+
* so Claude Code can be signed in for the terminal and signed out for the
|
|
7
|
+
* bot. The HQ app's own preflight runs with the app's environment and cannot
|
|
8
|
+
* see this either. Only the bot can, so it asks the tool itself (`claude auth
|
|
9
|
+
* status --json`, `codex login status`) with the exact environment its turns
|
|
10
|
+
* use, at start and before each turn, and reports the answer in status.json.
|
|
11
|
+
* The app reads it through `hq bot status --json` / `hq bot list --json`
|
|
12
|
+
* before it sends a brief job.
|
|
13
|
+
*/
|
|
14
|
+
import type { BotRuntime, SpawnLike } from "./runtime/types.js";
|
|
15
|
+
import { type RuntimeSignInCheck } from "./runtime-sign-in.js";
|
|
16
|
+
export interface SignInCheckOptions {
|
|
17
|
+
runtime: BotRuntime;
|
|
18
|
+
/** The environment the bot's turns spawn the tool with. */
|
|
19
|
+
env: NodeJS.ProcessEnv;
|
|
20
|
+
cwd: string;
|
|
21
|
+
spawn?: SpawnLike;
|
|
22
|
+
timeoutMs?: number;
|
|
23
|
+
now?: () => Date;
|
|
24
|
+
platform?: NodeJS.Platform;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Ask the tool whether it is signed in. Never throws: a tool that cannot be
|
|
28
|
+
* asked (not installed, hung, no status command) answers `signedIn: null`,
|
|
29
|
+
* and the turn then runs as before and reports its own failure.
|
|
30
|
+
*/
|
|
31
|
+
export declare function checkRuntimeSignIn(opts: SignInCheckOptions): Promise<RuntimeSignInCheck>;
|
|
32
|
+
//# sourceMappingURL=runtime-sign-in-check.d.ts.map
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The bot's own look at its coding tool's sign-in, in the bot's environment.
|
|
3
|
+
*
|
|
4
|
+
* The owner's terminal and the bot are different processes with different
|
|
5
|
+
* environments: a bot started by launchd has none of the shell's variables,
|
|
6
|
+
* so Claude Code can be signed in for the terminal and signed out for the
|
|
7
|
+
* bot. The HQ app's own preflight runs with the app's environment and cannot
|
|
8
|
+
* see this either. Only the bot can, so it asks the tool itself (`claude auth
|
|
9
|
+
* status --json`, `codex login status`) with the exact environment its turns
|
|
10
|
+
* use, at start and before each turn, and reports the answer in status.json.
|
|
11
|
+
* The app reads it through `hq bot status --json` / `hq bot list --json`
|
|
12
|
+
* before it sends a brief job.
|
|
13
|
+
*/
|
|
14
|
+
import { defaultSpawn } from "./runtime/index.js";
|
|
15
|
+
import { SIGN_IN_CHECK_TIMEOUT_MS } from "./runtime-sign-in.js";
|
|
16
|
+
/**
|
|
17
|
+
* Ask the tool whether it is signed in. Never throws: a tool that cannot be
|
|
18
|
+
* asked (not installed, hung, no status command) answers `signedIn: null`,
|
|
19
|
+
* and the turn then runs as before and reports its own failure.
|
|
20
|
+
*/
|
|
21
|
+
export async function checkRuntimeSignIn(opts) {
|
|
22
|
+
const checkedAt = (opts.now ?? (() => new Date()))().toISOString();
|
|
23
|
+
const status = opts.runtime.signInStatus;
|
|
24
|
+
if (!status)
|
|
25
|
+
return { signedIn: null, checkedAt, detail: `${opts.runtime.id} has no sign-in status command` };
|
|
26
|
+
const spawn = opts.spawn ?? defaultSpawn;
|
|
27
|
+
const env = { ...(opts.runtime.runtimeEnv?.(opts.env) ?? opts.env), HQ_NO_ENSURE_HQ_DESKTOP: "1" };
|
|
28
|
+
let binary;
|
|
29
|
+
try {
|
|
30
|
+
binary = opts.runtime.resolveBinary?.(env, opts.cwd, opts.platform ?? process.platform) ?? opts.runtime.binary;
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
binary = opts.runtime.binary;
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
const result = await spawn(binary, status.args, { cwd: opts.cwd, env, timeoutMs: opts.timeoutMs ?? SIGN_IN_CHECK_TIMEOUT_MS });
|
|
37
|
+
const signedIn = status.parse(result);
|
|
38
|
+
if (signedIn === null) {
|
|
39
|
+
const tail = `${result.stderr ?? ""}`.trim().split("\n").filter(Boolean).slice(-2).join(" | ").slice(0, 200);
|
|
40
|
+
return {
|
|
41
|
+
signedIn: null,
|
|
42
|
+
checkedAt,
|
|
43
|
+
detail: `${opts.runtime.id} ${status.args.join(" ")} did not say (exit ${result.code ?? "?"}${tail ? `: ${tail}` : ""})`,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
return { signedIn, checkedAt };
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
50
|
+
return { signedIn: null, checkedAt, detail: `${opts.runtime.id} ${status.args.join(" ")} could not run: ${message.slice(0, 200)}` };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=runtime-sign-in-check.js.map
|
|
@@ -13,12 +13,41 @@ import type { BotRuntimeId } from "./config.js";
|
|
|
13
13
|
export declare const SIGN_IN_RETRY_MS = 60000;
|
|
14
14
|
export declare function isRuntimeSignInFailure(text: string | undefined | null): boolean;
|
|
15
15
|
export declare function runtimeDisplayName(runtime: BotRuntimeId | string): string;
|
|
16
|
-
/**
|
|
16
|
+
/**
|
|
17
|
+
* Reported in status.json and `hq bot list --json` while the sign-in is broken.
|
|
18
|
+
*
|
|
19
|
+
* expired a turn failed because the tool's login no longer works.
|
|
20
|
+
* signed-out the bot's own check (`claude auth status` in the bot's
|
|
21
|
+
* environment, before a turn) found no login at all. A bot
|
|
22
|
+
* started by launchd has a bare environment, so it can be
|
|
23
|
+
* signed out while the owner's terminal is signed in.
|
|
24
|
+
*/
|
|
17
25
|
export interface RuntimeSignInIssue {
|
|
18
|
-
state: "expired";
|
|
26
|
+
state: "expired" | "signed-out";
|
|
19
27
|
runtime: BotRuntimeId;
|
|
20
28
|
since: string;
|
|
21
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* What the bot's own sign-in check last said, in the bot's environment.
|
|
32
|
+
* `signedIn` is null when the tool could not be asked (not installed, timed
|
|
33
|
+
* out, a runtime with no status command).
|
|
34
|
+
*/
|
|
35
|
+
export interface RuntimeSignInCheck {
|
|
36
|
+
signedIn: boolean | null;
|
|
37
|
+
checkedAt: string;
|
|
38
|
+
/** Why the check could not tell, for the log and the JSON output. */
|
|
39
|
+
detail?: string;
|
|
40
|
+
}
|
|
41
|
+
/** How long a passed check stands in for another before a turn. */
|
|
42
|
+
export declare const SIGN_IN_CHECK_FRESH_MS = 30000;
|
|
43
|
+
/** How often the bot asks the tool again while it is signed out. */
|
|
44
|
+
export declare const SIGN_IN_CHECK_MS = 15000;
|
|
45
|
+
/** The tool's status command may not hang a turn. */
|
|
46
|
+
export declare const SIGN_IN_CHECK_TIMEOUT_MS = 15000;
|
|
47
|
+
/** The terminal command that signs the tool in on this computer. */
|
|
48
|
+
export declare function signInCommand(runtime: BotRuntimeId | string): string;
|
|
49
|
+
/** The excerpt a turn held by the pre-turn check records (classifies as signed-out). */
|
|
50
|
+
export declare function signedOutExcerpt(runtime: BotRuntimeId | string): string;
|
|
22
51
|
/** The one message a conversation gets when the bot cannot work until its tool signs in again. */
|
|
23
|
-
export declare function signInNeededText(runtime: BotRuntimeId | string, kickoff?: boolean): string;
|
|
52
|
+
export declare function signInNeededText(runtime: BotRuntimeId | string, kickoff?: boolean, state?: RuntimeSignInIssue["state"]): string;
|
|
24
53
|
//# sourceMappingURL=runtime-sign-in.d.ts.map
|
|
@@ -22,10 +22,25 @@ export function isRuntimeSignInFailure(text) {
|
|
|
22
22
|
export function runtimeDisplayName(runtime) {
|
|
23
23
|
return runtime === "claude" ? "Claude Code" : runtime === "codex" ? "Codex" : runtime === "grok" ? "Grok" : runtime;
|
|
24
24
|
}
|
|
25
|
+
/** How long a passed check stands in for another before a turn. */
|
|
26
|
+
export const SIGN_IN_CHECK_FRESH_MS = 30_000;
|
|
27
|
+
/** How often the bot asks the tool again while it is signed out. */
|
|
28
|
+
export const SIGN_IN_CHECK_MS = 15_000;
|
|
29
|
+
/** The tool's status command may not hang a turn. */
|
|
30
|
+
export const SIGN_IN_CHECK_TIMEOUT_MS = 15_000;
|
|
31
|
+
/** The terminal command that signs the tool in on this computer. */
|
|
32
|
+
export function signInCommand(runtime) {
|
|
33
|
+
return runtime === "claude" ? "claude auth login" : runtime === "codex" ? "codex login" : runtime === "grok" ? "grok login" : `${runtime} login`;
|
|
34
|
+
}
|
|
35
|
+
/** The excerpt a turn held by the pre-turn check records (classifies as signed-out). */
|
|
36
|
+
export function signedOutExcerpt(runtime) {
|
|
37
|
+
return `${runtimeDisplayName(runtime)} is not logged in on this computer (checked before the turn)`;
|
|
38
|
+
}
|
|
25
39
|
/** The one message a conversation gets when the bot cannot work until its tool signs in again. */
|
|
26
|
-
export function signInNeededText(runtime, kickoff = false) {
|
|
40
|
+
export function signInNeededText(runtime, kickoff = false, state = "expired") {
|
|
27
41
|
const tool = runtimeDisplayName(runtime);
|
|
28
|
-
|
|
42
|
+
const why = state === "signed-out" ? `${tool} isn't signed in on this computer` : `${tool} needs you to sign in again on this computer`;
|
|
43
|
+
return (`I can't work right now: ${why}. ` +
|
|
29
44
|
`Use "Sign in to ${tool}" in HQ, and I'll ${kickoff ? "get started" : "answer this"} as soon as it works again. No need to send it twice.`);
|
|
30
45
|
}
|
|
31
46
|
//# sourceMappingURL=runtime-sign-in.js.map
|
package/dist/lib/bot/status.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* status.json — atomic, metadata-only runtime status (local-bots US-003).
|
|
3
3
|
*/
|
|
4
4
|
import type { BotRuntimeId } from "./config.js";
|
|
5
|
-
import type { RuntimeSignInIssue } from "./runtime-sign-in.js";
|
|
5
|
+
import type { RuntimeSignInCheck, RuntimeSignInIssue } from "./runtime-sign-in.js";
|
|
6
6
|
export type BotState = "starting" | "running" | "stopped" | "failed";
|
|
7
7
|
export interface BotStatusFile {
|
|
8
8
|
v: 1;
|
|
@@ -44,6 +44,12 @@ export interface BotStatusFile {
|
|
|
44
44
|
* reason); cleared by the next turn that works. `null` = known to work.
|
|
45
45
|
*/
|
|
46
46
|
runtimeSignIn?: RuntimeSignInIssue | null;
|
|
47
|
+
/**
|
|
48
|
+
* The bot's own last look at its coding tool's sign-in, in the bot's
|
|
49
|
+
* environment (at start, before a turn, and while signed out). What
|
|
50
|
+
* `hq bot status --json` reports as `runtimeSignedIn`.
|
|
51
|
+
*/
|
|
52
|
+
runtimeSignInCheck?: RuntimeSignInCheck | null;
|
|
47
53
|
/**
|
|
48
54
|
* Set when the installed coding tool is too old for the model it was asked
|
|
49
55
|
* to use and the last turn ran on `to` instead (`from` is "" when the model
|
|
@@ -55,6 +61,18 @@ export interface BotStatusFile {
|
|
|
55
61
|
to: string;
|
|
56
62
|
at: string;
|
|
57
63
|
} | null;
|
|
64
|
+
/**
|
|
65
|
+
* The last time the coding tool's process exited before its turn started
|
|
66
|
+
* (runtime-crashed): when, exit code, how long it lived, and its stderr.
|
|
67
|
+
*/
|
|
68
|
+
lastCrash?: {
|
|
69
|
+
at: string;
|
|
70
|
+
code: number | null;
|
|
71
|
+
durationMs?: number;
|
|
72
|
+
stderr: string;
|
|
73
|
+
};
|
|
74
|
+
/** When the last model turn that worked had STARTED (ISO); a later crash is not cleared by it. */
|
|
75
|
+
lastTurnOkAt?: string;
|
|
58
76
|
/**
|
|
59
77
|
* Set while the bot holds its model turns after FAILURE_LIMIT failures in a
|
|
60
78
|
* window (it used to exit instead, and a bot that exited could neither load
|
|
@@ -73,6 +91,22 @@ export declare function defaultBotStatus(pid: number, runtime: BotRuntimeId, now
|
|
|
73
91
|
export declare function readBotStatus(dir: string): BotStatusFile | null;
|
|
74
92
|
export declare function writeBotStatus(dir: string, status: BotStatusFile): void;
|
|
75
93
|
export declare function patchBotStatus(dir: string, patch: Partial<BotStatusFile>, fallback: () => BotStatusFile, now?: () => Date): BotStatusFile;
|
|
94
|
+
/** A coding-tool crash the bot has not recovered from yet (see `runtimeCrashSinceStart`). */
|
|
95
|
+
export interface RuntimeCrash {
|
|
96
|
+
at: string;
|
|
97
|
+
code: number | null;
|
|
98
|
+
durationMs?: number;
|
|
99
|
+
/** First line of the tool's stderr, at most 200 characters. */
|
|
100
|
+
message: string;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* The coding tool crashed before its turn started, in this bot process, and
|
|
104
|
+
* no turn has worked since. Null when there is no such crash: none recorded,
|
|
105
|
+
* one from before the bot last started (a restart is the fix people reach
|
|
106
|
+
* for, so it clears this), or one followed by a good turn. Surfaces read it
|
|
107
|
+
* to warn before sending the bot work it would fail.
|
|
108
|
+
*/
|
|
109
|
+
export declare function runtimeCrashSinceStart(status: BotStatusFile | null | undefined): RuntimeCrash | null;
|
|
76
110
|
/** Live process check for a status file's pid. */
|
|
77
111
|
export declare function isPidAlive(pid: number): boolean;
|
|
78
112
|
//# sourceMappingURL=status.d.ts.map
|
package/dist/lib/bot/status.js
CHANGED
|
@@ -51,6 +51,34 @@ export function patchBotStatus(dir, patch, fallback, now = () => new Date()) {
|
|
|
51
51
|
writeBotStatus(dir, next);
|
|
52
52
|
return next;
|
|
53
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* The coding tool crashed before its turn started, in this bot process, and
|
|
56
|
+
* no turn has worked since. Null when there is no such crash: none recorded,
|
|
57
|
+
* one from before the bot last started (a restart is the fix people reach
|
|
58
|
+
* for, so it clears this), or one followed by a good turn. Surfaces read it
|
|
59
|
+
* to warn before sending the bot work it would fail.
|
|
60
|
+
*/
|
|
61
|
+
export function runtimeCrashSinceStart(status) {
|
|
62
|
+
const crash = status?.lastCrash;
|
|
63
|
+
if (!crash)
|
|
64
|
+
return null;
|
|
65
|
+
const at = Date.parse(crash.at);
|
|
66
|
+
if (!Number.isFinite(at))
|
|
67
|
+
return null;
|
|
68
|
+
const started = Date.parse(status.startedAt);
|
|
69
|
+
if (Number.isFinite(started) && at < started)
|
|
70
|
+
return null;
|
|
71
|
+
const ok = status.lastTurnOkAt ? Date.parse(status.lastTurnOkAt) : Number.NaN;
|
|
72
|
+
if (Number.isFinite(ok) && ok >= at)
|
|
73
|
+
return null;
|
|
74
|
+
const message = (crash.stderr ?? "").split("\n").map((l) => l.trim()).find(Boolean)?.slice(0, 200) ?? "";
|
|
75
|
+
return {
|
|
76
|
+
at: crash.at,
|
|
77
|
+
code: crash.code,
|
|
78
|
+
...(crash.durationMs !== undefined ? { durationMs: crash.durationMs } : {}),
|
|
79
|
+
message,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
54
82
|
/** Live process check for a status file's pid. */
|
|
55
83
|
export function isPidAlive(pid) {
|
|
56
84
|
if (!Number.isInteger(pid) || pid <= 0)
|
|
@@ -92,6 +92,8 @@ export interface SyncCoreRunning {
|
|
|
92
92
|
}
|
|
93
93
|
export declare const SYNC_CORE_RECONCILE_UNIT = "sync-core";
|
|
94
94
|
export declare const SYNC_CORE_RECONCILE_INTERVAL_SEC = 600;
|
|
95
|
+
export declare const SYNC_CORE_MANIFEST_UNIT = "sync-core-manifest";
|
|
96
|
+
export declare const SYNC_CORE_MANIFEST_INTERVAL_SEC: number;
|
|
95
97
|
export declare const SYNC_CORE_LEGACY_SETTLE_MS: number;
|
|
96
98
|
export interface PlanContext {
|
|
97
99
|
machineType: MachineType;
|
|
@@ -126,6 +128,8 @@ export interface PlanContext {
|
|
|
126
128
|
now?: number;
|
|
127
129
|
/** A legacy desktop app or sync unit is still managing sync. */
|
|
128
130
|
desktopAppManagesSync: boolean;
|
|
131
|
+
/** A legacy sync unit (not the desktop app) that the daemon cannot migrate is installed. */
|
|
132
|
+
legacySyncUnitManagesSync?: boolean;
|
|
129
133
|
/** Full process list, or null when the daemon could not read it. */
|
|
130
134
|
syncRunnerProcesses: readonly SyncRunnerProcess[] | null;
|
|
131
135
|
/** PID recorded in the watch-owner status, when it is readable. */
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import * as path from "node:path";
|
|
13
13
|
import { syncCoreUnitId } from "../sync-core/engine-home.js";
|
|
14
|
+
import { engineOwnedManifestScopes } from "../sync-core/manifest-scopes.js";
|
|
14
15
|
import { SYNC_RUNNER_OWNER, syncRunnerUnleasedBlockReason } from "./sync-runner.js";
|
|
15
16
|
import { vaultSkipEnv } from "../vault-sync-switches.js";
|
|
16
17
|
export const SERVICE_NAMES = [
|
|
@@ -82,10 +83,14 @@ export const DEFAULT_SERVICES = {
|
|
|
82
83
|
/** Defaults plus this host's overrides, in catalog order. Unknown names are ignored. */
|
|
83
84
|
export function enabledServices(machineType, overrides) {
|
|
84
85
|
const defaults = DEFAULT_SERVICES[machineType];
|
|
85
|
-
|
|
86
|
+
const wanted = (s) => (defaults.includes(s) || overrides.enable.includes(s)) && !overrides.disable.includes(s);
|
|
87
|
+
const wakeSubscriber = wanted("sync-core") && !overrides.disable.includes("mesh");
|
|
88
|
+
return SERVICE_NAMES.filter((s) => wanted(s) || (s === "mesh" && wakeSubscriber));
|
|
86
89
|
}
|
|
87
90
|
export const SYNC_CORE_RECONCILE_UNIT = "sync-core";
|
|
88
91
|
export const SYNC_CORE_RECONCILE_INTERVAL_SEC = 600;
|
|
92
|
+
export const SYNC_CORE_MANIFEST_UNIT = "sync-core-manifest";
|
|
93
|
+
export const SYNC_CORE_MANIFEST_INTERVAL_SEC = 24 * 3600;
|
|
89
94
|
export const SYNC_CORE_LEGACY_SETTLE_MS = 10 * 60 * 1000;
|
|
90
95
|
function base(service, over = {}) {
|
|
91
96
|
return {
|
|
@@ -127,6 +132,20 @@ export const CLAUDE_RUNNER_SCRIPT = "/usr/local/bin/outpost-runner.sh";
|
|
|
127
132
|
const NO_HQ_ROOT = "no HQ folder configured (run `hq daemon install --hq-root <path>`)";
|
|
128
133
|
const NOT_SIGNED_IN = "not signed in (run `hq login`)";
|
|
129
134
|
const KIT_NOT_SET_UP = "the agent kit is not set up on this machine (run `hq agent kit install`)";
|
|
135
|
+
function syncOwnerBlock(ctx) {
|
|
136
|
+
if (ctx.machineType === "desktop" && ctx.syncOwner === "desktop-app" && ctx.desktopAppManagesSync) {
|
|
137
|
+
return "the desktop app is still managing sync (run `hq daemon sync take-ownership` after it stops its runner)";
|
|
138
|
+
}
|
|
139
|
+
if (ctx.machineType === "desktop" && ctx.syncOwner === "daemon" && !ctx.syncPaused && ctx.hqRoot && ctx.signedIn) {
|
|
140
|
+
return ctx.syncRunnerProcesses === null
|
|
141
|
+
? "could not check for an existing sync runner; waiting to avoid starting a duplicate"
|
|
142
|
+
: syncRunnerUnleasedBlockReason(ctx.hqRoot, ctx.syncRunnerProcesses, ctx.syncRunnerOwnerPid, ctx.platform, ctx.daemonSyncRunnerPids);
|
|
143
|
+
}
|
|
144
|
+
return undefined;
|
|
145
|
+
}
|
|
146
|
+
function daemonAloneRunsLegacySync(ctx) {
|
|
147
|
+
return ctx.syncOwner === "daemon" && ctx.legacySyncUnitManagesSync !== true && syncOwnerBlock(ctx) === undefined;
|
|
148
|
+
}
|
|
130
149
|
function planService(service, ctx) {
|
|
131
150
|
const agentMachine = ctx.machineType === "agent-kit" || ctx.machineType === "agent-box";
|
|
132
151
|
switch (service) {
|
|
@@ -139,19 +158,7 @@ function planService(service, ctx) {
|
|
|
139
158
|
offCompanies: new Set([...(ctx.vaultSync?.offCompanies ?? []), ...running.companies]),
|
|
140
159
|
}
|
|
141
160
|
: ctx.vaultSync;
|
|
142
|
-
|
|
143
|
-
if (ctx.machineType === "desktop" && ctx.syncOwner === "desktop-app" && ctx.desktopAppManagesSync) {
|
|
144
|
-
ownerBlock = "the desktop app is still managing sync (run `hq daemon sync take-ownership` after it stops its runner)";
|
|
145
|
-
}
|
|
146
|
-
else if (ctx.machineType === "desktop" &&
|
|
147
|
-
ctx.syncOwner === "daemon" &&
|
|
148
|
-
!ctx.syncPaused &&
|
|
149
|
-
ctx.hqRoot &&
|
|
150
|
-
ctx.signedIn) {
|
|
151
|
-
ownerBlock = ctx.syncRunnerProcesses === null
|
|
152
|
-
? "could not check for an existing sync runner; waiting to avoid starting a duplicate"
|
|
153
|
-
: syncRunnerUnleasedBlockReason(ctx.hqRoot, ctx.syncRunnerProcesses, ctx.syncRunnerOwnerPid, ctx.platform, ctx.daemonSyncRunnerPids);
|
|
154
|
-
}
|
|
161
|
+
const ownerBlock = syncOwnerBlock(ctx);
|
|
155
162
|
const commonBlock = firstBlock([
|
|
156
163
|
[ctx.syncPaused, "sync is paused (run `hq daemon sync resume`)"],
|
|
157
164
|
[
|
|
@@ -219,7 +226,8 @@ function planService(service, ctx) {
|
|
|
219
226
|
: ctx.vaultSync?.offCompanies.has(vault.slug ?? "") === true;
|
|
220
227
|
const offSince = vault.legacyOffSince ? Date.parse(vault.legacyOffSince) : Number.NaN;
|
|
221
228
|
const settled = Number.isFinite(offSince) && now - offSince >= SYNC_CORE_LEGACY_SETTLE_MS;
|
|
222
|
-
const
|
|
229
|
+
const settleNeeded = ctx.desktopAppManagesSync && !daemonAloneRunsLegacySync(ctx);
|
|
230
|
+
const legacyReady = ctx.legacySyncReadyForCore === true && (!settleNeeded || settled);
|
|
223
231
|
const what = vault.kind === "personal" ? "the personal vault" : `the ${vault.slug} vault`;
|
|
224
232
|
return base(service, {
|
|
225
233
|
id: syncCoreUnitId(vault.uid),
|
|
@@ -232,11 +240,10 @@ function planService(service, ctx) {
|
|
|
232
240
|
HQ_STATE_DIR: path.join(ctx.home, ".hq"),
|
|
233
241
|
HQ_SYNC_ENGINE: "rust-test",
|
|
234
242
|
HQ_SYNC_MODE: full ? "full" : "readonly",
|
|
235
|
-
...(full ? { HQ_SYNC_FULL_VAULT: "1" } : {}),
|
|
236
|
-
HQ_SYNC_PROTOCOL: "1",
|
|
243
|
+
...(full ? { HQ_SYNC_FULL_VAULT: "1", HQ_SYNC_PROTOCOL: "1" } : {}),
|
|
237
244
|
HQ_SYNC_CORE_RELEASE: vault.release,
|
|
238
245
|
},
|
|
239
|
-
heartbeatTimeoutMs: 180_000,
|
|
246
|
+
...(full ? { heartbeatTimeoutMs: 180_000 } : {}),
|
|
240
247
|
blocked: firstBlock([
|
|
241
248
|
[ctx.syncPaused, "sync is paused (run `hq daemon sync resume`)"],
|
|
242
249
|
[ctx.vaultSync?.allOff === true, "cloud sync is off for every vault on this computer (turn it back on in HQ Settings > Sync)"],
|
|
@@ -250,7 +257,28 @@ function planService(service, ctx) {
|
|
|
250
257
|
]),
|
|
251
258
|
});
|
|
252
259
|
});
|
|
253
|
-
|
|
260
|
+
const manifestScopes = engineOwnedManifestScopes(ctx.vaultSync, ctx.syncCoreVaults);
|
|
261
|
+
if (manifestScopes.length === 0)
|
|
262
|
+
return [reconcile, ...engines];
|
|
263
|
+
const manifest = base(service, {
|
|
264
|
+
id: SYNC_CORE_MANIFEST_UNIT,
|
|
265
|
+
description: `uploads a daily sync-audit manifest for ${manifestScopes.map((scope) => scope.kind === "personal" ? "the personal vault" : `the ${scope.slug} vault`).join(", ")}`,
|
|
266
|
+
kind: "scheduled",
|
|
267
|
+
intervalSec: SYNC_CORE_MANIFEST_INTERVAL_SEC,
|
|
268
|
+
initialDelaySec: 300,
|
|
269
|
+
command: {
|
|
270
|
+
type: "hq",
|
|
271
|
+
args: ["daemon", "service", "sync-core-manifest", ...(ctx.hqRoot ? ["--hq-root", ctx.hqRoot] : [])],
|
|
272
|
+
},
|
|
273
|
+
env: { HQ_NO_UPDATE_CHECK: "1" },
|
|
274
|
+
backgroundQos: true,
|
|
275
|
+
blocked: firstBlock([
|
|
276
|
+
[ctx.syncPaused, "sync is paused (run `hq daemon sync resume`)"],
|
|
277
|
+
[!ctx.hqRoot, NO_HQ_ROOT],
|
|
278
|
+
[!ctx.signedIn, NOT_SIGNED_IN],
|
|
279
|
+
]),
|
|
280
|
+
});
|
|
281
|
+
return [reconcile, ...engines, manifest];
|
|
254
282
|
}
|
|
255
283
|
case "mesh": {
|
|
256
284
|
const args = ["mesh", "daemon", "run", ...(ctx.hqRoot ? ["--hq-root", ctx.hqRoot] : [])];
|
package/dist/lib/daemon/run.js
CHANGED
|
@@ -1021,6 +1021,7 @@ export async function runDaemon(opts) {
|
|
|
1021
1021
|
syncOwner,
|
|
1022
1022
|
syncCoreVaults: syncCoreVaultPlans(home, hqRoot, platform),
|
|
1023
1023
|
desktopAppManagesSync: appManagesSync,
|
|
1024
|
+
legacySyncUnitManagesSync: legacy.some((u) => u.service === "sync" && !u.migratable),
|
|
1024
1025
|
syncRunnerProcesses,
|
|
1025
1026
|
syncRunnerOwnerPid,
|
|
1026
1027
|
daemonSyncRunnerPids,
|