@indigoai-us/hq-cli 5.333.0 → 5.335.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.335.0] — 2026-10-03
6
+ - Keep expected invalid-refresh-token events countable at warning level and tag each rejection with its allowlisted Cognito OAuth error class.
7
+ - Keep the intentional HQ Anywhere flag-off refusal for `hq mcp login` out of Sentry while preserving its message and exit code.
8
+
9
+ ## [5.334.0] — 2026-10-03
10
+ - Keep keep-alive audit verdicts for expired stops independent of the lane's current state, and report restarts recorded after the grace window separately.
11
+
5
12
  ## [5.333.0] — 2026-10-03
6
13
  - Limit session-start telemetry to 30 events per machine per rolling minute.
7
14
  - Start the globally registered HQ hook with BASH_ENV cleared, so an inherited shell profile no longer runs before every hook event.
@@ -11105,7 +11105,7 @@ export declare const COMMAND_CATALOG: readonly [{
11105
11105
  readonly subcommands: readonly [];
11106
11106
  }, {
11107
11107
  readonly name: "keep-alive-audit";
11108
- readonly description: "Audit lead lane keep-alive timing";
11108
+ readonly description: "Audit lead lane keep-alive timing. A later envelope proves another turn ran and may reclassify an expired stop as restarted_untimed when no launch record remains; late_restarts_after_window counts known launches after the grace window.";
11109
11109
  readonly aliases: readonly [];
11110
11110
  readonly hidden: false;
11111
11111
  readonly helpOption: true;
@@ -13486,7 +13486,7 @@ export const COMMAND_CATALOG = [
13486
13486
  },
13487
13487
  {
13488
13488
  "name": "keep-alive-audit",
13489
- "description": "Audit lead lane keep-alive timing",
13489
+ "description": "Audit lead lane keep-alive timing. A later envelope proves another turn ran and may reclassify an expired stop as restarted_untimed when no launch record remains; late_restarts_after_window counts known launches after the grace window.",
13490
13490
  "aliases": [],
13491
13491
  "hidden": false,
13492
13492
  "helpOption": true,
@@ -2,7 +2,7 @@ import { buildKeepAliveAudit, formatKeepAliveAuditText } from "../lib/lanes/keep
2
2
  export function registerLaneKeepAliveAuditCommand(parent, deps) {
3
3
  parent
4
4
  .command("keep-alive-audit")
5
- .description("Audit lead lane keep-alive timing")
5
+ .description("Audit lead lane keep-alive timing. A later envelope proves another turn ran and may reclassify an expired stop as restarted_untimed when no launch record remains; late_restarts_after_window counts known launches after the grace window.")
6
6
  .option("--since <iso>", "Include stopped turns at or after this ISO date-time")
7
7
  .option("--lane <id>", "Audit one exact lane id")
8
8
  .option("--json", "Print the report as JSON")
@@ -5,6 +5,7 @@ import * as os from "node:os";
5
5
  import * as path from "node:path";
6
6
  import open from "open";
7
7
  import { resolveCognitoTokenSource } from "../../utils/cognito-session.js";
8
+ import { markExpectedUserError } from "../../utils/expected-cli-error.js";
8
9
  import { anywhereRuntimeEnabled } from "../../commands/anywhere-runtime-flag.js";
9
10
  import { resolveHqMcpOAuthOrigin } from "./mcp-endpoint.js";
10
11
  const MCP_SCOPE = "openid email profile hq-mcp/use";
@@ -128,7 +129,7 @@ function newPkce() {
128
129
  async function runLogin(options) {
129
130
  const env = options.env ?? process.env;
130
131
  if (!(options.isEnabled ?? (() => anywhereRuntimeEnabled(env)))()) {
131
- throw new McpOAuthError("HQ Anywhere is not enabled. Enable the hq-anywhere-runtime flag to use `hq mcp login`.");
132
+ throw markExpectedUserError(new McpOAuthError("HQ Anywhere is not enabled. Enable the hq-anywhere-runtime flag to use `hq mcp login`."));
132
133
  }
133
134
  if ((options.isMachine ?? (() => resolveCognitoTokenSource(env) === "machine"))()) {
134
135
  throw new McpOAuthError("`hq mcp login` is for person accounts; this box uses machine credentials.");
@@ -19,6 +19,7 @@ interface KeepAliveAuditSummary {
19
19
  by_verdict: Record<KeepAliveAuditVerdict, number>;
20
20
  not_restarted: number;
21
21
  restarted_untimed: number;
22
+ late_restarts_after_window: number;
22
23
  late_over_10m: number;
23
24
  early_wake_by_others: number;
24
25
  self_wake: number;
@@ -4,7 +4,6 @@ import { LEAD_LANE_KEEP_ALIVE_BASE_BACKOFF_MS } from "./keep-alive.js";
4
4
  import { LanesError } from "./errors.js";
5
5
  import { laneRunDir } from "./spawn.js";
6
6
  import { listQuestionIds, readAllLanes, tryReadQuestion } from "./store.js";
7
- import { HISTORY_CAP } from "./types.js";
8
7
  function readDoneClassifications(hqRoot, laneId) {
9
8
  const root = fs.realpathSync(hqRoot);
10
9
  const watcherLog = path.join(laneRunDir(root, laneId), "logs", "watcher.out");
@@ -309,17 +308,21 @@ function firstRestartAfter(lane, stop, nextEnvelopeMs) {
309
308
  // cannot establish its launch time.
310
309
  return { at: null, trigger: "keep_alive", caller: null };
311
310
  }
312
- const resumeHistory = lane.resume_history ?? [];
313
- const keepAliveHistory = lane.keep_alive_history ?? [];
314
- if (resumeHistory.length >= HISTORY_CAP && keepAliveHistory.length >= HISTORY_CAP) {
315
- // Both append-only histories can roll this resume out. The later envelope
316
- // proves a turn ran, while its trigger and launch time are no longer known.
317
- return { at: null, trigger: "unknown", caller: null };
318
- }
319
- return null;
311
+ const interruptedKeepAliveResume = (lane.keep_alive_history ?? []).some((entry) => {
312
+ const at = parseTime(entry.at);
313
+ return entry.resumed_from !== "done" && at !== undefined && at > stop.mtimeMs && at < nextEnvelopeMs;
314
+ });
315
+ if (interruptedKeepAliveResume)
316
+ return null;
317
+ // A later envelope proves the stop was followed by another turn even when
318
+ // neither capped history has a matching entry. Report the restart as
319
+ // untimed instead of using the lane's mutable current state as a proxy.
320
+ return nextEnvelopeMs === undefined ? null : { at: null, trigger: "unknown", caller: null };
320
321
  }
321
322
  function classifyVerdict(input) {
322
323
  const { dueAt, restart, lane, nowMs } = input;
324
+ if (restart?.trigger === "unknown" && laneWasKeepAliveOffAtStop(lane, input.stoppedAt))
325
+ return "keep_alive_off";
323
326
  if (restart) {
324
327
  if (restart.at === null)
325
328
  return "restarted_untimed";
@@ -356,7 +359,7 @@ function classifyVerdict(input) {
356
359
  }
357
360
  if (laneWasKeepAliveOffAtStop(lane, input.stoppedAt))
358
361
  return "keep_alive_off";
359
- if (lane.state === "done" && nowMs > dueAt + KEEP_ALIVE_NOT_RESTARTED_GRACE_MS)
362
+ if (nowMs > dueAt + KEEP_ALIVE_NOT_RESTARTED_GRACE_MS)
360
363
  return "not_restarted";
361
364
  return "waiting";
362
365
  }
@@ -413,8 +416,12 @@ export function buildKeepAliveAudit(input) {
413
416
  rows.sort((a, b) => a.stopped_at.localeCompare(b.stopped_at) || a.lane.localeCompare(b.lane));
414
417
  const byVerdict = Object.fromEntries(VERDICTS.map((verdict) => [verdict, 0]));
415
418
  let lateOverTenMinutes = 0;
419
+ let lateRestartsAfterWindow = 0;
416
420
  for (const row of rows) {
417
421
  byVerdict[row.verdict] += 1;
422
+ if (row.restart !== null && row.restart.at !== null && Date.parse(row.restart.at) > Date.parse(row.due_at) + KEEP_ALIVE_NOT_RESTARTED_GRACE_MS) {
423
+ lateRestartsAfterWindow += 1;
424
+ }
418
425
  if (row.verdict === "late" && row.restart !== null && row.restart.at !== null && Date.parse(row.restart.at) - Date.parse(row.due_at) > LATE_KPI_THRESHOLD_MS) {
419
426
  lateOverTenMinutes += 1;
420
427
  }
@@ -427,6 +434,7 @@ export function buildKeepAliveAudit(input) {
427
434
  by_verdict: byVerdict,
428
435
  not_restarted: byVerdict.not_restarted,
429
436
  restarted_untimed: byVerdict.restarted_untimed,
437
+ late_restarts_after_window: lateRestartsAfterWindow,
430
438
  late_over_10m: lateOverTenMinutes,
431
439
  early_wake_by_others: byVerdict.early_wake,
432
440
  self_wake: byVerdict.self_wake,
@@ -455,7 +463,7 @@ export function formatKeepAliveAuditText(result) {
455
463
  ...values.map(tableLine),
456
464
  "",
457
465
  `Since ${result.since}; ${counts}`,
458
- `not_restarted=${result.summary.not_restarted}, late_over_10m=${result.summary.late_over_10m}, early_wake_by_others=${result.summary.early_wake_by_others}, self_wake=${result.summary.self_wake}`,
466
+ `not_restarted=${result.summary.not_restarted}, late_restarts_after_window=${result.summary.late_restarts_after_window}, late_over_10m=${result.summary.late_over_10m}, early_wake_by_others=${result.summary.early_wake_by_others}, self_wake=${result.summary.self_wake}`,
459
467
  `Stopped and not restarted on their own: ${result.summary.stopped_and_not_restarted_on_their_own}`,
460
468
  ...result.errors.map((error) => `Error ${error.lane} (${error.error}) at ${error.path}: ${error.message}`),
461
469
  ].join("\n");
@@ -1,5 +1,15 @@
1
1
  import { CLI_VERSION } from "../cli-version.js";
2
2
  import { CognitoRefreshError } from "@indigoai-us/hq-cloud";
3
+ const OAUTH_ERROR_CLASSES = new Set([
4
+ "invalid_grant",
5
+ "invalid_client",
6
+ "unauthorized_client",
7
+ "invalid_request",
8
+ "invalid_scope",
9
+ "NotAuthorizedException",
10
+ "other",
11
+ "unknown",
12
+ ]);
3
13
  /** Capture only fixed, aggregatable auth diagnostics; never attach tokens or identity. */
4
14
  export async function reportNoninteractivePersonRefreshRejection(error) {
5
15
  try {
@@ -11,6 +21,15 @@ export async function reportNoninteractivePersonRefreshRejection(error) {
11
21
  const rejectionKind = error instanceof CognitoRefreshError
12
22
  ? "cognito_refresh_rejection"
13
23
  : "classified_auth_error";
24
+ const rawOauthError = error instanceof CognitoRefreshError &&
25
+ "oauthError" in error &&
26
+ typeof error.oauthError === "string"
27
+ ? error.oauthError
28
+ : "unknown";
29
+ const oauthError = OAUTH_ERROR_CLASSES.has(rawOauthError)
30
+ ? rawOauthError
31
+ : "unknown";
32
+ const expectedReauthentication = oauthError === "invalid_grant" || oauthError === "NotAuthorizedException";
14
33
  Sentry.withScope((scope) => {
15
34
  scope.setUser(null);
16
35
  scope.clearBreadcrumbs();
@@ -29,10 +48,11 @@ export async function reportNoninteractivePersonRefreshRejection(error) {
29
48
  ...(Number.isInteger(statusCode) ? { status_code: statusCode } : {}),
30
49
  },
31
50
  });
32
- scope.setLevel("error");
51
+ scope.setLevel(expectedReauthentication ? "warning" : "error");
33
52
  scope.setFingerprint(["hq-cli", "person-refresh-rejected"]);
34
53
  scope.setTag("component", "person-session-refresh");
35
54
  scope.setTag("auth.rejection_kind", rejectionKind);
55
+ scope.setTag("auth.oauth_error", oauthError);
36
56
  scope.setTag("auth.fresh_cache_generation_found", "false");
37
57
  scope.setTag("platform", process.platform);
38
58
  scope.setTag("cli_version", CLI_VERSION);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.333.0",
3
+ "version": "5.335.0",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {