@indigoai-us/hq-cloud 6.15.66 → 6.15.68

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.
@@ -14,7 +14,7 @@ import { runRunner, runRunnerWithLoop, resolveDeletePolicy, resolveSkipPersonal,
14
14
  import { PresignObjectIO, S3SdkObjectIO } from "../object-io.js";
15
15
  import { setBandwidthGovernorForTesting } from "../bandwidth.js";
16
16
  import { FakeClock, TreeWatcher, } from "../watcher.js";
17
- import { adaptivePollMs, advanceMonotonicDeadline, DEFAULT_EVENT_BATCH_LIMIT, DEFAULT_FAST_LANE_MAX_BYTES, DEFAULT_FAST_LANE_MAX_FILE_BYTES, DEFAULT_FAST_LANE_MAX_FILES, DEFAULT_FULL_RECONCILE_MS, EVENT_BATCH_LIMIT_ENV, FAST_LANE_MAX_BYTES_ENV, FAST_LANE_MAX_FILES_ENV, FULL_RECONCILE_MS_ENV, isFastLocalBatch, isFastReceiverBatch, partitionLocalBatch, journalEntriesForWatcherDelete, resolveEventBatchLimit, resolveFullReconcileMs, shouldQuarantineWatcherDeleteIntents, WATCHER_REDISCOVERY_RETRY_MS, WATCHER_REDISCOVERY_RETRY_MAX_MS, } from "./sync-runner-watch-loop.js";
17
+ import { adaptivePollMs, advanceMonotonicDeadline, DEFAULT_EVENT_BATCH_LIMIT, DEFAULT_FAST_LANE_MAX_BYTES, DEFAULT_FAST_LANE_MAX_FILE_BYTES, DEFAULT_FAST_LANE_MAX_FILES, DEFAULT_FULL_RECONCILE_MS, EVENT_BATCH_LIMIT_ENV, FAST_LANE_MAX_BYTES_ENV, FAST_LANE_MAX_FILES_ENV, FULL_RECONCILE_MS_ENV, isFastLocalBatch, isFastReceiverBatch, partitionLocalBatch, journalEntriesForWatcherDelete, resolveEventBatchLimit, resolveFullReconcileMs, resolveWatchPollMs, shouldQuarantineWatcherDeleteIntents, WATCHER_REDISCOVERY_RETRY_MS, WATCHER_REDISCOVERY_RETRY_MAX_MS, } from "./sync-runner-watch-loop.js";
18
18
  import { PERSONAL_VAULT_JOURNAL_SLUG, readJournal, writeJournal, } from "../journal.js";
19
19
  import { HQ_CLOUD_VERSION } from "../version.js";
20
20
  import { lockPathFor, OPERATION_LOCKED_EXIT } from "../operation-lock.js";
@@ -3695,6 +3695,17 @@ describe("adaptivePollMs (whole-box CPU backoff)", () => {
3695
3695
  expect(adaptivePollMs(4, 4, 30_000, 300_000)).toBe(300_000);
3696
3696
  });
3697
3697
  });
3698
+ describe("resolveWatchPollMs", () => {
3699
+ it("holds the floor after a receiver observed messages, then resumes normal backoff only after empty", () => {
3700
+ // Regression (v6.15.66): a bulk receive was waiting behind a never-ending
3701
+ // stream of fast passes while the CPU sampler stretched ticks to 10 min.
3702
+ expect(resolveWatchPollMs(undefined, true, 1_000, 1)).toBe(60_000);
3703
+ expect(resolveWatchPollMs(undefined, false, 1_000, 1)).toBe(600_000);
3704
+ });
3705
+ it("preserves an explicit poll cadence regardless of receiver activity", () => {
3706
+ expect(resolveWatchPollMs(45_000, true, 1_000, 1)).toBe(45_000);
3707
+ });
3708
+ });
3698
3709
  describe("advanceMonotonicDeadline", () => {
3699
3710
  it("gives an overrun pass a full cooldown instead of running an immediate catch-up", () => {
3700
3711
  // A pass due at t=0 took through t=1,800 with a 600 ms cadence. The next
@@ -3741,6 +3752,90 @@ describe("runRunnerWithLoop — adaptive poll interval (no --poll-remote-ms)", (
3741
3752
  },
3742
3753
  };
3743
3754
  }
3755
+ function makeInterruptibleCapturingSleep() {
3756
+ const delays = [];
3757
+ const pending = [];
3758
+ return {
3759
+ delays,
3760
+ sleep: (delay) => new Promise((resolve) => {
3761
+ delays.push(delay);
3762
+ pending.push(resolve);
3763
+ }),
3764
+ resolveLatest() {
3765
+ const resolve = pending.at(-1);
3766
+ if (!resolve)
3767
+ throw new Error("poll sleep was not pending");
3768
+ resolve();
3769
+ },
3770
+ };
3771
+ }
3772
+ it("resets a long adaptive deadline when a received batch tightens the cadence", async () => {
3773
+ const sleep = makeInterruptibleCapturingSleep();
3774
+ const watcher = makeWatcherStub();
3775
+ const clockState = { now: 0 };
3776
+ let syncBatch;
3777
+ let shutdown = () => { };
3778
+ const loop = runRunnerWithLoop(["--companies", "--watch", "--event-push", "--hq-root", "/tmp/hq"], {
3779
+ runPass: vi.fn().mockResolvedValue(0),
3780
+ createWatcher: () => watcher,
3781
+ createReceiver: ({ syncBatchFn }) => {
3782
+ syncBatch = syncBatchFn;
3783
+ return { connected: true, start: async () => { }, dispose: async () => { } };
3784
+ },
3785
+ sleep: sleep.sleep,
3786
+ sampleLoadAvg: () => 1_000,
3787
+ monotonicNow: () => clockState.now,
3788
+ onShutdownSignal: (handler) => {
3789
+ shutdown = handler;
3790
+ return () => { };
3791
+ },
3792
+ });
3793
+ await flushLoopMicrotasks();
3794
+ expect(sleep.delays).toEqual([600_000]);
3795
+ clockState.now = 1_000;
3796
+ await syncBatch({ events: [], signal: new AbortController().signal });
3797
+ await flushLoopMicrotasks();
3798
+ // The receive interrupts a 10-minute wait, so its next poll is one minute
3799
+ // from the current monotonic time, not one minute after the old deadline.
3800
+ expect(sleep.delays).toEqual([600_000, 60_000]);
3801
+ shutdown();
3802
+ await loop;
3803
+ });
3804
+ it("lets an injected receiver clear queued-work cadence state after an empty receive", async () => {
3805
+ const sleep = makeInterruptibleCapturingSleep();
3806
+ const watcher = makeWatcherStub();
3807
+ const clockState = { now: 0 };
3808
+ let reportReceiveActivity;
3809
+ let shutdown = () => { };
3810
+ const loop = runRunnerWithLoop(["--companies", "--watch", "--event-push", "--hq-root", "/tmp/hq"], {
3811
+ runPass: vi.fn().mockResolvedValue(0),
3812
+ createWatcher: () => watcher,
3813
+ createReceiver: ({ onReceiveActivity }) => {
3814
+ reportReceiveActivity = onReceiveActivity;
3815
+ return { connected: true, start: async () => { }, dispose: async () => { } };
3816
+ },
3817
+ sleep: sleep.sleep,
3818
+ sampleLoadAvg: () => 1_000,
3819
+ monotonicNow: () => clockState.now,
3820
+ onShutdownSignal: (handler) => {
3821
+ shutdown = handler;
3822
+ return () => { };
3823
+ },
3824
+ });
3825
+ await flushLoopMicrotasks();
3826
+ expect(reportReceiveActivity).toBeTypeOf("function");
3827
+ clockState.now = 1_000;
3828
+ reportReceiveActivity(true);
3829
+ await flushLoopMicrotasks();
3830
+ expect(sleep.delays).toEqual([600_000, 60_000]);
3831
+ reportReceiveActivity(false);
3832
+ clockState.now = 61_000;
3833
+ sleep.resolveLatest();
3834
+ await flushLoopMicrotasks();
3835
+ expect(sleep.delays).toEqual([600_000, 60_000, 600_000]);
3836
+ shutdown();
3837
+ await loop;
3838
+ });
3744
3839
  it("waits only the bounded remainder of the idle cadence slot", async () => {
3745
3840
  const { calls, sleep } = makeCapturingSleep();
3746
3841
  const monotonicNow = vi
@@ -5103,6 +5198,65 @@ describe("runRunnerWithLoop — event-push wiring", () => {
5103
5198
  await loop;
5104
5199
  fs.rmSync(hqRoot, { recursive: true, force: true });
5105
5200
  });
5201
+ it("REGRESSION: a queued bulk receiver drain gets a turn before later fast arrivals", async () => {
5202
+ const hqRoot = fs.mkdtempSync(path.join(os.tmpdir(), "hq-bulk-drain-fairness-"));
5203
+ const watcher = makeWatcherStub();
5204
+ let receiverSync;
5205
+ let triggerShutdown = () => { };
5206
+ let releaseFirstFast = () => { };
5207
+ const firstFast = new Promise((resolve) => { releaseFirstFast = resolve; });
5208
+ const order = [];
5209
+ const journal = readJournal("acme");
5210
+ journal.files["realtime.md"] = {
5211
+ hash: "sha256:x", size: 16, syncedAt: "2026-08-26T00:00:00.000Z", direction: "down",
5212
+ };
5213
+ writeJournal("acme", journal);
5214
+ const runPass = vi.fn().mockImplementation(async (argv) => {
5215
+ const scope = argv[argv.indexOf("--scope-path") + 1];
5216
+ if (scope === "realtime.md") {
5217
+ order.push(`fast-${order.filter((item) => item.startsWith("fast-")).length + 1}`);
5218
+ if (order.length === 1)
5219
+ await firstFast;
5220
+ }
5221
+ else {
5222
+ order.push("bulk");
5223
+ }
5224
+ return completedPassOutcome();
5225
+ });
5226
+ const loop = runRunnerWithLoop(["--companies", "--watch", "--event-push", "--hq-root", hqRoot], {
5227
+ runPass,
5228
+ createWatcher: () => watcher,
5229
+ createReceiver: ({ syncBatchFn }) => {
5230
+ receiverSync = syncBatchFn;
5231
+ return { connected: true, start: async () => { }, dispose: async () => { } };
5232
+ },
5233
+ sleep: () => new Promise(() => { }),
5234
+ onShutdownSignal: (handler) => {
5235
+ triggerShutdown = handler;
5236
+ return () => { };
5237
+ },
5238
+ });
5239
+ await flushLoopMicrotasks();
5240
+ order.splice(0);
5241
+ const event = (relativePath, sequenceNumber) => ({
5242
+ kind: "upsert", relativePath, contentHash: "sha256:x", mtime: "2026-08-26T00:00:00.000Z",
5243
+ originDeviceId: "peer", originTenantId: "tenant-acme", sequenceNumber,
5244
+ eventTimestamp: "2026-08-26T00:00:00.000Z",
5245
+ });
5246
+ const fastOne = receiverSync({ events: [event("companies/acme/realtime.md", 1)], signal: new AbortController().signal });
5247
+ await flushLoopMicrotasks();
5248
+ const bulk = receiverSync({ events: [event("companies/acme/bulk.log", 2)], signal: new AbortController().signal });
5249
+ const fastTwo = receiverSync({ events: [event("companies/acme/realtime.md", 3)], signal: new AbortController().signal });
5250
+ releaseFirstFast();
5251
+ await flushLoopMicrotasks(30);
5252
+ // origin/main failure: ["fast-1", "fast-2"] — the queued bulk path is
5253
+ // perpetually leapfrogged and the SQS receiver cannot issue its next poll.
5254
+ expect(order.slice(0, 2)).toEqual(["fast-1", "bulk"]);
5255
+ await Promise.all([fastOne, bulk, fastTwo]);
5256
+ triggerShutdown();
5257
+ await loop;
5258
+ fs.rmSync(hqRoot, { recursive: true, force: true });
5259
+ });
5106
5260
  it("fast lane: keeps disjoint journal rows under a 16-path interleave with a bulk pass", async () => {
5107
5261
  const hqRoot = fs.mkdtempSync(path.join(os.tmpdir(), "hq-fast-lane-journal-"));
5108
5262
  const paths = Array.from({ length: 16 }, (_, index) => {