@indigoai-us/hq-cli 5.272.0 → 5.273.0-alpha.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.
@@ -12,11 +12,17 @@ export interface DaemonConfig {
12
12
  hqRoot?: string;
13
13
  /** Exact hq-cloud package version preferred by this daemon, including prereleases. */
14
14
  hqCloudVersion?: string;
15
+ /** Persisted machine-local sync pause. */
16
+ syncPaused?: boolean;
17
+ /** Explicit desktop handoff; absent values preserve the legacy per-machine owner. */
18
+ syncOwner?: SyncOwner;
15
19
  /** Services turned on here beyond the machine type's defaults. */
16
20
  enable: string[];
17
21
  /** Default services turned off here. */
18
22
  disable: string[];
19
23
  }
24
+ export type SyncOwner = "daemon" | "desktop-app";
25
+ export declare function effectiveSyncOwner(config: Pick<DaemonConfig, "syncOwner">, machineType: MachineType): SyncOwner;
20
26
  export declare function emptyDaemonConfig(): DaemonConfig;
21
27
  export declare function readDaemonConfig(file: string): DaemonConfig;
22
28
  export declare function writeDaemonConfig(file: string, config: DaemonConfig): void;
@@ -10,8 +10,11 @@ import { valid as validSemver } from "semver";
10
10
  import { isServiceName } from "./catalog.js";
11
11
  import { isMachineType } from "./machine-type.js";
12
12
  import { ensurePrivateDir } from "./paths.js";
13
+ export function effectiveSyncOwner(config, machineType) {
14
+ return config.syncOwner ?? (machineType === "desktop" ? "desktop-app" : "daemon");
15
+ }
13
16
  export function emptyDaemonConfig() {
14
- return { version: 1, enable: [], disable: [] };
17
+ return { version: 1, syncPaused: false, enable: [], disable: [] };
15
18
  }
16
19
  function stringList(value) {
17
20
  return Array.isArray(value) ? value.filter((v) => typeof v === "string") : [];
@@ -36,9 +39,16 @@ export function readDaemonConfig(file) {
36
39
  if (!raw || typeof raw !== "object")
37
40
  throw new Error(`hq daemon config at ${file} is not a JSON object`);
38
41
  const obj = raw;
39
- const config = { version: 1, enable: stringList(obj.enable), disable: stringList(obj.disable) };
42
+ const config = {
43
+ version: 1,
44
+ syncPaused: obj.syncPaused === true,
45
+ enable: stringList(obj.enable),
46
+ disable: stringList(obj.disable),
47
+ };
40
48
  if (isMachineType(obj.machineType))
41
49
  config.machineType = obj.machineType;
50
+ if (obj.syncOwner === "daemon" || obj.syncOwner === "desktop-app")
51
+ config.syncOwner = obj.syncOwner;
42
52
  if (typeof obj.hqRoot === "string" && obj.hqRoot.trim())
43
53
  config.hqRoot = obj.hqRoot;
44
54
  if (typeof obj.hqCloudVersion === "string" && validSemver(obj.hqCloudVersion) === obj.hqCloudVersion) {
@@ -56,6 +66,9 @@ export function writeDaemonConfig(file, config) {
56
66
  ordered.hqRoot = config.hqRoot;
57
67
  if (config.hqCloudVersion)
58
68
  ordered.hqCloudVersion = config.hqCloudVersion;
69
+ ordered.syncPaused = config.syncPaused === true;
70
+ if (config.syncOwner)
71
+ ordered.syncOwner = config.syncOwner;
59
72
  ordered.enable = config.enable;
60
73
  ordered.disable = config.disable;
61
74
  fs.writeFileSync(tmp, `${JSON.stringify(ordered, null, 2)}\n`, { mode: 0o600 });
@@ -15,6 +15,10 @@ export type ControlRequest = {
15
15
  action: "stop";
16
16
  unit: string;
17
17
  }
18
+ /** Re-read persisted configuration and apply any changed blocks immediately. */
19
+ | {
20
+ action: "reconcile";
21
+ }
18
22
  /** Restart the whole daemon, for example after the updater installed a new CLI. */
19
23
  | {
20
24
  action: "restart-daemon";
@@ -19,6 +19,8 @@ function isControlRequest(value) {
19
19
  return typeof v.unit === "string";
20
20
  if (v.action === "restart-daemon")
21
21
  return true;
22
+ if (v.action === "reconcile")
23
+ return true;
22
24
  return false;
23
25
  }
24
26
  // Orders requests written by one process within the same millisecond; the
@@ -16,7 +16,7 @@ import { agentKitPaths } from "../agent-kit/paths.js";
16
16
  import { readBotConfig } from "../bot/config.js";
17
17
  import { botsRoot } from "../bot/paths.js";
18
18
  import { CLAUDE_RUNNER_SCRIPT, CODEX_RUNNER_SCRIPT, enabledServices, planUnits } from "./catalog.js";
19
- import { readDaemonConfig } from "./config.js";
19
+ import { effectiveSyncOwner, readDaemonConfig } from "./config.js";
20
20
  import { findLegacyUnits, legacyOwnerLookup } from "./legacy.js";
21
21
  import { readLegacySyncEnv } from "./legacy-sync-env.js";
22
22
  import { acquireDaemonLock, releaseDaemonLock } from "./lock.js";
@@ -26,6 +26,7 @@ import { appendPrivate, daemonPaths, ensurePrivateDir, openPrivateAppend } from
26
26
  import { takeControlRequests } from "./requests.js";
27
27
  import { currentHqCloud, hqCloudRunners } from "./hq-cloud.js";
28
28
  import { runnerSessionName } from "./remote-control.js";
29
+ import { listSyncRunnerProcesses, readWatchRunnerOwnerStatus, syncHeartbeatTimeoutMs, syncRunnerNodeOptions, watchRunnerOwnerStatusPath, } from "./sync-runner.js";
29
30
  import { STOP_ESCALATION_MS, createSupervisor } from "./supervisor.js";
30
31
  import { userToolDirs } from "./tools.js";
31
32
  export const LOG_ROTATE_BYTES = 5 * 1024 * 1024;
@@ -213,7 +214,10 @@ function makeLogger(paths, foreground) {
213
214
  * an on-failure unit down. The daemon's updater handles CLI updates.
214
215
  */
215
216
  export function unitProcessEnv(base, unit) {
216
- return { ...base, ...unit.env, HQ_DAEMON_UNIT: unit.id, HQ_NO_UPDATE_CHECK: "1" };
217
+ const env = { ...base, ...unit.env, HQ_DAEMON_UNIT: unit.id, HQ_NO_UPDATE_CHECK: "1" };
218
+ if (unit.service === "sync")
219
+ env.NODE_OPTIONS = syncRunnerNodeOptions(env);
220
+ return env;
217
221
  }
218
222
  export async function runDaemon(opts) {
219
223
  const env = opts.env ?? process.env;
@@ -258,10 +262,11 @@ export async function runDaemon(opts) {
258
262
  return legacyCache.units;
259
263
  };
260
264
  let warnedMissingPin = null;
265
+ const daemonSyncRunnerPids = new Set();
261
266
  const plan = () => {
262
267
  const config = readDaemonConfig(paths.configPath);
263
268
  const legacy = legacyUnits();
264
- const configured = config.hqRoot ?? env.HQ_ROOT?.trim() ?? undefined;
269
+ const configured = config.hqRoot ?? (env.HQ_ROOT?.trim() || undefined);
265
270
  const hqRoot = configured && fs.existsSync(configured) ? configured : undefined;
266
271
  // Re-picked every reconcile, so a newer hq-cloud from the updater is used as soon as it lands.
267
272
  const hqCloud = currentHqCloud(paths.root);
@@ -273,7 +278,33 @@ export async function runDaemon(opts) {
273
278
  }
274
279
  }
275
280
  const runners = hqCloud ? hqCloudRunners(hqCloud.root) : { syncRunner: null, heartbeat: null, sessionHost: null };
276
- return planUnits(enabledServices(machineType, config), {
281
+ const services = enabledServices(machineType, config);
282
+ const isSignedIn = signedIn(env, home, log);
283
+ const syncOwner = effectiveSyncOwner(config, machineType);
284
+ let syncRunnerProcesses = [];
285
+ let syncRunnerOwnerPid = null;
286
+ if (machineType === "desktop" &&
287
+ syncOwner === "daemon" &&
288
+ services.includes("sync") &&
289
+ !config.syncPaused &&
290
+ hqRoot &&
291
+ isSignedIn) {
292
+ const processList = listSyncRunnerProcesses(platform);
293
+ if (!processList.ok) {
294
+ syncRunnerProcesses = null;
295
+ log("warn", `${processList.error}; sync will wait before taking ownership`);
296
+ }
297
+ else {
298
+ syncRunnerProcesses = processList.processes;
299
+ try {
300
+ syncRunnerOwnerPid = readWatchRunnerOwnerStatus(watchRunnerOwnerStatusPath(hqRoot, home, env))?.pid ?? null;
301
+ }
302
+ catch (error) {
303
+ log("warn", `could not verify the sync watch-owner status: ${error instanceof Error ? error.message : String(error)}`);
304
+ }
305
+ }
306
+ }
307
+ return planUnits(services, {
277
308
  machineType,
278
309
  platform,
279
310
  home,
@@ -282,14 +313,20 @@ export async function runDaemon(opts) {
282
313
  heartbeatRunnerPath: runners.heartbeat,
283
314
  sessionHostRunnerPath: runners.sessionHost,
284
315
  legacySyncEnv: readLegacySyncEnv(platform),
285
- signedIn: signedIn(env, home, log),
316
+ signedIn: isSignedIn,
286
317
  kitConfigured: fs.existsSync(agentKitPaths(home, env).kitConfigPath),
318
+ syncPaused: config.syncPaused === true,
319
+ syncOwner,
287
320
  desktopAppManagesSync: desktopAppManagesSync({
288
321
  machineType,
289
322
  host: opts.host,
290
323
  legacy,
291
324
  appRunning: () => desktopAppRunning(platform),
292
325
  }),
326
+ syncRunnerProcesses,
327
+ syncRunnerOwnerPid,
328
+ daemonSyncRunnerPids,
329
+ syncHeartbeatTimeoutMs: syncHeartbeatTimeoutMs(env),
293
330
  bots: listLocalBots(),
294
331
  legacyOwner: legacyOwnerLookup(legacy),
295
332
  cliSelfUpdatable: updatable,
@@ -311,20 +348,27 @@ export async function runDaemon(opts) {
311
348
  : command.type === "node"
312
349
  ? [opts.nodeBinary, [command.script, ...command.args]]
313
350
  : [command.file, command.args];
314
- const fd = openPrivateAppend(paths.unitLogPath(unit.id));
315
- try {
316
- const cwd = unit.env.HQ_ROOT && fs.existsSync(unit.env.HQ_ROOT) ? unit.env.HQ_ROOT : home;
317
- const child = spawn(file, args, {
318
- cwd,
319
- env: unitProcessEnv(env, unit),
320
- stdio: ["ignore", fd, fd],
321
- windowsHide: true,
322
- });
323
- return child;
324
- }
325
- finally {
326
- fs.closeSync(fd);
327
- }
351
+ const logPath = paths.unitLogPath(unit.id);
352
+ const logFd = openPrivateAppend(logPath);
353
+ const logStream = fs.createWriteStream(logPath, { fd: logFd, autoClose: true });
354
+ logStream.on("error", (err) => log("error", `could not write ${unit.id} log ${logPath}: ${err.message}`));
355
+ const cwd = unit.env.HQ_ROOT && fs.existsSync(unit.env.HQ_ROOT) ? unit.env.HQ_ROOT : home;
356
+ const child = spawn(file, args, {
357
+ cwd,
358
+ env: unitProcessEnv(env, unit),
359
+ stdio: ["ignore", "pipe", "pipe"],
360
+ windowsHide: true,
361
+ });
362
+ if (unit.service === "sync" && child.pid !== undefined)
363
+ daemonSyncRunnerPids.add(child.pid);
364
+ child.stdout?.pipe(logStream, { end: false });
365
+ child.stderr?.pipe(logStream, { end: false });
366
+ child.on("close", () => {
367
+ if (child.pid !== undefined)
368
+ daemonSyncRunnerPids.delete(child.pid);
369
+ logStream.end();
370
+ });
371
+ return child;
328
372
  };
329
373
  // Set once the shutdown path exists below; a restart request before then is dropped.
330
374
  let restartDaemon = () => { };
@@ -346,6 +390,21 @@ export async function runDaemon(opts) {
346
390
  restartDaemon: () => restartDaemon(),
347
391
  writeState: (state) => writeJsonAtomic(paths.statePath, state),
348
392
  log,
393
+ syncOwnerBlockReason: () => {
394
+ const config = readDaemonConfig(paths.configPath);
395
+ const hqRoot = config.hqRoot ?? env.HQ_ROOT?.trim();
396
+ if (!hqRoot)
397
+ return undefined;
398
+ const statusPath = watchRunnerOwnerStatusPath(hqRoot, home, env);
399
+ try {
400
+ const owner = readWatchRunnerOwnerStatus(statusPath);
401
+ return owner ? `sync owned by ${owner.owner} pid ${owner.pid}` : undefined;
402
+ }
403
+ catch (err) {
404
+ log("error", `could not read sync owner status at ${statusPath}: ${err instanceof Error ? err.message : String(err)}`);
405
+ return undefined;
406
+ }
407
+ },
349
408
  });
350
409
  return await new Promise((resolve) => {
351
410
  let stopping = false;
@@ -20,12 +20,16 @@
20
20
  import type { UnitPlan } from "./catalog.js";
21
21
  import type { MachineType } from "./machine-type.js";
22
22
  import type { ControlRequest } from "./requests.js";
23
+ import { syncPassResult } from "./sync-runner.js";
23
24
  export declare const BACKOFF_BASE_MS = 1000;
24
25
  export declare const BACKOFF_MAX_MS = 60000;
25
26
  export declare const HEALTHY_RUN_MS = 60000;
26
27
  export declare const STOP_ESCALATION_MS = 10000;
27
28
  export interface SupervisedChild {
28
29
  pid?: number;
30
+ stdout?: {
31
+ on(event: "data" | "end", handler: (chunk?: Buffer | string) => void): unknown;
32
+ };
29
33
  on(event: "exit", handler: (code: number | null, signal: string | null) => void): unknown;
30
34
  on(event: "error", handler: (err: Error) => void): unknown;
31
35
  kill(signal?: NodeJS.Signals | number): boolean;
@@ -49,6 +53,8 @@ export interface UnitState {
49
53
  lastRunAt?: number;
50
54
  nextRunAt?: number;
51
55
  lastResult?: "ok" | "failed";
56
+ lastHeartbeatAt?: string;
57
+ lastPassResult?: ReturnType<typeof syncPassResult>;
52
58
  }
53
59
  /** An app that runs the daemon as its own child process instead of an OS unit. */
54
60
  export type HostApp = "desktop";
@@ -79,6 +85,8 @@ export interface SupervisorDeps {
79
85
  clearTimeout?: (handle: unknown) => void;
80
86
  reconcileMs?: number;
81
87
  requestPollMs?: number;
88
+ /** Read hq-cloud's lease status after it exits with the lease-conflict code. */
89
+ syncOwnerBlockReason?: () => string | undefined;
82
90
  }
83
91
  export interface Supervisor {
84
92
  start(): void;
@@ -17,6 +17,8 @@
17
17
  *
18
18
  * Processes, time, and state output are injected; `run.ts` wires the real ones.
19
19
  */
20
+ import { StringDecoder } from "node:string_decoder";
21
+ import { SYNC_RUNNER_LEASE_CONFLICT_EXIT, SYNC_RUNNER_LEASE_RETRY_MS, parseSyncRunnerProtocolLine, syncPassResult, } from "./sync-runner.js";
20
22
  export const BACKOFF_BASE_MS = 1_000;
21
23
  export const BACKOFF_MAX_MS = 60_000;
22
24
  export const HEALTHY_RUN_MS = 60_000;
@@ -25,7 +27,15 @@ export function backoffMs(attempt) {
25
27
  return Math.min(BACKOFF_MAX_MS, BACKOFF_BASE_MS * 2 ** Math.max(0, attempt));
26
28
  }
27
29
  function planKey(plan) {
28
- return JSON.stringify([plan.kind, plan.restart, plan.intervalSec, plan.initialDelaySec, plan.command, plan.env]);
30
+ return JSON.stringify([
31
+ plan.kind,
32
+ plan.restart,
33
+ plan.intervalSec,
34
+ plan.initialDelaySec,
35
+ plan.heartbeatTimeoutMs,
36
+ plan.command,
37
+ plan.env,
38
+ ]);
29
39
  }
30
40
  function errorMessage(err) {
31
41
  return err instanceof Error ? err.message : String(err);
@@ -68,6 +78,11 @@ export function createSupervisor(deps) {
68
78
  clearT(slot.timer);
69
79
  slot.timer = undefined;
70
80
  };
81
+ const clearHeartbeatTimer = (slot) => {
82
+ if (slot.heartbeatTimer !== undefined)
83
+ clearT(slot.heartbeatTimer);
84
+ slot.heartbeatTimer = undefined;
85
+ };
71
86
  const setBlocked = (slot) => {
72
87
  const block = slot.plan.blocked;
73
88
  slot.state.status = block.state;
@@ -78,6 +93,7 @@ export function createSupervisor(deps) {
78
93
  /** Stop the slot's child (if any), then run `then` once it has exited. */
79
94
  const terminate = (slot, then) => {
80
95
  clearTimer(slot);
96
+ clearHeartbeatTimer(slot);
81
97
  const child = slot.child;
82
98
  if (!child) {
83
99
  then?.();
@@ -122,6 +138,69 @@ export function createSupervisor(deps) {
122
138
  const watch = (slot, child) => {
123
139
  live.add(child);
124
140
  let ended = false;
141
+ const timeoutMs = slot.plan.heartbeatTimeoutMs;
142
+ const decoder = timeoutMs ? new StringDecoder("utf8") : null;
143
+ let stdoutRemainder = "";
144
+ let lastHeartbeatPersistedAt = now() - 15_000;
145
+ const armHeartbeatWatchdog = () => {
146
+ if (!timeoutMs)
147
+ return;
148
+ clearHeartbeatTimer(slot);
149
+ slot.heartbeatTimer = setT(() => {
150
+ slot.heartbeatTimer = undefined;
151
+ if (stopped || slot.child !== child || slot.expectingExit)
152
+ return;
153
+ deps.log("warn", `${slot.plan.id} emitted no sync protocol heartbeat for ${timeoutMs}ms; restarting it`);
154
+ terminate(slot, () => scheduleRestart(slot));
155
+ emit();
156
+ }, timeoutMs);
157
+ };
158
+ const acceptProtocolLine = (line) => {
159
+ let event;
160
+ try {
161
+ event = parseSyncRunnerProtocolLine(line);
162
+ }
163
+ catch (err) {
164
+ deps.log("error", `${slot.plan.id} emitted invalid sync protocol output (${Buffer.byteLength(line)} bytes): ${errorMessage(err)}`);
165
+ return;
166
+ }
167
+ if (!event)
168
+ return;
169
+ const at = now();
170
+ slot.state.lastHeartbeatAt = new Date(at).toISOString();
171
+ const pass = syncPassResult(event, slot.state.lastHeartbeatAt);
172
+ if (pass)
173
+ slot.state.lastPassResult = pass;
174
+ armHeartbeatWatchdog();
175
+ if (pass || at - lastHeartbeatPersistedAt >= 15_000) {
176
+ lastHeartbeatPersistedAt = at;
177
+ emit();
178
+ }
179
+ };
180
+ if (decoder && child.stdout) {
181
+ child.stdout.on("data", (chunk) => {
182
+ if (chunk === undefined)
183
+ return;
184
+ stdoutRemainder += decoder.write(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
185
+ let newline = stdoutRemainder.indexOf("\n");
186
+ while (newline >= 0) {
187
+ acceptProtocolLine(stdoutRemainder.slice(0, newline).trim());
188
+ stdoutRemainder = stdoutRemainder.slice(newline + 1);
189
+ newline = stdoutRemainder.indexOf("\n");
190
+ }
191
+ // A malformed or unexpectedly large line is not a heartbeat. Bound
192
+ // retained data so a broken child cannot grow daemon memory forever.
193
+ if (Buffer.byteLength(stdoutRemainder) > 1024 * 1024)
194
+ stdoutRemainder = "";
195
+ });
196
+ child.stdout.on("end", () => {
197
+ stdoutRemainder += decoder.end();
198
+ if (stdoutRemainder)
199
+ acceptProtocolLine(stdoutRemainder.trim());
200
+ stdoutRemainder = "";
201
+ });
202
+ }
203
+ armHeartbeatWatchdog();
125
204
  const onEnd = (code, signal) => {
126
205
  if (ended)
127
206
  return;
@@ -132,6 +211,7 @@ export function createSupervisor(deps) {
132
211
  if (slot.child !== child)
133
212
  return;
134
213
  slot.child = undefined;
214
+ clearHeartbeatTimer(slot);
135
215
  if (slot.killTimer !== undefined) {
136
216
  clearT(slot.killTimer);
137
217
  slot.killTimer = undefined;
@@ -159,6 +239,20 @@ export function createSupervisor(deps) {
159
239
  emit();
160
240
  return;
161
241
  }
242
+ if (slot.plan.service === "sync" && code === SYNC_RUNNER_LEASE_CONFLICT_EXIT) {
243
+ const reason = deps.syncOwnerBlockReason?.() ?? "sync owned by another runner";
244
+ slot.state.status = "waiting";
245
+ slot.state.reason = reason;
246
+ slot.state.nextRunAt = now() + SYNC_RUNNER_LEASE_RETRY_MS;
247
+ deps.log("info", `${reason}; retrying in ${SYNC_RUNNER_LEASE_RETRY_MS}ms`);
248
+ slot.timer = setT(() => {
249
+ slot.timer = undefined;
250
+ startLong(slot);
251
+ emit();
252
+ }, SYNC_RUNNER_LEASE_RETRY_MS);
253
+ emit();
254
+ return;
255
+ }
162
256
  deps.log("warn", `${slot.plan.id} exited (code=${code ?? "null"} signal=${signal ?? "none"})`);
163
257
  if (ranFor >= HEALTHY_RUN_MS)
164
258
  slot.attempts = 0;
@@ -369,6 +463,11 @@ export function createSupervisor(deps) {
369
463
  emit();
370
464
  };
371
465
  const handleRequest = (req) => {
466
+ if (req.action === "reconcile") {
467
+ deps.log("info", "reconciling daemon configuration (requested)");
468
+ reconcile();
469
+ return;
470
+ }
372
471
  if (req.action === "restart-daemon") {
373
472
  deps.log("info", "restarting the daemon (requested)");
374
473
  if (deps.restartDaemon)
@@ -475,6 +574,7 @@ export function createSupervisor(deps) {
475
574
  clearT(requestTimer);
476
575
  for (const slot of slots.values()) {
477
576
  clearTimer(slot);
577
+ clearHeartbeatTimer(slot);
478
578
  slot.child = undefined;
479
579
  if (slot.killTimer !== undefined)
480
580
  clearT(slot.killTimer);
@@ -0,0 +1,58 @@
1
+ /** Local status and resource controls shared by the daemon and its sync runner. */
2
+ export declare const SYNC_RUNNER_OWNER = "hq-daemon";
3
+ export declare const SYNC_RUNNER_LEASE_CONFLICT_EXIT = 20;
4
+ export declare const SYNC_RUNNER_LEASE_RETRY_MS = 15000;
5
+ export declare const DEFAULT_SYNC_HEARTBEAT_TIMEOUT_MS = 300000;
6
+ export declare const DEFAULT_SYNC_RUNNER_HEAP_MB = 3584;
7
+ export declare const SYNC_HEARTBEAT_TIMEOUT_ENV = "HQ_DAEMON_SYNC_HEARTBEAT_TIMEOUT_SECONDS";
8
+ export declare const SYNC_RUNNER_HEAP_ENV = "HQ_SYNC_RUNNER_MAX_OLD_SPACE_MB";
9
+ export interface WatchRunnerOwnerStatus {
10
+ owner: string;
11
+ pid: number;
12
+ version: string;
13
+ startedAt: string;
14
+ heartbeatAt: string;
15
+ }
16
+ export interface SyncRunnerProcess {
17
+ pid: number;
18
+ commandLine: string;
19
+ }
20
+ export type SyncRunnerProcessList = {
21
+ ok: true;
22
+ processes: SyncRunnerProcess[];
23
+ } | {
24
+ ok: false;
25
+ error: string;
26
+ };
27
+ export interface ProcessListCommandResult {
28
+ status: number | null;
29
+ stdout: string;
30
+ stderr: string;
31
+ error?: Error;
32
+ }
33
+ export type ProcessListRunner = (command: string, args: string[]) => ProcessListCommandResult;
34
+ export interface SyncPassResult {
35
+ status: "ok" | "partial" | "failed";
36
+ completedAt: string;
37
+ companiesAttempted?: number;
38
+ errors: number;
39
+ transient: number;
40
+ }
41
+ /** The status file path used by hq-cloud's watch-owner lease. */
42
+ export declare function watchRunnerOwnerStatusPath(hqRoot: string, home?: string, env?: NodeJS.ProcessEnv): string;
43
+ /** Read the watch-owner status JSON without taking or changing its lease. */
44
+ export declare function readWatchRunnerOwnerStatus(statusPath: string): WatchRunnerOwnerStatus | null;
45
+ /** Match a live process command line to the hq-cloud watch runner executable. */
46
+ export declare function isSyncRunnerCommandLine(commandLine: string): boolean;
47
+ /** Return a clear wait reason when an unleased runner already targets this HQ root. */
48
+ export declare function syncRunnerUnleasedBlockReason(hqRoot: string, processes: readonly SyncRunnerProcess[], ownerPid: number | null, platform?: NodeJS.Platform, daemonRunnerPids?: ReadonlySet<number>): string | undefined;
49
+ /** Read complete process command lines so root matching can include trailing arguments. */
50
+ export declare function listSyncRunnerProcesses(platform?: NodeJS.Platform, run?: ProcessListRunner): SyncRunnerProcessList;
51
+ /** The watchdog follows the runner's NDJSON protocol, the same liveness signal used by the desktop app. */
52
+ export declare function parseSyncRunnerProtocolLine(line: string): Record<string, unknown> | null;
53
+ /** Summarize the runner's all-complete event for daemon status. */
54
+ export declare function syncPassResult(event: Record<string, unknown>, completedAt?: string): SyncPassResult | null;
55
+ export declare function syncHeartbeatTimeoutMs(env: NodeJS.ProcessEnv): number;
56
+ /** Preserve an explicit NODE_OPTIONS heap setting; otherwise apply the desktop runner's default/override. */
57
+ export declare function syncRunnerNodeOptions(env: NodeJS.ProcessEnv): string | undefined;
58
+ //# sourceMappingURL=sync-runner.d.ts.map