@indigoai-us/hq-cli 5.179.1 → 5.180.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,8 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.180.0] — 2026-09-24
6
+
5
7
  ## [5.179.1] — 2026-09-24
6
8
 
7
9
  ### Added
@@ -13,6 +15,11 @@
13
15
  person's inbox while the answer they need still reaches them. `hq dm inbox`
14
16
  and `hq dm thread` mark agent-audience messages with an `[agent]` tag.
15
17
 
18
+ ### Fixed
19
+
20
+ - Windows checkpoint stamps now use atomic replacement and bounded retries for
21
+ transient filesystem errors, so a brief lock does not abort the checkpoint.
22
+
16
23
  ## [5.179.0] — 2026-09-24
17
24
 
18
25
  ### Fixed
@@ -48,7 +48,11 @@ export type WriteStampsOptions = {
48
48
  * permission errno so the HqStateWriteError conversion can be proven without a
49
49
  * real unwritable path.
50
50
  */
51
- fs?: Pick<typeof fs, "mkdirSync" | "writeFileSync">;
51
+ fs?: Pick<typeof fs, "mkdirSync" | "writeFileSync" | "renameSync" | "unlinkSync">;
52
+ /** Runtime seam for proving the Windows-only atomic path without host assumptions. */
53
+ platform?: NodeJS.Platform;
54
+ /** Synchronous delay seam for the bounded Windows lock retry. */
55
+ waitForRetry?: (delayMs: number) => void;
52
56
  };
53
57
  export declare function writeStamps(liveRoot: string, sessionId: string | undefined, options?: WriteStampsOptions): string[];
54
58
  /**
@@ -8,11 +8,13 @@
8
8
  */
9
9
  import * as fs from "node:fs";
10
10
  import * as path from "node:path";
11
+ import { randomUUID } from "node:crypto";
11
12
  import { execFileSync, spawn } from "node:child_process";
12
13
  import { homedir } from "node:os";
13
14
  import { resolveLiveRoot } from "../utils/hq-roots.js";
14
15
  import { peekIdToken } from "../utils/id-token.js";
15
16
  import { toHqStateWriteError } from "../utils/hq-state-write-error.js";
17
+ import { checkpointStampWriteErrorFor, isCheckpointStampWriteError, } from "../utils/checkpoint-stamp-write-error.js";
16
18
  const DEFAULT_TRIGGER = "stop-gate";
17
19
  const BACKENDS = new Set(["auto", "claude", "codex", "grok", "none"]);
18
20
  // Pinned by operator directive 2026-09-15; change defaults here deliberately.
@@ -408,14 +410,65 @@ function writeHqState(targetPath, operation, perform) {
408
410
  perform();
409
411
  }
410
412
  catch (error) {
413
+ if (isCheckpointStampWriteError(error))
414
+ throw error;
411
415
  const typed = toHqStateWriteError(error, targetPath, operation);
412
416
  if (typed)
413
417
  throw typed;
414
418
  throw error;
415
419
  }
416
420
  }
421
+ const WINDOWS_CHECKPOINT_WRITE_RETRY_CODES = new Set(["EBUSY", "EPERM", "UNKNOWN"]);
422
+ const WINDOWS_CHECKPOINT_WRITE_RETRY_DELAYS_MS = [25, 50, 100];
423
+ function waitForCheckpointWriteRetry(delayMs) {
424
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT)), 0, 0, delayMs);
425
+ }
426
+ function removeCheckpointStampTemp(tempPath, fsImpl, waitForRetry) {
427
+ for (let attempt = 0;; attempt += 1) {
428
+ try {
429
+ fsImpl.unlinkSync(tempPath);
430
+ return;
431
+ }
432
+ catch (error) {
433
+ const code = error?.code;
434
+ if (code === "ENOENT")
435
+ return;
436
+ if (typeof code === "string" &&
437
+ WINDOWS_CHECKPOINT_WRITE_RETRY_CODES.has(code) &&
438
+ attempt < WINDOWS_CHECKPOINT_WRITE_RETRY_DELAYS_MS.length) {
439
+ waitForRetry(WINDOWS_CHECKPOINT_WRITE_RETRY_DELAYS_MS[attempt]);
440
+ continue;
441
+ }
442
+ throw checkpointStampWriteErrorFor(error, "unlink") ?? error;
443
+ }
444
+ }
445
+ }
446
+ function writeCheckpointStampAtomically(targetPath, contents, fsImpl, waitForRetry) {
447
+ for (let attempt = 0;; attempt += 1) {
448
+ const tempPath = path.join(path.dirname(targetPath), `.hq-checkpoint-stamp.tmp.${process.pid}.${randomUUID()}`);
449
+ let failedOperation = "open";
450
+ try {
451
+ fsImpl.writeFileSync(tempPath, contents, { flag: "wx" });
452
+ failedOperation = "rename";
453
+ fsImpl.renameSync(tempPath, targetPath);
454
+ return;
455
+ }
456
+ catch (error) {
457
+ removeCheckpointStampTemp(tempPath, fsImpl, waitForRetry);
458
+ const code = error?.code;
459
+ if (typeof code !== "string" ||
460
+ !WINDOWS_CHECKPOINT_WRITE_RETRY_CODES.has(code) ||
461
+ attempt >= WINDOWS_CHECKPOINT_WRITE_RETRY_DELAYS_MS.length) {
462
+ throw checkpointStampWriteErrorFor(error, failedOperation) ?? error;
463
+ }
464
+ waitForRetry(WINDOWS_CHECKPOINT_WRITE_RETRY_DELAYS_MS[attempt]);
465
+ }
466
+ }
467
+ }
417
468
  export function writeStamps(liveRoot, sessionId, options = {}) {
418
469
  const fsImpl = options.fs ?? fs;
470
+ const platform = options.platform ?? process.platform;
471
+ const waitForRetry = options.waitForRetry ?? waitForCheckpointWriteRetry;
419
472
  const stateDir = path.join(liveRoot, "workspace", "orchestrator", "hook-state");
420
473
  writeHqState(stateDir, "create the checkpoint state directory", () => fsImpl.mkdirSync(stateDir, { recursive: true }));
421
474
  const sessionKey = sessionId
@@ -427,7 +480,9 @@ export function writeStamps(liveRoot, sessionId, options = {}) {
427
480
  path.join(stateDir, `checkpoint-cli-last-${sessionKey}`),
428
481
  ];
429
482
  for (const stampPath of stampPaths) {
430
- writeHqState(stampPath, "write its checkpoint state file", () => fsImpl.writeFileSync(stampPath, timestamp));
483
+ writeHqState(stampPath, "write its checkpoint state file", () => platform === "win32"
484
+ ? writeCheckpointStampAtomically(stampPath, timestamp, fsImpl, waitForRetry)
485
+ : fsImpl.writeFileSync(stampPath, timestamp));
431
486
  }
432
487
  return stampPaths;
433
488
  }
package/dist/main.js CHANGED
@@ -25,6 +25,7 @@ import { qmdQueryDocumentMessage } from "./utils/qmd-query-document-error.js";
25
25
  import { qmdModelDownloadMessage } from "./utils/qmd-model-download-error.js";
26
26
  import { qmdWorkdirMissingMessage } from "./utils/qmd-workdir-missing-error.js";
27
27
  import { hqStateWriteErrorMessage } from "./utils/hq-state-write-error.js";
28
+ import { checkpointStampWriteCaptureContext, checkpointStampWriteMessage, } from "./utils/checkpoint-stamp-write-error.js";
28
29
  import { incompleteInstallMessage, incompleteInstallCaptureContext, } from "./utils/incomplete-install-error.js";
29
30
  import { isExpectedUserError } from "./utils/expected-cli-error.js";
30
31
  import { isVarlockEnvError } from "./run/env-graph-guard.js";
@@ -734,6 +735,8 @@ export async function handleTopLevelError(err, deps = defaultTopLevelErrorDepend
734
735
  // caller-owned conditions. Keep them at error level and attach bounded,
735
736
  // installation-independent context so OneDrive/antivirus/path-length
736
737
  // hypotheses can be separated without creating one issue per host path.
738
+ const checkpointStampContext = checkpointStampWriteCaptureContext(err);
739
+ const checkpointStampMessage = checkpointStampWriteMessage(err);
737
740
  const fsContext = filesystemErrorCaptureContext(err);
738
741
  const fsMsg = fsContext ? filesystemErrorMessage(err) : null;
739
742
  if (qmdMsg) {
@@ -781,6 +784,10 @@ export async function handleTopLevelError(err, deps = defaultTopLevelErrorDepend
781
784
  else if (transportMsg) {
782
785
  deps.stderr.write(`hq: ${transportMsg}\n`);
783
786
  }
787
+ else if (checkpointStampContext && checkpointStampMessage) {
788
+ deps.sentry.captureException(err, checkpointStampContext);
789
+ deps.stderr.write(`hq: ${checkpointStampMessage}\n`);
790
+ }
784
791
  else if (fsMsg && fsContext) {
785
792
  deps.sentry.captureException(err, fsContext);
786
793
  deps.stderr.write(`hq: ${fsMsg}\n`);
@@ -0,0 +1,28 @@
1
+ export type CheckpointStampFsOperation = "open" | "write" | "rename" | "unlink";
2
+ type CheckpointStampErrnoCode = "EBUSY" | "EPERM" | "UNKNOWN";
3
+ type CheckpointStampWriteDiagnostics = {
4
+ fs_op: CheckpointStampFsOperation;
5
+ target_kind: "checkpoint_stamp";
6
+ errno_code: CheckpointStampErrnoCode;
7
+ platform: "win32";
8
+ };
9
+ export type CheckpointStampWriteCaptureContext = {
10
+ fingerprint: ["fs-write-failed", "checkpoint_stamp", CheckpointStampErrnoCode];
11
+ tags: CheckpointStampWriteDiagnostics;
12
+ };
13
+ export declare class CheckpointStampWriteError extends Error {
14
+ name: string;
15
+ readonly code: CheckpointStampErrnoCode;
16
+ readonly syscall: CheckpointStampFsOperation;
17
+ readonly checkpointStampWriteDiagnostics: CheckpointStampWriteDiagnostics;
18
+ constructor(diagnostics: CheckpointStampWriteDiagnostics, originalStack?: string);
19
+ }
20
+ /** Wrap only the captured Windows lock/error classes, keeping the original frames and dropping paths. */
21
+ export declare function checkpointStampWriteErrorFor(error: unknown, fallbackOperation: CheckpointStampFsOperation): CheckpointStampWriteError | null;
22
+ export declare function isCheckpointStampWriteError(error: unknown): boolean;
23
+ /** Value-free report context for the Windows checkpoint stamp write boundary. */
24
+ export declare function checkpointStampWriteCaptureContext(error: unknown): CheckpointStampWriteCaptureContext | null;
25
+ /** User-facing text stays actionable without including the HQ or home path. */
26
+ export declare function checkpointStampWriteMessage(error: unknown): string | null;
27
+ export {};
28
+ //# sourceMappingURL=checkpoint-stamp-write-error.d.ts.map
@@ -0,0 +1,99 @@
1
+ const CHECKPOINT_STAMP_ERRNO_CODES = new Set([
2
+ "EBUSY",
3
+ "EPERM",
4
+ "UNKNOWN",
5
+ ]);
6
+ const CHECKPOINT_STAMP_FS_OPERATIONS = new Set([
7
+ "open",
8
+ "write",
9
+ "rename",
10
+ "unlink",
11
+ ]);
12
+ function errorLike(value) {
13
+ return value !== null && typeof value === "object" ? value : null;
14
+ }
15
+ function isCheckpointStampErrnoCode(value) {
16
+ return typeof value === "string" && CHECKPOINT_STAMP_ERRNO_CODES.has(value);
17
+ }
18
+ function isCheckpointStampFsOperation(value) {
19
+ return (typeof value === "string" &&
20
+ CHECKPOINT_STAMP_FS_OPERATIONS.has(value));
21
+ }
22
+ export class CheckpointStampWriteError extends Error {
23
+ name = "CheckpointStampWriteError";
24
+ code;
25
+ syscall;
26
+ checkpointStampWriteDiagnostics;
27
+ constructor(diagnostics, originalStack) {
28
+ super(`checkpoint stamp write failed during ${diagnostics.fs_op} (${diagnostics.errno_code})`);
29
+ this.code = diagnostics.errno_code;
30
+ this.syscall = diagnostics.fs_op;
31
+ this.checkpointStampWriteDiagnostics = diagnostics;
32
+ if (originalStack) {
33
+ const frames = originalStack.split("\n").slice(1);
34
+ if (frames.length > 0)
35
+ this.stack = `${this.name}: ${this.message}\n${frames.join("\n")}`;
36
+ }
37
+ }
38
+ }
39
+ /** Wrap only the captured Windows lock/error classes, keeping the original frames and dropping paths. */
40
+ export function checkpointStampWriteErrorFor(error, fallbackOperation) {
41
+ const record = errorLike(error);
42
+ if (!record || !isCheckpointStampErrnoCode(record.code))
43
+ return null;
44
+ const fsOperation = isCheckpointStampFsOperation(record.syscall)
45
+ ? record.syscall
46
+ : fallbackOperation;
47
+ const diagnostics = {
48
+ fs_op: fsOperation,
49
+ target_kind: "checkpoint_stamp",
50
+ errno_code: record.code,
51
+ platform: "win32",
52
+ };
53
+ return new CheckpointStampWriteError(diagnostics, error instanceof Error ? error.stack : undefined);
54
+ }
55
+ function diagnosticsFor(error) {
56
+ const record = errorLike(error);
57
+ if (!record || record.name !== "CheckpointStampWriteError")
58
+ return null;
59
+ const diagnostics = error
60
+ .checkpointStampWriteDiagnostics;
61
+ if (diagnostics === null || typeof diagnostics !== "object")
62
+ return null;
63
+ const candidate = diagnostics;
64
+ if (!isCheckpointStampFsOperation(candidate.fs_op) ||
65
+ candidate.target_kind !== "checkpoint_stamp" ||
66
+ !isCheckpointStampErrnoCode(candidate.errno_code) ||
67
+ candidate.platform !== "win32") {
68
+ return null;
69
+ }
70
+ return {
71
+ fs_op: candidate.fs_op,
72
+ target_kind: "checkpoint_stamp",
73
+ errno_code: candidate.errno_code,
74
+ platform: "win32",
75
+ };
76
+ }
77
+ export function isCheckpointStampWriteError(error) {
78
+ return diagnosticsFor(error) !== null;
79
+ }
80
+ /** Value-free report context for the Windows checkpoint stamp write boundary. */
81
+ export function checkpointStampWriteCaptureContext(error) {
82
+ const tags = diagnosticsFor(error);
83
+ if (!tags)
84
+ return null;
85
+ return {
86
+ // This is a grouping key. Keep it independent of installation paths.
87
+ fingerprint: ["fs-write-failed", tags.target_kind, tags.errno_code],
88
+ tags,
89
+ };
90
+ }
91
+ /** User-facing text stays actionable without including the HQ or home path. */
92
+ export function checkpointStampWriteMessage(error) {
93
+ const diagnostics = diagnosticsFor(error);
94
+ if (!diagnostics)
95
+ return null;
96
+ return (`hq could not write a checkpoint state file (${diagnostics.errno_code} during ${diagnostics.fs_op}). ` +
97
+ "Check the checkpoint state folder and retry.");
98
+ }
99
+ //# sourceMappingURL=checkpoint-stamp-write-error.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.179.1",
3
+ "version": "5.180.0",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {