@indigoai-us/hq-cli 5.345.173 → 5.345.175

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.
@@ -19,6 +19,14 @@ export class RuntimeError extends Error {
19
19
  * must never decide what went wrong; only the message may.
20
20
  */
21
21
  excerptIsOutputTail;
22
+ /** How long the runtime process lived, in ms, when known. */
23
+ durationMs;
24
+ /** True when stdout carried at least one stream event (`system`, `assistant`, `result`): the turn started. */
25
+ sawStreamEvent;
26
+ /** Crash evidence for the bot log: the binary spawned, full stderr and the head of stdout (bounded). */
27
+ binaryPath;
28
+ stderrFull;
29
+ stdoutHead;
22
30
  constructor(message, stderrExcerpt, exitCode, opts = {}) {
23
31
  super(message);
24
32
  this.stderrExcerpt = stderrExcerpt;
@@ -27,6 +35,11 @@ export class RuntimeError extends Error {
27
35
  this.started = opts.started ?? true;
28
36
  this.aborted = opts.aborted ?? false;
29
37
  this.excerptIsOutputTail = opts.excerptIsOutputTail ?? false;
38
+ this.durationMs = opts.durationMs;
39
+ this.sawStreamEvent = opts.sawStreamEvent ?? false;
40
+ this.binaryPath = opts.binaryPath;
41
+ this.stderrFull = opts.stderrFull;
42
+ this.stdoutHead = opts.stdoutHead;
30
43
  }
31
44
  }
32
45
  /**
@@ -27,16 +27,27 @@ export type RuntimeFailureClass =
27
27
  | "transient"
28
28
  /** The tool is not installed (or not on PATH) on this computer. */
29
29
  | "not-installed"
30
+ /** The tool's process exited before the turn started (e.g. Bun's "error: ... (Unexpected)"). A restart heals it. */
31
+ | "runtime-crashed"
30
32
  /** Anything else. Neutral copy; never blames the sign-in. */
31
33
  | "unknown";
32
- /** The four classes that get a repair card in the DM. */
33
- export type RuntimeRepairClass = Extract<RuntimeFailureClass, "signed-out" | "cli-outdated" | "model-unsupported" | "transient">;
34
+ /** The classes that get a repair card in the DM. */
35
+ export type RuntimeRepairClass = Extract<RuntimeFailureClass, "signed-out" | "cli-outdated" | "model-unsupported" | "transient" | "runtime-crashed">;
34
36
  /** The one thing the person can do about it. The desktop maps each to a button. */
35
- export type RuntimeRepairAction = "signIn" | "update" | "switchModel" | "tryAgain";
37
+ export type RuntimeRepairAction = "signIn" | "update" | "switchModel" | "tryAgain" | "restart";
36
38
  export declare const RUNTIME_REPAIR_ACTIONS: Record<RuntimeRepairClass, RuntimeRepairAction>;
37
39
  export declare function isRuntimeRepairClass(cls: RuntimeFailureClass): cls is RuntimeRepairClass;
38
40
  /** Sort a runtime's failure text into one class. Order matters: most specific first. */
39
41
  export declare function classifyRuntimeFailure(text: string | undefined | null): RuntimeFailureClass;
42
+ export declare function isBunProcessError(text: string | undefined | null): boolean;
43
+ /** A process that exits non-zero this soon, with no stream event, never started its turn. */
44
+ export declare const RUNTIME_CRASH_MAX_MS = 5000;
45
+ /**
46
+ * True when the coding tool's process exited before the turn started: a
47
+ * non-zero exit code, not stopped by the owner, no stream event on stdout,
48
+ * and either Bun's process error shape on stderr or a life under 5 s.
49
+ */
50
+ export declare function isRuntimeCrash(err: unknown): boolean;
40
51
  /**
41
52
  * Class of a failed turn's error. A RuntimeError's excerpt (the runtime's own
42
53
  * reported error: failureReason, turn.failed, stderr) is the better evidence;
@@ -57,11 +68,11 @@ export type RuntimeFailureContext = "kickoff" | "message";
57
68
  */
58
69
  export declare function runtimeFailureReason(cls: RuntimeFailureClass, runtime: BotRuntimeId | string): string;
59
70
  /** The one next step, as a full sentence. */
60
- export declare function runtimeFailureNextStep(cls: RuntimeFailureClass, runtime: BotRuntimeId | string, context?: RuntimeFailureContext): string;
71
+ export declare function runtimeFailureNextStep(cls: RuntimeFailureClass, runtime: BotRuntimeId | string, context?: RuntimeFailureContext, botName?: string): string;
61
72
  /** "My Codex app is out of date. Update Codex, then send me any message." */
62
- export declare function runtimeFailureText(cls: RuntimeFailureClass, runtime: BotRuntimeId | string, context?: RuntimeFailureContext): string;
73
+ export declare function runtimeFailureText(cls: RuntimeFailureClass, runtime: BotRuntimeId | string, context?: RuntimeFailureContext, botName?: string): string;
63
74
  /** A failure after the model started (and maybe posted part of the answer). */
64
- export declare function runtimeDidNotFinishText(cls: RuntimeFailureClass, runtime: BotRuntimeId | string, progressPosted: boolean): string;
75
+ export declare function runtimeDidNotFinishText(cls: RuntimeFailureClass, runtime: BotRuntimeId | string, progressPosted: boolean, botName?: string): string;
65
76
  /**
66
77
  * The machine-readable repair payload a failure DM carries. The DM transport
67
78
  * (POST /v1/notify/dm) stores structured data only in `richContent`
@@ -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
- if (e.excerptIsOutputTail === true)
77
- return classifyRuntimeFailure(message);
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
- return classifyRuntimeFailure(message);
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 {
@@ -55,6 +55,16 @@ export interface BotStatusFile {
55
55
  to: string;
56
56
  at: string;
57
57
  } | null;
58
+ /**
59
+ * The last time the coding tool's process exited before its turn started
60
+ * (runtime-crashed): when, exit code, how long it lived, and its stderr.
61
+ */
62
+ lastCrash?: {
63
+ at: string;
64
+ code: number | null;
65
+ durationMs?: number;
66
+ stderr: string;
67
+ };
58
68
  /**
59
69
  * Set while the bot holds its model turns after FAILURE_LIMIT failures in a
60
70
  * window (it used to exit instead, and a bot that exited could neither load
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.345.173",
3
+ "version": "5.345.175",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -114,11 +114,11 @@
114
114
  "*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}": "eslint --cache --no-error-on-unmatched-pattern"
115
115
  },
116
116
  "optionalDependencies": {
117
- "@indigoai-us/hq-hook-win32-x64": "5.345.173",
118
- "@indigoai-us/hq-hook-win32-arm64": "5.345.173",
119
- "@indigoai-us/hq-hook-darwin-x64": "5.345.173",
120
- "@indigoai-us/hq-hook-darwin-arm64": "5.345.173",
121
- "@indigoai-us/hq-hook-linux-x64": "5.345.173",
122
- "@indigoai-us/hq-hook-linux-arm64": "5.345.173"
117
+ "@indigoai-us/hq-hook-win32-x64": "5.345.175",
118
+ "@indigoai-us/hq-hook-win32-arm64": "5.345.175",
119
+ "@indigoai-us/hq-hook-darwin-x64": "5.345.175",
120
+ "@indigoai-us/hq-hook-darwin-arm64": "5.345.175",
121
+ "@indigoai-us/hq-hook-linux-x64": "5.345.175",
122
+ "@indigoai-us/hq-hook-linux-arm64": "5.345.175"
123
123
  }
124
124
  }