@indigoai-us/hq-cli 5.179.1 → 5.181.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,14 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.181.0] — 2026-09-24
6
+
7
+ ### Changed
8
+
9
+ - `hq daemon` runs the session host from the hq-cloud copy it keeps up to date (hq-cloud's `hq-outpost-session-host`, when that copy has it), the same way it already runs sync and the outpost heartbeat, and restarts it after an hq-cloud update. Session-host fixes, such as starting on an Outpost that signs in with machine credentials, now reach boxes without an hq CLI release.
10
+
11
+ ## [5.180.0] — 2026-09-24
12
+
5
13
  ## [5.179.1] — 2026-09-24
6
14
 
7
15
  ### Added
@@ -13,6 +21,11 @@
13
21
  person's inbox while the answer they need still reaches them. `hq dm inbox`
14
22
  and `hq dm thread` mark agent-audience messages with an `[agent]` tag.
15
23
 
24
+ ### Fixed
25
+
26
+ - Windows checkpoint stamps now use atomic replacement and bounded retries for
27
+ transient filesystem errors, so a brief lock does not abort the checkpoint.
28
+
16
29
  ## [5.179.0] — 2026-09-24
17
30
 
18
31
  ### 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
  }
@@ -432,7 +432,7 @@ async function runServiceSelfUpdate(d) {
432
432
  updateCli: cliSelfUpdatable() ? () => daemonSelfUpdate({ env }) : null,
433
433
  tools: TOOL_NAMES.map((name) => hostToolUpdater(name, env, d.home)),
434
434
  updateHqCloud: async () => updateManagedHqCloud(paths.root, env),
435
- hqCloudUnits: hqCloudUnits(detected.type),
435
+ hqCloudUnits: hqCloudUnits(detected.type, enabledServices(detected.type, readDaemonConfig(paths.configPath))),
436
436
  requestDaemonRestart: () => writeControlRequest(paths.requestsDir, { action: "restart-daemon" }),
437
437
  requestRestart: (unit) => writeControlRequest(paths.requestsDir, { action: "restart", unit }),
438
438
  log: d.out,
@@ -64,6 +64,8 @@ export interface PlanContext {
64
64
  syncRunnerPath: string | null;
65
65
  /** hq-cloud's `hq-outpost-heartbeat` bin, when the hq-cloud in use has one. */
66
66
  heartbeatRunnerPath: string | null;
67
+ /** hq-cloud's `hq-outpost-session-host` bin, when the hq-cloud in use has one. */
68
+ sessionHostRunnerPath: string | null;
67
69
  signedIn: boolean;
68
70
  /** ~/.hq-agent/kit.json exists. */
69
71
  kitConfigured: boolean;
@@ -89,11 +91,12 @@ export interface PlanContext {
89
91
  codexRunnerScript: string | null;
90
92
  }
91
93
  /**
92
- * The units that run hq-cloud's code on a machine type, restarted after the
93
- * updater installs a new hq-cloud. Only outposts and self-hosted machines run
94
- * the outpost heartbeat; agent machines run the agent kit's own.
94
+ * The units that run hq-cloud's code on this host, restarted after the updater
95
+ * installs a new hq-cloud. Only outposts and self-hosted machines run the
96
+ * outpost heartbeat (agent machines run the agent kit's own); the session host
97
+ * is included wherever it is enabled.
95
98
  */
96
- export declare function hqCloudUnits(machineType: MachineType): ServiceName[];
99
+ export declare function hqCloudUnits(machineType: MachineType, enabled: readonly ServiceName[]): ServiceName[];
97
100
  /** Written by outpost provisioning; the daemon runs it rather than re-implementing Codex login and relay setup. */
98
101
  export declare const CODEX_RUNNER_SCRIPT = "/usr/local/bin/outpost-codex-runner.sh";
99
102
  /** The outpost's Claude runner script, read for the session name it uses. */
@@ -97,12 +97,18 @@ function firstBlock(checks) {
97
97
  return hit ? { state: "waiting", reason: hit[1] } : undefined;
98
98
  }
99
99
  /**
100
- * The units that run hq-cloud's code on a machine type, restarted after the
101
- * updater installs a new hq-cloud. Only outposts and self-hosted machines run
102
- * the outpost heartbeat; agent machines run the agent kit's own.
100
+ * The units that run hq-cloud's code on this host, restarted after the updater
101
+ * installs a new hq-cloud. Only outposts and self-hosted machines run the
102
+ * outpost heartbeat (agent machines run the agent kit's own); the session host
103
+ * is included wherever it is enabled.
103
104
  */
104
- export function hqCloudUnits(machineType) {
105
- return machineType === "outpost" || machineType === "self-hosted" ? ["sync", "heartbeat"] : ["sync"];
105
+ export function hqCloudUnits(machineType, enabled) {
106
+ const units = ["sync"];
107
+ if (machineType === "outpost" || machineType === "self-hosted")
108
+ units.push("heartbeat");
109
+ if (enabled.includes("session-host"))
110
+ units.push("session-host");
111
+ return units;
106
112
  }
107
113
  /** Written by outpost provisioning; the daemon runs it rather than re-implementing Codex login and relay setup. */
108
114
  export const CODEX_RUNNER_SCRIPT = "/usr/local/bin/outpost-codex-runner.sh";
@@ -181,7 +187,10 @@ function planService(service, ctx) {
181
187
  case "session-host":
182
188
  return [
183
189
  base(service, {
184
- command: { type: "hq", args: ["outposts", "session-host"] },
190
+ // hq-cloud's own session host picks up hq-cloud updates without a CLI release.
191
+ command: ctx.sessionHostRunnerPath
192
+ ? { type: "node", script: ctx.sessionHostRunnerPath, args: [] }
193
+ : { type: "hq", args: ["outposts", "session-host"] },
185
194
  env: ctx.hqRoot ? { HQ_ROOT: ctx.hqRoot } : {},
186
195
  blocked: firstBlock([[!ctx.signedIn, NOT_SIGNED_IN]]),
187
196
  }),
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Which hq-cloud the daemon runs sync and the outpost heartbeat from.
2
+ * Which hq-cloud the daemon runs sync, the outpost heartbeat and the session host from.
3
3
  *
4
4
  * The hq CLI ships with a copy of hq-cloud, but hq-cloud releases more often
5
5
  * than the CLI. The updater keeps a newer copy of the same major version in
@@ -27,6 +27,8 @@ export interface HqCloudRunners {
27
27
  syncRunner: string | null;
28
28
  /** `hq-outpost-heartbeat`, which older hq-cloud releases do not have. */
29
29
  heartbeat: string | null;
30
+ /** `hq-outpost-session-host`, which older hq-cloud releases do not have. */
31
+ sessionHost: string | null;
30
32
  }
31
33
  export declare function hqCloudRunners(root: string, exists?: (p: string) => boolean): HqCloudRunners;
32
34
  /** Install the newest release of the bundled major version, never older than the bundled copy. */
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Which hq-cloud the daemon runs sync and the outpost heartbeat from.
2
+ * Which hq-cloud the daemon runs sync, the outpost heartbeat and the session host from.
3
3
  *
4
4
  * The hq CLI ships with a copy of hq-cloud, but hq-cloud releases more often
5
5
  * than the CLI. The updater keeps a newer copy of the same major version in
@@ -48,7 +48,11 @@ export function hqCloudRunners(root, exists = fs.existsSync) {
48
48
  const p = path.join(root, "dist", "bin", name);
49
49
  return exists(p) ? p : null;
50
50
  };
51
- return { syncRunner: bin("sync-runner.js"), heartbeat: bin("outpost-heartbeat.js") };
51
+ return {
52
+ syncRunner: bin("sync-runner.js"),
53
+ heartbeat: bin("outpost-heartbeat.js"),
54
+ sessionHost: bin("outpost-session-host.js"),
55
+ };
52
56
  }
53
57
  /** Install the newest release of the bundled major version, never older than the bundled copy. */
54
58
  export function hqCloudInstallCommand(prefix, bundledVersion, platform = process.platform) {
@@ -249,7 +249,7 @@ export async function runDaemon(opts) {
249
249
  const hqRoot = configured && fs.existsSync(configured) ? configured : undefined;
250
250
  // Re-picked every reconcile, so a newer hq-cloud from the updater is used as soon as it lands.
251
251
  const hqCloud = currentHqCloud(paths.root);
252
- const runners = hqCloud ? hqCloudRunners(hqCloud.root) : { syncRunner: null, heartbeat: null };
252
+ const runners = hqCloud ? hqCloudRunners(hqCloud.root) : { syncRunner: null, heartbeat: null, sessionHost: null };
253
253
  return planUnits(enabledServices(machineType, config), {
254
254
  machineType,
255
255
  platform,
@@ -257,6 +257,7 @@ export async function runDaemon(opts) {
257
257
  hqRoot,
258
258
  syncRunnerPath: runners.syncRunner,
259
259
  heartbeatRunnerPath: runners.heartbeat,
260
+ sessionHostRunnerPath: runners.sessionHost,
260
261
  signedIn: signedIn(env, home, log),
261
262
  kitConfigured: fs.existsSync(agentKitPaths(home, env).kitConfigPath),
262
263
  desktopAppManagesSync: machineType === "desktop" &&
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.181.0",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {