@indigoai-us/hq-cloud 6.11.11 → 6.11.12

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 (160) hide show
  1. package/dist/bin/sync-runner.d.ts +2 -0
  2. package/dist/bin/sync-runner.d.ts.map +1 -1
  3. package/dist/bin/sync-runner.js +231 -52
  4. package/dist/bin/sync-runner.js.map +1 -1
  5. package/dist/bin/sync-runner.test.js +265 -11
  6. package/dist/bin/sync-runner.test.js.map +1 -1
  7. package/dist/cli/rescue-classify-ordering.test.js +58 -0
  8. package/dist/cli/rescue-classify-ordering.test.js.map +1 -1
  9. package/dist/cli/rescue-core.js +138 -15
  10. package/dist/cli/rescue-core.js.map +1 -1
  11. package/dist/cli/share.d.ts +2 -1
  12. package/dist/cli/share.d.ts.map +1 -1
  13. package/dist/cli/share.js +100 -32
  14. package/dist/cli/share.js.map +1 -1
  15. package/dist/cli/share.test.js +30 -0
  16. package/dist/cli/share.test.js.map +1 -1
  17. package/dist/cli/sync.d.ts +28 -1
  18. package/dist/cli/sync.d.ts.map +1 -1
  19. package/dist/cli/sync.js +178 -58
  20. package/dist/cli/sync.js.map +1 -1
  21. package/dist/cli/sync.test.js +362 -1
  22. package/dist/cli/sync.test.js.map +1 -1
  23. package/dist/cognito-auth.d.ts.map +1 -1
  24. package/dist/cognito-auth.js +55 -10
  25. package/dist/cognito-auth.js.map +1 -1
  26. package/dist/cognito-auth.test.js +61 -0
  27. package/dist/cognito-auth.test.js.map +1 -1
  28. package/dist/index.d.ts +2 -1
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +1 -1
  31. package/dist/index.js.map +1 -1
  32. package/dist/journal.d.ts.map +1 -1
  33. package/dist/journal.js +93 -6
  34. package/dist/journal.js.map +1 -1
  35. package/dist/journal.test.js +59 -0
  36. package/dist/journal.test.js.map +1 -1
  37. package/dist/machine-auth.test.js +60 -2
  38. package/dist/machine-auth.test.js.map +1 -1
  39. package/dist/object-io.d.ts +37 -1
  40. package/dist/object-io.d.ts.map +1 -1
  41. package/dist/object-io.js +148 -29
  42. package/dist/object-io.js.map +1 -1
  43. package/dist/object-io.test.js +121 -0
  44. package/dist/object-io.test.js.map +1 -1
  45. package/dist/operation-lock.d.ts +8 -8
  46. package/dist/operation-lock.d.ts.map +1 -1
  47. package/dist/operation-lock.js +99 -32
  48. package/dist/operation-lock.js.map +1 -1
  49. package/dist/operation-lock.test.js +51 -4
  50. package/dist/operation-lock.test.js.map +1 -1
  51. package/dist/personal-vault.d.ts.map +1 -1
  52. package/dist/personal-vault.js +8 -2
  53. package/dist/personal-vault.js.map +1 -1
  54. package/dist/personal-vault.test.js +34 -0
  55. package/dist/personal-vault.test.js.map +1 -1
  56. package/dist/prefix-coalesce.d.ts +20 -9
  57. package/dist/prefix-coalesce.d.ts.map +1 -1
  58. package/dist/prefix-coalesce.js +124 -28
  59. package/dist/prefix-coalesce.js.map +1 -1
  60. package/dist/prefix-coalesce.test.js +57 -2
  61. package/dist/prefix-coalesce.test.js.map +1 -1
  62. package/dist/remote-pull.d.ts +6 -1
  63. package/dist/remote-pull.d.ts.map +1 -1
  64. package/dist/remote-pull.js +62 -13
  65. package/dist/remote-pull.js.map +1 -1
  66. package/dist/remote-pull.test.js +189 -0
  67. package/dist/remote-pull.test.js.map +1 -1
  68. package/dist/s3.d.ts +2 -0
  69. package/dist/s3.d.ts.map +1 -1
  70. package/dist/s3.js +197 -116
  71. package/dist/s3.js.map +1 -1
  72. package/dist/s3.test.js +109 -0
  73. package/dist/s3.test.js.map +1 -1
  74. package/dist/scope-shrink.d.ts +3 -2
  75. package/dist/scope-shrink.d.ts.map +1 -1
  76. package/dist/scope-shrink.js +1 -1
  77. package/dist/scope-shrink.js.map +1 -1
  78. package/dist/skill-telemetry.d.ts +1 -1
  79. package/dist/skill-telemetry.d.ts.map +1 -1
  80. package/dist/skill-telemetry.js +69 -9
  81. package/dist/skill-telemetry.js.map +1 -1
  82. package/dist/skill-telemetry.test.js +86 -0
  83. package/dist/skill-telemetry.test.js.map +1 -1
  84. package/dist/sync/event-sync.d.ts +6 -0
  85. package/dist/sync/event-sync.d.ts.map +1 -1
  86. package/dist/sync/event-sync.js +34 -1
  87. package/dist/sync/event-sync.js.map +1 -1
  88. package/dist/sync/event-sync.test.js +73 -0
  89. package/dist/sync/event-sync.test.js.map +1 -1
  90. package/dist/sync/metrics.d.ts +17 -1
  91. package/dist/sync/metrics.d.ts.map +1 -1
  92. package/dist/sync/metrics.js +32 -1
  93. package/dist/sync/metrics.js.map +1 -1
  94. package/dist/sync/metrics.test.js +74 -1
  95. package/dist/sync/metrics.test.js.map +1 -1
  96. package/dist/sync/pull-scope.d.ts.map +1 -1
  97. package/dist/sync/pull-scope.js +15 -7
  98. package/dist/sync/pull-scope.js.map +1 -1
  99. package/dist/sync/push-receiver.d.ts +6 -5
  100. package/dist/sync/push-receiver.d.ts.map +1 -1
  101. package/dist/sync/push-receiver.js +13 -15
  102. package/dist/sync/push-receiver.js.map +1 -1
  103. package/dist/sync/push-receiver.test.js +36 -1
  104. package/dist/sync/push-receiver.test.js.map +1 -1
  105. package/dist/telemetry.d.ts +1 -1
  106. package/dist/telemetry.d.ts.map +1 -1
  107. package/dist/telemetry.js +59 -6
  108. package/dist/telemetry.js.map +1 -1
  109. package/dist/telemetry.test.js +74 -0
  110. package/dist/telemetry.test.js.map +1 -1
  111. package/dist/types.d.ts +8 -0
  112. package/dist/types.d.ts.map +1 -1
  113. package/dist/watcher.d.ts +36 -0
  114. package/dist/watcher.d.ts.map +1 -1
  115. package/dist/watcher.js +152 -30
  116. package/dist/watcher.js.map +1 -1
  117. package/dist/watcher.test.js +103 -0
  118. package/dist/watcher.test.js.map +1 -1
  119. package/package.json +1 -1
  120. package/src/bin/sync-runner.test.ts +298 -11
  121. package/src/bin/sync-runner.ts +254 -52
  122. package/src/cli/rescue-classify-ordering.test.ts +61 -0
  123. package/src/cli/rescue-core.ts +174 -15
  124. package/src/cli/share.test.ts +38 -0
  125. package/src/cli/share.ts +103 -34
  126. package/src/cli/sync.test.ts +435 -1
  127. package/src/cli/sync.ts +217 -64
  128. package/src/cognito-auth.test.ts +77 -0
  129. package/src/cognito-auth.ts +73 -11
  130. package/src/index.ts +8 -0
  131. package/src/journal.test.ts +72 -0
  132. package/src/journal.ts +95 -8
  133. package/src/machine-auth.test.ts +64 -2
  134. package/src/object-io.test.ts +142 -0
  135. package/src/object-io.ts +182 -30
  136. package/src/operation-lock.test.ts +63 -4
  137. package/src/operation-lock.ts +99 -31
  138. package/src/personal-vault.test.ts +42 -0
  139. package/src/personal-vault.ts +8 -2
  140. package/src/prefix-coalesce.test.ts +71 -1
  141. package/src/prefix-coalesce.ts +155 -30
  142. package/src/remote-pull.test.ts +205 -0
  143. package/src/remote-pull.ts +77 -14
  144. package/src/s3.test.ts +126 -0
  145. package/src/s3.ts +237 -122
  146. package/src/scope-shrink.ts +6 -3
  147. package/src/skill-telemetry.test.ts +109 -0
  148. package/src/skill-telemetry.ts +82 -14
  149. package/src/sync/event-sync.test.ts +75 -0
  150. package/src/sync/event-sync.ts +54 -1
  151. package/src/sync/metrics.test.ts +81 -0
  152. package/src/sync/metrics.ts +59 -4
  153. package/src/sync/pull-scope.ts +23 -7
  154. package/src/sync/push-receiver.test.ts +38 -1
  155. package/src/sync/push-receiver.ts +15 -18
  156. package/src/telemetry.test.ts +85 -0
  157. package/src/telemetry.ts +69 -6
  158. package/src/types.ts +8 -0
  159. package/src/watcher.test.ts +117 -0
  160. package/src/watcher.ts +209 -33
package/src/watcher.ts CHANGED
@@ -163,7 +163,9 @@ export class WatchPushDriver {
163
163
  }
164
164
  this.timer = this.clock.setTimeout(() => {
165
165
  this.timer = null;
166
- void this.fire();
166
+ void this.fire().catch((err) => {
167
+ console.error("WatchPushDriver push failed:", err);
168
+ });
167
169
  }, this.debounceMs);
168
170
  }
169
171
 
@@ -400,6 +402,36 @@ function supportsRecursiveWatch(): boolean {
400
402
  return process.platform === "darwin" || process.platform === "win32";
401
403
  }
402
404
 
405
+ function startChokidarTreeWatch(
406
+ hqRoot: string,
407
+ shouldEmit: WatchPathFilter,
408
+ onEvent: (absolutePath: string) => void,
409
+ onError: (err: unknown) => void,
410
+ ): WatchBackend {
411
+ const cw = watch(hqRoot, {
412
+ // chokidar fallback (Linux): see toChokidarIgnored for why the descent
413
+ // probe must not prune ancestor dirs of allowlisted leaves.
414
+ ignored: toChokidarIgnored(shouldEmit, hqRoot),
415
+ persistent: true,
416
+ ignoreInitial: true,
417
+ awaitWriteFinish: {
418
+ stabilityThreshold: 500,
419
+ pollInterval: 100,
420
+ },
421
+ });
422
+ cw.on("add", onEvent)
423
+ .on("change", onEvent)
424
+ .on("unlink", onEvent)
425
+ .on("addDir", onEvent)
426
+ .on("unlinkDir", onEvent)
427
+ .on("error", onError);
428
+ return {
429
+ close: () => {
430
+ void cw.close();
431
+ },
432
+ };
433
+ }
434
+
403
435
  /**
404
436
  * Start watching `hqRoot`, calling `onEvent(absolutePath)` for every change.
405
437
  *
@@ -441,8 +473,39 @@ function startTreeWatch(
441
473
  onEvent(path.resolve(hqRoot, rel));
442
474
  },
443
475
  );
444
- native.on("error", onError);
445
- return { close: () => native.close() };
476
+ let closed = false;
477
+ let fallback: WatchBackend | null = null;
478
+ native.on("error", (err) => {
479
+ onError(err);
480
+ if (closed || fallback !== null) return;
481
+ try {
482
+ native.close();
483
+ } catch {
484
+ /* already closed */
485
+ }
486
+ try {
487
+ fallback = startChokidarTreeWatch(
488
+ hqRoot,
489
+ shouldEmit,
490
+ onEvent,
491
+ onError,
492
+ );
493
+ } catch (fallbackErr) {
494
+ onError(fallbackErr);
495
+ }
496
+ });
497
+ return {
498
+ close: () => {
499
+ closed = true;
500
+ try {
501
+ native.close();
502
+ } catch {
503
+ /* already closed */
504
+ }
505
+ fallback?.close();
506
+ fallback = null;
507
+ },
508
+ };
446
509
  } catch (err) {
447
510
  // Recursive watch unexpectedly unavailable — fall back to chokidar
448
511
  // rather than leaving the daemon with no watcher at all.
@@ -450,28 +513,7 @@ function startTreeWatch(
450
513
  }
451
514
  }
452
515
 
453
- const cw = watch(hqRoot, {
454
- // chokidar fallback (Linux): see toChokidarIgnored for why the descent
455
- // probe must not prune ancestor dirs of allowlisted leaves.
456
- ignored: toChokidarIgnored(shouldEmit, hqRoot),
457
- persistent: true,
458
- ignoreInitial: true,
459
- awaitWriteFinish: {
460
- stabilityThreshold: 500,
461
- pollInterval: 100,
462
- },
463
- });
464
- cw.on("add", onEvent)
465
- .on("change", onEvent)
466
- .on("unlink", onEvent)
467
- .on("addDir", onEvent)
468
- .on("unlinkDir", onEvent)
469
- .on("error", onError);
470
- return {
471
- close: () => {
472
- void cw.close();
473
- },
474
- };
516
+ return startChokidarTreeWatch(hqRoot, shouldEmit, onEvent, onError);
475
517
  }
476
518
 
477
519
  /**
@@ -533,6 +575,25 @@ export function createWatchPathFilter(
533
575
  };
534
576
  }
535
577
 
578
+ export const DEFAULT_TREE_WATCHER_MAX_PENDING_PATHS = 4096;
579
+ export const DEFAULT_TREE_WATCHER_MAX_PENDING_BYTES = 1024 * 1024;
580
+
581
+ export interface TreeWatcherBacklogOverflow {
582
+ pendingPaths: number;
583
+ pendingBytes: number;
584
+ maxPendingPaths: number;
585
+ maxPendingBytes: number;
586
+ droppedPaths: number;
587
+ droppedBytes: number;
588
+ }
589
+
590
+ function estimatePendingEntryBytes(
591
+ absolutePath: string,
592
+ relativePath: string,
593
+ ): number {
594
+ return Buffer.byteLength(absolutePath) + Buffer.byteLength(relativePath) + 64;
595
+ }
596
+
536
597
  export interface TreeWatcherOptions {
537
598
  /** Sync root to watch (== personal-vault root in personalMode). */
538
599
  hqRoot: string;
@@ -547,6 +608,12 @@ export interface TreeWatcherOptions {
547
608
  * from {@link createWatchPathFilter}.
548
609
  */
549
610
  pathFilter?: WatchPathFilter;
611
+ /** Maximum distinct paths retained in one debounce window. */
612
+ maxPendingPaths?: number;
613
+ /** Approximate maximum path-string bytes retained in one debounce window. */
614
+ maxPendingBytes?: number;
615
+ /** Backlog overflow signal. Defaults to a console warning. */
616
+ onBacklogOverflow?: (info: TreeWatcherBacklogOverflow) => void;
550
617
  }
551
618
 
552
619
  /**
@@ -569,6 +636,12 @@ export interface TreeWatcherOptions {
569
636
  export interface TreeChangeBatch {
570
637
  /** Map of absolutePath → relativePath for every path in the settled burst. */
571
638
  paths: Map<string, string>;
639
+ /** True when path detail was dropped after the watcher backlog cap was hit. */
640
+ overflowed?: boolean;
641
+ /** Count of paths dropped from the detailed batch after overflow. */
642
+ droppedPaths?: number;
643
+ /** Approximate path-string bytes dropped from the detailed batch. */
644
+ droppedBytes?: number;
572
645
  }
573
646
 
574
647
  /**
@@ -591,11 +664,19 @@ export class TreeWatcher {
591
664
  private readonly debounceMs: number;
592
665
  private readonly clock: Clock;
593
666
  private readonly shouldEmit: WatchPathFilter;
667
+ private readonly maxPendingPaths: number;
668
+ private readonly maxPendingBytes: number;
669
+ private readonly onBacklogOverflow: (info: TreeWatcherBacklogOverflow) => void;
594
670
  private backend: WatchBackend | null = null;
595
671
  private timer: unknown = null;
596
672
  private listeners = new Set<TreeChangeListener>();
597
673
  /** Paths accumulated for the current (in-flight) debounce window. */
598
674
  private pending = new Map<string, string>();
675
+ private pendingBytes = 0;
676
+ private overflowed = false;
677
+ private overflowLogged = false;
678
+ private droppedPaths = 0;
679
+ private droppedBytes = 0;
599
680
  private disposed = false;
600
681
 
601
682
  constructor(opts: TreeWatcherOptions) {
@@ -604,6 +685,21 @@ export class TreeWatcher {
604
685
  this.clock = opts.clock ?? systemClock;
605
686
  this.shouldEmit =
606
687
  opts.pathFilter ?? createWatchPathFilter(opts.hqRoot, opts.personalMode ?? false);
688
+ this.maxPendingPaths = Math.max(
689
+ 1,
690
+ opts.maxPendingPaths ?? DEFAULT_TREE_WATCHER_MAX_PENDING_PATHS,
691
+ );
692
+ this.maxPendingBytes = Math.max(
693
+ 1,
694
+ opts.maxPendingBytes ?? DEFAULT_TREE_WATCHER_MAX_PENDING_BYTES,
695
+ );
696
+ this.onBacklogOverflow =
697
+ opts.onBacklogOverflow ??
698
+ ((info) => {
699
+ console.warn(
700
+ `TreeWatcher backlog cap exceeded; dropping ${info.droppedPaths} path(s) until the next resync`,
701
+ );
702
+ });
607
703
  }
608
704
 
609
705
  /**
@@ -633,10 +729,19 @@ export class TreeWatcher {
633
729
  this.hqRoot,
634
730
  this.shouldEmit,
635
731
  (absolutePath) => this.handleEvent(absolutePath),
636
- (err) => console.error("TreeWatcher error:", err),
732
+ (err) => {
733
+ console.error("TreeWatcher error:", err);
734
+ this.signalBackendResync();
735
+ },
637
736
  );
638
737
  }
639
738
 
739
+ private signalBackendResync(): void {
740
+ if (this.disposed) return;
741
+ this.overflowed = true;
742
+ this.arm();
743
+ }
744
+
640
745
  /**
641
746
  * Test/seam entry point: feed a raw filesystem path as if the backend
642
747
  * reported it. Applies the emit filter then arms the debounce. Real watch
@@ -651,10 +756,42 @@ export class TreeWatcher {
651
756
  if (!this.shouldEmit(absolutePath, false)) return;
652
757
  const abs = path.resolve(absolutePath);
653
758
  const rel = path.relative(this.hqRoot, abs).split(path.sep).join("/");
759
+ if (!this.pending.has(abs)) {
760
+ const entryBytes = estimatePendingEntryBytes(abs, rel);
761
+ if (
762
+ this.pending.size >= this.maxPendingPaths ||
763
+ this.pendingBytes + entryBytes > this.maxPendingBytes
764
+ ) {
765
+ this.recordBacklogOverflow(entryBytes);
766
+ this.arm();
767
+ return;
768
+ }
769
+ this.pendingBytes += entryBytes;
770
+ }
654
771
  this.pending.set(abs, rel);
655
772
  this.arm();
656
773
  }
657
774
 
775
+ private recordBacklogOverflow(entryBytes: number): void {
776
+ this.overflowed = true;
777
+ this.droppedPaths += 1;
778
+ this.droppedBytes += entryBytes;
779
+ if (this.overflowLogged) return;
780
+ this.overflowLogged = true;
781
+ try {
782
+ this.onBacklogOverflow({
783
+ pendingPaths: this.pending.size,
784
+ pendingBytes: this.pendingBytes,
785
+ maxPendingPaths: this.maxPendingPaths,
786
+ maxPendingBytes: this.maxPendingBytes,
787
+ droppedPaths: this.droppedPaths,
788
+ droppedBytes: this.droppedBytes,
789
+ });
790
+ } catch (err) {
791
+ console.error("TreeWatcher backlog overflow logger error:", err);
792
+ }
793
+ }
794
+
658
795
  private arm(): void {
659
796
  if (this.timer !== null) {
660
797
  this.clock.clearTimeout(this.timer);
@@ -671,7 +808,12 @@ export class TreeWatcher {
671
808
  // Snapshot + clear the accumulated paths so the next window starts fresh
672
809
  // even if a listener re-enters synchronously.
673
810
  const batch: TreeChangeBatch = { paths: new Map(this.pending) };
674
- this.pending.clear();
811
+ if (this.overflowed) {
812
+ batch.overflowed = true;
813
+ batch.droppedPaths = this.droppedPaths;
814
+ batch.droppedBytes = this.droppedBytes;
815
+ }
816
+ this.clearPending();
675
817
  // First changed relative path of the burst — the US-003 routing argument.
676
818
  // undefined when the window settled with no captured path (e.g. a synthetic
677
819
  // arm() with no handleEvent).
@@ -685,6 +827,15 @@ export class TreeWatcher {
685
827
  }
686
828
  }
687
829
 
830
+ private clearPending(): void {
831
+ this.pending.clear();
832
+ this.pendingBytes = 0;
833
+ this.overflowed = false;
834
+ this.overflowLogged = false;
835
+ this.droppedPaths = 0;
836
+ this.droppedBytes = 0;
837
+ }
838
+
688
839
  /** True while the watch backend is active. */
689
840
  isWatching(): boolean {
690
841
  return this.backend !== null;
@@ -705,7 +856,7 @@ export class TreeWatcher {
705
856
  this.clock.clearTimeout(this.timer);
706
857
  this.timer = null;
707
858
  }
708
- this.pending.clear();
859
+ this.clearPending();
709
860
  if (this.backend) {
710
861
  this.backend.close();
711
862
  this.backend = null;
@@ -792,6 +943,7 @@ export interface EmitterLogger {
792
943
  * running TreeWatcher (returns an unsubscribe fn). Flag-gated + failure-safe.
793
944
  */
794
945
  export class PushEventEmitter {
946
+ private static readonly MAX_CONCURRENT_PUBLISHES = 16;
795
947
  private readonly originTenantId: string;
796
948
  private readonly originDeviceId: string;
797
949
  private readonly transport: PushTransport;
@@ -801,6 +953,7 @@ export class PushEventEmitter {
801
953
  private readonly logger: EmitterLogger | undefined;
802
954
  private internalSeq = 0;
803
955
  private readonly nextSeq: () => number;
956
+ private publishTail: Promise<void> = Promise.resolve();
804
957
 
805
958
  constructor(opts: PushEventEmitterOptions) {
806
959
  this.originTenantId = opts.originTenantId;
@@ -848,12 +1001,35 @@ export class PushEventEmitter {
848
1001
  */
849
1002
  async emitForBatch(batch: TreeChangeBatch): Promise<void> {
850
1003
  if (!this.enabled) return;
851
- const entries = [...batch.paths.entries()];
852
- await Promise.all(
853
- entries.map(([absolutePath, relativePath]) =>
854
- this.emitOne(absolutePath, relativePath),
855
- ),
1004
+ const entriesByRel = new Map<string, [string, string]>();
1005
+ for (const [absolutePath, relativePath] of batch.paths.entries()) {
1006
+ entriesByRel.set(relativePath, [absolutePath, relativePath]);
1007
+ }
1008
+ const entries = [...entriesByRel.values()];
1009
+ const run = this.publishTail.then(
1010
+ () => this.emitEntries(entries),
1011
+ () => this.emitEntries(entries),
856
1012
  );
1013
+ this.publishTail = run.catch(() => undefined);
1014
+ await run;
1015
+ }
1016
+
1017
+ private async emitEntries(entries: Array<[string, string]>): Promise<void> {
1018
+ for (
1019
+ let i = 0;
1020
+ i < entries.length;
1021
+ i += PushEventEmitter.MAX_CONCURRENT_PUBLISHES
1022
+ ) {
1023
+ const chunk = entries.slice(
1024
+ i,
1025
+ i + PushEventEmitter.MAX_CONCURRENT_PUBLISHES,
1026
+ );
1027
+ await Promise.all(
1028
+ chunk.map(([absolutePath, relativePath]) =>
1029
+ this.emitOne(absolutePath, relativePath),
1030
+ ),
1031
+ );
1032
+ }
857
1033
  }
858
1034
 
859
1035
  private async emitOne(