@indigoai-us/hq-cloud 6.15.65 → 6.15.67

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_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, 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
@@ -4697,6 +4792,62 @@ describe("runRunnerWithLoop — event-push wiring", () => {
4697
4792
  await loop;
4698
4793
  fs.rmSync(hqRoot, { recursive: true, force: true });
4699
4794
  });
4795
+ it("fast lane: peels a tiny watcher path from mixed multi-megabyte residue", async () => {
4796
+ const hqRoot = fs.mkdtempSync(path.join(os.tmpdir(), "hq-fast-lane-mixed-watch-"));
4797
+ const tinyRel = "companies/indigo/realtime.md";
4798
+ const tiny = path.join(hqRoot, tinyRel);
4799
+ const bulk = [0, 1, 2].map((index) => path.join(hqRoot, `companies/indigo/session-${index}.log`));
4800
+ fs.mkdirSync(path.dirname(tiny), { recursive: true });
4801
+ fs.writeFileSync(tiny, "tiny realtime edit");
4802
+ for (const file of bulk)
4803
+ fs.writeFileSync(file, Buffer.alloc(8 * 1024 * 1024));
4804
+ const watcher = makeBatchWatcherStub();
4805
+ let triggerShutdown = () => { };
4806
+ let releaseBulk = () => { };
4807
+ const bulkGate = new Promise((resolve) => { releaseBulk = resolve; });
4808
+ const calls = [];
4809
+ let fullPasses = 0;
4810
+ const runPass = vi.fn().mockImplementation(async (passArgv) => {
4811
+ calls.push(passArgv);
4812
+ if (!passArgv.includes("--scope-path")) {
4813
+ fullPasses += 1;
4814
+ if (fullPasses === 2)
4815
+ await bulkGate;
4816
+ }
4817
+ return completedPassOutcome();
4818
+ });
4819
+ const loop = runRunnerWithLoop(["--companies", "--watch", "--event-push", "--direction", "push", "--hq-root", hqRoot], {
4820
+ runPass,
4821
+ clock: new FakeClock(),
4822
+ createWatcher: () => watcher,
4823
+ sleep: () => new Promise(() => { }),
4824
+ onShutdownSignal: (handler) => {
4825
+ triggerShutdown = handler;
4826
+ return () => { };
4827
+ },
4828
+ });
4829
+ await flushLoopMicrotasks();
4830
+ watcher.emit();
4831
+ await flushLoopMicrotasks();
4832
+ watcher.emit(tinyRel, {
4833
+ paths: new Map([
4834
+ [tiny, tinyRel],
4835
+ ...bulk.map((file, index) => [file, `companies/indigo/session-${index}.log`]),
4836
+ ]),
4837
+ });
4838
+ await flushLoopMicrotasks(30);
4839
+ // origin/main failure: expected a scoped tiny path while the bulk pass is
4840
+ // held, but #408 kept all four paths in the slow lane.
4841
+ expect(calls.filter((argv) => argv.includes("--scope-path"))).toHaveLength(1);
4842
+ expect(calls.flat().join(" ")).toContain("realtime.md");
4843
+ releaseBulk();
4844
+ await flushLoopMicrotasks(30);
4845
+ expect(calls.filter((argv) => argv.includes("--scope-path"))).toHaveLength(2);
4846
+ expect(calls.flat().join(" ")).toContain("session-0.log");
4847
+ triggerShutdown();
4848
+ await loop;
4849
+ fs.rmSync(hqRoot, { recursive: true, force: true });
4850
+ });
4700
4851
  it("fast lane: a small receiver pull dispatches during a slow bulk pass", async () => {
4701
4852
  const watcher = makeWatcherStub();
4702
4853
  let triggerShutdown = () => { };
@@ -4705,6 +4856,14 @@ describe("runRunnerWithLoop — event-push wiring", () => {
4705
4856
  const bulkGate = new Promise((resolve) => { releaseBulk = resolve; });
4706
4857
  const order = [];
4707
4858
  let fullPasses = 0;
4859
+ const journal = readJournal("acme");
4860
+ journal.files["realtime.md"] = {
4861
+ hash: "sha256:x",
4862
+ size: 16,
4863
+ syncedAt: "2026-08-26T00:00:00.000Z",
4864
+ direction: "down",
4865
+ };
4866
+ writeJournal("acme", journal);
4708
4867
  const runPass = vi.fn().mockImplementation(async (passArgv) => {
4709
4868
  if (passArgv.includes("--scope-path")) {
4710
4869
  order.push("fast-pull");
@@ -4954,12 +5113,28 @@ describe("runRunnerWithLoop — event-push wiring", () => {
4954
5113
  eventTimestamp: "2026-08-26T00:00:00.000Z",
4955
5114
  })),
4956
5115
  ];
5116
+ for (const [slug, prefix, count] of [["acme", "a", 17], ["beta", "b", 16]]) {
5117
+ const journal = readJournal(slug);
5118
+ for (let index = 0; index < count; index++) {
5119
+ journal.files[`${prefix}-${index}.md`] = {
5120
+ hash: "sha256:x",
5121
+ size: 16,
5122
+ syncedAt: "2026-08-26T00:00:00.000Z",
5123
+ direction: "down",
5124
+ };
5125
+ }
5126
+ writeJournal(slug, journal);
5127
+ }
4957
5128
  const receiverDrain = receiverSync({
4958
5129
  events,
4959
5130
  signal: new AbortController().signal,
4960
5131
  });
4961
5132
  await flushLoopMicrotasks(20);
4962
- expect(runPass).not.toHaveBeenCalled();
5133
+ // The 33-event mixed delivery peels its first 32 bounded paths into the
5134
+ // fast lane (17 acme + 15 beta); the final beta event remains slow.
5135
+ expect(runPass).toHaveBeenCalledTimes(2);
5136
+ expect(runPass.mock.calls.flat().join(" ")).toContain("a-16.md");
5137
+ expect(runPass.mock.calls.flat().join(" ")).toContain("b-14.md");
4963
5138
  releaseBulk();
4964
5139
  await receiverDrain;
4965
5140
  triggerShutdown();
@@ -5023,6 +5198,65 @@ describe("runRunnerWithLoop — event-push wiring", () => {
5023
5198
  await loop;
5024
5199
  fs.rmSync(hqRoot, { recursive: true, force: true });
5025
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
+ });
5026
5260
  it("fast lane: keeps disjoint journal rows under a 16-path interleave with a bulk pass", async () => {
5027
5261
  const hqRoot = fs.mkdtempSync(path.join(os.tmpdir(), "hq-fast-lane-journal-"));
5028
5262
  const paths = Array.from({ length: 16 }, (_, index) => {
@@ -5171,6 +5405,30 @@ describe("runRunnerWithLoop — event-push wiring", () => {
5171
5405
  fs.rmSync(hqRoot, { recursive: true, force: true });
5172
5406
  }
5173
5407
  });
5408
+ it("fast lane: keeps a latest large revision in the slow residue and caps 33 small paths", () => {
5409
+ const hqRoot = fs.mkdtempSync(path.join(os.tmpdir(), "hq-fast-lane-partition-"));
5410
+ const rel = "companies/indigo/revised.md";
5411
+ const absolute = path.join(hqRoot, rel);
5412
+ fs.mkdirSync(path.dirname(absolute), { recursive: true });
5413
+ // The map is the coalescing/affinity boundary: its one value is the latest
5414
+ // revision, so this path cannot appear in both sub-batches.
5415
+ fs.writeFileSync(absolute, Buffer.alloc(DEFAULT_FAST_LANE_MAX_FILE_BYTES + 1));
5416
+ const revised = partitionLocalBatch({ paths: new Map([[absolute, rel]]) });
5417
+ expect(revised.fast).toBeNull();
5418
+ expect(revised.slow?.paths.get(absolute)).toBe(rel);
5419
+ const entries = [];
5420
+ for (let index = 0; index < DEFAULT_FAST_LANE_MAX_FILES + 1; index++) {
5421
+ const file = path.join(hqRoot, `companies/indigo/small-${index}.md`);
5422
+ fs.writeFileSync(file, "x");
5423
+ entries.push([file, `companies/indigo/small-${index}.md`]);
5424
+ }
5425
+ const capped = partitionLocalBatch({ paths: new Map(entries) });
5426
+ expect(capped.fast?.paths.size).toBe(DEFAULT_FAST_LANE_MAX_FILES);
5427
+ // The 33rd path deliberately remains in the slow residue, rather than
5428
+ // creating a second concurrent realtime dispatch.
5429
+ expect(capped.slow?.paths.size).toBe(1);
5430
+ fs.rmSync(hqRoot, { recursive: true, force: true });
5431
+ });
5174
5432
  it("U07: drains the first watcher event after activation without guarded overlap", async () => {
5175
5433
  const watcher = makeWatcherStub();
5176
5434
  let triggerShutdown = () => { };