@indigoai-us/hq-cloud 6.18.91 → 6.18.93

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 (54) hide show
  1. package/dist/bin/sync-runner-company.d.ts +6 -0
  2. package/dist/bin/sync-runner-company.d.ts.map +1 -1
  3. package/dist/bin/sync-runner-company.js +126 -4
  4. package/dist/bin/sync-runner-company.js.map +1 -1
  5. package/dist/bin/sync-runner-telemetry.d.ts +9 -3
  6. package/dist/bin/sync-runner-telemetry.d.ts.map +1 -1
  7. package/dist/bin/sync-runner-telemetry.js +7 -1
  8. package/dist/bin/sync-runner-telemetry.js.map +1 -1
  9. package/dist/bin/sync-runner-watch-loop.d.ts.map +1 -1
  10. package/dist/bin/sync-runner-watch-loop.js +177 -7
  11. package/dist/bin/sync-runner-watch-loop.js.map +1 -1
  12. package/dist/bin/sync-runner.d.ts +18 -3
  13. package/dist/bin/sync-runner.d.ts.map +1 -1
  14. package/dist/bin/sync-runner.js +103 -34
  15. package/dist/bin/sync-runner.js.map +1 -1
  16. package/dist/cli/reindex.d.ts.map +1 -1
  17. package/dist/cli/reindex.js +26 -0
  18. package/dist/cli/reindex.js.map +1 -1
  19. package/dist/cli/sync.d.ts +17 -0
  20. package/dist/cli/sync.d.ts.map +1 -1
  21. package/dist/cli/sync.js +33 -1
  22. package/dist/cli/sync.js.map +1 -1
  23. package/dist/ignore.d.ts +16 -1
  24. package/dist/ignore.d.ts.map +1 -1
  25. package/dist/ignore.js +59 -1
  26. package/dist/ignore.js.map +1 -1
  27. package/dist/lib/machine-id.d.ts.map +1 -1
  28. package/dist/lib/machine-id.js +16 -0
  29. package/dist/lib/machine-id.js.map +1 -1
  30. package/dist/personal-vault.d.ts +22 -5
  31. package/dist/personal-vault.d.ts.map +1 -1
  32. package/dist/personal-vault.js +60 -8
  33. package/dist/personal-vault.js.map +1 -1
  34. package/dist/skill-telemetry.d.ts +5 -0
  35. package/dist/skill-telemetry.d.ts.map +1 -1
  36. package/dist/skill-telemetry.js +20 -1
  37. package/dist/skill-telemetry.js.map +1 -1
  38. package/dist/sync/state-store.d.ts +20 -1
  39. package/dist/sync/state-store.d.ts.map +1 -1
  40. package/dist/sync/state-store.js +36 -5
  41. package/dist/sync/state-store.js.map +1 -1
  42. package/dist/telemetry-admission.d.ts +106 -0
  43. package/dist/telemetry-admission.d.ts.map +1 -0
  44. package/dist/telemetry-admission.js +142 -0
  45. package/dist/telemetry-admission.js.map +1 -0
  46. package/dist/telemetry-scan-cap.d.ts +10 -0
  47. package/dist/telemetry-scan-cap.d.ts.map +1 -0
  48. package/dist/telemetry-scan-cap.js +23 -0
  49. package/dist/telemetry-scan-cap.js.map +1 -0
  50. package/dist/telemetry.d.ts +6 -0
  51. package/dist/telemetry.d.ts.map +1 -1
  52. package/dist/telemetry.js +14 -1
  53. package/dist/telemetry.js.map +1 -1
  54. package/package.json +1 -1
@@ -3757,6 +3757,38 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
3757
3757
  }
3758
3758
  }
3759
3759
  const routePushFailureBackoff = new RoutePushFailureBackoff();
3760
+ /**
3761
+ * Same 5 s doubling to 5 min schedule, for a route's scoped (watcher) pull.
3762
+ * Kept apart from the push state so a failing pull never holds an upload
3763
+ * and a failing push never resets the pull's step. Retry timers use the
3764
+ * `pull:<route>` key in the shared scheduler.
3765
+ */
3766
+ const routePullFailureBackoff = new RoutePushFailureBackoff();
3767
+ /**
3768
+ * Routes with a scoped pull running now. Coordinator watcher lanes and the
3769
+ * pending-batch drain can run concurrently; without this, both could pass
3770
+ * the backoff check before either pull failed and run the same route's pull
3771
+ * twice.
3772
+ */
3773
+ const scopedPullsInFlight = new Set();
3774
+ /**
3775
+ * Pending watcher paths whose push already succeeded and that wait only
3776
+ * for their route's scoped pull (backing off, running, or deferred by
3777
+ * admission). Later drains skip the push for them, so a route whose pull
3778
+ * keeps failing does not re-run a no-op push pass on every wake. A fresh
3779
+ * watcher event for a path removes it, so a new edit is still pushed.
3780
+ */
3781
+ const pullOnlyWatcherPaths = new Set();
3782
+ /**
3783
+ * One set per running scoped drain, holding paths that got a fresh watcher
3784
+ * event while that drain ran. Each set lives only as long as its drain.
3785
+ */
3786
+ const activeDrainFreshPaths = new Set();
3787
+ const noteFreshWatcherEvent = (absolutePath) => {
3788
+ pullOnlyWatcherPaths.delete(absolutePath);
3789
+ for (const freshPaths of activeDrainFreshPaths)
3790
+ freshPaths.add(absolutePath);
3791
+ };
3760
3792
  const pushRetryScheduler = deps.pushRetryScheduler ?? systemPushRetryScheduler;
3761
3793
  const routePushRetryTimerHandles = new Map();
3762
3794
  const clearRoutePushRetry = (route) => {
@@ -4612,7 +4644,7 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
4612
4644
  return false;
4613
4645
  };
4614
4646
  let emitScopedPushFailureTelemetry = null;
4615
- const runScopedDrain = async (batch, watcherBatchGeneration, _wakeOnPushFailure = false, allowFastRealtimeOvertake = false) => {
4647
+ const runScopedDrainOnce = async (freshDuringThisDrain, batch, watcherBatchGeneration, _wakeOnPushFailure = false, allowFastRealtimeOvertake = false) => {
4616
4648
  if (batch) {
4617
4649
  // Read each routed quarantine shard once for this watcher batch. Filter
4618
4650
  // before resolving delete snapshots or submitting work to the coordinator
@@ -4694,6 +4726,20 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
4694
4726
  emitScopedPushFailureTelemetry?.(event);
4695
4727
  }
4696
4728
  };
4729
+ /**
4730
+ * Routes whose push did not run in this drain because the route's failure
4731
+ * backoff held it. Their paths are back in the pending batch with a retry
4732
+ * scheduled, so the paired scoped pull for the
4733
+ * same paths waits for that retry. Without this, every wake of the loop
4734
+ * (a receiver pull for any company, a retry timer, another watcher batch)
4735
+ * re-took the restored paths, skipped the held push and still ran the
4736
+ * pull leg. A route with a deterministic error then produced one errored
4737
+ * runner session and `company_sync_leg_completed` per wake, all day,
4738
+ * however long the push backoff was. A push that runs and fails still
4739
+ * gets its pull in the same drain, as before, so a pull that would clear
4740
+ * the failure is not starved: at most one pull per push attempt.
4741
+ */
4742
+ const routesAwaitingPushRetry = new Set();
4697
4743
  /**
4698
4744
  * The push never ran: a chore held the slot past the admission deadline.
4699
4745
  * Put the paths back and retry without counting a failure, so the route
@@ -4704,8 +4750,51 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
4704
4750
  restorePendingWatcherPaths(group.paths, watcherBatchGeneration, group.changes);
4705
4751
  scheduleRoutePushRetry(routeId, REALTIME_ADMISSION_RETRY_MS);
4706
4752
  };
4753
+ /** Paths whose push succeeded (or needed nothing) in this drain. */
4754
+ const pushSettledThisDrain = new Set();
4755
+ /**
4756
+ * Keep a group's paths pending for its pull only. A path qualifies when
4757
+ * its push is already settled and no fresh watcher event arrived for it
4758
+ * since this drain started.
4759
+ */
4760
+ const keepForPullOnly = (group) => {
4761
+ for (const absolutePath of group.paths.keys()) {
4762
+ if (freshDuringThisDrain.has(absolutePath))
4763
+ continue;
4764
+ if (!watcherDrainRunsPush ||
4765
+ pushSettledThisDrain.has(absolutePath) ||
4766
+ pullOnlyWatcherPaths.has(absolutePath)) {
4767
+ pullOnlyWatcherPaths.add(absolutePath);
4768
+ }
4769
+ }
4770
+ restorePendingWatcherPaths(group.paths, watcherBatchGeneration, group.changes);
4771
+ };
4772
+ /** The part of a group that still needs a push, or null when none does. */
4773
+ const withoutPullOnlyPaths = (group) => {
4774
+ const pushPaths = [...group.paths.entries()].filter(([absolutePath]) => !pullOnlyWatcherPaths.has(absolutePath));
4775
+ if (pushPaths.length === group.paths.size)
4776
+ return group;
4777
+ if (pushPaths.length === 0)
4778
+ return null;
4779
+ const paths = new Map(pushPaths);
4780
+ const changes = new Map([...group.changes.entries()].filter(([absolutePath]) => paths.has(absolutePath)));
4781
+ return {
4782
+ route: group.route,
4783
+ paths,
4784
+ changes,
4785
+ relPaths: [...new Set(paths.values())],
4786
+ deleteScopeRoots: [
4787
+ ...new Set([...changes.values()].flatMap((item) => (item.deleteSnapshots ?? []).flatMap((snapshot) => snapshot.deleteScopeRoot === undefined ? [] : [snapshot.deleteScopeRoot]))),
4788
+ ],
4789
+ };
4790
+ };
4707
4791
  if (watcherDrainRunsPush && grouped) {
4708
- for (const group of grouped.values()) {
4792
+ for (const routeGroup of grouped.values()) {
4793
+ // Paths already pushed that wait only for their pull skip the push;
4794
+ // the pull half below still covers them.
4795
+ const group = withoutPullOnlyPaths(routeGroup);
4796
+ if (group === null)
4797
+ continue;
4709
4798
  // Paused workspaces: drop the scoped push without uploading or
4710
4799
  // publishing event-sync metadata (path/hash must not leak while Off).
4711
4800
  if (isPausedRoute(group.route)) {
@@ -4716,6 +4805,7 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
4716
4805
  if (retryState && retryState.retryAt > monotonicNow()) {
4717
4806
  scheduleRoutePushRetry(routeId, retryState.retryAt - monotonicNow());
4718
4807
  restorePendingWatcherPaths(group.paths, watcherBatchGeneration, group.changes);
4808
+ routesAwaitingPushRetry.add(routeId);
4719
4809
  continue;
4720
4810
  }
4721
4811
  clearRoutePushRetry(routeId);
@@ -4824,6 +4914,8 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
4824
4914
  if (execution.accepted && pushExit === 0) {
4825
4915
  routePushFailureBackoff.succeeded(routeId);
4826
4916
  clearRoutePushRetry(routeId);
4917
+ for (const absolutePath of group.paths.keys())
4918
+ pushSettledThisDrain.add(absolutePath);
4827
4919
  const pushPathResults = typeof result === "number" ? [] : (result.result?.pushPathResults ?? []);
4828
4920
  const refusedPaths = new Set(pushPathResults
4829
4921
  .filter((pathResult) => pathResult.status === "refused")
@@ -4883,6 +4975,8 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
4883
4975
  }
4884
4976
  else if (pushExit === UNRESOLVED_COMPANY_EXIT) {
4885
4977
  acknowledgeDurableWatcherUploads(group.relPaths);
4978
+ for (const absolutePath of group.paths.keys())
4979
+ pushSettledThisDrain.add(absolutePath);
4886
4980
  routePushFailureBackoff.succeeded(routeId);
4887
4981
  clearRoutePushRetry(routeId);
4888
4982
  if (group.route.kind === "company") {
@@ -4928,6 +5022,39 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
4928
5022
  // Paused workspaces sync in NO direction while Off.
4929
5023
  if (isPausedRoute(group.route))
4930
5024
  continue;
5025
+ // The push for these paths is pending a scheduled retry; the pull
5026
+ // runs with it then. Report the drain as deferred, not failed, so
5027
+ // callers neither re-queue the paths again nor wake the loop early.
5028
+ if (routesAwaitingPushRetry.has(routeKey(group.route))) {
5029
+ // The push half restored the paths it held. Paths that were
5030
+ // waiting only for this pull were not part of that push; keep them.
5031
+ keepForPullOnly(group);
5032
+ deferred = true;
5033
+ continue;
5034
+ }
5035
+ // A failed scoped pull backs off per route on the same schedule as
5036
+ // the push. Until its retry is due, keep the paths pending and skip
5037
+ // the pull. Before this, a failed pull made the callers restore the
5038
+ // paths and wake the loop at once, so a route whose pull always
5039
+ // failed re-ran it every few seconds for as long as the machine was
5040
+ // awake (usage-only-pricing US-001).
5041
+ const pullRouteId = routeKey(group.route);
5042
+ const pullRetryKey = `pull:${pullRouteId}`;
5043
+ const pullRetryState = routePullFailureBackoff.state(pullRouteId);
5044
+ if (pullRetryState && pullRetryState.retryAt > monotonicNow()) {
5045
+ keepForPullOnly(group);
5046
+ scheduleRoutePushRetry(pullRetryKey, pullRetryState.retryAt - monotonicNow());
5047
+ deferred = true;
5048
+ continue;
5049
+ }
5050
+ if (scopedPullsInFlight.has(pullRouteId)) {
5051
+ // The running pull decides the backoff. Keep these paths and look
5052
+ // again after the admission retry interval.
5053
+ keepForPullOnly(group);
5054
+ scheduleRoutePushRetry(pullRetryKey, REALTIME_ADMISSION_RETRY_MS);
5055
+ deferred = true;
5056
+ continue;
5057
+ }
4931
5058
  const exactFileUpserts = [...group.paths.entries()].flatMap(([absolutePath, relativePath]) => {
4932
5059
  const kind = group.changes.get(absolutePath)?.kind;
4933
5060
  // Only create/change events are file upserts. Keep unlinks on the
@@ -4948,26 +5075,50 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
4948
5075
  const lane = isFastLocalBatch(group.paths.keys(), group.changes)
4949
5076
  ? "fast"
4950
5077
  : "slow";
4951
- const execution = parallelCoordinatorEnabled
4952
- ? await runRealtimePass("watcher", "pull", group.relPaths, lane, (slice, telemetry, coordinatorFenced) => {
5078
+ scopedPullsInFlight.add(pullRouteId);
5079
+ const execution = await (parallelCoordinatorEnabled
5080
+ ? runRealtimePass("watcher", "pull", group.relPaths, lane, (slice, telemetry, coordinatorFenced) => {
4953
5081
  const slicePaths = slice.paths.map((path) => path.canonicalPath);
4954
5082
  return runGuarded(buildTargetedPullArgv(group.route, passArgv, slicePaths, exactFileUpserts.filter((relativePath) => slicePaths.includes(relativePath))), undefined, lane, "realtime", false, slice.signal, false, telemetry, coordinatorFenced, undefined, routeKey(group.route), undefined, coordinatorFenced === true);
4955
5083
  })
4956
- : await runGuarded(pullArgv, undefined, lane, "realtime").then((result) => ({ result, accepted: true }), (error) => {
5084
+ : runGuarded(pullArgv, undefined, lane, "realtime").then((result) => ({ result, accepted: true }), (error) => {
4957
5085
  if (error instanceof RealtimeAdmissionTimeoutError) {
4958
5086
  return { accepted: false, admissionTimeout: error };
4959
5087
  }
4960
5088
  throw error;
4961
- });
5089
+ })).finally(() => {
5090
+ scopedPullsInFlight.delete(pullRouteId);
5091
+ });
4962
5092
  if ("admissionTimeout" in execution && execution.admissionTimeout) {
4963
5093
  // The pull never ran. Keep its paths and retry, without the
4964
5094
  // pass-failure handling the shared exit code would get.
4965
- restorePendingWatcherPaths(group.paths, watcherBatchGeneration, group.changes);
5095
+ keepForPullOnly(group);
4966
5096
  scheduleRoutePushRetry(`pull:${routeKey(group.route)}`, REALTIME_ADMISSION_RETRY_MS);
4967
5097
  deferred = true;
4968
5098
  continue;
4969
5099
  }
4970
5100
  const exit = scopedRouteDrainExit(execution);
5101
+ if (exit === 0) {
5102
+ routePullFailureBackoff.succeeded(pullRouteId);
5103
+ clearRoutePushRetry(pullRetryKey);
5104
+ for (const absolutePath of group.paths.keys())
5105
+ pullOnlyWatcherPaths.delete(absolutePath);
5106
+ }
5107
+ else if (exit !== runtime.authRequiredPassExit) {
5108
+ // Auth-required keeps its exit so the loop can park for sign-in.
5109
+ // Every other failure keeps the paths, schedules the retry and
5110
+ // reports the drain deferred, so no caller re-queues the paths or
5111
+ // wakes the loop early.
5112
+ const state = routePullFailureBackoff.failed(pullRouteId, monotonicNow());
5113
+ keepForPullOnly(group);
5114
+ scheduleRoutePushRetry(pullRetryKey, state.delayMs);
5115
+ if (state.shouldReport) {
5116
+ (deps.stderr ?? process.stderr).write(`hq-sync-runner: scoped pull for ${pullRouteId} failed (exit=${exit}, ` +
5117
+ `consecutive=${state.consecutiveFailures}); retrying in ${state.delayMs}ms\n`);
5118
+ }
5119
+ deferred = true;
5120
+ continue;
5121
+ }
4971
5122
  if (exit !== 0 && worstExit === 0)
4972
5123
  worstExit = exit;
4973
5124
  }
@@ -4979,6 +5130,16 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
4979
5130
  // when a peer change arrives between those reconciles.
4980
5131
  return 0;
4981
5132
  };
5133
+ const runScopedDrain = async (batch, watcherBatchGeneration, wakeOnPushFailure = false, allowFastRealtimeOvertake = false) => {
5134
+ const freshDuringThisDrain = new Set();
5135
+ activeDrainFreshPaths.add(freshDuringThisDrain);
5136
+ try {
5137
+ return await runScopedDrainOnce(freshDuringThisDrain, batch, watcherBatchGeneration, wakeOnPushFailure, allowFastRealtimeOvertake);
5138
+ }
5139
+ finally {
5140
+ activeDrainFreshPaths.delete(freshDuringThisDrain);
5141
+ }
5142
+ };
4982
5143
  /**
4983
5144
  * Overflow drain: the watcher hit its backlog cap, so per-path detail is
4984
5145
  * gone — but when every dropped path still carried route evidence
@@ -5219,6 +5380,15 @@ export async function runWatchLoop(argv, parsed, deps, runtime) {
5219
5380
  catch {
5220
5381
  /* diagnostics must never affect sync */
5221
5382
  }
5383
+ // A fresh event means the path needs a push again, even if it was
5384
+ // waiting only for its route's pull.
5385
+ if (batch) {
5386
+ for (const absolutePath of batch.paths.keys())
5387
+ noteFreshWatcherEvent(absolutePath);
5388
+ }
5389
+ else if (changedRelPath) {
5390
+ noteFreshWatcherEvent(localPathForVaultKey(hqRoot, changedRelPath));
5391
+ }
5222
5392
  // Peel a bounded realtime sub-batch from a mixed watcher batch while a
5223
5393
  // bulk pass is in flight. The residue enters the ordinary pending drain
5224
5394
  // immediately; a latest path appears in exactly one of these maps.