@indigoai-us/hq-cloud 6.18.33 → 6.18.35

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 (48) hide show
  1. package/dist/bin/sync-runner-watch-loop.d.ts.map +1 -1
  2. package/dist/bin/sync-runner-watch-loop.js +31 -8
  3. package/dist/bin/sync-runner-watch-loop.js.map +1 -1
  4. package/dist/bin/sync-runner.d.ts.map +1 -1
  5. package/dist/bin/sync-runner.js +4 -2
  6. package/dist/bin/sync-runner.js.map +1 -1
  7. package/dist/bin/sync-runner.test.js +3 -1
  8. package/dist/bin/sync-runner.test.js.map +1 -1
  9. package/dist/cli/reindex-async.d.ts +9 -0
  10. package/dist/cli/reindex-async.d.ts.map +1 -0
  11. package/dist/cli/reindex-async.js +76 -0
  12. package/dist/cli/reindex-async.js.map +1 -0
  13. package/dist/cli/rescue-core.d.ts.map +1 -1
  14. package/dist/cli/rescue-core.js +1 -1
  15. package/dist/cli/rescue-core.js.map +1 -1
  16. package/dist/cli/rescue-rsync-preflight.test.js +1 -1
  17. package/dist/cli/rescue-rsync-preflight.test.js.map +1 -1
  18. package/dist/journal.d.ts +1 -0
  19. package/dist/journal.d.ts.map +1 -1
  20. package/dist/journal.js +1 -1
  21. package/dist/journal.js.map +1 -1
  22. package/dist/sync/full-pass-cap-governor.bench.test.d.ts +2 -0
  23. package/dist/sync/full-pass-cap-governor.bench.test.d.ts.map +1 -0
  24. package/dist/sync/full-pass-cap-governor.bench.test.js +179 -0
  25. package/dist/sync/full-pass-cap-governor.bench.test.js.map +1 -0
  26. package/dist/sync/full-pass-caps.d.ts +12 -2
  27. package/dist/sync/full-pass-caps.d.ts.map +1 -1
  28. package/dist/sync/full-pass-caps.js +78 -38
  29. package/dist/sync/full-pass-caps.js.map +1 -1
  30. package/dist/sync/full-pass-caps.test.js +144 -1
  31. package/dist/sync/full-pass-caps.test.js.map +1 -1
  32. package/dist/watcher-replan-sweep.test.js +21 -3
  33. package/dist/watcher-replan-sweep.test.js.map +1 -1
  34. package/dist/watcher.d.ts +2 -0
  35. package/dist/watcher.d.ts.map +1 -1
  36. package/dist/watcher.js +166 -50
  37. package/dist/watcher.js.map +1 -1
  38. package/dist/watcher.test.js +59 -5
  39. package/dist/watcher.test.js.map +1 -1
  40. package/dist/watcher.warmup-cancel.test.d.ts +2 -0
  41. package/dist/watcher.warmup-cancel.test.d.ts.map +1 -0
  42. package/dist/watcher.warmup-cancel.test.js +59 -0
  43. package/dist/watcher.warmup-cancel.test.js.map +1 -0
  44. package/dist/watcher.warmup-responsive.test.d.ts +2 -0
  45. package/dist/watcher.warmup-responsive.test.d.ts.map +1 -0
  46. package/dist/watcher.warmup-responsive.test.js +62 -0
  47. package/dist/watcher.warmup-responsive.test.js.map +1 -0
  48. package/package.json +3 -2
package/dist/watcher.js CHANGED
@@ -528,6 +528,16 @@ export function startScopedRecursiveWatch(hqRoot, shouldEmit, onEvent, onError,
528
528
  let budgetExhausted = false;
529
529
  /** True once the initial plan is live; later attaches announce their files. */
530
530
  let planned = false;
531
+ let initialDiscoveryComplete = false;
532
+ const readyListeners = new Set();
533
+ const notifyReady = () => {
534
+ if (initialDiscoveryComplete || closed)
535
+ return;
536
+ initialDiscoveryComplete = true;
537
+ for (const listener of readyListeners)
538
+ listener();
539
+ readyListeners.clear();
540
+ };
531
541
  /** Retires the bootstrap watch — replaced once that watch exists. */
532
542
  let closeBootstrap = () => { };
533
543
  /**
@@ -586,6 +596,8 @@ export function startScopedRecursiveWatch(hqRoot, shouldEmit, onEvent, onError,
586
596
  }
587
597
  /** `dev:ino` of the directory at `absolutePath`; undefined if absent or not a directory. */
588
598
  function directoryIdentity(absolutePath) {
599
+ if (opts.directoryIdentity)
600
+ return opts.directoryIdentity(absolutePath);
589
601
  try {
590
602
  const st = fs.lstatSync(absolutePath);
591
603
  return st.isDirectory() ? `${st.dev}:${st.ino}` : undefined;
@@ -719,7 +731,7 @@ export function startScopedRecursiveWatch(hqRoot, shouldEmit, onEvent, onError,
719
731
  addWatch(dir, false);
720
732
  }
721
733
  /**
722
- * Plan `dir` and attach the result. In shallow mode every in-scope
734
+ * Plan `dir` and attach the result on recursive-watch platforms. In shallow mode every in-scope
723
735
  * directory is attached the moment the walk discovers it — BEFORE the walk
724
736
  * lists that directory — rather than after the whole plan is computed. The
725
737
  * plan is a snapshot: a child created after its parent was listed is in
@@ -747,6 +759,66 @@ export function startScopedRecursiveWatch(hqRoot, shouldEmit, onEvent, onError,
747
759
  listChildDirs: opts.listChildDirs,
748
760
  }));
749
761
  }
762
+ /**
763
+ * Linux needs one inotify watch per included directory. Keep the parent
764
+ * attached before listing it, but yield regularly so a large root cannot
765
+ * hold the runner's event loop while tens of thousands of watches open.
766
+ */
767
+ async function planShallowAndAttach(dir, initial = false) {
768
+ const pending = [dir];
769
+ let workSinceYield = 0;
770
+ const yieldIfNeeded = async () => {
771
+ workSinceYield += 1;
772
+ if (workSinceYield < 128)
773
+ return closed;
774
+ workSinceYield = 0;
775
+ await new Promise((resolve) => setImmediate(resolve));
776
+ return closed;
777
+ };
778
+ while (pending.length > 0 && !closed) {
779
+ const current = pending.pop();
780
+ try {
781
+ if (opts.listChildDirs) {
782
+ for (const child of opts.listChildDirs(current)) {
783
+ if (shouldEmit(child, true)) {
784
+ onDirectorySeen?.(path.resolve(child));
785
+ addWatch(child, false);
786
+ pending.push(child);
787
+ }
788
+ if (await yieldIfNeeded())
789
+ return;
790
+ }
791
+ }
792
+ else {
793
+ const entries = await fs.promises.readdir(current, { withFileTypes: true });
794
+ for (const entry of entries) {
795
+ const absolutePath = path.join(current, entry.name);
796
+ if (entry.isDirectory()) {
797
+ if (shouldEmit(absolutePath, true)) {
798
+ onDirectorySeen?.(path.resolve(absolutePath));
799
+ addWatch(absolutePath, false);
800
+ pending.push(absolutePath);
801
+ }
802
+ }
803
+ else if (!initial && shouldEmit(absolutePath, false)) {
804
+ onEvent(absolutePath, "rename");
805
+ }
806
+ if (await yieldIfNeeded())
807
+ return;
808
+ }
809
+ }
810
+ }
811
+ catch (err) {
812
+ const code = err?.code;
813
+ if (code !== "ENOENT" && code !== "ENOTDIR")
814
+ onError(err);
815
+ }
816
+ }
817
+ if (initial) {
818
+ planned = true;
819
+ notifyReady();
820
+ }
821
+ }
750
822
  /**
751
823
  * Announce every in-scope file already inside a directory that was just
752
824
  * attached. A shallow parent watch reports only that the directory
@@ -853,6 +925,11 @@ export function startScopedRecursiveWatch(hqRoot, shouldEmit, onEvent, onError,
853
925
  return;
854
926
  const attachedAt = Date.now();
855
927
  onDirectorySeen?.(absolutePath);
928
+ if (!recursiveWatches) {
929
+ addWatch(absolutePath, false);
930
+ void planShallowAndAttach(absolutePath);
931
+ return;
932
+ }
856
933
  planAndAttach(absolutePath);
857
934
  if (!planned)
858
935
  return;
@@ -903,7 +980,7 @@ export function startScopedRecursiveWatch(hqRoot, shouldEmit, onEvent, onError,
903
980
  // dropped the moment the scoped plan is live.
904
981
  // No such watch exists in shallow mode: Node's Linux "recursive" shim would
905
982
  // walk the WHOLE tree — worktrees, node_modules and all — to emulate it.
906
- // The runner's initial sync pass covers the planning window there instead.
983
+ // In shallow mode, each parent is attached before its child list is read.
907
984
  let bootstrap = null;
908
985
  if (recursiveWatches) {
909
986
  try {
@@ -924,42 +1001,49 @@ export function startScopedRecursiveWatch(hqRoot, shouldEmit, onEvent, onError,
924
1001
  onError(err);
925
1002
  }
926
1003
  }
927
- // Unbounded on purpose: this walk replaces TreeWatcher's separate known-kinds
928
- // seed walk (same tree, same filter, ~3.6s on a real HQ root), so planning
929
- // the tree exhaustively costs nothing over the status quo and leaves no
930
- // excluded subtree covered by a recursive root.
931
- planAndAttach(root);
932
- planned = true;
933
- // The scoped plan is live, so narrow from whole-tree to in-scope-only — but
934
- // NOT synchronously. Events observed during the planning walk can still be
935
- // sitting in the OS queue undelivered, and closing the handle here drops
936
- // them, reopening the very gap the bootstrap watch exists to close. Hold it
937
- // open for one drain window instead; the overlap only costs duplicate events
938
- // for excluded paths, which the emit filter drops and the debounce coalesces.
939
- if (bootstrap) {
940
- const handle = bootstrap;
941
- bootstrap = null;
942
- const timer = setTimeout(() => {
943
- try {
944
- handle.close();
945
- }
946
- catch {
947
- /* already closed */
948
- }
949
- closeBootstrap = () => { };
950
- }, BOOTSTRAP_DRAIN_MS);
951
- // Never hold the process open just to retire a temporary watch.
952
- timer.unref?.();
953
- closeBootstrap = () => {
954
- clearTimeout(timer);
955
- try {
956
- handle.close();
957
- }
958
- catch {
959
- /* already closed */
960
- }
961
- closeBootstrap = () => { };
962
- };
1004
+ if (!recursiveWatches) {
1005
+ addWatch(root, false);
1006
+ void planShallowAndAttach(root, true);
1007
+ }
1008
+ else {
1009
+ // Unbounded on purpose: this walk replaces TreeWatcher's separate known-kinds
1010
+ // seed walk (same tree, same filter, ~3.6s on a real HQ root), so planning
1011
+ // the tree exhaustively costs nothing over the status quo and leaves no
1012
+ // excluded subtree covered by a recursive root.
1013
+ planAndAttach(root);
1014
+ planned = true;
1015
+ notifyReady();
1016
+ // The scoped plan is live, so narrow from whole-tree to in-scope-only — but
1017
+ // NOT synchronously. Events observed during the planning walk can still be
1018
+ // sitting in the OS queue undelivered, and closing the handle here drops
1019
+ // them, reopening the very gap the bootstrap watch exists to close. Hold it
1020
+ // open for one drain window instead; the overlap only costs duplicate events
1021
+ // for excluded paths, which the emit filter drops and the debounce coalesces.
1022
+ if (bootstrap) {
1023
+ const handle = bootstrap;
1024
+ bootstrap = null;
1025
+ const timer = setTimeout(() => {
1026
+ try {
1027
+ handle.close();
1028
+ }
1029
+ catch {
1030
+ /* already closed */
1031
+ }
1032
+ closeBootstrap = () => { };
1033
+ }, BOOTSTRAP_DRAIN_MS);
1034
+ // Never hold the process open just to retire a temporary watch.
1035
+ timer.unref?.();
1036
+ closeBootstrap = () => {
1037
+ clearTimeout(timer);
1038
+ try {
1039
+ handle.close();
1040
+ }
1041
+ catch {
1042
+ /* already closed */
1043
+ }
1044
+ closeBootstrap = () => { };
1045
+ };
1046
+ }
963
1047
  }
964
1048
  return {
965
1049
  close: () => {
@@ -987,6 +1071,14 @@ export function startScopedRecursiveWatch(hqRoot, shouldEmit, onEvent, onError,
987
1071
  mode: "native",
988
1072
  needsKnownKinds: true,
989
1073
  seededKnownKinds: onDirectorySeen !== undefined,
1074
+ onReady: (listener) => {
1075
+ if (initialDiscoveryComplete) {
1076
+ listener();
1077
+ return () => { };
1078
+ }
1079
+ readyListeners.add(listener);
1080
+ return () => readyListeners.delete(listener);
1081
+ },
990
1082
  watchedPathCount: () => watchers.size,
991
1083
  attachFailures,
992
1084
  probeCoverage: () => probeScopedWatchCoverage({
@@ -1945,7 +2037,7 @@ export class TreeWatcher {
1945
2037
  if (marker)
1946
2038
  marker.epoch += 1;
1947
2039
  }
1948
- enqueueDeleteSnapshotCapture(absolutePath, relativePath, kind) {
2040
+ enqueueDeleteSnapshotCapture(absolutePath, relativePath, kind, deferBatchSchedule = false) {
1949
2041
  // This delete supersedes whatever capture is already in flight for the
1950
2042
  // path, whether or not the queue has room for a new one.
1951
2043
  const live = this.deleteSnapshotEpochs.get(absolutePath);
@@ -1967,7 +2059,7 @@ export class TreeWatcher {
1967
2059
  this.deleteSnapshotEpochs.set(absolutePath, marker);
1968
2060
  const isCurrent = () => this.deleteSnapshotEpochs.get(absolutePath)?.epoch === epoch;
1969
2061
  if (this.queueLocalDeleteSnapshotsBatch) {
1970
- return this.enqueueDeleteSnapshotCaptureBatch(absolutePath, { relativePath, kind, isCurrent });
2062
+ return this.enqueueDeleteSnapshotCaptureBatch(absolutePath, { relativePath, kind, isCurrent }, !deferBatchSchedule);
1971
2063
  }
1972
2064
  const run = this.deleteSnapshotCaptureTail.then(() => this.queueLocalDeleteSnapshots(relativePath, kind, isCurrent), () => this.queueLocalDeleteSnapshots(relativePath, kind, isCurrent));
1973
2065
  this.deleteSnapshotCaptureTail = run.then(() => undefined, () => undefined);
@@ -1975,7 +2067,7 @@ export class TreeWatcher {
1975
2067
  void capture.catch(() => undefined);
1976
2068
  return capture;
1977
2069
  }
1978
- enqueueDeleteSnapshotCaptureBatch(absolutePath, capture) {
2070
+ enqueueDeleteSnapshotCaptureBatch(absolutePath, capture, schedule = true) {
1979
2071
  const promise = new Promise((resolve, reject) => {
1980
2072
  this.pendingDeleteSnapshotCaptureBatch.push({
1981
2073
  absolutePath,
@@ -1985,7 +2077,8 @@ export class TreeWatcher {
1985
2077
  });
1986
2078
  });
1987
2079
  void promise.catch(() => undefined);
1988
- this.scheduleDeleteSnapshotCaptureBatch();
2080
+ if (schedule)
2081
+ this.scheduleDeleteSnapshotCaptureBatch();
1989
2082
  return promise;
1990
2083
  }
1991
2084
  scheduleDeleteSnapshotCaptureBatch() {
@@ -2345,22 +2438,29 @@ export class TreeWatcher {
2345
2438
  cancelled: true,
2346
2439
  });
2347
2440
  const yieldToEventLoop = async () => {
2348
- if (cancelled())
2441
+ if (cancelled()) {
2442
+ if (this.pendingDeleteSnapshotCaptureBatch.length > 0) {
2443
+ this.scheduleDeleteSnapshotCaptureBatch();
2444
+ }
2349
2445
  return false;
2446
+ }
2350
2447
  workSinceYield += 1;
2351
2448
  if (workSinceYield < yieldEvery)
2352
2449
  return true;
2353
2450
  workSinceYield = 0;
2451
+ if (this.pendingDeleteSnapshotCaptureBatch.length > 0) {
2452
+ this.scheduleDeleteSnapshotCaptureBatch();
2453
+ }
2354
2454
  await new Promise((resolve) => setImmediate(resolve));
2355
2455
  return !cancelled();
2356
2456
  };
2357
- const record = (absolutePath, kind) => {
2457
+ const record = async (absolutePath, kind) => {
2358
2458
  const absolute = path.resolve(absolutePath);
2359
2459
  const relative = vaultKeyForLocalPath(this.hqRoot, absolute);
2360
2460
  observed.add(relative);
2361
2461
  let entry;
2362
2462
  try {
2363
- entry = fs.lstatSync(absolute);
2463
+ entry = await fs.promises.lstat(absolute);
2364
2464
  }
2365
2465
  catch {
2366
2466
  return;
@@ -2376,7 +2476,7 @@ export class TreeWatcher {
2376
2476
  return false;
2377
2477
  let entries;
2378
2478
  try {
2379
- entries = fs.readdirSync(dir, { withFileTypes: true });
2479
+ entries = await fs.promises.readdir(dir, { withFileTypes: true });
2380
2480
  }
2381
2481
  catch {
2382
2482
  return !cancelled();
@@ -2396,7 +2496,7 @@ export class TreeWatcher {
2396
2496
  if (entry.isFile() || entry.isSymbolicLink()) {
2397
2497
  // A symlink may be admitted only by a directory-only include rule.
2398
2498
  if (this.shouldEmit(absolute, false) || this.shouldEmit(absolute, true)) {
2399
- record(absolute, "change");
2499
+ await record(absolute, "change");
2400
2500
  }
2401
2501
  }
2402
2502
  if (!(await yieldToEventLoop()))
@@ -2421,8 +2521,17 @@ export class TreeWatcher {
2421
2521
  return cancelledResult();
2422
2522
  continue;
2423
2523
  }
2424
- if (isPresentOnDisk(absolute)) {
2425
- record(absolute, "change");
2524
+ let present = false;
2525
+ try {
2526
+ await fs.promises.lstat(absolute);
2527
+ present = true;
2528
+ }
2529
+ catch {
2530
+ // Missing paths become deletes; other errors preserve the previous
2531
+ // behavior of treating an unreadable path as absent.
2532
+ }
2533
+ if (present) {
2534
+ await record(absolute, "change");
2426
2535
  if (!(await yieldToEventLoop()))
2427
2536
  return cancelledResult();
2428
2537
  continue;
@@ -2432,7 +2541,7 @@ export class TreeWatcher {
2432
2541
  let deleteSnapshotCapture;
2433
2542
  try {
2434
2543
  if (this.queueLocalDeleteSnapshotsBatch || this.queueLocalDeleteSnapshots) {
2435
- deleteSnapshotCapture = this.enqueueDeleteSnapshotCapture(absolute, relative, "unlink");
2544
+ deleteSnapshotCapture = this.enqueueDeleteSnapshotCapture(absolute, relative, "unlink", true);
2436
2545
  }
2437
2546
  else {
2438
2547
  deleteSnapshots = this.captureLocalDeleteSnapshots?.(relative, "unlink") ?? [];
@@ -2449,6 +2558,13 @@ export class TreeWatcher {
2449
2558
  if (!(await yieldToEventLoop()))
2450
2559
  return cancelledResult();
2451
2560
  }
2561
+ // Keep the warm-up rows from the same bounded slice together before the
2562
+ // queued snapshot callback's setImmediate flush runs. Promise-based lstat
2563
+ // otherwise spaces successive rows across I/O turns and splits one logical
2564
+ // warm-up batch into a sequence of one-row captures.
2565
+ if (this.pendingDeleteSnapshotCaptureBatch.length > 0) {
2566
+ this.scheduleDeleteSnapshotCaptureBatch();
2567
+ }
2452
2568
  return { batch: { paths, changes }, scannedPaths };
2453
2569
  }
2454
2570
  /** True while the watch backend is active. */