@indigoai-us/hq-cli 5.127.0 → 5.128.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 CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.128.0] — 2026-09-22
6
+
7
+ ### Fixed
8
+
9
+ - Expected hook-timeout warnings are counted as Sentry metrics, so a slow hook
10
+ does not create its own Sentry issue. Genuine timeouts use stable grouping.
11
+
5
12
  ## [5.127.0] — 2026-09-22
6
13
 
7
14
  ### Added
@@ -12,6 +12,7 @@
12
12
  * than truncated. Unknown scalar metadata is dropped instead: the allowlist,
13
13
  * not a caller version match, determines what may leave the machine.
14
14
  */
15
+ import * as Sentry from "@sentry/node";
15
16
  import type { CaptureContext, SeverityLevel } from "@sentry/node";
16
17
  export declare const DEFAULT_SENTRY_REPORT_TIMEOUT_MS = 750;
17
18
  export declare const MAX_SENTRY_REPORT_TIMEOUT_MS = 5000;
@@ -41,6 +42,7 @@ export type SentryReportOptions = {
41
42
  export type SentryReportDependencies = {
42
43
  sentry: {
43
44
  captureMessage: (message: string, context?: CaptureContext | SeverityLevel) => string;
45
+ metrics: Pick<typeof Sentry.metrics, "count">;
44
46
  flush: (timeout?: number) => PromiseLike<boolean>;
45
47
  };
46
48
  stdout: Pick<NodeJS.WriteStream, "write">;
@@ -29,6 +29,9 @@ const SENTRY_REPORT_LEVELS = new Set([
29
29
  "info",
30
30
  "debug",
31
31
  ]);
32
+ const HOOK_TIMEOUT_WARNING_TYPE = "hook_timeout_warning";
33
+ const HOOK_TIMEOUT_WARNING_METRIC = "hq.hook_timeout_warning";
34
+ const HOOK_TIMEOUT_EXCEEDED_FINGERPRINT = "hook-timeout-exceeded";
32
35
  /**
33
36
  * The only hook diagnostics that can be attached as metadata. The list omits
34
37
  * command text, prompts, payloads, company names, and repositories. `hook_path`
@@ -182,21 +185,43 @@ function resolveTimeout(timeoutMs) {
182
185
  return null;
183
186
  return timeout;
184
187
  }
185
- function sentryEnvelope(event) {
188
+ function sentryEnvelope(event, overrides) {
186
189
  return {
187
- level: event.level,
188
- fingerprint: [event.fingerprint],
190
+ level: overrides?.level ?? event.level,
191
+ fingerprint: overrides?.fingerprint ?? [event.fingerprint],
189
192
  tags: { sentry_event_type: event.type },
190
193
  extra: event.metadata,
191
194
  };
192
195
  }
193
196
  /** Build the same scrubbed event projection that the live SDK path uses. */
194
- function scrubbedSentryEnvelope(event) {
197
+ function scrubbedSentryEnvelope(event, overrides) {
195
198
  return beforeSend({
196
199
  message: event.message,
197
- ...sentryEnvelope(event),
200
+ ...sentryEnvelope(event, overrides),
198
201
  }, {});
199
202
  }
203
+ /**
204
+ * The watchdog's relative threshold fires before the declared timeout and
205
+ * clamps remaining_ms at zero, so its real warnings stay on the metric path.
206
+ * Keep a defensive branch for a genuine exceeded payload from another
207
+ * producer without restoring per-hook grouping.
208
+ */
209
+ function hookTimeoutExceededOverrides(event) {
210
+ if (event.type !== HOOK_TIMEOUT_WARNING_TYPE)
211
+ return undefined;
212
+ const elapsedMs = event.metadata.elapsed_ms;
213
+ const declaredTimeoutMs = event.metadata.declared_timeout_ms;
214
+ if (typeof elapsedMs !== "number" ||
215
+ typeof declaredTimeoutMs !== "number" ||
216
+ elapsedMs < declaredTimeoutMs) {
217
+ return undefined;
218
+ }
219
+ const hookEvent = typeof event.metadata.hook_event === "string" ? event.metadata.hook_event : "unknown";
220
+ return {
221
+ level: "warning",
222
+ fingerprint: [HOOK_TIMEOUT_EXCEEDED_FINGERPRINT, hookEvent],
223
+ };
224
+ }
200
225
  /**
201
226
  * Resolve true only when the local transport queue drains within the budget.
202
227
  * This does not confirm that the ingest service accepted the event.
@@ -234,13 +259,28 @@ export async function reportSentryEvent(stdin, options = {}, dependencies = defa
234
259
  const noun = result.droppedMetadataKeys === 1 ? "key" : "keys";
235
260
  dependencies.stderr.write(`Dropped ${result.droppedMetadataKeys} unsupported Sentry report metadata ${noun}.\n`);
236
261
  }
237
- const envelope = sentryEnvelope(result.event);
262
+ const overrides = hookTimeoutExceededOverrides(result.event);
238
263
  if (options.dryRun) {
239
- const scrubbed = scrubbedSentryEnvelope(result.event);
264
+ const scrubbed = scrubbedSentryEnvelope(result.event, overrides);
240
265
  if (scrubbed)
241
266
  dependencies.stdout.write(`${JSON.stringify(scrubbed)}\n`);
242
267
  return result;
243
268
  }
269
+ if (result.event.type === HOOK_TIMEOUT_WARNING_TYPE && !overrides) {
270
+ try {
271
+ dependencies.sentry.metrics.count(HOOK_TIMEOUT_WARNING_METRIC, 1, {
272
+ attributes: result.event.metadata,
273
+ });
274
+ if (await flushWithin(dependencies.sentry, timeoutMs))
275
+ return result;
276
+ }
277
+ catch {
278
+ // Report failure below without exposing event fields or transport details.
279
+ }
280
+ dependencies.stderr.write("Sentry report failed.\n");
281
+ return invalidInput();
282
+ }
283
+ const envelope = sentryEnvelope(result.event, overrides);
244
284
  try {
245
285
  dependencies.sentry.captureMessage(result.event.message, envelope);
246
286
  if (await flushWithin(dependencies.sentry, timeoutMs))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.127.0",
3
+ "version": "5.128.0",
4
4
  "description": "HQ by Indigo management CLI \u2014 modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {