@indigoai-us/hq-cli 5.276.0 → 5.278.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.
Files changed (83) hide show
  1. package/CHANGELOG.md +99 -0
  2. package/assets/scaffold/core/scripts/hq-delegate-pickup.sh +99 -0
  3. package/assets/scaffold/core/scripts/hq-delegate-repo.sh +158 -0
  4. package/assets/scaffold/core/scripts/hq-delegate-resolve.sh +145 -0
  5. package/assets/scaffold/core/scripts/hq-delegate-secrets.sh +129 -0
  6. package/assets/scaffold/core/scripts/hq-delegate-transfer.sh +175 -0
  7. package/assets/scaffold/core/scripts/lib/secret-patterns.sh +58 -0
  8. package/dist/command-catalog.generated.d.ts +239 -1
  9. package/dist/command-catalog.generated.js +302 -1
  10. package/dist/commands/core-delegate.d.ts +6 -0
  11. package/dist/commands/core-delegate.js +987 -0
  12. package/dist/commands/core.js +10 -0
  13. package/dist/commands/daemon.d.ts +3 -0
  14. package/dist/commands/daemon.js +173 -1
  15. package/dist/commands/dm.d.ts +2 -0
  16. package/dist/commands/dm.js +11 -4
  17. package/dist/commands/lanes-accounts.js +44 -2
  18. package/dist/commands/lanes-message.js +69 -12
  19. package/dist/commands/lanes-resume.js +2 -0
  20. package/dist/commands/lanes-shared.d.ts +3 -0
  21. package/dist/commands/lanes-shared.js +12 -2
  22. package/dist/commands/lanes.d.ts +5 -1
  23. package/dist/commands/lanes.js +399 -47
  24. package/dist/commands/secrets.d.ts +16 -1
  25. package/dist/commands/secrets.js +77 -48
  26. package/dist/commands/sync-mode.d.ts +11 -0
  27. package/dist/commands/sync-mode.js +30 -0
  28. package/dist/fast-scaffold.d.ts +17 -0
  29. package/dist/fast-scaffold.js +91 -0
  30. package/dist/index.js +17 -1
  31. package/dist/lanes-drain-hook.js +154 -7
  32. package/dist/lib/agent-kit/run/inbox.js +3 -2
  33. package/dist/lib/checkpoint-ontology/apply.d.ts +6 -0
  34. package/dist/lib/checkpoint-ontology/apply.js +111 -3
  35. package/dist/lib/checkpoint-ontology/plan.d.ts +23 -0
  36. package/dist/lib/checkpoint-ontology/plan.js +23 -0
  37. package/dist/lib/checkpoint-ontology/tree.d.ts +2 -0
  38. package/dist/lib/checkpoint-ontology/tree.js +34 -0
  39. package/dist/lib/core-utils/jq-json-serializer.d.ts +5 -0
  40. package/dist/lib/core-utils/jq-json-serializer.js +10 -0
  41. package/dist/lib/core-utils/secret-patterns.d.ts +41 -0
  42. package/dist/lib/core-utils/secret-patterns.js +33 -0
  43. package/dist/lib/daemon/catalog.d.ts +18 -2
  44. package/dist/lib/daemon/catalog.js +18 -2
  45. package/dist/lib/daemon/config.d.ts +6 -0
  46. package/dist/lib/daemon/config.js +15 -2
  47. package/dist/lib/daemon/requests.d.ts +4 -0
  48. package/dist/lib/daemon/requests.js +2 -0
  49. package/dist/lib/daemon/run.js +94 -8
  50. package/dist/lib/daemon/supervisor.d.ts +8 -0
  51. package/dist/lib/daemon/supervisor.js +101 -1
  52. package/dist/lib/daemon/sync-runner.d.ts +58 -0
  53. package/dist/lib/daemon/sync-runner.js +277 -0
  54. package/dist/lib/doctor/__testing__/run-doctor.d.ts +8 -0
  55. package/dist/lib/doctor/__testing__/run-doctor.js +52 -0
  56. package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
  57. package/dist/lib/flag-registry-worker-source.generated.js +1 -1
  58. package/dist/lib/lanes/claude-balance.d.ts +6 -1
  59. package/dist/lib/lanes/claude-balance.js +46 -8
  60. package/dist/lib/lanes/config.d.ts +2 -1
  61. package/dist/lib/lanes/config.js +31 -0
  62. package/dist/lib/lanes/hire-grant-dispatch.d.ts +2 -0
  63. package/dist/lib/lanes/hire-grant-dispatch.js +41 -0
  64. package/dist/lib/lanes/index.d.ts +1 -1
  65. package/dist/lib/lanes/keep-alive.d.ts +3 -0
  66. package/dist/lib/lanes/keep-alive.js +3 -0
  67. package/dist/lib/lanes/outpost-dispatch.d.ts +4 -0
  68. package/dist/lib/lanes/outpost-dispatch.js +11 -2
  69. package/dist/lib/lanes/reconcile.d.ts +9 -0
  70. package/dist/lib/lanes/reconcile.js +127 -2
  71. package/dist/lib/lanes/runaway-tool-guard.d.ts +38 -0
  72. package/dist/lib/lanes/runaway-tool-guard.js +284 -0
  73. package/dist/lib/lanes/spawn.js +17 -10
  74. package/dist/lib/lanes/store.js +45 -1
  75. package/dist/lib/lanes/types.d.ts +37 -1
  76. package/dist/lib/lanes/types.js +2 -2
  77. package/dist/lib/lanes/watcher.d.ts +2 -0
  78. package/dist/lib/lanes/watcher.js +13 -2
  79. package/dist/secrets-env-fast-path.d.ts +5 -0
  80. package/dist/secrets-env-fast-path.js +16 -0
  81. package/dist/secrets-env-fast-route.d.ts +18 -0
  82. package/dist/secrets-env-fast-route.js +54 -0
  83. package/package.json +2 -2
@@ -17,7 +17,7 @@ import { agentKitPaths } from "../agent-kit/paths.js";
17
17
  import { readBotConfig } from "../bot/config.js";
18
18
  import { botsRoot } from "../bot/paths.js";
19
19
  import { CLAUDE_RUNNER_SCRIPT, CODEX_RUNNER_SCRIPT, enabledServices, planUnits } from "./catalog.js";
20
- import { readDaemonConfig } from "./config.js";
20
+ import { effectiveSyncOwner, readDaemonConfig } from "./config.js";
21
21
  import { SUPERVISOR_HANDOFF_FLAG, readDaemonFlag } from "./flag.js";
22
22
  import { findLegacyUnits, legacyOwnerLookup } from "./legacy.js";
23
23
  import { readLegacySyncEnv } from "./legacy-sync-env.js";
@@ -29,6 +29,7 @@ import { takeControlRequests } from "./requests.js";
29
29
  import { currentHqCloud, hqCloudRunners } from "./hq-cloud.js";
30
30
  import { runnerSessionName } from "./remote-control.js";
31
31
  import { adoptChild, createChildTracker, finishSuccession, handoffEligible, readProcStart, stopOrphans, successionPaths, systemdKillMode, takeSuccession, writeHandoff, } from "./succession.js";
32
+ import { listSyncRunnerProcesses, readWatchRunnerOwnerStatus, syncHeartbeatTimeoutMs, syncRunnerNodeOptions, watchRunnerOwnerStatusPath, } from "./sync-runner.js";
32
33
  import { STOP_ESCALATION_MS, createSupervisor, } from "./supervisor.js";
33
34
  import { userToolDirs } from "./tools.js";
34
35
  export const LOG_ROTATE_BYTES = 5 * 1024 * 1024;
@@ -218,7 +219,10 @@ function makeLogger(paths, foreground) {
218
219
  * an on-failure unit down. The daemon's updater handles CLI updates.
219
220
  */
220
221
  export function unitProcessEnv(base, unit) {
221
- return { ...base, ...unit.env, HQ_DAEMON_UNIT: unit.id, HQ_NO_UPDATE_CHECK: "1" };
222
+ const env = { ...base, ...unit.env, HQ_DAEMON_UNIT: unit.id, HQ_NO_UPDATE_CHECK: "1" };
223
+ if (unit.service === "sync")
224
+ env.NODE_OPTIONS = syncRunnerNodeOptions(env);
225
+ return env;
222
226
  }
223
227
  export async function runDaemon(opts) {
224
228
  const env = opts.env ?? process.env;
@@ -289,10 +293,11 @@ export async function runDaemon(opts) {
289
293
  return legacyCache.units;
290
294
  };
291
295
  let warnedMissingPin = null;
296
+ const daemonSyncRunnerPids = new Set();
292
297
  const plan = () => {
293
298
  const config = readDaemonConfig(paths.configPath);
294
299
  const legacy = legacyUnits();
295
- const configured = config.hqRoot ?? env.HQ_ROOT?.trim() ?? undefined;
300
+ const configured = config.hqRoot ?? (env.HQ_ROOT?.trim() || undefined);
296
301
  const hqRoot = configured && fs.existsSync(configured) ? configured : undefined;
297
302
  // Re-picked every reconcile, so a newer hq-cloud from the updater is used as soon as it lands.
298
303
  const hqCloud = currentHqCloud(paths.root);
@@ -304,7 +309,33 @@ export async function runDaemon(opts) {
304
309
  }
305
310
  }
306
311
  const runners = hqCloud ? hqCloudRunners(hqCloud.root) : { syncRunner: null, heartbeat: null, sessionHost: null };
307
- return planUnits(enabledServices(machineType, config), {
312
+ const services = enabledServices(machineType, config);
313
+ const isSignedIn = signedIn(env, home, log);
314
+ const syncOwner = effectiveSyncOwner(config, machineType);
315
+ let syncRunnerProcesses = [];
316
+ let syncRunnerOwnerPid = null;
317
+ if (machineType === "desktop" &&
318
+ syncOwner === "daemon" &&
319
+ services.includes("sync") &&
320
+ !config.syncPaused &&
321
+ hqRoot &&
322
+ isSignedIn) {
323
+ const processList = listSyncRunnerProcesses(platform);
324
+ if (!processList.ok) {
325
+ syncRunnerProcesses = null;
326
+ log("warn", `${processList.error}; sync will wait before taking ownership`);
327
+ }
328
+ else {
329
+ syncRunnerProcesses = processList.processes;
330
+ try {
331
+ syncRunnerOwnerPid = readWatchRunnerOwnerStatus(watchRunnerOwnerStatusPath(hqRoot, home, env))?.pid ?? null;
332
+ }
333
+ catch (error) {
334
+ log("warn", `could not verify the sync watch-owner status: ${error instanceof Error ? error.message : String(error)}`);
335
+ }
336
+ }
337
+ }
338
+ return planUnits(services, {
308
339
  machineType,
309
340
  platform,
310
341
  home,
@@ -313,14 +344,20 @@ export async function runDaemon(opts) {
313
344
  heartbeatRunnerPath: runners.heartbeat,
314
345
  sessionHostRunnerPath: runners.sessionHost,
315
346
  legacySyncEnv: readLegacySyncEnv(platform),
316
- signedIn: signedIn(env, home, log),
347
+ signedIn: isSignedIn,
317
348
  kitConfigured: fs.existsSync(agentKitPaths(home, env).kitConfigPath),
349
+ syncPaused: config.syncPaused === true,
350
+ syncOwner,
318
351
  desktopAppManagesSync: desktopAppManagesSync({
319
352
  machineType,
320
353
  host: opts.host,
321
354
  legacy,
322
355
  appRunning: () => desktopAppRunning(platform),
323
356
  }),
357
+ syncRunnerProcesses,
358
+ syncRunnerOwnerPid,
359
+ daemonSyncRunnerPids,
360
+ syncHeartbeatTimeoutMs: syncHeartbeatTimeoutMs(env),
324
361
  bots: listLocalBots(),
325
362
  legacyOwner: legacyOwnerLookup(legacy),
326
363
  cliSelfUpdatable: updatable,
@@ -342,9 +379,37 @@ export async function runDaemon(opts) {
342
379
  : command.type === "node"
343
380
  ? [opts.nodeBinary, [command.script, ...command.args]]
344
381
  : [command.file, command.args];
345
- const fd = openPrivateAppend(paths.unitLogPath(unit.id));
382
+ const logPath = paths.unitLogPath(unit.id);
383
+ const cwd = unit.env.HQ_ROOT && fs.existsSync(unit.env.HQ_ROOT) ? unit.env.HQ_ROOT : home;
384
+ if (unit.heartbeatTimeoutMs) {
385
+ // The supervisor reads this unit's protocol heartbeats from stdout, so its
386
+ // output goes through the daemon. A piped child cannot outlive the daemon,
387
+ // so it is never handed over: a handoff stops it and the next daemon
388
+ // starts it again.
389
+ const logFd = openPrivateAppend(logPath);
390
+ const logStream = fs.createWriteStream(logPath, { fd: logFd, autoClose: true });
391
+ logStream.on("error", (err) => log("error", `could not write ${unit.id} log ${logPath}: ${err.message}`));
392
+ const child = spawn(file, args, {
393
+ cwd,
394
+ env: unitProcessEnv(env, unit),
395
+ stdio: ["ignore", "pipe", "pipe"],
396
+ windowsHide: true,
397
+ });
398
+ spawned.add(child);
399
+ if (unit.service === "sync" && child.pid !== undefined)
400
+ daemonSyncRunnerPids.add(child.pid);
401
+ child.stdout?.pipe(logStream, { end: false });
402
+ child.stderr?.pipe(logStream, { end: false });
403
+ child.on("close", () => {
404
+ spawned.delete(child);
405
+ if (child.pid !== undefined)
406
+ daemonSyncRunnerPids.delete(child.pid);
407
+ logStream.end();
408
+ });
409
+ return child;
410
+ }
411
+ const fd = openPrivateAppend(logPath);
346
412
  try {
347
- const cwd = unit.env.HQ_ROOT && fs.existsSync(unit.env.HQ_ROOT) ? unit.env.HQ_ROOT : home;
348
413
  const child = spawn(file, args, {
349
414
  cwd,
350
415
  env: unitProcessEnv(env, unit),
@@ -352,7 +417,13 @@ export async function runDaemon(opts) {
352
417
  windowsHide: true,
353
418
  });
354
419
  spawned.add(child);
355
- child.once("exit", () => spawned.delete(child));
420
+ if (unit.service === "sync" && child.pid !== undefined)
421
+ daemonSyncRunnerPids.add(child.pid);
422
+ child.once("exit", () => {
423
+ spawned.delete(child);
424
+ if (child.pid !== undefined)
425
+ daemonSyncRunnerPids.delete(child.pid);
426
+ });
356
427
  // Only long-running services are handed over; a scheduled run left behind finishes on its own.
357
428
  if (unit.kind === "long-running")
358
429
  tracker?.track(unit.id, child);
@@ -382,6 +453,21 @@ export async function runDaemon(opts) {
382
453
  restartDaemon: () => restartDaemon(),
383
454
  writeState: (state) => writeJsonAtomic(paths.statePath, state),
384
455
  log,
456
+ syncOwnerBlockReason: () => {
457
+ const config = readDaemonConfig(paths.configPath);
458
+ const hqRoot = config.hqRoot ?? env.HQ_ROOT?.trim();
459
+ if (!hqRoot)
460
+ return undefined;
461
+ const statusPath = watchRunnerOwnerStatusPath(hqRoot, home, env);
462
+ try {
463
+ const owner = readWatchRunnerOwnerStatus(statusPath);
464
+ return owner ? `sync owned by ${owner.owner} pid ${owner.pid}` : undefined;
465
+ }
466
+ catch (err) {
467
+ log("error", `could not read sync owner status at ${statusPath}: ${err instanceof Error ? err.message : String(err)}`);
468
+ return undefined;
469
+ }
470
+ },
385
471
  cliVersion: CLI_VERSION,
386
472
  adopt,
387
473
  });
@@ -24,6 +24,7 @@
24
24
  import type { UnitPlan } from "./catalog.js";
25
25
  import type { MachineType } from "./machine-type.js";
26
26
  import type { ControlRequest } from "./requests.js";
27
+ import { syncPassResult } from "./sync-runner.js";
27
28
  export declare const BACKOFF_BASE_MS = 1000;
28
29
  export declare const BACKOFF_MAX_MS = 60000;
29
30
  export declare const HEALTHY_RUN_MS = 60000;
@@ -34,6 +35,9 @@ export declare const HANDOFF_STAGGER_MS = 15000;
34
35
  export declare const HANDOFF_DRAIN_MAX_MS: number;
35
36
  export interface SupervisedChild {
36
37
  pid?: number;
38
+ stdout?: {
39
+ on(event: "data" | "end", handler: (chunk?: Buffer | string) => void): unknown;
40
+ };
37
41
  on(event: "exit", handler: (code: number | null, signal: string | null) => void): unknown;
38
42
  on(event: "error", handler: (err: Error) => void): unknown;
39
43
  kill(signal?: NodeJS.Signals | number): boolean;
@@ -57,6 +61,8 @@ export interface UnitState {
57
61
  lastRunAt?: number;
58
62
  nextRunAt?: number;
59
63
  lastResult?: "ok" | "failed";
64
+ lastHeartbeatAt?: string;
65
+ lastPassResult?: ReturnType<typeof syncPassResult>;
60
66
  }
61
67
  /** An app that runs the daemon as its own child process instead of an OS unit. */
62
68
  export type HostApp = "desktop";
@@ -99,6 +105,8 @@ export interface SupervisorDeps {
99
105
  clearTimeout?: (handle: unknown) => void;
100
106
  reconcileMs?: number;
101
107
  requestPollMs?: number;
108
+ /** Read hq-cloud's lease status after it exits with the lease-conflict code. */
109
+ syncOwnerBlockReason?: () => string | undefined;
102
110
  /** The running CLI's version; part of the key of units that run `hq` commands. */
103
111
  cliVersion?: string;
104
112
  /** Services to take over from the previous daemon instead of starting them. */
@@ -21,6 +21,8 @@
21
21
  *
22
22
  * Processes, time, and state output are injected; `run.ts` wires the real ones.
23
23
  */
24
+ import { StringDecoder } from "node:string_decoder";
25
+ import { SYNC_RUNNER_LEASE_CONFLICT_EXIT, SYNC_RUNNER_LEASE_RETRY_MS, parseSyncRunnerProtocolLine, syncPassResult, } from "./sync-runner.js";
24
26
  export const BACKOFF_BASE_MS = 1_000;
25
27
  export const BACKOFF_MAX_MS = 60_000;
26
28
  export const HEALTHY_RUN_MS = 60_000;
@@ -39,7 +41,15 @@ export function backoffMs(attempt) {
39
41
  * `keepAcrossCliUpdates` running.
40
42
  */
41
43
  export function planKey(plan, cliVersion) {
42
- const fields = [plan.kind, plan.restart, plan.intervalSec, plan.initialDelaySec, plan.command, plan.env];
44
+ const fields = [
45
+ plan.kind,
46
+ plan.restart,
47
+ plan.intervalSec,
48
+ plan.initialDelaySec,
49
+ plan.heartbeatTimeoutMs,
50
+ plan.command,
51
+ plan.env,
52
+ ];
43
53
  if (cliVersion !== undefined && plan.command?.type === "hq" && !plan.keepAcrossCliUpdates)
44
54
  fields.push(cliVersion);
45
55
  return JSON.stringify(fields);
@@ -89,6 +99,11 @@ export function createSupervisor(deps) {
89
99
  clearT(slot.timer);
90
100
  slot.timer = undefined;
91
101
  };
102
+ const clearHeartbeatTimer = (slot) => {
103
+ if (slot.heartbeatTimer !== undefined)
104
+ clearT(slot.heartbeatTimer);
105
+ slot.heartbeatTimer = undefined;
106
+ };
92
107
  const setBlocked = (slot) => {
93
108
  const block = slot.plan.blocked;
94
109
  slot.state.status = block.state;
@@ -111,6 +126,7 @@ export function createSupervisor(deps) {
111
126
  */
112
127
  const terminate = (slot, then, drain = false) => {
113
128
  clearTimer(slot);
129
+ clearHeartbeatTimer(slot);
114
130
  const child = slot.child;
115
131
  if (!child) {
116
132
  then?.();
@@ -163,6 +179,69 @@ export function createSupervisor(deps) {
163
179
  const watch = (slot, child) => {
164
180
  live.add(child);
165
181
  let ended = false;
182
+ const timeoutMs = slot.plan.heartbeatTimeoutMs;
183
+ const decoder = timeoutMs ? new StringDecoder("utf8") : null;
184
+ let stdoutRemainder = "";
185
+ let lastHeartbeatPersistedAt = now() - 15_000;
186
+ const armHeartbeatWatchdog = () => {
187
+ if (!timeoutMs)
188
+ return;
189
+ clearHeartbeatTimer(slot);
190
+ slot.heartbeatTimer = setT(() => {
191
+ slot.heartbeatTimer = undefined;
192
+ if (stopped || slot.child !== child || slot.expectingExit)
193
+ return;
194
+ deps.log("warn", `${slot.plan.id} emitted no sync protocol heartbeat for ${timeoutMs}ms; restarting it`);
195
+ terminate(slot, () => scheduleRestart(slot));
196
+ emit();
197
+ }, timeoutMs);
198
+ };
199
+ const acceptProtocolLine = (line) => {
200
+ let event;
201
+ try {
202
+ event = parseSyncRunnerProtocolLine(line);
203
+ }
204
+ catch (err) {
205
+ deps.log("error", `${slot.plan.id} emitted invalid sync protocol output (${Buffer.byteLength(line)} bytes): ${errorMessage(err)}`);
206
+ return;
207
+ }
208
+ if (!event)
209
+ return;
210
+ const at = now();
211
+ slot.state.lastHeartbeatAt = new Date(at).toISOString();
212
+ const pass = syncPassResult(event, slot.state.lastHeartbeatAt);
213
+ if (pass)
214
+ slot.state.lastPassResult = pass;
215
+ armHeartbeatWatchdog();
216
+ if (pass || at - lastHeartbeatPersistedAt >= 15_000) {
217
+ lastHeartbeatPersistedAt = at;
218
+ emit();
219
+ }
220
+ };
221
+ if (decoder && child.stdout) {
222
+ child.stdout.on("data", (chunk) => {
223
+ if (chunk === undefined)
224
+ return;
225
+ stdoutRemainder += decoder.write(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
226
+ let newline = stdoutRemainder.indexOf("\n");
227
+ while (newline >= 0) {
228
+ acceptProtocolLine(stdoutRemainder.slice(0, newline).trim());
229
+ stdoutRemainder = stdoutRemainder.slice(newline + 1);
230
+ newline = stdoutRemainder.indexOf("\n");
231
+ }
232
+ // A malformed or unexpectedly large line is not a heartbeat. Bound
233
+ // retained data so a broken child cannot grow daemon memory forever.
234
+ if (Buffer.byteLength(stdoutRemainder) > 1024 * 1024)
235
+ stdoutRemainder = "";
236
+ });
237
+ child.stdout.on("end", () => {
238
+ stdoutRemainder += decoder.end();
239
+ if (stdoutRemainder)
240
+ acceptProtocolLine(stdoutRemainder.trim());
241
+ stdoutRemainder = "";
242
+ });
243
+ }
244
+ armHeartbeatWatchdog();
166
245
  const onEnd = (code, signal) => {
167
246
  if (ended)
168
247
  return;
@@ -173,6 +252,7 @@ export function createSupervisor(deps) {
173
252
  if (slot.child !== child)
174
253
  return;
175
254
  slot.child = undefined;
255
+ clearHeartbeatTimer(slot);
176
256
  if (slot.killTimer !== undefined) {
177
257
  clearT(slot.killTimer);
178
258
  slot.killTimer = undefined;
@@ -200,6 +280,20 @@ export function createSupervisor(deps) {
200
280
  emit();
201
281
  return;
202
282
  }
283
+ if (slot.plan.service === "sync" && code === SYNC_RUNNER_LEASE_CONFLICT_EXIT) {
284
+ const reason = deps.syncOwnerBlockReason?.() ?? "sync owned by another runner";
285
+ slot.state.status = "waiting";
286
+ slot.state.reason = reason;
287
+ slot.state.nextRunAt = now() + SYNC_RUNNER_LEASE_RETRY_MS;
288
+ deps.log("info", `${reason}; retrying in ${SYNC_RUNNER_LEASE_RETRY_MS}ms`);
289
+ slot.timer = setT(() => {
290
+ slot.timer = undefined;
291
+ startLong(slot);
292
+ emit();
293
+ }, SYNC_RUNNER_LEASE_RETRY_MS);
294
+ emit();
295
+ return;
296
+ }
203
297
  deps.log("warn", `${slot.plan.id} exited (code=${code ?? "null"} signal=${signal ?? "none"})`);
204
298
  if (ranFor >= HEALTHY_RUN_MS)
205
299
  slot.attempts = 0;
@@ -505,6 +599,11 @@ export function createSupervisor(deps) {
505
599
  }
506
600
  };
507
601
  const handleRequest = (req) => {
602
+ if (req.action === "reconcile") {
603
+ deps.log("info", "reconciling daemon configuration (requested)");
604
+ reconcile();
605
+ return;
606
+ }
508
607
  if (req.action === "restart-daemon") {
509
608
  deps.log("info", "restarting the daemon (requested)");
510
609
  if (deps.restartDaemon)
@@ -614,6 +713,7 @@ export function createSupervisor(deps) {
614
713
  clearT(handoffTimer);
615
714
  for (const slot of slots.values()) {
616
715
  clearTimer(slot);
716
+ clearHeartbeatTimer(slot);
617
717
  slot.child = undefined;
618
718
  if (slot.killTimer !== undefined)
619
719
  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