@indigoai-us/hq-cli 5.345.128 → 5.345.130

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 (63) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/assets/scaffold/core/scripts/lib/codex-output-schema.mjs +304 -0
  3. package/assets/scaffold/core/scripts/workflow-runner.mjs +2890 -0
  4. package/dist/command-catalog.generated.d.ts +29 -0
  5. package/dist/command-catalog.generated.js +34 -0
  6. package/dist/commands/core-bucket-c-work-mesh-offer-native.js +8 -2
  7. package/dist/commands/core-pipeline-workflow-runner-native.d.ts +175 -0
  8. package/dist/commands/core-pipeline-workflow-runner-native.js +3237 -0
  9. package/dist/commands/core-workflow-run-command.d.ts +4 -0
  10. package/dist/commands/core-workflow-run-command.js +41 -0
  11. package/dist/commands/core.js +4 -0
  12. package/dist/commands/daemon.js +2 -0
  13. package/dist/commands/lanes-accounts.js +0 -1
  14. package/dist/commands/lanes.js +26 -14
  15. package/dist/commands/mesh.js +32 -19
  16. package/dist/lib/bot/inbox-poll-backoff-flag.d.ts +3 -5
  17. package/dist/lib/bot/inbox-poll-backoff-flag.js +20 -12
  18. package/dist/lib/bot/inbox-wake.d.ts +18 -0
  19. package/dist/lib/bot/inbox-wake.js +125 -0
  20. package/dist/lib/bot/run.d.ts +8 -0
  21. package/dist/lib/bot/run.js +51 -7
  22. package/dist/lib/bot/runtime/messages-stream.js +31 -3
  23. package/dist/lib/core-utils/workflow-runner-schema.d.ts +7 -0
  24. package/dist/lib/core-utils/workflow-runner-schema.js +259 -0
  25. package/dist/lib/daemon/catalog.d.ts +4 -0
  26. package/dist/lib/daemon/catalog.js +6 -3
  27. package/dist/lib/daemon/flag-monitor.d.ts +4 -1
  28. package/dist/lib/daemon/flag-monitor.js +12 -6
  29. package/dist/lib/daemon/flag.d.ts +2 -0
  30. package/dist/lib/daemon/flag.js +11 -5
  31. package/dist/lib/daemon/hq-child-bootstrap.mjs +47 -7
  32. package/dist/lib/daemon/kill-switch-flags.d.ts +5 -0
  33. package/dist/lib/daemon/kill-switch-flags.js +5 -0
  34. package/dist/lib/daemon/lanes-reconcile-loader.d.ts +7 -0
  35. package/dist/lib/daemon/lanes-reconcile-loader.js +24 -0
  36. package/dist/lib/daemon/lanes-reconcile-watcher.d.ts +17 -0
  37. package/dist/lib/daemon/lanes-reconcile-watcher.js +80 -0
  38. package/dist/lib/daemon/remote-control.d.ts +8 -5
  39. package/dist/lib/daemon/remote-control.js +8 -7
  40. package/dist/lib/daemon/run.d.ts +4 -1
  41. package/dist/lib/daemon/run.js +133 -20
  42. package/dist/lib/doctor/json-output.d.ts +15 -3
  43. package/dist/lib/doctor/json-output.js +13 -1
  44. package/dist/lib/lanes/config.d.ts +8 -4
  45. package/dist/lib/lanes/config.js +30 -36
  46. package/dist/lib/lanes/daemon-reconcile.d.ts +3 -0
  47. package/dist/lib/lanes/daemon-reconcile.js +8 -0
  48. package/dist/lib/lanes/index.d.ts +1 -1
  49. package/dist/lib/lanes/index.js +1 -1
  50. package/dist/lib/lanes/reconcile-result.d.ts +10 -0
  51. package/dist/lib/lanes/reconcile-result.js +11 -0
  52. package/dist/lib/lanes/reconcile.js +3 -26
  53. package/dist/lib/lanes/throttle.js +5 -9
  54. package/dist/lib/lanes/types.d.ts +4 -5
  55. package/dist/lib/lanes/types.js +1 -0
  56. package/dist/lib/mesh/live/daemon/work-subscribe.d.ts +1 -1
  57. package/dist/lib/mesh/live/daemon/work-subscribe.js +8 -1
  58. package/dist/lib/mesh/live/paths.d.ts +2 -0
  59. package/dist/lib/mesh/live/paths.js +2 -0
  60. package/dist/lib/mesh/story-status.d.ts +2 -2
  61. package/dist/lib/mesh/story-status.js +2 -1
  62. package/dist/utils/resident-package-preload.js +6 -0
  63. package/package.json +1 -1
@@ -137,9 +137,11 @@ function relaySignal(signal) {
137
137
  throw new Error(`Unable to relay ${signal}`);
138
138
  }
139
139
  async function run() {
140
- const [prefix, version, ...args] = process.argv.slice(2);
140
+ const [prefix, version, ...rawArgs] = process.argv.slice(2);
141
141
  if (!prefix || !version)
142
142
  throw new Error("The daemon hq bootstrap arguments are invalid");
143
+ const watchHandoff = rawArgs[0] !== "--handoff-watch=off";
144
+ const args = watchHandoff ? rawArgs : rawArgs.slice(1);
143
145
  const lease = await acquireBootstrapLease(prefix, version);
144
146
  const token = randomBytes(16).toString("hex");
145
147
  const markerPathFor = (pid) => path.join(lease.directory, `handoff-${pid}-${token}.handoff`);
@@ -170,16 +172,54 @@ async function run() {
170
172
  const exit = await new Promise((resolve, reject) => {
171
173
  if (!child)
172
174
  return reject(new Error("The HQ CLI child did not start"));
173
- child.once("error", reject);
174
- child.once("exit", (code, signal) => resolve({ code, signal }));
175
- const poll = setInterval(() => {
175
+ let poll;
176
+ let watcher;
177
+ const closeHandoffWatch = () => {
178
+ if (poll)
179
+ clearInterval(poll);
180
+ poll = undefined;
181
+ watcher?.close();
182
+ watcher = undefined;
183
+ };
184
+ const releaseOnHandoff = () => {
176
185
  if (!handoffMarker || leaseReleased || !fs.existsSync(handoffMarker))
177
186
  return;
178
187
  fs.rmSync(handoffMarker, { force: true });
179
188
  releaseLease();
180
- }, 20);
181
- child.once("exit", () => clearInterval(poll));
182
- child.once("error", () => clearInterval(poll));
189
+ closeHandoffWatch();
190
+ };
191
+ const startHandoffPolling = () => {
192
+ if (!poll && !leaseReleased)
193
+ poll = setInterval(releaseOnHandoff, 20);
194
+ };
195
+ child.once("error", reject);
196
+ child.once("exit", (code, signal) => resolve({ code, signal }));
197
+ child.once("exit", closeHandoffWatch);
198
+ child.once("error", closeHandoffWatch);
199
+ if (watchHandoff && handoffMarker) {
200
+ try {
201
+ watcher = fs.watch(lease.directory, (_event, filename) => {
202
+ if (filename === null || filename.toString() === path.basename(handoffMarker))
203
+ releaseOnHandoff();
204
+ });
205
+ watcher.on("error", (error) => {
206
+ process.stderr.write(`hq daemon child bootstrap handoff watch failed (${error.code ?? error.name}); using the recovery poll\n`);
207
+ watcher?.close();
208
+ watcher = undefined;
209
+ startHandoffPolling();
210
+ });
211
+ // Close the create-before-watch race after the watcher is attached.
212
+ releaseOnHandoff();
213
+ }
214
+ catch (error) {
215
+ process.stderr.write(`hq daemon child bootstrap handoff watch unavailable (${error.code ?? "UnknownError"}); using the recovery poll\n`);
216
+ startHandoffPolling();
217
+ releaseOnHandoff();
218
+ }
219
+ }
220
+ else {
221
+ startHandoffPolling();
222
+ }
183
223
  });
184
224
  if (!leaseReleased)
185
225
  releaseLease();
@@ -0,0 +1,5 @@
1
+ export declare const DAEMON_CHILD_BOOTSTRAP_HANDOFF_WATCH_FLAG = "daemon.child-bootstrap-handoff-watch";
2
+ export declare const DAEMON_LANES_RECONCILE_FILE_WATCH_FLAG = "daemon.lanes-reconcile-file-watch";
3
+ export declare const DAEMON_SYNC_PROCESS_SCAN_ON_MISSING_FLAG = "daemon.sync-process-scan-on-missing";
4
+ export declare const DAEMON_SEARCH_INDEX_RUNNER_OWNS_INDEX_FLAG = "daemon.search-index-runner-owns-index";
5
+ //# sourceMappingURL=kill-switch-flags.d.ts.map
@@ -0,0 +1,5 @@
1
+ export const DAEMON_CHILD_BOOTSTRAP_HANDOFF_WATCH_FLAG = "daemon.child-bootstrap-handoff-watch";
2
+ export const DAEMON_LANES_RECONCILE_FILE_WATCH_FLAG = "daemon.lanes-reconcile-file-watch";
3
+ export const DAEMON_SYNC_PROCESS_SCAN_ON_MISSING_FLAG = "daemon.sync-process-scan-on-missing";
4
+ export const DAEMON_SEARCH_INDEX_RUNNER_OWNS_INDEX_FLAG = "daemon.search-index-runner-owns-index";
5
+ //# sourceMappingURL=kill-switch-flags.js.map
@@ -0,0 +1,7 @@
1
+ export interface DaemonLanesReconcileRegistrationOptions {
2
+ hqRoot: () => string;
3
+ env: NodeJS.ProcessEnv;
4
+ }
5
+ /** Load the lazy lanes command root only when the daemon needs its reconciler. */
6
+ export declare function ensureDaemonLanesReconcileHandler(options: DaemonLanesReconcileRegistrationOptions): Promise<void>;
7
+ //# sourceMappingURL=lanes-reconcile-loader.d.ts.map
@@ -0,0 +1,24 @@
1
+ import { daemonLanesReconcileHandler } from "../lanes/daemon-reconcile.js";
2
+ async function registerFromLanesCommand(options) {
3
+ const [{ Command }, { registerLanesCommand }] = await Promise.all([
4
+ import("commander"),
5
+ import("../../commands/lanes.js"),
6
+ ]);
7
+ const env = { ...options.env };
8
+ delete env.HQ_ROOT;
9
+ registerLanesCommand(new Command(), {
10
+ hqRoot: options.hqRoot,
11
+ cwd: options.hqRoot,
12
+ env,
13
+ });
14
+ }
15
+ /** Load the lazy lanes command root only when the daemon needs its reconciler. */
16
+ export async function ensureDaemonLanesReconcileHandler(options) {
17
+ if (daemonLanesReconcileHandler())
18
+ return;
19
+ await registerFromLanesCommand(options);
20
+ if (!daemonLanesReconcileHandler()) {
21
+ throw new Error("lanes command did not register its daemon reconcile handler");
22
+ }
23
+ }
24
+ //# sourceMappingURL=lanes-reconcile-loader.js.map
@@ -0,0 +1,17 @@
1
+ import * as fs from "node:fs";
2
+ type Watcher = Pick<fs.FSWatcher, "close">;
3
+ export interface LanesReconcileWatcher {
4
+ close(): void;
5
+ }
6
+ /** Reconcile at startup, on lane-store changes, and on a bounded in-process timer. */
7
+ export declare function createLanesReconcileWatcher(options: {
8
+ directories: readonly string[];
9
+ reconcile: () => Promise<void>;
10
+ debounceMs?: number;
11
+ intervalMs?: number;
12
+ watch?: (directory: string, listener: (event: string, filename: string | Buffer | null) => void) => Watcher;
13
+ onError?: (error: unknown) => void;
14
+ onWatchError?: (error: unknown) => void;
15
+ }): LanesReconcileWatcher;
16
+ export {};
17
+ //# sourceMappingURL=lanes-reconcile-watcher.d.ts.map
@@ -0,0 +1,80 @@
1
+ import * as fs from "node:fs";
2
+ /** Reconcile at startup, on lane-store changes, and on a bounded in-process timer. */
3
+ export function createLanesReconcileWatcher(options) {
4
+ const debounceMs = options.debounceMs ?? 250;
5
+ const intervalMs = options.intervalMs ?? 60_000;
6
+ const watch = options.watch ?? ((directory, listener) => fs.watch(directory, listener));
7
+ const watchers = [];
8
+ let timer;
9
+ let running = false;
10
+ let rerun = false;
11
+ let closed = false;
12
+ const run = async () => {
13
+ if (closed)
14
+ return;
15
+ if (running) {
16
+ rerun = true;
17
+ return;
18
+ }
19
+ running = true;
20
+ try {
21
+ await options.reconcile();
22
+ }
23
+ catch (error) {
24
+ options.onError?.(error);
25
+ }
26
+ finally {
27
+ running = false;
28
+ if (rerun && !closed) {
29
+ rerun = false;
30
+ schedule();
31
+ }
32
+ else {
33
+ schedule(intervalMs);
34
+ }
35
+ }
36
+ };
37
+ const schedule = (delayMs = debounceMs) => {
38
+ if (closed)
39
+ return;
40
+ if (timer)
41
+ clearTimeout(timer);
42
+ timer = setTimeout(() => {
43
+ timer = undefined;
44
+ void run();
45
+ }, delayMs);
46
+ };
47
+ const watchFailure = (error) => {
48
+ options.onWatchError?.(error);
49
+ close();
50
+ };
51
+ const close = () => {
52
+ if (closed)
53
+ return;
54
+ closed = true;
55
+ if (timer)
56
+ clearTimeout(timer);
57
+ timer = undefined;
58
+ for (const watcher of watchers.splice(0))
59
+ watcher.close();
60
+ };
61
+ try {
62
+ for (const directory of options.directories) {
63
+ const watcher = watch(directory, (_event, filename) => {
64
+ if (filename !== null && !filename.toString().endsWith(".json"))
65
+ return;
66
+ schedule();
67
+ });
68
+ const eventSource = watcher;
69
+ eventSource.on?.("error", watchFailure);
70
+ watchers.push(watcher);
71
+ }
72
+ }
73
+ catch (error) {
74
+ close();
75
+ throw error;
76
+ }
77
+ schedule(0);
78
+ return { close };
79
+ }
80
+ //# sourceMappingURL=lanes-reconcile-watcher.js.map
@@ -11,11 +11,12 @@
11
11
  * backoff (replacing the runner's `while true` loop and its watchdogs);
12
12
  * - the daemon stops the host -> the host kills the session and exits 0;
13
13
  * - a new version of the tool is installed (the updater, or the tool's own
14
- * auto-updater) -> once the session has been quiet for 10 minutes, or after
15
- * 2 hours at the latest, the host kills the session and exits 0, and the
16
- * daemon starts it again on the new version. Reinstalling the same version
17
- * (a long-running Claude session's auto-updater does that every few
18
- * minutes) and hq-cli releases leave the session alone.
14
+ * auto-updater) -> standalone hosts restart after 10 quiet minutes or 2
15
+ * hours at the latest. Shared Remote Control pools never restart solely for
16
+ * an update, because their terminal activity cannot establish that all
17
+ * connected client sessions are idle. A failed pool host is still restarted
18
+ * by the daemon. Reinstalling the same version and hq-cli releases leave
19
+ * running sessions alone.
19
20
  *
20
21
  * The tmux server is a private socket (`-L hq-daemon`), so it never touches a
21
22
  * user's own tmux sessions.
@@ -67,6 +68,8 @@ export interface RemoteControlHostDeps {
67
68
  label: string;
68
69
  /** Identity of the tool's binary on disk; a change means it was updated. Null when it cannot be read. */
69
70
  binaryFingerprint: () => string | null;
71
+ /** Shared pools must keep their clients connected across tool updates. */
72
+ restartOnToolUpdate?: boolean;
70
73
  now: () => number;
71
74
  run: RunCommand;
72
75
  sleep: (ms: number) => Promise<void>;
@@ -11,11 +11,12 @@
11
11
  * backoff (replacing the runner's `while true` loop and its watchdogs);
12
12
  * - the daemon stops the host -> the host kills the session and exits 0;
13
13
  * - a new version of the tool is installed (the updater, or the tool's own
14
- * auto-updater) -> once the session has been quiet for 10 minutes, or after
15
- * 2 hours at the latest, the host kills the session and exits 0, and the
16
- * daemon starts it again on the new version. Reinstalling the same version
17
- * (a long-running Claude session's auto-updater does that every few
18
- * minutes) and hq-cli releases leave the session alone.
14
+ * auto-updater) -> standalone hosts restart after 10 quiet minutes or 2
15
+ * hours at the latest. Shared Remote Control pools never restart solely for
16
+ * an update, because their terminal activity cannot establish that all
17
+ * connected client sessions are idle. A failed pool host is still restarted
18
+ * by the daemon. Reinstalling the same version and hq-cli releases leave
19
+ * running sessions alone.
19
20
  *
20
21
  * The tmux server is a private socket (`-L hq-daemon`), so it never touches a
21
22
  * user's own tmux sessions.
@@ -317,7 +318,7 @@ export async function runRemoteControlHost(deps) {
317
318
  return 1;
318
319
  }
319
320
  deps.log(`${deps.label} started in ${JSON.stringify(deps.cwd)} (creator pid ${invokerPid})`);
320
- const startedWith = deps.binaryFingerprint();
321
+ const startedWith = deps.restartOnToolUpdate === false ? null : deps.binaryFingerprint();
321
322
  let updatedAt = null;
322
323
  for (;;) {
323
324
  if (deps.signal.aborted) {
@@ -338,7 +339,7 @@ export async function runRemoteControlHost(deps) {
338
339
  return 1;
339
340
  }
340
341
  const now = deps.now();
341
- if (updatedAt === null) {
342
+ if (deps.restartOnToolUpdate !== false && updatedAt === null) {
342
343
  const current = deps.binaryFingerprint();
343
344
  if (startedWith !== null && current !== null && current !== startedWith) {
344
345
  updatedAt = now;
@@ -86,7 +86,10 @@ export declare function daemonPlanCanReleasePackageUseLease(plans: readonly Unit
86
86
  * PRIO_DARWIN_BG is inherited by everything the unit spawns (qmd included).
87
87
  */
88
88
  export declare function backgroundQosCommand(command: [string, string[]], backgroundQos: boolean, platform?: NodeJS.Platform | string, exists?: (file: string) => boolean): [string, string[]];
89
- export declare function daemonHqChildCommand(nodeBinary: string, hqBinary: string, args: readonly string[], bootstrapPath: string | null, packagePrefix: string | null): [string, string[]];
89
+ export declare function daemonHqChildCommand(nodeBinary: string, hqBinary: string, args: readonly string[], bootstrapPath: string | null, packagePrefix: string | null, handoffWatchEnabled?: boolean): [string, string[]];
90
+ export declare function daemonSyncUnitIsPresent(units: readonly {
91
+ id: string;
92
+ }[]): boolean;
90
93
  export declare function resolveSyncRunnerPath(): string | null;
91
94
  /** The hosted desktop app uses this env value when planning daemon services. */
92
95
  export declare function daemonInstantSyncEnabled(env: NodeJS.ProcessEnv, configured?: boolean): boolean;
@@ -24,7 +24,12 @@ import { CLAUDE_RUNNER_SCRIPT, CODEX_RUNNER_SCRIPT, enabledServices, planUnits }
24
24
  import { effectiveSyncOwner, readDaemonConfig } from "./config.js";
25
25
  import { assertPersonalServiceSessionCapability, readRemoteControlPoolsConfig, remoteControlPoolSecretsEnv, } from "./remote-control-pools.js";
26
26
  import { loadLanesConfig } from "../lanes/config.js";
27
+ import { daemonLanesReconcileHandler } from "../lanes/daemon-reconcile.js";
28
+ import { ensureDaemonLanesReconcileHandler } from "./lanes-reconcile-loader.js";
27
29
  import { SUPERVISOR_HANDOFF_FLAG, readDaemonFlag } from "./flag.js";
30
+ import { createBoundedDaemonFlagMonitor } from "./flag-monitor.js";
31
+ import { DAEMON_CHILD_BOOTSTRAP_HANDOFF_WATCH_FLAG, DAEMON_LANES_RECONCILE_FILE_WATCH_FLAG, DAEMON_SEARCH_INDEX_RUNNER_OWNS_INDEX_FLAG, DAEMON_SYNC_PROCESS_SCAN_ON_MISSING_FLAG, } from "./kill-switch-flags.js";
32
+ import { createLanesReconcileWatcher } from "./lanes-reconcile-watcher.js";
28
33
  import { findLegacyUnits, legacyMeshUnitsToRetire, legacyOwnerLookup } from "./legacy.js";
29
34
  import { readLegacySyncEnv } from "./legacy-sync-env.js";
30
35
  import { acquireDaemonLock, isProcessAlive, processCommandLine, releaseDaemonLock } from "./lock.js";
@@ -216,11 +221,14 @@ export function backgroundQosCommand(command, backgroundQos, platform = process.
216
221
  return command;
217
222
  return [DARWIN_TASKPOLICY, ["-b", command[0], ...command[1]]];
218
223
  }
219
- export function daemonHqChildCommand(nodeBinary, hqBinary, args, bootstrapPath, packagePrefix) {
224
+ export function daemonHqChildCommand(nodeBinary, hqBinary, args, bootstrapPath, packagePrefix, handoffWatchEnabled = true) {
220
225
  return bootstrapPath && packagePrefix
221
- ? [nodeBinary, [bootstrapPath, packagePrefix, CLI_VERSION, ...args]]
226
+ ? [nodeBinary, [bootstrapPath, packagePrefix, CLI_VERSION, ...(handoffWatchEnabled ? [] : ["--handoff-watch=off"]), ...args]]
222
227
  : [nodeBinary, [hqBinary, ...args]];
223
228
  }
229
+ export function daemonSyncUnitIsPresent(units) {
230
+ return units.some((unit) => unit.id === "sync");
231
+ }
224
232
  export function resolveSyncRunnerPath() {
225
233
  try {
226
234
  const require = createRequire(import.meta.url);
@@ -674,11 +682,104 @@ export async function runDaemon(opts) {
674
682
  };
675
683
  const daemonSyncRunnerPids = new Set();
676
684
  let supervisor = null;
685
+ const killSwitchMonitor = (flagKey) => createBoundedDaemonFlagMonitor({
686
+ fallback: true,
687
+ read: () => readDaemonFlag(flagKey, {
688
+ env,
689
+ fallback: true,
690
+ localOverrideMode: "ignore",
691
+ throwOnError: true,
692
+ }),
693
+ onReadError: (error) => log("warn", `could not read ${flagKey}; keeping its default-on behavior (${error instanceof Error ? error.name : "UnknownError"})`),
694
+ });
695
+ const childBootstrapHandoffWatchFlag = killSwitchMonitor(DAEMON_CHILD_BOOTSTRAP_HANDOFF_WATCH_FLAG);
696
+ const lanesReconcileFileWatchFlag = killSwitchMonitor(DAEMON_LANES_RECONCILE_FILE_WATCH_FLAG);
697
+ const searchIndexRunnerOwnsIndexFlag = killSwitchMonitor(DAEMON_SEARCH_INDEX_RUNNER_OWNS_INDEX_FLAG);
698
+ const syncProcessScanOnMissingFlag = killSwitchMonitor(DAEMON_SYNC_PROCESS_SCAN_ON_MISSING_FLAG);
699
+ let laneReconcileWatcher = null;
700
+ let laneReconcileWatcherRoot = null;
701
+ let laneReconcileWatchFailedRoot = null;
702
+ let laneReconcileHandlerLoadInFlight = false;
703
+ let laneReconcileHandlerLoadFailed = false;
704
+ const loadLaneReconcileHandler = (initialRoot) => {
705
+ if (daemonLanesReconcileHandler() || laneReconcileHandlerLoadInFlight || laneReconcileHandlerLoadFailed)
706
+ return;
707
+ laneReconcileHandlerLoadInFlight = true;
708
+ const resolveRoot = () => {
709
+ const configured = readDaemonConfig(paths.configPath).hqRoot ?? (env.HQ_ROOT?.trim() || undefined);
710
+ return configured && fs.existsSync(configured) ? configured : initialRoot;
711
+ };
712
+ void ensureDaemonLanesReconcileHandler({ hqRoot: resolveRoot, env }).then(() => {
713
+ supervisor?.reconcile();
714
+ }).catch((error) => {
715
+ laneReconcileHandlerLoadFailed = true;
716
+ log("error", `could not load the in-process lanes reconciler; retaining the scheduled unit (${error instanceof Error ? error.message : String(error)})`);
717
+ }).finally(() => {
718
+ laneReconcileHandlerLoadInFlight = false;
719
+ });
720
+ };
721
+ const closeRuntimeWatchers = () => {
722
+ laneReconcileWatcher?.close();
723
+ laneReconcileWatcher = null;
724
+ laneReconcileWatcherRoot = null;
725
+ childBootstrapHandoffWatchFlag.close();
726
+ lanesReconcileFileWatchFlag.close();
727
+ searchIndexRunnerOwnsIndexFlag.close();
728
+ syncProcessScanOnMissingFlag.close();
729
+ };
677
730
  const plan = () => {
678
731
  const config = readDaemonConfig(paths.configPath);
679
732
  const legacy = legacyUnits();
680
733
  const configured = config.hqRoot ?? (env.HQ_ROOT?.trim() || undefined);
681
734
  const hqRoot = configured && fs.existsSync(configured) ? configured : undefined;
735
+ if (laneReconcileWatcherRoot && laneReconcileWatcherRoot !== hqRoot) {
736
+ laneReconcileWatcher?.close();
737
+ laneReconcileWatcher = null;
738
+ laneReconcileWatcherRoot = null;
739
+ laneReconcileWatchFailedRoot = null;
740
+ }
741
+ const lanesReconcileEnabled = lanesReconcileFileWatchFlag.status().enabled;
742
+ if (!lanesReconcileEnabled) {
743
+ laneReconcileWatcher?.close();
744
+ laneReconcileWatcher = null;
745
+ laneReconcileWatcherRoot = null;
746
+ laneReconcileWatchFailedRoot = null;
747
+ }
748
+ else if (hqRoot) {
749
+ if (!daemonLanesReconcileHandler())
750
+ loadLaneReconcileHandler(hqRoot);
751
+ const laneReconcileHandler = daemonLanesReconcileHandler();
752
+ if (laneReconcileHandler && laneReconcileWatcherRoot !== hqRoot && laneReconcileWatchFailedRoot !== hqRoot) {
753
+ laneReconcileWatcher?.close();
754
+ laneReconcileWatcher = null;
755
+ laneReconcileWatcherRoot = null;
756
+ const laneStore = path.join(hqRoot, "workspace", "lanes");
757
+ const laneDirectories = [path.join(laneStore, "lanes"), path.join(laneStore, "questions")];
758
+ try {
759
+ for (const directory of laneDirectories)
760
+ fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
761
+ laneReconcileWatcher = createLanesReconcileWatcher({
762
+ directories: laneDirectories,
763
+ reconcile: laneReconcileHandler,
764
+ intervalMs: 15_000,
765
+ onError: (error) => log("error", `in-process lanes reconcile failed: ${error instanceof Error ? error.message : String(error)}`),
766
+ onWatchError: (error) => {
767
+ laneReconcileWatcher = null;
768
+ laneReconcileWatcherRoot = null;
769
+ laneReconcileWatchFailedRoot = hqRoot;
770
+ log("error", `lane-store watch failed; restoring the scheduled reconcile unit (${error instanceof Error ? error.message : String(error)})`);
771
+ supervisor?.reconcile();
772
+ },
773
+ });
774
+ laneReconcileWatcherRoot = hqRoot;
775
+ laneReconcileWatchFailedRoot = null;
776
+ }
777
+ catch (error) {
778
+ laneReconcileWatchFailedRoot = hqRoot;
779
+ log("error", `could not watch the lane store; retaining the scheduled reconcile unit (${error instanceof Error ? error.message : String(error)})`);
780
+ }
781
+ }
782
+ }
682
783
  let remoteControlPools = { version: 1, pools: [] };
683
784
  let remoteControlPoolsError;
684
785
  try {
@@ -810,21 +911,30 @@ export async function runDaemon(opts) {
810
911
  !config.syncPaused &&
811
912
  hqRoot &&
812
913
  isSignedIn) {
813
- const processList = listSyncRunnerProcesses(platform);
814
- if (!processList.ok) {
815
- syncRunnerProcesses = null;
816
- log("warn", `${processList.error}; sync will wait before taking ownership`);
817
- }
818
- else {
819
- syncRunnerProcesses = processList.processes;
820
- try {
821
- syncRunnerOwnerPid = readWatchRunnerOwnerStatus(watchRunnerOwnerStatusPath(hqRoot, home, env))?.pid ?? null;
914
+ const syncUnitPresent = daemonSyncUnitIsPresent(supervisor?.state().units ?? []);
915
+ if (!syncProcessScanOnMissingFlag.status().enabled || !syncUnitPresent) {
916
+ const processList = listSyncRunnerProcesses(platform);
917
+ if (!processList.ok) {
918
+ syncRunnerProcesses = null;
919
+ log("warn", `${processList.error}; sync will wait before taking ownership`);
822
920
  }
823
- catch (error) {
824
- log("warn", `could not verify the sync watch-owner status: ${error instanceof Error ? error.message : String(error)}`);
921
+ else {
922
+ syncRunnerProcesses = processList.processes;
923
+ try {
924
+ syncRunnerOwnerPid = readWatchRunnerOwnerStatus(watchRunnerOwnerStatusPath(hqRoot, home, env))?.pid ?? null;
925
+ }
926
+ catch (error) {
927
+ log("warn", `could not verify the sync watch-owner status: ${error instanceof Error ? error.message : String(error)}`);
928
+ }
825
929
  }
826
930
  }
827
931
  }
932
+ const appManagesSync = desktopAppManagesSync({
933
+ machineType,
934
+ host: opts.host,
935
+ legacy,
936
+ appRunning: () => desktopAppRunning(platform),
937
+ });
828
938
  const plans = planUnits(services, {
829
939
  machineType,
830
940
  platform,
@@ -840,15 +950,15 @@ export async function runDaemon(opts) {
840
950
  kitConfigured: fs.existsSync(agentKitPaths(home, env).kitConfigPath),
841
951
  syncPaused: config.syncPaused === true,
842
952
  syncOwner,
843
- desktopAppManagesSync: desktopAppManagesSync({
844
- machineType,
845
- host: opts.host,
846
- legacy,
847
- appRunning: () => desktopAppRunning(platform),
848
- }),
953
+ desktopAppManagesSync: appManagesSync,
849
954
  syncRunnerProcesses,
850
955
  syncRunnerOwnerPid,
851
956
  daemonSyncRunnerPids,
957
+ lanesReconcileInProcess: Boolean(laneReconcileWatcher && laneReconcileWatcherRoot === hqRoot),
958
+ searchIndexOwnedBySyncRunner: searchIndexRunnerOwnsIndexFlag.status().enabled &&
959
+ hqCloud !== null && !hqCloudVersionOlder(hqCloud.version, "6.18.75") &&
960
+ syncOwner === "daemon" && services.includes("sync") && !config.syncPaused &&
961
+ !appManagesSync && Boolean(hqRoot && isSignedIn && runners.syncRunner),
852
962
  syncHeartbeatTimeoutMs: syncHeartbeatTimeoutMs(env),
853
963
  bots: listLocalBots(),
854
964
  legacyOwner: legacyOwnerLookup(legacy),
@@ -884,7 +994,7 @@ export async function runDaemon(opts) {
884
994
  if (!command || command.type === "internal")
885
995
  throw new Error(`${unit.id} has no process to start`);
886
996
  let [file, args] = command.type === "hq"
887
- ? daemonHqChildCommand(opts.nodeBinary, opts.hqBinary, command.args, stagedHqChildBootstrap?.bootstrapPath ?? null, cliPackagePrefix)
997
+ ? daemonHqChildCommand(opts.nodeBinary, opts.hqBinary, command.args, stagedHqChildBootstrap?.bootstrapPath ?? null, cliPackagePrefix, childBootstrapHandoffWatchFlag.status().enabled)
888
998
  : command.type === "node"
889
999
  ? [opts.nodeBinary, [command.script, ...command.args]]
890
1000
  : [command.file, command.args];
@@ -1112,6 +1222,7 @@ export async function runDaemon(opts) {
1112
1222
  stopping = true;
1113
1223
  log("info", `${why}; stopping services`);
1114
1224
  closeHqd();
1225
+ closeRuntimeWatchers();
1115
1226
  // Keep shutdown bounded, but let a stopping sync runner reach its persist deadline.
1116
1227
  const cap = createShutdownCap();
1117
1228
  extendSyncShutdownCap = cap.extendForSyncRunnerPersist;
@@ -1139,6 +1250,7 @@ export async function runDaemon(opts) {
1139
1250
  return;
1140
1251
  stopping = true;
1141
1252
  closeHqd();
1253
+ closeRuntimeWatchers();
1142
1254
  const released = supervisor.release();
1143
1255
  // What these readers have not read stays in the FIFO for the next daemon.
1144
1256
  for (const tail of protocolTails)
@@ -1207,6 +1319,7 @@ export async function runDaemon(opts) {
1207
1319
  catch (err) {
1208
1320
  log("error", `supervisor failed to start: ${err.message}`);
1209
1321
  closeHqd();
1322
+ closeRuntimeWatchers();
1210
1323
  releaseDaemonLock(paths.lockPath);
1211
1324
  resolve(1);
1212
1325
  }
@@ -106,9 +106,10 @@ export interface BuildDoctorJsonInput {
106
106
  clientHealthCheckStatusEnabledCheckIds?: ReadonlySet<string>;
107
107
  /**
108
108
  * JSON consumed by the SessionStart health hook (`--client-health-report`).
109
- * Ordinary hq-core upgrade WARNs and leftover `sync.journal.personal` WARNs
110
- * are omitted from every JSON document (`--json` and `--client-health-report`),
111
- * including `summary` tallies: `--fix` cannot apply a release, and the leftover
109
+ * Ordinary hq-core upgrade WARNs, leftover `sync.journal.personal` WARNs, and
110
+ * leftover `sync.manifest.personal` WARNs are omitted from every JSON document
111
+ * (`--json` and `--client-health-report`), including `summary` tallies: `--fix`
112
+ * cannot apply a release or upload personal manifests, and the leftover
112
113
  * personal shard is not a live sync target. Company journals this `--fix`
113
114
  * pass did not try to refresh are omitted the same way: the repair cap leaves
114
115
  * them for a later session, and older hooks that only call `hq doctor --json`
@@ -141,6 +142,17 @@ export declare function isLeftoverPersonalJournalWarn(result: {
141
142
  checkId: string;
142
143
  status: DoctorStatus;
143
144
  }): boolean;
145
+ /**
146
+ * True when doctor still WARNs on a stale personal sync manifest.
147
+ * `--fix` does not upload manifests (network, auth, vault write), so the row
148
+ * stays WARN after a repair pass. JSON omits the WARN so `--json` health hooks
149
+ * do not file it as a failed repair. FAIL stays visible, and human
150
+ * `hq doctor` still prints the WARN.
151
+ */
152
+ export declare function isLeftoverPersonalManifestWarn(result: {
153
+ checkId: string;
154
+ status: DoctorStatus;
155
+ }): boolean;
144
156
  /** Assemble the machine-readable document from a completed run. */
145
157
  export declare function buildDoctorJson(input: BuildDoctorJsonInput): DoctorJsonDocument;
146
158
  /** Serialise the document as pretty-printed JSON with a trailing newline. */
@@ -50,8 +50,20 @@ export function isOrdinaryCoreUpdateWarn(result) {
50
50
  export function isLeftoverPersonalJournalWarn(result) {
51
51
  return result.checkId === "sync.journal.personal" && result.status === "WARN";
52
52
  }
53
+ /**
54
+ * True when doctor still WARNs on a stale personal sync manifest.
55
+ * `--fix` does not upload manifests (network, auth, vault write), so the row
56
+ * stays WARN after a repair pass. JSON omits the WARN so `--json` health hooks
57
+ * do not file it as a failed repair. FAIL stays visible, and human
58
+ * `hq doctor` still prints the WARN.
59
+ */
60
+ export function isLeftoverPersonalManifestWarn(result) {
61
+ return result.checkId === "sync.manifest.personal" && result.status === "WARN";
62
+ }
53
63
  function omitFromDoctorJson(result) {
54
- return isOrdinaryCoreUpdateWarn(result) || isLeftoverPersonalJournalWarn(result);
64
+ return (isOrdinaryCoreUpdateWarn(result) ||
65
+ isLeftoverPersonalJournalWarn(result) ||
66
+ isLeftoverPersonalManifestWarn(result));
55
67
  }
56
68
  /** Tally the results that actually ship in the JSON document. */
57
69
  function summarizeJsonResults(results) {
@@ -9,10 +9,14 @@
9
9
  import * as fs from "node:fs";
10
10
  import { type AccountConfig, type LaneProvider, type LanesConfig, type RunawaySearchGuardConfig, type LivenessWindows, type ProviderConfig } from "./types.js";
11
11
  export declare const LANES_CONFIG_REL: string;
12
- export declare const DEFAULT_MAX_QUEUED = 3;
13
- export declare const DEFAULT_CPU_BUSY_PAUSE = 0.85;
14
- export declare const DEFAULT_CPU_BUSY_RESUME = 0.7;
15
- export declare const DEFAULT_MIN_AVAILABLE_MEM_MB = 1024;
12
+ /**
13
+ * Owner directive 2026-10-14: no default lane caps or CPU/memory admission
14
+ * gates. A provider has no running or queued limit unless the operator sets
15
+ * `max_running` / `max_queued` explicitly; this value stands for "no limit"
16
+ * and stays a finite integer so it survives JSON output.
17
+ */
18
+ export declare const UNLIMITED_LANES: number;
19
+ export declare const DEFAULT_MAX_QUEUED: number;
16
20
  /**
17
21
  * Owner directive 2026-09-22: "if it's been an hour since last msg, always
18
22
  * run compact". Override per provider with `compact_after_ms`.